mirror of https://github.com/Chocobozzz/PeerTube
Add nth abuse count for a given video, add reporter/reportee reports stats
parent
844db39ee5
commit
5fd4ca0051
|
@ -7,19 +7,23 @@
|
|||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.moderation-expanded-label {
|
||||
font-weight: $font-semibold;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
text-align: right;
|
||||
}
|
||||
.moderation-expanded {
|
||||
font-size: 90%;
|
||||
|
||||
.moderation-expanded-text {
|
||||
display: inline-block;
|
||||
word-wrap: break-word;
|
||||
|
||||
::ng-deep p:last-child {
|
||||
margin-bottom: 0px !important;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,3 +62,9 @@
|
|||
.chip {
|
||||
@include chip;
|
||||
}
|
||||
|
||||
my-action-dropdown.show {
|
||||
::ng-deep .dropdown-root {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,13 @@
|
|||
<td *ngIf="!videoAbuse.video.deleted">
|
||||
<a [href]="getVideoUrl(videoAbuse)" class="video-abuse-video-link" i18n-title title="Open video in a new tab" target="_blank" rel="noopener noreferrer">
|
||||
<div class="video-abuse-video">
|
||||
<div class="video-abuse-video-image"><img [src]="videoAbuse.video.thumbnailPath"></div>
|
||||
<div class="video-abuse-video-image">
|
||||
<img [src]="videoAbuse.video.thumbnailPath">
|
||||
<span
|
||||
class="video-abuse-video-image-label" *ngIf="videoAbuse.count > 1"
|
||||
i18n-title title="This video has been reported multiple times."
|
||||
>{{ videoAbuse.nth }}/{{ videoAbuse.count }}</span>
|
||||
</div>
|
||||
<div class="video-abuse-video-text">
|
||||
<div>
|
||||
{{ videoAbuse.video.name }}
|
||||
|
@ -85,11 +91,14 @@
|
|||
<td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse">
|
||||
<span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
|
||||
<span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span>
|
||||
<span *ngIf="videoAbuse.moderationComment" [title]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
|
||||
<span *ngIf="videoAbuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
|
||||
</td>
|
||||
|
||||
<td class="action-cell">
|
||||
<my-action-dropdown placement="bottom-right auto" container="body" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
|
||||
<my-action-dropdown
|
||||
[ngClass]="{ 'show': expanded }" placement="bottom-right auto" container="body"
|
||||
i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"
|
||||
></my-action-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
@ -97,14 +106,59 @@
|
|||
<ng-template pTemplate="rowexpansion" let-videoAbuse>
|
||||
<tr>
|
||||
<td class="expand-cell" colspan="6">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex moderation-expanded">
|
||||
<!-- report metadata -->
|
||||
<div class="col-8">
|
||||
<div class="d-flex">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Reason:</span>
|
||||
<span class="col-3 moderation-expanded-label" i18n>Reporter</span>
|
||||
<span class="col-9 moderation-expanded-text">
|
||||
<div class="chip">
|
||||
<img
|
||||
class="avatar"
|
||||
[src]="videoAbuse.reporterAccount.avatar.path"
|
||||
(error)="switchToDefaultAvatar($event)"
|
||||
alt="Avatar"
|
||||
>
|
||||
<div>
|
||||
<span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a routerLink="/admin/moderation/video-abuses/list" class="ml-auto text-muted video-abuse-links" i18n>
|
||||
{videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Reportee</span>
|
||||
<span class="col-9 moderation-expanded-text">
|
||||
<div class="chip">
|
||||
<img
|
||||
class="avatar"
|
||||
[src]="videoAbuse.video.channel.ownerAccount?.avatar.path"
|
||||
(error)="switchToDefaultAvatar($event)"
|
||||
alt="Avatar"
|
||||
>
|
||||
<div>
|
||||
<span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a routerLink="/admin/moderation/video-abuses/list" class="ml-auto text-muted video-abuse-links" *ngIf="!videoAbuse.video.deleted" i18n>
|
||||
{videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Updated</span>
|
||||
<time class="col-9 moderation-expanded-text video-abuse-date-updated">{{ videoAbuse.updatedAt | date: 'medium' }}</time>
|
||||
</div>
|
||||
|
||||
<!-- report text -->
|
||||
<div class="mt-3 d-flex">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Report</span>
|
||||
<span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span>
|
||||
</div>
|
||||
<div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Note:</span>
|
||||
<span class="col-3 moderation-expanded-label" i18n>Note</span>
|
||||
<span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.video-abuse-date-updated {
|
||||
font-size: 90%;
|
||||
margin-top: .1rem;
|
||||
}
|
||||
|
||||
.video-abuse-links {
|
||||
@include disable-default-a-behaviour;
|
||||
}
|
||||
|
||||
.video-abuse-video-link {
|
||||
@include disable-outline;
|
||||
position: relative;
|
||||
|
@ -32,6 +41,7 @@
|
|||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
|
@ -42,6 +52,17 @@
|
|||
span {
|
||||
color: var(--inputPlaceholderColor);
|
||||
}
|
||||
|
||||
.video-abuse-video-image-label {
|
||||
@include static-thumbnail-overlay;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
padding: 0 3px;
|
||||
line-height: 1.3;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.video-abuse-video-text {
|
||||
|
|
|
@ -46,7 +46,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
|
|||
private i18n: I18n,
|
||||
private markdownRenderer: MarkdownService,
|
||||
private sanitizer: DomSanitizer,
|
||||
private route: ActivatedRoute,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
super()
|
||||
|
||||
|
@ -223,7 +223,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
|
|||
}
|
||||
|
||||
getVideoEmbed (videoAbuse: VideoAbuse) {
|
||||
const absoluteAPIUrl = 'http://localhost:9000' || getAbsoluteAPIUrl()
|
||||
const absoluteAPIUrl = 'http://localhost:9000' || getAbsoluteAPIUrl() // TODO
|
||||
const embedUrl = buildVideoLink({
|
||||
baseUrl: absoluteAPIUrl + '/videos/embed/' + videoAbuse.video.uuid,
|
||||
warningTitle: false
|
||||
|
|
|
@ -30,9 +30,16 @@
|
|||
</a>
|
||||
</td>
|
||||
|
||||
<td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
|
||||
<td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
|
||||
<td>{{ videoBlacklist.createdAt }}</td>
|
||||
<ng-container *ngIf="videoBlacklist.reason">
|
||||
<td class="c-hand" [pRowToggler]="videoBlacklist">{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
|
||||
<td class="c-hand" [pRowToggler]="videoBlacklist">{{ booleanToText(videoBlacklist.unfederated) }}</td>
|
||||
<td class="c-hand" [pRowToggler]="videoBlacklist">{{ videoBlacklist.createdAt }}</td>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!videoBlacklist.reason">
|
||||
<td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
|
||||
<td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
|
||||
<td>{{ videoBlacklist.createdAt }}</td>
|
||||
</ng-container>
|
||||
|
||||
<td class="action-cell">
|
||||
<my-action-dropdown i18n-label placement="bottom-right" label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>
|
||||
|
|
|
@ -11,14 +11,13 @@ import {
|
|||
import { AccountModel } from '../account/account'
|
||||
import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
|
||||
import { VideoModel } from './video'
|
||||
import { VideoAbuseState, Video } from '../../../shared'
|
||||
import { VideoAbuseState, VideoDetails } from '../../../shared'
|
||||
import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
|
||||
import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models'
|
||||
import * as Bluebird from 'bluebird'
|
||||
import { literal, Op } from 'sequelize'
|
||||
import { ThumbnailModel } from './thumbnail'
|
||||
import { VideoChannelModel } from './video-channel'
|
||||
import { ActorModel } from '../activitypub/actor'
|
||||
import { VideoBlacklistModel } from './video-blacklist'
|
||||
|
||||
export enum ScopeNames {
|
||||
|
@ -78,9 +77,73 @@ export enum ScopeNames {
|
|||
})
|
||||
}
|
||||
|
||||
console.log(where)
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
include: [
|
||||
[
|
||||
literal(
|
||||
'(' +
|
||||
'SELECT t.count ' +
|
||||
'FROM ( ' +
|
||||
'SELECT id, ' +
|
||||
'count(id) OVER (PARTITION BY "videoId") ' +
|
||||
'FROM "videoAbuse" ' +
|
||||
') t ' +
|
||||
'WHERE t.id = "VideoAbuseModel".id ' +
|
||||
')'
|
||||
),
|
||||
'countReportsForVideo'
|
||||
],
|
||||
[
|
||||
literal(
|
||||
'(' +
|
||||
'SELECT t.nth ' +
|
||||
'FROM ( ' +
|
||||
'SELECT id, ' +
|
||||
'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
|
||||
'FROM "videoAbuse" ' +
|
||||
') t ' +
|
||||
'WHERE t.id = "VideoAbuseModel".id ' +
|
||||
')'
|
||||
),
|
||||
'nthReportForVideo'
|
||||
],
|
||||
[
|
||||
literal(
|
||||
'(' +
|
||||
'SELECT count("videoAbuse"."id") ' +
|
||||
'FROM "videoAbuse" ' +
|
||||
'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
|
||||
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
|
||||
'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
|
||||
'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
|
||||
')'
|
||||
),
|
||||
'countReportsForReporter'
|
||||
],
|
||||
[
|
||||
literal(
|
||||
'(' +
|
||||
'WITH ' +
|
||||
'ids AS ( ' +
|
||||
'SELECT "account"."id" ' +
|
||||
'FROM "account" ' +
|
||||
'INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ' +
|
||||
'INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" ' +
|
||||
'WHERE "video"."id" = "VideoAbuseModel"."videoId" ' +
|
||||
') ' +
|
||||
'SELECT count("videoAbuse"."id") ' +
|
||||
'FROM "videoAbuse" ' +
|
||||
'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
|
||||
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
|
||||
'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
|
||||
'INNER JOIN ids ON "account"."id" = ids.id ' +
|
||||
')'
|
||||
),
|
||||
'countReportsForReportee'
|
||||
]
|
||||
]
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: AccountModel,
|
||||
|
@ -96,13 +159,8 @@ export enum ScopeNames {
|
|||
model: ThumbnailModel
|
||||
},
|
||||
{
|
||||
model: VideoChannelModel.unscoped(),
|
||||
where: { ...search(options.searchVideoChannel, 'name') },
|
||||
include: [
|
||||
{
|
||||
model: ActorModel
|
||||
}
|
||||
]
|
||||
model: VideoChannelModel.scope([ 'WITH_ACTOR', 'WITH_ACCOUNT' ]),
|
||||
where: { ...search(options.searchVideoChannel, 'name') }
|
||||
},
|
||||
{
|
||||
attributes: [ 'id', 'reason', 'unfederated' ],
|
||||
|
@ -149,7 +207,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
|||
@AllowNull(true)
|
||||
@Default(null)
|
||||
@Column(DataType.JSONB)
|
||||
deletedVideo: Video
|
||||
deletedVideo: VideoDetails
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
@ -229,6 +287,11 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
|||
}
|
||||
|
||||
toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
|
||||
const countReportsForVideo = this.get('countReportsForVideo') as number
|
||||
const nthReportForVideo = this.get('nthReportForVideo') as number
|
||||
const countReportsForReporter = this.get('countReportsForReporter') as number
|
||||
const countReportsForReportee = this.get('countReportsForReportee') as number
|
||||
|
||||
const video = this.Video
|
||||
? this.Video
|
||||
: this.deletedVideo
|
||||
|
@ -250,9 +313,14 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
|||
deleted: !this.Video,
|
||||
blacklisted: this.Video && this.Video.isBlacklisted(),
|
||||
thumbnailPath: this.Video?.getMiniatureStaticPath(),
|
||||
channel: this.Video?.VideoChannel.toFormattedSummaryJSON() || this.deletedVideo?.channel
|
||||
channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
|
||||
},
|
||||
createdAt: this.createdAt
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
count: countReportsForVideo || 0,
|
||||
nth: nthReportForVideo || 0,
|
||||
countReportsForReporter: countReportsForReporter || 0,
|
||||
countReportsForReportee: countReportsForReportee || 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Account } from '../../actors/index'
|
||||
import { VideoConstant } from '../video-constant.model'
|
||||
import { VideoAbuseState } from './video-abuse-state.model'
|
||||
import { VideoChannelSummary } from '../channel/video-channel.model'
|
||||
import { VideoChannel } from '../channel/video-channel.model'
|
||||
|
||||
export interface VideoAbuse {
|
||||
id: number
|
||||
|
@ -19,8 +19,15 @@ export interface VideoAbuse {
|
|||
deleted: boolean
|
||||
blacklisted: boolean
|
||||
thumbnailPath?: string
|
||||
channel?: VideoChannelSummary
|
||||
channel?: VideoChannel
|
||||
}
|
||||
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
|
||||
count?: number
|
||||
nth?: number
|
||||
|
||||
countReportsForReporter?: number
|
||||
countReportsForReportee?: number
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue