factorize account/server blocklists for users and instance (#2875)
authorRigel Kent <sendmemail@rigelk.eu>
Mon, 15 Jun 2020 11:18:22 +0000 (13:18 +0200)
committerGitHub <noreply@github.com>
Mon, 15 Jun 2020 11:18:22 +0000 (13:18 +0200)
32 files changed:
client/src/app/+admin/admin.module.ts
client/src/app/+admin/follows/followers-list/followers-list.component.html
client/src/app/+admin/follows/following-list/following-list.component.html
client/src/app/+admin/follows/follows.component.scss
client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts
client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html [deleted file]
client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss [deleted file]
client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
client/src/app/+admin/moderation/moderation.component.scss
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html [deleted file]
client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss [deleted file]
client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts
client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html [deleted file]
client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss [deleted file]
client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts
client/src/app/+my-account/my-account-routing.module.ts
client/src/app/+my-account/my-account.component.ts
client/src/app/shared/blocklist/account-blocklist.component.html [new file with mode: 0644]
client/src/app/shared/blocklist/account-blocklist.component.scss [new file with mode: 0644]
client/src/app/shared/blocklist/account-blocklist.component.ts [new file with mode: 0644]
client/src/app/shared/blocklist/blocklist.service.ts
client/src/app/shared/blocklist/index.ts
client/src/app/shared/blocklist/server-blocklist.component.html [new file with mode: 0644]
client/src/app/shared/blocklist/server-blocklist.component.scss [new file with mode: 0644]
client/src/app/shared/blocklist/server-blocklist.component.ts [new file with mode: 0644]
client/src/app/shared/shared.module.ts
client/src/sass/application.scss
client/src/sass/include/_mixins.scss

index eb073f70930fd28ff249df20bf834a0aad983e63..eccec8a49cb15806b015b59a646b8fd5d94743ae 100644 (file)
@@ -27,7 +27,6 @@ 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'
 import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-abuse-details.component'
 
 @NgModule({
@@ -76,9 +75,7 @@ import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-
     DebugComponent,
 
     ConfigComponent,
-    EditCustomConfigComponent,
-
-    BatchDomainsModalComponent
+    EditCustomConfigComponent
   ],
 
   exports: [
index 298871fce3f99ae22a4248399c8dc5a9d303361c..48b5681f4126180b70df9d1f3acdf2a2cb768bab 100644 (file)
@@ -56,7 +56,7 @@
   <ng-template pTemplate="emptymessage">
     <tr>
       <td colspan="6">
-        <div class="empty-table-message">
+        <div class="no-results">
           <ng-container *ngIf="search" i18n>No follower found matching current filters.</ng-container>
           <ng-container *ngIf="!search" i18n>Your instance doesn't have any follower.</ng-container>
         </div>
index ed521d6503fae0085a0e28d1e3ce7d24dc4c7c0a..a8fbf65d4a325e67c8a9cb3f3958785740ebf0da 100644 (file)
@@ -58,7 +58,7 @@
   <ng-template pTemplate="emptymessage">
     <tr>
       <td colspan="6">
-        <div class="empty-table-message">
+        <div class="no-results">
           <ng-container *ngIf="search" i18n>No host found matching current filters.</ng-container>
           <ng-container *ngIf="!search" i18n>Your instance is not following anyone.</ng-container>
         </div>
index 32394f698c324abe0a10bd2efa90f7b0444de89e..0cffcb5558565071909b4f36ffa84249e8892c3d 100644 (file)
@@ -4,7 +4,3 @@
   flex-grow: 0;
   margin-right: 30px;
 }
-
-.empty-table-message {
-  @include empty-state;
-}
index c08154bcd1c86751fb64415b8971543532b1ce0b..44586cb6709f7bba285de87e1a0b3eb076304ba4 100644 (file)
@@ -73,7 +73,7 @@
   <ng-template pTemplate="emptymessage">
     <tr>
       <td colspan="6">
-        <div class="empty-table-message">
+        <div class="no-results">
           <ng-container *ngIf="isDisplayingRemoteVideos()" i18n>Your instance doesn't mirror any video.</ng-container>
           <ng-container *ngIf="!isDisplayingRemoteVideos()" i18n>Your instance has no mirrored videos.</ng-container>
         </div>
index b7d40be6063ea7ead5b2c187ead1a09428eca94d..486785f35ef54cf97220b6c2b0fee1d60ada4226 100644 (file)
@@ -54,7 +54,7 @@
   <ng-template pTemplate="emptymessage">
     <tr>
       <td colspan="6">
-        <div class="empty-table-message">
+        <div class="no-results">
           <ng-container *ngIf="search" i18n>No account found matching current filters.</ng-container>
           <ng-container *ngIf="!search" i18n>No account found.</ng-container>
         </div>
index 73a9ae75d4ad577a3d3f5cf06ce3cf07331f4c88..90a17619471c877c3d410197e2e5797ba3ec3567 100644 (file)
@@ -1,70 +1,15 @@
-import { Component, OnInit } 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 { AccountBlock, BlocklistService } from '@app/shared/blocklist'
-import { Actor } from '@app/shared/actor/actor.model'
+import { Component } from '@angular/core'
+import { GenericAccountBlocklistComponent, BlocklistComponentType } from '@app/shared/blocklist'
 
 @Component({
   selector: 'my-instance-account-blocklist',
-  styleUrls: [ '../moderation.component.scss', './instance-account-blocklist.component.scss' ],
-  templateUrl: './instance-account-blocklist.component.html'
+  styleUrls: [ '../moderation.component.scss', '../../../shared/blocklist/account-blocklist.component.scss' ],
+  templateUrl: '../../../shared/blocklist/account-blocklist.component.html'
 })
-export class InstanceAccountBlocklistComponent extends RestTable implements OnInit {
-  blockedAccounts: AccountBlock[] = []
-  totalRecords = 0
-  sort: SortMeta = { field: 'createdAt', order: -1 }
-  pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
-
-  constructor (
-    private notifier: Notifier,
-    private blocklistService: BlocklistService,
-    private i18n: I18n
-  ) {
-    super()
-  }
-
-  ngOnInit () {
-    this.initialize()
-  }
+export class InstanceAccountBlocklistComponent extends GenericAccountBlocklistComponent {
+  mode = BlocklistComponentType.Instance
 
   getIdentifier () {
     return 'InstanceAccountBlocklistComponent'
   }
-
-  switchToDefaultAvatar ($event: Event) {
-    ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
-  }
-
-  unblockAccount (accountBlock: AccountBlock) {
-    const blockedAccount = accountBlock.blockedAccount
-
-    this.blocklistService.unblockAccountByInstance(blockedAccount)
-        .subscribe(
-          () => {
-            this.notifier.success(
-              this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost })
-            )
-
-            this.loadData()
-          }
-        )
-  }
-
-  protected loadData () {
-    return this.blocklistService.getInstanceAccountBlocklist({
-      pagination: this.pagination,
-      sort: this.sort,
-      search: this.search
-    })
-      .subscribe(
-        resultList => {
-          this.blockedAccounts = resultList.data
-          this.totalRecords = resultList.total
-        },
-
-        err => this.notifier.error(err.message)
-      )
-  }
 }
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
deleted file mode 100644 (file)
index 7a77b88..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-<p-table
-  [value]="blockedServers" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
-  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" (onPage)="onPage($event)"
-  [showCurrentPageReport]="true" i18n-currentPageReportTemplate
-  currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} muted instances"
->
-  <ng-template pTemplate="caption">
-    <div class="caption">
-      <div class="ml-auto has-feedback has-clear">
-        <input
-          type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
-          (keyup)="onSearch($event)"
-        >
-        <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
-        <span class="sr-only" i18n>Clear filters</span>
-      </div>
-      <a class="ml-2 block-button" (click)="addServersToBlock()" (key.enter)="addServersToBlock()">
-        <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
-        <ng-container i18n>Mute domain</ng-container>
-      </a>
-    </div>
-  </ng-template>
-
-  <ng-template pTemplate="header">
-    <tr>
-      <th style="width: 100%;" i18n>Instance</th>
-      <th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
-      <th style="width: 150px;"></th> <!-- column for action buttons -->
-    </tr>
-  </ng-template>
-
-  <ng-template pTemplate="body" let-serverBlock>
-    <tr>
-      <td>
-        <a [href]="'https://' + serverBlock.blockedServer.host" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
-          {{ serverBlock.blockedServer.host }}
-          <span class="glyphicon glyphicon-new-window"></span>
-        </a>
-      </td>
-      <td>{{ serverBlock.createdAt | date: 'short' }}</td>
-      <td class="action-cell">
-        <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
-      </td>
-    </tr>
-  </ng-template>
-
-  <ng-template pTemplate="emptymessage">
-    <tr>
-      <td colspan="6">
-        <div class="empty-table-message">
-          <ng-container *ngIf="search" i18n>No server found matching current filters.</ng-container>
-          <ng-container *ngIf="!search" i18n>No server found.</ng-container>
-        </div>
-      </td>
-    </tr>
-  </ng-template>
-</p-table>
-
-<my-batch-domains-modal #batchDomainsModal i18n-action action="Mute domains" (domains)="onDomainsToBlock($event)"></my-batch-domains-modal>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
deleted file mode 100644 (file)
index c1f6611..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-@import '_variables';
-@import '_mixins';
-
-a {
-  @include disable-default-a-behaviour;
-  display: inline-block;
-
-  &, &:hover {
-    color: pvar(--mainForegroundColor);
-  }
-
-  span {
-    font-size: 80%;
-    color: pvar(--inputPlaceholderColor);
-  }
-}
-
-.unblock-button {
-  @include peertube-button;
-  @include grey-button;
-}
-
-.block-button {
-  @include create-button;
-}
index 559c9c0b0ca289d3590a515a20f8d2c1bdaf0056..9d4ec174a5c6e1d47dc28ebf67d677593a84d7f7 100644 (file)
@@ -1,84 +1,15 @@
-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'
+import { Component } from '@angular/core'
+import { GenericServerBlocklistComponent, BlocklistComponentType } from '@app/shared/blocklist'
 
 @Component({
   selector: 'my-instance-server-blocklist',
-  styleUrls: [ '../moderation.component.scss', './instance-server-blocklist.component.scss' ],
-  templateUrl: './instance-server-blocklist.component.html'
+  styleUrls: [ '../../../shared/blocklist/server-blocklist.component.scss' ],
+  templateUrl: '../../../shared/blocklist/server-blocklist.component.html'
 })
-export class InstanceServerBlocklistComponent extends RestTable implements OnInit {
-  @ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent
-
-  blockedServers: ServerBlock[] = []
-  totalRecords = 0
-  sort: SortMeta = { field: 'createdAt', order: -1 }
-  pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
-
-  constructor (
-    private notifier: Notifier,
-    private blocklistService: BlocklistService,
-    private i18n: I18n
-  ) {
-    super()
-  }
-
-  ngOnInit () {
-    this.initialize()
-  }
+export class InstanceServerBlocklistComponent extends GenericServerBlocklistComponent {
+  mode = BlocklistComponentType.Instance
 
   getIdentifier () {
     return 'InstanceServerBlocklistComponent'
   }
-
-  unblockServer (serverBlock: ServerBlock) {
-    const host = serverBlock.blockedServer.host
-
-    this.blocklistService.unblockServerByInstance(host)
-      .subscribe(
-        () => {
-          this.notifier.success(this.i18n('Instance {{host}} unmuted by your instance.', { host }))
-
-          this.loadData()
-        }
-      )
-  }
-
-  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({
-      pagination: this.pagination,
-      sort: this.sort,
-      search: this.search
-    })
-      .subscribe(
-        resultList => {
-          this.blockedServers = resultList.data
-          this.totalRecords = resultList.total
-        },
-
-        err => this.notifier.error(err.message)
-      )
-  }
 }
index 404eb050488f3e0df6c2dda6fd23aef423243b09..ba68cf6f6981f669eff2f10db56fda745fd1e8b7 100644 (file)
   }
 }
 
-.empty-table-message {
-  @include empty-state;
-}
-
 .moderation-expanded {
   font-size: 90%;
 
index a2ed515594155d1ea23f2714dc51c34c86bd7b60..64641b28a731b0081a11787204274ac357666722 100644 (file)
   <ng-template pTemplate="emptymessage">
     <tr>
       <td colspan="6">
-        <div class="empty-table-message">
+        <div class="no-results">
           <ng-container *ngIf="search" i18n>No video abuses found matching current filters.</ng-container>
           <ng-container *ngIf="!search" i18n>No video abuses found.</ng-container>
         </div>
index a01a2566db9c4baadc019adac134031e297ba512..ec20e46f134e0aadf09f26b18609b87bb0274ffe 100644 (file)
   <ng-template pTemplate="emptymessage">
     <tr>
       <td colspan="6">
-        <div class="empty-table-message">
+        <div class="no-results">
           <ng-container *ngIf="search" i18n>No blocked video found matching current filters.</ng-container>
           <ng-container *ngIf="!search" i18n>No blocked video found.</ng-container>
         </div>
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html
deleted file mode 100644 (file)
index 90f6575..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<div class="admin-sub-header">
-  <h1 i18n class="form-sub-title">Muted accounts</h1>
-</div>
-
-<p-table
-  [value]="blockedAccounts" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage"
-  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
->
-
-  <ng-template pTemplate="header">
-    <tr>
-      <th i18n>Account</th>
-      <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
-      <th></th> <!-- column for action buttons -->
-    </tr>
-  </ng-template>
-
-  <ng-template pTemplate="body" let-accountBlock>
-    <tr>
-      <td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
-      <td>{{ accountBlock.createdAt }}</td>
-      <td class="action-cell">
-        <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
-      </td>
-    </tr>
-  </ng-template>
-</p-table>
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss
deleted file mode 100644 (file)
index 6028b75..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-@import '_variables';
-@import '_mixins';
-
-.unblock-button {
-  @include peertube-button;
-  @include grey-button;
-}
\ No newline at end of file
index fd1fabcdbfea284fd51baea9949d45ba80836555..e48c39cdfb25d2f6cfa4a764847a98088aa5c8c3 100644 (file)
@@ -1,59 +1,15 @@
-import { Component, OnInit } 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 { AccountBlock, BlocklistService } from '@app/shared/blocklist'
+import { Component } from '@angular/core'
+import { GenericAccountBlocklistComponent, BlocklistComponentType } from '@app/shared/blocklist'
 
 @Component({
   selector: 'my-account-blocklist',
-  styleUrls: [ './my-account-blocklist.component.scss' ],
-  templateUrl: './my-account-blocklist.component.html'
+  styleUrls: [ '../../shared/blocklist/account-blocklist.component.scss' ],
+  templateUrl: '../../shared/blocklist/account-blocklist.component.html'
 })
-export class MyAccountBlocklistComponent extends RestTable implements OnInit {
-  blockedAccounts: AccountBlock[] = []
-  totalRecords = 0
-  sort: SortMeta = { field: 'createdAt', order: -1 }
-  pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
-
-  constructor (
-    private notifier: Notifier,
-    private blocklistService: BlocklistService,
-    private i18n: I18n
-  ) {
-    super()
-  }
-
-  ngOnInit () {
-    this.initialize()
-  }
+export class MyAccountBlocklistComponent extends GenericAccountBlocklistComponent {
+  mode = BlocklistComponentType.Account
 
   getIdentifier () {
     return 'MyAccountBlocklistComponent'
   }
-
-  unblockAccount (accountBlock: AccountBlock) {
-    const blockedAccount = accountBlock.blockedAccount
-
-    this.blocklistService.unblockAccountByUser(blockedAccount)
-        .subscribe(
-          () => {
-            this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost }))
-
-            this.loadData()
-          }
-        )
-  }
-
-  protected loadData () {
-    return this.blocklistService.getUserAccountBlocklist(this.pagination, this.sort)
-      .subscribe(
-        resultList => {
-          this.blockedAccounts = resultList.data
-          this.totalRecords = resultList.total
-        },
-
-        err => this.notifier.error(err.message)
-      )
-  }
 }
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html
deleted file mode 100644 (file)
index c31cff1..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<div class="admin-sub-header">
-  <h1 i18n class="form-sub-title">Muted instances</h1>
-</div>
-
-<p-table
-  [value]="blockedServers" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage"
-  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
->
-
-  <ng-template pTemplate="header">
-    <tr>
-      <th i18n>Instance</th>
-      <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
-      <th></th> <!-- column for action buttons -->
-    </tr>
-  </ng-template>
-
-  <ng-template pTemplate="body" let-serverBlock>
-    <tr>
-      <td>{{ serverBlock.blockedServer.host }}</td>
-      <td>{{ serverBlock.createdAt }}</td>
-      <td class="action-cell">
-        <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
-      </td>
-    </tr>
-  </ng-template>
-</p-table>
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss
deleted file mode 100644 (file)
index 6028b75..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-@import '_variables';
-@import '_mixins';
-
-.unblock-button {
-  @include peertube-button;
-  @include grey-button;
-}
\ No newline at end of file
index 483c11804c032cca6b96caeb2ec504176ca12a27..cfaba1c7b4959f425c83bd02a42978b355cd25cb 100644 (file)
@@ -1,60 +1,15 @@
-import { Component, OnInit } 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 { ServerBlock } from '../../../../../shared'
-import { BlocklistService } from '@app/shared/blocklist'
+import { Component } from '@angular/core'
+import { GenericServerBlocklistComponent, BlocklistComponentType } from '@app/shared/blocklist'
 
 @Component({
   selector: 'my-account-server-blocklist',
-  styleUrls: [ './my-account-server-blocklist.component.scss' ],
-  templateUrl: './my-account-server-blocklist.component.html'
+  styleUrls: [ '../../+admin/moderation/moderation.component.scss', '../../shared/blocklist/server-blocklist.component.scss' ],
+  templateUrl: '../../shared/blocklist/server-blocklist.component.html'
 })
-export class MyAccountServerBlocklistComponent extends RestTable implements OnInit {
-  blockedServers: ServerBlock[] = []
-  totalRecords = 0
-  sort: SortMeta = { field: 'createdAt', order: -1 }
-  pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
-
-  constructor (
-    private notifier: Notifier,
-    private blocklistService: BlocklistService,
-    private i18n: I18n
-  ) {
-    super()
-  }
-
-  ngOnInit () {
-    this.initialize()
-  }
+export class MyAccountServerBlocklistComponent extends GenericServerBlocklistComponent {
+  mode = BlocklistComponentType.Account
 
   getIdentifier () {
     return 'MyAccountServerBlocklistComponent'
   }
-
-  unblockServer (serverBlock: ServerBlock) {
-    const host = serverBlock.blockedServer.host
-
-    this.blocklistService.unblockServerByUser(host)
-      .subscribe(
-        () => {
-          this.notifier.success(this.i18n('Instance {{host}} unmuted.', { host }))
-
-          this.loadData()
-        }
-      )
-  }
-
-  protected loadData () {
-    return this.blocklistService.getUserServerBlocklist(this.pagination, this.sort)
-      .subscribe(
-        resultList => {
-          this.blockedServers = resultList.data
-          this.totalRecords = resultList.total
-        },
-
-        err => this.notifier.error(err.message)
-      )
-  }
 }
index f44b60ec989eb1b94d47236147caf8b01c217eac..f6b711e09304e0e5683de519decf572549233e8a 100644 (file)
@@ -140,7 +140,7 @@ const myAccountRoutes: Routes = [
         component: MyAccountServerBlocklistComponent,
         data: {
           meta: {
-            title: 'Muted instances'
+            title: 'Muted servers'
           }
         }
       },
index 05dcf522d4c13bdf6b8ba1be1fe161c6f0cf7492..ca447c0540a793968bbdc2097ae02b52dd31a76d 100644 (file)
@@ -72,7 +72,7 @@ export class MyAccountComponent implements OnInit {
           iconName: 'user'
         },
         {
-          label: this.i18n('Muted instances'),
+          label: this.i18n('Muted servers'),
           routerLink: '/my-account/blocklist/servers',
           iconName: 'server'
         },
diff --git a/client/src/app/shared/blocklist/account-blocklist.component.html b/client/src/app/shared/blocklist/account-blocklist.component.html
new file mode 100644 (file)
index 0000000..486785f
--- /dev/null
@@ -0,0 +1,64 @@
+<p-table
+  [value]="blockedAccounts" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" (onPage)="onPage($event)"
+  [showCurrentPageReport]="true" i18n-currentPageReportTemplate
+  currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} muted accounts"
+>
+  <ng-template pTemplate="caption">
+    <div class="caption">
+      <div class="ml-auto has-feedback has-clear">
+        <input
+          type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
+          (keyup)="onSearch($event)"
+        >
+        <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
+        <span class="sr-only" i18n>Clear filters</span>
+      </div>
+    </div>
+  </ng-template>
+
+  <ng-template pTemplate="header">
+    <tr>
+      <th style="width: 100%;" i18n>Account</th>
+      <th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
+      <th style="width: 150px;"></th> <!-- column for action buttons -->
+    </tr>
+  </ng-template>
+
+  <ng-template pTemplate="body" let-accountBlock>
+    <tr>
+      <td>
+        <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
+          <div class="chip two-lines">
+            <img
+              class="avatar"
+              [src]="accountBlock.blockedAccount.avatar?.path"
+              (error)="switchToDefaultAvatar($event)"
+              alt="Avatar"
+            >
+            <div>
+              {{ accountBlock.blockedAccount.displayName }}
+              <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span>
+            </div>
+          </div>
+        </a>
+      </td>
+
+      <td>{{ accountBlock.createdAt | date: 'short' }}</td>
+      <td class="action-cell">
+        <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
+      </td>
+    </tr>
+  </ng-template>
+
+  <ng-template pTemplate="emptymessage">
+    <tr>
+      <td colspan="6">
+        <div class="no-results">
+          <ng-container *ngIf="search" i18n>No account found matching current filters.</ng-container>
+          <ng-container *ngIf="!search" i18n>No account found.</ng-container>
+        </div>
+      </td>
+    </tr>
+  </ng-template>
+</p-table>
diff --git a/client/src/app/shared/blocklist/account-blocklist.component.scss b/client/src/app/shared/blocklist/account-blocklist.component.scss
new file mode 100644 (file)
index 0000000..aa8363f
--- /dev/null
@@ -0,0 +1,16 @@
+@import '_variables';
+@import '_mixins';
+
+.caption {
+  justify-content: flex-end;
+
+  input {
+    @include peertube-input-text(250px);
+    flex-grow: 1;
+  }
+}
+
+.unblock-button {
+  @include peertube-button;
+  @include grey-button;
+}
\ No newline at end of file
diff --git a/client/src/app/shared/blocklist/account-blocklist.component.ts b/client/src/app/shared/blocklist/account-blocklist.component.ts
new file mode 100644 (file)
index 0000000..dc5ac40
--- /dev/null
@@ -0,0 +1,79 @@
+import { OnInit } from '@angular/core'
+import { Notifier } from '@app/core'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { RestPagination, RestTable } from '@app/shared/rest'
+import { SortMeta } from 'primeng/api'
+import { AccountBlock } from './account-block.model'
+import { BlocklistService, BlocklistComponentType } from './blocklist.service'
+import { Actor } from '@app/shared/actor/actor.model'
+
+export class GenericAccountBlocklistComponent extends RestTable implements OnInit {
+  // @ts-ignore: "Abstract methods can only appear within an abstract class"
+  abstract mode: BlocklistComponentType
+
+  blockedAccounts: AccountBlock[] = []
+  totalRecords = 0
+  sort: SortMeta = { field: 'createdAt', order: -1 }
+  pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+
+  constructor (
+    private notifier: Notifier,
+    private blocklistService: BlocklistService,
+    private i18n: I18n
+  ) {
+    super()
+  }
+
+  // @ts-ignore: "Abstract methods can only appear within an abstract class"
+  abstract getIdentifier (): string
+
+  ngOnInit () {
+    this.initialize()
+  }
+
+  switchToDefaultAvatar ($event: Event) {
+    ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
+  }
+
+  unblockAccount (accountBlock: AccountBlock) {
+    const blockedAccount = accountBlock.blockedAccount
+    const operation = this.mode === BlocklistComponentType.Account
+      ? this.blocklistService.unblockAccountByUser(blockedAccount)
+      : this.blocklistService.unblockAccountByInstance(blockedAccount)
+
+    operation.subscribe(
+      () => {
+        this.notifier.success(
+          this.mode === BlocklistComponentType.Account
+            ? this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost })
+            : this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost })
+        )
+
+        this.loadData()
+      }
+    )
+  }
+
+  protected loadData () {
+    const operation = this.mode === BlocklistComponentType.Account
+      ? this.blocklistService.getUserAccountBlocklist({
+        pagination: this.pagination,
+        sort: this.sort,
+        search: this.search
+      })
+      : this.blocklistService.getInstanceAccountBlocklist({
+        pagination: this.pagination,
+        sort: this.sort,
+        search: this.search
+      })
+
+    return operation.subscribe(
+      resultList => {
+        this.blockedAccounts = resultList.data
+        this.totalRecords = resultList.total
+      },
+
+      err => this.notifier.error(err.message)
+    )
+  }
+}
index 5cf265bc1f3f22c7c224e705d3b66bc91801830b..c70a8173a010405c6a4ae1387746dd7290a51770 100644 (file)
@@ -8,6 +8,8 @@ import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '../
 import { Account } from '@app/shared/account/account.model'
 import { AccountBlock } from '@app/shared/blocklist/account-block.model'
 
+export enum BlocklistComponentType { Account, Instance }
+
 @Injectable()
 export class BlocklistService {
   static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
@@ -21,10 +23,14 @@ export class BlocklistService {
 
   /*********************** User -> Account blocklist ***********************/
 
-  getUserAccountBlocklist (pagination: RestPagination, sort: SortMeta) {
+  getUserAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
+    const { pagination, sort, search } = options
+
     let params = new HttpParams()
     params = this.restService.addRestGetParams(params, pagination, sort)
 
+    if (search) params = params.append('search', search)
+
     return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', { params })
                .pipe(
                  map(res => this.restExtractor.convertResultListDateToHuman(res)),
@@ -49,10 +55,14 @@ export class BlocklistService {
 
   /*********************** User -> Server blocklist ***********************/
 
-  getUserServerBlocklist (pagination: RestPagination, sort: SortMeta) {
+  getUserServerBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
+    const { pagination, sort, search } = options
+
     let params = new HttpParams()
     params = this.restService.addRestGetParams(params, pagination, sort)
 
+    if (search) params = params.append('search', search)
+
     return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params })
                .pipe(
                  map(res => this.restExtractor.convertResultListDateToHuman(res)),
@@ -76,7 +86,7 @@ export class BlocklistService {
 
   /*********************** Instance -> Account blocklist ***********************/
 
-  getInstanceAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search: string }) {
+  getInstanceAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
     const { pagination, sort, search } = options
 
     let params = new HttpParams()
@@ -108,7 +118,7 @@ export class BlocklistService {
 
   /*********************** Instance -> Server blocklist ***********************/
 
-  getInstanceServerBlocklist (options: { pagination: RestPagination, sort: SortMeta, search: string }) {
+  getInstanceServerBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
     const { pagination, sort, search } = options
 
     let params = new HttpParams()
index 5886ca07eee4a33910dc364a34f29e2309eeb7b1..188057b19fba6c5e63cfcec4f4073f3d944aadef 100644 (file)
@@ -1,2 +1,4 @@
 export * from './blocklist.service'
 export * from './account-block.model'
+export * from './server-blocklist.component'
+export * from './account-blocklist.component'
diff --git a/client/src/app/shared/blocklist/server-blocklist.component.html b/client/src/app/shared/blocklist/server-blocklist.component.html
new file mode 100644 (file)
index 0000000..977e0e1
--- /dev/null
@@ -0,0 +1,59 @@
+<p-table
+  [value]="blockedServers" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+  [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" (onPage)="onPage($event)"
+  [showCurrentPageReport]="true" i18n-currentPageReportTemplate
+  currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} muted instances"
+>
+  <ng-template pTemplate="caption">
+    <div class="caption">
+      <div class="ml-auto has-feedback has-clear">
+        <input
+          type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
+          (keyup)="onSearch($event)"
+        >
+        <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
+        <span class="sr-only" i18n>Clear filters</span>
+      </div>
+      <a class="ml-2 block-button" (click)="addServersToBlock()" (key.enter)="addServersToBlock()">
+        <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
+        <ng-container i18n>Mute domain</ng-container>
+      </a>
+    </div>
+  </ng-template>
+
+  <ng-template pTemplate="header">
+    <tr>
+      <th style="width: 100%;" i18n>Instance</th>
+      <th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
+      <th style="width: 150px;"></th> <!-- column for action buttons -->
+    </tr>
+  </ng-template>
+
+  <ng-template pTemplate="body" let-serverBlock>
+    <tr>
+      <td>
+        <a [href]="'https://' + serverBlock.blockedServer.host" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
+          {{ serverBlock.blockedServer.host }}
+          <span class="glyphicon glyphicon-new-window"></span>
+        </a>
+      </td>
+      <td>{{ serverBlock.createdAt | date: 'short' }}</td>
+      <td class="action-cell">
+        <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
+      </td>
+    </tr>
+  </ng-template>
+
+  <ng-template pTemplate="emptymessage">
+    <tr>
+      <td colspan="6">
+        <div class="no-results">
+          <ng-container *ngIf="search" i18n>No server found matching current filters.</ng-container>
+          <ng-container *ngIf="!search" i18n>No server found.</ng-container>
+        </div>
+      </td>
+    </tr>
+  </ng-template>
+</p-table>
+
+<my-batch-domains-modal #batchDomainsModal i18n-action action="Mute domains" (domains)="onDomainsToBlock($event)"></my-batch-domains-modal>
diff --git a/client/src/app/shared/blocklist/server-blocklist.component.scss b/client/src/app/shared/blocklist/server-blocklist.component.scss
new file mode 100644 (file)
index 0000000..9ddb768
--- /dev/null
@@ -0,0 +1,34 @@
+@import '_variables';
+@import '_mixins';
+
+a {
+  @include disable-default-a-behaviour;
+  display: inline-block;
+
+  &, &:hover {
+    color: pvar(--mainForegroundColor);
+  }
+
+  span {
+    font-size: 80%;
+    color: pvar(--inputPlaceholderColor);
+  }
+}
+
+.caption {
+  justify-content: flex-end;
+
+  input {
+    @include peertube-input-text(250px);
+    flex-grow: 1;
+  }
+}
+
+.unblock-button {
+  @include peertube-button;
+  @include grey-button;
+}
+
+.block-button {
+  @include create-button;
+}
diff --git a/client/src/app/shared/blocklist/server-blocklist.component.ts b/client/src/app/shared/blocklist/server-blocklist.component.ts
new file mode 100644 (file)
index 0000000..f2b36ba
--- /dev/null
@@ -0,0 +1,101 @@
+import { OnInit, ViewChild } from '@angular/core'
+import { Notifier } from '@app/core'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { RestPagination, RestTable } from '@app/shared/rest'
+import { SortMeta } from 'primeng/api'
+import { BlocklistService, BlocklistComponentType } from './blocklist.service'
+import { ServerBlock } from '../../../../../shared/models/blocklist/server-block.model'
+import { BatchDomainsModalComponent } from '@app/+admin/config/shared/batch-domains-modal.component'
+
+export class GenericServerBlocklistComponent extends RestTable implements OnInit {
+  @ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent
+
+  // @ts-ignore: "Abstract methods can only appear within an abstract class"
+  public abstract mode: BlocklistComponentType
+
+  blockedServers: ServerBlock[] = []
+  totalRecords = 0
+  sort: SortMeta = { field: 'createdAt', order: -1 }
+  pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+
+  constructor (
+    protected notifier: Notifier,
+    protected blocklistService: BlocklistService,
+    protected i18n: I18n
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    this.initialize()
+  }
+
+  // @ts-ignore: "Abstract methods can only appear within an abstract class"
+  public abstract getIdentifier (): string
+
+  unblockServer (serverBlock: ServerBlock) {
+    const operation = (host: string) => this.mode === BlocklistComponentType.Account
+        ? this.blocklistService.unblockServerByUser(host)
+        : this.blocklistService.unblockServerByInstance(host)
+    const host = serverBlock.blockedServer.host
+
+    operation(host).subscribe(
+      () => {
+        this.notifier.success(
+          this.mode === BlocklistComponentType.Account
+            ? this.i18n('Instance {{host}} unmuted.', { host })
+            : this.i18n('Instance {{host}} unmuted by your instance.', { host })
+        )
+
+        this.loadData()
+      }
+    )
+  }
+
+  addServersToBlock () {
+    this.batchDomainsModal.openModal()
+  }
+
+  onDomainsToBlock (domains: string[]) {
+    const operation = (domain: string) => this.mode === BlocklistComponentType.Account
+      ? this.blocklistService.blockServerByUser(domain)
+      : this.blocklistService.blockServerByInstance(domain)
+
+    domains.forEach(domain => {
+      operation(domain).subscribe(
+        () => {
+          this.notifier.success(
+            this.mode === BlocklistComponentType.Account
+              ? this.i18n('Instance {{domain}} muted.', { domain })
+              : this.i18n('Instance {{domain}} muted by your instance.', { domain })
+          )
+
+          this.loadData()
+        }
+      )
+    })
+  }
+
+  protected loadData () {
+    const operation = this.mode === BlocklistComponentType.Account
+      ? this.blocklistService.getUserServerBlocklist({
+        pagination: this.pagination,
+        sort: this.sort,
+        search: this.search
+      })
+      : this.blocklistService.getInstanceServerBlocklist({
+        pagination: this.pagination,
+        sort: this.sort,
+        search: this.search
+      })
+
+    return operation.subscribe(
+      resultList => {
+        this.blockedServers = resultList.data
+        this.totalRecords = resultList.total
+      },
+
+      err => this.notifier.error(err.message)
+    )
+  }
+}
index 2035097d75dd869bda7a4f9da01d6f475725da43..98fab9e16c3b5473231c1c7845152fff9a208b8a 100644 (file)
@@ -10,6 +10,7 @@ import { NgModule } from '@angular/core'
 import { FormsModule, ReactiveFormsModule } from '@angular/forms'
 import { RouterModule } from '@angular/router'
 import { BatchDomainsValidatorsService } from '@app/+admin/config/shared/batch-domains-validators.service'
+import { BatchDomainsModalComponent } from '@app/+admin/config/shared/batch-domains-modal.component'
 import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
 import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings'
 import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
@@ -192,7 +193,8 @@ import { VideoService } from './video/video.service'
 
     MyAccountVideoSettingsComponent,
     MyAccountInterfaceSettingsComponent,
-    ActorAvatarInfoComponent
+    ActorAvatarInfoComponent,
+    BatchDomainsModalComponent
   ],
 
   exports: [
@@ -274,7 +276,8 @@ import { VideoService } from './video/video.service'
 
     MyAccountVideoSettingsComponent,
     MyAccountInterfaceSettingsComponent,
-    ActorAvatarInfoComponent
+    ActorAvatarInfoComponent,
+    BatchDomainsModalComponent
   ],
 
   providers: [
index aaa1c05bdd4965094342f270d6d71c4485537206..4f753e04120a34da126cc52ce0d13e0a424fc9dc 100644 (file)
@@ -258,6 +258,8 @@ table {
 
 .no-results {
   height: 40vh;
+  max-height: 500px;
+
   display: flex;
   align-items: center;
   justify-content: center;
index 5971bb72a46422cc6283160edb0981032347e956..eb80ea0e32b6c80c32b641e960b690c75ae07cc4 100644 (file)
   }
 }
 
-@mixin empty-state {
-  min-height: 40vh;
-  max-height: 500px;
-
-  display: flex;
-  justify-content: center;
-  align-items: center;
-}
-
 @mixin admin-sub-header-responsive ($horizontal-margins) {
   flex-direction: column;