aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/shared-abuse-list
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-07-27 11:40:30 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-07-31 11:35:19 +0200
commit94148c9028829b5576a5dcbfba2c7fb9cf6443d3 (patch)
tree2774f272329111abd03e8441ff936da11fb1a3f3 /client/src/app/shared/shared-abuse-list
parent441e453ae53e491b09c9b09b00b041788176ce64 (diff)
downloadPeerTube-94148c9028829b5576a5dcbfba2c7fb9cf6443d3.tar.gz
PeerTube-94148c9028829b5576a5dcbfba2c7fb9cf6443d3.tar.zst
PeerTube-94148c9028829b5576a5dcbfba2c7fb9cf6443d3.zip
Add abuse messages management in my account
Diffstat (limited to 'client/src/app/shared/shared-abuse-list')
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.html115
-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.ts55
-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.scss107
-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.html50
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss72
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts127
-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.html38
-rw-r--r--client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.scss6
-rw-r--r--client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts70
-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
15 files changed, 1429 insertions, 0 deletions
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
new file mode 100644
index 000000000..431fdf5aa
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
@@ -0,0 +1,115 @@
1<div class="d-flex moderation-expanded">
2 <!-- report left part (report details) -->
3 <div class="col-8">
4
5 <!-- report metadata -->
6 <div class="d-flex" *ngIf="isAdminView && abuse.reporterAccount">
7 <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
8
9 <span class="col-9 moderation-expanded-text">
10 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
11 class="chip"
12 >
13 <img
14 class="avatar"
15 [src]="abuse.reporterAccount.avatar?.path"
16 (error)="switchToDefaultAvatar($event)"
17 alt="Avatar"
18 >
19 <div>
20 <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
21 </div>
22 </a>
23
24 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
25 class="ml-auto text-muted abuse-details-links" i18n
26 >
27 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
28 </a>
29 </span>
30 </div>
31
32 <div class="d-flex" *ngIf="abuse.flaggedAccount">
33 <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
34 <span class="col-9 moderation-expanded-text">
35 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
36 class="chip"
37 >
38 <img
39 class="avatar"
40 [src]="abuse.flaggedAccount?.avatar?.path"
41 (error)="switchToDefaultAvatar($event)"
42 alt="Avatar"
43 >
44 <div>
45 <span class="text-muted">{{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}</span>
46 </div>
47 </a>
48
49 <a *ngIf="isAdminView" [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
50 class="ml-auto text-muted abuse-details-links" i18n
51 >
52 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
53 </a>
54 </span>
55 </div>
56
57 <div class="d-flex" *ngIf="abuse.updatedAt">
58 <span class="col-3 moderation-expanded-label" i18n>Updated</span>
59 <time class="col-9 moderation-expanded-text abuse-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time>
60 </div>
61
62 <!-- report text -->
63 <div class="mt-3 d-flex">
64 <span class="col-3 moderation-expanded-label">
65 <ng-container i18n>Report</ng-container>
66 <a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
67 </span>
68 <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
69 </div>
70
71 <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
72 <span class="col-3"></span>
73 <span class="col-9">
74 <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ baseRoute ]"
75 [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light"
76 >
77 <div>{{ reason.label }}</div>
78 </a>
79 </span>
80 </div>
81
82 <div *ngIf="abuse.video?.startAt" class="mt-2 d-flex">
83 <span class="col-3 moderation-expanded-label" i18n>Reported part</span>
84 <span class="col-9">
85 {{ startAt }}<ng-container *ngIf="abuse.video.endAt"> - {{ endAt }}</ng-container>
86 </span>
87 </div>
88
89 <div class="mt-3 d-flex" *ngIf="isAdminView && abuse.moderationComment">
90 <span class="col-3 moderation-expanded-label" i18n>Note</span>
91 <span class="col-9 moderation-expanded-text d-block" [innerHTML]="abuse.moderationCommentHtml"></span>
92 </div>
93
94 </div>
95
96 <!-- report right part (video/comment details) -->
97 <div class="col-4">
98 <div *ngIf="abuse.video" class="screenratio">
99 <div>
100 <span i18n *ngIf="abuse.video.deleted">The video was deleted</span>
101 <span i18n *ngIf="!abuse.video.deleted">The video was blocked</span>
102 </div>
103
104 <div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div>
105 </div>
106
107 <div *ngIf="abuse.comment" class="comment-html">
108 <div>
109 <strong i18n>Comment:</strong>
110 </div>
111
112 <div [innerHTML]="abuse.commentHtml"></div>
113 </div>
114 </div>
115</div>
diff --git a/client/src/app/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/shared/shared-abuse-list/abuse-details.component.ts b/client/src/app/shared/shared-abuse-list/abuse-details.component.ts
new file mode 100644
index 000000000..cdd4bf2c8
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.ts
@@ -0,0 +1,55 @@
1import { Component, Input } from '@angular/core'
2import { durationToString } from '@app/helpers'
3import { Actor } from '@app/shared/shared-main'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { AbusePredefinedReasonsString } from '@shared/models'
6import { ProcessedAbuse } from './processed-abuse.model'
7
8@Component({
9 selector: 'my-abuse-details',
10 templateUrl: './abuse-details.component.html',
11 styleUrls: [ '../shared-moderation/moderation.scss', './abuse-details.component.scss' ]
12})
13export class AbuseDetailsComponent {
14 @Input() abuse: ProcessedAbuse
15 @Input() isAdminView: boolean
16 @Input() baseRoute: string
17
18 private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
19
20 constructor (
21 private i18n: I18n
22 ) {
23 this.predefinedReasonsTranslations = {
24 violentOrRepulsive: this.i18n('Violent or Repulsive'),
25 hatefulOrAbusive: this.i18n('Hateful or Abusive'),
26 spamOrMisleading: this.i18n('Spam or Misleading'),
27 privacy: this.i18n('Privacy'),
28 rights: this.i18n('Rights'),
29 serverRules: this.i18n('Server rules'),
30 thumbnails: this.i18n('Thumbnails'),
31 captions: this.i18n('Captions')
32 }
33 }
34
35 get startAt () {
36 return durationToString(this.abuse.video.startAt)
37 }
38
39 get endAt () {
40 return durationToString(this.abuse.video.endAt)
41 }
42
43 getPredefinedReasons () {
44 if (!this.abuse.predefinedReasons) return []
45
46 return this.abuse.predefinedReasons.map(r => ({
47 id: r,
48 label: this.predefinedReasonsTranslations[r]
49 }))
50 }
51
52 switchToDefaultAvatar ($event: Event) {
53 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
54 }
55}
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/shared/shared-abuse-list/abuse-list-table.component.scss b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.scss
new file mode 100644
index 000000000..7ed7c9e87
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.scss
@@ -0,0 +1,107 @@
1@import 'variables';
2@import 'mixins';
3@import 'miniature';
4
5.table-video-link {
6 @include disable-outline;
7
8 position: relative;
9 top: 3px;
10}
11
12.table-comment-link,
13.table-account-link {
14 @include disable-outline;
15
16 color: var(--mainForegroundColor);
17
18 ::ng-deep p:last-child {
19 margin: 0;
20 }
21}
22
23.table-account-link {
24 display: flex;
25 flex-direction: column;
26}
27
28.comment-flagged-account,
29.account-flagged-handle {
30 font-size: 11px;
31 color: var(--greyForegroundColor);
32}
33
34.table-video {
35 display: inline-flex;
36
37 .table-video-image {
38 @include miniature-thumbnail;
39
40 $image-height: 45px;
41
42 height: $image-height;
43 width: #{(16/9) * $image-height};
44 margin-right: 0.5rem;
45 border-radius: 2px;
46 border: none;
47 background: transparent;
48 display: inline-flex;
49 justify-content: center;
50 align-items: center;
51 position: relative;
52
53 img {
54 height: 100%;
55 width: 100%;
56 border-radius: 2px;
57 }
58
59 span {
60 color: pvar(--inputPlaceholderColor);
61 }
62
63 .table-video-image-label {
64 @include static-thumbnail-overlay;
65 position: absolute;
66 border-radius: 3px;
67 font-size: 10px;
68 padding: 0 3px;
69 line-height: 1.3;
70 bottom: 2px;
71 right: 2px;
72 }
73 }
74
75 .table-video-text {
76 display: inline-flex;
77 flex-direction: column;
78 justify-content: center;
79 font-size: 90%;
80 color: pvar(--mainForegroundColor);
81 line-height: 1rem;
82
83 div .glyphicon {
84 font-size: 80%;
85 color: gray;
86 margin-left: 0.1rem;
87 }
88
89 div + div {
90 color: var(--greyForegroundColor);
91 font-size: 11px;
92 }
93 }
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-abuse-list/abuse-message-modal.component.html b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html
new file mode 100644
index 000000000..cb965b71d
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html
@@ -0,0 +1,50 @@
1<ng-template #modal>
2 <div class="modal-header">
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>
7
8 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
9 </div>
10
11 <div class="modal-body">
12 <div class="messages" #messagesBlock>
13 <div
14 *ngFor="let message of abuseMessages"
15 class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }"
16 >
17
18 <div class="author">{{ message.account.name }}</div>
19
20 <div class="bubble">
21 <div class="content">{{ message.message }}</div>
22 <div class="date">{{ message.createdAt | date }}</div>
23 </div>
24 </div>
25 </div>
26
27 <div class="no-messages" *ngIf="noResults" i18n>
28 No messages for now.
29 </div>
30
31 <form novalidate [formGroup]="form" (ngSubmit)="addMessage()">
32 <div class="form-group">
33 <textarea
34 formControlName="message" ngbAutofocus [placeholder]="getPlaceholderMessage()"
35 [ngClass]="{ 'input-error': formErrors['message'] }" class="form-control"
36 ></textarea>
37
38 <div *ngIf="formErrors.message" class="form-error">
39 {{ formErrors.message }}
40 </div>
41 </div>
42
43 <div class="form-group inputs">
44 <input type="submit" i18n-value value="Add a message" class="action-button-submit" [disabled]="!form.valid || sendingMessage">
45 </div>
46 </form>
47
48 </div>
49
50</ng-template>
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss
new file mode 100644
index 000000000..4dd025fc4
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss
@@ -0,0 +1,72 @@
1@import 'variables';
2@import 'mixins';
3
4form {
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 }
11}
12
13textarea {
14 @include peertube-textarea(100%, 70px);
15
16 margin-top: 20px;
17}
18
19.messages {
20 display: flex;
21 flex-direction: column;
22 overflow-y: scroll;
23}
24
25.no-messages {
26 display: flex;
27 font-size: 15px;
28 justify-content: center;
29}
30
31.message-block {
32 margin: 0 5px 10px 0;
33 max-width: 60%;
34
35 .author {
36 color: var(--greyForegroundColor);
37 font-size: 14px;
38 padding: 0 0 3px 10px;
39 }
40
41 .bubble {
42 border-radius: 10px;
43 padding: 5px 10px;
44 color: var(--mainForegroundColor);
45 background-color: var(--greyBackgroundColor);
46
47 .content {
48 font-size: 15px;
49 }
50
51 .date {
52 font-size: 13px;
53 color: var(--greyForegroundColor);
54 }
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 }
72}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
new file mode 100644
index 000000000..03f5ad735
--- /dev/null
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
@@ -0,0 +1,127 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { AuthService, Notifier } from '@app/core'
3import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { AbuseMessage, UserAbuse } from '@shared/models'
8import { AbuseService } from '../shared-moderation'
9
10@Component({
11 selector: 'my-abuse-message-modal',
12 templateUrl: './abuse-message-modal.component.html',
13 styleUrls: [ './abuse-message-modal.component.scss' ]
14})
15export class AbuseMessageModalComponent extends FormReactive implements OnInit {
16 @ViewChild('modal', { static: true }) modal: NgbModal
17 @ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef
18
19 @Input() isAdminView: boolean
20
21 @Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>()
22
23 abuseMessages: AbuseMessage[] = []
24 textareaMessage: string
25 sendingMessage = false
26 noResults = false
27
28 private openedModal: NgbModalRef
29 private abuse: UserAbuse
30
31 constructor (
32 protected formValidatorService: FormValidatorService,
33 private abuseValidatorsService: AbuseValidatorsService,
34 private modalService: NgbModal,
35 private i18n: I18n,
36 private auth: AuthService,
37 private notifier: Notifier,
38 private abuseService: AbuseService
39 ) {
40 super()
41 }
42
43 ngOnInit () {
44 this.buildForm({
45 message: this.abuseValidatorsService.ABUSE_MESSAGE
46 })
47 }
48
49 openModal (abuse: UserAbuse) {
50 this.abuse = abuse
51
52 this.openedModal = this.modalService.open(this.modal, { centered: true })
53
54 this.loadMessages()
55 }
56
57 hide () {
58 this.abuseMessages = []
59 this.openedModal.close()
60 }
61
62 addMessage () {
63 this.sendingMessage = true
64
65 this.abuseService.addAbuseMessage(this.abuse, this.form.value['message'])
66 .subscribe(
67 () => {
68 this.form.reset()
69 this.sendingMessage = false
70 this.countMessagesUpdated.emit({ abuseId: this.abuse.id, countMessages: this.abuseMessages.length + 1 })
71
72 this.loadMessages()
73 },
74
75 err => {
76 this.sendingMessage = false
77 console.error(err)
78 this.notifier.error('Sorry but you cannot send this message. Please retry later')
79 }
80 )
81 }
82
83 deleteMessage (abuseMessage: AbuseMessage) {
84 this.abuseService.deleteAbuseMessage(this.abuse, abuseMessage)
85 .subscribe(
86 () => {
87 this.countMessagesUpdated.emit({ abuseId: this.abuse.id, countMessages: this.abuseMessages.length - 1 })
88
89 this.abuseMessages = this.abuseMessages.filter(m => m.id !== abuseMessage.id)
90 },
91
92 err => this.notifier.error(err.message)
93 )
94 }
95
96 isMessageByMe (abuseMessage: AbuseMessage) {
97 return this.auth.getUser().account.id === abuseMessage.account.id
98 }
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
108 private loadMessages () {
109 this.abuseService.listAbuseMessages(this.abuse)
110 .subscribe(
111 res => {
112 this.abuseMessages = res.data
113 this.noResults = this.abuseMessages.length === 0
114
115 setTimeout(() => {
116 if (!this.messagesBlock) return
117
118 const element = this.messagesBlock.nativeElement as HTMLElement
119 element.scrollIntoView({ block: 'end', inline: 'nearest' })
120 })
121 },
122
123 err => this.notifier.error(err.message)
124 )
125 }
126
127}
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/shared/shared-abuse-list/moderation-comment-modal.component.html b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.html
new file mode 100644
index 000000000..8082e93f4
--- /dev/null
+++ b/client/src/app/shared/shared-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/shared/shared-abuse-list/moderation-comment-modal.component.scss b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.scss
new file mode 100644
index 000000000..afcdb9a16
--- /dev/null
+++ b/client/src/app/shared/shared-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/shared/shared-abuse-list/moderation-comment-modal.component.ts b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
new file mode 100644
index 000000000..ecb7966bf
--- /dev/null
+++ b/client/src/app/shared/shared-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 { AdminAbuse } from '@shared/models'
9
10@Component({
11 selector: 'my-moderation-comment-modal',
12 templateUrl: './moderation-comment-modal.component.html',
13 styleUrls: [ './moderation-comment-modal.component.scss' ]
14})
15export class ModerationCommentModalComponent extends FormReactive implements OnInit {
16 @ViewChild('modal', { static: true }) modal: NgbModal
17 @Output() commentUpdated = new EventEmitter<string>()
18
19 private abuseToComment: AdminAbuse
20 private openedModal: NgbModalRef
21
22 constructor (
23 protected formValidatorService: FormValidatorService,
24 private modalService: NgbModal,
25 private notifier: Notifier,
26 private abuseService: AbuseService,
27 private abuseValidatorsService: AbuseValidatorsService,
28 private i18n: I18n
29 ) {
30 super()
31 }
32
33 ngOnInit () {
34 this.buildForm({
35 moderationComment: this.abuseValidatorsService.ABUSE_MODERATION_COMMENT
36 })
37 }
38
39 openModal (abuseToComment: AdminAbuse) {
40 this.abuseToComment = abuseToComment
41 this.openedModal = this.modalService.open(this.modal, { centered: true })
42
43 this.form.patchValue({
44 moderationComment: this.abuseToComment.moderationComment
45 })
46 }
47
48 hide () {
49 this.abuseToComment = undefined
50 this.openedModal.close()
51 this.form.reset()
52 }
53
54 async banUser () {
55 const moderationComment: string = this.form.value[ 'moderationComment' ]
56
57 this.abuseService.updateAbuse(this.abuseToComment, { moderationComment })
58 .subscribe(
59 () => {
60 this.notifier.success(this.i18n('Comment updated.'))
61
62 this.commentUpdated.emit(moderationComment)
63 this.hide()
64 },
65
66 err => this.notifier.error(err.message)
67 )
68 }
69
70}
diff --git a/client/src/app/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 { }