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
+
|
-
-
-
-
-
- 1"
- i18n-title title="This video has been reported multiple times."
- >
- {{ abuse.nth }}/{{ abuse.count }}
-
-
- |
+
+
+
+
+
+
+
+ |
+
{{ 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 @@
@@ -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
|