From d95d15598847c7f020aa056e7e6e0c02d2bbf732 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 1 Jul 2020 16:05:30 +0200 Subject: Use 3 tables to represent abuses --- client/src/app/+admin/admin.component.ts | 2 +- client/src/app/+admin/admin.module.ts | 10 +- .../abuse-list/abuse-details.component.html | 93 ++++++ .../abuse-list/abuse-details.component.ts | 52 ++++ .../abuse-list/abuse-list.component.html | 149 ++++++++++ .../abuse-list/abuse-list.component.scss | 23 ++ .../moderation/abuse-list/abuse-list.component.ts | 329 +++++++++++++++++++++ .../src/app/+admin/moderation/abuse-list/index.ts | 3 + .../moderation-comment-modal.component.html | 38 +++ .../moderation-comment-modal.component.scss | 6 + .../moderation-comment-modal.component.ts | 70 +++++ client/src/app/+admin/moderation/index.ts | 2 +- .../src/app/+admin/moderation/moderation.routes.ts | 15 +- .../+admin/moderation/video-abuse-list/index.ts | 2 - .../moderation-comment-modal.component.html | 38 --- .../moderation-comment-modal.component.scss | 6 - .../moderation-comment-modal.component.ts | 70 ----- .../video-abuse-details.component.html | 93 ------ .../video-abuse-details.component.ts | 52 ---- .../video-abuse-list.component.html | 149 ---------- .../video-abuse-list.component.scss | 23 -- .../video-abuse-list/video-abuse-list.component.ts | 328 -------------------- 22 files changed, 781 insertions(+), 772 deletions(-) create mode 100644 client/src/app/+admin/moderation/abuse-list/abuse-details.component.html create mode 100644 client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts create mode 100644 client/src/app/+admin/moderation/abuse-list/abuse-list.component.html create mode 100644 client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss create mode 100644 client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts create mode 100644 client/src/app/+admin/moderation/abuse-list/index.ts create mode 100644 client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html create mode 100644 client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss create mode 100644 client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/index.ts delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss delete mode 100644 client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts (limited to 'client/src/app/+admin') diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index 6f340884f..1e137e63e 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts @@ -91,7 +91,7 @@ export class AdminComponent implements OnInit { } hasVideoAbusesRight () { - return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES) + return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES) } hasVideoBlocklistRight () { diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 728227a84..c59bd2927 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -14,10 +14,10 @@ import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponen import { FollowingListComponent } from './follows/following-list/following-list.component' import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' -import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlockListComponent } from './moderation' +import { ModerationCommentModalComponent, AbuseListComponent, VideoBlockListComponent } from './moderation' import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' import { ModerationComponent } from './moderation/moderation.component' -import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-abuse-details.component' +import { AbuseDetailsComponent } from './moderation/abuse-list/abuse-details.component' import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component' import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component' import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component' @@ -60,8 +60,10 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom ModerationComponent, VideoBlockListComponent, - VideoAbuseListComponent, - VideoAbuseDetailsComponent, + + AbuseListComponent, + AbuseDetailsComponent, + ModerationCommentModalComponent, InstanceServerBlocklistComponent, InstanceAccountBlocklistComponent, 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 new file mode 100644 index 000000000..d031ea8ed --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html @@ -0,0 +1,93 @@ +
+ + + + +
+
+
+ The video was deleted + The video was blocked +
+
+
+
+
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 new file mode 100644 index 000000000..8f87630b8 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts @@ -0,0 +1,52 @@ +import { Component, Input } from '@angular/core' +import { Actor } from '@app/shared/shared-main' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { AbusePredefinedReasonsString } from '@shared/models' +import { ProcessedAbuse } from './abuse-list.component' +import { durationToString } from '@app/helpers' + +@Component({ + selector: 'my-abuse-details', + templateUrl: './abuse-details.component.html', + styleUrls: [ '../moderation.component.scss' ] +}) +export class AbuseDetailsComponent { + @Input() abuse: ProcessedAbuse + + private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string } + + constructor ( + private i18n: I18n + ) { + this.predefinedReasonsTranslations = { + violentOrRepulsive: this.i18n('Violent or Repulsive'), + hatefulOrAbusive: this.i18n('Hateful or Abusive'), + spamOrMisleading: this.i18n('Spam or Misleading'), + privacy: this.i18n('Privacy'), + rights: this.i18n('Rights'), + serverRules: this.i18n('Server rules'), + thumbnails: this.i18n('Thumbnails'), + captions: this.i18n('Captions') + } + } + + get startAt () { + return durationToString(this.abuse.startAt) + } + + get endAt () { + return durationToString(this.abuse.endAt) + } + + getPredefinedReasons () { + if (!this.abuse.predefinedReasons) return [] + return this.abuse.predefinedReasons.map(r => ({ + id: r, + label: this.predefinedReasonsTranslations[r] + })) + } + + switchToDefaultAvatar ($event: Event) { + ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() + } +} 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 new file mode 100644 index 000000000..167f32fe6 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html @@ -0,0 +1,149 @@ + + +
+
+
+ + + + Clear filters +
+
+
+
+ + + + + Reporter + Video + Created + State + + + + + + + + + + + + + + +
+ Avatar +
+ {{ abuse.reporterAccount.displayName }} + {{ abuse.reporterAccount.nameWithHost }} +
+
+
+ + + + +
+
+ + + {{ abuse.nth }}/{{ abuse.count }} + +
+
+
+ + + {{ 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 }}
+
+
+ + + {{ abuse.createdAt | date: 'short' }} + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ No video abuses found matching current filters. + No video abuses found. +
+ + +
+
+ + 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 new file mode 100644 index 000000000..8eee15b64 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss @@ -0,0 +1,23 @@ +@import 'mixins'; +@import 'miniature'; + +.video-details-date-updated { + font-size: 90%; + margin-top: .1rem; +} + +.video-details-links { + @include disable-default-a-behaviour; +} + +.video-abuse-states .glyphicon-comment { + margin-left: 0.5rem; +} + +.input-group { + @include peertube-input-group(300px); + + .dropdown-toggle::after { + margin-left: 0; + } +} 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 new file mode 100644 index 000000000..427ec4d5d --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts @@ -0,0 +1,329 @@ +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 { 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' +import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { Abuse, AbuseState } from '@shared/models' +import { ModerationCommentModalComponent } from './moderation-comment-modal.component' + +export type ProcessedAbuse = Abuse & { + moderationCommentHtml?: string, + reasonHtml?: string + embedHtml?: string + updatedAt?: Date + + // override bare server-side definitions with rich client-side definitions + reporterAccount: Account + + video: Abuse['video'] & { + channel: Abuse['video']['channel'] & { + ownerAccount: Account + } + } +} + +@Component({ + selector: 'my-abuse-list', + templateUrl: './abuse-list.component.html', + styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ] +}) +export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit { + @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent + + abuses: ProcessedAbuse[] = [] + totalRecords = 0 + sort: SortMeta = { field: 'createdAt', order: 1 } + pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + + abuseActions: DropdownAction[][] = [] + + constructor ( + private notifier: Notifier, + private abuseService: AbuseService, + private blocklistService: BlocklistService, + private videoService: VideoService, + private videoBlocklistService: VideoBlockService, + private confirmService: ConfirmService, + private i18n: I18n, + private markdownRenderer: MarkdownService, + private sanitizer: DomSanitizer, + private route: ActivatedRoute, + private router: Router + ) { + super() + + this.abuseActions = [ + [ + { + label: this.i18n('Internal actions'), + isHeader: true + }, + { + label: this.i18n('Delete report'), + handler: abuse => this.removeAbuse(abuse) + }, + { + label: this.i18n('Add note'), + handler: abuse => this.openModerationCommentModal(abuse), + isDisplayed: abuse => !abuse.moderationComment + }, + { + label: this.i18n('Update note'), + handler: abuse => this.openModerationCommentModal(abuse), + isDisplayed: abuse => !!abuse.moderationComment + }, + { + label: this.i18n('Mark as accepted'), + handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED), + isDisplayed: abuse => !this.isAbuseAccepted(abuse) + }, + { + label: this.i18n('Mark as rejected'), + handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED), + isDisplayed: abuse => !this.isAbuseRejected(abuse) + } + ], + [ + { + label: this.i18n('Actions for the video'), + isHeader: true, + isDisplayed: abuse => !abuse.video.deleted + }, + { + label: this.i18n('Block video'), + isDisplayed: abuse => !abuse.video.deleted && !abuse.video.blacklisted, + handler: abuse => { + this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true) + .subscribe( + () => { + this.notifier.success(this.i18n('Video blocked.')) + + this.updateAbuseState(abuse, AbuseState.ACCEPTED) + }, + + err => this.notifier.error(err.message) + ) + } + }, + { + label: this.i18n('Unblock video'), + isDisplayed: abuse => !abuse.video.deleted && abuse.video.blacklisted, + handler: abuse => { + this.videoBlocklistService.unblockVideo(abuse.video.id) + .subscribe( + () => { + this.notifier.success(this.i18n('Video unblocked.')) + + this.updateAbuseState(abuse, AbuseState.ACCEPTED) + }, + + err => this.notifier.error(err.message) + ) + } + }, + { + label: this.i18n('Delete video'), + isDisplayed: abuse => !abuse.video.deleted, + handler: async abuse => { + const res = await this.confirmService.confirm( + this.i18n('Do you really want to delete this video?'), + this.i18n('Delete') + ) + if (res === false) return + + this.videoService.removeVideo(abuse.video.id) + .subscribe( + () => { + this.notifier.success(this.i18n('Video deleted.')) + + this.updateAbuseState(abuse, AbuseState.ACCEPTED) + }, + + err => this.notifier.error(err.message) + ) + } + } + ], + [ + { + label: this.i18n('Actions for the reporter'), + isHeader: true + }, + { + label: this.i18n('Mute reporter'), + handler: async abuse => { + const account = abuse.reporterAccount as Account + + this.blocklistService.blockAccountByInstance(account) + .subscribe( + () => { + this.notifier.success( + this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }) + ) + + account.mutedByInstance = true + }, + + err => this.notifier.error(err.message) + ) + } + }, + { + label: this.i18n('Mute server'), + isDisplayed: abuse => !abuse.reporterAccount.userId, + handler: async abuse => { + this.blocklistService.blockServerByInstance(abuse.reporterAccount.host) + .subscribe( + () => { + this.notifier.success( + this.i18n('Server {{host}} muted by the instance.', { host: abuse.reporterAccount.host }) + ) + }, + + err => this.notifier.error(err.message) + ) + } + } + ] + ] + } + + ngOnInit () { + this.initialize() + + this.route.queryParams + .subscribe(params => { + this.search = params.search || '' + + this.setTableFilter(this.search) + this.loadData() + }) + } + + ngAfterViewInit () { + if (this.search) this.setTableFilter(this.search) + } + + getIdentifier () { + return 'AbuseListComponent' + } + + openModerationCommentModal (abuse: Abuse) { + this.moderationCommentModal.openModal(abuse) + } + + onModerationCommentUpdated () { + this.loadData() + } + + /* Table filter functions */ + onAbuseSearch (event: Event) { + this.onSearch(event) + this.setQueryParams((event.target as HTMLInputElement).value) + } + + setQueryParams (search: string) { + const queryParams: Params = {} + if (search) Object.assign(queryParams, { search }) + + this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams }) + } + + resetTableFilter () { + this.setTableFilter('') + this.setQueryParams('') + this.resetSearch() + } + /* END Table filter functions */ + + isAbuseAccepted (abuse: Abuse) { + return abuse.state.id === AbuseState.ACCEPTED + } + + isAbuseRejected (abuse: Abuse) { + return abuse.state.id === AbuseState.REJECTED + } + + getVideoUrl (abuse: Abuse) { + return Video.buildClientUrl(abuse.video.uuid) + } + + getVideoEmbed (abuse: Abuse) { + return buildVideoEmbed( + buildVideoLink({ + baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`, + title: false, + warningTitle: false, + startTime: abuse.startAt, + stopTime: abuse.endAt + }) + ) + } + + switchToDefaultAvatar ($event: Event) { + ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() + } + + async removeAbuse (abuse: Abuse) { + const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) + if (res === false) return + + this.abuseService.removeAbuse(abuse).subscribe( + () => { + this.notifier.success(this.i18n('Abuse deleted.')) + this.loadData() + }, + + err => this.notifier.error(err.message) + ) + } + + updateAbuseState (abuse: Abuse, state: AbuseState) { + this.abuseService.updateAbuse(abuse, { state }) + .subscribe( + () => this.loadData(), + + err => this.notifier.error(err.message) + ) + } + + protected loadData () { + return this.abuseService.getAbuses({ + pagination: this.pagination, + sort: this.sort, + search: this.search + }).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) + }) + + 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 = abuses + }, + + err => this.notifier.error(err.message) + ) + } + + private toHtml (text: string) { + return this.markdownRenderer.textMarkdownToHTML(text) + } +} diff --git a/client/src/app/+admin/moderation/abuse-list/index.ts b/client/src/app/+admin/moderation/abuse-list/index.ts new file mode 100644 index 000000000..c6037dab4 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/index.ts @@ -0,0 +1,3 @@ +export * from './abuse-details.component' +export * from './abuse-list.component' +export * from './moderation-comment-modal.component' diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html new file mode 100644 index 000000000..8082e93f4 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html @@ -0,0 +1,38 @@ + + + + + + diff --git a/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss new file mode 100644 index 000000000..afcdb9a16 --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss @@ -0,0 +1,6 @@ +@import 'variables'; +@import 'mixins'; + +textarea { + @include peertube-textarea(100%, 100px); +} 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 new file mode 100644 index 000000000..23738f9cd --- /dev/null +++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts @@ -0,0 +1,70 @@ +import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' +import { Notifier } from '@app/core' +import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms' +import { AbuseService } from '@app/shared/shared-moderation' +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 { Abuse } from '@shared/models' + +@Component({ + selector: 'my-moderation-comment-modal', + templateUrl: './moderation-comment-modal.component.html', + styleUrls: [ './moderation-comment-modal.component.scss' ] +}) +export class ModerationCommentModalComponent extends FormReactive implements OnInit { + @ViewChild('modal', { static: true }) modal: NgbModal + @Output() commentUpdated = new EventEmitter() + + private abuseToComment: Abuse + private openedModal: NgbModalRef + + constructor ( + protected formValidatorService: FormValidatorService, + private modalService: NgbModal, + private notifier: Notifier, + private abuseService: AbuseService, + private abuseValidatorsService: AbuseValidatorsService, + private i18n: I18n + ) { + super() + } + + ngOnInit () { + this.buildForm({ + moderationComment: this.abuseValidatorsService.ABUSE_MODERATION_COMMENT + }) + } + + openModal (abuseToComment: Abuse) { + this.abuseToComment = abuseToComment + this.openedModal = this.modalService.open(this.modal, { centered: true }) + + this.form.patchValue({ + moderationComment: this.abuseToComment.moderationComment + }) + } + + hide () { + this.abuseToComment = undefined + this.openedModal.close() + this.form.reset() + } + + async banUser () { + const moderationComment: string = this.form.value[ 'moderationComment' ] + + this.abuseService.updateAbuse(this.abuseToComment, { moderationComment }) + .subscribe( + () => { + this.notifier.success(this.i18n('Comment updated.')) + + this.commentUpdated.emit(moderationComment) + this.hide() + }, + + err => this.notifier.error(err.message) + ) + } + +} diff --git a/client/src/app/+admin/moderation/index.ts b/client/src/app/+admin/moderation/index.ts index 16249236c..53e4bc991 100644 --- a/client/src/app/+admin/moderation/index.ts +++ b/client/src/app/+admin/moderation/index.ts @@ -1,5 +1,5 @@ +export * from './abuse-list' export * from './instance-blocklist' -export * from './video-abuse-list' export * from './video-block-list' export * from './moderation.component' export * from './moderation.routes' diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts index cd837bcb9..1e207e5e8 100644 --- a/client/src/app/+admin/moderation/moderation.routes.ts +++ b/client/src/app/+admin/moderation/moderation.routes.ts @@ -1,7 +1,7 @@ import { Routes } from '@angular/router' import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' import { ModerationComponent } from '@app/+admin/moderation/moderation.component' -import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' +import { AbuseListComponent } from '@app/+admin/moderation/abuse-list' import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' import { UserRightGuard } from '@app/core' import { UserRight } from '@shared/models' @@ -13,20 +13,25 @@ export const ModerationRoutes: Routes = [ children: [ { path: '', - redirectTo: 'video-abuses/list', + redirectTo: 'abuses/list', pathMatch: 'full' }, { path: 'video-abuses', - redirectTo: 'video-abuses/list', + redirectTo: 'abuses/list', pathMatch: 'full' }, { path: 'video-abuses/list', - component: VideoAbuseListComponent, + redirectTo: 'abuses/list', + pathMatch: 'full' + }, + { + path: 'abuses/list', + component: AbuseListComponent, canActivate: [ UserRightGuard ], data: { - userRight: UserRight.MANAGE_VIDEO_ABUSES, + userRight: UserRight.MANAGE_ABUSES, meta: { title: 'Video reports' } diff --git a/client/src/app/+admin/moderation/video-abuse-list/index.ts b/client/src/app/+admin/moderation/video-abuse-list/index.ts deleted file mode 100644 index da7176e52..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './video-abuse-list.component' -export * from './moderation-comment-modal.component' diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html deleted file mode 100644 index 8082e93f4..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss deleted file mode 100644 index afcdb9a16..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import 'variables'; -@import 'mixins'; - -textarea { - @include peertube-textarea(100%, 100px); -} diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts deleted file mode 100644 index 3cd763ca4..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' -import { Notifier } from '@app/core' -import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms' -import { VideoAbuseService } from '@app/shared/shared-moderation' -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 { VideoAbuse } from '@shared/models' - -@Component({ - selector: 'my-moderation-comment-modal', - templateUrl: './moderation-comment-modal.component.html', - styleUrls: [ './moderation-comment-modal.component.scss' ] -}) -export class ModerationCommentModalComponent extends FormReactive implements OnInit { - @ViewChild('modal', { static: true }) modal: NgbModal - @Output() commentUpdated = new EventEmitter() - - private abuseToComment: VideoAbuse - private openedModal: NgbModalRef - - constructor ( - protected formValidatorService: FormValidatorService, - private modalService: NgbModal, - private notifier: Notifier, - private videoAbuseService: VideoAbuseService, - private videoAbuseValidatorsService: VideoAbuseValidatorsService, - private i18n: I18n - ) { - super() - } - - ngOnInit () { - this.buildForm({ - moderationComment: this.videoAbuseValidatorsService.VIDEO_ABUSE_MODERATION_COMMENT - }) - } - - openModal (abuseToComment: VideoAbuse) { - this.abuseToComment = abuseToComment - this.openedModal = this.modalService.open(this.modal, { centered: true }) - - this.form.patchValue({ - moderationComment: this.abuseToComment.moderationComment - }) - } - - hide () { - this.abuseToComment = undefined - this.openedModal.close() - this.form.reset() - } - - async banUser () { - const moderationComment: string = this.form.value[ 'moderationComment' ] - - this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment }) - .subscribe( - () => { - this.notifier.success(this.i18n('Comment updated.')) - - this.commentUpdated.emit(moderationComment) - this.hide() - }, - - err => this.notifier.error(err.message) - ) - } - -} diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html deleted file mode 100644 index ec808cdb8..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html +++ /dev/null @@ -1,93 +0,0 @@ - diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts deleted file mode 100644 index 5db2887fa..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Component, Input } from '@angular/core' -import { Actor } from '@app/shared/shared-main' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model' -import { ProcessedVideoAbuse } from './video-abuse-list.component' -import { durationToString } from '@app/helpers' - -@Component({ - selector: 'my-video-abuse-details', - templateUrl: './video-abuse-details.component.html', - styleUrls: [ '../moderation.component.scss' ] -}) -export class VideoAbuseDetailsComponent { - @Input() videoAbuse: ProcessedVideoAbuse - - private predefinedReasonsTranslations: { [key in VideoAbusePredefinedReasonsString]: string } - - constructor ( - private i18n: I18n - ) { - this.predefinedReasonsTranslations = { - violentOrRepulsive: this.i18n('Violent or Repulsive'), - hatefulOrAbusive: this.i18n('Hateful or Abusive'), - spamOrMisleading: this.i18n('Spam or Misleading'), - privacy: this.i18n('Privacy'), - rights: this.i18n('Rights'), - serverRules: this.i18n('Server rules'), - thumbnails: this.i18n('Thumbnails'), - captions: this.i18n('Captions') - } - } - - get startAt () { - return durationToString(this.videoAbuse.startAt) - } - - get endAt () { - return durationToString(this.videoAbuse.endAt) - } - - getPredefinedReasons () { - if (!this.videoAbuse.predefinedReasons) return [] - return this.videoAbuse.predefinedReasons.map(r => ({ - id: r, - label: this.predefinedReasonsTranslations[r] - })) - } - - switchToDefaultAvatar ($event: Event) { - ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() - } -} diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html deleted file mode 100644 index 64641b28a..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ /dev/null @@ -1,149 +0,0 @@ - - -
-
-
- - - - Clear filters -
-
-
-
- - - - - Reporter - Video - Created - State - - - - - - - - - - - - - - -
- Avatar -
- {{ videoAbuse.reporterAccount.displayName }} - {{ videoAbuse.reporterAccount.nameWithHost }} -
-
-
- - - - -
-
- - - {{ videoAbuse.nth }}/{{ videoAbuse.count }} - -
-
-
- - - {{ videoAbuse.video.name }} -
-
by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }}
-
-
-
- - - -
-
- Deleted -
-
-
- {{ videoAbuse.video.name }} - -
-
by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }}
-
-
- - - {{ videoAbuse.createdAt | date: 'short' }} - - - - - - - - - - - -
- - - - - - - - - - - - -
- No video abuses found matching current filters. - No video abuses found. -
- - -
-
- - diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss deleted file mode 100644 index 8eee15b64..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss +++ /dev/null @@ -1,23 +0,0 @@ -@import 'mixins'; -@import 'miniature'; - -.video-details-date-updated { - font-size: 90%; - margin-top: .1rem; -} - -.video-details-links { - @include disable-default-a-behaviour; -} - -.video-abuse-states .glyphicon-comment { - margin-left: 0.5rem; -} - -.input-group { - @include peertube-input-group(300px); - - .dropdown-toggle::after { - margin-left: 0; - } -} diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts deleted file mode 100644 index 409dd42c7..000000000 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { SortMeta } from 'primeng/api' -import { filter } from 'rxjs/operators' -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 { 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' -import { BlocklistService, VideoAbuseService, VideoBlockService } from '@app/shared/shared-moderation' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { VideoAbuse, VideoAbuseState } from '@shared/models' -import { ModerationCommentModalComponent } from './moderation-comment-modal.component' - -export type ProcessedVideoAbuse = VideoAbuse & { - moderationCommentHtml?: string, - reasonHtml?: string - embedHtml?: string - updatedAt?: Date - // override bare server-side definitions with rich client-side definitions - reporterAccount: Account - video: VideoAbuse['video'] & { - channel: VideoAbuse['video']['channel'] & { - ownerAccount: Account - } - } -} - -@Component({ - selector: 'my-video-abuse-list', - templateUrl: './video-abuse-list.component.html', - styleUrls: [ '../moderation.component.scss', './video-abuse-list.component.scss' ] -}) -export class VideoAbuseListComponent extends RestTable implements OnInit, AfterViewInit { - @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent - - videoAbuses: ProcessedVideoAbuse[] = [] - totalRecords = 0 - sort: SortMeta = { field: 'createdAt', order: 1 } - pagination: RestPagination = { count: this.rowsPerPage, start: 0 } - - videoAbuseActions: DropdownAction[][] = [] - - constructor ( - private notifier: Notifier, - private videoAbuseService: VideoAbuseService, - private blocklistService: BlocklistService, - private videoService: VideoService, - private videoBlocklistService: VideoBlockService, - private confirmService: ConfirmService, - private i18n: I18n, - private markdownRenderer: MarkdownService, - private sanitizer: DomSanitizer, - private route: ActivatedRoute, - private router: Router - ) { - super() - - this.videoAbuseActions = [ - [ - { - label: this.i18n('Internal actions'), - isHeader: true - }, - { - label: this.i18n('Delete report'), - handler: videoAbuse => this.removeVideoAbuse(videoAbuse) - }, - { - label: this.i18n('Add note'), - handler: videoAbuse => this.openModerationCommentModal(videoAbuse), - isDisplayed: videoAbuse => !videoAbuse.moderationComment - }, - { - label: this.i18n('Update note'), - handler: videoAbuse => this.openModerationCommentModal(videoAbuse), - isDisplayed: videoAbuse => !!videoAbuse.moderationComment - }, - { - label: this.i18n('Mark as accepted'), - handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED), - isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse) - }, - { - label: this.i18n('Mark as rejected'), - handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED), - isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse) - } - ], - [ - { - label: this.i18n('Actions for the video'), - isHeader: true, - isDisplayed: videoAbuse => !videoAbuse.video.deleted - }, - { - label: this.i18n('Block video'), - isDisplayed: videoAbuse => !videoAbuse.video.deleted && !videoAbuse.video.blacklisted, - handler: videoAbuse => { - this.videoBlocklistService.blockVideo(videoAbuse.video.id, undefined, true) - .subscribe( - () => { - this.notifier.success(this.i18n('Video blocked.')) - - this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) - }, - - err => this.notifier.error(err.message) - ) - } - }, - { - label: this.i18n('Unblock video'), - isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted, - handler: videoAbuse => { - this.videoBlocklistService.unblockVideo(videoAbuse.video.id) - .subscribe( - () => { - this.notifier.success(this.i18n('Video unblocked.')) - - this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) - }, - - err => this.notifier.error(err.message) - ) - } - }, - { - label: this.i18n('Delete video'), - isDisplayed: videoAbuse => !videoAbuse.video.deleted, - handler: async videoAbuse => { - const res = await this.confirmService.confirm( - this.i18n('Do you really want to delete this video?'), - this.i18n('Delete') - ) - if (res === false) return - - this.videoService.removeVideo(videoAbuse.video.id) - .subscribe( - () => { - this.notifier.success(this.i18n('Video deleted.')) - - this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) - }, - - err => this.notifier.error(err.message) - ) - } - } - ], - [ - { - label: this.i18n('Actions for the reporter'), - isHeader: true - }, - { - label: this.i18n('Mute reporter'), - handler: async videoAbuse => { - const account = videoAbuse.reporterAccount as Account - - this.blocklistService.blockAccountByInstance(account) - .subscribe( - () => { - this.notifier.success( - this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }) - ) - - account.mutedByInstance = true - }, - - err => this.notifier.error(err.message) - ) - } - }, - { - label: this.i18n('Mute server'), - isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId, - handler: async videoAbuse => { - this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host) - .subscribe( - () => { - this.notifier.success( - this.i18n('Server {{host}} muted by the instance.', { host: videoAbuse.reporterAccount.host }) - ) - }, - - err => this.notifier.error(err.message) - ) - } - } - ] - ] - } - - ngOnInit () { - this.initialize() - - this.route.queryParams - .subscribe(params => { - this.search = params.search || '' - - this.setTableFilter(this.search) - this.loadData() - }) - } - - ngAfterViewInit () { - if (this.search) this.setTableFilter(this.search) - } - - getIdentifier () { - return 'VideoAbuseListComponent' - } - - openModerationCommentModal (videoAbuse: VideoAbuse) { - this.moderationCommentModal.openModal(videoAbuse) - } - - onModerationCommentUpdated () { - this.loadData() - } - - /* Table filter functions */ - onAbuseSearch (event: Event) { - this.onSearch(event) - this.setQueryParams((event.target as HTMLInputElement).value) - } - - setQueryParams (search: string) { - const queryParams: Params = {} - if (search) Object.assign(queryParams, { search }) - - this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams }) - } - - resetTableFilter () { - this.setTableFilter('') - this.setQueryParams('') - this.resetSearch() - } - /* END Table filter functions */ - - isVideoAbuseAccepted (videoAbuse: VideoAbuse) { - return videoAbuse.state.id === VideoAbuseState.ACCEPTED - } - - isVideoAbuseRejected (videoAbuse: VideoAbuse) { - return videoAbuse.state.id === VideoAbuseState.REJECTED - } - - getVideoUrl (videoAbuse: VideoAbuse) { - return Video.buildClientUrl(videoAbuse.video.uuid) - } - - getVideoEmbed (videoAbuse: VideoAbuse) { - return buildVideoEmbed( - buildVideoLink({ - baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`, - title: false, - warningTitle: false, - startTime: videoAbuse.startAt, - stopTime: videoAbuse.endAt - }) - ) - } - - switchToDefaultAvatar ($event: Event) { - ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() - } - - async removeVideoAbuse (videoAbuse: VideoAbuse) { - const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) - if (res === false) return - - this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe( - () => { - this.notifier.success(this.i18n('Abuse deleted.')) - this.loadData() - }, - - err => this.notifier.error(err.message) - ) - } - - updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) { - this.videoAbuseService.updateVideoAbuse(videoAbuse, { state }) - .subscribe( - () => this.loadData(), - - err => this.notifier.error(err.message) - ) - } - - protected loadData () { - return this.videoAbuseService.getVideoAbuses({ - pagination: this.pagination, - sort: this.sort, - search: this.search - }).subscribe( - async resultList => { - this.totalRecords = resultList.total - const videoAbuses = [] - - 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) - }) - - if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) - if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt - - videoAbuses.push(abuse as ProcessedVideoAbuse) - } - - this.videoAbuses = videoAbuses - }, - - err => this.notifier.error(err.message) - ) - } - - private toHtml (text: string) { - return this.markdownRenderer.textMarkdownToHTML(text) - } -} -- cgit v1.2.3