aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+admin/moderation
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-07-27 11:40:30 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-07-31 11:35:19 +0200
commit94148c9028829b5576a5dcbfba2c7fb9cf6443d3 (patch)
tree2774f272329111abd03e8441ff936da11fb1a3f3 /client/src/app/+admin/moderation
parent441e453ae53e491b09c9b09b00b041788176ce64 (diff)
downloadPeerTube-94148c9028829b5576a5dcbfba2c7fb9cf6443d3.tar.gz
PeerTube-94148c9028829b5576a5dcbfba2c7fb9cf6443d3.tar.zst
PeerTube-94148c9028829b5576a5dcbfba2c7fb9cf6443d3.zip
Add abuse messages management in my account
Diffstat (limited to 'client/src/app/+admin/moderation')
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-details.component.html115
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts53
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.html193
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss32
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts470
-rw-r--r--client/src/app/+admin/moderation/abuse-list/index.ts2
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html38
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss6
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts70
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts2
-rw-r--r--client/src/app/+admin/moderation/moderation.component.scss181
-rw-r--r--client/src/app/+admin/moderation/moderation.component.ts2
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.scss9
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts2
14 files changed, 16 insertions, 1159 deletions
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html
deleted file mode 100644
index cba9cfb73..000000000
--- a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html
+++ /dev/null
@@ -1,115 +0,0 @@
1<div class="d-flex moderation-expanded">
2 <!-- report left part (report details) -->
3 <div class="col-8">
4
5 <!-- report metadata -->
6 <div class="d-flex" *ngIf="abuse.reporterAccount">
7 <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
8
9 <span class="col-9 moderation-expanded-text">
10 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
11 class="chip"
12 >
13 <img
14 class="avatar"
15 [src]="abuse.reporterAccount.avatar?.path"
16 (error)="switchToDefaultAvatar($event)"
17 alt="Avatar"
18 >
19 <div>
20 <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
21 </div>
22 </a>
23
24 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
25 class="ml-auto text-muted abuse-details-links" i18n
26 >
27 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
28 </a>
29 </span>
30 </div>
31
32 <div class="d-flex" *ngIf="abuse.flaggedAccount">
33 <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
34 <span class="col-9 moderation-expanded-text">
35 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
36 class="chip"
37 >
38 <img
39 class="avatar"
40 [src]="abuse.flaggedAccount?.avatar?.path"
41 (error)="switchToDefaultAvatar($event)"
42 alt="Avatar"
43 >
44 <div>
45 <span class="text-muted">{{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}</span>
46 </div>
47 </a>
48
49 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
50 class="ml-auto text-muted abuse-details-links" i18n
51 >
52 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
53 </a>
54 </span>
55 </div>
56
57 <div class="d-flex" *ngIf="abuse.updatedAt">
58 <span class="col-3 moderation-expanded-label" i18n>Updated</span>
59 <time class="col-9 moderation-expanded-text abuse-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time>
60 </div>
61
62 <!-- report text -->
63 <div class="mt-3 d-flex">
64 <span class="col-3 moderation-expanded-label">
65 <ng-container i18n>Report</ng-container>
66 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
67 </span>
68 <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
69 </div>
70
71 <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
72 <span class="col-3"></span>
73 <span class="col-9">
74 <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ '/admin/moderation/abuses/list' ]"
75 [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light"
76 >
77 <div>{{ reason.label }}</div>
78 </a>
79 </span>
80 </div>
81
82 <div *ngIf="abuse.video?.startAt" class="mt-2 d-flex">
83 <span class="col-3 moderation-expanded-label" i18n>Reported part</span>
84 <span class="col-9">
85 {{ startAt }}<ng-container *ngIf="abuse.video.endAt"> - {{ endAt }}</ng-container>
86 </span>
87 </div>
88
89 <div class="mt-3 d-flex" *ngIf="abuse.moderationComment">
90 <span class="col-3 moderation-expanded-label" i18n>Note</span>
91 <span class="col-9 moderation-expanded-text d-block" [innerHTML]="abuse.moderationCommentHtml"></span>
92 </div>
93
94 </div>
95
96 <!-- report right part (video/comment details) -->
97 <div class="col-4">
98 <div *ngIf="abuse.video" class="screenratio">
99 <div>
100 <span i18n *ngIf="abuse.video.deleted">The video was deleted</span>
101 <span i18n *ngIf="!abuse.video.deleted">The video was blocked</span>
102 </div>
103
104 <div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div>
105 </div>
106
107 <div *ngIf="abuse.comment" class="comment-html">
108 <div>
109 <strong i18n>Comment:</strong>
110 </div>
111
112 <div [innerHTML]="abuse.commentHtml"></div>
113 </div>
114 </div>
115</div>
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts
deleted file mode 100644
index fb0f65764..000000000
--- a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts
+++ /dev/null
@@ -1,53 +0,0 @@
1import { Component, Input } from '@angular/core'
2import { Actor } from '@app/shared/shared-main'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { AbusePredefinedReasonsString } from '@shared/models'
5import { ProcessedAbuse } from './abuse-list.component'
6import { durationToString } from '@app/helpers'
7
8@Component({
9 selector: 'my-abuse-details',
10 templateUrl: './abuse-details.component.html',
11 styleUrls: [ '../moderation.component.scss' ]
12})
13export class AbuseDetailsComponent {
14 @Input() abuse: ProcessedAbuse
15
16 private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
17
18 constructor (
19 private i18n: I18n
20 ) {
21 this.predefinedReasonsTranslations = {
22 violentOrRepulsive: this.i18n('Violent or Repulsive'),
23 hatefulOrAbusive: this.i18n('Hateful or Abusive'),
24 spamOrMisleading: this.i18n('Spam or Misleading'),
25 privacy: this.i18n('Privacy'),
26 rights: this.i18n('Rights'),
27 serverRules: this.i18n('Server rules'),
28 thumbnails: this.i18n('Thumbnails'),
29 captions: this.i18n('Captions')
30 }
31 }
32
33 get startAt () {
34 return durationToString(this.abuse.video.startAt)
35 }
36
37 get endAt () {
38 return durationToString(this.abuse.video.endAt)
39 }
40
41 getPredefinedReasons () {
42 if (!this.abuse.predefinedReasons) return []
43
44 return this.abuse.predefinedReasons.map(r => ({
45 id: r,
46 label: this.predefinedReasonsTranslations[r]
47 }))
48 }
49
50 switchToDefaultAvatar ($event: Event) {
51 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
52 }
53}
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
index 9fae5667f..9a6c124e1 100644
--- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
@@ -3,195 +3,4 @@
3 <ng-container i18n>Reports</ng-container> 3 <ng-container i18n>Reports</ng-container>
4</h1> 4</h1>
5 5
6<p-table 6<my-abuse-list-table viewType="admin" baseRoute="/admin/moderation/abuses/list"></my-abuse-list-table>
7 [value]="abuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
8 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true" [lazyLoadOnInit]="false"
9 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
10 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
11 (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
12>
13 <ng-template pTemplate="caption">
14 <div class="caption">
15 <div class="ml-auto">
16 <div class="input-group has-feedback has-clear">
17 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
18 <div class="input-group-text" ngbDropdownToggle>
19 <span class="caret" aria-haspopup="menu" role="button"></span>
20 </div>
21
22 <div role="menu" ngbDropdownMenu>
23 <h6 class="dropdown-header" i18n>Advanced report filters</h6>
24 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
25 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
26 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
27 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
28 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
29 </div>
30 </div>
31 <input
32 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
33 (keyup)="onAbuseSearch($event)"
34 >
35 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
36 <span class="sr-only" i18n>Clear filters</span>
37 </div>
38 </div>
39 </div>
40 </ng-template>
41
42 <ng-template pTemplate="header">
43 <tr> <!-- header -->
44 <th style="width: 40px;"></th>
45 <th style="width: 20%;" pResizableColumn i18n>Reporter</th>
46 <th i18n>Video/Comment/Account</th>
47 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
48 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
49 <th i18n style="width: 80px;">Messages</th>
50 <th style="width: 150px;"></th>
51 </tr>
52 </ng-template>
53
54 <ng-template pTemplate="body" let-expanded="expanded" let-abuse>
55 <tr>
56 <td class="c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
57 <span class="expander">
58 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
59 </span>
60 </td>
61
62 <td>
63 <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
64 <div class="chip two-lines">
65 <img
66 class="avatar"
67 [src]="abuse.reporterAccount.avatar?.path"
68 (error)="switchToDefaultAvatar($event)"
69 alt="Avatar"
70 >
71 <div>
72 {{ abuse.reporterAccount.displayName }}
73 <span>{{ abuse.reporterAccount.nameWithHost }}</span>
74 </div>
75 </div>
76 </a>
77
78 <span i18n *ngIf="!abuse.reporterAccount">
79 Deleted account
80 </span>
81 </td>
82
83 <ng-container *ngIf="abuse.video">
84
85 <td *ngIf="!abuse.video.deleted">
86 <a [href]="getVideoUrl(abuse)" class="table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
87 <div class="table-video">
88 <div class="table-video-image">
89 <img [src]="abuse.video.thumbnailPath">
90 <span
91 class="table-video-image-label" *ngIf="abuse.count > 1"
92 i18n-title title="This video has been reported multiple times."
93 >
94 {{ abuse.nth }}/{{ abuse.count }}
95 </span>
96 </div>
97
98 <div class="table-video-text">
99 <div>
100 <span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
101 <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
102 {{ abuse.video.name }}
103 </div>
104 <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
105 </div>
106 </div>
107 </a>
108 </td>
109
110 <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse">
111 <div class="table-video" i18n-title title="Video was deleted">
112 <div class="table-video-image">
113 <span i18n>Deleted</span>
114 </div>
115
116 <div class="table-video-text">
117 <div>
118 {{ abuse.video.name }}
119 <span class="glyphicon glyphicon-trash"></span>
120 </div>
121 <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
122 </div>
123 </div>
124 </td>
125 </ng-container>
126
127 <ng-container *ngIf="abuse.comment">
128 <td>
129 <a [href]="getCommentUrl(abuse)" [innerHTML]="abuse.truncatedCommentHtml" class="table-comment-link"
130 [title]="abuse.comment.video.name" target="_blank" rel="noopener noreferrer"
131 ></a>
132
133 <div class="comment-flagged-account" *ngIf="abuse.flaggedAccount">by {{ abuse.flaggedAccount.displayName }}</div>
134 </td>
135 </ng-container>
136
137 <ng-container *ngIf="!abuse.comment && !abuse.video">
138 <td *ngIf="abuse.flaggedAccount">
139 <a [href]="getAccountUrl(abuse)" class="table-account-link" target="_blank" rel="noopener noreferrer">
140 <span>{{ abuse.flaggedAccount.displayName }}</span>
141
142 <span class="account-flagged-handle">{{ abuse.flaggedAccount.nameWithHostForced }}</span>
143 </a>
144 </td>
145
146 <td i18n *ngIf="!abuse.flaggedAccount">
147 Account deleted
148 </td>
149
150 </ng-container>
151
152
153 <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
154
155 <td class="c-hand abuse-states" [pRowToggler]="abuse">
156 <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
157 <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
158 <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
159 </td>
160
161 <td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)">
162 {{ abuse.countMessages }}
163
164 <my-global-icon iconName="message-circle"></my-global-icon>
165 </td>
166
167 <td class="action-cell">
168 <my-action-dropdown
169 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
170 i18n-label label="Actions" [actions]="abuseActions" [entry]="abuse"
171 ></my-action-dropdown>
172 </td>
173 </tr>
174 </ng-template>
175
176 <ng-template pTemplate="rowexpansion" let-abuse>
177 <tr>
178 <td class="expand-cell" colspan="6">
179 <my-abuse-details [abuse]="abuse"></my-abuse-details>
180 </td>
181 </tr>
182 </ng-template>
183
184 <ng-template pTemplate="emptymessage">
185 <tr>
186 <td colspan="6">
187 <div class="no-results">
188 <ng-container *ngIf="search" i18n>No abuses found matching current filters.</ng-container>
189 <ng-container *ngIf="!search" i18n>No abuses found.</ng-container>
190 </div>
191 </td>
192 </tr>
193 </ng-template>
194</p-table>
195
196<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
197<my-abuse-message-modal #abuseMessagesModal (countMessagesUpdated)="onCountMessagesUpdated($event)"></my-abuse-message-modal>
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss
deleted file mode 100644
index 48536e3c2..000000000
--- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss
+++ /dev/null
@@ -1,32 +0,0 @@
1@import 'mixins';
2@import 'miniature';
3
4.video-details-date-updated {
5 font-size: 90%;
6 margin-top: .1rem;
7}
8
9.video-details-links {
10 @include disable-default-a-behaviour;
11}
12
13.abuse-states .glyphicon-comment {
14 margin-left: 0.5rem;
15}
16
17.input-group {
18 @include peertube-input-group(300px);
19
20 .dropdown-toggle::after {
21 margin-left: 0;
22 }
23}
24
25.abuse-messages {
26 my-global-icon {
27 width: 22px;
28 margin-left: 3px;
29 position: relative;
30 top: -2px;
31 }
32}
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
index 86121fe58..85a150de9 100644
--- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
@@ -1,474 +1,10 @@
1import * as debug from 'debug' 1import { Component } from '@angular/core'
2import truncate from 'lodash-es/truncate'
3import { SortMeta } from 'primeng/api'
4import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
5import { environment } from 'src/environments/environment'
6import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
7import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
8import { ActivatedRoute, Params, Router } from '@angular/router'
9import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
10import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
11import { AbuseService, BlocklistService, VideoBlockService, AbuseMessageModalComponent } from '@app/shared/shared-moderation'
12import { VideoCommentService } from '@app/shared/shared-video-comment'
13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { AdminAbuse, AbuseState } from '@shared/models'
15import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
16
17const logger = debug('peertube:moderation:AbuseListComponent')
18
19// Don't use an abuse model because we need external services to compute some properties
20// And this model is only used in this component
21export type ProcessedAbuse = AdminAbuse & {
22 moderationCommentHtml?: string,
23 reasonHtml?: string
24 embedHtml?: SafeHtml
25 updatedAt?: Date
26
27 // override bare server-side definitions with rich client-side definitions
28 reporterAccount?: Account
29 flaggedAccount?: Account
30
31 truncatedCommentHtml?: string
32 commentHtml?: string
33
34 video: AdminAbuse['video'] & {
35 channel: AdminAbuse['video']['channel'] & {
36 ownerAccount: Account
37 }
38 }
39}
40 2
41@Component({ 3@Component({
42 selector: 'my-abuse-list', 4 selector: 'my-abuse-list',
43 templateUrl: './abuse-list.component.html', 5 templateUrl: './abuse-list.component.html',
44 styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ] 6 styleUrls: [ ]
45}) 7})
46export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit { 8export class AbuseListComponent {
47 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
48 @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
49
50 abuses: ProcessedAbuse[] = []
51 totalRecords = 0
52 sort: SortMeta = { field: 'createdAt', order: 1 }
53 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
54
55 abuseActions: DropdownAction<ProcessedAbuse>[][] = []
56
57 constructor (
58 private notifier: Notifier,
59 private abuseService: AbuseService,
60 private blocklistService: BlocklistService,
61 private commentService: VideoCommentService,
62 private videoService: VideoService,
63 private videoBlocklistService: VideoBlockService,
64 private confirmService: ConfirmService,
65 private i18n: I18n,
66 private markdownRenderer: MarkdownService,
67 private sanitizer: DomSanitizer,
68 private route: ActivatedRoute,
69 private router: Router
70 ) {
71 super()
72
73 this.abuseActions = [
74 this.buildInternalActions(),
75
76 this.buildFlaggedAccountActions(),
77
78 this.buildCommentActions(),
79
80 this.buildVideoActions(),
81
82 this.buildAccountActions()
83 ]
84 }
85
86 ngOnInit () {
87 this.initialize()
88
89 this.route.queryParams
90 .subscribe(params => {
91 this.search = params.search || ''
92
93 logger('On URL change (search: %s).', this.search)
94
95 this.setTableFilter(this.search)
96 this.loadData()
97 })
98 }
99
100 ngAfterViewInit () {
101 if (this.search) this.setTableFilter(this.search)
102 }
103
104 getIdentifier () {
105 return 'AbuseListComponent'
106 }
107
108 openModerationCommentModal (abuse: AdminAbuse) {
109 this.moderationCommentModal.openModal(abuse)
110 }
111
112 onModerationCommentUpdated () {
113 this.loadData()
114 }
115
116 /* Table filter functions */
117 onAbuseSearch (event: Event) {
118 this.onSearch(event)
119 this.setQueryParams((event.target as HTMLInputElement).value)
120 }
121
122 setQueryParams (search: string) {
123 const queryParams: Params = {}
124 if (search) Object.assign(queryParams, { search })
125
126 this.router.navigate([ '/admin/moderation/abuses/list' ], { queryParams })
127 }
128
129 resetTableFilter () {
130 this.setTableFilter('')
131 this.setQueryParams('')
132 this.resetSearch()
133 }
134 /* END Table filter functions */
135
136 isAbuseAccepted (abuse: AdminAbuse) {
137 return abuse.state.id === AbuseState.ACCEPTED
138 }
139
140 isAbuseRejected (abuse: AdminAbuse) {
141 return abuse.state.id === AbuseState.REJECTED
142 }
143
144 getVideoUrl (abuse: AdminAbuse) {
145 return Video.buildClientUrl(abuse.video.uuid)
146 }
147
148 getCommentUrl (abuse: AdminAbuse) {
149 return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
150 }
151
152 getAccountUrl (abuse: ProcessedAbuse) {
153 return '/accounts/' + abuse.flaggedAccount.nameWithHost
154 }
155
156 getVideoEmbed (abuse: AdminAbuse) {
157 return buildVideoEmbed(
158 buildVideoLink({
159 baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
160 title: false,
161 warningTitle: false,
162 startTime: abuse.startAt,
163 stopTime: abuse.endAt
164 })
165 )
166 }
167
168 switchToDefaultAvatar ($event: Event) {
169 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
170 }
171
172 async removeAbuse (abuse: AdminAbuse) {
173 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
174 if (res === false) return
175
176 this.abuseService.removeAbuse(abuse).subscribe(
177 () => {
178 this.notifier.success(this.i18n('Abuse deleted.'))
179 this.loadData()
180 },
181
182 err => this.notifier.error(err.message)
183 )
184 }
185
186 updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
187 this.abuseService.updateAbuse(abuse, { state })
188 .subscribe(
189 () => this.loadData(),
190
191 err => this.notifier.error(err.message)
192 )
193 }
194
195 onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
196 const abuse = this.abuses.find(a => a.id === event.abuseId)
197
198 if (!abuse) {
199 console.error('Cannot find abuse %d.', event.abuseId)
200 return
201 }
202
203 abuse.countMessages = event.countMessages
204 }
205
206 openAbuseMessagesModal (abuse: AdminAbuse) {
207 this.abuseMessagesModal.openModal(abuse)
208 }
209
210 protected loadData () {
211 logger('Load data.')
212
213 return this.abuseService.getAdminAbuses({
214 pagination: this.pagination,
215 sort: this.sort,
216 search: this.search
217 }).subscribe(
218 async resultList => {
219 this.totalRecords = resultList.total
220
221 this.abuses = []
222
223 for (const a of resultList.data) {
224 const abuse = a as ProcessedAbuse
225
226 abuse.reasonHtml = await this.toHtml(abuse.reason)
227 abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment)
228
229 if (abuse.video) {
230 abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
231
232 if (abuse.video.channel?.ownerAccount) {
233 abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
234 }
235 }
236
237 if (abuse.comment) {
238 if (abuse.comment.deleted) {
239 abuse.truncatedCommentHtml = abuse.commentHtml = this.i18n('Deleted comment')
240 } else {
241 const truncated = truncate(abuse.comment.text, { length: 100 })
242 abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true)
243 abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true)
244 }
245 }
246
247 if (abuse.reporterAccount) {
248 abuse.reporterAccount = new Account(abuse.reporterAccount)
249 }
250
251 if (abuse.flaggedAccount) {
252 abuse.flaggedAccount = new Account(abuse.flaggedAccount)
253 }
254
255 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
256
257 this.abuses.push(abuse)
258 }
259 },
260
261 err => this.notifier.error(err.message)
262 )
263 }
264
265 private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
266 return [
267 {
268 label: this.i18n('Internal actions'),
269 isHeader: true
270 },
271 {
272 label: this.i18n('Delete report'),
273 handler: abuse => this.removeAbuse(abuse)
274 },
275 {
276 label: this.i18n('Messages'),
277 handler: abuse => this.openAbuseMessagesModal(abuse)
278 },
279 {
280 label: this.i18n('Add internal note'),
281 handler: abuse => this.openModerationCommentModal(abuse),
282 isDisplayed: abuse => !abuse.moderationComment
283 },
284 {
285 label: this.i18n('Update note'),
286 handler: abuse => this.openModerationCommentModal(abuse),
287 isDisplayed: abuse => !!abuse.moderationComment
288 },
289 {
290 label: this.i18n('Mark as accepted'),
291 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
292 isDisplayed: abuse => !this.isAbuseAccepted(abuse)
293 },
294 {
295 label: this.i18n('Mark as rejected'),
296 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
297 isDisplayed: abuse => !this.isAbuseRejected(abuse)
298 }
299 ]
300 }
301
302 private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
303 return [
304 {
305 label: this.i18n('Actions for the flagged account'),
306 isHeader: true,
307 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video
308 },
309
310 {
311 label: this.i18n('Mute account'),
312 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
313 handler: abuse => this.muteAccountHelper(abuse.flaggedAccount)
314 },
315
316 {
317 label: this.i18n('Mute server account'),
318 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
319 handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host)
320 }
321 ]
322 }
323
324 private buildAccountActions (): DropdownAction<ProcessedAbuse>[] {
325 return [
326 {
327 label: this.i18n('Actions for the reporter'),
328 isHeader: true,
329 isDisplayed: abuse => !!abuse.reporterAccount
330 },
331
332 {
333 label: this.i18n('Mute reporter'),
334 isDisplayed: abuse => !!abuse.reporterAccount,
335 handler: abuse => this.muteAccountHelper(abuse.reporterAccount)
336 },
337
338 {
339 label: this.i18n('Mute server'),
340 isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
341 handler: abuse => this.muteServerHelper(abuse.reporterAccount.host)
342 }
343 ]
344 }
345
346 private buildVideoActions (): DropdownAction<ProcessedAbuse>[] {
347 return [
348 {
349 label: this.i18n('Actions for the video'),
350 isHeader: true,
351 isDisplayed: abuse => abuse.video && !abuse.video.deleted
352 },
353 {
354 label: this.i18n('Block video'),
355 isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted,
356 handler: abuse => {
357 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
358 .subscribe(
359 () => {
360 this.notifier.success(this.i18n('Video blocked.'))
361
362 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
363 },
364
365 err => this.notifier.error(err.message)
366 )
367 }
368 },
369 {
370 label: this.i18n('Unblock video'),
371 isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
372 handler: abuse => {
373 this.videoBlocklistService.unblockVideo(abuse.video.id)
374 .subscribe(
375 () => {
376 this.notifier.success(this.i18n('Video unblocked.'))
377
378 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
379 },
380
381 err => this.notifier.error(err.message)
382 )
383 }
384 },
385 {
386 label: this.i18n('Delete video'),
387 isDisplayed: abuse => abuse.video && !abuse.video.deleted,
388 handler: async abuse => {
389 const res = await this.confirmService.confirm(
390 this.i18n('Do you really want to delete this video?'),
391 this.i18n('Delete')
392 )
393 if (res === false) return
394
395 this.videoService.removeVideo(abuse.video.id)
396 .subscribe(
397 () => {
398 this.notifier.success(this.i18n('Video deleted.'))
399
400 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
401 },
402
403 err => this.notifier.error(err.message)
404 )
405 }
406 }
407 ]
408 }
409
410 private buildCommentActions (): DropdownAction<ProcessedAbuse>[] {
411 return [
412 {
413 label: this.i18n('Actions for the comment'),
414 isHeader: true,
415 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted
416 },
417
418 {
419 label: this.i18n('Delete comment'),
420 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted,
421 handler: async abuse => {
422 const res = await this.confirmService.confirm(
423 this.i18n('Do you really want to delete this comment?'),
424 this.i18n('Delete')
425 )
426 if (res === false) return
427
428 this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id)
429 .subscribe(
430 () => {
431 this.notifier.success(this.i18n('Comment deleted.'))
432
433 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
434 },
435
436 err => this.notifier.error(err.message)
437 )
438 }
439 }
440 ]
441 }
442
443 private muteAccountHelper (account: Account) {
444 this.blocklistService.blockAccountByInstance(account)
445 .subscribe(
446 () => {
447 this.notifier.success(
448 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
449 )
450
451 account.mutedByInstance = true
452 },
453
454 err => this.notifier.error(err.message)
455 )
456 }
457
458 private muteServerHelper (host: string) {
459 this.blocklistService.blockServerByInstance(host)
460 .subscribe(
461 () => {
462 this.notifier.success(
463 this.i18n('Server {{host}} muted by the instance.', { host: host })
464 )
465 },
466
467 err => this.notifier.error(err.message)
468 )
469 }
470 9
471 private toHtml (text: string) {
472 return this.markdownRenderer.textMarkdownToHTML(text)
473 }
474} 10}
diff --git a/client/src/app/+admin/moderation/abuse-list/index.ts b/client/src/app/+admin/moderation/abuse-list/index.ts
index c6037dab4..45cebdf4e 100644
--- a/client/src/app/+admin/moderation/abuse-list/index.ts
+++ b/client/src/app/+admin/moderation/abuse-list/index.ts
@@ -1,3 +1 @@
1export * from './abuse-details.component'
2export * from './abuse-list.component' export * from './abuse-list.component'
3export * from './moderation-comment-modal.component'
diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html
deleted file mode 100644
index 8082e93f4..000000000
--- a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html
+++ /dev/null
@@ -1,38 +0,0 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Moderation comment</h4>
4
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
6 </div>
7
8 <div class="modal-body">
9 <form novalidate [formGroup]="form" (ngSubmit)="banUser()">
10 <div class="form-group">
11 <textarea
12 formControlName="moderationComment" ngbAutofocus i18-placeholder placeholder="Comment this report…"
13 [ngClass]="{ 'input-error': formErrors['moderationComment'] }" class="form-control">
14 </textarea>
15 <div *ngIf="formErrors.moderationComment" class="form-error">
16 {{ formErrors.moderationComment }}
17 </div>
18 </div>
19
20 <div class="form-group" i18n>
21 This comment can only be seen by you or the other moderators.
22 </div>
23
24 <div class="form-group inputs">
25 <input
26 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
27 (click)="hide()" (key.enter)="hide()"
28 >
29
30 <input
31 type="submit" i18n-value value="Update this comment" class="action-button-submit"
32 [disabled]="!form.valid"
33 >
34 </div>
35 </form>
36 </div>
37
38</ng-template>
diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss
deleted file mode 100644
index afcdb9a16..000000000
--- a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss
+++ /dev/null
@@ -1,6 +0,0 @@
1@import 'variables';
2@import 'mixins';
3
4textarea {
5 @include peertube-textarea(100%, 100px);
6}
diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts
deleted file mode 100644
index ecb7966bf..000000000
--- a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts
+++ /dev/null
@@ -1,70 +0,0 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms'
4import { AbuseService } from '@app/shared/shared-moderation'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { AdminAbuse } from '@shared/models'
9
10@Component({
11 selector: 'my-moderation-comment-modal',
12 templateUrl: './moderation-comment-modal.component.html',
13 styleUrls: [ './moderation-comment-modal.component.scss' ]
14})
15export class ModerationCommentModalComponent extends FormReactive implements OnInit {
16 @ViewChild('modal', { static: true }) modal: NgbModal
17 @Output() commentUpdated = new EventEmitter<string>()
18
19 private abuseToComment: AdminAbuse
20 private openedModal: NgbModalRef
21
22 constructor (
23 protected formValidatorService: FormValidatorService,
24 private modalService: NgbModal,
25 private notifier: Notifier,
26 private abuseService: AbuseService,
27 private abuseValidatorsService: AbuseValidatorsService,
28 private i18n: I18n
29 ) {
30 super()
31 }
32
33 ngOnInit () {
34 this.buildForm({
35 moderationComment: this.abuseValidatorsService.ABUSE_MODERATION_COMMENT
36 })
37 }
38
39 openModal (abuseToComment: AdminAbuse) {
40 this.abuseToComment = abuseToComment
41 this.openedModal = this.modalService.open(this.modal, { centered: true })
42
43 this.form.patchValue({
44 moderationComment: this.abuseToComment.moderationComment
45 })
46 }
47
48 hide () {
49 this.abuseToComment = undefined
50 this.openedModal.close()
51 this.form.reset()
52 }
53
54 async banUser () {
55 const moderationComment: string = this.form.value[ 'moderationComment' ]
56
57 this.abuseService.updateAbuse(this.abuseToComment, { moderationComment })
58 .subscribe(
59 () => {
60 this.notifier.success(this.i18n('Comment updated.'))
61
62 this.commentUpdated.emit(moderationComment)
63 this.hide()
64 },
65
66 err => this.notifier.error(err.message)
67 )
68 }
69
70}
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
index d9fec29ce..548f3c917 100644
--- 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
@@ -3,7 +3,7 @@ import { BlocklistComponentType, GenericAccountBlocklistComponent } from '@app/s
3 3
4@Component({ 4@Component({
5 selector: 'my-instance-account-blocklist', 5 selector: 'my-instance-account-blocklist',
6 styleUrls: [ '../moderation.component.scss', '../../../shared/shared-moderation/account-blocklist.component.scss' ], 6 styleUrls: [ '../../../shared/shared-moderation/moderation.scss', '../../../shared/shared-moderation/account-blocklist.component.scss' ],
7 templateUrl: '../../../shared/shared-moderation/account-blocklist.component.html' 7 templateUrl: '../../../shared/shared-moderation/account-blocklist.component.html'
8}) 8})
9export class InstanceAccountBlocklistComponent extends GenericAccountBlocklistComponent { 9export class InstanceAccountBlocklistComponent extends GenericAccountBlocklistComponent {
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss
deleted file mode 100644
index 65fe94d39..000000000
--- a/client/src/app/+admin/moderation/moderation.component.scss
+++ /dev/null
@@ -1,181 +0,0 @@
1@import 'variables';
2@import 'mixins';
3@import 'miniature';
4
5.form-sub-title {
6 flex-grow: 0;
7 margin-right: 30px;
8}
9
10.caption {
11 justify-content: flex-end;
12
13 input {
14 @include peertube-input-text(250px);
15 flex-grow: 1;
16 }
17}
18
19.moderation-expanded {
20 font-size: 90%;
21
22 .moderation-expanded-label {
23 font-weight: $font-semibold;
24 display: inline-block;
25 vertical-align: top;
26 text-align: right;
27 }
28
29 .moderation-expanded-text {
30 display: inline-flex;
31 word-wrap: break-word;
32
33 ::ng-deep p:last-child {
34 margin-bottom: 0px !important;
35 }
36 }
37}
38
39.table-states {
40 & > :not(:first-child) {
41 margin-left: .4rem;
42 }
43}
44
45p-calendar {
46 display: block;
47
48 ::ng-deep {
49 .ui-widget-content {
50 min-width: 400px;
51 }
52
53 input {
54 @include peertube-input-text(100%);
55 }
56 }
57}
58
59.screenratio {
60 div {
61 @include miniature-thumbnail;
62
63 display: inline-flex;
64 justify-content: center;
65 align-items: center;
66 color: pvar(--inputPlaceholderColor);
67 }
68
69 @include large-screen-ratio($selector: 'div, ::ng-deep iframe') {
70 width: 100% !important;
71 height: 100% !important;
72 left: 0;
73 };
74}
75
76.comment-html {
77 background-color: #ececec;
78 padding: 10px;
79}
80
81.chip {
82 @include chip;
83}
84
85my-action-dropdown.show {
86 ::ng-deep .dropdown-root {
87 display: block !important;
88 }
89}
90
91
92.table-video-link {
93 @include disable-outline;
94
95 position: relative;
96 top: 3px;
97}
98
99.table-comment-link,
100.table-account-link {
101 @include disable-outline;
102
103 color: var(--mainForegroundColor);
104
105 ::ng-deep p:last-child {
106 margin: 0;
107 }
108}
109
110.table-account-link {
111 display: flex;
112 flex-direction: column;
113}
114
115.comment-flagged-account,
116.account-flagged-handle {
117 font-size: 11px;
118 color: var(--greyForegroundColor);
119}
120
121.table-video {
122 display: inline-flex;
123
124 .table-video-image {
125 @include miniature-thumbnail;
126
127 $image-height: 45px;
128
129 height: $image-height;
130 width: #{(16/9) * $image-height};
131 margin-right: 0.5rem;
132 border-radius: 2px;
133 border: none;
134 background: transparent;
135 display: inline-flex;
136 justify-content: center;
137 align-items: center;
138 position: relative;
139
140 img {
141 height: 100%;
142 width: 100%;
143 border-radius: 2px;
144 }
145
146 span {
147 color: pvar(--inputPlaceholderColor);
148 }
149
150 .table-video-image-label {
151 @include static-thumbnail-overlay;
152 position: absolute;
153 border-radius: 3px;
154 font-size: 10px;
155 padding: 0 3px;
156 line-height: 1.3;
157 bottom: 2px;
158 right: 2px;
159 }
160 }
161
162 .table-video-text {
163 display: inline-flex;
164 flex-direction: column;
165 justify-content: center;
166 font-size: 90%;
167 color: pvar(--mainForegroundColor);
168 line-height: 1rem;
169
170 div .glyphicon {
171 font-size: 80%;
172 color: gray;
173 margin-left: 0.1rem;
174 }
175
176 div + div {
177 color: var(--greyForegroundColor);
178 font-size: 11px;
179 }
180 }
181}
diff --git a/client/src/app/+admin/moderation/moderation.component.ts b/client/src/app/+admin/moderation/moderation.component.ts
index b0f5eb224..85665ea4f 100644
--- a/client/src/app/+admin/moderation/moderation.component.ts
+++ b/client/src/app/+admin/moderation/moderation.component.ts
@@ -3,7 +3,7 @@ import { ServerService } from '@app/core'
3 3
4@Component({ 4@Component({
5 templateUrl: './moderation.component.html', 5 templateUrl: './moderation.component.html',
6 styleUrls: [ './moderation.component.scss' ] 6 styleUrls: [ ]
7}) 7})
8export class ModerationComponent implements OnInit { 8export class ModerationComponent implements OnInit {
9 autoBlockVideosEnabled = false 9 autoBlockVideosEnabled = false
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.scss b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.scss
index 43a365608..c92d1c39c 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.scss
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.scss
@@ -16,3 +16,12 @@ my-global-icon {
16 margin-left: 0; 16 margin-left: 0;
17 } 17 }
18} 18}
19
20.caption {
21 justify-content: flex-end;
22
23 input {
24 @include peertube-input-text(250px);
25 flex-grow: 1;
26 }
27}
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
index 5a5132527..dfdf65c19 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
@@ -11,7 +11,7 @@ import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
11@Component({ 11@Component({
12 selector: 'my-video-block-list', 12 selector: 'my-video-block-list',
13 templateUrl: './video-block-list.component.html', 13 templateUrl: './video-block-list.component.html',
14 styleUrls: [ '../moderation.component.scss', './video-block-list.component.scss' ] 14 styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-block-list.component.scss' ]
15}) 15})
16export class VideoBlockListComponent extends RestTable implements OnInit, AfterViewInit { 16export class VideoBlockListComponent extends RestTable implements OnInit, AfterViewInit {
17 blocklist: (VideoBlacklist & { reasonHtml?: string })[] = [] 17 blocklist: (VideoBlacklist & { reasonHtml?: string })[] = []