aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+accounts/accounts.component.html6
-rw-r--r--client/src/app/+admin/admin.module.ts3
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/index.ts2
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html22
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss7
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts59
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html23
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss7
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts60
-rw-r--r--client/src/app/+admin/moderation/moderation.component.html4
-rw-r--r--client/src/app/+admin/moderation/moderation.component.ts8
-rw-r--r--client/src/app/+admin/moderation/moderation.routes.ts23
-rw-r--r--client/src/app/shared/account/account.model.ts12
-rw-r--r--client/src/app/shared/blocklist/account-block.model.ts2
-rw-r--r--client/src/app/shared/blocklist/blocklist.service.ts56
-rw-r--r--client/src/app/shared/blocklist/index.ts2
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts137
-rw-r--r--client/src/sass/include/_bootstrap-variables.scss4
-rw-r--r--server/models/video/video-comment.ts4
-rw-r--r--server/models/video/video.ts6
-rw-r--r--server/tests/api/users/blocklist.ts31
21 files changed, 437 insertions, 41 deletions
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 60dbcdf1d..c1377c1ea 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -10,8 +10,10 @@
10 <div class="actor-name">{{ account.nameWithHost }}</div> 10 <div class="actor-name">{{ account.nameWithHost }}</div>
11 11
12 <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span> 12 <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span>
13 <span *ngIf="account.muted" class="badge badge-danger" i18n>Muted</span> 13 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
14 <span *ngIf="account.mutedServer" class="badge badge-danger" i18n>Instance muted</span> 14 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Muted by your instance</span>
15 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Instance muted</span>
16 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
15 17
16 <my-user-moderation-dropdown 18 <my-user-moderation-dropdown
17 buttonSize="small" [account]="account" [user]="user" 19 buttonSize="small" [account]="account" [user]="user"
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 8c6db98d9..c06ae1d60 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -15,6 +15,7 @@ import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklis
15import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 15import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
17import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' 17import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
18import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
18 19
19@NgModule({ 20@NgModule({
20 imports: [ 21 imports: [
@@ -41,6 +42,8 @@ import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service
41 VideoBlacklistListComponent, 42 VideoBlacklistListComponent,
42 VideoAbuseListComponent, 43 VideoAbuseListComponent,
43 ModerationCommentModalComponent, 44 ModerationCommentModalComponent,
45 InstanceServerBlocklistComponent,
46 InstanceAccountBlocklistComponent,
44 47
45 JobsComponent, 48 JobsComponent,
46 JobsListComponent, 49 JobsListComponent,
diff --git a/client/src/app/+admin/moderation/instance-blocklist/index.ts b/client/src/app/+admin/moderation/instance-blocklist/index.ts
new file mode 100644
index 000000000..3e7a344bb
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/index.ts
@@ -0,0 +1,2 @@
1export * from './instance-account-blocklist.component'
2export * from './instance-server-blocklist.component'
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
new file mode 100644
index 000000000..7797bc56e
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
@@ -0,0 +1,22 @@
1<p-table
2 [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4>
5
6 <ng-template pTemplate="header">
7 <tr>
8 <th i18n>Account</th>
9 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
10 </tr>
11 </ng-template>
12
13 <ng-template pTemplate="body" let-accountBlock>
14 <tr>
15 <td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
16 <td>{{ accountBlock.createdAt }}</td>
17 <td class="action-cell">
18 <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
19 </td>
20 </tr>
21 </ng-template>
22</p-table>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts
new file mode 100644
index 000000000..3f243aee4
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts
@@ -0,0 +1,59 @@
1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { BlocklistService, AccountBlock } from '@app/shared/blocklist'
7
8@Component({
9 selector: 'my-instance-account-blocklist',
10 styleUrls: [ './instance-account-blocklist.component.scss' ],
11 templateUrl: './instance-account-blocklist.component.html'
12})
13export class InstanceAccountBlocklistComponent extends RestTable implements OnInit {
14 blockedAccounts: AccountBlock[] = []
15 totalRecords = 0
16 rowsPerPage = 10
17 sort: SortMeta = { field: 'createdAt', order: -1 }
18 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
19
20 constructor (
21 private notificationsService: NotificationsService,
22 private blocklistService: BlocklistService,
23 private i18n: I18n
24 ) {
25 super()
26 }
27
28 ngOnInit () {
29 this.initialize()
30 }
31
32 unblockAccount (accountBlock: AccountBlock) {
33 const blockedAccount = accountBlock.blockedAccount
34
35 this.blocklistService.unblockAccountByInstance(blockedAccount)
36 .subscribe(
37 () => {
38 this.notificationsService.success(
39 this.i18n('Success'),
40 this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost })
41 )
42
43 this.loadData()
44 }
45 )
46 }
47
48 protected loadData () {
49 return this.blocklistService.getInstanceAccountBlocklist(this.pagination, this.sort)
50 .subscribe(
51 resultList => {
52 this.blockedAccounts = resultList.data
53 this.totalRecords = resultList.total
54 },
55
56 err => this.notificationsService.error(this.i18n('Error'), err.message)
57 )
58 }
59}
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
new file mode 100644
index 000000000..859c0f916
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
@@ -0,0 +1,23 @@
1<p-table
2 [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4>
5
6 <ng-template pTemplate="header">
7 <tr>
8 <th i18n>Instance</th>
9 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
10 <th></th>
11 </tr>
12 </ng-template>
13
14 <ng-template pTemplate="body" let-serverBlock>
15 <tr>
16 <td>{{ serverBlock.blockedServer.host }}</td>
17 <td>{{ serverBlock.createdAt }}</td>
18 <td class="action-cell">
19 <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
20 </td>
21 </tr>
22 </ng-template>
23</p-table>
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
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
new file mode 100644
index 000000000..9459117a3
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
@@ -0,0 +1,60 @@
1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { BlocklistService } from '@app/shared/blocklist'
7import { ServerBlock } from '../../../../../../shared'
8
9@Component({
10 selector: 'my-instance-server-blocklist',
11 styleUrls: [ './instance-server-blocklist.component.scss' ],
12 templateUrl: './instance-server-blocklist.component.html'
13})
14export class InstanceServerBlocklistComponent extends RestTable implements OnInit {
15 blockedAccounts: ServerBlock[] = []
16 totalRecords = 0
17 rowsPerPage = 10
18 sort: SortMeta = { field: 'createdAt', order: -1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20
21 constructor (
22 private notificationsService: NotificationsService,
23 private blocklistService: BlocklistService,
24 private i18n: I18n
25 ) {
26 super()
27 }
28
29 ngOnInit () {
30 this.initialize()
31 }
32
33 unblockServer (serverBlock: ServerBlock) {
34 const host = serverBlock.blockedServer.host
35
36 this.blocklistService.unblockServerByInstance(host)
37 .subscribe(
38 () => {
39 this.notificationsService.success(
40 this.i18n('Success'),
41 this.i18n('Instance {{host}} unmuted by your instance.', { host })
42 )
43
44 this.loadData()
45 }
46 )
47 }
48
49 protected loadData () {
50 return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort)
51 .subscribe(
52 resultList => {
53 this.blockedAccounts = resultList.data
54 this.totalRecords = resultList.total
55 },
56
57 err => this.notificationsService.error(this.i18n('Error'), err.message)
58 )
59 }
60}
diff --git a/client/src/app/+admin/moderation/moderation.component.html b/client/src/app/+admin/moderation/moderation.component.html
index 91e87fcd4..8ec7278ef 100644
--- a/client/src/app/+admin/moderation/moderation.component.html
+++ b/client/src/app/+admin/moderation/moderation.component.html
@@ -5,6 +5,10 @@
5 <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a> 5 <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a>
6 6
7 <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a> 7 <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a>
8
9 <a *ngIf="hasAccountsBlacklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a>
10
11 <a *ngIf="hasServersBlacklistRight()" i18n routerLink="blocklist/servers" routerLinkActive="active">Muted servers</a>
8 </div> 12 </div>
9</div> 13</div>
10 14
diff --git a/client/src/app/+admin/moderation/moderation.component.ts b/client/src/app/+admin/moderation/moderation.component.ts
index 0f4efb970..7f85f920e 100644
--- a/client/src/app/+admin/moderation/moderation.component.ts
+++ b/client/src/app/+admin/moderation/moderation.component.ts
@@ -16,4 +16,12 @@ export class ModerationComponent {
16 hasVideoBlacklistRight () { 16 hasVideoBlacklistRight () {
17 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) 17 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
18 } 18 }
19
20 hasAccountsBlacklistRight () {
21 return this.auth.getUser().hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)
22 }
23
24 hasServersBlacklistRight () {
25 return this.auth.getUser().hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)
26 }
19} 27}
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts
index 6d81b9b36..bc6dd49d5 100644
--- a/client/src/app/+admin/moderation/moderation.routes.ts
+++ b/client/src/app/+admin/moderation/moderation.routes.ts
@@ -4,6 +4,7 @@ import { UserRightGuard } from '@app/core'
4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' 4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list' 5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list'
6import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 6import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
7import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
7 8
8export const ModerationRoutes: Routes = [ 9export const ModerationRoutes: Routes = [
9 { 10 {
@@ -46,6 +47,28 @@ export const ModerationRoutes: Routes = [
46 title: 'Blacklisted videos' 47 title: 'Blacklisted videos'
47 } 48 }
48 } 49 }
50 },
51 {
52 path: 'blocklist/accounts',
53 component: InstanceAccountBlocklistComponent,
54 canActivate: [ UserRightGuard ],
55 data: {
56 userRight: UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
57 meta: {
58 title: 'Muted accounts'
59 }
60 }
61 },
62 {
63 path: 'blocklist/servers',
64 component: InstanceServerBlocklistComponent,
65 canActivate: [ UserRightGuard ],
66 data: {
67 userRight: UserRight.MANAGE_SERVER_REDUNDANCY,
68 meta: {
69 title: 'Muted instances'
70 }
71 }
49 } 72 }
50 ] 73 ]
51 } 74 }
diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts
index 0aba9428a..c5cd2051c 100644
--- a/client/src/app/shared/account/account.model.ts
+++ b/client/src/app/shared/account/account.model.ts
@@ -5,8 +5,10 @@ export class Account extends Actor implements ServerAccount {
5 displayName: string 5 displayName: string
6 description: string 6 description: string
7 nameWithHost: string 7 nameWithHost: string
8 muted: boolean 8 mutedByUser: boolean
9 mutedServer: boolean 9 mutedByInstance: boolean
10 mutedServerByUser: boolean
11 mutedServerByInstance: boolean
10 12
11 userId?: number 13 userId?: number
12 14
@@ -18,7 +20,9 @@ export class Account extends Actor implements ServerAccount {
18 this.userId = hash.userId 20 this.userId = hash.userId
19 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) 21 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
20 22
21 this.muted = false 23 this.mutedByUser = false
22 this.mutedServer = false 24 this.mutedByInstance = false
25 this.mutedServerByUser = false
26 this.mutedServerByInstance = false
23 } 27 }
24} 28}
diff --git a/client/src/app/shared/blocklist/account-block.model.ts b/client/src/app/shared/blocklist/account-block.model.ts
index 336680f65..e7b433d88 100644
--- a/client/src/app/shared/blocklist/account-block.model.ts
+++ b/client/src/app/shared/blocklist/account-block.model.ts
@@ -11,4 +11,4 @@ export class AccountBlock implements AccountBlockServer {
11 this.blockedAccount = new Account(block.blockedAccount) 11 this.blockedAccount = new Account(block.blockedAccount)
12 this.createdAt = block.createdAt 12 this.createdAt = block.createdAt
13 } 13 }
14} \ No newline at end of file 14}
diff --git a/client/src/app/shared/blocklist/blocklist.service.ts b/client/src/app/shared/blocklist/blocklist.service.ts
index d9c318258..c1f7312f0 100644
--- a/client/src/app/shared/blocklist/blocklist.service.ts
+++ b/client/src/app/shared/blocklist/blocklist.service.ts
@@ -11,6 +11,7 @@ import { AccountBlock } from '@app/shared/blocklist/account-block.model'
11@Injectable() 11@Injectable()
12export class BlocklistService { 12export class BlocklistService {
13 static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist' 13 static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
14 static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist'
14 15
15 constructor ( 16 constructor (
16 private authHttp: HttpClient, 17 private authHttp: HttpClient,
@@ -73,6 +74,61 @@ export class BlocklistService {
73 .pipe(catchError(err => this.restExtractor.handleError(err))) 74 .pipe(catchError(err => this.restExtractor.handleError(err)))
74 } 75 }
75 76
77 /*********************** Instance -> Account blocklist ***********************/
78
79 getInstanceAccountBlocklist (pagination: RestPagination, sort: SortMeta) {
80 let params = new HttpParams()
81 params = this.restService.addRestGetParams(params, pagination, sort)
82
83 return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', { params })
84 .pipe(
85 map(res => this.restExtractor.convertResultListDateToHuman(res)),
86 map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
87 catchError(err => this.restExtractor.handleError(err))
88 )
89 }
90
91 blockAccountByInstance (account: Account) {
92 const body = { accountName: account.nameWithHost }
93
94 return this.authHttp.post(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', body)
95 .pipe(catchError(err => this.restExtractor.handleError(err)))
96 }
97
98 unblockAccountByInstance (account: Account) {
99 const path = BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts/' + account.nameWithHost
100
101 return this.authHttp.delete(path)
102 .pipe(catchError(err => this.restExtractor.handleError(err)))
103 }
104
105 /*********************** Instance -> Server blocklist ***********************/
106
107 getInstanceServerBlocklist (pagination: RestPagination, sort: SortMeta) {
108 let params = new HttpParams()
109 params = this.restService.addRestGetParams(params, pagination, sort)
110
111 return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers', { params })
112 .pipe(
113 map(res => this.restExtractor.convertResultListDateToHuman(res)),
114 catchError(err => this.restExtractor.handleError(err))
115 )
116 }
117
118 blockServerByInstance (host: string) {
119 const body = { host }
120
121 return this.authHttp.post(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers', body)
122 .pipe(catchError(err => this.restExtractor.handleError(err)))
123 }
124
125 unblockServerByInstance (host: string) {
126 const path = BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers/' + host
127
128 return this.authHttp.delete(path)
129 .pipe(catchError(err => this.restExtractor.handleError(err)))
130 }
131
76 private formatAccountBlock (accountBlock: AccountBlockServer) { 132 private formatAccountBlock (accountBlock: AccountBlockServer) {
77 return new AccountBlock(accountBlock) 133 return new AccountBlock(accountBlock)
78 } 134 }
diff --git a/client/src/app/shared/blocklist/index.ts b/client/src/app/shared/blocklist/index.ts
index 8cf6a55f7..5886ca07e 100644
--- a/client/src/app/shared/blocklist/index.ts
+++ b/client/src/app/shared/blocklist/index.ts
@@ -1,2 +1,2 @@
1export * from './blocklist.service' 1export * from './blocklist.service'
2export * from './account-block.model' \ No newline at end of file 2export * from './account-block.model'
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
index 2f4a55f37..908f0b8e0 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
@@ -26,7 +26,7 @@ export class UserModerationDropdownComponent implements OnChanges {
26 @Output() userChanged = new EventEmitter() 26 @Output() userChanged = new EventEmitter()
27 @Output() userDeleted = new EventEmitter() 27 @Output() userDeleted = new EventEmitter()
28 28
29 userActions: DropdownAction<User>[] = [] 29 userActions: DropdownAction<{ user: User, account: Account }>[] = []
30 30
31 constructor ( 31 constructor (
32 private authService: AuthService, 32 private authService: AuthService,
@@ -106,7 +106,7 @@ export class UserModerationDropdownComponent implements OnChanges {
106 this.i18n('Account {{nameWithHost}} muted.', { nameWithHost: account.nameWithHost }) 106 this.i18n('Account {{nameWithHost}} muted.', { nameWithHost: account.nameWithHost })
107 ) 107 )
108 108
109 this.account.muted = true 109 this.account.mutedByUser = true
110 this.userChanged.emit() 110 this.userChanged.emit()
111 }, 111 },
112 112
@@ -123,7 +123,7 @@ export class UserModerationDropdownComponent implements OnChanges {
123 this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: account.nameWithHost }) 123 this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: account.nameWithHost })
124 ) 124 )
125 125
126 this.account.muted = false 126 this.account.mutedByUser = false
127 this.userChanged.emit() 127 this.userChanged.emit()
128 }, 128 },
129 129
@@ -140,7 +140,7 @@ export class UserModerationDropdownComponent implements OnChanges {
140 this.i18n('Instance {{host}} muted.', { host }) 140 this.i18n('Instance {{host}} muted.', { host })
141 ) 141 )
142 142
143 this.account.mutedServer = true 143 this.account.mutedServerByUser = true
144 this.userChanged.emit() 144 this.userChanged.emit()
145 }, 145 },
146 146
@@ -157,7 +157,75 @@ export class UserModerationDropdownComponent implements OnChanges {
157 this.i18n('Instance {{host}} unmuted.', { host }) 157 this.i18n('Instance {{host}} unmuted.', { host })
158 ) 158 )
159 159
160 this.account.mutedServer = false 160 this.account.mutedServerByUser = false
161 this.userChanged.emit()
162 },
163
164 err => this.notificationsService.error(this.i18n('Error'), err.message)
165 )
166 }
167
168 blockAccountByInstance (account: Account) {
169 this.blocklistService.blockAccountByInstance(account)
170 .subscribe(
171 () => {
172 this.notificationsService.success(
173 this.i18n('Success'),
174 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
175 )
176
177 this.account.mutedByInstance = true
178 this.userChanged.emit()
179 },
180
181 err => this.notificationsService.error(this.i18n('Error'), err.message)
182 )
183 }
184
185 unblockAccountByInstance (account: Account) {
186 this.blocklistService.unblockAccountByInstance(account)
187 .subscribe(
188 () => {
189 this.notificationsService.success(
190 this.i18n('Success'),
191 this.i18n('Account {{nameWithHost}} unmuted by the instance.', { nameWithHost: account.nameWithHost })
192 )
193
194 this.account.mutedByInstance = false
195 this.userChanged.emit()
196 },
197
198 err => this.notificationsService.error(this.i18n('Error'), err.message)
199 )
200 }
201
202 blockServerByInstance (host: string) {
203 this.blocklistService.blockServerByInstance(host)
204 .subscribe(
205 () => {
206 this.notificationsService.success(
207 this.i18n('Success'),
208 this.i18n('Instance {{host}} muted by the instance.', { host })
209 )
210
211 this.account.mutedServerByInstance = true
212 this.userChanged.emit()
213 },
214
215 err => this.notificationsService.error(this.i18n('Error'), err.message)
216 )
217 }
218
219 unblockServerByInstance (host: string) {
220 this.blocklistService.unblockServerByInstance(host)
221 .subscribe(
222 () => {
223 this.notificationsService.success(
224 this.i18n('Success'),
225 this.i18n('Instance {{host}} unmuted by the instance.', { host })
226 )
227
228 this.account.mutedServerByInstance = false
161 this.userChanged.emit() 229 this.userChanged.emit()
162 }, 230 },
163 231
@@ -189,41 +257,74 @@ export class UserModerationDropdownComponent implements OnChanges {
189 }, 257 },
190 { 258 {
191 label: this.i18n('Ban'), 259 label: this.i18n('Ban'),
192 handler: ({ user }) => this.openBanUserModal(user), 260 handler: ({ user }: { user: User }) => this.openBanUserModal(user),
193 isDisplayed: ({ user }) => !user.muted 261 isDisplayed: ({ user }: { user: User }) => !user.blocked
194 }, 262 },
195 { 263 {
196 label: this.i18n('Unban'), 264 label: this.i18n('Unban'),
197 handler: ({ user }) => this.unbanUser(user), 265 handler: ({ user }: { user: User }) => this.unbanUser(user),
198 isDisplayed: ({ user }) => user.muted 266 isDisplayed: ({ user }: { user: User }) => user.blocked
199 } 267 }
200 ]) 268 ])
201 } 269 }
202 270
203 // User actions on accounts/servers 271 // Actions on accounts/servers
204 if (this.account) { 272 if (this.account) {
273 // User actions
205 this.userActions = this.userActions.concat([ 274 this.userActions = this.userActions.concat([
206 { 275 {
207 label: this.i18n('Mute this account'), 276 label: this.i18n('Mute this account'),
208 isDisplayed: ({ account }) => account.muted === false, 277 isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === false,
209 handler: ({ account }) => this.blockAccountByUser(account) 278 handler: ({ account }: { account: Account }) => this.blockAccountByUser(account)
210 }, 279 },
211 { 280 {
212 label: this.i18n('Unmute this account'), 281 label: this.i18n('Unmute this account'),
213 isDisplayed: ({ account }) => account.muted === true, 282 isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === true,
214 handler: ({ account }) => this.unblockAccountByUser(account) 283 handler: ({ account }: { account: Account }) => this.unblockAccountByUser(account)
215 }, 284 },
216 { 285 {
217 label: this.i18n('Mute the instance'), 286 label: this.i18n('Mute the instance'),
218 isDisplayed: ({ account }) => !account.userId && account.mutedServer === false, 287 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === false,
219 handler: ({ account }) => this.blockServerByUser(account.host) 288 handler: ({ account }: { account: Account }) => this.blockServerByUser(account.host)
220 }, 289 },
221 { 290 {
222 label: this.i18n('Unmute the instance'), 291 label: this.i18n('Unmute the instance'),
223 isDisplayed: ({ account }) => !account.userId && account.mutedServer === true, 292 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === true,
224 handler: ({ account }) => this.unblockServerByUser(account.host) 293 handler: ({ account }: { account: Account }) => this.unblockServerByUser(account.host)
225 } 294 }
226 ]) 295 ])
296
297 // Instance actions
298 if (authUser.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) {
299 this.userActions = this.userActions.concat([
300 {
301 label: this.i18n('Mute this account by your instance'),
302 isDisplayed: ({ account }: { account: Account }) => account.mutedByInstance === false,
303 handler: ({ account }: { account: Account }) => this.blockAccountByInstance(account)
304 },
305 {
306 label: this.i18n('Unmute this account by your instance'),
307 isDisplayed: ({ account }: { account: Account }) => account.mutedByInstance === true,
308 handler: ({ account }: { account: Account }) => this.unblockAccountByInstance(account)
309 }
310 ])
311 }
312
313 // Instance actions
314 if (authUser.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) {
315 this.userActions = this.userActions.concat([
316 {
317 label: this.i18n('Mute the instance by your instance'),
318 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === false,
319 handler: ({ account }: { account: Account }) => this.blockServerByInstance(account.host)
320 },
321 {
322 label: this.i18n('Unmute the instance by your instance'),
323 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === true,
324 handler: ({ account }: { account: Account }) => this.unblockServerByInstance(account.host)
325 }
326 ])
327 }
227 } 328 }
228 } 329 }
229 } 330 }
diff --git a/client/src/sass/include/_bootstrap-variables.scss b/client/src/sass/include/_bootstrap-variables.scss
index ce2532af5..77a20cfe1 100644
--- a/client/src/sass/include/_bootstrap-variables.scss
+++ b/client/src/sass/include/_bootstrap-variables.scss
@@ -29,4 +29,6 @@ $input-btn-focus-color: inherit;
29$input-focus-border-color: #ced4da; 29$input-focus-border-color: #ced4da;
30 30
31$nav-pills-link-active-bg: #F0F0F0; 31$nav-pills-link-active-bg: #F0F0F0;
32$nav-pills-link-active-color: #000; \ No newline at end of file 32$nav-pills-link-active-color: #000;
33
34$zindex-dropdown: 10000; \ No newline at end of file
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 08c6b3ff0..dd6d08139 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -294,7 +294,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
294 static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) { 294 static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) {
295 const serverActor = await getServerActor() 295 const serverActor = await getServerActor()
296 const serverAccountId = serverActor.Account.id 296 const serverAccountId = serverActor.Account.id
297 const userAccountId = user.Account.id 297 const userAccountId = user ? user.Account.id : undefined
298 298
299 const query = { 299 const query = {
300 offset: start, 300 offset: start,
@@ -330,7 +330,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
330 static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) { 330 static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) {
331 const serverActor = await getServerActor() 331 const serverActor = await getServerActor()
332 const serverAccountId = serverActor.Account.id 332 const serverAccountId = serverActor.Account.id
333 const userAccountId = user.Account.id 333 const userAccountId = user ? user.Account.id : undefined
334 334
335 const query = { 335 const query = {
336 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], 336 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index eab99cba7..6c183933b 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1255,9 +1255,11 @@ export class VideoModel extends Model<VideoModel> {
1255 1255
1256 // threshold corresponds to how many video the field should have to be returned 1256 // threshold corresponds to how many video the field should have to be returned
1257 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1257 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1258 const actorId = (await getServerActor()).id 1258 const serverActor = await getServerActor()
1259 const actorId = serverActor.id
1259 1260
1260 const scopeOptions = { 1261 const scopeOptions: AvailableForListIDsOptions = {
1262 serverAccountId: serverActor.Account.id,
1261 actorId, 1263 actorId,
1262 includeLocalVideos: true 1264 includeLocalVideos: true
1263 } 1265 }
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/users/blocklist.ts
index 99fe04b8c..eed4b9f3e 100644
--- a/server/tests/api/users/blocklist.ts
+++ b/server/tests/api/users/blocklist.ts
@@ -14,7 +14,7 @@ import {
14 userLogin 14 userLogin
15} from '../../utils/index' 15} from '../../utils/index'
16import { setAccessTokensToServers } from '../../utils/users/login' 16import { setAccessTokensToServers } from '../../utils/users/login'
17import { getVideosListWithToken } from '../../utils/videos/videos' 17import { getVideosListWithToken, getVideosList } from '../../utils/videos/videos'
18import { 18import {
19 addVideoCommentReply, 19 addVideoCommentReply,
20 addVideoCommentThread, 20 addVideoCommentThread,
@@ -41,9 +41,17 @@ import {
41const expect = chai.expect 41const expect = chai.expect
42 42
43async function checkAllVideos (url: string, token: string) { 43async function checkAllVideos (url: string, token: string) {
44 const res = await getVideosListWithToken(url, token) 44 {
45 const res = await getVideosListWithToken(url, token)
45 46
46 expect(res.body.data).to.have.lengthOf(4) 47 expect(res.body.data).to.have.lengthOf(4)
48 }
49
50 {
51 const res = await getVideosList(url)
52
53 expect(res.body.data).to.have.lengthOf(4)
54 }
47} 55}
48 56
49async function checkAllComments (url: string, token: string, videoUUID: string) { 57async function checkAllComments (url: string, token: string, videoUUID: string) {
@@ -444,16 +452,19 @@ describe('Test blocklist', function () {
444 452
445 it('Should hide its videos', async function () { 453 it('Should hide its videos', async function () {
446 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 454 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) {
447 const res = await getVideosListWithToken(servers[ 0 ].url, token) 455 const res1 = await getVideosList(servers[ 0 ].url)
456 const res2 = await getVideosListWithToken(servers[ 0 ].url, token)
448 457
449 const videos: Video[] = res.body.data 458 for (const res of [ res1, res2 ]) {
450 expect(videos).to.have.lengthOf(2) 459 const videos: Video[] = res.body.data
460 expect(videos).to.have.lengthOf(2)
451 461
452 const v1 = videos.find(v => v.name === 'video user 2') 462 const v1 = videos.find(v => v.name === 'video user 2')
453 const v2 = videos.find(v => v.name === 'video server 2') 463 const v2 = videos.find(v => v.name === 'video server 2')
454 464
455 expect(v1).to.be.undefined 465 expect(v1).to.be.undefined
456 expect(v2).to.be.undefined 466 expect(v2).to.be.undefined
467 }
457 } 468 }
458 }) 469 })
459 470