Add ability to report comments in front end
This commit is contained in:
parent
310b5219b3
commit
8ca56654a1
|
@ -47,8 +47,8 @@ export class AdminComponent implements OnInit {
|
||||||
|
|
||||||
if (this.hasAbusesRight()) {
|
if (this.hasAbusesRight()) {
|
||||||
moderationItems.children.push({
|
moderationItems.children.push({
|
||||||
label: this.i18n('Video reports'),
|
label: this.i18n('Reports'),
|
||||||
routerLink: '/admin/moderation/video-abuses/list',
|
routerLink: '/admin/moderation/abuses/list',
|
||||||
iconName: 'flag'
|
iconName: 'flag'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,13 @@
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
|
|
||||||
<!-- report metadata -->
|
<!-- report metadata -->
|
||||||
<div class="d-flex">
|
<div class="d-flex" *ngIf="abuse.reporterAccount">
|
||||||
<span class="col-3 moderation-expanded-label" i18n>Reporter</span>
|
<span class="col-3 moderation-expanded-label" i18n>Reporter</span>
|
||||||
|
|
||||||
<span class="col-9 moderation-expanded-text">
|
<span class="col-9 moderation-expanded-text">
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" class="chip">
|
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
|
||||||
|
class="chip"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
class="avatar"
|
class="avatar"
|
||||||
[src]="abuse.reporterAccount.avatar?.path"
|
[src]="abuse.reporterAccount.avatar?.path"
|
||||||
|
@ -17,27 +20,35 @@
|
||||||
<span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
|
<span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
|
|
||||||
|
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
|
||||||
|
class="ml-auto text-muted abuse-details-links" i18n
|
||||||
|
>
|
||||||
{abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
{abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex">
|
<div class="d-flex" *ngIf="abuse.flaggedAccount">
|
||||||
<span class="col-3 moderation-expanded-label" i18n>Reportee</span>
|
<span class="col-3 moderation-expanded-label" i18n>Reportee</span>
|
||||||
<span class="col-9 moderation-expanded-text">
|
<span class="col-9 moderation-expanded-text">
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.video.channel.ownerAccount.displayName + '"' }" class="chip">
|
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
|
||||||
|
class="chip"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
class="avatar"
|
class="avatar"
|
||||||
[src]="abuse.video.channel.ownerAccount?.avatar?.path"
|
[src]="abuse.flaggedAccount?.avatar?.path"
|
||||||
(error)="switchToDefaultAvatar($event)"
|
(error)="switchToDefaultAvatar($event)"
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-muted">{{ abuse.video.channel.ownerAccount ? abuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
|
<span class="text-muted">{{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.video.channel.ownerAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
|
|
||||||
|
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
|
||||||
|
class="ml-auto text-muted abuse-details-links" i18n
|
||||||
|
>
|
||||||
{abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
{abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
@ -45,7 +56,7 @@
|
||||||
|
|
||||||
<div class="d-flex" *ngIf="abuse.updatedAt">
|
<div class="d-flex" *ngIf="abuse.updatedAt">
|
||||||
<span class="col-3 moderation-expanded-label" i18n>Updated</span>
|
<span class="col-3 moderation-expanded-label" i18n>Updated</span>
|
||||||
<time class="col-9 moderation-expanded-text video-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time>
|
<time class="col-9 moderation-expanded-text abuse-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- report text -->
|
<!-- report text -->
|
||||||
|
@ -60,34 +71,45 @@
|
||||||
<div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
|
<div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
|
||||||
<span class="col-3"></span>
|
<span class="col-3"></span>
|
||||||
<span class="col-9">
|
<span class="col-9">
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" *ngFor="let reason of getPredefinedReasons()">
|
<a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ '/admin/moderation/abuses/list' ]"
|
||||||
|
[queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light"
|
||||||
|
>
|
||||||
<div>{{ reason.label }}</div>
|
<div>{{ reason.label }}</div>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="abuse.startAt" class="mt-2 d-flex">
|
<div *ngIf="abuse.video?.startAt" class="mt-2 d-flex">
|
||||||
<span class="col-3 moderation-expanded-label" i18n>Reported part</span>
|
<span class="col-3 moderation-expanded-label" i18n>Reported part</span>
|
||||||
<span class="col-9">
|
<span class="col-9">
|
||||||
{{ startAt }}<ng-container *ngIf="abuse.endAt"> - {{ endAt }}</ng-container>
|
{{ startAt }}<ng-container *ngIf="abuse.video.endAt"> - {{ endAt }}</ng-container>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 d-flex" *ngIf="abuse.moderationComment">
|
<div class="mt-3 d-flex" *ngIf="abuse.moderationComment">
|
||||||
<span class="col-3 moderation-expanded-label" i18n>Note</span>
|
<span class="col-3 moderation-expanded-label" i18n>Note</span>
|
||||||
<span class="col-9 moderation-expanded-text" [innerHTML]="abuse.moderationCommentHtml"></span>
|
<span class="col-9 moderation-expanded-text d-block" [innerHTML]="abuse.moderationCommentHtml"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- report right part (video details) -->
|
<!-- report right part (video/comment details) -->
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="screenratio">
|
<div *ngIf="abuse.video" class="screenratio">
|
||||||
<div *ngIf="abuse.video.deleted || abuse.video.blacklisted">
|
<div>
|
||||||
<span i18n *ngIf="abuse.video.deleted">The video was deleted</span>
|
<span i18n *ngIf="abuse.video.deleted">The video was deleted</span>
|
||||||
<span i18n *ngIf="!abuse.video.deleted">The video was blocked</span>
|
<span i18n *ngIf="!abuse.video.deleted">The video was blocked</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div>
|
<div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="abuse.comment" class="comment-html">
|
||||||
|
<div>
|
||||||
|
<strong i18n>Comment:</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div [innerHTML]="abuse.commentHtml"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,15 +31,16 @@ export class AbuseDetailsComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
get startAt () {
|
get startAt () {
|
||||||
return durationToString(this.abuse.startAt)
|
return durationToString(this.abuse.video.startAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
get endAt () {
|
get endAt () {
|
||||||
return durationToString(this.abuse.endAt)
|
return durationToString(this.abuse.video.endAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
getPredefinedReasons () {
|
getPredefinedReasons () {
|
||||||
if (!this.abuse.predefinedReasons) return []
|
if (!this.abuse.predefinedReasons) return []
|
||||||
|
|
||||||
return this.abuse.predefinedReasons.map(r => ({
|
return this.abuse.predefinedReasons.map(r => ({
|
||||||
id: r,
|
id: r,
|
||||||
label: this.predefinedReasonsTranslations[r]
|
label: this.predefinedReasonsTranslations[r]
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
<tr> <!-- header -->
|
<tr> <!-- header -->
|
||||||
<th style="width: 40px;"></th>
|
<th style="width: 40px;"></th>
|
||||||
<th style="width: 20%;" pResizableColumn i18n>Reporter</th>
|
<th style="width: 20%;" pResizableColumn i18n>Reporter</th>
|
||||||
<th i18n>Video</th>
|
<th i18n>Video/Comment/Account</th>
|
||||||
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
<th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
|
<th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||||
<th style="width: 150px;"></th>
|
<th style="width: 150px;"></th>
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<a [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
|
<a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
|
||||||
<div class="chip two-lines">
|
<div class="chip two-lines">
|
||||||
<img
|
<img
|
||||||
class="avatar"
|
class="avatar"
|
||||||
|
@ -64,54 +64,73 @@
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{{ abuse.reporterAccount.displayName }}
|
{{ abuse.reporterAccount.displayName }}
|
||||||
<span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
|
<span>{{ abuse.reporterAccount.nameWithHost }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<span i18n *ngIf="!abuse.reporterAccount">
|
||||||
|
Deleted account
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td *ngIf="!abuse.video.deleted">
|
<ng-container *ngIf="abuse.video">
|
||||||
<a [href]="getVideoUrl(abuse)" class="video-table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
|
|
||||||
<div class="video-table-video">
|
<td *ngIf="!abuse.video.deleted">
|
||||||
<div class="video-table-video-image">
|
<a [href]="getVideoUrl(abuse)" class="table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
|
||||||
<img [src]="abuse.video.thumbnailPath">
|
<div class="table-video">
|
||||||
<span
|
<div class="table-video-image">
|
||||||
class="video-table-video-image-label" *ngIf="abuse.count > 1"
|
<img [src]="abuse.video.thumbnailPath">
|
||||||
i18n-title title="This video has been reported multiple times."
|
<span
|
||||||
>
|
class="table-video-image-label" *ngIf="abuse.count > 1"
|
||||||
{{ abuse.nth }}/{{ abuse.count }}
|
i18n-title title="This video has been reported multiple times."
|
||||||
</span>
|
>
|
||||||
</div>
|
{{ abuse.nth }}/{{ abuse.count }}
|
||||||
<div class="video-table-video-text">
|
</span>
|
||||||
<div>
|
|
||||||
<span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
|
|
||||||
<span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
|
|
||||||
{{ abuse.video.name }}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse">
|
<div class="table-video-text">
|
||||||
<div class="video-table-video" i18n-title title="Video was deleted">
|
<div>
|
||||||
<div class="video-table-video-image">
|
<span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
|
||||||
<span i18n>Deleted</span>
|
<span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
|
||||||
</div>
|
{{ abuse.video.name }}
|
||||||
<div class="video-table-video-text">
|
</div>
|
||||||
<div>
|
<div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
|
||||||
{{ abuse.video.name }}
|
</div>
|
||||||
<span class="glyphicon glyphicon-trash"></span>
|
</div>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse">
|
||||||
|
<div class="table-video" i18n-title title="Video was deleted">
|
||||||
|
<div class="table-video-image">
|
||||||
|
<span i18n>Deleted</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-video-text">
|
||||||
|
<div>
|
||||||
|
{{ abuse.video.name }}
|
||||||
|
<span class="glyphicon glyphicon-trash"></span>
|
||||||
|
</div>
|
||||||
|
<div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="abuse.comment">
|
||||||
|
<td>
|
||||||
|
<a [href]="getCommentUrl(abuse)" [innerHTML]="abuse.truncatedCommentHtml" class="table-comment-link"
|
||||||
|
[title]="abuse.comment.video.name" target="_blank" rel="noopener noreferrer"
|
||||||
|
></a>
|
||||||
|
|
||||||
|
<div class="comment-flagged-account" *ngIf="abuse.flaggedAccount">by {{ abuse.flaggedAccount.displayName }}</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
|
<td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
|
||||||
|
|
||||||
<td class="c-hand video-abuse-states" [pRowToggler]="abuse">
|
<td class="c-hand abuse-states" [pRowToggler]="abuse">
|
||||||
<span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
|
<span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
|
||||||
<span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
|
<span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
|
||||||
<span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
|
<span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
@include disable-default-a-behaviour;
|
@include disable-default-a-behaviour;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-abuse-states .glyphicon-comment {
|
.abuse-states .glyphicon-comment {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api'
|
||||||
import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
|
import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
|
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 { ActivatedRoute, Params, Router } from '@angular/router'
|
||||||
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
|
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||||
import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
|
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 { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { Abuse, AbuseState } from '@shared/models'
|
import { Abuse, AbuseState } from '@shared/models'
|
||||||
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
||||||
|
import truncate from 'lodash-es/truncate'
|
||||||
|
|
||||||
export type ProcessedAbuse = Abuse & {
|
export type ProcessedAbuse = Abuse & {
|
||||||
moderationCommentHtml?: string,
|
moderationCommentHtml?: string,
|
||||||
reasonHtml?: string
|
reasonHtml?: string
|
||||||
embedHtml?: string
|
embedHtml?: SafeHtml
|
||||||
updatedAt?: Date
|
updatedAt?: Date
|
||||||
|
|
||||||
// override bare server-side definitions with rich client-side definitions
|
// override bare server-side definitions with rich client-side definitions
|
||||||
reporterAccount: Account
|
reporterAccount?: Account
|
||||||
|
flaggedAccount?: Account
|
||||||
|
|
||||||
|
truncatedCommentHtml?: string
|
||||||
|
commentHtml?: string
|
||||||
|
|
||||||
video: Abuse['video'] & {
|
video: Abuse['video'] & {
|
||||||
channel: Abuse['video']['channel'] & {
|
channel: Abuse['video']['channel'] & {
|
||||||
|
@ -92,11 +97,11 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
{
|
{
|
||||||
label: this.i18n('Actions for the video'),
|
label: this.i18n('Actions for the video'),
|
||||||
isHeader: true,
|
isHeader: true,
|
||||||
isDisplayed: abuse => !abuse.video.deleted
|
isDisplayed: abuse => abuse.video && !abuse.video.deleted
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Block video'),
|
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 => {
|
handler: abuse => {
|
||||||
this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
|
this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
@ -112,7 +117,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Unblock video'),
|
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 => {
|
handler: abuse => {
|
||||||
this.videoBlocklistService.unblockVideo(abuse.video.id)
|
this.videoBlocklistService.unblockVideo(abuse.video.id)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
@ -128,7 +133,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Delete video'),
|
label: this.i18n('Delete video'),
|
||||||
isDisplayed: abuse => !abuse.video.deleted,
|
isDisplayed: abuse => abuse.video && !abuse.video.deleted,
|
||||||
handler: async abuse => {
|
handler: async abuse => {
|
||||||
const res = await this.confirmService.confirm(
|
const res = await this.confirmService.confirm(
|
||||||
this.i18n('Do you really want to delete this video?'),
|
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'),
|
label: this.i18n('Actions for the reporter'),
|
||||||
isHeader: true
|
isHeader: true,
|
||||||
|
isDisplayed: abuse => !!abuse.reporterAccount
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Mute reporter'),
|
label: this.i18n('Mute reporter'),
|
||||||
|
isDisplayed: abuse => !!abuse.reporterAccount,
|
||||||
handler: async abuse => {
|
handler: async abuse => {
|
||||||
const account = abuse.reporterAccount as Account
|
const account = abuse.reporterAccount as Account
|
||||||
|
|
||||||
|
@ -175,7 +182,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Mute server'),
|
label: this.i18n('Mute server'),
|
||||||
isDisplayed: abuse => !abuse.reporterAccount.userId,
|
isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
|
||||||
handler: async abuse => {
|
handler: async abuse => {
|
||||||
this.blocklistService.blockServerByInstance(abuse.reporterAccount.host)
|
this.blocklistService.blockServerByInstance(abuse.reporterAccount.host)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
@ -231,7 +238,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
const queryParams: Params = {}
|
const queryParams: Params = {}
|
||||||
if (search) Object.assign(queryParams, { search })
|
if (search) Object.assign(queryParams, { search })
|
||||||
|
|
||||||
this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams })
|
this.router.navigate([ '/admin/moderation/abuses/list' ], { queryParams })
|
||||||
}
|
}
|
||||||
|
|
||||||
resetTableFilter () {
|
resetTableFilter () {
|
||||||
|
@ -253,6 +260,10 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
return Video.buildClientUrl(abuse.video.uuid)
|
return Video.buildClientUrl(abuse.video.uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCommentUrl (abuse: Abuse) {
|
||||||
|
return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
|
||||||
|
}
|
||||||
|
|
||||||
getVideoEmbed (abuse: Abuse) {
|
getVideoEmbed (abuse: Abuse) {
|
||||||
return buildVideoEmbed(
|
return buildVideoEmbed(
|
||||||
buildVideoLink({
|
buildVideoLink({
|
||||||
|
@ -300,23 +311,45 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
}).subscribe(
|
}).subscribe(
|
||||||
async resultList => {
|
async resultList => {
|
||||||
this.totalRecords = resultList.total
|
this.totalRecords = resultList.total
|
||||||
const abuses = []
|
|
||||||
|
|
||||||
for (const abuse of resultList.data) {
|
this.abuses = []
|
||||||
Object.assign(abuse, {
|
|
||||||
reasonHtml: await this.toHtml(abuse.reason),
|
for (const a of resultList.data) {
|
||||||
moderationCommentHtml: await this.toHtml(abuse.moderationComment),
|
const abuse = a as ProcessedAbuse
|
||||||
embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)),
|
|
||||||
reporterAccount: new Account(abuse.reporterAccount)
|
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
|
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)
|
err => this.notifier.error(err.message)
|
||||||
|
|
|
@ -25,18 +25,18 @@
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.moderation-expanded-text {
|
.moderation-expanded-text {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|
||||||
::ng-deep p:last-child {
|
::ng-deep p:last-child {
|
||||||
margin-bottom: 0px !important;
|
margin-bottom: 0px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-table-states {
|
.table-states {
|
||||||
& > :not(:first-child) {
|
& > :not(:first-child) {
|
||||||
margin-left: .4rem;
|
margin-left: .4rem;
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ p-calendar {
|
||||||
.screenratio {
|
.screenratio {
|
||||||
div {
|
div {
|
||||||
@include miniature-thumbnail;
|
@include miniature-thumbnail;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -72,6 +73,11 @@ p-calendar {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-html {
|
||||||
|
background-color: #ececec;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
@include chip;
|
@include chip;
|
||||||
}
|
}
|
||||||
|
@ -83,16 +89,32 @@ my-action-dropdown.show {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.video-table-video-link {
|
.table-video-link {
|
||||||
@include disable-outline;
|
@include disable-outline;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 3px;
|
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;
|
display: inline-flex;
|
||||||
|
|
||||||
.video-table-video-image {
|
.table-video-image {
|
||||||
@include miniature-thumbnail;
|
@include miniature-thumbnail;
|
||||||
|
|
||||||
$image-height: 45px;
|
$image-height: 45px;
|
||||||
|
@ -118,7 +140,7 @@ my-action-dropdown.show {
|
||||||
color: pvar(--inputPlaceholderColor);
|
color: pvar(--inputPlaceholderColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-table-video-image-label {
|
.table-video-image-label {
|
||||||
@include static-thumbnail-overlay;
|
@include static-thumbnail-overlay;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -130,7 +152,7 @@ my-action-dropdown.show {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-table-video-text {
|
.table-video-text {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -145,7 +167,8 @@ my-action-dropdown.show {
|
||||||
}
|
}
|
||||||
|
|
||||||
div + div {
|
div + div {
|
||||||
font-size: 80%;
|
color: var(--greyForegroundColor);
|
||||||
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const ModerationRoutes: Routes = [
|
||||||
data: {
|
data: {
|
||||||
userRight: UserRight.MANAGE_ABUSES,
|
userRight: UserRight.MANAGE_ABUSES,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Video reports'
|
title: 'Reports'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
<div *ngIf="isRemovableByUser()" (click)="onWantToDelete()" class="comment-action-delete" i18n>Delete</div>
|
<div *ngIf="isRemovableByUser()" (click)="onWantToDelete()" class="comment-action-delete" i18n>Delete</div>
|
||||||
|
|
||||||
<my-user-moderation-dropdown
|
<my-user-moderation-dropdown
|
||||||
|
[prependActions]="prependModerationActions"
|
||||||
buttonSize="small" [account]="commentAccount" [user]="commentUser" i18n-label label="Options" placement="bottom-left auto"
|
buttonSize="small" [account]="commentAccount" [user]="commentUser" i18n-label label="Options" placement="bottom-left auto"
|
||||||
></my-user-moderation-dropdown>
|
></my-user-moderation-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,3 +94,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="prependModerationActions">
|
||||||
|
<my-comment-report #commentReportModal [comment]="comment"></my-comment-report>
|
||||||
|
</ng-container>
|
||||||
|
|
|
@ -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 { MarkdownService, Notifier, UserService } from '@app/core'
|
||||||
import { AuthService } from '@app/core/auth'
|
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 { User, UserRight } from '@shared/models'
|
||||||
import { VideoCommentThreadTree } from './video-comment-thread-tree.model'
|
import { VideoCommentThreadTree } from './video-comment-thread-tree.model'
|
||||||
import { VideoComment } from './video-comment.model'
|
import { VideoComment } from './video-comment.model'
|
||||||
|
@ -12,6 +15,8 @@ import { VideoComment } from './video-comment.model'
|
||||||
styleUrls: ['./video-comment.component.scss']
|
styleUrls: ['./video-comment.component.scss']
|
||||||
})
|
})
|
||||||
export class VideoCommentComponent implements OnInit, OnChanges {
|
export class VideoCommentComponent implements OnInit, OnChanges {
|
||||||
|
@ViewChild('commentReportModal') commentReportModal: CommentReportComponent
|
||||||
|
|
||||||
@Input() video: Video
|
@Input() video: Video
|
||||||
@Input() comment: VideoComment
|
@Input() comment: VideoComment
|
||||||
@Input() parentComments: VideoComment[] = []
|
@Input() parentComments: VideoComment[] = []
|
||||||
|
@ -26,6 +31,8 @@ export class VideoCommentComponent implements OnInit, OnChanges {
|
||||||
@Output() resetReply = new EventEmitter()
|
@Output() resetReply = new EventEmitter()
|
||||||
@Output() timestampClicked = new EventEmitter<number>()
|
@Output() timestampClicked = new EventEmitter<number>()
|
||||||
|
|
||||||
|
prependModerationActions: DropdownAction<any>[]
|
||||||
|
|
||||||
sanitizedCommentHTML = ''
|
sanitizedCommentHTML = ''
|
||||||
newParentComments: VideoComment[] = []
|
newParentComments: VideoComment[] = []
|
||||||
|
|
||||||
|
@ -33,6 +40,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
|
||||||
commentUser: User
|
commentUser: User
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
private i18n: I18n,
|
||||||
private markdownService: MarkdownService,
|
private markdownService: MarkdownService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
|
@ -127,5 +135,20 @@ export class VideoCommentComponent implements OnInit, OnChanges {
|
||||||
} else {
|
} else {
|
||||||
this.comment.account = null
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,8 +46,10 @@ export abstract class Actor implements ActorServer {
|
||||||
this.host = hash.host
|
this.host = hash.host
|
||||||
this.followingCount = hash.followingCount
|
this.followingCount = hash.followingCount
|
||||||
this.followersCount = hash.followersCount
|
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.avatar = hash.avatar
|
||||||
|
|
||||||
this.updateComputedAttributes()
|
this.updateComputedAttributes()
|
||||||
|
|
|
@ -5,18 +5,20 @@ import { catchError, map } from 'rxjs/operators'
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { RestExtractor, RestPagination, RestService } from '@app/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 { environment } from '../../../environments/environment'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AbuseService {
|
export class AbuseService {
|
||||||
private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses'
|
private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses'
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
private i18n: I18n,
|
||||||
private authHttp: HttpClient,
|
private authHttp: HttpClient,
|
||||||
private restService: RestService,
|
private restService: RestService,
|
||||||
private restExtractor: RestExtractor
|
private restExtractor: RestExtractor
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
getAbuses (options: {
|
getAbuses (options: {
|
||||||
pagination: RestPagination,
|
pagination: RestPagination,
|
||||||
|
@ -24,7 +26,7 @@ export class AbuseService {
|
||||||
search?: string
|
search?: string
|
||||||
}): Observable<ResultList<Abuse>> {
|
}): Observable<ResultList<Abuse>> {
|
||||||
const { pagination, sort, search } = options
|
const { pagination, sort, search } = options
|
||||||
const url = AbuseService.BASE_ABUSE_URL + 'abuse'
|
const url = AbuseService.BASE_ABUSE_URL
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
@ -60,39 +62,93 @@ export class AbuseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<Abuse>>(url, { params })
|
return this.authHttp.get<ResultList<Abuse>>(url, { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
reportVideo (parameters: AbuseCreate) {
|
reportVideo (parameters: AbuseCreate) {
|
||||||
const url = AbuseService.BASE_ABUSE_URL
|
const url = AbuseService.BASE_ABUSE_URL
|
||||||
|
|
||||||
const body = omit(parameters, [ 'id' ])
|
const body = omit(parameters, ['id'])
|
||||||
|
|
||||||
return this.authHttp.post(url, body)
|
return this.authHttp.post(url, body)
|
||||||
.pipe(
|
.pipe(
|
||||||
map(this.restExtractor.extractDataBool),
|
map(this.restExtractor.extractDataBool),
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) {
|
updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) {
|
||||||
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
|
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
|
||||||
|
|
||||||
return this.authHttp.put(url, abuseUpdate)
|
return this.authHttp.put(url, abuseUpdate)
|
||||||
.pipe(
|
.pipe(
|
||||||
map(this.restExtractor.extractDataBool),
|
map(this.restExtractor.extractDataBool),
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAbuse (abuse: Abuse) {
|
removeAbuse (abuse: Abuse) {
|
||||||
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
|
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
|
||||||
|
|
||||||
return this.authHttp.delete(url)
|
return this.authHttp.delete(url)
|
||||||
.pipe(
|
.pipe(
|
||||||
map(this.restExtractor.extractDataBool),
|
map(this.restExtractor.extractDataBool),
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
<ng-template #modal>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 i18n class="modal-title">Report comment</h4>
|
||||||
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<form novalidate [formGroup]="form" (ngSubmit)="report()">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-5 form-group">
|
||||||
|
|
||||||
|
<label i18n for="reportPredefinedReasons">What is the issue?</label>
|
||||||
|
|
||||||
|
<div class="ml-2 mt-2 d-flex flex-column">
|
||||||
|
<ng-container formGroupName="predefinedReasons">
|
||||||
|
|
||||||
|
<div class="form-group" *ngFor="let reason of predefinedReasons">
|
||||||
|
<my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label">
|
||||||
|
<ng-template *ngIf="reason.help" ptTemplate="help">
|
||||||
|
<div [innerHTML]="reason.help"></div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-container *ngIf="reason.description" ngProjectAs="description">
|
||||||
|
<div [innerHTML]="reason.description"></div>
|
||||||
|
</ng-container>
|
||||||
|
</my-peertube-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-7">
|
||||||
|
<div i18n class="information">
|
||||||
|
Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemoteComment()"> and will be forwarded to the comment origin ({{ originHost }}) too</ng-container>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea
|
||||||
|
i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus
|
||||||
|
[ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control"
|
||||||
|
></textarea>
|
||||||
|
<div *ngIf="formErrors.reason" class="form-error">
|
||||||
|
{{ formErrors.reason }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group inputs">
|
||||||
|
<input
|
||||||
|
type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
|
||||||
|
(click)="hide()" (key.enter)="hide()"
|
||||||
|
>
|
||||||
|
<input type="submit" i18n-value value="Submit" class="action-button-submit" [disabled]="!form.valid">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,11 @@
|
||||||
|
@import 'variables';
|
||||||
|
@import 'mixins';
|
||||||
|
|
||||||
|
.information {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
@include peertube-textarea(100%, 100px);
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import { AbuseService } from './abuse.service'
|
||||||
import { VideoBlockComponent } from './video-block.component'
|
import { VideoBlockComponent } from './video-block.component'
|
||||||
import { VideoBlockService } from './video-block.service'
|
import { VideoBlockService } from './video-block.service'
|
||||||
import { VideoReportComponent } from './video-report.component'
|
import { VideoReportComponent } from './video-report.component'
|
||||||
|
import { CommentReportComponent } from './comment-report.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -25,7 +26,8 @@ import { VideoReportComponent } from './video-report.component'
|
||||||
UserModerationDropdownComponent,
|
UserModerationDropdownComponent,
|
||||||
VideoBlockComponent,
|
VideoBlockComponent,
|
||||||
VideoReportComponent,
|
VideoReportComponent,
|
||||||
BatchDomainsModalComponent
|
BatchDomainsModalComponent,
|
||||||
|
CommentReportComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -33,7 +35,8 @@ import { VideoReportComponent } from './video-report.component'
|
||||||
UserModerationDropdownComponent,
|
UserModerationDropdownComponent,
|
||||||
VideoBlockComponent,
|
VideoBlockComponent,
|
||||||
VideoReportComponent,
|
VideoReportComponent,
|
||||||
BatchDomainsModalComponent
|
BatchDomainsModalComponent,
|
||||||
|
CommentReportComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
@ -16,6 +16,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
@Input() user: User
|
@Input() user: User
|
||||||
@Input() account: Account
|
@Input() account: Account
|
||||||
|
@Input() prependActions: DropdownAction<{ user: User, account: Account }>[]
|
||||||
|
|
||||||
@Input() buttonSize: 'normal' | 'small' = 'normal'
|
@Input() buttonSize: 'normal' | 'small' = 'normal'
|
||||||
@Input() placement = 'left-top left-bottom auto'
|
@Input() placement = 'left-top left-bottom auto'
|
||||||
|
@ -250,6 +251,12 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
|
||||||
private buildActions () {
|
private buildActions () {
|
||||||
this.userActions = []
|
this.userActions = []
|
||||||
|
|
||||||
|
if (this.prependActions) {
|
||||||
|
this.userActions = [
|
||||||
|
this.prependActions
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
if (this.authService.isLoggedIn()) {
|
if (this.authService.isLoggedIn()) {
|
||||||
const authUser = this.authService.getUser()
|
const authUser = this.authService.getUser()
|
||||||
|
|
||||||
|
|
|
@ -14,16 +14,19 @@
|
||||||
|
|
||||||
<div class="ml-2 mt-2 d-flex flex-column">
|
<div class="ml-2 mt-2 d-flex flex-column">
|
||||||
<ng-container formGroupName="predefinedReasons">
|
<ng-container formGroupName="predefinedReasons">
|
||||||
|
|
||||||
<div class="form-group" *ngFor="let reason of predefinedReasons">
|
<div class="form-group" *ngFor="let reason of predefinedReasons">
|
||||||
<my-peertube-checkbox formControlName="{{reason.id}}" labelText="{{reason.label}}">
|
<my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label">
|
||||||
<ng-template *ngIf="reason.help" ptTemplate="help">
|
<ng-template *ngIf="reason.help" ptTemplate="help">
|
||||||
<div [innerHTML]="reason.help"></div>
|
<div [innerHTML]="reason.help"></div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-container *ngIf="reason.description" ngProjectAs="description">
|
<ng-container *ngIf="reason.description" ngProjectAs="description">
|
||||||
<div [innerHTML]="reason.description"></div>
|
<div [innerHTML]="reason.description"></div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -73,7 +76,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<textarea
|
<textarea
|
||||||
i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus
|
i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus
|
||||||
[ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control"
|
[ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
|
@ -79,48 +79,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.predefinedReasons = [
|
this.predefinedReasons = this.abuseService.getPrefefinedReasons('video')
|
||||||
{
|
|
||||||
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.embedHtml = this.getVideoEmbed()
|
this.embedHtml = this.getVideoEmbed()
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,7 +140,7 @@ export enum ScopeNames {
|
||||||
model: VideoModel.unscoped(),
|
model: VideoModel.unscoped(),
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
attributes: [ 'filename', 'fileUrl' ],
|
attributes: [ 'filename', 'fileUrl', 'type' ],
|
||||||
model: ThumbnailModel
|
model: ThumbnailModel
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -395,6 +395,8 @@ export class AbuseModel extends Model<AbuseModel> {
|
||||||
|
|
||||||
comment = {
|
comment = {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
|
threadId: entity.getThreadId(),
|
||||||
|
|
||||||
text: entity.text ?? '',
|
text: entity.text ?? '',
|
||||||
|
|
||||||
deleted: entity.isDeleted(),
|
deleted: entity.isDeleted(),
|
||||||
|
|
|
@ -655,7 +655,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
threadId: this.originCommentId || this.id,
|
threadId: this.getThreadId(),
|
||||||
inReplyToCommentId: this.inReplyToCommentId || null,
|
inReplyToCommentId: this.inReplyToCommentId || null,
|
||||||
videoId: this.videoId,
|
videoId: this.videoId,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
|
|
|
@ -25,6 +25,7 @@ export interface VideoAbuse {
|
||||||
|
|
||||||
export interface VideoCommentAbuse {
|
export interface VideoCommentAbuse {
|
||||||
id: number
|
id: number
|
||||||
|
threadId: number
|
||||||
|
|
||||||
video: {
|
video: {
|
||||||
id: number
|
id: number
|
||||||
|
|
Loading…
Reference in New Issue