Add ability to report account

pull/2962/head
Chocobozzz 2020-07-09 15:54:24 +02:00 committed by Chocobozzz
parent 8ca56654a1
commit cfde28bac3
33 changed files with 514 additions and 214 deletions

View File

@ -22,6 +22,7 @@
<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
<my-user-moderation-dropdown
[prependActions]="prependModerationActions"
buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto"
(userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
></my-user-moderation-dropdown>
@ -50,3 +51,7 @@
<router-outlet></router-outlet>
</div>
</div>
<ng-container *ngIf="prependModerationActions">
<my-account-report #accountReportModal [account]="account"></my-account-report>
</ng-container>

View File

@ -1,9 +1,10 @@
import { Subscription } from 'rxjs'
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core'
import { Account, AccountService, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { AccountReportComponent } from '@app/shared/shared-moderation'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { User, UserRight } from '@shared/models'
@ -12,6 +13,8 @@ import { User, UserRight } from '@shared/models'
styleUrls: [ './accounts.component.scss' ]
})
export class AccountsComponent implements OnInit, OnDestroy {
@ViewChild('accountReportModal') accountReportModal: AccountReportComponent
account: Account
accountUser: User
videoChannels: VideoChannel[] = []
@ -20,6 +23,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
isAccountManageable = false
accountFollowerTitle = ''
prependModerationActions: DropdownAction<any>[]
private routeSub: Subscription
constructor (
@ -42,24 +47,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
map(params => params[ 'accountId' ]),
distinctUntilChanged(),
switchMap(accountId => this.accountService.getAccount(accountId)),
tap(account => {
this.account = account
if (this.authService.isLoggedIn()) {
this.authService.userInformationLoaded.subscribe(
() => {
this.isAccountManageable = this.account.userId && this.account.userId === this.authService.getUser().id
this.accountFollowerTitle = this.i18n(
'{{followers}} direct account followers',
{ followers: this.subscribersDisplayFor(account.followersCount) }
)
}
)
}
this.getUserIfNeeded(account)
}),
tap(account => this.onAccount(account)),
switchMap(account => this.videoChannelService.listAccountVideoChannels(account)),
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ]))
)
@ -107,6 +95,41 @@ export class AccountsComponent implements OnInit, OnDestroy {
return this.i18n('{count, plural, =1 {1 subscriber} other {{{count}} subscribers}}', { count })
}
private onAccount (account: Account) {
this.prependModerationActions = undefined
this.account = account
if (this.authService.isLoggedIn()) {
this.authService.userInformationLoaded.subscribe(
() => {
this.isAccountManageable = this.account.userId && this.account.userId === this.authService.getUser().id
this.accountFollowerTitle = this.i18n(
'{{followers}} direct account followers',
{ followers: this.subscribersDisplayFor(account.followersCount) }
)
// It's not our account, we can report it
if (!this.isAccountManageable) {
this.prependModerationActions = [
{
label: this.i18n('Report account'),
handler: () => this.showReportModal()
}
]
}
}
)
}
this.getUserIfNeeded(account)
}
private showReportModal () {
this.accountReportModal.show()
}
private getUserIfNeeded (account: Account) {
if (!account.userId || !this.authService.isLoggedIn()) return

View File

@ -1,6 +1,6 @@
<p-table
[value]="abuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true"
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true" [lazyLoadOnInit]="false"
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
(onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
@ -128,6 +128,22 @@
</td>
</ng-container>
<ng-container *ngIf="!abuse.comment && !abuse.video">
<td *ngIf="abuse.flaggedAccount">
<a [href]="getAccountUrl(abuse)" class="table-account-link" target="_blank" rel="noopener noreferrer">
<span>{{ abuse.flaggedAccount.displayName }}</span>
<span class="account-flagged-handle">{{ abuse.flaggedAccount.nameWithHostForced }}</span>
</a>
</td>
<td i18n *ngIf="!abuse.flaggedAccount">
Account deleted
</td>
</ng-container>
<td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
<td class="c-hand abuse-states" [pRowToggler]="abuse">

View File

@ -1,3 +1,5 @@
import * as debug from 'debug'
import truncate from 'lodash-es/truncate'
import { SortMeta } from 'primeng/api'
import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
import { environment } from 'src/environments/environment'
@ -7,11 +9,15 @@ 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 { VideoCommentService } from '@app/shared/shared-video-comment'
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'
const logger = debug('peertube:moderation:AbuseListComponent')
// Don't use an abuse model because we need external services to compute some properties
// And this model is only used in this component
export type ProcessedAbuse = Abuse & {
moderationCommentHtml?: string,
reasonHtml?: string
@ -45,12 +51,13 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
abuseActions: DropdownAction<Abuse>[][] = []
abuseActions: DropdownAction<ProcessedAbuse>[][] = []
constructor (
private notifier: Notifier,
private abuseService: AbuseService,
private blocklistService: BlocklistService,
private commentService: VideoCommentService,
private videoService: VideoService,
private videoBlocklistService: VideoBlockService,
private confirmService: ConfirmService,
@ -63,140 +70,15 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
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 && !abuse.video.deleted
},
{
label: this.i18n('Block video'),
isDisplayed: abuse => abuse.video && !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.buildInternalActions(),
this.updateAbuseState(abuse, AbuseState.ACCEPTED)
},
this.buildFlaggedAccountActions(),
err => this.notifier.error(err.message)
)
}
},
{
label: this.i18n('Unblock video'),
isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
handler: abuse => {
this.videoBlocklistService.unblockVideo(abuse.video.id)
.subscribe(
() => {
this.notifier.success(this.i18n('Video unblocked.'))
this.buildCommentActions(),
this.updateAbuseState(abuse, AbuseState.ACCEPTED)
},
this.buildVideoActions(),
err => this.notifier.error(err.message)
)
}
},
{
label: this.i18n('Delete video'),
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?'),
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,
isDisplayed: abuse => !!abuse.reporterAccount
},
{
label: this.i18n('Mute reporter'),
isDisplayed: abuse => !!abuse.reporterAccount,
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 && !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)
)
}
}
]
this.buildAccountActions()
]
}
@ -207,6 +89,8 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
.subscribe(params => {
this.search = params.search || ''
logger('On URL change (search: %s).', this.search)
this.setTableFilter(this.search)
this.loadData()
})
@ -264,6 +148,10 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
}
getAccountUrl (abuse: ProcessedAbuse) {
return '/accounts/' + abuse.flaggedAccount.nameWithHost
}
getVideoEmbed (abuse: Abuse) {
return buildVideoEmbed(
buildVideoLink({
@ -304,6 +192,8 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
}
protected loadData () {
logger('Load data.')
return this.abuseService.getAbuses({
pagination: this.pagination,
sort: this.sort,
@ -356,6 +246,208 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
)
}
private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
return [
{
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)
}
]
}
private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
return [
{
label: this.i18n('Actions for the flagged account'),
isHeader: true,
isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video
},
{
label: this.i18n('Mute account'),
isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
handler: abuse => this.muteAccountHelper(abuse.flaggedAccount)
},
{
label: this.i18n('Mute server account'),
isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host)
}
]
}
private buildAccountActions (): DropdownAction<ProcessedAbuse>[] {
return [
{
label: this.i18n('Actions for the reporter'),
isHeader: true,
isDisplayed: abuse => !!abuse.reporterAccount
},
{
label: this.i18n('Mute reporter'),
isDisplayed: abuse => !!abuse.reporterAccount,
handler: abuse => this.muteAccountHelper(abuse.reporterAccount)
},
{
label: this.i18n('Mute server'),
isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
handler: abuse => this.muteServerHelper(abuse.reporterAccount.host)
}
]
}
private buildVideoActions (): DropdownAction<ProcessedAbuse>[] {
return [
{
label: this.i18n('Actions for the video'),
isHeader: true,
isDisplayed: abuse => abuse.video && !abuse.video.deleted
},
{
label: this.i18n('Block video'),
isDisplayed: abuse => abuse.video && !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 && !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 && !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)
)
}
}
]
}
private buildCommentActions (): DropdownAction<ProcessedAbuse>[] {
return [
{
label: this.i18n('Actions for the comment'),
isHeader: true,
isDisplayed: abuse => abuse.comment && !abuse.comment.deleted
},
{
label: this.i18n('Delete comment'),
isDisplayed: abuse => abuse.comment && !abuse.comment.deleted,
handler: async abuse => {
const res = await this.confirmService.confirm(
this.i18n('Do you really want to delete this comment?'),
this.i18n('Delete')
)
if (res === false) return
this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id)
.subscribe(
() => {
this.notifier.success(this.i18n('Comment deleted.'))
this.updateAbuseState(abuse, AbuseState.ACCEPTED)
},
err => this.notifier.error(err.message)
)
}
}
]
}
private muteAccountHelper (account: 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)
)
}
private muteServerHelper (host: string) {
this.blocklistService.blockServerByInstance(host)
.subscribe(
() => {
this.notifier.success(
this.i18n('Server {{host}} muted by the instance.', { host: host })
)
},
err => this.notifier.error(err.message)
)
}
private toHtml (text: string) {
return this.markdownRenderer.textMarkdownToHTML(text)
}

View File

@ -96,7 +96,8 @@ my-action-dropdown.show {
top: 3px;
}
.table-comment-link {
.table-comment-link,
.table-account-link {
@include disable-outline;
color: var(--mainForegroundColor);
@ -106,7 +107,13 @@ my-action-dropdown.show {
}
}
.comment-flagged-account {
.table-account-link {
display: flex;
flex-direction: column;
}
.comment-flagged-account,
.account-flagged-handle {
font-size: 11px;
color: var(--greyForegroundColor);
}

View File

@ -4,10 +4,9 @@ import { Router } from '@angular/router'
import { Notifier, User } from '@app/core'
import { FormReactive, FormValidatorService, VideoCommentValidatorsService } from '@app/shared/shared-forms'
import { Video } from '@app/shared/shared-main'
import { VideoComment, VideoCommentService } from '@app/shared/shared-video-comment'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { VideoCommentCreate } from '@shared/models'
import { VideoComment } from './video-comment.model'
import { VideoCommentService } from './video-comment.service'
@Component({
selector: 'my-video-comment-add',

View File

@ -3,11 +3,10 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild }
import { MarkdownService, Notifier, UserService } from '@app/core'
import { AuthService } from '@app/core/auth'
import { Account, Actor, DropdownAction, Video } from '@app/shared/shared-main'
import { CommentReportComponent } from '@app/shared/shared-moderation/comment-report.component'
import { CommentReportComponent } from '@app/shared/shared-moderation/report-modals/comment-report.component'
import { VideoComment, VideoCommentThreadTree } from '@app/shared/shared-video-comment'
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'
@Component({
selector: 'my-video-comment',
@ -136,7 +135,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
this.comment.account = null
}
if (this.isUserLoggedIn()) {
if (this.isUserLoggedIn() && this.authService.getUser().account.id !== this.comment.account.id) {
this.prependModerationActions = [
{
label: this.i18n('Report comment'),

View File

@ -4,10 +4,8 @@ import { ActivatedRoute } from '@angular/router'
import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifier, User } from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service'
import { Syndication, VideoDetails } from '@app/shared/shared-main'
import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { VideoCommentThreadTree } from './video-comment-thread-tree.model'
import { VideoComment } from './video-comment.model'
import { VideoCommentService } from './video-comment.service'
@Component({
selector: 'my-video-comments',

View File

@ -5,16 +5,17 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons'
import { SharedMainModule } from '@app/shared/shared-main'
import { SharedModerationModule } from '@app/shared/shared-moderation'
import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
import { RecommendationsModule } from './recommendations/recommendations.module'
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
import { VideoCommentService } from '../../shared/shared-video-comment/video-comment.service'
import { VideoCommentAddComponent } from './comment/video-comment-add.component'
import { VideoCommentComponent } from './comment/video-comment.component'
import { VideoCommentService } from './comment/video-comment.service'
import { VideoCommentsComponent } from './comment/video-comments.component'
import { VideoShareComponent } from './modal/video-share.component'
import { VideoSupportComponent } from './modal/video-support.component'
import { RecommendationsModule } from './recommendations/recommendations.module'
import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive'
import { VideoDurationPipe } from './video-duration-formatter.pipe'
import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
@ -34,7 +35,8 @@ import { VideoWatchComponent } from './video-watch.component'
SharedVideoPlaylistModule,
SharedUserSubscriptionModule,
SharedModerationModule,
SharedGlobalIconModule
SharedGlobalIconModule,
SharedVideoCommentModule
],
declarations: [

View File

@ -3,6 +3,9 @@ import { LazyLoadEvent, SortMeta } from 'primeng/api'
import { RestPagination } from './rest-pagination'
import { Subject } from 'rxjs'
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
import * as debug from 'debug'
const logger = debug('peertube:tables:RestTable')
export abstract class RestTable {
@ -15,7 +18,7 @@ export abstract class RestTable {
rowsPerPage = this.rowsPerPageOptions[0]
expandedRows = {}
private searchStream: Subject<string>
protected searchStream: Subject<string>
abstract getIdentifier (): string
@ -37,6 +40,8 @@ export abstract class RestTable {
}
loadLazy (event: LazyLoadEvent) {
logger('Load lazy %o.', event)
this.sort = {
order: event.sortOrder,
field: event.sortField
@ -65,6 +70,9 @@ export abstract class RestTable {
)
.subscribe(search => {
this.search = search
logger('On search %s.', this.search)
this.loadData()
})
}
@ -75,14 +83,18 @@ export abstract class RestTable {
}
onPage (event: { first: number, rows: number }) {
logger('On page %o.', event)
if (this.rowsPerPage !== event.rows) {
this.rowsPerPage = event.rows
this.pagination = {
start: event.first,
count: this.rowsPerPage
}
this.loadData()
}
this.expandedRows = {}
}

View File

@ -14,6 +14,8 @@ export abstract class Actor implements ActorServer {
avatarUrl: string
isLocal: boolean
static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) {
if (actor?.avatar?.url) return actor.avatar.url
@ -52,6 +54,10 @@ export abstract class Actor implements ActorServer {
this.avatar = hash.avatar
const absoluteAPIUrl = getAbsoluteAPIUrl()
const thisHost = new URL(absoluteAPIUrl).host
this.isLocal = this.host.trim() === thisHost
this.updateComputedAttributes()
}

View File

@ -34,7 +34,9 @@ export class UserNotification implements UserNotificationServer {
threadId: number
video: {
id: number
uuid: string
name: string
}
}
@ -115,13 +117,15 @@ export class UserNotification implements UserNotificationServer {
case UserNotificationType.COMMENT_MENTION:
if (!this.comment) break
this.accountUrl = this.buildAccountUrl(this.comment.account)
this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ]
this.commentUrl = this.buildCommentUrl(this.comment)
break
case UserNotificationType.NEW_ABUSE_FOR_MODERATORS:
this.abuseUrl = '/admin/moderation/abuses/list'
if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video)
else if (this.abuse.comment) this.commentUrl = this.buildCommentUrl(this.abuse.comment)
else if (this.abuse.account) this.accountUrl = this.buildAccountUrl(this.abuse.account)
break
case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
@ -190,6 +194,10 @@ export class UserNotification implements UserNotificationServer {
return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
}
private buildCommentUrl (comment: { video: { uuid: string }, threadId: number }) {
return [ this.buildVideoUrl(comment.video), { threadId: comment.threadId } ]
}
private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) {
actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor)
}

View File

@ -45,9 +45,22 @@
<ng-container *ngSwitchCase="UserNotificationType.NEW_ABUSE_FOR_MODERATORS">
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
<div class="message" i18n>
<div class="message" *ngIf="notification.videoUrl" i18n>
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.abuse.video.name }}</a>
</div>
<div class="message" *ngIf="notification.commentUrl" i18n>
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new comment abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.abuse.comment.video.name }}</a>
</div>
<div class="message" *ngIf="notification.accountUrl" i18n>
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new account abuse</a> has been created on account <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.abuse.account.displayName }}</a>
</div>
<!-- Deleted entity associated to the abuse -->
<div class="message" *ngIf="!notification.videoUrl && !notification.commentUrl && !notification.accountUrl" i18n>
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new abuse</a> has been created
</div>
</ng-container>
<ng-container *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS">

View File

@ -1,11 +0,0 @@
@import 'variables';
@import 'mixins';
.information {
margin-bottom: 20px;
}
textarea {
@include peertube-textarea(100%, 100px);
}

View File

@ -1,3 +1,5 @@
export * from './report-modals'
export * from './abuse.service'
export * from './account-block.model'
export * from './account-blocklist.component'
@ -9,5 +11,4 @@ export * from './user-ban-modal.component'
export * from './user-moderation-dropdown.component'
export * from './video-block.component'
export * from './video-block.service'
export * from './video-report.component'
export * from './shared-moderation.module'

View File

@ -0,0 +1,94 @@
import { mapValues, pickBy } from 'lodash-es'
import { Component, Input, OnInit, ViewChild } from '@angular/core'
import { Notifier } from '@app/core'
import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { Account } from '@app/shared/shared-main'
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-account-report',
templateUrl: './report.component.html',
styleUrls: [ './report.component.scss' ]
})
export class AccountReportComponent extends FormReactive implements OnInit {
@Input() account: Account = null
@ViewChild('modal', { static: true }) modal: NgbModal
error: string = null
predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
modalTitle: string
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.isRemote()) {
return this.account.host
}
return ''
}
ngOnInit () {
this.modalTitle = this.i18n('Report {{displayName}}', { displayName: this.account.displayName })
this.buildForm({
reason: this.abuseValidatorsService.ABUSE_REASON,
predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null)
})
this.predefinedReasons = this.abuseService.getPrefefinedReasons('account')
}
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,
account: {
id: this.account.id
}
}).subscribe(
() => {
this.notifier.success(this.i18n('Account reported.'))
this.hide()
},
err => this.notifier.error(err.message)
)
}
isRemote () {
return !this.account.isLocal
}
}

View File

@ -1,28 +1,27 @@
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 { VideoComment } from '@app/shared/shared-video-comment'
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'
import { AbuseService } from '../abuse.service'
@Component({
selector: 'my-comment-report',
templateUrl: './comment-report.component.html',
styleUrls: [ './comment-report.component.scss' ]
templateUrl: './report.component.html',
styleUrls: [ './report.component.scss' ]
})
export class CommentReportComponent extends FormReactive implements OnInit {
@Input() comment: VideoComment = null
@ViewChild('modal', { static: true }) modal: NgbModal
modalTitle: string
error: string = null
predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
embedHtml: SafeHtml
private openedModal: NgbModalRef
@ -42,7 +41,7 @@ export class CommentReportComponent extends FormReactive implements OnInit {
}
get originHost () {
if (this.isRemoteComment()) {
if (this.isRemote()) {
return this.comment.account.host
}
@ -50,6 +49,8 @@ export class CommentReportComponent extends FormReactive implements OnInit {
}
ngOnInit () {
this.modalTitle = this.i18n('Report comment')
this.buildForm({
reason: this.abuseValidatorsService.ABUSE_REASON,
predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null)
@ -87,7 +88,7 @@ export class CommentReportComponent extends FormReactive implements OnInit {
)
}
isRemoteComment () {
isRemote () {
return !this.comment.isLocal
}
}

View File

@ -0,0 +1,3 @@
export * from './account-report.component'
export * from './comment-report.component'
export * from './video-report.component'

View File

@ -1,6 +1,6 @@
<ng-template #modal>
<div class="modal-header">
<h4 i18n class="modal-title">Report comment</h4>
<h4 class="modal-title">{{ modalTitle }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
</div>
@ -34,7 +34,7 @@
<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>.
Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemote()"> and will be forwarded to the comment origin ({{ originHost }}) too</ng-container>.
</div>
<div class="form-group">

View File

@ -72,7 +72,7 @@
</div>
<div i18n class="information">
Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemoteVideo()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>.
Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemote()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>.
</div>
<div class="form-group">

View File

@ -8,13 +8,13 @@ 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 { Video } from '../shared-main'
import { AbuseService } from './abuse.service'
import { Video } from '../../shared-main'
import { AbuseService } from '../abuse.service'
@Component({
selector: 'my-video-report',
templateUrl: './video-report.component.html',
styleUrls: [ './video-report.component.scss' ]
styleUrls: [ './report.component.scss' ]
})
export class VideoReportComponent extends FormReactive implements OnInit {
@Input() video: Video = null
@ -44,7 +44,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
}
get originHost () {
if (this.isRemoteVideo()) {
if (this.isRemote()) {
return this.video.account.host
}
@ -116,7 +116,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
)
}
isRemoteVideo () {
isRemote () {
return !this.video.isLocal
}
}

View File

@ -3,22 +3,23 @@ import { NgModule } from '@angular/core'
import { SharedFormModule } from '../shared-forms/shared-form.module'
import { SharedGlobalIconModule } from '../shared-icons'
import { SharedMainModule } from '../shared-main/shared-main.module'
import { SharedVideoCommentModule } from '../shared-video-comment'
import { AbuseService } from './abuse.service'
import { BatchDomainsModalComponent } from './batch-domains-modal.component'
import { BlocklistService } from './blocklist.service'
import { BulkService } from './bulk.service'
import { UserBanModalComponent } from './user-ban-modal.component'
import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
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'
import { VideoReportComponent, AccountReportComponent, CommentReportComponent } from './report-modals'
@NgModule({
imports: [
SharedMainModule,
SharedFormModule,
SharedGlobalIconModule
SharedGlobalIconModule,
SharedVideoCommentModule
],
declarations: [
@ -27,7 +28,8 @@ import { CommentReportComponent } from './comment-report.component'
VideoBlockComponent,
VideoReportComponent,
BatchDomainsModalComponent,
CommentReportComponent
CommentReportComponent,
AccountReportComponent
],
exports: [
@ -36,7 +38,8 @@ import { CommentReportComponent } from './comment-report.component'
VideoBlockComponent,
VideoReportComponent,
BatchDomainsModalComponent,
CommentReportComponent
CommentReportComponent,
AccountReportComponent
],
providers: [

View File

@ -0,0 +1,5 @@
export * from './video-comment.service'
export * from './video-comment.model'
export * from './video-comment-thread-tree.model'
export * from './shared-video-comment.module'

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core'
import { SharedMainModule } from '../shared-main/shared-main.module'
import { VideoCommentService } from './video-comment.service'
@NgModule({
imports: [
SharedMainModule
],
declarations: [ ],
exports: [ ],
providers: [
VideoCommentService
]
})
export class SharedVideoCommentModule { }

View File

@ -11,7 +11,7 @@ import {
VideoCommentCreate,
VideoCommentThreadTree as VideoCommentThreadTreeServerModel
} from '@shared/models'
import { environment } from '../../../../environments/environment'
import { environment } from '../../../environments/environment'
import { VideoCommentThreadTree } from './video-comment-thread-tree.model'
import { VideoComment } from './video-comment.model'

View File

@ -311,7 +311,8 @@ class Emailer {
videoPublishedAt: new Date(video.publishedAt).toLocaleString(),
videoName: video.name,
reason: abuse.reason,
videoChannel: video.VideoChannel,
videoChannel: abuse.video.channel,
reporter,
action
}
}
@ -330,6 +331,7 @@ class Emailer {
commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
reason: abuse.reason,
flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(),
reporter,
action
}
}
@ -346,6 +348,7 @@ class Emailer {
accountDisplayName: account.getDisplayName(),
isLocal: account.isOwned(),
reason: abuse.reason,
reporter,
action
}
}

View File

@ -6,8 +6,8 @@ block title
block content
p
| #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}account "
a(href=accountUrl) #{accountDisplayName}
| #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}account
a(href=accountUrl) #{accountDisplayName}
p The reporter, #{reporter}, cited the following reason(s):
blockquote #{reason}

View File

@ -6,10 +6,10 @@ block title
block content
p
| #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}comment "
a(href=commentUrl) on video #{videoName}
| of #{flaggedAccount}
| created on #{commentCreatedAt}
| #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}
a(href=commentUrl) comment on video "#{videoName}"
| of #{flaggedAccount}
| created on #{commentCreatedAt}
p The reporter, #{reporter}, cited the following reason(s):
blockquote #{reason}

View File

@ -62,9 +62,9 @@ export interface Abuse {
// FIXME: deprecated in 2.3, remove the following properties
// @deprecated
startAt: null
startAt?: null
// @deprecated
endAt: null
endAt?: null
// @deprecated
count?: number

View File

@ -73,7 +73,9 @@ export interface UserNotification {
threadId: number
video: {
id: number
uuid: string
name: string
}
}