aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-07-24 17:21:25 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-07-31 11:35:19 +0200
commit441e453ae53e491b09c9b09b00b041788176ce64 (patch)
tree6104afc6b8344b39ec95211ed236ed784895d65d
parentedbc9325462ddf4536775871ebc25e06f46612d1 (diff)
downloadPeerTube-441e453ae53e491b09c9b09b00b041788176ce64.tar.gz
PeerTube-441e453ae53e491b09c9b09b00b041788176ce64.tar.zst
PeerTube-441e453ae53e491b09c9b09b00b041788176ce64.zip
Add abuse message management in admin
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.html8
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss9
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts50
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts6
-rw-r--r--client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts10
-rw-r--r--client/src/app/shared/shared-icons/global-icon.component.ts3
-rw-r--r--client/src/app/shared/shared-moderation/abuse-message-modal.component.html40
-rw-r--r--client/src/app/shared/shared-moderation/abuse-message-modal.component.scss57
-rw-r--r--client/src/app/shared/shared-moderation/abuse-message-modal.component.ts115
-rw-r--r--client/src/app/shared/shared-moderation/abuse.service.ts41
-rw-r--r--client/src/app/shared/shared-moderation/index.ts1
-rw-r--r--client/src/app/shared/shared-moderation/shared-moderation.module.ts9
-rw-r--r--client/src/assets/images/feather/message-circle.svg1
-rw-r--r--client/src/sass/application.scss1
-rw-r--r--client/src/sass/include/_variables.scss1
-rw-r--r--server/models/abuse/abuse-message.ts2
-rw-r--r--shared/models/moderation/abuse/abuse-message.model.ts1
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'
8import { ActivatedRoute, Params, Router } from '@angular/router' 8import { ActivatedRoute, Params, Router } from '@angular/router'
9import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' 9import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
10import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' 10import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
11import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' 11import { AbuseService, BlocklistService, VideoBlockService, AbuseMessageModalComponent } from '@app/shared/shared-moderation'
12import { VideoCommentService } from '@app/shared/shared-video-comment' 12import { VideoCommentService } from '@app/shared/shared-video-comment'
13import { I18n } from '@ngx-translate/i18n-polyfill' 13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Abuse, AbuseState } from '@shared/models' 14import { AdminAbuse, AbuseState } from '@shared/models'
15import { ModerationCommentModalComponent } from './moderation-comment-modal.component' 15import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
16 16
17const logger = debug('peertube:moderation:AbuseListComponent') 17const 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
21export type ProcessedAbuse = Abuse & { 21export 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})
46export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit { 46export 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'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { Abuse } from '@shared/models' 8import { 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'
7export class AbuseValidatorsService { 7export 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
71export type GlobalIconName = keyof typeof icons 70export 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
4form {
5 margin: 20px 20px 0 0;
6}
7
8textarea {
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 @@
1import { Component, ElementRef, EventEmitter, Output, ViewChild, OnInit } from '@angular/core'
2import { Notifier, AuthService } from '@app/core'
3import { FormReactive, FormValidatorService, AbuseValidatorsService } 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 './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})
15export 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'
5import { HttpClient, HttpParams } from '@angular/common/http' 5import { HttpClient, HttpParams } from '@angular/common/http'
6import { Injectable } from '@angular/core' 6import { Injectable } from '@angular/core'
7import { RestExtractor, RestPagination, RestService } from '@app/core' 7import { RestExtractor, RestPagination, RestService } from '@app/core'
8import { Abuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList } from '@shared/models' 8import { AdminAbuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList, UserAbuse, AbuseMessage } from '@shared/models'
9import { environment } from '../../../environments/environment' 9import { environment } from '../../../environments/environment'
10import { I18n } from '@ngx-translate/i18n-polyfill' 10import { 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 @@
1export * from './report-modals' 1export * from './report-modals'
2 2
3export * from './abuse-message-modal.component'
3export * from './abuse.service' 4export * from './abuse.service'
4export * from './account-block.model' 5export * from './account-block.model'
5export * from './account-blocklist.component' 6export * 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'
4import { SharedGlobalIconModule } from '../shared-icons' 4import { SharedGlobalIconModule } from '../shared-icons'
5import { SharedMainModule } from '../shared-main/shared-main.module' 5import { SharedMainModule } from '../shared-main/shared-main.module'
6import { SharedVideoCommentModule } from '../shared-video-comment' 6import { SharedVideoCommentModule } from '../shared-video-comment'
7import { AbuseMessageModalComponent } from './abuse-message-modal.component'
7import { AbuseService } from './abuse.service' 8import { AbuseService } from './abuse.service'
8import { BatchDomainsModalComponent } from './batch-domains-modal.component' 9import { BatchDomainsModalComponent } from './batch-domains-modal.component'
9import { BlocklistService } from './blocklist.service' 10import { BlocklistService } from './blocklist.service'
10import { BulkService } from './bulk.service' 11import { BulkService } from './bulk.service'
12import { AccountReportComponent, CommentReportComponent, VideoReportComponent } from './report-modals'
11import { UserBanModalComponent } from './user-ban-modal.component' 13import { UserBanModalComponent } from './user-ban-modal.component'
12import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' 14import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
13import { VideoBlockComponent } from './video-block.component' 15import { VideoBlockComponent } from './video-block.component'
14import { VideoBlockService } from './video-block.service' 16import { VideoBlockService } from './video-block.service'
15import { 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}