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 /client/src/app/shared | |
parent | edbc9325462ddf4536775871ebc25e06f46612d1 (diff) | |
download | PeerTube-441e453ae53e491b09c9b09b00b041788176ce64.tar.gz PeerTube-441e453ae53e491b09c9b09b00b041788176ce64.tar.zst PeerTube-441e453ae53e491b09c9b09b00b041788176ce64.zip |
Add abuse message management in admin
Diffstat (limited to 'client/src/app/shared')
8 files changed, 265 insertions, 11 deletions
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: [ |