margin-right: 30px;
}
-.moderation-expanded-label {
- font-weight: $font-semibold;
- display: inline-block;
- vertical-align: top;
- text-align: right;
-}
-
-.moderation-expanded-text {
- display: inline-block;
- word-wrap: break-word;
+.moderation-expanded {
+ font-size: 90%;
- ::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;
+ }
}
}
.chip {
@include chip;
}
+
+my-action-dropdown.show {
+ ::ng-deep .dropdown-root {
+ display: block !important;
+ }
+}
<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 }}
<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>
<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>
}
}
+.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;
display: inline-flex;
justify-content: center;
align-items: center;
+ position: relative;
img {
height: 100%;
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 {
private i18n: I18n,
private markdownRenderer: MarkdownService,
private sanitizer: DomSanitizer,
- private route: ActivatedRoute,
+ private route: ActivatedRoute
) {
super()
}
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
</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>
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 {
})
}
- 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,
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' ],
@AllowNull(true)
@Default(null)
@Column(DataType.JSONB)
- deletedVideo: Video
+ deletedVideo: VideoDetails
@CreatedAt
createdAt: Date
}
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
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
}
}
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
deleted: boolean
blacklisted: boolean
thumbnailPath?: string
- channel?: VideoChannelSummary
+ channel?: VideoChannel
}
createdAt: Date
+ updatedAt: Date
+
+ count?: number
+ nth?: number
+
+ countReportsForReporter?: number
+ countReportsForReportee?: number
}