aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+admin/moderation/abuse-list
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-07-01 16:05:30 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-07-10 14:02:41 +0200
commitd95d15598847c7f020aa056e7e6e0c02d2bbf732 (patch)
treea8a593f1269688caf9e5f99559996f346290fec5 /client/src/app/+admin/moderation/abuse-list
parent72493e44e9b455a04c4f093ed6c6ffa300b98d8b (diff)
downloadPeerTube-d95d15598847c7f020aa056e7e6e0c02d2bbf732.tar.gz
PeerTube-d95d15598847c7f020aa056e7e6e0c02d2bbf732.tar.zst
PeerTube-d95d15598847c7f020aa056e7e6e0c02d2bbf732.zip
Use 3 tables to represent abuses
Diffstat (limited to 'client/src/app/+admin/moderation/abuse-list')
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-details.component.html93
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts52
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.html149
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss23
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts329
-rw-r--r--client/src/app/+admin/moderation/abuse-list/index.ts3
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html38
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss6
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts70
9 files changed, 763 insertions, 0 deletions
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html
new file mode 100644
index 000000000..d031ea8ed
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html
@@ -0,0 +1,93 @@
1<div class="d-flex moderation-expanded">
2 <!-- report left part (report details) -->
3 <div class="col-8">
4
5 <!-- report metadata -->
6 <div class="d-flex">
7 <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
8 <span class="col-9 moderation-expanded-text">
9 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" class="chip">
10 <img
11 class="avatar"
12 [src]="abuse.reporterAccount.avatar?.path"
13 (error)="switchToDefaultAvatar($event)"
14 alt="Avatar"
15 >
16 <div>
17 <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
18 </div>
19 </a>
20 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
21 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
22 </a>
23 </span>
24 </div>
25
26 <div class="d-flex">
27 <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
28 <span class="col-9 moderation-expanded-text">
29 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.video.channel.ownerAccount.displayName + '&quot;' }" class="chip">
30 <img
31 class="avatar"
32 [src]="abuse.video.channel.ownerAccount?.avatar?.path"
33 (error)="switchToDefaultAvatar($event)"
34 alt="Avatar"
35 >
36 <div>
37 <span class="text-muted">{{ abuse.video.channel.ownerAccount ? abuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
38 </div>
39 </a>
40 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.video.channel.ownerAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
41 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
42 </a>
43 </span>
44 </div>
45
46 <div class="d-flex" *ngIf="abuse.updatedAt">
47 <span class="col-3 moderation-expanded-label" i18n>Updated</span>
48 <time class="col-9 moderation-expanded-text video-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time>
49 </div>
50
51 <!-- report text -->
52 <div class="mt-3 d-flex">
53 <span class="col-3 moderation-expanded-label">
54 <ng-container i18n>Report</ng-container>
55 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
56 </span>
57 <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
58 </div>
59
60 <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
61 <span class="col-3"></span>
62 <span class="col-9">
63 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" *ngFor="let reason of getPredefinedReasons()">
64 <div>{{ reason.label }}</div>
65 </a>
66 </span>
67 </div>
68
69 <div *ngIf="abuse.startAt" class="mt-2 d-flex">
70 <span class="col-3 moderation-expanded-label" i18n>Reported part</span>
71 <span class="col-9">
72 {{ startAt }}<ng-container *ngIf="abuse.endAt"> - {{ endAt }}</ng-container>
73 </span>
74 </div>
75
76 <div class="mt-3 d-flex" *ngIf="abuse.moderationComment">
77 <span class="col-3 moderation-expanded-label" i18n>Note</span>
78 <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.moderationCommentHtml"></span>
79 </div>
80
81 </div>
82
83 <!-- report right part (video details) -->
84 <div class="col-4">
85 <div class="screenratio">
86 <div *ngIf="abuse.video.deleted || abuse.video.blacklisted">
87 <span i18n *ngIf="abuse.video.deleted">The video was deleted</span>
88 <span i18n *ngIf="!abuse.video.deleted">The video was blocked</span>
89 </div>
90 <div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div>
91 </div>
92 </div>
93</div>
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts
new file mode 100644
index 000000000..8f87630b8
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts
@@ -0,0 +1,52 @@
1import { Component, Input } from '@angular/core'
2import { Actor } from '@app/shared/shared-main'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { AbusePredefinedReasonsString } from '@shared/models'
5import { ProcessedAbuse } from './abuse-list.component'
6import { durationToString } from '@app/helpers'
7
8@Component({
9 selector: 'my-abuse-details',
10 templateUrl: './abuse-details.component.html',
11 styleUrls: [ '../moderation.component.scss' ]
12})
13export class AbuseDetailsComponent {
14 @Input() abuse: ProcessedAbuse
15
16 private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
17
18 constructor (
19 private i18n: I18n
20 ) {
21 this.predefinedReasonsTranslations = {
22 violentOrRepulsive: this.i18n('Violent or Repulsive'),
23 hatefulOrAbusive: this.i18n('Hateful or Abusive'),
24 spamOrMisleading: this.i18n('Spam or Misleading'),
25 privacy: this.i18n('Privacy'),
26 rights: this.i18n('Rights'),
27 serverRules: this.i18n('Server rules'),
28 thumbnails: this.i18n('Thumbnails'),
29 captions: this.i18n('Captions')
30 }
31 }
32
33 get startAt () {
34 return durationToString(this.abuse.startAt)
35 }
36
37 get endAt () {
38 return durationToString(this.abuse.endAt)
39 }
40
41 getPredefinedReasons () {
42 if (!this.abuse.predefinedReasons) return []
43 return this.abuse.predefinedReasons.map(r => ({
44 id: r,
45 label: this.predefinedReasonsTranslations[r]
46 }))
47 }
48
49 switchToDefaultAvatar ($event: Event) {
50 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
51 }
52}
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
new file mode 100644
index 000000000..167f32fe6
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
@@ -0,0 +1,149 @@
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"
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 style="width: 20%;" pResizableColumn i18n>Reporter</th>
41 <th i18n>Video</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 style="width: 150px;"></th>
45 </tr>
46 </ng-template>
47
48 <ng-template pTemplate="body" let-expanded="expanded" let-abuse>
49 <tr>
50 <td class="c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
51 <span class="expander">
52 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
53 </span>
54 </td>
55
56 <td>
57 <a [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
58 <div class="chip two-lines">
59 <img
60 class="avatar"
61 [src]="abuse.reporterAccount.avatar?.path"
62 (error)="switchToDefaultAvatar($event)"
63 alt="Avatar"
64 >
65 <div>
66 {{ abuse.reporterAccount.displayName }}
67 <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
68 </div>
69 </div>
70 </a>
71 </td>
72
73 <td *ngIf="!abuse.video.deleted">
74 <a [href]="getVideoUrl(abuse)" class="video-table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
75 <div class="video-table-video">
76 <div class="video-table-video-image">
77 <img [src]="abuse.video.thumbnailPath">
78 <span
79 class="video-table-video-image-label" *ngIf="abuse.count > 1"
80 i18n-title title="This video has been reported multiple times."
81 >
82 {{ abuse.nth }}/{{ abuse.count }}
83 </span>
84 </div>
85 <div class="video-table-video-text">
86 <div>
87 <span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
88 <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
89 {{ abuse.video.name }}
90 </div>
91 <div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
92 </div>
93 </div>
94 </a>
95 </td>
96
97 <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse">
98 <div class="video-table-video" i18n-title title="Video was deleted">
99 <div class="video-table-video-image">
100 <span i18n>Deleted</span>
101 </div>
102 <div class="video-table-video-text">
103 <div>
104 {{ abuse.video.name }}
105 <span class="glyphicon glyphicon-trash"></span>
106 </div>
107 <div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
108 </div>
109 </div>
110 </td>
111
112 <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
113
114 <td class="c-hand video-abuse-states" [pRowToggler]="abuse">
115 <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
116 <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
117 <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
118 </td>
119
120 <td class="action-cell">
121 <my-action-dropdown
122 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
123 i18n-label label="Actions" [actions]="abuseActions" [entry]="abuse"
124 ></my-action-dropdown>
125 </td>
126 </tr>
127 </ng-template>
128
129 <ng-template pTemplate="rowexpansion" let-abuse>
130 <tr>
131 <td class="expand-cell" colspan="6">
132 <my-abuse-details [abuse]="abuse"></my-abuse-details>
133 </td>
134 </tr>
135 </ng-template>
136
137 <ng-template pTemplate="emptymessage">
138 <tr>
139 <td colspan="6">
140 <div class="no-results">
141 <ng-container *ngIf="search" i18n>No video abuses found matching current filters.</ng-container>
142 <ng-container *ngIf="!search" i18n>No video abuses found.</ng-container>
143 </div>
144 </td>
145 </tr>
146 </ng-template>
147</p-table>
148
149<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-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
new file mode 100644
index 000000000..8eee15b64
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss
@@ -0,0 +1,23 @@
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.video-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}
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
new file mode 100644
index 000000000..427ec4d5d
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
@@ -0,0 +1,329 @@
1import { SortMeta } from 'primeng/api'
2import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
3import { environment } from 'src/environments/environment'
4import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
5import { DomSanitizer } from '@angular/platform-browser'
6import { ActivatedRoute, Params, Router } from '@angular/router'
7import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
8import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { Abuse, AbuseState } from '@shared/models'
12import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
13
14export type ProcessedAbuse = Abuse & {
15 moderationCommentHtml?: string,
16 reasonHtml?: string
17 embedHtml?: string
18 updatedAt?: Date
19
20 // override bare server-side definitions with rich client-side definitions
21 reporterAccount: Account
22
23 video: Abuse['video'] & {
24 channel: Abuse['video']['channel'] & {
25 ownerAccount: Account
26 }
27 }
28}
29
30@Component({
31 selector: 'my-abuse-list',
32 templateUrl: './abuse-list.component.html',
33 styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ]
34})
35export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit {
36 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
37
38 abuses: ProcessedAbuse[] = []
39 totalRecords = 0
40 sort: SortMeta = { field: 'createdAt', order: 1 }
41 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
42
43 abuseActions: DropdownAction<Abuse>[][] = []
44
45 constructor (
46 private notifier: Notifier,
47 private abuseService: AbuseService,
48 private blocklistService: BlocklistService,
49 private videoService: VideoService,
50 private videoBlocklistService: VideoBlockService,
51 private confirmService: ConfirmService,
52 private i18n: I18n,
53 private markdownRenderer: MarkdownService,
54 private sanitizer: DomSanitizer,
55 private route: ActivatedRoute,
56 private router: Router
57 ) {
58 super()
59
60 this.abuseActions = [
61 [
62 {
63 label: this.i18n('Internal actions'),
64 isHeader: true
65 },
66 {
67 label: this.i18n('Delete report'),
68 handler: abuse => this.removeAbuse(abuse)
69 },
70 {
71 label: this.i18n('Add note'),
72 handler: abuse => this.openModerationCommentModal(abuse),
73 isDisplayed: abuse => !abuse.moderationComment
74 },
75 {
76 label: this.i18n('Update note'),
77 handler: abuse => this.openModerationCommentModal(abuse),
78 isDisplayed: abuse => !!abuse.moderationComment
79 },
80 {
81 label: this.i18n('Mark as accepted'),
82 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
83 isDisplayed: abuse => !this.isAbuseAccepted(abuse)
84 },
85 {
86 label: this.i18n('Mark as rejected'),
87 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
88 isDisplayed: abuse => !this.isAbuseRejected(abuse)
89 }
90 ],
91 [
92 {
93 label: this.i18n('Actions for the video'),
94 isHeader: true,
95 isDisplayed: abuse => !abuse.video.deleted
96 },
97 {
98 label: this.i18n('Block video'),
99 isDisplayed: abuse => !abuse.video.deleted && !abuse.video.blacklisted,
100 handler: abuse => {
101 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
102 .subscribe(
103 () => {
104 this.notifier.success(this.i18n('Video blocked.'))
105
106 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
107 },
108
109 err => this.notifier.error(err.message)
110 )
111 }
112 },
113 {
114 label: this.i18n('Unblock video'),
115 isDisplayed: abuse => !abuse.video.deleted && abuse.video.blacklisted,
116 handler: abuse => {
117 this.videoBlocklistService.unblockVideo(abuse.video.id)
118 .subscribe(
119 () => {
120 this.notifier.success(this.i18n('Video unblocked.'))
121
122 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
123 },
124
125 err => this.notifier.error(err.message)
126 )
127 }
128 },
129 {
130 label: this.i18n('Delete video'),
131 isDisplayed: abuse => !abuse.video.deleted,
132 handler: async abuse => {
133 const res = await this.confirmService.confirm(
134 this.i18n('Do you really want to delete this video?'),
135 this.i18n('Delete')
136 )
137 if (res === false) return
138
139 this.videoService.removeVideo(abuse.video.id)
140 .subscribe(
141 () => {
142 this.notifier.success(this.i18n('Video deleted.'))
143
144 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
145 },
146
147 err => this.notifier.error(err.message)
148 )
149 }
150 }
151 ],
152 [
153 {
154 label: this.i18n('Actions for the reporter'),
155 isHeader: true
156 },
157 {
158 label: this.i18n('Mute reporter'),
159 handler: async abuse => {
160 const account = abuse.reporterAccount as Account
161
162 this.blocklistService.blockAccountByInstance(account)
163 .subscribe(
164 () => {
165 this.notifier.success(
166 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
167 )
168
169 account.mutedByInstance = true
170 },
171
172 err => this.notifier.error(err.message)
173 )
174 }
175 },
176 {
177 label: this.i18n('Mute server'),
178 isDisplayed: abuse => !abuse.reporterAccount.userId,
179 handler: async abuse => {
180 this.blocklistService.blockServerByInstance(abuse.reporterAccount.host)
181 .subscribe(
182 () => {
183 this.notifier.success(
184 this.i18n('Server {{host}} muted by the instance.', { host: abuse.reporterAccount.host })
185 )
186 },
187
188 err => this.notifier.error(err.message)
189 )
190 }
191 }
192 ]
193 ]
194 }
195
196 ngOnInit () {
197 this.initialize()
198
199 this.route.queryParams
200 .subscribe(params => {
201 this.search = params.search || ''
202
203 this.setTableFilter(this.search)
204 this.loadData()
205 })
206 }
207
208 ngAfterViewInit () {
209 if (this.search) this.setTableFilter(this.search)
210 }
211
212 getIdentifier () {
213 return 'AbuseListComponent'
214 }
215
216 openModerationCommentModal (abuse: Abuse) {
217 this.moderationCommentModal.openModal(abuse)
218 }
219
220 onModerationCommentUpdated () {
221 this.loadData()
222 }
223
224 /* Table filter functions */
225 onAbuseSearch (event: Event) {
226 this.onSearch(event)
227 this.setQueryParams((event.target as HTMLInputElement).value)
228 }
229
230 setQueryParams (search: string) {
231 const queryParams: Params = {}
232 if (search) Object.assign(queryParams, { search })
233
234 this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams })
235 }
236
237 resetTableFilter () {
238 this.setTableFilter('')
239 this.setQueryParams('')
240 this.resetSearch()
241 }
242 /* END Table filter functions */
243
244 isAbuseAccepted (abuse: Abuse) {
245 return abuse.state.id === AbuseState.ACCEPTED
246 }
247
248 isAbuseRejected (abuse: Abuse) {
249 return abuse.state.id === AbuseState.REJECTED
250 }
251
252 getVideoUrl (abuse: Abuse) {
253 return Video.buildClientUrl(abuse.video.uuid)
254 }
255
256 getVideoEmbed (abuse: Abuse) {
257 return buildVideoEmbed(
258 buildVideoLink({
259 baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
260 title: false,
261 warningTitle: false,
262 startTime: abuse.startAt,
263 stopTime: abuse.endAt
264 })
265 )
266 }
267
268 switchToDefaultAvatar ($event: Event) {
269 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
270 }
271
272 async removeAbuse (abuse: Abuse) {
273 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
274 if (res === false) return
275
276 this.abuseService.removeAbuse(abuse).subscribe(
277 () => {
278 this.notifier.success(this.i18n('Abuse deleted.'))
279 this.loadData()
280 },
281
282 err => this.notifier.error(err.message)
283 )
284 }
285
286 updateAbuseState (abuse: Abuse, state: AbuseState) {
287 this.abuseService.updateAbuse(abuse, { state })
288 .subscribe(
289 () => this.loadData(),
290
291 err => this.notifier.error(err.message)
292 )
293 }
294
295 protected loadData () {
296 return this.abuseService.getAbuses({
297 pagination: this.pagination,
298 sort: this.sort,
299 search: this.search
300 }).subscribe(
301 async resultList => {
302 this.totalRecords = resultList.total
303 const abuses = []
304
305 for (const abuse of resultList.data) {
306 Object.assign(abuse, {
307 reasonHtml: await this.toHtml(abuse.reason),
308 moderationCommentHtml: await this.toHtml(abuse.moderationComment),
309 embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)),
310 reporterAccount: new Account(abuse.reporterAccount)
311 })
312
313 if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
314 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
315
316 abuses.push(abuse as ProcessedAbuse)
317 }
318
319 this.abuses = abuses
320 },
321
322 err => this.notifier.error(err.message)
323 )
324 }
325
326 private toHtml (text: string) {
327 return this.markdownRenderer.textMarkdownToHTML(text)
328 }
329}
diff --git a/client/src/app/+admin/moderation/abuse-list/index.ts b/client/src/app/+admin/moderation/abuse-list/index.ts
new file mode 100644
index 000000000..c6037dab4
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/index.ts
@@ -0,0 +1,3 @@
1export * from './abuse-details.component'
2export * from './abuse-list.component'
3export * from './moderation-comment-modal.component'
diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html
new file mode 100644
index 000000000..8082e93f4
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html
@@ -0,0 +1,38 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Moderation comment</h4>
4
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
6 </div>
7
8 <div class="modal-body">
9 <form novalidate [formGroup]="form" (ngSubmit)="banUser()">
10 <div class="form-group">
11 <textarea
12 formControlName="moderationComment" ngbAutofocus i18-placeholder placeholder="Comment this report…"
13 [ngClass]="{ 'input-error': formErrors['moderationComment'] }" class="form-control">
14 </textarea>
15 <div *ngIf="formErrors.moderationComment" class="form-error">
16 {{ formErrors.moderationComment }}
17 </div>
18 </div>
19
20 <div class="form-group" i18n>
21 This comment can only be seen by you or the other moderators.
22 </div>
23
24 <div class="form-group inputs">
25 <input
26 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
27 (click)="hide()" (key.enter)="hide()"
28 >
29
30 <input
31 type="submit" i18n-value value="Update this comment" class="action-button-submit"
32 [disabled]="!form.valid"
33 >
34 </div>
35 </form>
36 </div>
37
38</ng-template>
diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss
new file mode 100644
index 000000000..afcdb9a16
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss
@@ -0,0 +1,6 @@
1@import 'variables';
2@import 'mixins';
3
4textarea {
5 @include peertube-textarea(100%, 100px);
6}
diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts
new file mode 100644
index 000000000..23738f9cd
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts
@@ -0,0 +1,70 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms'
4import { AbuseService } from '@app/shared/shared-moderation'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { Abuse } from '@shared/models'
9
10@Component({
11 selector: 'my-moderation-comment-modal',
12 templateUrl: './moderation-comment-modal.component.html',
13 styleUrls: [ './moderation-comment-modal.component.scss' ]
14})
15export class ModerationCommentModalComponent extends FormReactive implements OnInit {
16 @ViewChild('modal', { static: true }) modal: NgbModal
17 @Output() commentUpdated = new EventEmitter<string>()
18
19 private abuseToComment: Abuse
20 private openedModal: NgbModalRef
21
22 constructor (
23 protected formValidatorService: FormValidatorService,
24 private modalService: NgbModal,
25 private notifier: Notifier,
26 private abuseService: AbuseService,
27 private abuseValidatorsService: AbuseValidatorsService,
28 private i18n: I18n
29 ) {
30 super()
31 }
32
33 ngOnInit () {
34 this.buildForm({
35 moderationComment: this.abuseValidatorsService.ABUSE_MODERATION_COMMENT
36 })
37 }
38
39 openModal (abuseToComment: Abuse) {
40 this.abuseToComment = abuseToComment
41 this.openedModal = this.modalService.open(this.modal, { centered: true })
42
43 this.form.patchValue({
44 moderationComment: this.abuseToComment.moderationComment
45 })
46 }
47
48 hide () {
49 this.abuseToComment = undefined
50 this.openedModal.close()
51 this.form.reset()
52 }
53
54 async banUser () {
55 const moderationComment: string = this.form.value[ 'moderationComment' ]
56
57 this.abuseService.updateAbuse(this.abuseToComment, { moderationComment })
58 .subscribe(
59 () => {
60 this.notifier.success(this.i18n('Comment updated.'))
61
62 this.commentUpdated.emit(moderationComment)
63 this.hide()
64 },
65
66 err => this.notifier.error(err.message)
67 )
68 }
69
70}