Add abuse messages management in my account
This commit is contained in:
parent
441e453ae5
commit
94148c9028
|
@ -2,6 +2,7 @@ import { ChartModule } from 'primeng/chart'
|
||||||
import { SelectButtonModule } from 'primeng/selectbutton'
|
import { SelectButtonModule } from 'primeng/selectbutton'
|
||||||
import { TableModule } from 'primeng/table'
|
import { TableModule } from 'primeng/table'
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
|
import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
|
||||||
import { SharedFormModule } from '@app/shared/shared-forms'
|
import { SharedFormModule } from '@app/shared/shared-forms'
|
||||||
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
||||||
import { SharedMainModule } from '@app/shared/shared-main'
|
import { SharedMainModule } from '@app/shared/shared-main'
|
||||||
|
@ -14,10 +15,9 @@ import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponen
|
||||||
import { FollowingListComponent } from './follows/following-list/following-list.component'
|
import { FollowingListComponent } from './follows/following-list/following-list.component'
|
||||||
import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
|
import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
|
||||||
import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
|
import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
|
||||||
import { ModerationCommentModalComponent, AbuseListComponent, VideoBlockListComponent } from './moderation'
|
import { AbuseListComponent, VideoBlockListComponent } from './moderation'
|
||||||
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
|
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
|
||||||
import { ModerationComponent } from './moderation/moderation.component'
|
import { ModerationComponent } from './moderation/moderation.component'
|
||||||
import { AbuseDetailsComponent } from './moderation/abuse-list/abuse-details.component'
|
|
||||||
import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component'
|
import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component'
|
||||||
import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component'
|
import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component'
|
||||||
import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component'
|
import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component'
|
||||||
|
@ -36,6 +36,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
|
||||||
SharedFormModule,
|
SharedFormModule,
|
||||||
SharedModerationModule,
|
SharedModerationModule,
|
||||||
SharedGlobalIconModule,
|
SharedGlobalIconModule,
|
||||||
|
SharedAbuseListModule,
|
||||||
|
|
||||||
TableModule,
|
TableModule,
|
||||||
SelectButtonModule,
|
SelectButtonModule,
|
||||||
|
@ -60,11 +61,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
|
||||||
|
|
||||||
ModerationComponent,
|
ModerationComponent,
|
||||||
VideoBlockListComponent,
|
VideoBlockListComponent,
|
||||||
|
|
||||||
AbuseListComponent,
|
AbuseListComponent,
|
||||||
AbuseDetailsComponent,
|
|
||||||
|
|
||||||
ModerationCommentModalComponent,
|
|
||||||
InstanceServerBlocklistComponent,
|
InstanceServerBlocklistComponent,
|
||||||
InstanceAccountBlocklistComponent,
|
InstanceAccountBlocklistComponent,
|
||||||
|
|
||||||
|
|
|
@ -3,195 +3,4 @@
|
||||||
<ng-container i18n>Reports</ng-container>
|
<ng-container i18n>Reports</ng-container>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p-table
|
<my-abuse-list-table viewType="admin" baseRoute="/admin/moderation/abuses/list"></my-abuse-list-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" [lazyLoadOnInit]="false"
|
|
||||||
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
|
||||||
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
|
|
||||||
(onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
|
|
||||||
>
|
|
||||||
<ng-template pTemplate="caption">
|
|
||||||
<div class="caption">
|
|
||||||
<div class="ml-auto">
|
|
||||||
<div class="input-group has-feedback has-clear">
|
|
||||||
<div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
|
|
||||||
<div class="input-group-text" ngbDropdownToggle>
|
|
||||||
<span class="caret" aria-haspopup="menu" role="button"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div role="menu" ngbDropdownMenu>
|
|
||||||
<h6 class="dropdown-header" i18n>Advanced report filters</h6>
|
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
|
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
|
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
|
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
|
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
|
||||||
(keyup)="onAbuseSearch($event)"
|
|
||||||
>
|
|
||||||
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
|
|
||||||
<span class="sr-only" i18n>Clear filters</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template pTemplate="header">
|
|
||||||
<tr> <!-- header -->
|
|
||||||
<th style="width: 40px;"></th>
|
|
||||||
<th style="width: 20%;" pResizableColumn i18n>Reporter</th>
|
|
||||||
<th i18n>Video/Comment/Account</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 style="width: 80px;">Messages</th>
|
|
||||||
<th style="width: 150px;"></th>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template pTemplate="body" let-expanded="expanded" let-abuse>
|
|
||||||
<tr>
|
|
||||||
<td class="c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
|
|
||||||
<span class="expander">
|
|
||||||
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<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">
|
|
||||||
<img
|
|
||||||
class="avatar"
|
|
||||||
[src]="abuse.reporterAccount.avatar?.path"
|
|
||||||
(error)="switchToDefaultAvatar($event)"
|
|
||||||
alt="Avatar"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{{ abuse.reporterAccount.displayName }}
|
|
||||||
<span>{{ abuse.reporterAccount.nameWithHost }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<span i18n *ngIf="!abuse.reporterAccount">
|
|
||||||
Deleted account
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<ng-container *ngIf="abuse.video">
|
|
||||||
|
|
||||||
<td *ngIf="!abuse.video.deleted">
|
|
||||||
<a [href]="getVideoUrl(abuse)" class="table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
|
|
||||||
<div class="table-video">
|
|
||||||
<div class="table-video-image">
|
|
||||||
<img [src]="abuse.video.thumbnailPath">
|
|
||||||
<span
|
|
||||||
class="table-video-image-label" *ngIf="abuse.count > 1"
|
|
||||||
i18n-title title="This video has been reported multiple times."
|
|
||||||
>
|
|
||||||
{{ abuse.nth }}/{{ abuse.count }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-video-text">
|
|
||||||
<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 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" 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>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<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="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)">
|
|
||||||
{{ abuse.countMessages }}
|
|
||||||
|
|
||||||
<my-global-icon iconName="message-circle"></my-global-icon>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="action-cell">
|
|
||||||
<my-action-dropdown
|
|
||||||
[ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
|
|
||||||
i18n-label label="Actions" [actions]="abuseActions" [entry]="abuse"
|
|
||||||
></my-action-dropdown>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template pTemplate="rowexpansion" let-abuse>
|
|
||||||
<tr>
|
|
||||||
<td class="expand-cell" colspan="6">
|
|
||||||
<my-abuse-details [abuse]="abuse"></my-abuse-details>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template pTemplate="emptymessage">
|
|
||||||
<tr>
|
|
||||||
<td colspan="6">
|
|
||||||
<div class="no-results">
|
|
||||||
<ng-container *ngIf="search" i18n>No abuses found matching current filters.</ng-container>
|
|
||||||
<ng-container *ngIf="!search" i18n>No abuses found.</ng-container>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
</p-table>
|
|
||||||
|
|
||||||
<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
|
|
||||||
<my-abuse-message-modal #abuseMessagesModal (countMessagesUpdated)="onCountMessagesUpdated($event)"></my-abuse-message-modal>
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
@import 'mixins';
|
|
||||||
@import 'miniature';
|
|
||||||
|
|
||||||
.video-details-date-updated {
|
|
||||||
font-size: 90%;
|
|
||||||
margin-top: .1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-details-links {
|
|
||||||
@include disable-default-a-behaviour;
|
|
||||||
}
|
|
||||||
|
|
||||||
.abuse-states .glyphicon-comment {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group {
|
|
||||||
@include peertube-input-group(300px);
|
|
||||||
|
|
||||||
.dropdown-toggle::after {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.abuse-messages {
|
|
||||||
my-global-icon {
|
|
||||||
width: 22px;
|
|
||||||
margin-left: 3px;
|
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,474 +1,10 @@
|
||||||
import * as debug from 'debug'
|
import { Component } from '@angular/core'
|
||||||
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'
|
|
||||||
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
|
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router'
|
|
||||||
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
|
|
||||||
import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
|
|
||||||
import { AbuseService, BlocklistService, VideoBlockService, AbuseMessageModalComponent } from '@app/shared/shared-moderation'
|
|
||||||
import { VideoCommentService } from '@app/shared/shared-video-comment'
|
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
|
||||||
import { AdminAbuse, AbuseState } from '@shared/models'
|
|
||||||
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
|
||||||
|
|
||||||
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 = AdminAbuse & {
|
|
||||||
moderationCommentHtml?: string,
|
|
||||||
reasonHtml?: string
|
|
||||||
embedHtml?: SafeHtml
|
|
||||||
updatedAt?: Date
|
|
||||||
|
|
||||||
// override bare server-side definitions with rich client-side definitions
|
|
||||||
reporterAccount?: Account
|
|
||||||
flaggedAccount?: Account
|
|
||||||
|
|
||||||
truncatedCommentHtml?: string
|
|
||||||
commentHtml?: string
|
|
||||||
|
|
||||||
video: AdminAbuse['video'] & {
|
|
||||||
channel: AdminAbuse['video']['channel'] & {
|
|
||||||
ownerAccount: Account
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-abuse-list',
|
selector: 'my-abuse-list',
|
||||||
templateUrl: './abuse-list.component.html',
|
templateUrl: './abuse-list.component.html',
|
||||||
styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ]
|
styleUrls: [ ]
|
||||||
})
|
})
|
||||||
export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit {
|
export class AbuseListComponent {
|
||||||
@ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
|
|
||||||
@ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
|
|
||||||
|
|
||||||
abuses: ProcessedAbuse[] = []
|
|
||||||
totalRecords = 0
|
|
||||||
sort: SortMeta = { field: 'createdAt', order: 1 }
|
|
||||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
|
||||||
|
|
||||||
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,
|
|
||||||
private i18n: I18n,
|
|
||||||
private markdownRenderer: MarkdownService,
|
|
||||||
private sanitizer: DomSanitizer,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
|
|
||||||
this.abuseActions = [
|
|
||||||
this.buildInternalActions(),
|
|
||||||
|
|
||||||
this.buildFlaggedAccountActions(),
|
|
||||||
|
|
||||||
this.buildCommentActions(),
|
|
||||||
|
|
||||||
this.buildVideoActions(),
|
|
||||||
|
|
||||||
this.buildAccountActions()
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.initialize()
|
|
||||||
|
|
||||||
this.route.queryParams
|
|
||||||
.subscribe(params => {
|
|
||||||
this.search = params.search || ''
|
|
||||||
|
|
||||||
logger('On URL change (search: %s).', this.search)
|
|
||||||
|
|
||||||
this.setTableFilter(this.search)
|
|
||||||
this.loadData()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit () {
|
|
||||||
if (this.search) this.setTableFilter(this.search)
|
|
||||||
}
|
|
||||||
|
|
||||||
getIdentifier () {
|
|
||||||
return 'AbuseListComponent'
|
|
||||||
}
|
|
||||||
|
|
||||||
openModerationCommentModal (abuse: AdminAbuse) {
|
|
||||||
this.moderationCommentModal.openModal(abuse)
|
|
||||||
}
|
|
||||||
|
|
||||||
onModerationCommentUpdated () {
|
|
||||||
this.loadData()
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table filter functions */
|
|
||||||
onAbuseSearch (event: Event) {
|
|
||||||
this.onSearch(event)
|
|
||||||
this.setQueryParams((event.target as HTMLInputElement).value)
|
|
||||||
}
|
|
||||||
|
|
||||||
setQueryParams (search: string) {
|
|
||||||
const queryParams: Params = {}
|
|
||||||
if (search) Object.assign(queryParams, { search })
|
|
||||||
|
|
||||||
this.router.navigate([ '/admin/moderation/abuses/list' ], { queryParams })
|
|
||||||
}
|
|
||||||
|
|
||||||
resetTableFilter () {
|
|
||||||
this.setTableFilter('')
|
|
||||||
this.setQueryParams('')
|
|
||||||
this.resetSearch()
|
|
||||||
}
|
|
||||||
/* END Table filter functions */
|
|
||||||
|
|
||||||
isAbuseAccepted (abuse: AdminAbuse) {
|
|
||||||
return abuse.state.id === AbuseState.ACCEPTED
|
|
||||||
}
|
|
||||||
|
|
||||||
isAbuseRejected (abuse: AdminAbuse) {
|
|
||||||
return abuse.state.id === AbuseState.REJECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
getVideoUrl (abuse: AdminAbuse) {
|
|
||||||
return Video.buildClientUrl(abuse.video.uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
getCommentUrl (abuse: AdminAbuse) {
|
|
||||||
return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
|
|
||||||
}
|
|
||||||
|
|
||||||
getAccountUrl (abuse: ProcessedAbuse) {
|
|
||||||
return '/accounts/' + abuse.flaggedAccount.nameWithHost
|
|
||||||
}
|
|
||||||
|
|
||||||
getVideoEmbed (abuse: AdminAbuse) {
|
|
||||||
return buildVideoEmbed(
|
|
||||||
buildVideoLink({
|
|
||||||
baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
|
|
||||||
title: false,
|
|
||||||
warningTitle: false,
|
|
||||||
startTime: abuse.startAt,
|
|
||||||
stopTime: abuse.endAt
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
switchToDefaultAvatar ($event: Event) {
|
|
||||||
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeAbuse (abuse: AdminAbuse) {
|
|
||||||
const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
|
|
||||||
if (res === false) return
|
|
||||||
|
|
||||||
this.abuseService.removeAbuse(abuse).subscribe(
|
|
||||||
() => {
|
|
||||||
this.notifier.success(this.i18n('Abuse deleted.'))
|
|
||||||
this.loadData()
|
|
||||||
},
|
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
|
|
||||||
this.abuseService.updateAbuse(abuse, { state })
|
|
||||||
.subscribe(
|
|
||||||
() => this.loadData(),
|
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
|
|
||||||
const abuse = this.abuses.find(a => a.id === event.abuseId)
|
|
||||||
|
|
||||||
if (!abuse) {
|
|
||||||
console.error('Cannot find abuse %d.', event.abuseId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
abuse.countMessages = event.countMessages
|
|
||||||
}
|
|
||||||
|
|
||||||
openAbuseMessagesModal (abuse: AdminAbuse) {
|
|
||||||
this.abuseMessagesModal.openModal(abuse)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadData () {
|
|
||||||
logger('Load data.')
|
|
||||||
|
|
||||||
return this.abuseService.getAdminAbuses({
|
|
||||||
pagination: this.pagination,
|
|
||||||
sort: this.sort,
|
|
||||||
search: this.search
|
|
||||||
}).subscribe(
|
|
||||||
async resultList => {
|
|
||||||
this.totalRecords = resultList.total
|
|
||||||
|
|
||||||
this.abuses = []
|
|
||||||
|
|
||||||
for (const a of resultList.data) {
|
|
||||||
const abuse = a as ProcessedAbuse
|
|
||||||
|
|
||||||
abuse.reasonHtml = await this.toHtml(abuse.reason)
|
|
||||||
abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment)
|
|
||||||
|
|
||||||
if (abuse.video) {
|
|
||||||
abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
|
|
||||||
|
|
||||||
if (abuse.video.channel?.ownerAccount) {
|
|
||||||
abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abuse.comment) {
|
|
||||||
if (abuse.comment.deleted) {
|
|
||||||
abuse.truncatedCommentHtml = abuse.commentHtml = this.i18n('Deleted comment')
|
|
||||||
} else {
|
|
||||||
const truncated = truncate(abuse.comment.text, { length: 100 })
|
|
||||||
abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true)
|
|
||||||
abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abuse.reporterAccount) {
|
|
||||||
abuse.reporterAccount = new Account(abuse.reporterAccount)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abuse.flaggedAccount) {
|
|
||||||
abuse.flaggedAccount = new Account(abuse.flaggedAccount)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
|
|
||||||
|
|
||||||
this.abuses.push(abuse)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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('Messages'),
|
|
||||||
handler: abuse => this.openAbuseMessagesModal(abuse)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: this.i18n('Add internal 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
export * from './abuse-details.component'
|
|
||||||
export * from './abuse-list.component'
|
export * from './abuse-list.component'
|
||||||
export * from './moderation-comment-modal.component'
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { BlocklistComponentType, GenericAccountBlocklistComponent } from '@app/s
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-instance-account-blocklist',
|
selector: 'my-instance-account-blocklist',
|
||||||
styleUrls: [ '../moderation.component.scss', '../../../shared/shared-moderation/account-blocklist.component.scss' ],
|
styleUrls: [ '../../../shared/shared-moderation/moderation.scss', '../../../shared/shared-moderation/account-blocklist.component.scss' ],
|
||||||
templateUrl: '../../../shared/shared-moderation/account-blocklist.component.html'
|
templateUrl: '../../../shared/shared-moderation/account-blocklist.component.html'
|
||||||
})
|
})
|
||||||
export class InstanceAccountBlocklistComponent extends GenericAccountBlocklistComponent {
|
export class InstanceAccountBlocklistComponent extends GenericAccountBlocklistComponent {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ServerService } from '@app/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './moderation.component.html',
|
templateUrl: './moderation.component.html',
|
||||||
styleUrls: [ './moderation.component.scss' ]
|
styleUrls: [ ]
|
||||||
})
|
})
|
||||||
export class ModerationComponent implements OnInit {
|
export class ModerationComponent implements OnInit {
|
||||||
autoBlockVideosEnabled = false
|
autoBlockVideosEnabled = false
|
||||||
|
|
|
@ -16,3 +16,12 @@ my-global-icon {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
input {
|
||||||
|
@include peertube-input-text(250px);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-block-list',
|
selector: 'my-video-block-list',
|
||||||
templateUrl: './video-block-list.component.html',
|
templateUrl: './video-block-list.component.html',
|
||||||
styleUrls: [ '../moderation.component.scss', './video-block-list.component.scss' ]
|
styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-block-list.component.scss' ]
|
||||||
})
|
})
|
||||||
export class VideoBlockListComponent extends RestTable implements OnInit, AfterViewInit {
|
export class VideoBlockListComponent extends RestTable implements OnInit, AfterViewInit {
|
||||||
blocklist: (VideoBlacklist & { reasonHtml?: string })[] = []
|
blocklist: (VideoBlacklist & { reasonHtml?: string })[] = []
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<h1>
|
||||||
|
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
|
||||||
|
<ng-container i18n>Reports</ng-container>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<my-abuse-list-table viewType="user" baseRoute="/my-account/abuses"></my-abuse-list-table>
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
import { Component } from '@angular/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-account-abuses-list',
|
||||||
|
templateUrl: './my-account-abuses-list.component.html',
|
||||||
|
styleUrls: [ ]
|
||||||
|
})
|
||||||
|
export class MyAccountAbusesListComponent {
|
||||||
|
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import { BlocklistComponentType, GenericServerBlocklistComponent } from '@app/sh
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-server-blocklist',
|
selector: 'my-account-server-blocklist',
|
||||||
styleUrls: [ '../../+admin/moderation/moderation.component.scss', '../../shared/shared-moderation/server-blocklist.component.scss' ],
|
styleUrls: [ '../../shared/shared-moderation/moderation.scss', '../../shared/shared-moderation/server-blocklist.component.scss' ],
|
||||||
templateUrl: '../../shared/shared-moderation/server-blocklist.component.html'
|
templateUrl: '../../shared/shared-moderation/server-blocklist.component.html'
|
||||||
})
|
})
|
||||||
export class MyAccountServerBlocklistComponent extends GenericServerBlocklistComponent {
|
export class MyAccountServerBlocklistComponent extends GenericServerBlocklistComponent {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playli
|
||||||
import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component'
|
import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component'
|
||||||
import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
|
import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
|
||||||
import { MyAccountComponent } from './my-account.component'
|
import { MyAccountComponent } from './my-account.component'
|
||||||
|
import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
|
||||||
|
|
||||||
const myAccountRoutes: Routes = [
|
const myAccountRoutes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -162,6 +163,15 @@ const myAccountRoutes: Routes = [
|
||||||
title: 'Notifications'
|
title: 'Notifications'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'abuses',
|
||||||
|
component: MyAccountAbusesListComponent,
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: 'My abuses'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,11 @@ export class MyAccountComponent implements OnInit {
|
||||||
routerLink: '/my-account/blocklist/servers',
|
routerLink: '/my-account/blocklist/servers',
|
||||||
iconName: 'peertube-x'
|
iconName: 'peertube-x'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: this.i18n('My abuses'),
|
||||||
|
routerLink: '/my-account/abuses',
|
||||||
|
iconName: 'flag'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Ownership changes'),
|
label: this.i18n('Ownership changes'),
|
||||||
routerLink: '/my-account/ownership',
|
routerLink: '/my-account/ownership',
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { InputSwitchModule } from 'primeng/inputswitch'
|
||||||
import { TableModule } from 'primeng/table'
|
import { TableModule } from 'primeng/table'
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
|
import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
|
||||||
import { SharedFormModule } from '@app/shared/shared-forms'
|
import { SharedFormModule } from '@app/shared/shared-forms'
|
||||||
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
||||||
import { SharedMainModule } from '@app/shared/shared-main'
|
import { SharedMainModule } from '@app/shared/shared-main'
|
||||||
|
@ -11,6 +12,7 @@ import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-setti
|
||||||
import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription/shared-user-subscription.module'
|
import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription/shared-user-subscription.module'
|
||||||
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
|
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
|
||||||
import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module'
|
import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist/shared-video-playlist.module'
|
||||||
|
import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
|
||||||
import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
|
import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
|
||||||
import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
|
import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
|
||||||
import { MyAccountHistoryComponent } from './my-account-history/my-account-history.component'
|
import { MyAccountHistoryComponent } from './my-account-history/my-account-history.component'
|
||||||
|
@ -50,7 +52,8 @@ import { MyAccountComponent } from './my-account.component'
|
||||||
SharedUserSubscriptionModule,
|
SharedUserSubscriptionModule,
|
||||||
SharedVideoPlaylistModule,
|
SharedVideoPlaylistModule,
|
||||||
SharedUserInterfaceSettingsModule,
|
SharedUserInterfaceSettingsModule,
|
||||||
SharedGlobalIconModule
|
SharedGlobalIconModule,
|
||||||
|
SharedAbuseListModule
|
||||||
],
|
],
|
||||||
|
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -69,6 +72,7 @@ import { MyAccountComponent } from './my-account.component'
|
||||||
MyAccountDangerZoneComponent,
|
MyAccountDangerZoneComponent,
|
||||||
MyAccountSubscriptionsComponent,
|
MyAccountSubscriptionsComponent,
|
||||||
MyAccountBlocklistComponent,
|
MyAccountBlocklistComponent,
|
||||||
|
MyAccountAbusesListComponent,
|
||||||
MyAccountServerBlocklistComponent,
|
MyAccountServerBlocklistComponent,
|
||||||
MyAccountHistoryComponent,
|
MyAccountHistoryComponent,
|
||||||
MyAccountNotificationsComponent,
|
MyAccountNotificationsComponent,
|
||||||
|
|
|
@ -36,7 +36,10 @@ function populateAsyncUserVideoChannels (authService: AuthService, channel: { id
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAbsoluteAPIUrl () {
|
function getAbsoluteAPIUrl () {
|
||||||
let absoluteAPIUrl = environment.apiUrl
|
let absoluteAPIUrl = environment.hmr === true
|
||||||
|
? 'http://localhost:9000'
|
||||||
|
: environment.apiUrl
|
||||||
|
|
||||||
if (!absoluteAPIUrl) {
|
if (!absoluteAPIUrl) {
|
||||||
// The API is on the same domain
|
// The API is on the same domain
|
||||||
absoluteAPIUrl = window.location.origin
|
absoluteAPIUrl = window.location.origin
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
|
|
||||||
<!-- report metadata -->
|
<!-- report metadata -->
|
||||||
<div class="d-flex" *ngIf="abuse.reporterAccount">
|
<div class="d-flex" *ngIf="isAdminView && 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 + '"' }"
|
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
|
||||||
class="chip"
|
class="chip"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
|
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
|
||||||
class="ml-auto text-muted abuse-details-links" i18n
|
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>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<div class="d-flex" *ngIf="abuse.flaggedAccount">
|
<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.flaggedAccount.displayName + '"' }"
|
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
|
||||||
class="chip"
|
class="chip"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
|
<a *ngIf="isAdminView" [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
|
||||||
class="ml-auto text-muted abuse-details-links" i18n
|
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>
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
<div class="mt-3 d-flex">
|
<div class="mt-3 d-flex">
|
||||||
<span class="col-3 moderation-expanded-label">
|
<span class="col-3 moderation-expanded-label">
|
||||||
<ng-container i18n>Report</ng-container>
|
<ng-container i18n>Report</ng-container>
|
||||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
|
<a [routerLink]="[ baseRoute ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
|
||||||
</span>
|
</span>
|
||||||
<span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
|
<span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
<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 *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ '/admin/moderation/abuses/list' ]"
|
<a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ baseRoute ]"
|
||||||
[queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light"
|
[queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light"
|
||||||
>
|
>
|
||||||
<div>{{ reason.label }}</div>
|
<div>{{ reason.label }}</div>
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 d-flex" *ngIf="abuse.moderationComment">
|
<div class="mt-3 d-flex" *ngIf="isAdminView && 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 d-block" [innerHTML]="abuse.moderationCommentHtml"></span>
|
<span class="col-9 moderation-expanded-text d-block" [innerHTML]="abuse.moderationCommentHtml"></span>
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,34 @@
|
||||||
|
@import 'variables';
|
||||||
|
@import 'mixins';
|
||||||
|
@import 'miniature';
|
||||||
|
|
||||||
|
.screenratio {
|
||||||
|
div {
|
||||||
|
@include miniature-thumbnail;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: pvar(--inputPlaceholderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include large-screen-ratio($selector: 'div, ::ng-deep iframe') {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
left: 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-html {
|
||||||
|
background-color: #ececec;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.abuse-details-date-updated {
|
||||||
|
font-size: 90%;
|
||||||
|
margin-top: .1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.abuse-details-links {
|
||||||
|
@include disable-default-a-behaviour;
|
||||||
|
}
|
|
@ -1,17 +1,19 @@
|
||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { durationToString } from '@app/helpers'
|
||||||
import { Actor } from '@app/shared/shared-main'
|
import { Actor } from '@app/shared/shared-main'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { AbusePredefinedReasonsString } from '@shared/models'
|
import { AbusePredefinedReasonsString } from '@shared/models'
|
||||||
import { ProcessedAbuse } from './abuse-list.component'
|
import { ProcessedAbuse } from './processed-abuse.model'
|
||||||
import { durationToString } from '@app/helpers'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-abuse-details',
|
selector: 'my-abuse-details',
|
||||||
templateUrl: './abuse-details.component.html',
|
templateUrl: './abuse-details.component.html',
|
||||||
styleUrls: [ '../moderation.component.scss' ]
|
styleUrls: [ '../shared-moderation/moderation.scss', './abuse-details.component.scss' ]
|
||||||
})
|
})
|
||||||
export class AbuseDetailsComponent {
|
export class AbuseDetailsComponent {
|
||||||
@Input() abuse: ProcessedAbuse
|
@Input() abuse: ProcessedAbuse
|
||||||
|
@Input() isAdminView: boolean
|
||||||
|
@Input() baseRoute: string
|
||||||
|
|
||||||
private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
|
private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
<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" [lazyLoadOnInit]="false"
|
||||||
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
|
||||||
|
(onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
|
||||||
|
>
|
||||||
|
<ng-template pTemplate="caption">
|
||||||
|
<div class="caption">
|
||||||
|
<div class="ml-auto">
|
||||||
|
<div class="input-group has-feedback has-clear">
|
||||||
|
<div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
|
||||||
|
<div class="input-group-text" ngbDropdownToggle>
|
||||||
|
<span class="caret" aria-haspopup="menu" role="button"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="menu" ngbDropdownMenu>
|
||||||
|
<h6 class="dropdown-header" i18n>Advanced report filters</h6>
|
||||||
|
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
|
||||||
|
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
|
||||||
|
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
|
||||||
|
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
|
||||||
|
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
||||||
|
(keyup)="onAbuseSearch($event)"
|
||||||
|
>
|
||||||
|
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
|
||||||
|
<span class="sr-only" i18n>Clear filters</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr> <!-- header -->
|
||||||
|
<th style="width: 40px;"></th>
|
||||||
|
<th *ngIf="isAdminView()" style="width: 20%;" pResizableColumn i18n>Reporter</th>
|
||||||
|
<th i18n>Video/Comment/Account</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 style="width: 80px;">Messages</th>
|
||||||
|
<th style="width: 150px;"></th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="body" let-expanded="expanded" let-abuse>
|
||||||
|
<tr>
|
||||||
|
<td class="c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
|
||||||
|
<span class="expander">
|
||||||
|
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td *ngIf="isAdminView()">
|
||||||
|
<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">
|
||||||
|
<img
|
||||||
|
class="avatar"
|
||||||
|
[src]="abuse.reporterAccount.avatar?.path"
|
||||||
|
(error)="switchToDefaultAvatar($event)"
|
||||||
|
alt="Avatar"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{{ abuse.reporterAccount.displayName }}
|
||||||
|
<span>{{ abuse.reporterAccount.nameWithHost }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span i18n *ngIf="!abuse.reporterAccount">
|
||||||
|
Deleted account
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<ng-container *ngIf="abuse.video">
|
||||||
|
|
||||||
|
<td *ngIf="!abuse.video.deleted">
|
||||||
|
<a [href]="getVideoUrl(abuse)" class="table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="table-video">
|
||||||
|
<div class="table-video-image">
|
||||||
|
<img [src]="abuse.video.thumbnailPath">
|
||||||
|
<span
|
||||||
|
class="table-video-image-label" *ngIf="abuse.count > 1"
|
||||||
|
i18n-title title="This video has been reported multiple times."
|
||||||
|
>
|
||||||
|
{{ abuse.nth }}/{{ abuse.count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-video-text">
|
||||||
|
<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 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" 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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<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="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)">
|
||||||
|
<ng-container *ngIf="isLocalAbuse(abuse)">
|
||||||
|
{{ abuse.countMessages }}
|
||||||
|
|
||||||
|
<my-global-icon iconName="message-circle"></my-global-icon>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="action-cell">
|
||||||
|
<my-action-dropdown
|
||||||
|
[ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
|
||||||
|
i18n-label label="Actions" [actions]="abuseActions" [entry]="abuse"
|
||||||
|
></my-action-dropdown>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="rowexpansion" let-abuse>
|
||||||
|
<tr>
|
||||||
|
<td class="expand-cell" colspan="6">
|
||||||
|
<my-abuse-details [abuse]="abuse" [baseRoute]="baseRoute" [isAdminView]="isAdminView()"></my-abuse-details>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="emptymessage">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6">
|
||||||
|
<div class="no-results">
|
||||||
|
<ng-container *ngIf="search" i18n>No abuses found matching current filters.</ng-container>
|
||||||
|
<ng-container *ngIf="!search" i18n>No abuses found.</ng-container>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
||||||
|
|
||||||
|
<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
|
||||||
|
<my-abuse-message-modal #abuseMessagesModal [isAdminView]="isAdminView()" (countMessagesUpdated)="onCountMessagesUpdated($event)"></my-abuse-message-modal>
|
|
@ -2,93 +2,6 @@
|
||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
@import 'miniature';
|
@import 'miniature';
|
||||||
|
|
||||||
.form-sub-title {
|
|
||||||
flex-grow: 0;
|
|
||||||
margin-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.caption {
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
input {
|
|
||||||
@include peertube-input-text(250px);
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.moderation-expanded {
|
|
||||||
font-size: 90%;
|
|
||||||
|
|
||||||
.moderation-expanded-label {
|
|
||||||
font-weight: $font-semibold;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moderation-expanded-text {
|
|
||||||
display: inline-flex;
|
|
||||||
word-wrap: break-word;
|
|
||||||
|
|
||||||
::ng-deep p:last-child {
|
|
||||||
margin-bottom: 0px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-states {
|
|
||||||
& > :not(:first-child) {
|
|
||||||
margin-left: .4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p-calendar {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
::ng-deep {
|
|
||||||
.ui-widget-content {
|
|
||||||
min-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
@include peertube-input-text(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.screenratio {
|
|
||||||
div {
|
|
||||||
@include miniature-thumbnail;
|
|
||||||
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: pvar(--inputPlaceholderColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
@include large-screen-ratio($selector: 'div, ::ng-deep iframe') {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
left: 0;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-html {
|
|
||||||
background-color: #ececec;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip {
|
|
||||||
@include chip;
|
|
||||||
}
|
|
||||||
|
|
||||||
my-action-dropdown.show {
|
|
||||||
::ng-deep .dropdown-root {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.table-video-link {
|
.table-video-link {
|
||||||
@include disable-outline;
|
@include disable-outline;
|
||||||
|
|
||||||
|
@ -179,3 +92,16 @@ my-action-dropdown.show {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.abuse-states .glyphicon-comment {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.abuse-messages {
|
||||||
|
my-global-icon {
|
||||||
|
width: 22px;
|
||||||
|
margin-left: 3px;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,487 @@
|
||||||
|
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'
|
||||||
|
import { AfterViewInit, Component, OnInit, ViewChild, Input } from '@angular/core'
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser'
|
||||||
|
import { ActivatedRoute, Params, Router } from '@angular/router'
|
||||||
|
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||||
|
import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
|
||||||
|
import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
|
||||||
|
import { VideoCommentService } from '@app/shared/shared-video-comment'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { AbuseState, AdminAbuse } from '@shared/models'
|
||||||
|
import { AbuseMessageModalComponent } from './abuse-message-modal.component'
|
||||||
|
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
||||||
|
import { ProcessedAbuse } from './processed-abuse.model'
|
||||||
|
|
||||||
|
const logger = debug('peertube:moderation:AbuseListTableComponent')
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-abuse-list-table',
|
||||||
|
templateUrl: './abuse-list-table.component.html',
|
||||||
|
styleUrls: [ '../shared-moderation/moderation.scss', './abuse-list-table.component.scss' ]
|
||||||
|
})
|
||||||
|
export class AbuseListTableComponent extends RestTable implements OnInit, AfterViewInit {
|
||||||
|
@Input() viewType: 'admin' | 'user'
|
||||||
|
@Input() baseRoute: string
|
||||||
|
|
||||||
|
@ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
|
||||||
|
@ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
|
||||||
|
|
||||||
|
abuses: ProcessedAbuse[] = []
|
||||||
|
totalRecords = 0
|
||||||
|
sort: SortMeta = { field: 'createdAt', order: 1 }
|
||||||
|
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||||
|
|
||||||
|
abuseActions: DropdownAction<ProcessedAbuse>[][] = []
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private notifier: Notifier,
|
||||||
|
private abuseService: AbuseService,
|
||||||
|
private blocklistService: BlocklistService,
|
||||||
|
private commentService: VideoCommentService,
|
||||||
|
private videoService: VideoService,
|
||||||
|
private videoBlocklistService: VideoBlockService,
|
||||||
|
private confirmService: ConfirmService,
|
||||||
|
private i18n: I18n,
|
||||||
|
private markdownRenderer: MarkdownService,
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.abuseActions = [
|
||||||
|
this.buildInternalActions(),
|
||||||
|
|
||||||
|
this.buildFlaggedAccountActions(),
|
||||||
|
|
||||||
|
this.buildCommentActions(),
|
||||||
|
|
||||||
|
this.buildVideoActions(),
|
||||||
|
|
||||||
|
this.buildAccountActions()
|
||||||
|
]
|
||||||
|
|
||||||
|
this.initialize()
|
||||||
|
|
||||||
|
this.route.queryParams
|
||||||
|
.subscribe(params => {
|
||||||
|
this.search = params.search || ''
|
||||||
|
|
||||||
|
logger('On URL change (search: %s).', this.search)
|
||||||
|
|
||||||
|
this.setTableFilter(this.search)
|
||||||
|
this.loadData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit () {
|
||||||
|
if (this.search) this.setTableFilter(this.search)
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdminView () {
|
||||||
|
return this.viewType === 'admin'
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdentifier () {
|
||||||
|
return 'AbuseListTableComponent'
|
||||||
|
}
|
||||||
|
|
||||||
|
openModerationCommentModal (abuse: AdminAbuse) {
|
||||||
|
this.moderationCommentModal.openModal(abuse)
|
||||||
|
}
|
||||||
|
|
||||||
|
onModerationCommentUpdated () {
|
||||||
|
this.loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table filter functions */
|
||||||
|
onAbuseSearch (event: Event) {
|
||||||
|
this.onSearch(event)
|
||||||
|
this.setQueryParams((event.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
setQueryParams (search: string) {
|
||||||
|
const queryParams: Params = {}
|
||||||
|
if (search) Object.assign(queryParams, { search })
|
||||||
|
|
||||||
|
this.router.navigate([ this.baseRoute ], { queryParams })
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTableFilter () {
|
||||||
|
this.setTableFilter('')
|
||||||
|
this.setQueryParams('')
|
||||||
|
this.resetSearch()
|
||||||
|
}
|
||||||
|
/* END Table filter functions */
|
||||||
|
|
||||||
|
isAbuseAccepted (abuse: AdminAbuse) {
|
||||||
|
return abuse.state.id === AbuseState.ACCEPTED
|
||||||
|
}
|
||||||
|
|
||||||
|
isAbuseRejected (abuse: AdminAbuse) {
|
||||||
|
return abuse.state.id === AbuseState.REJECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideoUrl (abuse: AdminAbuse) {
|
||||||
|
return Video.buildClientUrl(abuse.video.uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommentUrl (abuse: AdminAbuse) {
|
||||||
|
return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccountUrl (abuse: ProcessedAbuse) {
|
||||||
|
return '/accounts/' + abuse.flaggedAccount.nameWithHost
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideoEmbed (abuse: AdminAbuse) {
|
||||||
|
return buildVideoEmbed(
|
||||||
|
buildVideoLink({
|
||||||
|
baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
|
||||||
|
title: false,
|
||||||
|
warningTitle: false,
|
||||||
|
startTime: abuse.startAt,
|
||||||
|
stopTime: abuse.endAt
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
switchToDefaultAvatar ($event: Event) {
|
||||||
|
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeAbuse (abuse: AdminAbuse) {
|
||||||
|
const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
|
||||||
|
if (res === false) return
|
||||||
|
|
||||||
|
this.abuseService.removeAbuse(abuse).subscribe(
|
||||||
|
() => {
|
||||||
|
this.notifier.success(this.i18n('Abuse deleted.'))
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
|
||||||
|
err => this.notifier.error(err.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
|
||||||
|
this.abuseService.updateAbuse(abuse, { state })
|
||||||
|
.subscribe(
|
||||||
|
() => this.loadData(),
|
||||||
|
|
||||||
|
err => this.notifier.error(err.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
|
||||||
|
const abuse = this.abuses.find(a => a.id === event.abuseId)
|
||||||
|
|
||||||
|
if (!abuse) {
|
||||||
|
console.error('Cannot find abuse %d.', event.abuseId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
abuse.countMessages = event.countMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
openAbuseMessagesModal (abuse: AdminAbuse) {
|
||||||
|
this.abuseMessagesModal.openModal(abuse)
|
||||||
|
}
|
||||||
|
|
||||||
|
isLocalAbuse (abuse: AdminAbuse) {
|
||||||
|
if (this.viewType === 'user') return true
|
||||||
|
|
||||||
|
return Actor.IS_LOCAL(abuse.reporterAccount.host)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected loadData () {
|
||||||
|
logger('Loading data.')
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
pagination: this.pagination,
|
||||||
|
sort: this.sort,
|
||||||
|
search: this.search
|
||||||
|
}
|
||||||
|
|
||||||
|
const observable = this.viewType === 'admin'
|
||||||
|
? this.abuseService.getAdminAbuses(options)
|
||||||
|
: this.abuseService.getUserAbuses(options)
|
||||||
|
|
||||||
|
return observable.subscribe(
|
||||||
|
async resultList => {
|
||||||
|
this.totalRecords = resultList.total
|
||||||
|
|
||||||
|
this.abuses = []
|
||||||
|
|
||||||
|
for (const a of resultList.data) {
|
||||||
|
const abuse = a as ProcessedAbuse
|
||||||
|
|
||||||
|
abuse.reasonHtml = await this.toHtml(abuse.reason)
|
||||||
|
|
||||||
|
if (abuse.moderationComment) {
|
||||||
|
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.updatedAt === abuse.createdAt) delete abuse.updatedAt
|
||||||
|
|
||||||
|
this.abuses.push(abuse)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
err => this.notifier.error(err.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: this.i18n('Internal actions'),
|
||||||
|
isHeader: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.isAdminView()
|
||||||
|
? this.i18n('Messages with reporter')
|
||||||
|
: this.i18n('Messages with moderators'),
|
||||||
|
handler: abuse => this.openAbuseMessagesModal(abuse),
|
||||||
|
isDisplayed: abuse => this.isLocalAbuse(abuse)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.i18n('Update note'),
|
||||||
|
handler: abuse => this.openModerationCommentModal(abuse),
|
||||||
|
isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.i18n('Mark as accepted'),
|
||||||
|
handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
|
||||||
|
isDisplayed: abuse => this.isAdminView() && !this.isAbuseAccepted(abuse)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.i18n('Mark as rejected'),
|
||||||
|
handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
|
||||||
|
isDisplayed: abuse => this.isAdminView() && !this.isAbuseRejected(abuse)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.i18n('Add internal note'),
|
||||||
|
handler: abuse => this.openModerationCommentModal(abuse),
|
||||||
|
isDisplayed: abuse => this.isAdminView() && !abuse.moderationComment
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.i18n('Delete report'),
|
||||||
|
handler: abuse => this.isAdminView() && this.removeAbuse(abuse)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
|
||||||
|
if (!this.isAdminView()) return []
|
||||||
|
|
||||||
|
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>[] {
|
||||||
|
if (!this.isAdminView()) return []
|
||||||
|
|
||||||
|
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>[] {
|
||||||
|
if (!this.isAdminView()) return []
|
||||||
|
|
||||||
|
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>[] {
|
||||||
|
if (!this.isAdminView()) return []
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
<ng-template #modal>
|
<ng-template #modal>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 i18n class="modal-title">Messages</h4>
|
<h4 class="modal-title">
|
||||||
|
<ng-container i18n *ngIf="isAdminView">Messages with the reporter</ng-container>
|
||||||
|
<ng-container i18n *ngIf="!isAdminView">Messages with the moderation team</ng-container>
|
||||||
|
</h4>
|
||||||
|
|
||||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,9 +24,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="no-messages" *ngIf="noResults" i18n>
|
||||||
|
No messages for now.
|
||||||
|
</div>
|
||||||
|
|
||||||
<form novalidate [formGroup]="form" (ngSubmit)="addMessage()">
|
<form novalidate [formGroup]="form" (ngSubmit)="addMessage()">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<textarea formControlName="message" ngbAutofocus [ngClass]="{ 'input-error': formErrors['message'] }" class="form-control"></textarea>
|
<textarea
|
||||||
|
formControlName="message" ngbAutofocus [placeholder]="getPlaceholderMessage()"
|
||||||
|
[ngClass]="{ 'input-error': formErrors['message'] }" class="form-control"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<div *ngIf="formErrors.message" class="form-error">
|
<div *ngIf="formErrors.message" class="form-error">
|
||||||
{{ formErrors.message }}
|
{{ formErrors.message }}
|
||||||
|
@ -31,7 +41,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group inputs">
|
<div class="form-group inputs">
|
||||||
<input type="submit" i18n-value value="Add message" class="action-button-submit" [disabled]="!form.valid || sendingMessage">
|
<input type="submit" i18n-value value="Add a message" class="action-button-submit" [disabled]="!form.valid || sendingMessage">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -3,6 +3,11 @@
|
||||||
|
|
||||||
form {
|
form {
|
||||||
margin: 20px 20px 0 0;
|
margin: 20px 20px 0 0;
|
||||||
|
|
||||||
|
.form-group:first-child {
|
||||||
|
// Keep place to display error message without modifying the height
|
||||||
|
min-height: 125px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
|
@ -15,35 +20,29 @@ textarea {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
margin-right: 5px;
|
}
|
||||||
|
|
||||||
|
.no-messages {
|
||||||
|
display: flex;
|
||||||
|
font-size: 15px;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-block {
|
.message-block {
|
||||||
margin-bottom: 10px;
|
margin: 0 5px 10px 0;
|
||||||
max-width: 60%;
|
max-width: 60%;
|
||||||
|
|
||||||
.author {
|
.author {
|
||||||
color: var(--greyForegroundColor);
|
color: var(--greyForegroundColor);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
padding: 0 0 3px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bubble {
|
.bubble {
|
||||||
color: var(--mainForegroundColor);
|
|
||||||
background-color: var(--greyBackgroundColor);
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
color: var(--mainForegroundColor);
|
||||||
&.by-me {
|
background-color: var(--greyBackgroundColor);
|
||||||
color: var(--mainForegroundColor);
|
|
||||||
background-color: var(--secondaryColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.by-moderator {
|
|
||||||
color: #fff;
|
|
||||||
background-color: var(--mainColor);
|
|
||||||
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
@ -54,4 +53,20 @@ textarea {
|
||||||
color: var(--greyForegroundColor);
|
color: var(--greyForegroundColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.by-me {
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
color: var(--mainBackgroundColor);
|
||||||
|
background-color: var(--mainColorLighter);
|
||||||
|
|
||||||
|
.date {
|
||||||
|
color: var(--mainBackgroundColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.by-moderator {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
import { Component, ElementRef, EventEmitter, Output, ViewChild, OnInit } from '@angular/core'
|
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
|
||||||
import { Notifier, AuthService } from '@app/core'
|
import { AuthService, Notifier } from '@app/core'
|
||||||
import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms'
|
import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { AbuseMessage, UserAbuse } from '@shared/models'
|
import { AbuseMessage, UserAbuse } from '@shared/models'
|
||||||
import { AbuseService } from './abuse.service'
|
import { AbuseService } from '../shared-moderation'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-abuse-message-modal',
|
selector: 'my-abuse-message-modal',
|
||||||
|
@ -16,11 +16,14 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
|
||||||
@ViewChild('modal', { static: true }) modal: NgbModal
|
@ViewChild('modal', { static: true }) modal: NgbModal
|
||||||
@ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef
|
@ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef
|
||||||
|
|
||||||
|
@Input() isAdminView: boolean
|
||||||
|
|
||||||
@Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>()
|
@Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>()
|
||||||
|
|
||||||
abuseMessages: AbuseMessage[] = []
|
abuseMessages: AbuseMessage[] = []
|
||||||
textareaMessage: string
|
textareaMessage: string
|
||||||
sendingMessage = false
|
sendingMessage = false
|
||||||
|
noResults = false
|
||||||
|
|
||||||
private openedModal: NgbModalRef
|
private openedModal: NgbModalRef
|
||||||
private abuse: UserAbuse
|
private abuse: UserAbuse
|
||||||
|
@ -29,9 +32,9 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
|
||||||
protected formValidatorService: FormValidatorService,
|
protected formValidatorService: FormValidatorService,
|
||||||
private abuseValidatorsService: AbuseValidatorsService,
|
private abuseValidatorsService: AbuseValidatorsService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
|
private i18n: I18n,
|
||||||
private auth: AuthService,
|
private auth: AuthService,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private i18n: I18n,
|
|
||||||
private abuseService: AbuseService
|
private abuseService: AbuseService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
@ -94,11 +97,20 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
|
||||||
return this.auth.getUser().account.id === abuseMessage.account.id
|
return this.auth.getUser().account.id === abuseMessage.account.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPlaceholderMessage () {
|
||||||
|
if (this.isAdminView) {
|
||||||
|
return this.i18n('Add a message to communicate with the reporter')
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.i18n('Add a message to communicate with the moderation team')
|
||||||
|
}
|
||||||
|
|
||||||
private loadMessages () {
|
private loadMessages () {
|
||||||
this.abuseService.listAbuseMessages(this.abuse)
|
this.abuseService.listAbuseMessages(this.abuse)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
res => {
|
res => {
|
||||||
this.abuseMessages = res.data
|
this.abuseMessages = res.data
|
||||||
|
this.noResults = this.abuseMessages.length === 0
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.messagesBlock) return
|
if (!this.messagesBlock) return
|
|
@ -0,0 +1,7 @@
|
||||||
|
export * from './abuse-message-modal.component'
|
||||||
|
export * from './abuse-list-table.component'
|
||||||
|
export * from './abuse-details.component'
|
||||||
|
export * from './moderation-comment-modal.component'
|
||||||
|
export * from './processed-abuse.model'
|
||||||
|
|
||||||
|
export * from './shared-abuse-list.module'
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { SafeHtml } from '@angular/platform-browser'
|
||||||
|
import { AdminAbuse } from '@shared/models'
|
||||||
|
import { Account } from '@app/shared/shared-main'
|
||||||
|
|
||||||
|
// 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 = AdminAbuse & {
|
||||||
|
moderationCommentHtml?: string,
|
||||||
|
reasonHtml?: string
|
||||||
|
embedHtml?: SafeHtml
|
||||||
|
updatedAt?: Date
|
||||||
|
|
||||||
|
// override bare server-side definitions with rich client-side definitions
|
||||||
|
reporterAccount?: Account
|
||||||
|
flaggedAccount?: Account
|
||||||
|
|
||||||
|
truncatedCommentHtml?: string
|
||||||
|
commentHtml?: string
|
||||||
|
|
||||||
|
video: AdminAbuse['video'] & {
|
||||||
|
channel: AdminAbuse['video']['channel'] & {
|
||||||
|
ownerAccount: Account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
|
||||||
|
import { TableModule } from 'primeng/table'
|
||||||
|
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 { SharedModerationModule } from '../shared-moderation'
|
||||||
|
import { SharedVideoCommentModule } from '../shared-video-comment'
|
||||||
|
import { AbuseDetailsComponent } from './abuse-details.component'
|
||||||
|
import { AbuseListTableComponent } from './abuse-list-table.component'
|
||||||
|
import { AbuseMessageModalComponent } from './abuse-message-modal.component'
|
||||||
|
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
TableModule,
|
||||||
|
|
||||||
|
SharedMainModule,
|
||||||
|
SharedFormModule,
|
||||||
|
SharedModerationModule,
|
||||||
|
SharedGlobalIconModule,
|
||||||
|
SharedVideoCommentModule
|
||||||
|
],
|
||||||
|
|
||||||
|
declarations: [
|
||||||
|
AbuseDetailsComponent,
|
||||||
|
AbuseListTableComponent,
|
||||||
|
ModerationCommentModalComponent,
|
||||||
|
AbuseMessageModalComponent
|
||||||
|
],
|
||||||
|
|
||||||
|
exports: [
|
||||||
|
AbuseDetailsComponent,
|
||||||
|
AbuseListTableComponent,
|
||||||
|
ModerationCommentModalComponent,
|
||||||
|
AbuseMessageModalComponent
|
||||||
|
],
|
||||||
|
|
||||||
|
providers: [
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SharedAbuseListModule { }
|
|
@ -41,6 +41,13 @@ export abstract class Actor implements ActorServer {
|
||||||
return accountName + '@' + host
|
return accountName + '@' + host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static IS_LOCAL (host: string) {
|
||||||
|
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
||||||
|
const thisHost = new URL(absoluteAPIUrl).host
|
||||||
|
|
||||||
|
return host.trim() === thisHost
|
||||||
|
}
|
||||||
|
|
||||||
protected constructor (hash: ActorServer) {
|
protected constructor (hash: ActorServer) {
|
||||||
this.id = hash.id
|
this.id = hash.id
|
||||||
this.url = hash.url
|
this.url = hash.url
|
||||||
|
@ -53,10 +60,7 @@ export abstract class Actor implements ActorServer {
|
||||||
if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString())
|
if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString())
|
||||||
|
|
||||||
this.avatar = hash.avatar
|
this.avatar = hash.avatar
|
||||||
|
this.isLocal = Actor.IS_LOCAL(this.host)
|
||||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
|
||||||
const thisHost = new URL(absoluteAPIUrl).host
|
|
||||||
this.isLocal = this.host.trim() === thisHost
|
|
||||||
|
|
||||||
this.updateComputedAttributes()
|
this.updateComputedAttributes()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,24 @@ 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 { AdminAbuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList, UserAbuse, AbuseMessage } from '@shared/models'
|
|
||||||
import { environment } from '../../../environments/environment'
|
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import {
|
||||||
|
AbuseCreate,
|
||||||
|
AbuseFilter,
|
||||||
|
AbuseMessage,
|
||||||
|
AbusePredefinedReasonsString,
|
||||||
|
AbuseState,
|
||||||
|
AbuseUpdate,
|
||||||
|
AdminAbuse,
|
||||||
|
ResultList,
|
||||||
|
UserAbuse
|
||||||
|
} from '@shared/models'
|
||||||
|
import { environment } from '../../../environments/environment'
|
||||||
|
|
||||||
@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'
|
||||||
|
private static BASE_MY_ABUSE_URL = environment.apiUrl + '/api/v1/users/me/abuses'
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private i18n: I18n,
|
private i18n: I18n,
|
||||||
|
@ -32,33 +43,7 @@ export class AbuseService {
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
const filters = this.restService.parseQueryStringFilter(search, {
|
params = this.buildParamsFromSearch(search, params)
|
||||||
id: { prefix: '#' },
|
|
||||||
state: {
|
|
||||||
prefix: 'state:',
|
|
||||||
handler: v => {
|
|
||||||
if (v === 'accepted') return AbuseState.ACCEPTED
|
|
||||||
if (v === 'pending') return AbuseState.PENDING
|
|
||||||
if (v === 'rejected') return AbuseState.REJECTED
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoIs: {
|
|
||||||
prefix: 'videoIs:',
|
|
||||||
handler: v => {
|
|
||||||
if (v === 'deleted') return v
|
|
||||||
if (v === 'blacklisted') return v
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
searchReporter: { prefix: 'reporter:' },
|
|
||||||
searchReportee: { prefix: 'reportee:' },
|
|
||||||
predefinedReason: { prefix: 'tag:' }
|
|
||||||
})
|
|
||||||
|
|
||||||
params = this.restService.addObjectParams(params, filters)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<AdminAbuse>>(url, { params })
|
return this.authHttp.get<ResultList<AdminAbuse>>(url, { params })
|
||||||
|
@ -67,6 +52,27 @@ export class AbuseService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserAbuses (options: {
|
||||||
|
pagination: RestPagination,
|
||||||
|
sort: SortMeta,
|
||||||
|
search?: string
|
||||||
|
}): Observable<ResultList<UserAbuse>> {
|
||||||
|
const { pagination, sort, search } = options
|
||||||
|
const url = AbuseService.BASE_MY_ABUSE_URL
|
||||||
|
|
||||||
|
let params = new HttpParams()
|
||||||
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
params = this.buildParamsFromSearch(search, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.authHttp.get<ResultList<UserAbuse>>(url, { params })
|
||||||
|
.pipe(
|
||||||
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
reportVideo (parameters: AbuseCreate) {
|
reportVideo (parameters: AbuseCreate) {
|
||||||
const url = AbuseService.BASE_ABUSE_URL
|
const url = AbuseService.BASE_ABUSE_URL
|
||||||
|
|
||||||
|
@ -180,4 +186,33 @@ export class AbuseService {
|
||||||
return reasons
|
return reasons
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildParamsFromSearch (search: string, params: HttpParams) {
|
||||||
|
const filters = this.restService.parseQueryStringFilter(search, {
|
||||||
|
id: { prefix: '#' },
|
||||||
|
state: {
|
||||||
|
prefix: 'state:',
|
||||||
|
handler: v => {
|
||||||
|
if (v === 'accepted') return AbuseState.ACCEPTED
|
||||||
|
if (v === 'pending') return AbuseState.PENDING
|
||||||
|
if (v === 'rejected') return AbuseState.REJECTED
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
videoIs: {
|
||||||
|
prefix: 'videoIs:',
|
||||||
|
handler: v => {
|
||||||
|
if (v === 'deleted') return v
|
||||||
|
if (v === 'blacklisted') return v
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchReporter: { prefix: 'reporter:' },
|
||||||
|
searchReportee: { prefix: 'reportee:' },
|
||||||
|
predefinedReason: { prefix: 'tag:' }
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.restService.addObjectParams(params, filters)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
export * from './report-modals'
|
export * from './report-modals'
|
||||||
|
|
||||||
export * from './abuse-message-modal.component'
|
|
||||||
export * from './abuse.service'
|
export * from './abuse.service'
|
||||||
export * from './account-block.model'
|
export * from './account-block.model'
|
||||||
export * from './account-blocklist.component'
|
export * from './account-blocklist.component'
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
@import 'variables';
|
||||||
|
@import 'mixins';
|
||||||
|
@import 'miniature';
|
||||||
|
|
||||||
|
.moderation-expanded {
|
||||||
|
font-size: 90%;
|
||||||
|
|
||||||
|
.moderation-expanded-label {
|
||||||
|
font-weight: $font-semibold;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moderation-expanded-text {
|
||||||
|
display: inline-flex;
|
||||||
|
word-wrap: break-word;
|
||||||
|
|
||||||
|
::ng-deep p:last-child {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
@include peertube-input-group(300px);
|
||||||
|
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
@include chip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
input {
|
||||||
|
@include peertube-input-text(250px);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my-action-dropdown.show {
|
||||||
|
::ng-deep .dropdown-root {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,3 +32,16 @@ a {
|
||||||
.block-button {
|
.block-button {
|
||||||
@include create-button;
|
@include create-button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
input {
|
||||||
|
@include peertube-input-text(250px);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
@include chip;
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { SharedFormModule } from '../shared-forms/shared-form.module'
|
||||||
import { SharedGlobalIconModule } from '../shared-icons'
|
import { SharedGlobalIconModule } from '../shared-icons'
|
||||||
import { SharedMainModule } from '../shared-main/shared-main.module'
|
import { SharedMainModule } from '../shared-main/shared-main.module'
|
||||||
import { SharedVideoCommentModule } from '../shared-video-comment'
|
import { SharedVideoCommentModule } from '../shared-video-comment'
|
||||||
import { AbuseMessageModalComponent } from './abuse-message-modal.component'
|
|
||||||
import { AbuseService } from './abuse.service'
|
import { AbuseService } from './abuse.service'
|
||||||
import { BatchDomainsModalComponent } from './batch-domains-modal.component'
|
import { BatchDomainsModalComponent } from './batch-domains-modal.component'
|
||||||
import { BlocklistService } from './blocklist.service'
|
import { BlocklistService } from './blocklist.service'
|
||||||
|
@ -30,8 +29,7 @@ import { VideoBlockService } from './video-block.service'
|
||||||
VideoReportComponent,
|
VideoReportComponent,
|
||||||
BatchDomainsModalComponent,
|
BatchDomainsModalComponent,
|
||||||
CommentReportComponent,
|
CommentReportComponent,
|
||||||
AccountReportComponent,
|
AccountReportComponent
|
||||||
AbuseMessageModalComponent
|
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -41,8 +39,7 @@ import { VideoBlockService } from './video-block.service'
|
||||||
VideoReportComponent,
|
VideoReportComponent,
|
||||||
BatchDomainsModalComponent,
|
BatchDomainsModalComponent,
|
||||||
CommentReportComponent,
|
CommentReportComponent,
|
||||||
AccountReportComponent,
|
AccountReportComponent
|
||||||
AbuseMessageModalComponent
|
|
||||||
],
|
],
|
||||||
|
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
asyncRetryTransactionMiddleware,
|
asyncRetryTransactionMiddleware,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
checkAbuseValidForMessagesValidator,
|
||||||
deleteAbuseMessageValidator,
|
deleteAbuseMessageValidator,
|
||||||
ensureUserHasRight,
|
ensureUserHasRight,
|
||||||
getAbuseValidator,
|
getAbuseValidator,
|
||||||
|
@ -58,12 +59,14 @@ abuseRouter.delete('/:id',
|
||||||
abuseRouter.get('/:id/messages',
|
abuseRouter.get('/:id/messages',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(getAbuseValidator),
|
asyncMiddleware(getAbuseValidator),
|
||||||
|
checkAbuseValidForMessagesValidator,
|
||||||
asyncRetryTransactionMiddleware(listAbuseMessages)
|
asyncRetryTransactionMiddleware(listAbuseMessages)
|
||||||
)
|
)
|
||||||
|
|
||||||
abuseRouter.post('/:id/messages',
|
abuseRouter.post('/:id/messages',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(getAbuseValidator),
|
asyncMiddleware(getAbuseValidator),
|
||||||
|
checkAbuseValidForMessagesValidator,
|
||||||
addAbuseMessageValidator,
|
addAbuseMessageValidator,
|
||||||
asyncRetryTransactionMiddleware(addAbuseMessage)
|
asyncRetryTransactionMiddleware(addAbuseMessage)
|
||||||
)
|
)
|
||||||
|
@ -71,6 +74,7 @@ abuseRouter.post('/:id/messages',
|
||||||
abuseRouter.delete('/:id/messages/:messageId',
|
abuseRouter.delete('/:id/messages/:messageId',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(getAbuseValidator),
|
asyncMiddleware(getAbuseValidator),
|
||||||
|
checkAbuseValidForMessagesValidator,
|
||||||
asyncMiddleware(deleteAbuseMessageValidator),
|
asyncMiddleware(deleteAbuseMessageValidator),
|
||||||
asyncRetryTransactionMiddleware(deleteAbuseMessage)
|
asyncRetryTransactionMiddleware(deleteAbuseMessage)
|
||||||
)
|
)
|
||||||
|
|
|
@ -43,6 +43,6 @@ async function listMyAbuses (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
total: resultList.total,
|
total: resultList.total,
|
||||||
data: resultList.data.map(d => d.toFormattedAdminJSON())
|
data: resultList.data.map(d => d.toFormattedUserJSON())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doesAbuseExist (abuseId: number | string, res: Response) {
|
async function doesAbuseExist (abuseId: number | string, res: Response) {
|
||||||
const abuse = await AbuseModel.loadById(parseInt(abuseId + '', 10))
|
const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10))
|
||||||
|
|
||||||
if (!abuse) {
|
if (!abuse) {
|
||||||
res.status(404)
|
res.status(404)
|
||||||
|
|
|
@ -201,6 +201,21 @@ const getAbuseValidator = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const checkAbuseValidForMessagesValidator = [
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking checkAbuseValidForMessagesValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
const abuse = res.locals.abuse
|
||||||
|
if (abuse.ReporterAccount.isOwned() === false) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'This abuse was created by a user of your instance.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const addAbuseMessageValidator = [
|
const addAbuseMessageValidator = [
|
||||||
body('message').custom(isAbuseMessageValid).not().isEmpty().withMessage('Should have a valid abuse message'),
|
body('message').custom(isAbuseMessageValid).not().isEmpty().withMessage('Should have a valid abuse message'),
|
||||||
|
|
||||||
|
@ -357,6 +372,7 @@ export {
|
||||||
abuseReportValidator,
|
abuseReportValidator,
|
||||||
abuseGetValidator,
|
abuseGetValidator,
|
||||||
addAbuseMessageValidator,
|
addAbuseMessageValidator,
|
||||||
|
checkAbuseValidForMessagesValidator,
|
||||||
abuseUpdateValidator,
|
abuseUpdateValidator,
|
||||||
deleteAbuseMessageValidator,
|
deleteAbuseMessageValidator,
|
||||||
abuseListForUserValidator,
|
abuseListForUserValidator,
|
||||||
|
|
|
@ -25,14 +25,14 @@ import {
|
||||||
AbusePredefinedReasonsString,
|
AbusePredefinedReasonsString,
|
||||||
AbuseState,
|
AbuseState,
|
||||||
AbuseVideoIs,
|
AbuseVideoIs,
|
||||||
AdminVideoAbuse,
|
|
||||||
AdminAbuse,
|
AdminAbuse,
|
||||||
|
AdminVideoAbuse,
|
||||||
AdminVideoCommentAbuse,
|
AdminVideoCommentAbuse,
|
||||||
UserAbuse,
|
UserAbuse,
|
||||||
UserVideoAbuse
|
UserVideoAbuse
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
||||||
import { MAbuse, MAbuseAdminFormattable, MAbuseAP, MUserAccountId, MAbuseUserFormattable } from '../../types/models'
|
import { MAbuse, MAbuseAdminFormattable, MAbuseAP, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models'
|
||||||
import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
|
import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
|
||||||
import { getSort, throwIfNotValid } from '../utils'
|
import { getSort, throwIfNotValid } from '../utils'
|
||||||
import { ThumbnailModel } from '../video/thumbnail'
|
import { ThumbnailModel } from '../video/thumbnail'
|
||||||
|
@ -266,7 +266,7 @@ export class AbuseModel extends Model<AbuseModel> {
|
||||||
VideoAbuse: VideoAbuseModel
|
VideoAbuse: VideoAbuseModel
|
||||||
|
|
||||||
// FIXME: deprecated in 2.3. Remove these validators
|
// FIXME: deprecated in 2.3. Remove these validators
|
||||||
static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> {
|
static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuseReporter> {
|
||||||
const videoWhere: WhereOptions = {}
|
const videoWhere: WhereOptions = {}
|
||||||
|
|
||||||
if (videoId) videoWhere.videoId = videoId
|
if (videoId) videoWhere.videoId = videoId
|
||||||
|
@ -278,6 +278,10 @@ export class AbuseModel extends Model<AbuseModel> {
|
||||||
model: VideoAbuseModel,
|
model: VideoAbuseModel,
|
||||||
required: true,
|
required: true,
|
||||||
where: videoWhere
|
where: videoWhere
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: AccountModel,
|
||||||
|
as: 'ReporterAccount'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
where: {
|
where: {
|
||||||
|
@ -287,11 +291,17 @@ export class AbuseModel extends Model<AbuseModel> {
|
||||||
return AbuseModel.findOne(query)
|
return AbuseModel.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadById (id: number): Bluebird<MAbuse> {
|
static loadByIdWithReporter (id: number): Bluebird<MAbuseReporter> {
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
id
|
id
|
||||||
}
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: AccountModel,
|
||||||
|
as: 'ReporterAccount'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return AbuseModel.findOne(query)
|
return AbuseModel.findOne(query)
|
||||||
|
@ -466,8 +476,6 @@ export class AbuseModel extends Model<AbuseModel> {
|
||||||
label: AbuseModel.getStateLabel(this.state)
|
label: AbuseModel.getStateLabel(this.state)
|
||||||
},
|
},
|
||||||
|
|
||||||
moderationComment: this.moderationComment,
|
|
||||||
|
|
||||||
countMessages,
|
countMessages,
|
||||||
|
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
|
@ -500,6 +508,8 @@ export class AbuseModel extends Model<AbuseModel> {
|
||||||
video,
|
video,
|
||||||
comment,
|
comment,
|
||||||
|
|
||||||
|
moderationComment: this.moderationComment,
|
||||||
|
|
||||||
reporterAccount: this.ReporterAccount
|
reporterAccount: this.ReporterAccount
|
||||||
? this.ReporterAccount.toFormattedJSON()
|
? this.ReporterAccount.toFormattedJSON()
|
||||||
: null,
|
: null,
|
||||||
|
@ -519,7 +529,7 @@ export class AbuseModel extends Model<AbuseModel> {
|
||||||
const countMessages = this.get('countMessages') as number
|
const countMessages = this.get('countMessages') as number
|
||||||
|
|
||||||
const video = this.buildBaseVideoAbuse()
|
const video = this.buildBaseVideoAbuse()
|
||||||
const comment: AdminVideoCommentAbuse = this.buildBaseVideoCommentAbuse()
|
const comment = this.buildBaseVideoCommentAbuse()
|
||||||
const abuse = this.buildBaseAbuse(countMessages || 0)
|
const abuse = this.buildBaseAbuse(countMessages || 0)
|
||||||
|
|
||||||
return Object.assign(abuse, {
|
return Object.assign(abuse, {
|
||||||
|
|
|
@ -3,21 +3,26 @@
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import { AbuseCreate, AbuseState } from '@shared/models'
|
import { AbuseCreate, AbuseState } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
|
addAbuseMessage,
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createUser,
|
createUser,
|
||||||
deleteAbuse,
|
deleteAbuse,
|
||||||
|
deleteAbuseMessage,
|
||||||
|
doubleFollow,
|
||||||
flushAndRunServer,
|
flushAndRunServer,
|
||||||
|
generateUserAccessToken,
|
||||||
|
getAdminAbusesList,
|
||||||
|
getVideoIdFromUUID,
|
||||||
|
listAbuseMessages,
|
||||||
makeGetRequest,
|
makeGetRequest,
|
||||||
makePostBodyRequest,
|
makePostBodyRequest,
|
||||||
|
reportAbuse,
|
||||||
ServerInfo,
|
ServerInfo,
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
updateAbuse,
|
updateAbuse,
|
||||||
uploadVideo,
|
uploadVideo,
|
||||||
userLogin,
|
userLogin,
|
||||||
generateUserAccessToken,
|
waitJobs
|
||||||
addAbuseMessage,
|
|
||||||
listAbuseMessages,
|
|
||||||
deleteAbuseMessage
|
|
||||||
} from '../../../../shared/extra-utils'
|
} from '../../../../shared/extra-utils'
|
||||||
import {
|
import {
|
||||||
checkBadCountPagination,
|
checkBadCountPagination,
|
||||||
|
@ -29,6 +34,7 @@ describe('Test abuses API validators', function () {
|
||||||
const basePath = '/api/v1/abuses/'
|
const basePath = '/api/v1/abuses/'
|
||||||
|
|
||||||
let server: ServerInfo
|
let server: ServerInfo
|
||||||
|
|
||||||
let userAccessToken = ''
|
let userAccessToken = ''
|
||||||
let userAccessToken2 = ''
|
let userAccessToken2 = ''
|
||||||
let abuseId: number
|
let abuseId: number
|
||||||
|
@ -321,7 +327,7 @@ describe('Test abuses API validators', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When listing abuse message', function () {
|
describe('When listing abuse messages', function () {
|
||||||
|
|
||||||
it('Should fail with an invalid abuse id', async function () {
|
it('Should fail with an invalid abuse id', async function () {
|
||||||
await listAbuseMessages(server.url, userAccessToken, 888, 404)
|
await listAbuseMessages(server.url, userAccessToken, 888, 404)
|
||||||
|
@ -382,7 +388,43 @@ describe('Test abuses API validators', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('When trying to manage messages of a remote abuse', function () {
|
||||||
|
let remoteAbuseId: number
|
||||||
|
let anotherServer: ServerInfo
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(20000)
|
||||||
|
|
||||||
|
anotherServer = await flushAndRunServer(2)
|
||||||
|
await setAccessTokensToServers([ anotherServer ])
|
||||||
|
|
||||||
|
await doubleFollow(anotherServer, server)
|
||||||
|
|
||||||
|
const server2VideoId = await getVideoIdFromUUID(anotherServer.url, server.video.uuid)
|
||||||
|
await reportAbuse({
|
||||||
|
url: anotherServer.url,
|
||||||
|
token: anotherServer.accessToken,
|
||||||
|
reason: 'remote server',
|
||||||
|
videoId: server2VideoId
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitJobs([ server, anotherServer ])
|
||||||
|
|
||||||
|
const res = await getAdminAbusesList({ url: server.url, token: server.accessToken, sort: '-createdAt' })
|
||||||
|
remoteAbuseId = res.body.data[0].id
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail when listing abuse messages of a remote abuse', async function () {
|
||||||
|
await listAbuseMessages(server.url, server.accessToken, remoteAbuseId, 400)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail when creating abuse message of a remote abuse', async function () {
|
||||||
|
await addAbuseMessage(server.url, server.accessToken, remoteAbuseId, 'message', 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests([ server ])
|
await cleanupTests([ server ])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,23 @@
|
||||||
|
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import { AbuseFilter, AbusePredefinedReasonsString, AbuseState, Account, AdminAbuse, UserAbuse, VideoComment, AbuseMessage } from '@shared/models'
|
|
||||||
import {
|
import {
|
||||||
|
AbuseFilter,
|
||||||
|
AbuseMessage,
|
||||||
|
AbusePredefinedReasonsString,
|
||||||
|
AbuseState,
|
||||||
|
Account,
|
||||||
|
AdminAbuse,
|
||||||
|
UserAbuse,
|
||||||
|
VideoComment
|
||||||
|
} from '@shared/models'
|
||||||
|
import {
|
||||||
|
addAbuseMessage,
|
||||||
addVideoCommentThread,
|
addVideoCommentThread,
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createUser,
|
createUser,
|
||||||
deleteAbuse,
|
deleteAbuse,
|
||||||
|
deleteAbuseMessage,
|
||||||
deleteVideoComment,
|
deleteVideoComment,
|
||||||
flushAndRunMultipleServers,
|
flushAndRunMultipleServers,
|
||||||
generateUserAccessToken,
|
generateUserAccessToken,
|
||||||
|
@ -18,6 +29,7 @@ import {
|
||||||
getVideoIdFromUUID,
|
getVideoIdFromUUID,
|
||||||
getVideosList,
|
getVideosList,
|
||||||
immutableAssign,
|
immutableAssign,
|
||||||
|
listAbuseMessages,
|
||||||
removeUser,
|
removeUser,
|
||||||
removeVideo,
|
removeVideo,
|
||||||
reportAbuse,
|
reportAbuse,
|
||||||
|
@ -26,10 +38,7 @@ import {
|
||||||
updateAbuse,
|
updateAbuse,
|
||||||
uploadVideo,
|
uploadVideo,
|
||||||
uploadVideoAndGetId,
|
uploadVideoAndGetId,
|
||||||
userLogin,
|
userLogin
|
||||||
addAbuseMessage,
|
|
||||||
listAbuseMessages,
|
|
||||||
deleteAbuseMessage
|
|
||||||
} from '../../../../shared/extra-utils/index'
|
} from '../../../../shared/extra-utils/index'
|
||||||
import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
|
import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
|
||||||
import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
|
import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
|
||||||
import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
|
import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
|
||||||
import { PickWith } from '@shared/core-utils'
|
import { PickWith } from '@shared/core-utils'
|
||||||
import { AbuseModel } from '../../../models/abuse/abuse'
|
import { AbuseModel } from '../../../models/abuse/abuse'
|
||||||
import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account'
|
import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl, MAccount } from '../account'
|
||||||
import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video'
|
import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video'
|
||||||
import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video'
|
import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video'
|
||||||
|
|
||||||
|
@ -18,6 +18,10 @@ export type MVideoAbuse = Omit<VideoAbuseModel, 'Abuse' | 'Video'>
|
||||||
|
|
||||||
export type MCommentAbuse = Omit<VideoCommentAbuseModel, 'Abuse' | 'VideoComment'>
|
export type MCommentAbuse = Omit<VideoCommentAbuseModel, 'Abuse' | 'VideoComment'>
|
||||||
|
|
||||||
|
export type MAbuseReporter =
|
||||||
|
MAbuse &
|
||||||
|
Use<'ReporterAccount', MAccountDefault>
|
||||||
|
|
||||||
// ############################################################################
|
// ############################################################################
|
||||||
|
|
||||||
export type MVideoAbuseVideo =
|
export type MVideoAbuseVideo =
|
||||||
|
|
|
@ -9,7 +9,8 @@ import {
|
||||||
MVideoFile,
|
MVideoFile,
|
||||||
MVideoImmutable,
|
MVideoImmutable,
|
||||||
MVideoPlaylistFull,
|
MVideoPlaylistFull,
|
||||||
MVideoPlaylistFullSummary
|
MVideoPlaylistFullSummary,
|
||||||
|
MAbuseReporter
|
||||||
} from '@server/types/models'
|
} from '@server/types/models'
|
||||||
import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
|
import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
|
||||||
import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server'
|
import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server'
|
||||||
|
@ -78,7 +79,7 @@ declare module 'express' {
|
||||||
|
|
||||||
videoCaption?: MVideoCaptionVideo
|
videoCaption?: MVideoCaptionVideo
|
||||||
|
|
||||||
abuse?: MAbuse
|
abuse?: MAbuseReporter
|
||||||
abuseMessage?: MAbuseMessage
|
abuseMessage?: MAbuseMessage
|
||||||
|
|
||||||
videoStreamingPlaylist?: MStreamingPlaylist
|
videoStreamingPlaylist?: MStreamingPlaylist
|
||||||
|
|
|
@ -79,4 +79,4 @@ export type UserVideoAbuse = Omit<AdminVideoAbuse, 'countReports' | 'nthReport'>
|
||||||
export type UserVideoCommentAbuse = AdminVideoCommentAbuse
|
export type UserVideoCommentAbuse = AdminVideoCommentAbuse
|
||||||
|
|
||||||
export type UserAbuse = Omit<AdminAbuse, 'reporterAccount' | 'countReportsForReportee' | 'countReportsForReporter' | 'startAt' | 'endAt'
|
export type UserAbuse = Omit<AdminAbuse, 'reporterAccount' | 'countReportsForReportee' | 'countReportsForReporter' | 'startAt' | 'endAt'
|
||||||
| 'count' | 'nth'>
|
| 'count' | 'nth' | 'moderationComment'>
|
||||||
|
|
Loading…
Reference in New Issue