From 8ca56654a176ee8f350d31282c6cac4a59f58499 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 9 Jul 2020 11:58:46 +0200 Subject: [PATCH] Add ability to report comments in front end --- client/src/app/+admin/admin.component.ts | 4 +- .../abuse-list/abuse-details.component.html | 54 +++++++---- .../abuse-list/abuse-details.component.ts | 5 +- .../abuse-list/abuse-list.component.html | 95 ++++++++++-------- .../abuse-list/abuse-list.component.scss | 2 +- .../abuse-list/abuse-list.component.ts | 77 ++++++++++----- .../moderation/moderation.component.scss | 41 ++++++-- .../+admin/moderation/moderation.routes.ts | 2 +- .../comment/video-comment.component.html | 5 + .../comment/video-comment.component.ts | 27 +++++- .../shared/shared-main/account/actor.model.ts | 6 +- .../shared/shared-moderation/abuse.service.ts | 96 +++++++++++++++---- .../comment-report.component.html | 62 ++++++++++++ .../comment-report.component.scss | 11 +++ .../comment-report.component.ts | 93 ++++++++++++++++++ .../shared-moderation.module.ts | 7 +- .../user-moderation-dropdown.component.ts | 7 ++ .../video-report.component.html | 7 +- .../video-report.component.ts | 43 +-------- server/models/abuse/abuse.ts | 4 +- server/models/video/video-comment.ts | 2 +- shared/models/moderation/abuse/abuse.model.ts | 1 + 22 files changed, 488 insertions(+), 163 deletions(-) create mode 100644 client/src/app/shared/shared-moderation/comment-report.component.html create mode 100644 client/src/app/shared/shared-moderation/comment-report.component.scss create mode 100644 client/src/app/shared/shared-moderation/comment-report.component.ts diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index 87ed33a45..4345d1945 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts @@ -47,8 +47,8 @@ export class AdminComponent implements OnInit { if (this.hasAbusesRight()) { moderationItems.children.push({ - label: this.i18n('Video reports'), - routerLink: '/admin/moderation/video-abuses/list', + label: this.i18n('Reports'), + routerLink: '/admin/moderation/abuses/list', iconName: 'flag' }) } diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html index d031ea8ed..cba9cfb73 100644 --- a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html +++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html @@ -3,10 +3,13 @@
- -
+
Reportee - + Avatar
- {{ abuse.video.channel.ownerAccount ? abuse.video.channel.ownerAccount.nameWithHost : '' }} + {{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}
- + + {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}
@@ -45,7 +56,7 @@
Updated - +
@@ -60,34 +71,45 @@ -
+
Reported part - {{ startAt }} - {{ endAt }} + {{ startAt }} - {{ endAt }}
Note - +
- +
-
-
+
+
The video was deleted The video was blocked
+
+ +
+
+ Comment: +
+ +
+
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts index 8f87630b8..fb0f65764 100644 --- a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts +++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts @@ -31,15 +31,16 @@ export class AbuseDetailsComponent { } get startAt () { - return durationToString(this.abuse.startAt) + return durationToString(this.abuse.video.startAt) } get endAt () { - return durationToString(this.abuse.endAt) + return durationToString(this.abuse.video.endAt) } getPredefinedReasons () { if (!this.abuse.predefinedReasons) return [] + return this.abuse.predefinedReasons.map(r => ({ id: r, label: this.predefinedReasonsTranslations[r] 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 333438269..1ad73e38a 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 @@ -38,7 +38,7 @@ Reporter - Video + Video/Comment/Account Created State @@ -54,7 +54,7 @@ - +
{{ abuse.reporterAccount.displayName }} - {{ abuse.reporterAccount.nameWithHost }} + {{ abuse.reporterAccount.nameWithHost }}
+ + + Deleted account + - - -
-
- - - {{ abuse.nth }}/{{ abuse.count }} - -
-
- - - - -
-
- Deleted -
-
-
- {{ abuse.video.name }} - +
+
+ + + {{ abuse.video.name }} +
+
by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }}
+
+
+ + + + +
+
+ Deleted +
+ +
+
+ {{ abuse.video.name }} + +
+
by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }}
-
by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }}
-
- + + + + + + + + + + {{ abuse.createdAt | date: 'short' }} - + 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 8eee15b64..c22f98c47 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 @@ -10,7 +10,7 @@ @include disable-default-a-behaviour; } -.video-abuse-states .glyphicon-comment { +.abuse-states .glyphicon-comment { margin-left: 0.5rem; } 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 427ec4d5d..1ea61ed37 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 @@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api' import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' import { environment } from 'src/environments/environment' import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core' -import { DomSanitizer } from '@angular/platform-browser' +import { DomSanitizer, SafeHtml } from '@angular/platform-browser' import { ActivatedRoute, Params, Router } from '@angular/router' import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' @@ -10,15 +10,20 @@ import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/s import { I18n } from '@ngx-translate/i18n-polyfill' import { Abuse, AbuseState } from '@shared/models' import { ModerationCommentModalComponent } from './moderation-comment-modal.component' +import truncate from 'lodash-es/truncate' export type ProcessedAbuse = Abuse & { moderationCommentHtml?: string, reasonHtml?: string - embedHtml?: string + embedHtml?: SafeHtml updatedAt?: Date // override bare server-side definitions with rich client-side definitions - reporterAccount: Account + reporterAccount?: Account + flaggedAccount?: Account + + truncatedCommentHtml?: string + commentHtml?: string video: Abuse['video'] & { channel: Abuse['video']['channel'] & { @@ -92,11 +97,11 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn { label: this.i18n('Actions for the video'), isHeader: true, - isDisplayed: abuse => !abuse.video.deleted + isDisplayed: abuse => abuse.video && !abuse.video.deleted }, { label: this.i18n('Block video'), - isDisplayed: abuse => !abuse.video.deleted && !abuse.video.blacklisted, + isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted, handler: abuse => { this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true) .subscribe( @@ -112,7 +117,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn }, { label: this.i18n('Unblock video'), - isDisplayed: abuse => !abuse.video.deleted && abuse.video.blacklisted, + isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted, handler: abuse => { this.videoBlocklistService.unblockVideo(abuse.video.id) .subscribe( @@ -128,7 +133,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn }, { label: this.i18n('Delete video'), - isDisplayed: abuse => !abuse.video.deleted, + isDisplayed: abuse => abuse.video && !abuse.video.deleted, handler: async abuse => { const res = await this.confirmService.confirm( this.i18n('Do you really want to delete this video?'), @@ -152,10 +157,12 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn [ { label: this.i18n('Actions for the reporter'), - isHeader: true + isHeader: true, + isDisplayed: abuse => !!abuse.reporterAccount }, { label: this.i18n('Mute reporter'), + isDisplayed: abuse => !!abuse.reporterAccount, handler: async abuse => { const account = abuse.reporterAccount as Account @@ -175,7 +182,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn }, { label: this.i18n('Mute server'), - isDisplayed: abuse => !abuse.reporterAccount.userId, + isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId, handler: async abuse => { this.blocklistService.blockServerByInstance(abuse.reporterAccount.host) .subscribe( @@ -231,7 +238,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn const queryParams: Params = {} if (search) Object.assign(queryParams, { search }) - this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams }) + this.router.navigate([ '/admin/moderation/abuses/list' ], { queryParams }) } resetTableFilter () { @@ -253,6 +260,10 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn return Video.buildClientUrl(abuse.video.uuid) } + getCommentUrl (abuse: Abuse) { + return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId + } + getVideoEmbed (abuse: Abuse) { return buildVideoEmbed( buildVideoLink({ @@ -300,23 +311,45 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn }).subscribe( async resultList => { this.totalRecords = resultList.total - const abuses = [] - for (const abuse of resultList.data) { - Object.assign(abuse, { - reasonHtml: await this.toHtml(abuse.reason), - moderationCommentHtml: await this.toHtml(abuse.moderationComment), - embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)), - reporterAccount: new Account(abuse.reporterAccount) - }) + this.abuses = [] + + for (const a of resultList.data) { + const abuse = a as ProcessedAbuse + + abuse.reasonHtml = await this.toHtml(abuse.reason) + abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment) + + if (abuse.video) { + abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)) + + if (abuse.video.channel?.ownerAccount) { + abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) + } + } + + if (abuse.comment) { + if (abuse.comment.deleted) { + abuse.truncatedCommentHtml = abuse.commentHtml = this.i18n('Deleted comment') + } else { + const truncated = truncate(abuse.comment.text, { length: 100 }) + abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true) + abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true) + } + } + + if (abuse.reporterAccount) { + abuse.reporterAccount = new Account(abuse.reporterAccount) + } + + if (abuse.flaggedAccount) { + abuse.flaggedAccount = new Account(abuse.flaggedAccount) + } - if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt - abuses.push(abuse as ProcessedAbuse) + this.abuses.push(abuse) } - - this.abuses = abuses }, err => this.notifier.error(err.message) diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss index 0ec420af9..f73c71dc5 100644 --- a/client/src/app/+admin/moderation/moderation.component.scss +++ b/client/src/app/+admin/moderation/moderation.component.scss @@ -25,18 +25,18 @@ vertical-align: top; text-align: right; } - + .moderation-expanded-text { display: inline-flex; word-wrap: break-word; - + ::ng-deep p:last-child { margin-bottom: 0px !important; } } } -.video-table-states { +.table-states { & > :not(:first-child) { margin-left: .4rem; } @@ -59,6 +59,7 @@ p-calendar { .screenratio { div { @include miniature-thumbnail; + display: inline-flex; justify-content: center; align-items: center; @@ -72,6 +73,11 @@ p-calendar { }; } +.comment-html { + background-color: #ececec; + padding: 10px; +} + .chip { @include chip; } @@ -83,16 +89,32 @@ my-action-dropdown.show { } -.video-table-video-link { +.table-video-link { @include disable-outline; + position: relative; top: 3px; } -.video-table-video { +.table-comment-link { + @include disable-outline; + + color: var(--mainForegroundColor); + + ::ng-deep p:last-child { + margin: 0; + } +} + +.comment-flagged-account { + font-size: 11px; + color: var(--greyForegroundColor); +} + +.table-video { display: inline-flex; - .video-table-video-image { + .table-video-image { @include miniature-thumbnail; $image-height: 45px; @@ -118,7 +140,7 @@ my-action-dropdown.show { color: pvar(--inputPlaceholderColor); } - .video-table-video-image-label { + .table-video-image-label { @include static-thumbnail-overlay; position: absolute; border-radius: 3px; @@ -130,7 +152,7 @@ my-action-dropdown.show { } } - .video-table-video-text { + .table-video-text { display: inline-flex; flex-direction: column; justify-content: center; @@ -145,7 +167,8 @@ my-action-dropdown.show { } div + div { - font-size: 80%; + color: var(--greyForegroundColor); + font-size: 11px; } } } diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts index 1e207e5e8..8a31a54dc 100644 --- a/client/src/app/+admin/moderation/moderation.routes.ts +++ b/client/src/app/+admin/moderation/moderation.routes.ts @@ -33,7 +33,7 @@ export const ModerationRoutes: Routes = [ data: { userRight: UserRight.MANAGE_ABUSES, meta: { - title: 'Video reports' + title: 'Reports' } } }, diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/comment/video-comment.component.html index 002de57e4..f02ea549a 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment.component.html +++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.html @@ -45,6 +45,7 @@
Delete
@@ -93,3 +94,7 @@
+ + + + diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts index 27846c1ad..2a4a6e737 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts +++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts @@ -1,7 +1,10 @@ -import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' + +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild } from '@angular/core' import { MarkdownService, Notifier, UserService } from '@app/core' import { AuthService } from '@app/core/auth' -import { Account, Actor, Video } from '@app/shared/shared-main' +import { Account, Actor, DropdownAction, Video } from '@app/shared/shared-main' +import { CommentReportComponent } from '@app/shared/shared-moderation/comment-report.component' +import { I18n } from '@ngx-translate/i18n-polyfill' import { User, UserRight } from '@shared/models' import { VideoCommentThreadTree } from './video-comment-thread-tree.model' import { VideoComment } from './video-comment.model' @@ -12,6 +15,8 @@ import { VideoComment } from './video-comment.model' styleUrls: ['./video-comment.component.scss'] }) export class VideoCommentComponent implements OnInit, OnChanges { + @ViewChild('commentReportModal') commentReportModal: CommentReportComponent + @Input() video: Video @Input() comment: VideoComment @Input() parentComments: VideoComment[] = [] @@ -26,6 +31,8 @@ export class VideoCommentComponent implements OnInit, OnChanges { @Output() resetReply = new EventEmitter() @Output() timestampClicked = new EventEmitter() + prependModerationActions: DropdownAction[] + sanitizedCommentHTML = '' newParentComments: VideoComment[] = [] @@ -33,6 +40,7 @@ export class VideoCommentComponent implements OnInit, OnChanges { commentUser: User constructor ( + private i18n: I18n, private markdownService: MarkdownService, private authService: AuthService, private userService: UserService, @@ -127,5 +135,20 @@ export class VideoCommentComponent implements OnInit, OnChanges { } else { this.comment.account = null } + + if (this.isUserLoggedIn()) { + this.prependModerationActions = [ + { + label: this.i18n('Report comment'), + handler: () => this.showReportModal() + } + ] + } else { + this.prependModerationActions = undefined + } + } + + private showReportModal () { + this.commentReportModal.show() } } diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts index 0fa161ce6..9ec6dbab1 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts @@ -46,8 +46,10 @@ export abstract class Actor implements ActorServer { this.host = hash.host this.followingCount = hash.followingCount this.followersCount = hash.followersCount - this.createdAt = new Date(hash.createdAt.toString()) - this.updatedAt = new Date(hash.updatedAt.toString()) + + if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) + if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) + this.avatar = hash.avatar this.updateComputedAttributes() diff --git a/client/src/app/shared/shared-moderation/abuse.service.ts b/client/src/app/shared/shared-moderation/abuse.service.ts index f45018d5c..95ac16955 100644 --- a/client/src/app/shared/shared-moderation/abuse.service.ts +++ b/client/src/app/shared/shared-moderation/abuse.service.ts @@ -5,18 +5,20 @@ import { catchError, map } from 'rxjs/operators' import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { RestExtractor, RestPagination, RestService } from '@app/core' -import { AbuseUpdate, ResultList, Abuse, AbuseCreate, AbuseState } from '@shared/models' +import { Abuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList } from '@shared/models' import { environment } from '../../../environments/environment' +import { I18n } from '@ngx-translate/i18n-polyfill' @Injectable() export class AbuseService { private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses' constructor ( + private i18n: I18n, private authHttp: HttpClient, private restService: RestService, private restExtractor: RestExtractor - ) {} + ) { } getAbuses (options: { pagination: RestPagination, @@ -24,7 +26,7 @@ export class AbuseService { search?: string }): Observable> { const { pagination, sort, search } = options - const url = AbuseService.BASE_ABUSE_URL + 'abuse' + const url = AbuseService.BASE_ABUSE_URL let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) @@ -60,39 +62,93 @@ export class AbuseService { } return this.authHttp.get>(url, { params }) - .pipe( - catchError(res => this.restExtractor.handleError(res)) - ) + .pipe( + catchError(res => this.restExtractor.handleError(res)) + ) } reportVideo (parameters: AbuseCreate) { const url = AbuseService.BASE_ABUSE_URL - const body = omit(parameters, [ 'id' ]) + const body = omit(parameters, ['id']) return this.authHttp.post(url, body) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(res => this.restExtractor.handleError(res)) - ) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) } updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) { const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id return this.authHttp.put(url, abuseUpdate) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(res => this.restExtractor.handleError(res)) - ) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) } removeAbuse (abuse: Abuse) { const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id return this.authHttp.delete(url) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(res => this.restExtractor.handleError(res)) - ) - }} + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + getPrefefinedReasons (type: AbuseFilter) { + let reasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [ + { + id: 'violentOrRepulsive', + label: this.i18n('Violent or repulsive'), + help: this.i18n('Contains offensive, violent, or coarse language or iconography.') + }, + { + id: 'hatefulOrAbusive', + label: this.i18n('Hateful or abusive'), + help: this.i18n('Contains abusive, racist or sexist language or iconography.') + }, + { + id: 'spamOrMisleading', + label: this.i18n('Spam, ad or false news'), + help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.') + }, + { + id: 'privacy', + label: this.i18n('Privacy breach or doxxing'), + help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).') + }, + { + id: 'rights', + label: this.i18n('Intellectual property violation'), + help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.') + }, + { + id: 'serverRules', + label: this.i18n('Breaks server rules'), + description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.') + } + ] + + if (type === 'video') { + reasons = reasons.concat([ + { + id: 'thumbnails', + label: this.i18n('Thumbnails'), + help: this.i18n('The above can only be seen in thumbnails.') + }, + { + id: 'captions', + label: this.i18n('Captions'), + help: this.i18n('The above can only be seen in captions (please describe which).') + } + ]) + } + + return reasons + } + +} diff --git a/client/src/app/shared/shared-moderation/comment-report.component.html b/client/src/app/shared/shared-moderation/comment-report.component.html new file mode 100644 index 000000000..1105b3788 --- /dev/null +++ b/client/src/app/shared/shared-moderation/comment-report.component.html @@ -0,0 +1,62 @@ + + + + + diff --git a/client/src/app/shared/shared-moderation/comment-report.component.scss b/client/src/app/shared/shared-moderation/comment-report.component.scss new file mode 100644 index 000000000..17a33d3a2 --- /dev/null +++ b/client/src/app/shared/shared-moderation/comment-report.component.scss @@ -0,0 +1,11 @@ +@import 'variables'; +@import 'mixins'; + +.information { + margin-bottom: 20px; +} + +textarea { + @include peertube-textarea(100%, 100px); +} + diff --git a/client/src/app/shared/shared-moderation/comment-report.component.ts b/client/src/app/shared/shared-moderation/comment-report.component.ts new file mode 100644 index 000000000..5db4b2dc1 --- /dev/null +++ b/client/src/app/shared/shared-moderation/comment-report.component.ts @@ -0,0 +1,93 @@ +import { mapValues, pickBy } from 'lodash-es' +import { Component, Input, OnInit, ViewChild } from '@angular/core' +import { SafeHtml } from '@angular/platform-browser' +import { VideoComment } from '@app/+videos/+video-watch/comment/video-comment.model' +import { Notifier } from '@app/core' +import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models' +import { AbuseService } from './abuse.service' + +@Component({ + selector: 'my-comment-report', + templateUrl: './comment-report.component.html', + styleUrls: [ './comment-report.component.scss' ] +}) +export class CommentReportComponent extends FormReactive implements OnInit { + @Input() comment: VideoComment = null + + @ViewChild('modal', { static: true }) modal: NgbModal + + error: string = null + predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] + embedHtml: SafeHtml + + private openedModal: NgbModalRef + + constructor ( + protected formValidatorService: FormValidatorService, + private modalService: NgbModal, + private abuseValidatorsService: AbuseValidatorsService, + private abuseService: AbuseService, + private notifier: Notifier, + private i18n: I18n + ) { + super() + } + + get currentHost () { + return window.location.host + } + + get originHost () { + if (this.isRemoteComment()) { + return this.comment.account.host + } + + return '' + } + + ngOnInit () { + this.buildForm({ + reason: this.abuseValidatorsService.ABUSE_REASON, + predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null) + }) + + this.predefinedReasons = this.abuseService.getPrefefinedReasons('comment') + } + + show () { + this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' }) + } + + hide () { + this.openedModal.close() + this.openedModal = null + } + + report () { + const reason = this.form.get('reason').value + const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[] + + this.abuseService.reportVideo({ + reason, + predefinedReasons, + comment: { + id: this.comment.id + } + }).subscribe( + () => { + this.notifier.success(this.i18n('Comment reported.')) + this.hide() + }, + + err => this.notifier.error(err.message) + ) + } + + isRemoteComment () { + return !this.comment.isLocal + } +} 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 742193e58..ff4021a33 100644 --- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts +++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts @@ -12,6 +12,7 @@ import { AbuseService } from './abuse.service' import { VideoBlockComponent } from './video-block.component' import { VideoBlockService } from './video-block.service' import { VideoReportComponent } from './video-report.component' +import { CommentReportComponent } from './comment-report.component' @NgModule({ imports: [ @@ -25,7 +26,8 @@ import { VideoReportComponent } from './video-report.component' UserModerationDropdownComponent, VideoBlockComponent, VideoReportComponent, - BatchDomainsModalComponent + BatchDomainsModalComponent, + CommentReportComponent ], exports: [ @@ -33,7 +35,8 @@ import { VideoReportComponent } from './video-report.component' UserModerationDropdownComponent, VideoBlockComponent, VideoReportComponent, - BatchDomainsModalComponent + BatchDomainsModalComponent, + CommentReportComponent ], providers: [ diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts index d3c37f082..78c2658df 100644 --- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts @@ -16,6 +16,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { @Input() user: User @Input() account: Account + @Input() prependActions: DropdownAction<{ user: User, account: Account }>[] @Input() buttonSize: 'normal' | 'small' = 'normal' @Input() placement = 'left-top left-bottom auto' @@ -250,6 +251,12 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { private buildActions () { this.userActions = [] + if (this.prependActions) { + this.userActions = [ + this.prependActions + ] + } + if (this.authService.isLoggedIn()) { const authUser = this.authService.getUser() diff --git a/client/src/app/shared/shared-moderation/video-report.component.html b/client/src/app/shared/shared-moderation/video-report.component.html index d6beb6d2a..b724ecb18 100644 --- a/client/src/app/shared/shared-moderation/video-report.component.html +++ b/client/src/app/shared/shared-moderation/video-report.component.html @@ -14,16 +14,19 @@
+
- +
+
+
@@ -73,7 +76,7 @@
- diff --git a/client/src/app/shared/shared-moderation/video-report.component.ts b/client/src/app/shared/shared-moderation/video-report.component.ts index 7977e4cca..26e7b62ba 100644 --- a/client/src/app/shared/shared-moderation/video-report.component.ts +++ b/client/src/app/shared/shared-moderation/video-report.component.ts @@ -79,48 +79,7 @@ export class VideoReportComponent extends FormReactive implements OnInit { } }) - this.predefinedReasons = [ - { - id: 'violentOrRepulsive', - label: this.i18n('Violent or repulsive'), - help: this.i18n('Contains offensive, violent, or coarse language or iconography.') - }, - { - id: 'hatefulOrAbusive', - label: this.i18n('Hateful or abusive'), - help: this.i18n('Contains abusive, racist or sexist language or iconography.') - }, - { - id: 'spamOrMisleading', - label: this.i18n('Spam, ad or false news'), - help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.') - }, - { - id: 'privacy', - label: this.i18n('Privacy breach or doxxing'), - help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).') - }, - { - id: 'rights', - label: this.i18n('Intellectual property violation'), - help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.') - }, - { - id: 'serverRules', - label: this.i18n('Breaks server rules'), - description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.') - }, - { - id: 'thumbnails', - label: this.i18n('Thumbnails'), - help: this.i18n('The above can only be seen in thumbnails.') - }, - { - id: 'captions', - label: this.i18n('Captions'), - help: this.i18n('The above can only be seen in captions (please describe which).') - } - ] + this.predefinedReasons = this.abuseService.getPrefefinedReasons('video') this.embedHtml = this.getVideoEmbed() } diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index dffd503b3..bd96cf79c 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts @@ -140,7 +140,7 @@ export enum ScopeNames { model: VideoModel.unscoped(), include: [ { - attributes: [ 'filename', 'fileUrl' ], + attributes: [ 'filename', 'fileUrl', 'type' ], model: ThumbnailModel }, { @@ -395,6 +395,8 @@ export class AbuseModel extends Model { comment = { id: entity.id, + threadId: entity.getThreadId(), + text: entity.text ?? '', deleted: entity.isDeleted(), diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index fa4d13c3b..75b914b8c 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -655,7 +655,7 @@ export class VideoCommentModel extends Model { id: this.id, url: this.url, text: this.text, - threadId: this.originCommentId || this.id, + threadId: this.getThreadId(), inReplyToCommentId: this.inReplyToCommentId || null, videoId: this.videoId, createdAt: this.createdAt, diff --git a/shared/models/moderation/abuse/abuse.model.ts b/shared/models/moderation/abuse/abuse.model.ts index e241dbd81..74798ab2c 100644 --- a/shared/models/moderation/abuse/abuse.model.ts +++ b/shared/models/moderation/abuse/abuse.model.ts @@ -25,6 +25,7 @@ export interface VideoAbuse { export interface VideoCommentAbuse { id: number + threadId: number video: { id: number