diff options
author | Chocobozzz <me@florianbigard.com> | 2020-07-24 17:21:25 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-07-31 11:35:19 +0200 |
commit | 441e453ae53e491b09c9b09b00b041788176ce64 (patch) | |
tree | 6104afc6b8344b39ec95211ed236ed784895d65d | |
parent | edbc9325462ddf4536775871ebc25e06f46612d1 (diff) | |
download | PeerTube-441e453ae53e491b09c9b09b00b041788176ce64.tar.gz PeerTube-441e453ae53e491b09c9b09b00b041788176ce64.tar.zst PeerTube-441e453ae53e491b09c9b09b00b041788176ce64.zip |
Add abuse message management in admin
17 files changed, 326 insertions, 29 deletions
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 48b31b99c..9fae5667f 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 | |||
@@ -46,6 +46,7 @@ | |||
46 | <th i18n>Video/Comment/Account</th> | 46 | <th i18n>Video/Comment/Account</th> |
47 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></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> | 48 | <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> |
49 | <th i18n style="width: 80px;">Messages</th> | ||
49 | <th style="width: 150px;"></th> | 50 | <th style="width: 150px;"></th> |
50 | </tr> | 51 | </tr> |
51 | </ng-template> | 52 | </ng-template> |
@@ -157,6 +158,12 @@ | |||
157 | <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span> | 158 | <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span> |
158 | </td> | 159 | </td> |
159 | 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 | |||
160 | <td class="action-cell"> | 167 | <td class="action-cell"> |
161 | <my-action-dropdown | 168 | <my-action-dropdown |
162 | [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" | 169 | [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" |
@@ -187,3 +194,4 @@ | |||
187 | </p-table> | 194 | </p-table> |
188 | 195 | ||
189 | <my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal> | 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 index c22f98c47..48536e3c2 100644 --- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss | |||
@@ -21,3 +21,12 @@ | |||
21 | margin-left: 0; | 21 | margin-left: 0; |
22 | } | 22 | } |
23 | } | 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 74c5fe2b3..86121fe58 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 | |||
@@ -8,17 +8,17 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser' | |||
8 | import { ActivatedRoute, Params, Router } from '@angular/router' | 8 | import { ActivatedRoute, Params, Router } from '@angular/router' |
9 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' | 9 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' |
10 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 10 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
11 | import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' | 11 | import { AbuseService, BlocklistService, VideoBlockService, AbuseMessageModalComponent } from '@app/shared/shared-moderation' |
12 | import { VideoCommentService } from '@app/shared/shared-video-comment' | 12 | import { VideoCommentService } from '@app/shared/shared-video-comment' |
13 | import { I18n } from '@ngx-translate/i18n-polyfill' | 13 | import { I18n } from '@ngx-translate/i18n-polyfill' |
14 | import { Abuse, AbuseState } from '@shared/models' | 14 | import { AdminAbuse, AbuseState } from '@shared/models' |
15 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' | 15 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' |
16 | 16 | ||
17 | const logger = debug('peertube:moderation:AbuseListComponent') | 17 | const logger = debug('peertube:moderation:AbuseListComponent') |
18 | 18 | ||
19 | // Don't use an abuse model because we need external services to compute some properties | 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 | 20 | // And this model is only used in this component |
21 | export type ProcessedAbuse = Abuse & { | 21 | export type ProcessedAbuse = AdminAbuse & { |
22 | moderationCommentHtml?: string, | 22 | moderationCommentHtml?: string, |
23 | reasonHtml?: string | 23 | reasonHtml?: string |
24 | embedHtml?: SafeHtml | 24 | embedHtml?: SafeHtml |
@@ -31,8 +31,8 @@ export type ProcessedAbuse = Abuse & { | |||
31 | truncatedCommentHtml?: string | 31 | truncatedCommentHtml?: string |
32 | commentHtml?: string | 32 | commentHtml?: string |
33 | 33 | ||
34 | video: Abuse['video'] & { | 34 | video: AdminAbuse['video'] & { |
35 | channel: Abuse['video']['channel'] & { | 35 | channel: AdminAbuse['video']['channel'] & { |
36 | ownerAccount: Account | 36 | ownerAccount: Account |
37 | } | 37 | } |
38 | } | 38 | } |
@@ -45,6 +45,7 @@ export type ProcessedAbuse = Abuse & { | |||
45 | }) | 45 | }) |
46 | export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit { | 46 | export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit { |
47 | @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent | 47 | @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent |
48 | @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent | ||
48 | 49 | ||
49 | abuses: ProcessedAbuse[] = [] | 50 | abuses: ProcessedAbuse[] = [] |
50 | totalRecords = 0 | 51 | totalRecords = 0 |
@@ -104,7 +105,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
104 | return 'AbuseListComponent' | 105 | return 'AbuseListComponent' |
105 | } | 106 | } |
106 | 107 | ||
107 | openModerationCommentModal (abuse: Abuse) { | 108 | openModerationCommentModal (abuse: AdminAbuse) { |
108 | this.moderationCommentModal.openModal(abuse) | 109 | this.moderationCommentModal.openModal(abuse) |
109 | } | 110 | } |
110 | 111 | ||
@@ -132,19 +133,19 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
132 | } | 133 | } |
133 | /* END Table filter functions */ | 134 | /* END Table filter functions */ |
134 | 135 | ||
135 | isAbuseAccepted (abuse: Abuse) { | 136 | isAbuseAccepted (abuse: AdminAbuse) { |
136 | return abuse.state.id === AbuseState.ACCEPTED | 137 | return abuse.state.id === AbuseState.ACCEPTED |
137 | } | 138 | } |
138 | 139 | ||
139 | isAbuseRejected (abuse: Abuse) { | 140 | isAbuseRejected (abuse: AdminAbuse) { |
140 | return abuse.state.id === AbuseState.REJECTED | 141 | return abuse.state.id === AbuseState.REJECTED |
141 | } | 142 | } |
142 | 143 | ||
143 | getVideoUrl (abuse: Abuse) { | 144 | getVideoUrl (abuse: AdminAbuse) { |
144 | return Video.buildClientUrl(abuse.video.uuid) | 145 | return Video.buildClientUrl(abuse.video.uuid) |
145 | } | 146 | } |
146 | 147 | ||
147 | getCommentUrl (abuse: Abuse) { | 148 | getCommentUrl (abuse: AdminAbuse) { |
148 | return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId | 149 | return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId |
149 | } | 150 | } |
150 | 151 | ||
@@ -152,7 +153,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
152 | return '/accounts/' + abuse.flaggedAccount.nameWithHost | 153 | return '/accounts/' + abuse.flaggedAccount.nameWithHost |
153 | } | 154 | } |
154 | 155 | ||
155 | getVideoEmbed (abuse: Abuse) { | 156 | getVideoEmbed (abuse: AdminAbuse) { |
156 | return buildVideoEmbed( | 157 | return buildVideoEmbed( |
157 | buildVideoLink({ | 158 | buildVideoLink({ |
158 | baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`, | 159 | baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`, |
@@ -168,7 +169,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
168 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() | 169 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() |
169 | } | 170 | } |
170 | 171 | ||
171 | async removeAbuse (abuse: Abuse) { | 172 | async removeAbuse (abuse: AdminAbuse) { |
172 | const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) | 173 | const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) |
173 | if (res === false) return | 174 | if (res === false) return |
174 | 175 | ||
@@ -182,7 +183,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
182 | ) | 183 | ) |
183 | } | 184 | } |
184 | 185 | ||
185 | updateAbuseState (abuse: Abuse, state: AbuseState) { | 186 | updateAbuseState (abuse: AdminAbuse, state: AbuseState) { |
186 | this.abuseService.updateAbuse(abuse, { state }) | 187 | this.abuseService.updateAbuse(abuse, { state }) |
187 | .subscribe( | 188 | .subscribe( |
188 | () => this.loadData(), | 189 | () => this.loadData(), |
@@ -191,10 +192,25 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
191 | ) | 192 | ) |
192 | } | 193 | } |
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 | |||
194 | protected loadData () { | 210 | protected loadData () { |
195 | logger('Load data.') | 211 | logger('Load data.') |
196 | 212 | ||
197 | return this.abuseService.getAbuses({ | 213 | return this.abuseService.getAdminAbuses({ |
198 | pagination: this.pagination, | 214 | pagination: this.pagination, |
199 | sort: this.sort, | 215 | sort: this.sort, |
200 | search: this.search | 216 | search: this.search |
@@ -257,7 +273,11 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
257 | handler: abuse => this.removeAbuse(abuse) | 273 | handler: abuse => this.removeAbuse(abuse) |
258 | }, | 274 | }, |
259 | { | 275 | { |
260 | label: this.i18n('Add note'), | 276 | label: this.i18n('Messages'), |
277 | handler: abuse => this.openAbuseMessagesModal(abuse) | ||
278 | }, | ||
279 | { | ||
280 | label: this.i18n('Add internal note'), | ||
261 | handler: abuse => this.openModerationCommentModal(abuse), | 281 | handler: abuse => this.openModerationCommentModal(abuse), |
262 | isDisplayed: abuse => !abuse.moderationComment | 282 | isDisplayed: abuse => !abuse.moderationComment |
263 | }, | 283 | }, |
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 index 23738f9cd..ecb7966bf 100644 --- 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 | |||
@@ -5,7 +5,7 @@ import { AbuseService } from '@app/shared/shared-moderation' | |||
5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
6 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 6 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { Abuse } from '@shared/models' | 8 | import { AdminAbuse } from '@shared/models' |
9 | 9 | ||
10 | @Component({ | 10 | @Component({ |
11 | selector: 'my-moderation-comment-modal', | 11 | selector: 'my-moderation-comment-modal', |
@@ -16,7 +16,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
16 | @ViewChild('modal', { static: true }) modal: NgbModal | 16 | @ViewChild('modal', { static: true }) modal: NgbModal |
17 | @Output() commentUpdated = new EventEmitter<string>() | 17 | @Output() commentUpdated = new EventEmitter<string>() |
18 | 18 | ||
19 | private abuseToComment: Abuse | 19 | private abuseToComment: AdminAbuse |
20 | private openedModal: NgbModalRef | 20 | private openedModal: NgbModalRef |
21 | 21 | ||
22 | constructor ( | 22 | constructor ( |
@@ -36,7 +36,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
36 | }) | 36 | }) |
37 | } | 37 | } |
38 | 38 | ||
39 | openModal (abuseToComment: Abuse) { | 39 | openModal (abuseToComment: AdminAbuse) { |
40 | this.abuseToComment = abuseToComment | 40 | this.abuseToComment = abuseToComment |
41 | this.openedModal = this.modalService.open(this.modal, { centered: true }) | 41 | this.openedModal = this.modalService.open(this.modal, { centered: true }) |
42 | 42 | ||
diff --git a/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts index 739115e19..5f15963f3 100644 --- a/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts +++ b/client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts | |||
@@ -7,6 +7,7 @@ import { BuildFormValidator } from './form-validator.service' | |||
7 | export class AbuseValidatorsService { | 7 | export class AbuseValidatorsService { |
8 | readonly ABUSE_REASON: BuildFormValidator | 8 | readonly ABUSE_REASON: BuildFormValidator |
9 | readonly ABUSE_MODERATION_COMMENT: BuildFormValidator | 9 | readonly ABUSE_MODERATION_COMMENT: BuildFormValidator |
10 | readonly ABUSE_MESSAGE: BuildFormValidator | ||
10 | 11 | ||
11 | constructor (private i18n: I18n) { | 12 | constructor (private i18n: I18n) { |
12 | this.ABUSE_REASON = { | 13 | this.ABUSE_REASON = { |
@@ -26,5 +27,14 @@ export class AbuseValidatorsService { | |||
26 | 'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.') | 27 | 'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.') |
27 | } | 28 | } |
28 | } | 29 | } |
30 | |||
31 | this.ABUSE_MESSAGE = { | ||
32 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], | ||
33 | MESSAGES: { | ||
34 | 'required': this.i18n('Abuse message is required.'), | ||
35 | 'minlength': this.i18n('Abuse message must be at least 2 characters long.'), | ||
36 | 'maxlength': this.i18n('Abuse message cannot be more than 3000 characters long.') | ||
37 | } | ||
38 | } | ||
29 | } | 39 | } |
30 | } | 40 | } |
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts index c58ef29fa..409681702 100644 --- a/client/src/app/shared/shared-icons/global-icon.component.ts +++ b/client/src/app/shared/shared-icons/global-icon.component.ts | |||
@@ -64,8 +64,7 @@ const icons = { | |||
64 | 'go': require('!!raw-loader?!../../../assets/images/feather/arrow-up-right.svg').default, | 64 | 'go': require('!!raw-loader?!../../../assets/images/feather/arrow-up-right.svg').default, |
65 | 'cross': require('!!raw-loader?!../../../assets/images/feather/x.svg').default, | 65 | 'cross': require('!!raw-loader?!../../../assets/images/feather/x.svg').default, |
66 | 'tick': require('!!raw-loader?!../../../assets/images/feather/check.svg').default, | 66 | 'tick': require('!!raw-loader?!../../../assets/images/feather/check.svg').default, |
67 | 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, | 67 | 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default |
68 | 'columns': require('!!raw-loader?!../../../assets/images/feather/columns.svg').default | ||
69 | } | 68 | } |
70 | 69 | ||
71 | export type GlobalIconName = keyof typeof icons | 70 | export type GlobalIconName = keyof typeof icons |
diff --git a/client/src/app/shared/shared-moderation/abuse-message-modal.component.html b/client/src/app/shared/shared-moderation/abuse-message-modal.component.html new file mode 100644 index 000000000..67c6a3081 --- /dev/null +++ b/client/src/app/shared/shared-moderation/abuse-message-modal.component.html | |||
@@ -0,0 +1,40 @@ | |||
1 | <ng-template #modal> | ||
2 | <div class="modal-header"> | ||
3 | <h4 i18n class="modal-title">Messages</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 | <div class="messages" #messagesBlock> | ||
10 | <div | ||
11 | *ngFor="let message of abuseMessages" | ||
12 | class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }" | ||
13 | > | ||
14 | |||
15 | <div class="author">{{ message.account.name }}</div> | ||
16 | |||
17 | <div class="bubble"> | ||
18 | <div class="content">{{ message.message }}</div> | ||
19 | <div class="date">{{ message.createdAt | date }}</div> | ||
20 | </div> | ||
21 | </div> | ||
22 | </div> | ||
23 | |||
24 | <form novalidate [formGroup]="form" (ngSubmit)="addMessage()"> | ||
25 | <div class="form-group"> | ||
26 | <textarea formControlName="message" ngbAutofocus [ngClass]="{ 'input-error': formErrors['message'] }" class="form-control"></textarea> | ||
27 | |||
28 | <div *ngIf="formErrors.message" class="form-error"> | ||
29 | {{ formErrors.message }} | ||
30 | </div> | ||
31 | </div> | ||
32 | |||
33 | <div class="form-group inputs"> | ||
34 | <input type="submit" i18n-value value="Add message" class="action-button-submit" [disabled]="!form.valid || sendingMessage"> | ||
35 | </div> | ||
36 | </form> | ||
37 | |||
38 | </div> | ||
39 | |||
40 | </ng-template> | ||
diff --git a/client/src/app/shared/shared-moderation/abuse-message-modal.component.scss b/client/src/app/shared/shared-moderation/abuse-message-modal.component.scss new file mode 100644 index 000000000..89d6b88c1 --- /dev/null +++ b/client/src/app/shared/shared-moderation/abuse-message-modal.component.scss | |||
@@ -0,0 +1,57 @@ | |||
1 | @import 'variables'; | ||
2 | @import 'mixins'; | ||
3 | |||
4 | form { | ||
5 | margin: 20px 20px 0 0; | ||
6 | } | ||
7 | |||
8 | textarea { | ||
9 | @include peertube-textarea(100%, 70px); | ||
10 | |||
11 | margin-top: 20px; | ||
12 | } | ||
13 | |||
14 | .messages { | ||
15 | display: flex; | ||
16 | flex-direction: column; | ||
17 | overflow-y: scroll; | ||
18 | margin-right: 5px; | ||
19 | } | ||
20 | |||
21 | .message-block { | ||
22 | margin-bottom: 10px; | ||
23 | max-width: 60%; | ||
24 | |||
25 | .author { | ||
26 | color: var(--greyForegroundColor); | ||
27 | font-size: 14px; | ||
28 | } | ||
29 | |||
30 | .bubble { | ||
31 | color: var(--mainForegroundColor); | ||
32 | background-color: var(--greyBackgroundColor); | ||
33 | border-radius: 10px; | ||
34 | padding: 5px 10px; | ||
35 | |||
36 | &.by-me { | ||
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 | |||
48 | .content { | ||
49 | font-size: 15px; | ||
50 | } | ||
51 | |||
52 | .date { | ||
53 | font-size: 13px; | ||
54 | color: var(--greyForegroundColor); | ||
55 | } | ||
56 | } | ||
57 | } | ||
diff --git a/client/src/app/shared/shared-moderation/abuse-message-modal.component.ts b/client/src/app/shared/shared-moderation/abuse-message-modal.component.ts new file mode 100644 index 000000000..5822dfe1d --- /dev/null +++ b/client/src/app/shared/shared-moderation/abuse-message-modal.component.ts | |||
@@ -0,0 +1,115 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Output, ViewChild, OnInit } from '@angular/core' | ||
2 | import { Notifier, AuthService } from '@app/core' | ||
3 | import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms' | ||
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { AbuseMessage, UserAbuse } from '@shared/models' | ||
8 | import { AbuseService } from './abuse.service' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-abuse-message-modal', | ||
12 | templateUrl: './abuse-message-modal.component.html', | ||
13 | styleUrls: [ './abuse-message-modal.component.scss' ] | ||
14 | }) | ||
15 | export class AbuseMessageModalComponent extends FormReactive implements OnInit { | ||
16 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
17 | @ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef | ||
18 | |||
19 | @Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>() | ||
20 | |||
21 | abuseMessages: AbuseMessage[] = [] | ||
22 | textareaMessage: string | ||
23 | sendingMessage = false | ||
24 | |||
25 | private openedModal: NgbModalRef | ||
26 | private abuse: UserAbuse | ||
27 | |||
28 | constructor ( | ||
29 | protected formValidatorService: FormValidatorService, | ||
30 | private abuseValidatorsService: AbuseValidatorsService, | ||
31 | private modalService: NgbModal, | ||
32 | private auth: AuthService, | ||
33 | private notifier: Notifier, | ||
34 | private i18n: I18n, | ||
35 | private abuseService: AbuseService | ||
36 | ) { | ||
37 | super() | ||
38 | } | ||
39 | |||
40 | ngOnInit () { | ||
41 | this.buildForm({ | ||
42 | message: this.abuseValidatorsService.ABUSE_MESSAGE | ||
43 | }) | ||
44 | } | ||
45 | |||
46 | openModal (abuse: UserAbuse) { | ||
47 | this.abuse = abuse | ||
48 | |||
49 | this.openedModal = this.modalService.open(this.modal, { centered: true }) | ||
50 | |||
51 | this.loadMessages() | ||
52 | } | ||
53 | |||
54 | hide () { | ||
55 | this.abuseMessages = [] | ||
56 | this.openedModal.close() | ||
57 | } | ||
58 | |||
59 | addMessage () { | ||
60 | this.sendingMessage = true | ||
61 | |||
62 | this.abuseService.addAbuseMessage(this.abuse, this.form.value['message']) | ||
63 | .subscribe( | ||
64 | () => { | ||
65 | this.form.reset() | ||
66 | this.sendingMessage = false | ||
67 | this.countMessagesUpdated.emit({ abuseId: this.abuse.id, countMessages: this.abuseMessages.length + 1 }) | ||
68 | |||
69 | this.loadMessages() | ||
70 | }, | ||
71 | |||
72 | err => { | ||
73 | this.sendingMessage = false | ||
74 | console.error(err) | ||
75 | this.notifier.error('Sorry but you cannot send this message. Please retry later') | ||
76 | } | ||
77 | ) | ||
78 | } | ||
79 | |||
80 | deleteMessage (abuseMessage: AbuseMessage) { | ||
81 | this.abuseService.deleteAbuseMessage(this.abuse, abuseMessage) | ||
82 | .subscribe( | ||
83 | () => { | ||
84 | this.countMessagesUpdated.emit({ abuseId: this.abuse.id, countMessages: this.abuseMessages.length - 1 }) | ||
85 | |||
86 | this.abuseMessages = this.abuseMessages.filter(m => m.id !== abuseMessage.id) | ||
87 | }, | ||
88 | |||
89 | err => this.notifier.error(err.message) | ||
90 | ) | ||
91 | } | ||
92 | |||
93 | isMessageByMe (abuseMessage: AbuseMessage) { | ||
94 | return this.auth.getUser().account.id === abuseMessage.account.id | ||
95 | } | ||
96 | |||
97 | private loadMessages () { | ||
98 | this.abuseService.listAbuseMessages(this.abuse) | ||
99 | .subscribe( | ||
100 | res => { | ||
101 | this.abuseMessages = res.data | ||
102 | |||
103 | setTimeout(() => { | ||
104 | if (!this.messagesBlock) return | ||
105 | |||
106 | const element = this.messagesBlock.nativeElement as HTMLElement | ||
107 | element.scrollIntoView({ block: 'end', inline: 'nearest' }) | ||
108 | }) | ||
109 | }, | ||
110 | |||
111 | err => this.notifier.error(err.message) | ||
112 | ) | ||
113 | } | ||
114 | |||
115 | } | ||
diff --git a/client/src/app/shared/shared-moderation/abuse.service.ts b/client/src/app/shared/shared-moderation/abuse.service.ts index 95ac16955..652d8370f 100644 --- a/client/src/app/shared/shared-moderation/abuse.service.ts +++ b/client/src/app/shared/shared-moderation/abuse.service.ts | |||
@@ -5,7 +5,7 @@ import { catchError, map } from 'rxjs/operators' | |||
5 | import { HttpClient, HttpParams } from '@angular/common/http' | 5 | import { HttpClient, HttpParams } from '@angular/common/http' |
6 | import { Injectable } from '@angular/core' | 6 | import { Injectable } from '@angular/core' |
7 | import { RestExtractor, RestPagination, RestService } from '@app/core' | 7 | import { RestExtractor, RestPagination, RestService } from '@app/core' |
8 | import { Abuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList } from '@shared/models' | 8 | import { AdminAbuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList, UserAbuse, AbuseMessage } from '@shared/models' |
9 | import { environment } from '../../../environments/environment' | 9 | import { environment } from '../../../environments/environment' |
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | 11 | ||
@@ -20,11 +20,11 @@ export class AbuseService { | |||
20 | private restExtractor: RestExtractor | 20 | private restExtractor: RestExtractor |
21 | ) { } | 21 | ) { } |
22 | 22 | ||
23 | getAbuses (options: { | 23 | getAdminAbuses (options: { |
24 | pagination: RestPagination, | 24 | pagination: RestPagination, |
25 | sort: SortMeta, | 25 | sort: SortMeta, |
26 | search?: string | 26 | search?: string |
27 | }): Observable<ResultList<Abuse>> { | 27 | }): Observable<ResultList<AdminAbuse>> { |
28 | const { pagination, sort, search } = options | 28 | const { pagination, sort, search } = options |
29 | const url = AbuseService.BASE_ABUSE_URL | 29 | const url = AbuseService.BASE_ABUSE_URL |
30 | 30 | ||
@@ -61,7 +61,7 @@ export class AbuseService { | |||
61 | params = this.restService.addObjectParams(params, filters) | 61 | params = this.restService.addObjectParams(params, filters) |
62 | } | 62 | } |
63 | 63 | ||
64 | return this.authHttp.get<ResultList<Abuse>>(url, { params }) | 64 | return this.authHttp.get<ResultList<AdminAbuse>>(url, { params }) |
65 | .pipe( | 65 | .pipe( |
66 | catchError(res => this.restExtractor.handleError(res)) | 66 | catchError(res => this.restExtractor.handleError(res)) |
67 | ) | 67 | ) |
@@ -79,7 +79,7 @@ export class AbuseService { | |||
79 | ) | 79 | ) |
80 | } | 80 | } |
81 | 81 | ||
82 | updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) { | 82 | updateAbuse (abuse: AdminAbuse, abuseUpdate: AbuseUpdate) { |
83 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id | 83 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id |
84 | 84 | ||
85 | return this.authHttp.put(url, abuseUpdate) | 85 | return this.authHttp.put(url, abuseUpdate) |
@@ -89,7 +89,7 @@ export class AbuseService { | |||
89 | ) | 89 | ) |
90 | } | 90 | } |
91 | 91 | ||
92 | removeAbuse (abuse: Abuse) { | 92 | removeAbuse (abuse: AdminAbuse) { |
93 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id | 93 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id |
94 | 94 | ||
95 | return this.authHttp.delete(url) | 95 | return this.authHttp.delete(url) |
@@ -99,6 +99,35 @@ export class AbuseService { | |||
99 | ) | 99 | ) |
100 | } | 100 | } |
101 | 101 | ||
102 | addAbuseMessage (abuse: UserAbuse, message: string) { | ||
103 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id + '/messages' | ||
104 | |||
105 | return this.authHttp.post(url, { message }) | ||
106 | .pipe( | ||
107 | map(this.restExtractor.extractDataBool), | ||
108 | catchError(res => this.restExtractor.handleError(res)) | ||
109 | ) | ||
110 | } | ||
111 | |||
112 | listAbuseMessages (abuse: UserAbuse) { | ||
113 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id + '/messages' | ||
114 | |||
115 | return this.authHttp.get<ResultList<AbuseMessage>>(url) | ||
116 | .pipe( | ||
117 | catchError(res => this.restExtractor.handleError(res)) | ||
118 | ) | ||
119 | } | ||
120 | |||
121 | deleteAbuseMessage (abuse: UserAbuse, abuseMessage: AbuseMessage) { | ||
122 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id + '/messages/' + abuseMessage.id | ||
123 | |||
124 | return this.authHttp.delete(url) | ||
125 | .pipe( | ||
126 | map(this.restExtractor.extractDataBool), | ||
127 | catchError(res => this.restExtractor.handleError(res)) | ||
128 | ) | ||
129 | } | ||
130 | |||
102 | getPrefefinedReasons (type: AbuseFilter) { | 131 | getPrefefinedReasons (type: AbuseFilter) { |
103 | let reasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [ | 132 | let reasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [ |
104 | { | 133 | { |
diff --git a/client/src/app/shared/shared-moderation/index.ts b/client/src/app/shared/shared-moderation/index.ts index 41c910ffe..c8082d4b3 100644 --- a/client/src/app/shared/shared-moderation/index.ts +++ b/client/src/app/shared/shared-moderation/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export * from './report-modals' | 1 | export * from './report-modals' |
2 | 2 | ||
3 | export * from './abuse-message-modal.component' | ||
3 | export * from './abuse.service' | 4 | export * from './abuse.service' |
4 | export * from './account-block.model' | 5 | export * from './account-block.model' |
5 | export * from './account-blocklist.component' | 6 | export * from './account-blocklist.component' |
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 8fa9ee794..b5b6daf27 100644 --- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts +++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts | |||
@@ -4,15 +4,16 @@ import { SharedFormModule } from '../shared-forms/shared-form.module' | |||
4 | import { SharedGlobalIconModule } from '../shared-icons' | 4 | import { SharedGlobalIconModule } from '../shared-icons' |
5 | import { SharedMainModule } from '../shared-main/shared-main.module' | 5 | import { SharedMainModule } from '../shared-main/shared-main.module' |
6 | import { SharedVideoCommentModule } from '../shared-video-comment' | 6 | import { SharedVideoCommentModule } from '../shared-video-comment' |
7 | import { AbuseMessageModalComponent } from './abuse-message-modal.component' | ||
7 | import { AbuseService } from './abuse.service' | 8 | import { AbuseService } from './abuse.service' |
8 | import { BatchDomainsModalComponent } from './batch-domains-modal.component' | 9 | import { BatchDomainsModalComponent } from './batch-domains-modal.component' |
9 | import { BlocklistService } from './blocklist.service' | 10 | import { BlocklistService } from './blocklist.service' |
10 | import { BulkService } from './bulk.service' | 11 | import { BulkService } from './bulk.service' |
12 | import { AccountReportComponent, CommentReportComponent, VideoReportComponent } from './report-modals' | ||
11 | import { UserBanModalComponent } from './user-ban-modal.component' | 13 | import { UserBanModalComponent } from './user-ban-modal.component' |
12 | import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' | 14 | import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' |
13 | import { VideoBlockComponent } from './video-block.component' | 15 | import { VideoBlockComponent } from './video-block.component' |
14 | import { VideoBlockService } from './video-block.service' | 16 | import { VideoBlockService } from './video-block.service' |
15 | import { VideoReportComponent, AccountReportComponent, CommentReportComponent } from './report-modals' | ||
16 | 17 | ||
17 | @NgModule({ | 18 | @NgModule({ |
18 | imports: [ | 19 | imports: [ |
@@ -29,7 +30,8 @@ import { VideoReportComponent, AccountReportComponent, CommentReportComponent } | |||
29 | VideoReportComponent, | 30 | VideoReportComponent, |
30 | BatchDomainsModalComponent, | 31 | BatchDomainsModalComponent, |
31 | CommentReportComponent, | 32 | CommentReportComponent, |
32 | AccountReportComponent | 33 | AccountReportComponent, |
34 | AbuseMessageModalComponent | ||
33 | ], | 35 | ], |
34 | 36 | ||
35 | exports: [ | 37 | exports: [ |
@@ -39,7 +41,8 @@ import { VideoReportComponent, AccountReportComponent, CommentReportComponent } | |||
39 | VideoReportComponent, | 41 | VideoReportComponent, |
40 | BatchDomainsModalComponent, | 42 | BatchDomainsModalComponent, |
41 | CommentReportComponent, | 43 | CommentReportComponent, |
42 | AccountReportComponent | 44 | AccountReportComponent, |
45 | AbuseMessageModalComponent | ||
43 | ], | 46 | ], |
44 | 47 | ||
45 | providers: [ | 48 | providers: [ |
diff --git a/client/src/assets/images/feather/message-circle.svg b/client/src/assets/images/feather/message-circle.svg new file mode 100644 index 000000000..4b21b32b6 --- /dev/null +++ b/client/src/assets/images/feather/message-circle.svg | |||
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-circle"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg> \ No newline at end of file | |||
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index be510c08f..7ed900574 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss | |||
@@ -32,6 +32,7 @@ body { | |||
32 | --secondaryColor: #{$secondary-color}; | 32 | --secondaryColor: #{$secondary-color}; |
33 | 33 | ||
34 | --greyForegroundColor: #{$grey-foreground-color}; | 34 | --greyForegroundColor: #{$grey-foreground-color}; |
35 | --greyBackgroundColor: #{$grey-background-color}; | ||
35 | 36 | ||
36 | --menuBackgroundColor: #{$menu-background}; | 37 | --menuBackgroundColor: #{$menu-background}; |
37 | --menuForegroundColor: #{$menu-color}; | 38 | --menuForegroundColor: #{$menu-color}; |
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index 7b95bb8cc..130462b89 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss | |||
@@ -92,6 +92,7 @@ $variables: ( | |||
92 | --secondaryColor: var(--secondaryColor), | 92 | --secondaryColor: var(--secondaryColor), |
93 | 93 | ||
94 | --greyForegroundColor: var(--greyForegroundColor), | 94 | --greyForegroundColor: var(--greyForegroundColor), |
95 | --greyBackgroundColor: var(--greyBackgroundColor), | ||
95 | 96 | ||
96 | --menuBackgroundColor: var(--menuBackgroundColor), | 97 | --menuBackgroundColor: var(--menuBackgroundColor), |
97 | --menuForegroundColor: var(--menuForegroundColor), | 98 | --menuForegroundColor: var(--menuForegroundColor), |
diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts index f7721c87d..fce20f7a7 100644 --- a/server/models/abuse/abuse-message.ts +++ b/server/models/abuse/abuse-message.ts | |||
@@ -94,6 +94,8 @@ export class AbuseMessageModel extends Model<AbuseMessageModel> { | |||
94 | 94 | ||
95 | return { | 95 | return { |
96 | id: this.id, | 96 | id: this.id, |
97 | createdAt: this.createdAt, | ||
98 | |||
97 | byModerator: this.byModerator, | 99 | byModerator: this.byModerator, |
98 | message: this.message, | 100 | message: this.message, |
99 | 101 | ||
diff --git a/shared/models/moderation/abuse/abuse-message.model.ts b/shared/models/moderation/abuse/abuse-message.model.ts index 02072d5ce..642496646 100644 --- a/shared/models/moderation/abuse/abuse-message.model.ts +++ b/shared/models/moderation/abuse/abuse-message.model.ts | |||
@@ -4,6 +4,7 @@ export interface AbuseMessage { | |||
4 | id: number | 4 | id: number |
5 | message: string | 5 | message: string |
6 | byModerator: boolean | 6 | byModerator: boolean |
7 | createdAt: Date | string | ||
7 | 8 | ||
8 | account: AccountSummary | 9 | account: AccountSummary |
9 | } | 10 | } |