aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+admin
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/+admin')
-rw-r--r--client/src/app/+admin/admin.module.ts8
-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
15 files changed, 19 insertions, 1164 deletions
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index c59bd2927..da517a55b 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -2,6 +2,7 @@ import { ChartModule } from 'primeng/chart'
2import { SelectButtonModule } from 'primeng/selectbutton' 2import { SelectButtonModule } from 'primeng/selectbutton'
3import { TableModule } from 'primeng/table' 3import { TableModule } from 'primeng/table'
4import { NgModule } from '@angular/core' 4import { NgModule } from '@angular/core'
5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
5import { SharedFormModule } from '@app/shared/shared-forms' 6import { SharedFormModule } from '@app/shared/shared-forms'
6import { SharedGlobalIconModule } from '@app/shared/shared-icons' 7import { SharedGlobalIconModule } from '@app/shared/shared-icons'
7import { SharedMainModule } from '@app/shared/shared-main' 8import { SharedMainModule } from '@app/shared/shared-main'
@@ -14,10 +15,9 @@ import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponen
14import { FollowingListComponent } from './follows/following-list/following-list.component' 15import { FollowingListComponent } from './follows/following-list/following-list.component'
15import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' 16import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
16import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' 17import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
17import { ModerationCommentModalComponent, AbuseListComponent, VideoBlockListComponent } from './moderation' 18import { AbuseListComponent, VideoBlockListComponent } from './moderation'
18import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' 19import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
19import { ModerationComponent } from './moderation/moderation.component' 20import { ModerationComponent } from './moderation/moderation.component'
20import { AbuseDetailsComponent } from './moderation/abuse-list/abuse-details.component'
21import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component' 21import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component'
22import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component' 22import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component'
23import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component' 23import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component'
@@ -36,6 +36,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
36 SharedFormModule, 36 SharedFormModule,
37 SharedModerationModule, 37 SharedModerationModule,
38 SharedGlobalIconModule, 38 SharedGlobalIconModule,
39 SharedAbuseListModule,
39 40
40 TableModule, 41 TableModule,
41 SelectButtonModule, 42 SelectButtonModule,
@@ -60,11 +61,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
60 61
61 ModerationComponent, 62 ModerationComponent,
62 VideoBlockListComponent, 63 VideoBlockListComponent,
63
64 AbuseListComponent, 64 AbuseListComponent,
65 AbuseDetailsComponent,
66 65
67 ModerationCommentModalComponent,
68 InstanceServerBlocklistComponent, 66 InstanceServerBlocklistComponent,
69 InstanceAccountBlocklistComponent, 67 InstanceAccountBlocklistComponent,
70 68
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 })[] = []