aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+accounts/accounts.component.html11
-rw-r--r--client/src/app/+admin/admin.module.ts3
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html2
-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/+admin/moderation/video-abuse-list/video-abuse-list.component.html2
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html2
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts1
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html6
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts28
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html26
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss7
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts59
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html27
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss7
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts60
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts24
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts20
-rw-r--r--client/src/app/+my-account/my-account.component.html16
-rw-r--r--client/src/app/+my-account/my-account.component.scss2
-rw-r--r--client/src/app/+my-account/my-account.component.ts15
-rw-r--r--client/src/app/+my-account/my-account.module.ts6
-rw-r--r--client/src/app/search/search.component.ts3
-rw-r--r--client/src/app/shared/account/account.model.ts9
-rw-r--r--client/src/app/shared/blocklist/account-block.model.ts14
-rw-r--r--client/src/app/shared/blocklist/blocklist.service.ts135
-rw-r--r--client/src/app/shared/blocklist/index.ts2
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.html6
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.scss5
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.html7
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts224
-rw-r--r--client/src/app/shared/rest/rest-table.ts4
-rw-r--r--client/src/app/shared/shared.module.ts2
41 files changed, 890 insertions, 60 deletions
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 036e794d2..c1377c1ea 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -10,8 +10,15 @@
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 13 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
14 <my-user-moderation-dropdown buttonSize="small" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"> 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>
17
18 <my-user-moderation-dropdown
19 buttonSize="small" [account]="account" [user]="user"
20 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
21 >
15 </my-user-moderation-dropdown> 22 </my-user-moderation-dropdown>
16 </div> 23 </div>
17 <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div> 24 <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div>
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/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index e2cbd35ca..dfbbfbb29 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -112,7 +112,7 @@
112 112
113 <my-peertube-checkbox 113 <my-peertube-checkbox
114 inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled" 114 inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled"
115 i18n-labelText labelText="Video import with HTTP enabled" 115 i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
116 ></my-peertube-checkbox> 116 ></my-peertube-checkbox>
117 117
118 <my-peertube-checkbox 118 <my-peertube-checkbox
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..f634ba834
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
@@ -0,0 +1,23 @@
1<p-table
2 [value]="blockedServers" [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..130009dc7
--- /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 blockedServers: 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.blockedServers = 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..01457936c 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="hasAccountsBlocklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a>
10
11 <a *ngIf="hasServersBlocklistRight()" 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..2b2618933 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 hasAccountsBlocklistRight () {
21 return this.auth.getUser().hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)
22 }
23
24 hasServersBlocklistRight () {
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/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
index 287ab3e46..0374b70ef 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
@@ -9,7 +9,7 @@
9 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 9 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
10 <th i18n>Video</th> 10 <th i18n>Video</th>
11 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> 11 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
12 <th style="width: 50px;"></th> 12 <th style="width: 120px;"></th>
13 </tr> 13 </tr>
14 </ng-template> 14 </ng-template>
15 15
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
index 0585e0490..ff4543b97 100644
--- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
+++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
@@ -8,7 +8,7 @@
8 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th> 8 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
9 <th i18n>Sensitive</th> 9 <th i18n>Sensitive</th>
10 <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> 10 <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
11 <th style="width: 50px;"></th> 11 <th style="width: 120px;"></th>
12 </tr> 12 </tr>
13 </ng-template> 13 </ng-template>
14 14
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts
index 07b087b5b..99ce5804b 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/users/user-edit/user-edit.ts
@@ -1,7 +1,6 @@
1import { ServerService } from '../../../core' 1import { ServerService } from '../../../core'
2import { FormReactive } from '../../../shared' 2import { FormReactive } from '../../../shared'
3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' 3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared'
4import { EditCustomConfigComponent } from '../../../+admin/config/edit-custom-config/'
5import { ConfigService } from '@app/+admin/config/shared/config.service' 4import { ConfigService } from '@app/+admin/config/shared/config.service'
6 5
7export abstract class UserEdit extends FormReactive { 6export abstract class UserEdit extends FormReactive {
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index afa9ccfe4..eb8d30e17 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -60,8 +60,10 @@
60 </td> 60 </td>
61 61
62 <td> 62 <td>
63 {{ user.username }} 63 <a i18n-title title="Go to the account page" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
64 <span *ngIf="user.blocked" class="banned-info">(banned)</span> 64 {{ user.username }}
65 <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
66 </a>
65 </td> 67 </td>
66 <td>{{ user.email }}</td> 68 <td>{{ user.email }}</td>
67 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> 69 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index 33384dc35..ab2250722 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -55,20 +55,6 @@ export class UserListComponent extends RestTable implements OnInit {
55 ] 55 ]
56 } 56 }
57 57
58 protected loadData () {
59 this.selectedUsers = []
60
61 this.userService.getUsers(this.pagination, this.sort, this.search)
62 .subscribe(
63 resultList => {
64 this.users = resultList.data
65 this.totalRecords = resultList.total
66 },
67
68 err => this.notificationsService.error(this.i18n('Error'), err.message)
69 )
70 }
71
72 openBanUserModal (users: User[]) { 58 openBanUserModal (users: User[]) {
73 for (const user of users) { 59 for (const user of users) {
74 if (user.username === 'root') { 60 if (user.username === 'root') {
@@ -131,4 +117,18 @@ export class UserListComponent extends RestTable implements OnInit {
131 isInSelectionMode () { 117 isInSelectionMode () {
132 return this.selectedUsers.length !== 0 118 return this.selectedUsers.length !== 0
133 } 119 }
120
121 protected loadData () {
122 this.selectedUsers = []
123
124 this.userService.getUsers(this.pagination, this.sort, this.search)
125 .subscribe(
126 resultList => {
127 this.users = resultList.data
128 this.totalRecords = resultList.total
129 },
130
131 err => this.notificationsService.error(this.i18n('Error'), err.message)
132 )
133 }
134} 134}
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
new file mode 100644
index 000000000..a96a11f5e
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html
@@ -0,0 +1,26 @@
1<div class="admin-sub-header">
2 <div i18n class="form-sub-title">Muted accounts</div>
3</div>
4
5<p-table
6 [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
7 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
8>
9
10 <ng-template pTemplate="header">
11 <tr>
12 <th i18n>Account</th>
13 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
14 </tr>
15 </ng-template>
16
17 <ng-template pTemplate="body" let-accountBlock>
18 <tr>
19 <td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
20 <td>{{ accountBlock.createdAt }}</td>
21 <td class="action-cell">
22 <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
23 </td>
24 </tr>
25 </ng-template>
26</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
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-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/+my-account/my-account-blocklist/my-account-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts
new file mode 100644
index 000000000..fbad28410
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-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-account-blocklist',
10 styleUrls: [ './my-account-blocklist.component.scss' ],
11 templateUrl: './my-account-blocklist.component.html'
12})
13export class MyAccountBlocklistComponent 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.unblockAccountByUser(blockedAccount)
36 .subscribe(
37 () => {
38 this.notificationsService.success(
39 this.i18n('Success'),
40 this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost })
41 )
42
43 this.loadData()
44 }
45 )
46 }
47
48 protected loadData () {
49 return this.blocklistService.getUserAccountBlocklist(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/+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
new file mode 100644
index 000000000..329cfb08f
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html
@@ -0,0 +1,27 @@
1<div class="admin-sub-header">
2 <div i18n class="form-sub-title">Muted instances</div>
3</div>
4
5<p-table
6 [value]="blockedServers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
7 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
8>
9
10 <ng-template pTemplate="header">
11 <tr>
12 <th i18n>Instance</th>
13 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
14 <th></th>
15 </tr>
16 </ng-template>
17
18 <ng-template pTemplate="body" let-serverBlock>
19 <tr>
20 <td>{{ serverBlock.blockedServer.host }}</td>
21 <td>{{ serverBlock.createdAt }}</td>
22 <td class="action-cell">
23 <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
24 </td>
25 </tr>
26 </ng-template>
27</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
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-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/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts
new file mode 100644
index 000000000..b411d6926
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-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 { ServerBlock } from '../../../../../shared'
7import { BlocklistService } from '@app/shared/blocklist'
8
9@Component({
10 selector: 'my-account-server-blocklist',
11 styleUrls: [ './my-account-server-blocklist.component.scss' ],
12 templateUrl: './my-account-server-blocklist.component.html'
13})
14export class MyAccountServerBlocklistComponent extends RestTable implements OnInit {
15 blockedServers: 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.unblockServerByUser(host)
37 .subscribe(
38 () => {
39 this.notificationsService.success(
40 this.i18n('Success'),
41 this.i18n('Instance {{host}} unmuted.', { host })
42 )
43
44 this.loadData()
45 }
46 )
47 }
48
49 protected loadData () {
50 return this.blocklistService.getUserServerBlocklist(this.pagination, this.sort)
51 .subscribe(
52 resultList => {
53 this.blockedServers = 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/+my-account/my-account-ownership/my-account-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts
index 520278671..0b51ac13c 100644
--- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts
+++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts
@@ -34,18 +34,6 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
34 this.initialize() 34 this.initialize()
35 } 35 }
36 36
37 protected loadData () {
38 return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort)
39 .subscribe(
40 resultList => {
41 this.videoChangeOwnerships = resultList.data
42 this.totalRecords = resultList.total
43 },
44
45 err => this.notificationsService.error(this.i18n('Error'), err.message)
46 )
47 }
48
49 createByString (account: Account) { 37 createByString (account: Account) {
50 return Account.CREATE_BY_STRING(account.name, account.host) 38 return Account.CREATE_BY_STRING(account.name, account.host)
51 } 39 }
@@ -65,4 +53,16 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
65 err => this.notificationsService.error(this.i18n('Error'), err.message) 53 err => this.notificationsService.error(this.i18n('Error'), err.message)
66 ) 54 )
67 } 55 }
56
57 protected loadData () {
58 return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort)
59 .subscribe(
60 resultList => {
61 this.videoChangeOwnerships = resultList.data
62 this.totalRecords = resultList.total
63 },
64
65 err => this.notificationsService.error(this.i18n('Error'), err.message)
66 )
67 }
68} 68}
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts
index 4b2168e35..601e517b4 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -11,6 +11,8 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun
11import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' 11import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
12import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' 12import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
13import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component' 13import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component'
14import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
15import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
14 16
15const myAccountRoutes: Routes = [ 17const myAccountRoutes: Routes = [
16 { 18 {
@@ -94,6 +96,24 @@ const myAccountRoutes: Routes = [
94 title: 'Ownership changes' 96 title: 'Ownership changes'
95 } 97 }
96 } 98 }
99 },
100 {
101 path: 'blocklist/accounts',
102 component: MyAccountBlocklistComponent,
103 data: {
104 meta: {
105 title: 'Muted accounts'
106 }
107 }
108 },
109 {
110 path: 'blocklist/servers',
111 component: MyAccountServerBlocklistComponent,
112 data: {
113 meta: {
114 title: 'Muted instances'
115 }
116 }
97 } 117 }
98 ] 118 ]
99 } 119 }
diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html
index b602fd69f..41333c25a 100644
--- a/client/src/app/+my-account/my-account.component.html
+++ b/client/src/app/+my-account/my-account.component.html
@@ -19,7 +19,21 @@
19 </div> 19 </div>
20 </div> 20 </div>
21 21
22 <a i18n routerLink="/my-account/ownership" routerLinkActive="active" class="title-page">Ownership changes</a> 22 <div ngbDropdown class="misc">
23 <span role="button" class="title-page" [ngClass]="{ active: miscLabel !== '' }" ngbDropdownToggle>
24 <ng-container i18n>Misc</ng-container>
25 <ng-container *ngIf="miscLabel"> - {{ miscLabel }}</ng-container>
26 </span>
27
28 <div ngbDropdownMenu>
29 <a class="dropdown-item" i18n routerLink="/my-account/blocklist/accounts">Muted accounts</a>
30
31 <a class="dropdown-item" i18n routerLink="/my-account/blocklist/servers">Muted instances</a>
32
33 <a class="dropdown-item" i18n routerLink="/my-account/ownership">Ownership changes</a>
34 </div>
35 </div>
36
23 </div> 37 </div>
24 38
25 <div class="margin-content"> 39 <div class="margin-content">
diff --git a/client/src/app/+my-account/my-account.component.scss b/client/src/app/+my-account/my-account.component.scss
index 20b2639b5..6243c6dcf 100644
--- a/client/src/app/+my-account/my-account.component.scss
+++ b/client/src/app/+my-account/my-account.component.scss
@@ -1,4 +1,4 @@
1.my-library { 1.my-library, .misc {
2 span[role=button] { 2 span[role=button] {
3 cursor: pointer; 3 cursor: pointer;
4 } 4 }
diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts
index bad60a8fb..d728caf07 100644
--- a/client/src/app/+my-account/my-account.component.ts
+++ b/client/src/app/+my-account/my-account.component.ts
@@ -13,6 +13,7 @@ import { Subscription } from 'rxjs'
13export class MyAccountComponent implements OnInit, OnDestroy { 13export class MyAccountComponent implements OnInit, OnDestroy {
14 14
15 libraryLabel = '' 15 libraryLabel = ''
16 miscLabel = ''
16 17
17 private routeSub: Subscription 18 private routeSub: Subscription
18 19
@@ -23,11 +24,11 @@ export class MyAccountComponent implements OnInit, OnDestroy {
23 ) {} 24 ) {}
24 25
25 ngOnInit () { 26 ngOnInit () {
26 this.updateLibraryLabel(this.router.url) 27 this.updateLabels(this.router.url)
27 28
28 this.routeSub = this.router.events 29 this.routeSub = this.router.events
29 .pipe(filter(event => event instanceof NavigationStart)) 30 .pipe(filter(event => event instanceof NavigationStart))
30 .subscribe((event: NavigationStart) => this.updateLibraryLabel(event.url)) 31 .subscribe((event: NavigationStart) => this.updateLabels(event.url))
31 } 32 }
32 33
33 ngOnDestroy () { 34 ngOnDestroy () {
@@ -40,7 +41,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
40 return importConfig.http.enabled || importConfig.torrent.enabled 41 return importConfig.http.enabled || importConfig.torrent.enabled
41 } 42 }
42 43
43 private updateLibraryLabel (url: string) { 44 private updateLabels (url: string) {
44 const [ path ] = url.split('?') 45 const [ path ] = url.split('?')
45 46
46 if (path.startsWith('/my-account/video-channels')) { 47 if (path.startsWith('/my-account/video-channels')) {
@@ -54,5 +55,13 @@ export class MyAccountComponent implements OnInit, OnDestroy {
54 } else { 55 } else {
55 this.libraryLabel = '' 56 this.libraryLabel = ''
56 } 57 }
58
59 if (path.startsWith('/my-account/blocklist/accounts')) {
60 this.miscLabel = this.i18n('Muted accounts')
61 } else if (path.startsWith('/my-account/blocklist/servers')) {
62 this.miscLabel = this.i18n('Muted instances')
63 } else {
64 this.miscLabel = ''
65 }
57 } 66 }
58} 67}
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index ad21162a8..017ebd57d 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -19,6 +19,8 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i
19import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' 19import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
20import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' 20import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone'
21import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' 21import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
22import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
23import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
22 24
23@NgModule({ 25@NgModule({
24 imports: [ 26 imports: [
@@ -45,7 +47,9 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub
45 ActorAvatarInfoComponent, 47 ActorAvatarInfoComponent,
46 MyAccountVideoImportsComponent, 48 MyAccountVideoImportsComponent,
47 MyAccountDangerZoneComponent, 49 MyAccountDangerZoneComponent,
48 MyAccountSubscriptionsComponent 50 MyAccountSubscriptionsComponent,
51 MyAccountBlocklistComponent,
52 MyAccountServerBlocklistComponent
49 ], 53 ],
50 54
51 exports: [ 55 exports: [
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts
index 911d56843..ecffcafc1 100644
--- a/client/src/app/search/search.component.ts
+++ b/client/src/app/search/search.component.ts
@@ -1,6 +1,6 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, RedirectService } from '@app/core' 3import { AuthService } from '@app/core'
4import { NotificationsService } from 'angular2-notifications' 4import { NotificationsService } from 'angular2-notifications'
5import { forkJoin, Subscription } from 'rxjs' 5import { forkJoin, Subscription } from 'rxjs'
6import { SearchService } from '@app/search/search.service' 6import { SearchService } from '@app/search/search.service'
@@ -40,7 +40,6 @@ export class SearchComponent implements OnInit, OnDestroy {
40 private route: ActivatedRoute, 40 private route: ActivatedRoute,
41 private router: Router, 41 private router: Router,
42 private metaService: MetaService, 42 private metaService: MetaService,
43 private redirectService: RedirectService,
44 private notificationsService: NotificationsService, 43 private notificationsService: NotificationsService,
45 private searchService: SearchService, 44 private searchService: SearchService,
46 private authService: AuthService 45 private authService: AuthService
diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts
index 42f2cfeaf..c5cd2051c 100644
--- a/client/src/app/shared/account/account.model.ts
+++ b/client/src/app/shared/account/account.model.ts
@@ -5,6 +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 mutedByUser: boolean
9 mutedByInstance: boolean
10 mutedServerByUser: boolean
11 mutedServerByInstance: boolean
8 12
9 userId?: number 13 userId?: number
10 14
@@ -15,5 +19,10 @@ export class Account extends Actor implements ServerAccount {
15 this.description = hash.description 19 this.description = hash.description
16 this.userId = hash.userId 20 this.userId = hash.userId
17 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) 21 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
22
23 this.mutedByUser = false
24 this.mutedByInstance = false
25 this.mutedServerByUser = false
26 this.mutedServerByInstance = false
18 } 27 }
19} 28}
diff --git a/client/src/app/shared/blocklist/account-block.model.ts b/client/src/app/shared/blocklist/account-block.model.ts
new file mode 100644
index 000000000..e7b433d88
--- /dev/null
+++ b/client/src/app/shared/blocklist/account-block.model.ts
@@ -0,0 +1,14 @@
1import { AccountBlock as AccountBlockServer } from '../../../../../shared'
2import { Account } from '../account/account.model'
3
4export class AccountBlock implements AccountBlockServer {
5 byAccount: Account
6 blockedAccount: Account
7 createdAt: Date | string
8
9 constructor (block: AccountBlockServer) {
10 this.byAccount = new Account(block.byAccount)
11 this.blockedAccount = new Account(block.blockedAccount)
12 this.createdAt = block.createdAt
13 }
14}
diff --git a/client/src/app/shared/blocklist/blocklist.service.ts b/client/src/app/shared/blocklist/blocklist.service.ts
new file mode 100644
index 000000000..c1f7312f0
--- /dev/null
+++ b/client/src/app/shared/blocklist/blocklist.service.ts
@@ -0,0 +1,135 @@
1import { Injectable } from '@angular/core'
2import { environment } from '../../../environments/environment'
3import { HttpClient, HttpParams } from '@angular/common/http'
4import { RestExtractor, RestPagination, RestService } from '../rest'
5import { SortMeta } from 'primeng/api'
6import { catchError, map } from 'rxjs/operators'
7import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '../../../../../shared'
8import { Account } from '@app/shared/account/account.model'
9import { AccountBlock } from '@app/shared/blocklist/account-block.model'
10
11@Injectable()
12export class BlocklistService {
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'
15
16 constructor (
17 private authHttp: HttpClient,
18 private restExtractor: RestExtractor,
19 private restService: RestService
20 ) { }
21
22 /*********************** User -> Account blocklist ***********************/
23
24 getUserAccountBlocklist (pagination: RestPagination, sort: SortMeta) {
25 let params = new HttpParams()
26 params = this.restService.addRestGetParams(params, pagination, sort)
27
28 return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', { params })
29 .pipe(
30 map(res => this.restExtractor.convertResultListDateToHuman(res)),
31 map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
32 catchError(err => this.restExtractor.handleError(err))
33 )
34 }
35
36 blockAccountByUser (account: Account) {
37 const body = { accountName: account.nameWithHost }
38
39 return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', body)
40 .pipe(catchError(err => this.restExtractor.handleError(err)))
41 }
42
43 unblockAccountByUser (account: Account) {
44 const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts/' + account.nameWithHost
45
46 return this.authHttp.delete(path)
47 .pipe(catchError(err => this.restExtractor.handleError(err)))
48 }
49
50 /*********************** User -> Server blocklist ***********************/
51
52 getUserServerBlocklist (pagination: RestPagination, sort: SortMeta) {
53 let params = new HttpParams()
54 params = this.restService.addRestGetParams(params, pagination, sort)
55
56 return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params })
57 .pipe(
58 map(res => this.restExtractor.convertResultListDateToHuman(res)),
59 catchError(err => this.restExtractor.handleError(err))
60 )
61 }
62
63 blockServerByUser (host: string) {
64 const body = { host }
65
66 return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', body)
67 .pipe(catchError(err => this.restExtractor.handleError(err)))
68 }
69
70 unblockServerByUser (host: string) {
71 const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers/' + host
72
73 return this.authHttp.delete(path)
74 .pipe(catchError(err => this.restExtractor.handleError(err)))
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
132 private formatAccountBlock (accountBlock: AccountBlockServer) {
133 return new AccountBlock(accountBlock)
134 }
135}
diff --git a/client/src/app/shared/blocklist/index.ts b/client/src/app/shared/blocklist/index.ts
new file mode 100644
index 000000000..5886ca07e
--- /dev/null
+++ b/client/src/app/shared/blocklist/index.ts
@@ -0,0 +1,2 @@
1export * from './blocklist.service'
2export * from './account-block.model'
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html
index 111627424..48230d6d8 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/buttons/action-dropdown.component.html
@@ -9,13 +9,13 @@
9 9
10 <div ngbDropdownMenu class="dropdown-menu"> 10 <div ngbDropdownMenu class="dropdown-menu">
11 <ng-container *ngFor="let action of actions"> 11 <ng-container *ngFor="let action of actions">
12 <div class="dropdown-item" *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true"> 12 <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true">
13 <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a> 13 <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a>
14 14
15 <span *ngIf="!action.linkBuilder" class="custom-action" class="dropdown-item" (click)="action.handler(entry)" role="button"> 15 <span *ngIf="!action.linkBuilder" class="custom-action dropdown-item" (click)="action.handler(entry)" role="button">
16 {{ action.label }} 16 {{ action.label }}
17 </span> 17 </span>
18 </div> 18 </ng-container>
19 </ng-container> 19 </ng-container>
20 </div> 20 </div>
21</div> \ No newline at end of file 21</div> \ No newline at end of file
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss
index 0a9aa7b04..92c4d1d2c 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.scss
+++ b/client/src/app/shared/buttons/action-dropdown.component.scss
@@ -46,5 +46,10 @@
46 .dropdown-item { 46 .dropdown-item {
47 cursor: pointer; 47 cursor: pointer;
48 color: #000 !important; 48 color: #000 !important;
49
50 a, span {
51 display: block;
52 width: 100%;
53 }
49 } 54 }
50} \ No newline at end of file 55} \ No newline at end of file
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.html b/client/src/app/shared/moderation/user-moderation-dropdown.component.html
index 01db7cd4a..7367a7e59 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.html
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.html
@@ -1,5 +1,8 @@
1<ng-container *ngIf="user && userActions.length !== 0"> 1<ng-container *ngIf="userActions.length !== 0">
2 <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal> 2 <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal>
3 3
4 <my-action-dropdown [actions]="userActions" [entry]="user" [buttonSize]="buttonSize" [placement]="placement"></my-action-dropdown> 4 <my-action-dropdown
5 [actions]="userActions" [entry]="{ user: user, account: account }"
6 [buttonSize]="buttonSize" [placement]="placement"
7 ></my-action-dropdown>
5</ng-container> \ No newline at end of file 8</ng-container> \ No newline at end of file
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 105c99d8b..908f0b8e0 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
@@ -1,4 +1,4 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' 4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
@@ -6,33 +6,38 @@ import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.com
6import { UserService } from '@app/shared/users' 6import { UserService } from '@app/shared/users'
7import { AuthService, ConfirmService } from '@app/core' 7import { AuthService, ConfirmService } from '@app/core'
8import { User, UserRight } from '../../../../../shared/models/users' 8import { User, UserRight } from '../../../../../shared/models/users'
9import { Account } from '@app/shared/account/account.model'
10import { BlocklistService } from '@app/shared/blocklist'
9 11
10@Component({ 12@Component({
11 selector: 'my-user-moderation-dropdown', 13 selector: 'my-user-moderation-dropdown',
12 templateUrl: './user-moderation-dropdown.component.html', 14 templateUrl: './user-moderation-dropdown.component.html',
13 styleUrls: [ './user-moderation-dropdown.component.scss' ] 15 styleUrls: [ './user-moderation-dropdown.component.scss' ]
14}) 16})
15export class UserModerationDropdownComponent implements OnInit { 17export class UserModerationDropdownComponent implements OnChanges {
16 @ViewChild('userBanModal') userBanModal: UserBanModalComponent 18 @ViewChild('userBanModal') userBanModal: UserBanModalComponent
17 19
18 @Input() user: User 20 @Input() user: User
21 @Input() account: Account
22
19 @Input() buttonSize: 'normal' | 'small' = 'normal' 23 @Input() buttonSize: 'normal' | 'small' = 'normal'
20 @Input() placement = 'left' 24 @Input() placement = 'left'
21 25
22 @Output() userChanged = new EventEmitter() 26 @Output() userChanged = new EventEmitter()
23 @Output() userDeleted = new EventEmitter() 27 @Output() userDeleted = new EventEmitter()
24 28
25 userActions: DropdownAction<User>[] = [] 29 userActions: DropdownAction<{ user: User, account: Account }>[] = []
26 30
27 constructor ( 31 constructor (
28 private authService: AuthService, 32 private authService: AuthService,
29 private notificationsService: NotificationsService, 33 private notificationsService: NotificationsService,
30 private confirmService: ConfirmService, 34 private confirmService: ConfirmService,
31 private userService: UserService, 35 private userService: UserService,
36 private blocklistService: BlocklistService,
32 private i18n: I18n 37 private i18n: I18n
33 ) { } 38 ) { }
34 39
35 ngOnInit () { 40 ngOnChanges () {
36 this.buildActions() 41 this.buildActions()
37 } 42 }
38 43
@@ -92,6 +97,142 @@ export class UserModerationDropdownComponent implements OnInit {
92 ) 97 )
93 } 98 }
94 99
100 blockAccountByUser (account: Account) {
101 this.blocklistService.blockAccountByUser(account)
102 .subscribe(
103 () => {
104 this.notificationsService.success(
105 this.i18n('Success'),
106 this.i18n('Account {{nameWithHost}} muted.', { nameWithHost: account.nameWithHost })
107 )
108
109 this.account.mutedByUser = true
110 this.userChanged.emit()
111 },
112
113 err => this.notificationsService.error(this.i18n('Error'), err.message)
114 )
115 }
116
117 unblockAccountByUser (account: Account) {
118 this.blocklistService.unblockAccountByUser(account)
119 .subscribe(
120 () => {
121 this.notificationsService.success(
122 this.i18n('Success'),
123 this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: account.nameWithHost })
124 )
125
126 this.account.mutedByUser = false
127 this.userChanged.emit()
128 },
129
130 err => this.notificationsService.error(this.i18n('Error'), err.message)
131 )
132 }
133
134 blockServerByUser (host: string) {
135 this.blocklistService.blockServerByUser(host)
136 .subscribe(
137 () => {
138 this.notificationsService.success(
139 this.i18n('Success'),
140 this.i18n('Instance {{host}} muted.', { host })
141 )
142
143 this.account.mutedServerByUser = true
144 this.userChanged.emit()
145 },
146
147 err => this.notificationsService.error(this.i18n('Error'), err.message)
148 )
149 }
150
151 unblockServerByUser (host: string) {
152 this.blocklistService.unblockServerByUser(host)
153 .subscribe(
154 () => {
155 this.notificationsService.success(
156 this.i18n('Success'),
157 this.i18n('Instance {{host}} unmuted.', { host })
158 )
159
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
229 this.userChanged.emit()
230 },
231
232 err => this.notificationsService.error(this.i18n('Error'), err.message)
233 )
234 }
235
95 getRouterUserEditLink (user: User) { 236 getRouterUserEditLink (user: User) {
96 return [ '/admin', 'users', 'update', user.id ] 237 return [ '/admin', 'users', 'update', user.id ]
97 } 238 }
@@ -102,28 +243,89 @@ export class UserModerationDropdownComponent implements OnInit {
102 if (this.authService.isLoggedIn()) { 243 if (this.authService.isLoggedIn()) {
103 const authUser = this.authService.getUser() 244 const authUser = this.authService.getUser()
104 245
105 if (authUser.hasRight(UserRight.MANAGE_USERS)) { 246 if (this.user && authUser.id === this.user.id) return
247
248 if (this.user && authUser.hasRight(UserRight.MANAGE_USERS)) {
106 this.userActions = this.userActions.concat([ 249 this.userActions = this.userActions.concat([
107 { 250 {
108 label: this.i18n('Edit'), 251 label: this.i18n('Edit'),
109 linkBuilder: this.getRouterUserEditLink 252 linkBuilder: ({ user }) => this.getRouterUserEditLink(user)
110 }, 253 },
111 { 254 {
112 label: this.i18n('Delete'), 255 label: this.i18n('Delete'),
113 handler: user => this.removeUser(user) 256 handler: ({ user }) => this.removeUser(user)
114 }, 257 },
115 { 258 {
116 label: this.i18n('Ban'), 259 label: this.i18n('Ban'),
117 handler: user => this.openBanUserModal(user), 260 handler: ({ user }: { user: User }) => this.openBanUserModal(user),
118 isDisplayed: user => !user.blocked 261 isDisplayed: ({ user }: { user: User }) => !user.blocked
119 }, 262 },
120 { 263 {
121 label: this.i18n('Unban'), 264 label: this.i18n('Unban'),
122 handler: user => this.unbanUser(user), 265 handler: ({ user }: { user: User }) => this.unbanUser(user),
123 isDisplayed: user => user.blocked 266 isDisplayed: ({ user }: { user: User }) => user.blocked
124 } 267 }
125 ]) 268 ])
126 } 269 }
270
271 // Actions on accounts/servers
272 if (this.account) {
273 // User actions
274 this.userActions = this.userActions.concat([
275 {
276 label: this.i18n('Mute this account'),
277 isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === false,
278 handler: ({ account }: { account: Account }) => this.blockAccountByUser(account)
279 },
280 {
281 label: this.i18n('Unmute this account'),
282 isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === true,
283 handler: ({ account }: { account: Account }) => this.unblockAccountByUser(account)
284 },
285 {
286 label: this.i18n('Mute the instance'),
287 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === false,
288 handler: ({ account }: { account: Account }) => this.blockServerByUser(account.host)
289 },
290 {
291 label: this.i18n('Unmute the instance'),
292 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === true,
293 handler: ({ account }: { account: Account }) => this.unblockServerByUser(account.host)
294 }
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 }
328 }
127 } 329 }
128 } 330 }
129} 331}
diff --git a/client/src/app/shared/rest/rest-table.ts b/client/src/app/shared/rest/rest-table.ts
index 26748f245..884588207 100644
--- a/client/src/app/shared/rest/rest-table.ts
+++ b/client/src/app/shared/rest/rest-table.ts
@@ -16,8 +16,6 @@ export abstract class RestTable {
16 private searchStream: Subject<string> 16 private searchStream: Subject<string>
17 private sortLocalStorageKey = 'rest-table-sort-' + this.constructor.name 17 private sortLocalStorageKey = 'rest-table-sort-' + this.constructor.name
18 18
19 protected abstract loadData (): void
20
21 initialize () { 19 initialize () {
22 this.loadSort() 20 this.loadSort()
23 this.initSearch() 21 this.initSearch()
@@ -71,4 +69,6 @@ export abstract class RestTable {
71 onSearch (search: string) { 69 onSearch (search: string) {
72 this.searchStream.next(search) 70 this.searchStream.next(search)
73 } 71 }
72
73 protected abstract loadData (): void
74} 74}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 9647a7966..40e05fcc7 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -58,6 +58,7 @@ import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-fe
58import { OverviewService } from '@app/shared/overview' 58import { OverviewService } from '@app/shared/overview'
59import { UserBanModalComponent } from '@app/shared/moderation' 59import { UserBanModalComponent } from '@app/shared/moderation'
60import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component' 60import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component'
61import { BlocklistService } from '@app/shared/blocklist'
61 62
62@NgModule({ 63@NgModule({
63 imports: [ 64 imports: [
@@ -172,6 +173,7 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod
172 OverviewService, 173 OverviewService,
173 VideoChangeOwnershipValidatorsService, 174 VideoChangeOwnershipValidatorsService,
174 VideoAcceptOwnershipValidatorsService, 175 VideoAcceptOwnershipValidatorsService,
176 BlocklistService,
175 177
176 I18nPrimengCalendarService, 178 I18nPrimengCalendarService,
177 ScreenService, 179 ScreenService,