aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/src/app/+admin/admin.module.ts8
-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/instance-blocklist/instance-account-blocklist.component.ts2
-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
-rw-r--r--client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html6
-rw-r--r--client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts11
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts10
-rw-r--r--client/src/app/+my-account/my-account.component.ts5
-rw-r--r--client/src/app/+my-account/my-account.module.ts6
-rw-r--r--client/src/app/helpers/utils.ts5
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.html (renamed from client/src/app/+admin/moderation/abuse-list/abuse-details.component.html)16
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.scss34
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.ts (renamed from client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts)8
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.html194
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.scss (renamed from client/src/app/+admin/moderation/moderation.component.scss)100
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts487
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html (renamed from client/src/app/shared/shared-moderation/abuse-message-modal.component.html)16
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss (renamed from client/src/app/shared/shared-moderation/abuse-message-modal.component.scss)47
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts (renamed from client/src/app/shared/shared-moderation/abuse-message-modal.component.ts)22
-rw-r--r--client/src/app/shared/shared-abuse-list/index.ts7
-rw-r--r--client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.html (renamed from client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html)0
-rw-r--r--client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.scss (renamed from client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss)0
-rw-r--r--client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts (renamed from client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts)0
-rw-r--r--client/src/app/shared/shared-abuse-list/processed-abuse.model.ts25
-rw-r--r--client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts42
-rw-r--r--client/src/app/shared/shared-main/account/actor.model.ts12
-rw-r--r--client/src/app/shared/shared-moderation/abuse.service.ts93
-rw-r--r--client/src/app/shared/shared-moderation/index.ts1
-rw-r--r--client/src/app/shared/shared-moderation/moderation.scss50
-rw-r--r--client/src/app/shared/shared-moderation/server-blocklist.component.scss13
-rw-r--r--client/src/app/shared/shared-moderation/shared-moderation.module.ts7
37 files changed, 1074 insertions, 865 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-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/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.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 })[] = []
diff --git a/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html
new file mode 100644
index 000000000..59ca61be6
--- /dev/null
+++ b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.html
@@ -0,0 +1,6 @@
1<h1>
2 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>Reports</ng-container>
4</h1>
5
6<my-abuse-list-table viewType="user" baseRoute="/my-account/abuses"></my-abuse-list-table>
diff --git a/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts
new file mode 100644
index 000000000..e5dd723ff
--- /dev/null
+++ b/client/src/app/+my-account/my-account-abuses/my-account-abuses-list.component.ts
@@ -0,0 +1,11 @@
1
2import { Component } from '@angular/core'
3
4@Component({
5 selector: 'my-account-abuses-list',
6 templateUrl: './my-account-abuses-list.component.html',
7 styleUrls: [ ]
8})
9export class MyAccountAbusesListComponent {
10
11}
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
index 9b983a197..50e724f87 100644
--- 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
@@ -3,7 +3,7 @@ import { BlocklistComponentType, GenericServerBlocklistComponent } from '@app/sh
3 3
4@Component({ 4@Component({
5 selector: 'my-account-server-blocklist', 5 selector: 'my-account-server-blocklist',
6 styleUrls: [ '../../+admin/moderation/moderation.component.scss', '../../shared/shared-moderation/server-blocklist.component.scss' ], 6 styleUrls: [ '../../shared/shared-moderation/moderation.scss', '../../shared/shared-moderation/server-blocklist.component.scss' ],
7 templateUrl: '../../shared/shared-moderation/server-blocklist.component.html' 7 templateUrl: '../../shared/shared-moderation/server-blocklist.component.html'
8}) 8})
9export class MyAccountServerBlocklistComponent extends GenericServerBlocklistComponent { 9export class MyAccountServerBlocklistComponent extends GenericServerBlocklistComponent {
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 ac9cf4cfd..0a4897d07 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -16,6 +16,7 @@ import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playli
16import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component' 16import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component'
17import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component' 17import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
18import { MyAccountComponent } from './my-account.component' 18import { MyAccountComponent } from './my-account.component'
19import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
19 20
20const myAccountRoutes: Routes = [ 21const myAccountRoutes: Routes = [
21 { 22 {
@@ -162,6 +163,15 @@ const myAccountRoutes: Routes = [
162 title: 'Notifications' 163 title: 'Notifications'
163 } 164 }
164 } 165 }
166 },
167 {
168 path: 'abuses',
169 component: MyAccountAbusesListComponent,
170 data: {
171 meta: {
172 title: 'My abuses'
173 }
174 }
165 } 175 }
166 ] 176 ]
167 } 177 }
diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts
index 5b2238f5a..dc2c8f39c 100644
--- a/client/src/app/+my-account/my-account.component.ts
+++ b/client/src/app/+my-account/my-account.component.ts
@@ -95,6 +95,11 @@ export class MyAccountComponent implements OnInit {
95 iconName: 'peertube-x' 95 iconName: 'peertube-x'
96 }, 96 },
97 { 97 {
98 label: this.i18n('My abuses'),
99 routerLink: '/my-account/abuses',
100 iconName: 'flag'
101 },
102 {
98 label: this.i18n('Ownership changes'), 103 label: this.i18n('Ownership changes'),
99 routerLink: '/my-account/ownership', 104 routerLink: '/my-account/ownership',
100 iconName: 'download' 105 iconName: 'download'
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 742a516d5..bf5a4fc8a 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -3,6 +3,7 @@ import { InputSwitchModule } from 'primeng/inputswitch'
3import { TableModule } from 'primeng/table' 3import { TableModule } from 'primeng/table'
4import { DragDropModule } from '@angular/cdk/drag-drop' 4import { DragDropModule } from '@angular/cdk/drag-drop'
5import { NgModule } from '@angular/core' 5import { NgModule } from '@angular/core'
6import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
6import { SharedFormModule } from '@app/shared/shared-forms' 7import { SharedFormModule } from '@app/shared/shared-forms'
7import { SharedGlobalIconModule } from '@app/shared/shared-icons' 8import { SharedGlobalIconModule } from '@app/shared/shared-icons'
8import { SharedMainModule } from '@app/shared/shared-main' 9import { SharedMainModule } from '@app/shared/shared-main'
@@ -11,6 +12,7 @@ import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-setti
11import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription/shared-user-subscription.module' 12import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription/shared-user-subscription.module'
12import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 13import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
13import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module' 14import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module'
15import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
14import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' 16import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
15import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' 17import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
16import { MyAccountHistoryComponent } from './my-account-history/my-account-history.component' 18import { MyAccountHistoryComponent } from './my-account-history/my-account-history.component'
@@ -50,7 +52,8 @@ import { MyAccountComponent } from './my-account.component'
50 SharedUserSubscriptionModule, 52 SharedUserSubscriptionModule,
51 SharedVideoPlaylistModule, 53 SharedVideoPlaylistModule,
52 SharedUserInterfaceSettingsModule, 54 SharedUserInterfaceSettingsModule,
53 SharedGlobalIconModule 55 SharedGlobalIconModule,
56 SharedAbuseListModule
54 ], 57 ],
55 58
56 declarations: [ 59 declarations: [
@@ -69,6 +72,7 @@ import { MyAccountComponent } from './my-account.component'
69 MyAccountDangerZoneComponent, 72 MyAccountDangerZoneComponent,
70 MyAccountSubscriptionsComponent, 73 MyAccountSubscriptionsComponent,
71 MyAccountBlocklistComponent, 74 MyAccountBlocklistComponent,
75 MyAccountAbusesListComponent,
72 MyAccountServerBlocklistComponent, 76 MyAccountServerBlocklistComponent,
73 MyAccountHistoryComponent, 77 MyAccountHistoryComponent,
74 MyAccountNotificationsComponent, 78 MyAccountNotificationsComponent,
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts
index 9bf22f62f..8e9f72adb 100644
--- a/client/src/app/helpers/utils.ts
+++ b/client/src/app/helpers/utils.ts
@@ -36,7 +36,10 @@ function populateAsyncUserVideoChannels (authService: AuthService, channel: { id
36} 36}
37 37
38function getAbsoluteAPIUrl () { 38function getAbsoluteAPIUrl () {
39 let absoluteAPIUrl = environment.apiUrl 39 let absoluteAPIUrl = environment.hmr === true
40 ? 'http://localhost:9000'
41 : environment.apiUrl
42
40 if (!absoluteAPIUrl) { 43 if (!absoluteAPIUrl) {
41 // The API is on the same domain 44 // The API is on the same domain
42 absoluteAPIUrl = window.location.origin 45 absoluteAPIUrl = window.location.origin
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
index cba9cfb73..431fdf5aa 100644
--- a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
@@ -3,11 +3,11 @@
3 <div class="col-8"> 3 <div class="col-8">
4 4
5 <!-- report metadata --> 5 <!-- report metadata -->
6 <div class="d-flex" *ngIf="abuse.reporterAccount"> 6 <div class="d-flex" *ngIf="isAdminView && abuse.reporterAccount">
7 <span class="col-3 moderation-expanded-label" i18n>Reporter</span> 7 <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
8 8
9 <span class="col-9 moderation-expanded-text"> 9 <span class="col-9 moderation-expanded-text">
10 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" 10 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
11 class="chip" 11 class="chip"
12 > 12 >
13 <img 13 <img
@@ -21,7 +21,7 @@
21 </div> 21 </div>
22 </a> 22 </a>
23 23
24 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" 24 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
25 class="ml-auto text-muted abuse-details-links" i18n 25 class="ml-auto text-muted abuse-details-links" i18n
26 > 26 >
27 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> 27 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
@@ -32,7 +32,7 @@
32 <div class="d-flex" *ngIf="abuse.flaggedAccount"> 32 <div class="d-flex" *ngIf="abuse.flaggedAccount">
33 <span class="col-3 moderation-expanded-label" i18n>Reportee</span> 33 <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
34 <span class="col-9 moderation-expanded-text"> 34 <span class="col-9 moderation-expanded-text">
35 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }" 35 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
36 class="chip" 36 class="chip"
37 > 37 >
38 <img 38 <img
@@ -46,7 +46,7 @@
46 </div> 46 </div>
47 </a> 47 </a>
48 48
49 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }" 49 <a *ngIf="isAdminView" [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
50 class="ml-auto text-muted abuse-details-links" i18n 50 class="ml-auto text-muted abuse-details-links" i18n
51 > 51 >
52 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> 52 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
@@ -63,7 +63,7 @@
63 <div class="mt-3 d-flex"> 63 <div class="mt-3 d-flex">
64 <span class="col-3 moderation-expanded-label"> 64 <span class="col-3 moderation-expanded-label">
65 <ng-container i18n>Report</ng-container> 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> 66 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
67 </span> 67 </span>
68 <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span> 68 <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
69 </div> 69 </div>
@@ -71,7 +71,7 @@
71 <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex"> 71 <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
72 <span class="col-3"></span> 72 <span class="col-3"></span>
73 <span class="col-9"> 73 <span class="col-9">
74 <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ '/admin/moderation/abuses/list' ]" 74 <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ baseRoute ]"
75 [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" 75 [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light"
76 > 76 >
77 <div>{{ reason.label }}</div> 77 <div>{{ reason.label }}</div>
@@ -86,7 +86,7 @@
86 </span> 86 </span>
87 </div> 87 </div>
88 88
89 <div class="mt-3 d-flex" *ngIf="abuse.moderationComment"> 89 <div class="mt-3 d-flex" *ngIf="isAdminView && abuse.moderationComment">
90 <span class="col-3 moderation-expanded-label" i18n>Note</span> 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> 91 <span class="col-9 moderation-expanded-text d-block" [innerHTML]="abuse.moderationCommentHtml"></span>
92 </div> 92 </div>
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.scss b/client/src/app/shared/shared-abuse-list/abuse-details.component.scss
new file mode 100644
index 000000000..d83eb974d
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.scss
@@ -0,0 +1,34 @@
1@import 'variables';
2@import 'mixins';
3@import 'miniature';
4
5.screenratio {
6 div {
7 @include miniature-thumbnail;
8
9 display: inline-flex;
10 justify-content: center;
11 align-items: center;
12 color: pvar(--inputPlaceholderColor);
13 }
14
15 @include large-screen-ratio($selector: 'div, ::ng-deep iframe') {
16 width: 100% !important;
17 height: 100% !important;
18 left: 0;
19 };
20}
21
22.comment-html {
23 background-color: #ececec;
24 padding: 10px;
25}
26
27.abuse-details-date-updated {
28 font-size: 90%;
29 margin-top: .1rem;
30}
31
32.abuse-details-links {
33 @include disable-default-a-behaviour;
34}
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts b/client/src/app/shared/shared-abuse-list/abuse-details.component.ts
index fb0f65764..cdd4bf2c8 100644
--- a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.ts
@@ -1,17 +1,19 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { durationToString } from '@app/helpers'
2import { Actor } from '@app/shared/shared-main' 3import { Actor } from '@app/shared/shared-main'
3import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
4import { AbusePredefinedReasonsString } from '@shared/models' 5import { AbusePredefinedReasonsString } from '@shared/models'
5import { ProcessedAbuse } from './abuse-list.component' 6import { ProcessedAbuse } from './processed-abuse.model'
6import { durationToString } from '@app/helpers'
7 7
8@Component({ 8@Component({
9 selector: 'my-abuse-details', 9 selector: 'my-abuse-details',
10 templateUrl: './abuse-details.component.html', 10 templateUrl: './abuse-details.component.html',
11 styleUrls: [ '../moderation.component.scss' ] 11 styleUrls: [ '../shared-moderation/moderation.scss', './abuse-details.component.scss' ]
12}) 12})
13export class AbuseDetailsComponent { 13export class AbuseDetailsComponent {
14 @Input() abuse: ProcessedAbuse 14 @Input() abuse: ProcessedAbuse
15 @Input() isAdminView: boolean
16 @Input() baseRoute: string
15 17
16 private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string } 18 private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
17 19
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
new file mode 100644
index 000000000..a6f707a47
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html
@@ -0,0 +1,194 @@
1<p-table
2 [value]="abuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true" [lazyLoadOnInit]="false"
4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
5 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
6 (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
7>
8 <ng-template pTemplate="caption">
9 <div class="caption">
10 <div class="ml-auto">
11 <div class="input-group has-feedback has-clear">
12 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
13 <div class="input-group-text" ngbDropdownToggle>
14 <span class="caret" aria-haspopup="menu" role="button"></span>
15 </div>
16
17 <div role="menu" ngbDropdownMenu>
18 <h6 class="dropdown-header" i18n>Advanced report filters</h6>
19 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
20 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
21 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
22 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
23 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
24 </div>
25 </div>
26 <input
27 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
28 (keyup)="onAbuseSearch($event)"
29 >
30 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
31 <span class="sr-only" i18n>Clear filters</span>
32 </div>
33 </div>
34 </div>
35 </ng-template>
36
37 <ng-template pTemplate="header">
38 <tr> <!-- header -->
39 <th style="width: 40px;"></th>
40 <th *ngIf="isAdminView()" style="width: 20%;" pResizableColumn i18n>Reporter</th>
41 <th i18n>Video/Comment/Account</th>
42 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
43 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
44 <th i18n style="width: 80px;">Messages</th>
45 <th style="width: 150px;"></th>
46 </tr>
47 </ng-template>
48
49 <ng-template pTemplate="body" let-expanded="expanded" let-abuse>
50 <tr>
51 <td class="c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
52 <span class="expander">
53 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
54 </span>
55 </td>
56
57 <td *ngIf="isAdminView()">
58 <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
59 <div class="chip two-lines">
60 <img
61 class="avatar"
62 [src]="abuse.reporterAccount.avatar?.path"
63 (error)="switchToDefaultAvatar($event)"
64 alt="Avatar"
65 >
66 <div>
67 {{ abuse.reporterAccount.displayName }}
68 <span>{{ abuse.reporterAccount.nameWithHost }}</span>
69 </div>
70 </div>
71 </a>
72
73 <span i18n *ngIf="!abuse.reporterAccount">
74 Deleted account
75 </span>
76 </td>
77
78 <ng-container *ngIf="abuse.video">
79
80 <td *ngIf="!abuse.video.deleted">
81 <a [href]="getVideoUrl(abuse)" class="table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
82 <div class="table-video">
83 <div class="table-video-image">
84 <img [src]="abuse.video.thumbnailPath">
85 <span
86 class="table-video-image-label" *ngIf="abuse.count > 1"
87 i18n-title title="This video has been reported multiple times."
88 >
89 {{ abuse.nth }}/{{ abuse.count }}
90 </span>
91 </div>
92
93 <div class="table-video-text">
94 <div>
95 <span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
96 <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
97 {{ abuse.video.name }}
98 </div>
99 <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
100 </div>
101 </div>
102 </a>
103 </td>
104
105 <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse">
106 <div class="table-video" i18n-title title="Video was deleted">
107 <div class="table-video-image">
108 <span i18n>Deleted</span>
109 </div>
110
111 <div class="table-video-text">
112 <div>
113 {{ abuse.video.name }}
114 <span class="glyphicon glyphicon-trash"></span>
115 </div>
116 <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
117 </div>
118 </div>
119 </td>
120 </ng-container>
121
122 <ng-container *ngIf="abuse.comment">
123 <td>
124 <a [href]="getCommentUrl(abuse)" [innerHTML]="abuse.truncatedCommentHtml" class="table-comment-link"
125 [title]="abuse.comment.video.name" target="_blank" rel="noopener noreferrer"
126 ></a>
127
128 <div class="comment-flagged-account" *ngIf="abuse.flaggedAccount">by {{ abuse.flaggedAccount.displayName }}</div>
129 </td>
130 </ng-container>
131
132 <ng-container *ngIf="!abuse.comment && !abuse.video">
133 <td *ngIf="abuse.flaggedAccount">
134 <a [href]="getAccountUrl(abuse)" class="table-account-link" target="_blank" rel="noopener noreferrer">
135 <span>{{ abuse.flaggedAccount.displayName }}</span>
136
137 <span class="account-flagged-handle">{{ abuse.flaggedAccount.nameWithHostForced }}</span>
138 </a>
139 </td>
140
141 <td i18n *ngIf="!abuse.flaggedAccount">
142 Account deleted
143 </td>
144
145 </ng-container>
146
147
148 <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
149
150 <td class="c-hand abuse-states" [pRowToggler]="abuse">
151 <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
152 <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
153 <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
154 </td>
155
156 <td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)">
157 <ng-container *ngIf="isLocalAbuse(abuse)">
158 {{ abuse.countMessages }}
159
160 <my-global-icon iconName="message-circle"></my-global-icon>
161 </ng-container>
162 </td>
163
164 <td class="action-cell">
165 <my-action-dropdown
166 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
167 i18n-label label="Actions" [actions]="abuseActions" [entry]="abuse"
168 ></my-action-dropdown>
169 </td>
170 </tr>
171 </ng-template>
172
173 <ng-template pTemplate="rowexpansion" let-abuse>
174 <tr>
175 <td class="expand-cell" colspan="6">
176 <my-abuse-details [abuse]="abuse" [baseRoute]="baseRoute" [isAdminView]="isAdminView()"></my-abuse-details>
177 </td>
178 </tr>
179 </ng-template>
180
181 <ng-template pTemplate="emptymessage">
182 <tr>
183 <td colspan="6">
184 <div class="no-results">
185 <ng-container *ngIf="search" i18n>No abuses found matching current filters.</ng-container>
186 <ng-container *ngIf="!search" i18n>No abuses found.</ng-container>
187 </div>
188 </td>
189 </tr>
190 </ng-template>
191</p-table>
192
193<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
194<my-abuse-message-modal #abuseMessagesModal [isAdminView]="isAdminView()" (countMessagesUpdated)="onCountMessagesUpdated($event)"></my-abuse-message-modal>
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.scss
index 65fe94d39..7ed7c9e87 100644
--- a/client/src/app/+admin/moderation/moderation.component.scss
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.scss
@@ -2,93 +2,6 @@
2@import 'mixins'; 2@import 'mixins';
3@import 'miniature'; 3@import 'miniature';
4 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 { 5.table-video-link {
93 @include disable-outline; 6 @include disable-outline;
94 7
@@ -179,3 +92,16 @@ my-action-dropdown.show {
179 } 92 }
180 } 93 }
181} 94}
95
96.abuse-states .glyphicon-comment {
97 margin-left: 0.5rem;
98}
99
100.abuse-messages {
101 my-global-icon {
102 width: 22px;
103 margin-left: 3px;
104 position: relative;
105 top: -2px;
106 }
107}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
new file mode 100644
index 000000000..1d17c9ec9
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
@@ -0,0 +1,487 @@
1import * as debug from 'debug'
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, Input } from '@angular/core'
7import { DomSanitizer } 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 } from '@app/shared/shared-moderation'
12import { VideoCommentService } from '@app/shared/shared-video-comment'
13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { AbuseState, AdminAbuse } from '@shared/models'
15import { AbuseMessageModalComponent } from './abuse-message-modal.component'
16import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
17import { ProcessedAbuse } from './processed-abuse.model'
18
19const logger = debug('peertube:moderation:AbuseListTableComponent')
20
21@Component({
22 selector: 'my-abuse-list-table',
23 templateUrl: './abuse-list-table.component.html',
24 styleUrls: [ '../shared-moderation/moderation.scss', './abuse-list-table.component.scss' ]
25})
26export class AbuseListTableComponent extends RestTable implements OnInit, AfterViewInit {
27 @Input() viewType: 'admin' | 'user'
28 @Input() baseRoute: string
29
30 @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
31 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
32
33 abuses: ProcessedAbuse[] = []
34 totalRecords = 0
35 sort: SortMeta = { field: 'createdAt', order: 1 }
36 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
37
38 abuseActions: DropdownAction<ProcessedAbuse>[][] = []
39
40 constructor (
41 private notifier: Notifier,
42 private abuseService: AbuseService,
43 private blocklistService: BlocklistService,
44 private commentService: VideoCommentService,
45 private videoService: VideoService,
46 private videoBlocklistService: VideoBlockService,
47 private confirmService: ConfirmService,
48 private i18n: I18n,
49 private markdownRenderer: MarkdownService,
50 private sanitizer: DomSanitizer,
51 private route: ActivatedRoute,
52 private router: Router
53 ) {
54 super()
55 }
56
57 ngOnInit () {
58 this.abuseActions = [
59 this.buildInternalActions(),
60
61 this.buildFlaggedAccountActions(),
62
63 this.buildCommentActions(),
64
65 this.buildVideoActions(),
66
67 this.buildAccountActions()
68 ]
69
70 this.initialize()
71
72 this.route.queryParams
73 .subscribe(params => {
74 this.search = params.search || ''
75
76 logger('On URL change (search: %s).', this.search)
77
78 this.setTableFilter(this.search)
79 this.loadData()
80 })
81 }
82
83 ngAfterViewInit () {
84 if (this.search) this.setTableFilter(this.search)
85 }
86
87 isAdminView () {
88 return this.viewType === 'admin'
89 }
90
91 getIdentifier () {
92 return 'AbuseListTableComponent'
93 }
94
95 openModerationCommentModal (abuse: AdminAbuse) {
96 this.moderationCommentModal.openModal(abuse)
97 }
98
99 onModerationCommentUpdated () {
100 this.loadData()
101 }
102
103 /* Table filter functions */
104 onAbuseSearch (event: Event) {
105 this.onSearch(event)
106 this.setQueryParams((event.target as HTMLInputElement).value)
107 }
108
109 setQueryParams (search: string) {
110 const queryParams: Params = {}
111 if (search) Object.assign(queryParams, { search })
112
113 this.router.navigate([ this.baseRoute ], { queryParams })
114 }
115
116 resetTableFilter () {
117 this.setTableFilter('')
118 this.setQueryParams('')
119 this.resetSearch()
120 }
121 /* END Table filter functions */
122
123 isAbuseAccepted (abuse: AdminAbuse) {
124 return abuse.state.id === AbuseState.ACCEPTED
125 }
126
127 isAbuseRejected (abuse: AdminAbuse) {
128 return abuse.state.id === AbuseState.REJECTED
129 }
130
131 getVideoUrl (abuse: AdminAbuse) {
132 return Video.buildClientUrl(abuse.video.uuid)
133 }
134
135 getCommentUrl (abuse: AdminAbuse) {
136 return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
137 }
138
139 getAccountUrl (abuse: ProcessedAbuse) {
140 return '/accounts/' + abuse.flaggedAccount.nameWithHost
141 }
142
143 getVideoEmbed (abuse: AdminAbuse) {
144 return buildVideoEmbed(
145 buildVideoLink({
146 baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
147 title: false,
148 warningTitle: false,
149 startTime: abuse.startAt,
150 stopTime: abuse.endAt
151 })
152 )
153 }
154
155 switchToDefaultAvatar ($event: Event) {
156 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
157 }
158
159 async removeAbuse (abuse: AdminAbuse) {
160 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
161 if (res === false) return
162
163 this.abuseService.removeAbuse(abuse).subscribe(
164 () => {
165 this.notifier.success(this.i18n('Abuse deleted.'))
166 this.loadData()
167 },
168
169 err => this.notifier.error(err.message)
170 )
171 }
172
173 updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
174 this.abuseService.updateAbuse(abuse, { state })
175 .subscribe(
176 () => this.loadData(),
177
178 err => this.notifier.error(err.message)
179 )
180 }
181
182 onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
183 const abuse = this.abuses.find(a => a.id === event.abuseId)
184
185 if (!abuse) {
186 console.error('Cannot find abuse %d.', event.abuseId)
187 return
188 }
189
190 abuse.countMessages = event.countMessages
191 }
192
193 openAbuseMessagesModal (abuse: AdminAbuse) {
194 this.abuseMessagesModal.openModal(abuse)
195 }
196
197 isLocalAbuse (abuse: AdminAbuse) {
198 if (this.viewType === 'user') return true
199
200 return Actor.IS_LOCAL(abuse.reporterAccount.host)
201 }
202
203 protected loadData () {
204 logger('Loading data.')
205
206 const options = {
207 pagination: this.pagination,
208 sort: this.sort,
209 search: this.search
210 }
211
212 const observable = this.viewType === 'admin'
213 ? this.abuseService.getAdminAbuses(options)
214 : this.abuseService.getUserAbuses(options)
215
216 return observable.subscribe(
217 async resultList => {
218 this.totalRecords = resultList.total
219
220 this.abuses = []
221
222 for (const a of resultList.data) {
223 const abuse = a as ProcessedAbuse
224
225 abuse.reasonHtml = await this.toHtml(abuse.reason)
226
227 if (abuse.moderationComment) {
228 abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment)
229 }
230
231 if (abuse.video) {
232 abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
233
234 if (abuse.video.channel?.ownerAccount) {
235 abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
236 }
237 }
238
239 if (abuse.comment) {
240 if (abuse.comment.deleted) {
241 abuse.truncatedCommentHtml = abuse.commentHtml = this.i18n('Deleted comment')
242 } else {
243 const truncated = truncate(abuse.comment.text, { length: 100 })
244 abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true)
245 abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true)
246 }
247 }
248
249 if (abuse.reporterAccount) {
250 abuse.reporterAccount = new Account(abuse.reporterAccount)
251 }
252
253 if (abuse.flaggedAccount) {
254 abuse.flaggedAccount = new Account(abuse.flaggedAccount)
255 }
256
257 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
258
259 this.abuses.push(abuse)
260 }
261 },
262
263 err => this.notifier.error(err.message)
264 )
265 }
266
267 private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
268 return [
269 {
270 label: this.i18n('Internal actions'),
271 isHeader: true
272 },
273 {
274 label: this.isAdminView()
275 ? this.i18n('Messages with reporter')
276 : this.i18n('Messages with moderators'),
277 handler: abuse => this.openAbuseMessagesModal(abuse),
278 isDisplayed: abuse => this.isLocalAbuse(abuse)
279 },
280 {
281 label: this.i18n('Update note'),
282 handler: abuse => this.openModerationCommentModal(abuse),
283 isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
284 },
285 {
286 label: this.i18n('Mark as accepted'),
287 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
288 isDisplayed: abuse => this.isAdminView() && !this.isAbuseAccepted(abuse)
289 },
290 {
291 label: this.i18n('Mark as rejected'),
292 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
293 isDisplayed: abuse => this.isAdminView() && !this.isAbuseRejected(abuse)
294 },
295 {
296 label: this.i18n('Add internal note'),
297 handler: abuse => this.openModerationCommentModal(abuse),
298 isDisplayed: abuse => this.isAdminView() && !abuse.moderationComment
299 },
300 {
301 label: this.i18n('Delete report'),
302 handler: abuse => this.isAdminView() && this.removeAbuse(abuse)
303 }
304 ]
305 }
306
307 private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
308 if (!this.isAdminView()) return []
309
310 return [
311 {
312 label: this.i18n('Actions for the flagged account'),
313 isHeader: true,
314 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video
315 },
316
317 {
318 label: this.i18n('Mute account'),
319 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
320 handler: abuse => this.muteAccountHelper(abuse.flaggedAccount)
321 },
322
323 {
324 label: this.i18n('Mute server account'),
325 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
326 handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host)
327 }
328 ]
329 }
330
331 private buildAccountActions (): DropdownAction<ProcessedAbuse>[] {
332 if (!this.isAdminView()) return []
333
334 return [
335 {
336 label: this.i18n('Actions for the reporter'),
337 isHeader: true,
338 isDisplayed: abuse => !!abuse.reporterAccount
339 },
340
341 {
342 label: this.i18n('Mute reporter'),
343 isDisplayed: abuse => !!abuse.reporterAccount,
344 handler: abuse => this.muteAccountHelper(abuse.reporterAccount)
345 },
346
347 {
348 label: this.i18n('Mute server'),
349 isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
350 handler: abuse => this.muteServerHelper(abuse.reporterAccount.host)
351 }
352 ]
353 }
354
355 private buildVideoActions (): DropdownAction<ProcessedAbuse>[] {
356 if (!this.isAdminView()) return []
357
358 return [
359 {
360 label: this.i18n('Actions for the video'),
361 isHeader: true,
362 isDisplayed: abuse => abuse.video && !abuse.video.deleted
363 },
364 {
365 label: this.i18n('Block video'),
366 isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted,
367 handler: abuse => {
368 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
369 .subscribe(
370 () => {
371 this.notifier.success(this.i18n('Video blocked.'))
372
373 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
374 },
375
376 err => this.notifier.error(err.message)
377 )
378 }
379 },
380 {
381 label: this.i18n('Unblock video'),
382 isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
383 handler: abuse => {
384 this.videoBlocklistService.unblockVideo(abuse.video.id)
385 .subscribe(
386 () => {
387 this.notifier.success(this.i18n('Video unblocked.'))
388
389 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
390 },
391
392 err => this.notifier.error(err.message)
393 )
394 }
395 },
396 {
397 label: this.i18n('Delete video'),
398 isDisplayed: abuse => abuse.video && !abuse.video.deleted,
399 handler: async abuse => {
400 const res = await this.confirmService.confirm(
401 this.i18n('Do you really want to delete this video?'),
402 this.i18n('Delete')
403 )
404 if (res === false) return
405
406 this.videoService.removeVideo(abuse.video.id)
407 .subscribe(
408 () => {
409 this.notifier.success(this.i18n('Video deleted.'))
410
411 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
412 },
413
414 err => this.notifier.error(err.message)
415 )
416 }
417 }
418 ]
419 }
420
421 private buildCommentActions (): DropdownAction<ProcessedAbuse>[] {
422 if (!this.isAdminView()) return []
423
424 return [
425 {
426 label: this.i18n('Actions for the comment'),
427 isHeader: true,
428 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted
429 },
430
431 {
432 label: this.i18n('Delete comment'),
433 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted,
434 handler: async abuse => {
435 const res = await this.confirmService.confirm(
436 this.i18n('Do you really want to delete this comment?'),
437 this.i18n('Delete')
438 )
439 if (res === false) return
440
441 this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id)
442 .subscribe(
443 () => {
444 this.notifier.success(this.i18n('Comment deleted.'))
445
446 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
447 },
448
449 err => this.notifier.error(err.message)
450 )
451 }
452 }
453 ]
454 }
455
456 private muteAccountHelper (account: Account) {
457 this.blocklistService.blockAccountByInstance(account)
458 .subscribe(
459 () => {
460 this.notifier.success(
461 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
462 )
463
464 account.mutedByInstance = true
465 },
466
467 err => this.notifier.error(err.message)
468 )
469 }
470
471 private muteServerHelper (host: string) {
472 this.blocklistService.blockServerByInstance(host)
473 .subscribe(
474 () => {
475 this.notifier.success(
476 this.i18n('Server {{host}} muted by the instance.', { host: host })
477 )
478 },
479
480 err => this.notifier.error(err.message)
481 )
482 }
483
484 private toHtml (text: string) {
485 return this.markdownRenderer.textMarkdownToHTML(text)
486 }
487}
diff --git a/client/src/app/shared/shared-moderation/abuse-message-modal.component.html b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html
index 67c6a3081..cb965b71d 100644
--- a/client/src/app/shared/shared-moderation/abuse-message-modal.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html
@@ -1,6 +1,9 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Messages</h4> 3 <h4 class="modal-title">
4 <ng-container i18n *ngIf="isAdminView">Messages with the reporter</ng-container>
5 <ng-container i18n *ngIf="!isAdminView">Messages with the moderation team</ng-container>
6 </h4>
4 7
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> 8 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
6 </div> 9 </div>
@@ -21,9 +24,16 @@
21 </div> 24 </div>
22 </div> 25 </div>
23 26
27 <div class="no-messages" *ngIf="noResults" i18n>
28 No messages for now.
29 </div>
30
24 <form novalidate [formGroup]="form" (ngSubmit)="addMessage()"> 31 <form novalidate [formGroup]="form" (ngSubmit)="addMessage()">
25 <div class="form-group"> 32 <div class="form-group">
26 <textarea formControlName="message" ngbAutofocus [ngClass]="{ 'input-error': formErrors['message'] }" class="form-control"></textarea> 33 <textarea
34 formControlName="message" ngbAutofocus [placeholder]="getPlaceholderMessage()"
35 [ngClass]="{ 'input-error': formErrors['message'] }" class="form-control"
36 ></textarea>
27 37
28 <div *ngIf="formErrors.message" class="form-error"> 38 <div *ngIf="formErrors.message" class="form-error">
29 {{ formErrors.message }} 39 {{ formErrors.message }}
@@ -31,7 +41,7 @@
31 </div> 41 </div>
32 42
33 <div class="form-group inputs"> 43 <div class="form-group inputs">
34 <input type="submit" i18n-value value="Add message" class="action-button-submit" [disabled]="!form.valid || sendingMessage"> 44 <input type="submit" i18n-value value="Add a message" class="action-button-submit" [disabled]="!form.valid || sendingMessage">
35 </div> 45 </div>
36 </form> 46 </form>
37 47
diff --git a/client/src/app/shared/shared-moderation/abuse-message-modal.component.scss b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss
index 89d6b88c1..4dd025fc4 100644
--- a/client/src/app/shared/shared-moderation/abuse-message-modal.component.scss
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss
@@ -3,6 +3,11 @@
3 3
4form { 4form {
5 margin: 20px 20px 0 0; 5 margin: 20px 20px 0 0;
6
7 .form-group:first-child {
8 // Keep place to display error message without modifying the height
9 min-height: 125px;
10 }
6} 11}
7 12
8textarea { 13textarea {
@@ -15,35 +20,29 @@ textarea {
15 display: flex; 20 display: flex;
16 flex-direction: column; 21 flex-direction: column;
17 overflow-y: scroll; 22 overflow-y: scroll;
18 margin-right: 5px; 23}
24
25.no-messages {
26 display: flex;
27 font-size: 15px;
28 justify-content: center;
19} 29}
20 30
21.message-block { 31.message-block {
22 margin-bottom: 10px; 32 margin: 0 5px 10px 0;
23 max-width: 60%; 33 max-width: 60%;
24 34
25 .author { 35 .author {
26 color: var(--greyForegroundColor); 36 color: var(--greyForegroundColor);
27 font-size: 14px; 37 font-size: 14px;
38 padding: 0 0 3px 10px;
28 } 39 }
29 40
30 .bubble { 41 .bubble {
31 color: var(--mainForegroundColor);
32 background-color: var(--greyBackgroundColor);
33 border-radius: 10px; 42 border-radius: 10px;
34 padding: 5px 10px; 43 padding: 5px 10px;
35 44 color: var(--mainForegroundColor);
36 &.by-me { 45 background-color: var(--greyBackgroundColor);
37 color: var(--mainForegroundColor);
38 background-color: var(--secondaryColor);
39 }
40
41 &.by-moderator {
42 color: #fff;
43 background-color: var(--mainColor);
44
45 align-self: flex-end;
46 }
47 46
48 .content { 47 .content {
49 font-size: 15px; 48 font-size: 15px;
@@ -54,4 +53,20 @@ textarea {
54 color: var(--greyForegroundColor); 53 color: var(--greyForegroundColor);
55 } 54 }
56 } 55 }
56
57 &.by-me {
58
59 .bubble {
60 color: var(--mainBackgroundColor);
61 background-color: var(--mainColorLighter);
62
63 .date {
64 color: var(--mainBackgroundColor);
65 }
66 }
67 }
68
69 &.by-moderator {
70 align-self: flex-end;
71 }
57} 72}
diff --git a/client/src/app/shared/shared-moderation/abuse-message-modal.component.ts b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
index 5822dfe1d..03f5ad735 100644
--- a/client/src/app/shared/shared-moderation/abuse-message-modal.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
@@ -1,11 +1,11 @@
1import { Component, ElementRef, EventEmitter, Output, ViewChild, OnInit } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier, AuthService } from '@app/core' 2import { AuthService, Notifier } from '@app/core'
3import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms' 3import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { AbuseMessage, UserAbuse } from '@shared/models' 7import { AbuseMessage, UserAbuse } from '@shared/models'
8import { AbuseService } from './abuse.service' 8import { AbuseService } from '../shared-moderation'
9 9
10@Component({ 10@Component({
11 selector: 'my-abuse-message-modal', 11 selector: 'my-abuse-message-modal',
@@ -16,11 +16,14 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
16 @ViewChild('modal', { static: true }) modal: NgbModal 16 @ViewChild('modal', { static: true }) modal: NgbModal
17 @ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef 17 @ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef
18 18
19 @Input() isAdminView: boolean
20
19 @Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>() 21 @Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>()
20 22
21 abuseMessages: AbuseMessage[] = [] 23 abuseMessages: AbuseMessage[] = []
22 textareaMessage: string 24 textareaMessage: string
23 sendingMessage = false 25 sendingMessage = false
26 noResults = false
24 27
25 private openedModal: NgbModalRef 28 private openedModal: NgbModalRef
26 private abuse: UserAbuse 29 private abuse: UserAbuse
@@ -29,9 +32,9 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
29 protected formValidatorService: FormValidatorService, 32 protected formValidatorService: FormValidatorService,
30 private abuseValidatorsService: AbuseValidatorsService, 33 private abuseValidatorsService: AbuseValidatorsService,
31 private modalService: NgbModal, 34 private modalService: NgbModal,
35 private i18n: I18n,
32 private auth: AuthService, 36 private auth: AuthService,
33 private notifier: Notifier, 37 private notifier: Notifier,
34 private i18n: I18n,
35 private abuseService: AbuseService 38 private abuseService: AbuseService
36 ) { 39 ) {
37 super() 40 super()
@@ -94,11 +97,20 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
94 return this.auth.getUser().account.id === abuseMessage.account.id 97 return this.auth.getUser().account.id === abuseMessage.account.id
95 } 98 }
96 99
100 getPlaceholderMessage () {
101 if (this.isAdminView) {
102 return this.i18n('Add a message to communicate with the reporter')
103 }
104
105 return this.i18n('Add a message to communicate with the moderation team')
106 }
107
97 private loadMessages () { 108 private loadMessages () {
98 this.abuseService.listAbuseMessages(this.abuse) 109 this.abuseService.listAbuseMessages(this.abuse)
99 .subscribe( 110 .subscribe(
100 res => { 111 res => {
101 this.abuseMessages = res.data 112 this.abuseMessages = res.data
113 this.noResults = this.abuseMessages.length === 0
102 114
103 setTimeout(() => { 115 setTimeout(() => {
104 if (!this.messagesBlock) return 116 if (!this.messagesBlock) return
diff --git a/client/src/app/shared/shared-abuse-list/index.ts b/client/src/app/shared/shared-abuse-list/index.ts
new file mode 100644
index 000000000..3bdd18201
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/index.ts
@@ -0,0 +1,7 @@
1export * from './abuse-message-modal.component'
2export * from './abuse-list-table.component'
3export * from './abuse-details.component'
4export * from './moderation-comment-modal.component'
5export * from './processed-abuse.model'
6
7export * from './shared-abuse-list.module'
diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.html
index 8082e93f4..8082e93f4 100644
--- a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html
+++ b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.html
diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.scss
index afcdb9a16..afcdb9a16 100644
--- a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss
+++ b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.scss
diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
index ecb7966bf..ecb7966bf 100644
--- a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts
+++ b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
diff --git a/client/src/app/shared/shared-abuse-list/processed-abuse.model.ts b/client/src/app/shared/shared-abuse-list/processed-abuse.model.ts
new file mode 100644
index 000000000..fce1a8db3
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/processed-abuse.model.ts
@@ -0,0 +1,25 @@
1import { SafeHtml } from '@angular/platform-browser'
2import { AdminAbuse } from '@shared/models'
3import { Account } from '@app/shared/shared-main'
4
5// Don't use an abuse model because we need external services to compute some properties
6// And this model is only used in this component
7export type ProcessedAbuse = AdminAbuse & {
8 moderationCommentHtml?: string,
9 reasonHtml?: string
10 embedHtml?: SafeHtml
11 updatedAt?: Date
12
13 // override bare server-side definitions with rich client-side definitions
14 reporterAccount?: Account
15 flaggedAccount?: Account
16
17 truncatedCommentHtml?: string
18 commentHtml?: string
19
20 video: AdminAbuse['video'] & {
21 channel: AdminAbuse['video']['channel'] & {
22 ownerAccount: Account
23 }
24 }
25}
diff --git a/client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts b/client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts
new file mode 100644
index 000000000..663cd902b
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/shared-abuse-list.module.ts
@@ -0,0 +1,42 @@
1
2import { TableModule } from 'primeng/table'
3import { NgModule } from '@angular/core'
4import { SharedFormModule } from '../shared-forms/shared-form.module'
5import { SharedGlobalIconModule } from '../shared-icons'
6import { SharedMainModule } from '../shared-main/shared-main.module'
7import { SharedModerationModule } from '../shared-moderation'
8import { SharedVideoCommentModule } from '../shared-video-comment'
9import { AbuseDetailsComponent } from './abuse-details.component'
10import { AbuseListTableComponent } from './abuse-list-table.component'
11import { AbuseMessageModalComponent } from './abuse-message-modal.component'
12import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
13
14@NgModule({
15 imports: [
16 TableModule,
17
18 SharedMainModule,
19 SharedFormModule,
20 SharedModerationModule,
21 SharedGlobalIconModule,
22 SharedVideoCommentModule
23 ],
24
25 declarations: [
26 AbuseDetailsComponent,
27 AbuseListTableComponent,
28 ModerationCommentModalComponent,
29 AbuseMessageModalComponent
30 ],
31
32 exports: [
33 AbuseDetailsComponent,
34 AbuseListTableComponent,
35 ModerationCommentModalComponent,
36 AbuseMessageModalComponent
37 ],
38
39 providers: [
40 ]
41})
42export class SharedAbuseListModule { }
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts
index bda88bdee..950e256ff 100644
--- a/client/src/app/shared/shared-main/account/actor.model.ts
+++ b/client/src/app/shared/shared-main/account/actor.model.ts
@@ -41,6 +41,13 @@ export abstract class Actor implements ActorServer {
41 return accountName + '@' + host 41 return accountName + '@' + host
42 } 42 }
43 43
44 static IS_LOCAL (host: string) {
45 const absoluteAPIUrl = getAbsoluteAPIUrl()
46 const thisHost = new URL(absoluteAPIUrl).host
47
48 return host.trim() === thisHost
49 }
50
44 protected constructor (hash: ActorServer) { 51 protected constructor (hash: ActorServer) {
45 this.id = hash.id 52 this.id = hash.id
46 this.url = hash.url 53 this.url = hash.url
@@ -53,10 +60,7 @@ export abstract class Actor implements ActorServer {
53 if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) 60 if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString())
54 61
55 this.avatar = hash.avatar 62 this.avatar = hash.avatar
56 63 this.isLocal = Actor.IS_LOCAL(this.host)
57 const absoluteAPIUrl = getAbsoluteAPIUrl()
58 const thisHost = new URL(absoluteAPIUrl).host
59 this.isLocal = this.host.trim() === thisHost
60 64
61 this.updateComputedAttributes() 65 this.updateComputedAttributes()
62 } 66 }
diff --git a/client/src/app/shared/shared-moderation/abuse.service.ts b/client/src/app/shared/shared-moderation/abuse.service.ts
index 652d8370f..c1aa62023 100644
--- a/client/src/app/shared/shared-moderation/abuse.service.ts
+++ b/client/src/app/shared/shared-moderation/abuse.service.ts
@@ -5,13 +5,24 @@ import { catchError, map } from 'rxjs/operators'
5import { HttpClient, HttpParams } from '@angular/common/http' 5import { HttpClient, HttpParams } from '@angular/common/http'
6import { Injectable } from '@angular/core' 6import { Injectable } from '@angular/core'
7import { RestExtractor, RestPagination, RestService } from '@app/core' 7import { RestExtractor, RestPagination, RestService } from '@app/core'
8import { AdminAbuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList, UserAbuse, AbuseMessage } from '@shared/models'
9import { environment } from '../../../environments/environment'
10import { I18n } from '@ngx-translate/i18n-polyfill' 8import { I18n } from '@ngx-translate/i18n-polyfill'
9import {
10 AbuseCreate,
11 AbuseFilter,
12 AbuseMessage,
13 AbusePredefinedReasonsString,
14 AbuseState,
15 AbuseUpdate,
16 AdminAbuse,
17 ResultList,
18 UserAbuse
19} from '@shared/models'
20import { environment } from '../../../environments/environment'
11 21
12@Injectable() 22@Injectable()
13export class AbuseService { 23export class AbuseService {
14 private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses' 24 private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses'
25 private static BASE_MY_ABUSE_URL = environment.apiUrl + '/api/v1/users/me/abuses'
15 26
16 constructor ( 27 constructor (
17 private i18n: I18n, 28 private i18n: I18n,
@@ -32,33 +43,7 @@ export class AbuseService {
32 params = this.restService.addRestGetParams(params, pagination, sort) 43 params = this.restService.addRestGetParams(params, pagination, sort)
33 44
34 if (search) { 45 if (search) {
35 const filters = this.restService.parseQueryStringFilter(search, { 46 params = this.buildParamsFromSearch(search, params)
36 id: { prefix: '#' },
37 state: {
38 prefix: 'state:',
39 handler: v => {
40 if (v === 'accepted') return AbuseState.ACCEPTED
41 if (v === 'pending') return AbuseState.PENDING
42 if (v === 'rejected') return AbuseState.REJECTED
43
44 return undefined
45 }
46 },
47 videoIs: {
48 prefix: 'videoIs:',
49 handler: v => {
50 if (v === 'deleted') return v
51 if (v === 'blacklisted') return v
52
53 return undefined
54 }
55 },
56 searchReporter: { prefix: 'reporter:' },
57 searchReportee: { prefix: 'reportee:' },
58 predefinedReason: { prefix: 'tag:' }
59 })
60
61 params = this.restService.addObjectParams(params, filters)
62 } 47 }
63 48
64 return this.authHttp.get<ResultList<AdminAbuse>>(url, { params }) 49 return this.authHttp.get<ResultList<AdminAbuse>>(url, { params })
@@ -67,6 +52,27 @@ export class AbuseService {
67 ) 52 )
68 } 53 }
69 54
55 getUserAbuses (options: {
56 pagination: RestPagination,
57 sort: SortMeta,
58 search?: string
59 }): Observable<ResultList<UserAbuse>> {
60 const { pagination, sort, search } = options
61 const url = AbuseService.BASE_MY_ABUSE_URL
62
63 let params = new HttpParams()
64 params = this.restService.addRestGetParams(params, pagination, sort)
65
66 if (search) {
67 params = this.buildParamsFromSearch(search, params)
68 }
69
70 return this.authHttp.get<ResultList<UserAbuse>>(url, { params })
71 .pipe(
72 catchError(res => this.restExtractor.handleError(res))
73 )
74 }
75
70 reportVideo (parameters: AbuseCreate) { 76 reportVideo (parameters: AbuseCreate) {
71 const url = AbuseService.BASE_ABUSE_URL 77 const url = AbuseService.BASE_ABUSE_URL
72 78
@@ -180,4 +186,33 @@ export class AbuseService {
180 return reasons 186 return reasons
181 } 187 }
182 188
189 private buildParamsFromSearch (search: string, params: HttpParams) {
190 const filters = this.restService.parseQueryStringFilter(search, {
191 id: { prefix: '#' },
192 state: {
193 prefix: 'state:',
194 handler: v => {
195 if (v === 'accepted') return AbuseState.ACCEPTED
196 if (v === 'pending') return AbuseState.PENDING
197 if (v === 'rejected') return AbuseState.REJECTED
198
199 return undefined
200 }
201 },
202 videoIs: {
203 prefix: 'videoIs:',
204 handler: v => {
205 if (v === 'deleted') return v
206 if (v === 'blacklisted') return v
207
208 return undefined
209 }
210 },
211 searchReporter: { prefix: 'reporter:' },
212 searchReportee: { prefix: 'reportee:' },
213 predefinedReason: { prefix: 'tag:' }
214 })
215
216 return this.restService.addObjectParams(params, filters)
217 }
183} 218}
diff --git a/client/src/app/shared/shared-moderation/index.ts b/client/src/app/shared/shared-moderation/index.ts
index c8082d4b3..41c910ffe 100644
--- a/client/src/app/shared/shared-moderation/index.ts
+++ b/client/src/app/shared/shared-moderation/index.ts
@@ -1,6 +1,5 @@
1export * from './report-modals' 1export * from './report-modals'
2 2
3export * from './abuse-message-modal.component'
4export * from './abuse.service' 3export * from './abuse.service'
5export * from './account-block.model' 4export * from './account-block.model'
6export * from './account-blocklist.component' 5export * from './account-blocklist.component'
diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss
new file mode 100644
index 000000000..260346dc5
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/moderation.scss
@@ -0,0 +1,50 @@
1@import 'variables';
2@import 'mixins';
3@import 'miniature';
4
5.moderation-expanded {
6 font-size: 90%;
7
8 .moderation-expanded-label {
9 font-weight: $font-semibold;
10 display: inline-block;
11 vertical-align: top;
12 text-align: right;
13 }
14
15 .moderation-expanded-text {
16 display: inline-flex;
17 word-wrap: break-word;
18
19 ::ng-deep p:last-child {
20 margin-bottom: 0px !important;
21 }
22 }
23}
24
25.input-group {
26 @include peertube-input-group(300px);
27
28 .dropdown-toggle::after {
29 margin-left: 0;
30 }
31}
32
33.chip {
34 @include chip;
35}
36
37.caption {
38 justify-content: flex-end;
39
40 input {
41 @include peertube-input-text(250px);
42 flex-grow: 1;
43 }
44}
45
46my-action-dropdown.show {
47 ::ng-deep .dropdown-root {
48 display: block !important;
49 }
50}
diff --git a/client/src/app/shared/shared-moderation/server-blocklist.component.scss b/client/src/app/shared/shared-moderation/server-blocklist.component.scss
index 9ddb76850..31db4d92b 100644
--- a/client/src/app/shared/shared-moderation/server-blocklist.component.scss
+++ b/client/src/app/shared/shared-moderation/server-blocklist.component.scss
@@ -32,3 +32,16 @@ a {
32.block-button { 32.block-button {
33 @include create-button; 33 @include create-button;
34} 34}
35
36.caption {
37 justify-content: flex-end;
38
39 input {
40 @include peertube-input-text(250px);
41 flex-grow: 1;
42 }
43}
44
45.chip {
46 @include chip;
47}
diff --git a/client/src/app/shared/shared-moderation/shared-moderation.module.ts b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
index b5b6daf27..b1b98f8d0 100644
--- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts
+++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
@@ -4,7 +4,6 @@ import { SharedFormModule } from '../shared-forms/shared-form.module'
4import { SharedGlobalIconModule } from '../shared-icons' 4import { SharedGlobalIconModule } from '../shared-icons'
5import { SharedMainModule } from '../shared-main/shared-main.module' 5import { SharedMainModule } from '../shared-main/shared-main.module'
6import { SharedVideoCommentModule } from '../shared-video-comment' 6import { SharedVideoCommentModule } from '../shared-video-comment'
7import { AbuseMessageModalComponent } from './abuse-message-modal.component'
8import { AbuseService } from './abuse.service' 7import { AbuseService } from './abuse.service'
9import { BatchDomainsModalComponent } from './batch-domains-modal.component' 8import { BatchDomainsModalComponent } from './batch-domains-modal.component'
10import { BlocklistService } from './blocklist.service' 9import { BlocklistService } from './blocklist.service'
@@ -30,8 +29,7 @@ import { VideoBlockService } from './video-block.service'
30 VideoReportComponent, 29 VideoReportComponent,
31 BatchDomainsModalComponent, 30 BatchDomainsModalComponent,
32 CommentReportComponent, 31 CommentReportComponent,
33 AccountReportComponent, 32 AccountReportComponent
34 AbuseMessageModalComponent
35 ], 33 ],
36 34
37 exports: [ 35 exports: [
@@ -41,8 +39,7 @@ import { VideoBlockService } from './video-block.service'
41 VideoReportComponent, 39 VideoReportComponent,
42 BatchDomainsModalComponent, 40 BatchDomainsModalComponent,
43 CommentReportComponent, 41 CommentReportComponent,
44 AccountReportComponent, 42 AccountReportComponent
45 AbuseMessageModalComponent
46 ], 43 ],
47 44
48 providers: [ 45 providers: [