Rich reporter field and video embed in moderation abuse list

pull/2639/head
Rigel Kent 2020-04-13 19:57:57 +02:00
parent 2bc9bd08cd
commit d6af81469b
No known key found for this signature in database
GPG Key ID: 5E53E96A494E452F
9 changed files with 163 additions and 28 deletions

View File

@ -8,18 +8,35 @@
.moderation-expanded-label { .moderation-expanded-label {
font-weight: $font-semibold; font-weight: $font-semibold;
min-width: 200px;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
text-align: right;
} }
.moderation-expanded-text { .moderation-expanded-text {
display: inline-block; display: inline-block;
word-wrap: break-word;
::ng-deep p:last-child {
margin-bottom: 0px !important;
}
} }
.moderation-expanded { .screenratio {
word-wrap: break-word; position: relative;
overflow: visible !important; width: 100%;
text-overflow: unset !important; height: 0;
white-space: unset !important; padding-bottom: 56%;
::ng-deep iframe {
position: absolute;
width: 100% !important;
height: 100% !important;
left: 0;
top: 0;
}
}
.chip {
@include chip;
} }

View File

@ -24,8 +24,19 @@
</td> </td>
<td> <td>
<a [href]="videoAbuse.reporterAccount.url" i18n-title title="Go to the account" target="_blank" rel="noopener noreferrer"> <a [href]="videoAbuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
{{ createByString(videoAbuse.reporterAccount) }} <div class="chip two-lines">
<img
class="avatar"
[src]="videoAbuse.reporterAccount.avatar.path"
(error)="switchToDefaultAvatar($event)"
alt="Avatar"
>
<div>
{{ videoAbuse.reporterAccount.displayName }}
<span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span>
</div>
</div>
</a> </a>
</td> </td>
@ -50,14 +61,22 @@
<ng-template pTemplate="rowexpansion" let-videoAbuse> <ng-template pTemplate="rowexpansion" let-videoAbuse>
<tr> <tr>
<td class="moderation-expanded" colspan="6"> <td class="expand-cell" colspan="6">
<div> <div class="d-flex">
<span i18n class="moderation-expanded-label">Reason:</span> <div class="col-8">
<span class="moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span> <div class="d-flex">
<span class="col-3 moderation-expanded-label" i18n>Reason:</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-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
</div>
</div>
<div class="col-4">
<div class="screenratio" [innerHTML]="videoAbuse.embedHtml"></div>
</div> </div>
<div *ngIf="videoAbuse.moderationComment">
<span i18n class="moderation-expanded-label">Moderation comment:</span>
<span class="moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -10,6 +10,10 @@ import { ConfirmService } from '../../../core/index'
import { ModerationCommentModalComponent } from './moderation-comment-modal.component' import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
import { Video } from '../../../shared/video/video.model' import { Video } from '../../../shared/video/video.model'
import { MarkdownService } from '@app/shared/renderer' import { MarkdownService } from '@app/shared/renderer'
import { Actor } from '@app/shared/actor/actor.model'
import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils'
import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
import { DomSanitizer } from '@angular/platform-browser'
@Component({ @Component({
selector: 'my-video-abuse-list', selector: 'my-video-abuse-list',
@ -32,7 +36,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
private videoAbuseService: VideoAbuseService, private videoAbuseService: VideoAbuseService,
private confirmService: ConfirmService, private confirmService: ConfirmService,
private i18n: I18n, private i18n: I18n,
private markdownRenderer: MarkdownService private markdownRenderer: MarkdownService,
private sanitizer: DomSanitizer
) { ) {
super() super()
@ -42,8 +47,14 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
handler: videoAbuse => this.removeVideoAbuse(videoAbuse) handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
}, },
{ {
label: this.i18n('Update moderation comment'), label: this.i18n('Add note'),
handler: videoAbuse => this.openModerationCommentModal(videoAbuse) handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
isDisplayed: videoAbuse => !videoAbuse.moderationComment
},
{
label: this.i18n('Update note'),
handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
isDisplayed: videoAbuse => !!videoAbuse.moderationComment
}, },
{ {
label: this.i18n('Mark as accepted'), label: this.i18n('Mark as accepted'),
@ -90,6 +101,19 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
return Video.buildClientUrl(videoAbuse.video.uuid) return Video.buildClientUrl(videoAbuse.video.uuid)
} }
getVideoEmbed (videoAbuse: VideoAbuse) {
const absoluteAPIUrl = 'http://localhost:9000' || getAbsoluteAPIUrl()
const embedUrl = buildVideoLink({
baseUrl: absoluteAPIUrl + '/videos/embed/' + videoAbuse.video.uuid,
warningTitle: false
})
return buildVideoEmbed(embedUrl)
}
switchToDefaultAvatar ($event: Event) {
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
}
async removeVideoAbuse (videoAbuse: VideoAbuse) { async removeVideoAbuse (videoAbuse: VideoAbuse) {
const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
if (res === false) return if (res === false) return
@ -125,7 +149,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
for (const abuse of this.videoAbuses) { for (const abuse of this.videoAbuses) {
Object.assign(abuse, { Object.assign(abuse, {
reasonHtml: await this.toHtml(abuse.reason), reasonHtml: await this.toHtml(abuse.reason),
moderationCommentHtml: await this.toHtml(abuse.moderationComment) moderationCommentHtml: await this.toHtml(abuse.moderationComment),
embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
}) })
} }

View File

@ -42,9 +42,9 @@
<ng-template pTemplate="rowexpansion" let-videoBlacklist> <ng-template pTemplate="rowexpansion" let-videoBlacklist>
<tr> <tr>
<td class="moderation-expanded" colspan="6"> <td class="expand-cell" colspan="6">
<span i18n class="moderation-expanded-label">Blacklist reason:</span> <span class="col-2 moderation-expanded-label" i18n>Blacklist reason:</span>
<span class="moderation-expanded-text" [innerHTML]="videoBlacklist.reasonHtml"></span> <span class="col-9 moderation-expanded-text" [innerHTML]="videoBlacklist.reasonHtml"></span>
</td> </td>
</tr> </tr>
</ng-template> </ng-template>

View File

@ -51,19 +51,30 @@
<ng-template pTemplate="body" let-expanded="expanded" let-user> <ng-template pTemplate="body" let-expanded="expanded" let-user>
<tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }"> <tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }">
<td class="expand-cell"> <td>
<p-tableCheckbox [value]="user"></p-tableCheckbox> <p-tableCheckbox [value]="user"></p-tableCheckbox>
</td> </td>
<td> <td class="expand-cell">
<span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user"> <span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user">
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
</span> </span>
</td> </td>
<td> <td>
<a i18n-title title="Go to the account page" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]"> <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
{{ user.username }} <div class="chip two-lines">
<img
class="avatar"
[src]="user?.account?.avatar?.path"
(error)="switchToDefaultAvatar($event)"
alt="Avatar"
>
<div>
{{ user.account.displayName }}
<span class="text-muted">{{ user.username }}</span>
</div>
</div>
<span i18n *ngIf="user.blocked" class="banned-info">(banned)</span> <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
</a> </a>
</td> </td>

View File

@ -29,3 +29,7 @@ p-tableCheckbox {
position: relative; position: relative;
top: -2.5px; top: -2.5px;
} }
.chip {
@include chip;
}

View File

@ -7,6 +7,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
import { ServerConfig, User } from '../../../../../../shared' import { ServerConfig, User } from '../../../../../../shared'
import { UserBanModalComponent } from '@app/shared/moderation' import { UserBanModalComponent } from '@app/shared/moderation'
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
import { Actor } from '@app/shared/actor/actor.model'
@Component({ @Component({
selector: 'my-user-list', selector: 'my-user-list',
@ -105,6 +106,10 @@ export class UserListComponent extends RestTable implements OnInit {
this.loadData() this.loadData()
} }
switchToDefaultAvatar ($event: Event) {
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
}
async unbanUsers (users: User[]) { async unbanUsers (users: User[]) {
const message = this.i18n('Do you really want to unban {{num}} users?', { num: users.length }) const message = this.i18n('Do you really want to unban {{num}} users?', { num: users.length })

View File

@ -56,7 +56,7 @@ export class MyAccountVideoChannelsComponent implements OnInit {
display: false, display: false,
ticks: { ticks: {
min: Math.max(0, this.videoChannelsMinimumDailyViews - (3 * this.videoChannelsMaximumDailyViews / 100)), min: Math.max(0, this.videoChannelsMinimumDailyViews - (3 * this.videoChannelsMaximumDailyViews / 100)),
max: this.videoChannelsMaximumDailyViews max: Math.max(1, this.videoChannelsMaximumDailyViews)
} }
}] }]
}, },

View File

@ -796,3 +796,57 @@
} }
} }
} }
@mixin chip {
$avatar-height: 1.2rem;
align-items: center;
border-radius: 5rem;
display: inline-flex;
font-size: 90%;
color: var(--mainForegroundColor);
height: $avatar-height;
line-height: .8rem;
margin: .1rem;
max-width: 320px;
overflow: hidden;
padding: .2rem .4rem;
text-decoration: none;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
.avatar {
margin-left: -.4rem;
margin-right: .2rem;
height: $avatar-height;
width: $avatar-height;
border-radius: 50%;
display: inline-block;
line-height: 1.25;
position: relative;
vertical-align: middle;
}
&.two-lines {
$avatar-height: 1.8rem;
height: $avatar-height;
.avatar {
height: $avatar-height;
width: $avatar-height;
}
div {
display: flex;
flex-direction: column;
font-size: 80%;
height: $avatar-height;
margin-left: .1rem;
margin-right: .1rem;
justify-content: center;
}
}
}