Add ability to delete and update abuse on client

This commit is contained in:
Chocobozzz 2018-08-13 11:54:11 +02:00
parent 7f7680641b
commit efc9e8450a
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
13 changed files with 260 additions and 20 deletions

View File

@ -11,7 +11,7 @@ import { JobsComponent } from './jobs/job.component'
import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
import { JobService } from './jobs/shared/job.service'
import { UserCreateComponent, UserListComponent, UsersComponent, UserService, UserUpdateComponent } from './users'
import { VideoAbuseListComponent, VideoAbusesComponent } from './video-abuses'
import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoAbusesComponent } from './video-abuses'
import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-blacklist'
import { UserBanModalComponent } from '@app/+admin/users/user-list/user-ban-modal.component'
@ -41,6 +41,7 @@ import { UserBanModalComponent } from '@app/+admin/users/user-list/user-ban-moda
VideoAbusesComponent,
VideoAbuseListComponent,
ModerationCommentModalComponent,
JobsComponent,
JobsListComponent,

View File

@ -5,12 +5,6 @@
@include create-button('../../../../assets/images/global/add.svg');
}
my-action-dropdown /deep/ .icon {
&.icon-ban {
background-image: url('../../../../assets/images/global/edit-black.svg');
}
}
tr.banned {
color: red;
}

View File

@ -1 +1,2 @@
export * from './video-abuse-list.component'
export * from './moderation-comment-modal.component'

View File

@ -0,0 +1,32 @@
<ng-template #modal>
<div class="modal-header">
<h4 i18n class="modal-title">Moderation comment</h4>
<span class="close" aria-hidden="true" (click)="hideModerationCommentModal()"></span>
</div>
<div class="modal-body">
<form novalidate [formGroup]="form" (ngSubmit)="banUser()">
<div class="form-group">
<textarea formControlName="moderationComment" [ngClass]="{ 'input-error': formErrors['moderationComment'] }">
</textarea>
<div *ngIf="formErrors.moderationComment" class="form-error">
{{ formErrors.moderationComment }}
</div>
</div>
<div i18n>
This comment can only be seen by you or the other moderators.
</div>
<div class="form-group inputs">
<span i18n class="action-button action-button-cancel" (click)="hideModerationCommentModal()">Cancel</span>
<input
type="submit" i18n-value value="Update this comment" class="action-button-submit"
[disabled]="!form.valid"
>
</div>
</form>
</div>
</ng-template>

View File

@ -0,0 +1,6 @@
@import 'variables';
@import 'mixins';
textarea {
@include peertube-textarea(100%, 100px);
}

View File

@ -0,0 +1,73 @@
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { FormReactive, VideoAbuseService, VideoAbuseValidatorsService } from '../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { VideoAbuse } from '../../../../../../shared/models/videos'
@Component({
selector: 'my-moderation-comment-modal',
templateUrl: './moderation-comment-modal.component.html',
styleUrls: [ './moderation-comment-modal.component.scss' ]
})
export class ModerationCommentModalComponent extends FormReactive implements OnInit {
@ViewChild('modal') modal: NgbModal
@Output() commentUpdated = new EventEmitter<string>()
private abuseToComment: VideoAbuse
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
private modalService: NgbModal,
private notificationsService: NotificationsService,
private videoAbuseService: VideoAbuseService,
private videoAbuseValidatorsService: VideoAbuseValidatorsService,
private i18n: I18n
) {
super()
}
ngOnInit () {
this.buildForm({
moderationComment: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON
})
}
openModal (abuseToComment: VideoAbuse) {
this.abuseToComment = abuseToComment
this.openedModal = this.modalService.open(this.modal)
this.form.patchValue({
moderationComment: this.abuseToComment.moderationComment
})
}
hideModerationCommentModal () {
this.abuseToComment = undefined
this.openedModal.close()
this.form.reset()
}
async banUser () {
const moderationComment: string = this.form.value['moderationComment']
this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment })
.subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('Comment updated.')
)
this.commentUpdated.emit(moderationComment)
this.hideModerationCommentModal()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}
}

View File

@ -4,31 +4,63 @@
<p-table
[value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
>
<ng-template pTemplate="header">
<tr>
<th style="width: 40px"></th>
<th i18n style="width: 80px;">State</th>
<th i18n>Reason</th>
<th i18n>Reporter</th>
<th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th i18n>Video</th>
<th style="width: 50px;"></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-videoAbuse>
<ng-template pTemplate="body" let-expanded="expanded" let-videoAbuse>
<tr>
<td>
<span *ngIf="videoAbuse.moderationComment" class="expander" [pRowToggler]="videoAbuse">
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
</span>
</td>
<td>
<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>
</td>
<td>{{ videoAbuse.reason }}</td>
<td>
<a [href]="videoAbuse.reporterAccount.url" i18n-title title="Go to the account" target="_blank" rel="noopener noreferrer">
{{ createByString(videoAbuse.reporterAccount) }}
</a>
</td>
<td>{{ videoAbuse.createdAt }}</td>
<td>
<a [href]="videoAbuse.video.url" i18n-title title="Go to the video" target="_blank" rel="noopener noreferrer">
{{ videoAbuse.video.name }}
</a>
</td>
<td class="action-cell">
<my-action-dropdown i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
</td>
</tr>
</ng-template>
<ng-template pTemplate="rowexpansion" let-videoAbuse>
<tr class="moderation-comment">
<td colspan="7">
<span i18n class="moderation-comment-label">Moderation comment:</span>
{{ videoAbuse.moderationComment }}
</td>
</tr>
</ng-template>
</p-table>
<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>

View File

@ -1,6 +1,6 @@
/deep/ a {
@import '_variables';
@import '_mixins';
&, &:hover, &:active, &:focus {
color: #000;
}
}
.moderation-comment-label {
font-weight: $font-semibold;
}

View File

@ -1,11 +1,13 @@
import { Component, OnInit } from '@angular/core'
import { Component, OnInit, ViewChild } from '@angular/core'
import { Account } from '@app/shared/account/account.model'
import { NotificationsService } from 'angular2-notifications'
import { SortMeta } from 'primeng/components/common/sortmeta'
import { VideoAbuse } from '../../../../../../shared'
import { VideoAbuse, VideoAbuseState } from '../../../../../../shared'
import { RestPagination, RestTable, VideoAbuseService } from '../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
import { ConfirmService } from '@app/core'
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
@Component({
selector: 'my-video-abuse-list',
@ -13,28 +15,97 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
styleUrls: [ './video-abuse-list.component.scss']
})
export class VideoAbuseListComponent extends RestTable implements OnInit {
@ViewChild('moderationCommentModal') moderationCommentModal: ModerationCommentModalComponent
videoAbuses: VideoAbuse[] = []
totalRecords = 0
rowsPerPage = 10
sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
videoAbuseActions: DropdownAction<VideoAbuse>[] = []
constructor (
private notificationsService: NotificationsService,
private videoAbuseService: VideoAbuseService,
private confirmService: ConfirmService,
private i18n: I18n
) {
super()
this.videoAbuseActions = [
{
label: this.i18n('Delete'),
handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
},
{
label: this.i18n('Update moderation comment'),
handler: videoAbuse => this.openModerationCommentModal(videoAbuse)
},
{
label: this.i18n('Mark as accepted'),
handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED),
isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse)
},
{
label: this.i18n('Mark as rejected'),
handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED),
isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse)
}
]
}
ngOnInit () {
this.loadSort()
}
openModerationCommentModal (videoAbuse: VideoAbuse) {
this.moderationCommentModal.openModal(videoAbuse)
}
onModerationCommentUpdated () {
this.loadData()
}
createByString (account: Account) {
return Account.CREATE_BY_STRING(account.name, account.host)
}
isVideoAbuseAccepted (videoAbuse: VideoAbuse) {
return videoAbuse.state.id === VideoAbuseState.ACCEPTED
}
isVideoAbuseRejected (videoAbuse: VideoAbuse) {
return videoAbuse.state.id === VideoAbuseState.REJECTED
}
async removeVideoAbuse (videoAbuse: VideoAbuse) {
const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse?'), this.i18n('Delete'))
if (res === false) return
this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('Abuse deleted.')
)
this.loadData()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}
updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) {
this.videoAbuseService.updateVideoAbuse(videoAbuse, { state })
.subscribe(
() => this.loadData(),
err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}
protected loadData () {
return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
.subscribe(

View File

@ -6,6 +6,7 @@ import { BuildFormValidator } from '@app/shared'
@Injectable()
export class VideoAbuseValidatorsService {
readonly VIDEO_ABUSE_REASON: BuildFormValidator
readonly VIDEO_ABUSE_MODERATION_COMMENT: BuildFormValidator
constructor (private i18n: I18n) {
this.VIDEO_ABUSE_REASON = {
@ -16,5 +17,14 @@ export class VideoAbuseValidatorsService {
'maxlength': this.i18n('Report reason cannot be more than 300 characters long.')
}
}
this.VIDEO_ABUSE_MODERATION_COMMENT = {
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ],
MESSAGES: {
'required': this.i18n('Moderation comment is required.'),
'minlength': this.i18n('Moderation comment must be at least 2 characters long.'),
'maxlength': this.i18n('Moderation comment cannot be more than 300 characters long.')
}
}
}
}

View File

@ -3,7 +3,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { SortMeta } from 'primeng/components/common/sortmeta'
import { Observable } from 'rxjs'
import { ResultList, VideoAbuse } from '../../../../../shared'
import { ResultList, VideoAbuse, VideoAbuseUpdate } from '../../../../../shared'
import { environment } from '../../../environments/environment'
import { RestExtractor, RestPagination, RestService } from '../rest'
@ -42,4 +42,23 @@ export class VideoAbuseService {
catchError(res => this.restExtractor.handleError(res))
)
}
}
updateVideoAbuse (videoAbuse: VideoAbuse, abuseUpdate: VideoAbuseUpdate) {
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id
return this.authHttp.put(url, abuseUpdate)
.pipe(
map(this.restExtractor.extractDataBool),
catchError(res => this.restExtractor.handleError(res))
)
}
removeVideoAbuse (videoAbuse: VideoAbuse) {
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id
return this.authHttp.delete(url)
.pipe(
map(this.restExtractor.extractDataBool),
catchError(res => this.restExtractor.handleError(res))
)
}}

View File

@ -2,5 +2,5 @@
@import 'mixins';
textarea {
@include peertube-textarea(100%, 60px);
@include peertube-textarea(100%, 100px);
}

View File

@ -4,6 +4,7 @@ export * from './user-video-rate.type'
export * from './video-abuse-state.model'
export * from './video-abuse-create.model'
export * from './video-abuse.model'
export * from './video-abuse-update.model'
export * from './video-blacklist.model'
export * from './video-channel-create.model'
export * from './video-channel-update.model'