]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Refactor follow/mute as modals in admin, add actions in abuse list
authorRigel Kent <sendmemail@rigelk.eu>
Tue, 14 Apr 2020 08:55:34 +0000 (10:55 +0200)
committerRigel Kent <par@rigelk.eu>
Tue, 14 Apr 2020 13:53:37 +0000 (15:53 +0200)
29 files changed:
client/src/app/+admin/admin.module.ts
client/src/app/+admin/config/shared/batch-domains-modal.component.html [new file with mode: 0644]
client/src/app/+admin/config/shared/batch-domains-modal.component.scss [new file with mode: 0644]
client/src/app/+admin/config/shared/batch-domains-modal.component.ts [new file with mode: 0644]
client/src/app/+admin/config/shared/batch-domains-validators.service.ts [new file with mode: 0644]
client/src/app/+admin/follows/followers-list/followers-list.component.ts
client/src/app/+admin/follows/following-add/following-add.component.html [deleted file]
client/src/app/+admin/follows/following-add/following-add.component.scss [deleted file]
client/src/app/+admin/follows/following-add/following-add.component.ts [deleted file]
client/src/app/+admin/follows/following-add/index.ts [deleted file]
client/src/app/+admin/follows/following-list/following-list.component.html
client/src/app/+admin/follows/following-list/following-list.component.scss
client/src/app/+admin/follows/following-list/following-list.component.ts
client/src/app/+admin/follows/follows.component.html
client/src/app/+admin/follows/follows.routes.ts
client/src/app/+admin/follows/index.ts
client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
client/src/app/shared/buttons/action-dropdown.component.html
client/src/app/shared/buttons/action-dropdown.component.scss
client/src/app/shared/buttons/action-dropdown.component.ts
client/src/app/shared/shared.module.ts
client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
server/middlewares/validators/blocklist.ts
server/tests/api/check-params/blocklist.ts

index fdbe70314e0ded871110195eedd92ce696977ace..16273f6d8d1c3aa536e6cc532528b5345072bbc5 100644 (file)
@@ -5,7 +5,7 @@ import { TableModule } from 'primeng/table'
 import { SharedModule } from '../shared'
 import { AdminRoutingModule } from './admin-routing.module'
 import { AdminComponent } from './admin.component'
-import { FollowersListComponent, FollowingAddComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
+import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
 import { FollowingListComponent } from './follows/following-list/following-list.component'
 import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
 import {
@@ -28,6 +28,7 @@ import { SelectButtonModule } from 'primeng/selectbutton'
 import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
 import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component'
 import { ChartModule } from 'primeng/chart'
+import { BatchDomainsModalComponent } from './config/shared/batch-domains-modal.component'
 
 @NgModule({
   imports: [
@@ -44,7 +45,6 @@ import { ChartModule } from 'primeng/chart'
     AdminComponent,
 
     FollowsComponent,
-    FollowingAddComponent,
     FollowersListComponent,
     FollowingListComponent,
     RedundancyCheckboxComponent,
@@ -76,7 +76,9 @@ import { ChartModule } from 'primeng/chart'
     DebugComponent,
 
     ConfigComponent,
-    EditCustomConfigComponent
+    EditCustomConfigComponent,
+
+    BatchDomainsModalComponent
   ],
 
   exports: [
diff --git a/client/src/app/+admin/config/shared/batch-domains-modal.component.html b/client/src/app/+admin/config/shared/batch-domains-modal.component.html
new file mode 100644 (file)
index 0000000..1b85c8f
--- /dev/null
@@ -0,0 +1,43 @@
+<ng-template #modal>
+  <div class="modal-header">
+    <h4 i18n class="modal-title">{{ action }}</h4>
+
+    <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
+  </div>
+
+  <div class="modal-body">
+    <form novalidate [formGroup]="form" (ngSubmit)="submit()">
+      <div class="form-group">
+        <label i18n for="hosts">1 host (without "http://") per line</label>
+
+        <textarea
+          [placeholder]="placeholder" formControlName="domains" type="text" id="hosts" name="hosts"
+          class="form-control" [ngClass]="{ 'input-error': formErrors['domains'] }" ngbAutofocus
+        ></textarea>
+
+        <div *ngIf="formErrors.domains" class="form-error">
+          {{ formErrors.domains }}
+
+          <div *ngIf="form.controls['domains'].errors.validDomains">
+            {{ form.controls['domains'].errors.validDomains.value }}
+          </div>
+        </div>
+      </div>
+
+      <ng-content select="warning"></ng-content>
+
+      <div class="form-group inputs">
+        <input
+          type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
+          (click)="hide()" (key.enter)="hide()"
+        >
+
+        <input
+          type="submit" [value]="action" class="action-button-submit"
+          [disabled]="!form.valid"
+        >
+      </div>
+    </form>
+  </div>
+
+</ng-template>
diff --git a/client/src/app/+admin/config/shared/batch-domains-modal.component.scss b/client/src/app/+admin/config/shared/batch-domains-modal.component.scss
new file mode 100644 (file)
index 0000000..9621a56
--- /dev/null
@@ -0,0 +1,3 @@
+textarea {
+  height: 200px;
+}
diff --git a/client/src/app/+admin/config/shared/batch-domains-modal.component.ts b/client/src/app/+admin/config/shared/batch-domains-modal.component.ts
new file mode 100644 (file)
index 0000000..620f272
--- /dev/null
@@ -0,0 +1,54 @@
+import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from '@angular/core'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
+import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
+import { FormReactive } from '@app/shared/forms'
+import { BatchDomainsValidatorsService } from './batch-domains-validators.service'
+
+@Component({
+  selector: 'my-batch-domains-modal',
+  templateUrl: './batch-domains-modal.component.html',
+  styleUrls: [ './batch-domains-modal.component.scss' ]
+})
+export class BatchDomainsModalComponent extends FormReactive implements OnInit {
+  @ViewChild('modal', { static: true }) modal: NgbModal
+  @Input() placeholder = 'example.com'
+  @Input() action: string
+  @Output() domains = new EventEmitter<string[]>()
+
+  private openedModal: NgbModalRef
+
+  constructor (
+    protected formValidatorService: FormValidatorService,
+    private modalService: NgbModal,
+    private batchDomainsValidatorsService: BatchDomainsValidatorsService,
+    private i18n: I18n
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    if (!this.action) this.action = this.i18n('Process domains')
+
+    this.buildForm({
+      domains: this.batchDomainsValidatorsService.DOMAINS
+    })
+  }
+
+  openModal () {
+    this.openedModal = this.modalService.open(this.modal, { centered: true })
+  }
+
+  hide () {
+    this.openedModal.close()
+  }
+
+  submit () {
+    this.domains.emit(
+      this.batchDomainsValidatorsService.getNotEmptyHosts(this.form.controls['domains'].value)
+    )
+    this.form.reset()
+    this.hide()
+  }
+}
diff --git a/client/src/app/+admin/config/shared/batch-domains-validators.service.ts b/client/src/app/+admin/config/shared/batch-domains-validators.service.ts
new file mode 100644 (file)
index 0000000..154ef3a
--- /dev/null
@@ -0,0 +1,68 @@
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { Validators, ValidatorFn } from '@angular/forms'
+import { Injectable } from '@angular/core'
+import { BuildFormValidator, validateHost } from '@app/shared'
+
+@Injectable()
+export class BatchDomainsValidatorsService {
+  readonly DOMAINS: BuildFormValidator
+
+  constructor (private i18n: I18n) {
+    this.DOMAINS = {
+      VALIDATORS: [ Validators.required, this.validDomains, this.isHostsUnique ],
+      MESSAGES: {
+        'required': this.i18n('Domain is required.'),
+        'validDomains': this.i18n('Domains entered are invalid.'),
+        'uniqueDomains': this.i18n('Domains entered contain duplicates.')
+      }
+    }
+  }
+
+  getNotEmptyHosts (hosts: string) {
+    return hosts
+      .split('\n')
+      .filter((host: string) => host && host.length !== 0) // Eject empty hosts
+  }
+
+  private validDomains: ValidatorFn = (control) => {
+    if (!control.value) return null
+
+    const newHostsErrors = []
+    const hosts = this.getNotEmptyHosts(control.value)
+
+    for (const host of hosts) {
+      if (validateHost(host) === false) {
+        newHostsErrors.push(this.i18n('{{host}} is not valid', { host }))
+      }
+    }
+
+    /* Is not valid. */
+    if (newHostsErrors.length !== 0) {
+      return {
+        'validDomains': {
+          reason: 'invalid',
+          value: newHostsErrors.join('. ') + '.'
+        }
+      }
+    }
+
+    /* Is valid. */
+    return null
+  }
+
+  private isHostsUnique: ValidatorFn = (control) => {
+    if (!control.value) return null
+
+    const hosts = this.getNotEmptyHosts(control.value)
+
+    if (hosts.every((host: string) => hosts.indexOf(host) === hosts.lastIndexOf(host))) {
+      return null
+    } else {
+      return {
+        'uniqueDomains': {
+          reason: 'invalid'
+        }
+      }
+    }
+  }
+}
index aff59a691ce552759e62a107c1849df9772b315b..5859028276166a65ff6f25f8c6eb193e9c70246f 100644 (file)
@@ -15,7 +15,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
   followers: ActorFollow[] = []
   totalRecords = 0
   rowsPerPage = 10
-  sort: SortMeta = { field: 'createdAt', order: 1 }
+  sort: SortMeta = { field: 'createdAt', order: -1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
 
   constructor (
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.html b/client/src/app/+admin/follows/following-add/following-add.component.html
deleted file mode 100644 (file)
index e08decb..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
-
-<form (ngSubmit)="addFollowing()">
-  <div class="form-group">
-    <label i18n for="hosts">1 host (without "http://") per line</label>
-
-    <textarea
-      type="text" class="form-control" placeholder="example.com" id="hosts" name="hosts"
-      [(ngModel)]="hostsString" (ngModelChange)="onHostsChanged()" [ngClass]="{ 'input-error': hostsError }"
-    ></textarea>
-
-    <div *ngIf="hostsError" class="form-error">
-      {{ hostsError }}
-    </div>
-  </div>
-
-  <div i18n *ngIf="httpEnabled() === false"  class="alert alert-warning">
-    It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
-  </div>
-
-  <input type="submit" i18n-value value="Add following" [disabled]="hostsError || !hostsString" class="btn btn-secondary">
-</form>
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.scss b/client/src/app/+admin/follows/following-add/following-add.component.scss
deleted file mode 100644 (file)
index 7594b50..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-@import '_variables';
-@import '_mixins';
-
-textarea {
-  height: 250px;
-}
-
-input[type=submit] {
-  @include peertube-button;
-  @include orange-button;
-}
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.ts b/client/src/app/+admin/follows/following-add/following-add.component.ts
deleted file mode 100644 (file)
index 308bbb0..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-import { Component } from '@angular/core'
-import { Router } from '@angular/router'
-import { Notifier } from '@app/core'
-import { ConfirmService } from '../../../core'
-import { validateHost } from '../../../shared'
-import { FollowService } from '@app/shared/instance/follow.service'
-import { I18n } from '@ngx-translate/i18n-polyfill'
-
-@Component({
-  selector: 'my-following-add',
-  templateUrl: './following-add.component.html',
-  styleUrls: [ './following-add.component.scss' ]
-})
-export class FollowingAddComponent {
-  hostsString = ''
-  hostsError: string = null
-  error: string = null
-
-  constructor (
-    private router: Router,
-    private notifier: Notifier,
-    private confirmService: ConfirmService,
-    private followService: FollowService,
-    private i18n: I18n
-  ) {}
-
-  httpEnabled () {
-    return window.location.protocol === 'https:'
-  }
-
-  onHostsChanged () {
-    this.hostsError = null
-
-    const newHostsErrors = []
-    const hosts = this.getNotEmptyHosts()
-
-    for (const host of hosts) {
-      if (validateHost(host) === false) {
-        newHostsErrors.push(this.i18n('{{host}} is not valid', { host }))
-      }
-    }
-
-    if (newHostsErrors.length !== 0) {
-      this.hostsError = newHostsErrors.join('. ')
-    }
-  }
-
-  async addFollowing () {
-    this.error = ''
-
-    const hosts = this.getNotEmptyHosts()
-    if (hosts.length === 0) {
-      this.error = this.i18n('You need to specify hosts to follow.')
-    }
-
-    if (!this.isHostsUnique(hosts)) {
-      this.error = this.i18n('Hosts need to be unique.')
-      return
-    }
-
-    const confirmMessage = this.i18n('If you confirm, you will send a follow request to:<br /> - ') + hosts.join('<br /> - ')
-    const res = await this.confirmService.confirm(confirmMessage, this.i18n('Follow new server(s)'))
-    if (res === false) return
-
-    this.followService.follow(hosts).subscribe(
-      () => {
-        this.notifier.success(this.i18n('Follow request(s) sent!'))
-
-        setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500)
-      },
-
-      err => this.notifier.error(err.message)
-    )
-  }
-
-  private isHostsUnique (hosts: string[]) {
-    return hosts.every(host => hosts.indexOf(host) === hosts.lastIndexOf(host))
-  }
-
-  private getNotEmptyHosts () {
-    return this.hostsString
-      .split('\n')
-      .filter(host => host && host.length !== 0) // Eject empty hosts
-  }
-}
diff --git a/client/src/app/+admin/follows/following-add/index.ts b/client/src/app/+admin/follows/following-add/index.ts
deleted file mode 100644 (file)
index 1b1897f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export * from './following-add.component'
index 01aba0c1126be0306ebb5b25dee30614c4565bd2..cb62d52ddba0470bc4104977f2e383fac79f98e5 100644 (file)
@@ -4,12 +4,16 @@
 >
   <ng-template pTemplate="caption">
     <div class="caption">
-      <div>
+      <div class="ml-auto">
         <input
           type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
           (keyup)="onSearch($event)"
         >
       </div>
+      <a class="ml-2 follow-button" (click)="addDomainsToFollow()" (key.enter)="addDomainsToFollow()">
+        <my-global-icon iconName="add"></my-global-icon>
+        <ng-container i18n>Follow domain</ng-container>
+      </a>
     </div>
   </ng-template>
 
@@ -42,3 +46,5 @@
     </tr>
   </ng-template>
 </p-table>
+
+<my-batch-domains-modal #batchDomainsModal i18n-action action="Follow domains" (domains)="addFollowing($event)"></my-batch-domains-modal>
index a6f0656b8b79d2f15cb6fe47be74c352f8488238..f4656b88daac8bb5811c6f9bfd270e0b4514127c 100644 (file)
@@ -7,4 +7,8 @@
   input {
     @include peertube-input-text(250px);
   }
-}
\ No newline at end of file
+}
+
+.follow-button {
+  @include create-button;
+}
index dd7629ead2b6036a1f36eb453226a4c0534aeb77..477a6c0d76c86405c05e266e9e1e069afe866b4d 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core'
+import { Component, OnInit, ViewChild } from '@angular/core'
 import { Notifier } from '@app/core'
 import { SortMeta } from 'primeng/api'
 import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
@@ -6,6 +6,7 @@ import { ConfirmService } from '../../../core/confirm/confirm.service'
 import { RestPagination, RestTable } from '../../../shared'
 import { FollowService } from '@app/shared/instance/follow.service'
 import { I18n } from '@ngx-translate/i18n-polyfill'
+import { BatchDomainsModalComponent } from '@app/+admin/config/shared/batch-domains-modal.component'
 
 @Component({
   selector: 'my-followers-list',
@@ -13,10 +14,12 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
   styleUrls: [ './following-list.component.scss' ]
 })
 export class FollowingListComponent extends RestTable implements OnInit {
+  @ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent
+
   following: ActorFollow[] = []
   totalRecords = 0
   rowsPerPage = 10
-  sort: SortMeta = { field: 'createdAt', order: 1 }
+  sort: SortMeta = { field: 'createdAt', order: -1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
 
   constructor (
@@ -36,6 +39,21 @@ export class FollowingListComponent extends RestTable implements OnInit {
     return 'FollowingListComponent'
   }
 
+  addDomainsToFollow () {
+    this.batchDomainsModal.openModal()
+  }
+
+  async addFollowing (hosts: string[]) {
+    this.followService.follow(hosts).subscribe(
+      () => {
+        this.notifier.success(this.i18n('Follow request(s) sent!'))
+        this.loadData()
+      },
+
+      err => this.notifier.error(err.message)
+    )
+  }
+
   async removeFollowing (follow: ActorFollow) {
     const res = await this.confirmService.confirm(
       this.i18n('Do you really want to unfollow {{host}}?', { host: follow.following.host }),
index 46581daf9832a1f57a729b8ac0873416b9c43f11..7b5bcc2db5ac657135146168f8285bab384560b6 100644 (file)
@@ -4,8 +4,6 @@
   <div class="admin-sub-nav">
     <a i18n routerLink="following-list" routerLinkActive="active">Following</a>
 
-    <a i18n routerLink="following-add" routerLinkActive="active">Follow</a>
-
     <a i18n routerLink="followers-list" routerLinkActive="active">Followers</a>
 
     <a i18n routerLink="video-redundancies-list" routerLinkActive="active">Video redundancies</a>
index 298733eb03559fad776ef150c6ac11da65c9b19a..8270ae444d3282b4356561733921c799822cd193 100644 (file)
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router'
 
 import { UserRightGuard } from '../../core'
 import { FollowsComponent } from './follows.component'
-import { FollowingAddComponent } from './following-add'
 import { FollowersListComponent } from './followers-list'
 import { UserRight } from '../../../../../shared'
 import { FollowingListComponent } from './following-list/following-list.component'
@@ -42,12 +41,7 @@ export const FollowsRoutes: Routes = [
       },
       {
         path: 'following-add',
-        component: FollowingAddComponent,
-        data: {
-          meta: {
-            title: 'Add follow'
-          }
-        }
+        redirectTo: 'following-list'
       },
       {
         path: 'video-redundancies-list',
index 4fcb35cb1d89ff08fd9fcf8add611a5b05c934a7..285955468a36e9d5f6cb7c4cd358ea11dd4e2ff2 100644 (file)
@@ -1,4 +1,3 @@
-export * from './following-add'
 export * from './followers-list'
 export * from './following-list'
 export * from './video-redundancies-list'
index 44c5c2fb81d5124636df36cee270e353533ea837..0e072d84b42027d8588f996b2bec42bd49a8f9bb 100644 (file)
@@ -4,6 +4,14 @@
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {first} to {last} of {totalRecords} muted instances"
 >
+  <ng-template pTemplate="caption">
+    <div class="caption">
+      <a class="ml-auto block-button" (click)="addServersToBlock()" (key.enter)="addServersToBlock()">
+        <my-global-icon iconName="add"></my-global-icon>
+        <ng-container i18n>Mute domain</ng-container>
+      </a>
+    </div>
+  </ng-template>
 
   <ng-template pTemplate="header">
     <tr>
     </tr>
   </ng-template>
 </p-table>
+
+<my-batch-domains-modal #batchDomainsModal i18n-action action="Mute domains" (domains)="onDomainsToBlock($event)">
+  <ng-container ngProjectAs="warning">
+    <div i18n *ngIf="httpEnabled() === false"  class="alert alert-warning">
+      It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
+    </div>
+  </ng-container>
+</my-batch-domains-modal>
index 6028b75eaa641b53cb7511293211ab9e1f7782bc..9d3bedd80621138ba58adf09d91cc5971527e158 100644 (file)
@@ -4,4 +4,8 @@
 .unblock-button {
   @include peertube-button;
   @include grey-button;
-}
\ No newline at end of file
+}
+
+.block-button {
+  @include create-button;
+}
index 5af6d8f76be8c8feb13b628724134ec1b95bab40..431729ef2136655187c5ecb0eca144889fe1bc42 100644 (file)
@@ -1,10 +1,11 @@
-import { Component, OnInit } from '@angular/core'
+import { Component, OnInit, ViewChild } from '@angular/core'
 import { Notifier } from '@app/core'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { RestPagination, RestTable } from '@app/shared'
 import { SortMeta } from 'primeng/api'
 import { BlocklistService } from '@app/shared/blocklist'
 import { ServerBlock } from '../../../../../../shared'
+import { BatchDomainsModalComponent } from '@app/+admin/config/shared/batch-domains-modal.component'
 
 @Component({
   selector: 'my-instance-server-blocklist',
@@ -12,6 +13,8 @@ import { ServerBlock } from '../../../../../../shared'
   templateUrl: './instance-server-blocklist.component.html'
 })
 export class InstanceServerBlocklistComponent extends RestTable implements OnInit {
+  @ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent
+
   blockedServers: ServerBlock[] = []
   totalRecords = 0
   rowsPerPage = 10
@@ -47,6 +50,27 @@ export class InstanceServerBlocklistComponent extends RestTable implements OnIni
       )
   }
 
+  httpEnabled () {
+    return window.location.protocol === 'https:'
+  }
+
+  addServersToBlock () {
+    this.batchDomainsModal.openModal()
+  }
+
+  onDomainsToBlock (domains: string[]) {
+    domains.forEach(domain => {
+      this.blocklistService.blockServerByInstance(domain)
+        .subscribe(
+          () => {
+            this.notifier.success(this.i18n('Instance {{domain}} muted by your instance.', { domain }))
+
+            this.loadData()
+          }
+        )
+    })
+  }
+
   protected loadData () {
     return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort)
       .subscribe(
index 155d10ddab4ef64fe41286db4d5576333f1def31..3899ee07f1c703752c06b888a8372cbe0d117ee0 100644 (file)
         </a>
       </td>
 
-      <td>
+      <td class="c-hand" [pRowToggler]="videoAbuse">
         <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
         <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span>
       </td>
 
       <td class="action-cell">
-        <my-action-dropdown placement="bottom-right" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
+        <my-action-dropdown placement="bottom-right auto" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
       </td>
     </tr>
   </ng-template>
index b135792a7108694a7ded539f8180f30571aff231..5e48cf24fd8e220469e69b6deb641b509fd8bb45 100644 (file)
@@ -1,9 +1,9 @@
 import { Component, OnInit, ViewChild } from '@angular/core'
-import { Account } from '../../../shared/account/account.model'
+import { Account } from '@app/shared/account/account.model'
 import { Notifier } from '@app/core'
 import { SortMeta } from 'primeng/api'
 import { VideoAbuse, VideoAbuseState } from '../../../../../../shared'
-import { RestPagination, RestTable, VideoAbuseService } from '../../../shared'
+import { RestPagination, RestTable, VideoAbuseService, VideoBlacklistService } from '../../../shared'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
 import { ConfirmService } from '../../../core/index'
@@ -14,6 +14,7 @@ import { Actor } from '@app/shared/actor/actor.model'
 import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils'
 import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
 import { DomSanitizer } from '@angular/platform-browser'
+import { BlocklistService } from '@app/shared/blocklist'
 
 @Component({
   selector: 'my-video-abuse-list',
@@ -29,11 +30,13 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
   sort: SortMeta = { field: 'createdAt', order: 1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
 
-  videoAbuseActions: DropdownAction<VideoAbuse>[] = []
+  videoAbuseActions: DropdownAction<VideoAbuse>[][] = []
 
   constructor (
     private notifier: Notifier,
     private videoAbuseService: VideoAbuseService,
+    private blocklistService: BlocklistService,
+    private videoBlacklistService: VideoBlacklistService,
     private confirmService: ConfirmService,
     private i18n: I18n,
     private markdownRenderer: MarkdownService,
@@ -42,30 +45,57 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
     super()
 
     this.videoAbuseActions = [
-      {
-        label: this.i18n('Delete this report'),
-        handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
-      },
-      {
-        label: this.i18n('Add note'),
-        handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
-        isDisplayed: videoAbuse => !videoAbuse.moderationComment
-      },
-      {
-        label: this.i18n('Update note'),
-        handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
-        isDisplayed: videoAbuse => !!videoAbuse.moderationComment
-      },
-      {
-        label: this.i18n('Mark as accepted'),
-        handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED),
-        isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse)
-      },
-      {
-        label: this.i18n('Mark as rejected'),
-        handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED),
-        isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse)
-      }
+      [
+        {
+          label: this.i18n('Internal actions'),
+          isHeader: true
+        },
+        {
+          label: this.i18n('Delete report'),
+          handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
+        },
+        {
+          label: this.i18n('Add note'),
+          handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
+          isDisplayed: videoAbuse => !videoAbuse.moderationComment
+        },
+        {
+          label: this.i18n('Update note'),
+          handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
+          isDisplayed: videoAbuse => !!videoAbuse.moderationComment
+        },
+        {
+          label: this.i18n('Mark as accepted'),
+          handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED),
+          isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse)
+        },
+        {
+          label: this.i18n('Mark as rejected'),
+          handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED),
+          isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse)
+        }
+      ],
+      [
+        {
+          label: this.i18n('Actions for the video'),
+          isHeader: true
+        },
+        {
+          label: this.i18n('Blacklist video'),
+          handler: videoAbuse => {
+            this.videoBlacklistService.blacklistVideo(videoAbuse.video.id, undefined, true)
+              .subscribe(
+                () => {
+                  this.notifier.success(this.i18n('Video blacklisted.'))
+
+                  this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
+                },
+
+                err => this.notifier.error(err.message)
+              )
+          }
+        }
+      ]
     ]
   }
 
index 43b6863afe43be32ef381b1bd6aef086ae5e6029..4e9965bee8077b8faa71f811dd559f982335614c 100644 (file)
@@ -18,7 +18,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
   blacklist: (VideoBlacklist & { reasonHtml?: string })[] = []
   totalRecords = 0
   rowsPerPage = 10
-  sort: SortMeta = { field: 'createdAt', order: 1 }
+  sort: SortMeta = { field: 'createdAt', order: -1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
   listBlacklistTypeFilter: VideoBlacklistType = undefined
 
index cd993db9f7449ec4ce5ff608f13eeb5f3fd46ee2..14cfe9a22865071935bf7b131cebc7754d9e9c20 100644 (file)
             </div>
           </ng-template>
 
-          <a *ngIf="action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" class="dropdown-item" [routerLink]="action.linkBuilder(entry)" [title]="action.title || ''">
+          <a
+            *ngIf="action.linkBuilder && !action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }"
+            class="dropdown-item"  [routerLink]="action.linkBuilder(entry)" [title]="action.title || ''"
+          >
             <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
           </a>
 
           <span
-            *ngIf="!action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" (click)="action.handler(entry)"
-            class="custom-action dropdown-item" role="button" [title]="action.title || ''"
+            *ngIf="!action.linkBuilder && !action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }"
+            class="custom-action dropdown-item" role="button" [title]="action.title || ''" (click)="action.handler(entry)"
           >
             <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
           </span>
 
+          <h6
+            *ngIf="!action.linkBuilder && action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }"
+            class="dropdown-header" role="button" [title]="action.title || ''" (click)="action.handler(entry)"
+          >
+            <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
+          </h6>
+
         </ng-container>
       </ng-container>
 
index 442c90984da9f5e41b9c58a3ae405d190eaca1ab..7a030f32c33452470e041d3154fa7c59956115a5 100644 (file)
 }
 
 .dropdown-menu {
+  .dropdown-header {
+    padding: 0.2rem 1rem;
+  }
+
   .dropdown-item {
     display: flex;
     cursor: pointer;
index 6649b092accb278f93c04452342ebcb64fc49b61..8fcaa38b968661525207e06ac835fb6bee4791c0 100644 (file)
@@ -9,6 +9,7 @@ export type DropdownAction<T> = {
   handler?: (a: T) => any
   linkBuilder?: (a: T) => (string | number)[]
   isDisplayed?: (a: T) => boolean
+  isHeader?: boolean
 }
 
 export type DropdownButtonSize = 'normal' | 'small'
index a952880a6c1c41edd3224d34e154bf42b4c5f686..01735c1878e5ce1d674fa570d7e48b7c524762fb 100644 (file)
@@ -107,6 +107,7 @@ import { InputSwitchModule } from 'primeng/inputswitch'
 import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings'
 import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
 import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
+import { BatchDomainsValidatorsService } from '@app/+admin/config/shared/batch-domains-validators.service'
 
 @NgModule({
   imports: [
@@ -297,6 +298,7 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i
     LoginValidatorsService,
     ResetPasswordValidatorsService,
     UserValidatorsService,
+    BatchDomainsValidatorsService,
     VideoPlaylistValidatorsService,
     VideoAbuseValidatorsService,
     VideoChannelValidatorsService,
index 0f7c19765c7e1fceb89cc8df8dca1764cc7fdb25..e1a8f6260d75ef0a2cc5c122fc9a90bf601822bf 100644 (file)
@@ -11,7 +11,6 @@ import { VideoCommentService } from './video-comment.service'
 import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
 import { VideoCommentValidatorsService } from '@app/shared/forms/form-validators/video-comment-validators.service'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
-import { AuthService } from '@app/core/auth'
 
 @Component({
   selector: 'my-video-comment-add',
@@ -38,7 +37,6 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
     private videoCommentValidatorsService: VideoCommentValidatorsService,
     private notifier: Notifier,
     private videoCommentService: VideoCommentService,
-    private authService: AuthService,
     private modalService: NgbModal,
     private router: Router
   ) {
index 47a0b1a1c13135de8ca6db78e40df96e3a637d4d..b2183437c8bf60cf90aa14b7aa508f95dd11f382 100644 (file)
@@ -84,11 +84,9 @@ const blockServerValidator = [
         .end()
     }
 
-    const server = await ServerModel.loadByHost(host)
+    let server = await ServerModel.loadByHost(host)
     if (!server) {
-      return res.status(404)
-                .send({ error: 'Server host not found.' })
-                .end()
+      server = await ServerModel.create({ host })
     }
 
     res.locals.server = server
index fb459f756f9ae59fd178aef64e3af2e3420588f2..1219ec9bd3fca0964f092d44575a58027330cee3 100644 (file)
@@ -175,13 +175,13 @@ describe('Test blocklist API validators', function () {
           })
         })
 
-        it('Should fail with an unknown server', async function () {
+        it('Should succeed with an unknown server', async function () {
           await makePostBodyRequest({
             url: server.url,
             token: server.accessToken,
             path,
             fields: { host: 'localhost:9003' },
-            statusCodeExpected: 404
+            statusCodeExpected: 204
           })
         })
 
@@ -218,7 +218,7 @@ describe('Test blocklist API validators', function () {
         it('Should fail with an unknown server block', async function () {
           await makeDeleteRequest({
             url: server.url,
-            path: path + '/localhost:9003',
+            path: path + '/localhost:9004',
             token: server.accessToken,
             statusCodeExpected: 404
           })
@@ -415,13 +415,13 @@ describe('Test blocklist API validators', function () {
           })
         })
 
-        it('Should fail with an unknown server', async function () {
+        it('Should succeed with an unknown server', async function () {
           await makePostBodyRequest({
             url: server.url,
             token: server.accessToken,
             path,
             fields: { host: 'localhost:9003' },
-            statusCodeExpected: 404
+            statusCodeExpected: 204
           })
         })
 
@@ -467,7 +467,7 @@ describe('Test blocklist API validators', function () {
         it('Should fail with an unknown server block', async function () {
           await makeDeleteRequest({
             url: server.url,
-            path: path + '/localhost:9003',
+            path: path + '/localhost:9004',
             token: server.accessToken,
             statusCodeExpected: 404
           })