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'
VideoAbusesComponent,
VideoAbuseListComponent,
+ ModerationCommentModalComponent,
JobsComponent,
JobsListComponent,
@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;
}
export * from './video-abuse-list.component'
+export * from './moderation-comment-modal.component'
--- /dev/null
+<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>
\ No newline at end of file
--- /dev/null
+@import 'variables';
+@import 'mixins';
+
+textarea {
+ @include peertube-textarea(100%, 100px);
+}
--- /dev/null
+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)
+ )
+ }
+
+}
<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>
\ No newline at end of file
-/deep/ a {
+@import '_variables';
+@import '_mixins';
- &, &:hover, &:active, &:focus {
- color: #000;
- }
-}
+.moderation-comment-label {
+ font-weight: $font-semibold;
+}
\ No newline at end of file
-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',
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(
@Injectable()
export class VideoAbuseValidatorsService {
readonly VIDEO_ABUSE_REASON: BuildFormValidator
+ readonly VIDEO_ABUSE_MODERATION_COMMENT: BuildFormValidator
constructor (private i18n: I18n) {
this.VIDEO_ABUSE_REASON = {
'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.')
+ }
+ }
}
}
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'
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))
+ )
+ }}
@import 'mixins';
textarea {
- @include peertube-textarea(100%, 60px);
+ @include peertube-textarea(100%, 100px);
}
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'