Add ability to delete and update abuse on client
authorChocobozzz <me@florianbigard.com>
Mon, 13 Aug 2018 09:54:11 +0000 (11:54 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 14 Aug 2018 07:27:17 +0000 (09:27 +0200)
13 files changed:
client/src/app/+admin/admin.module.ts
client/src/app/+admin/users/user-list/user-list.component.scss
client/src/app/+admin/video-abuses/video-abuse-list/index.ts
client/src/app/+admin/video-abuses/video-abuse-list/moderation-comment-modal.component.html [new file with mode: 0644]
client/src/app/+admin/video-abuses/video-abuse-list/moderation-comment-modal.component.scss [new file with mode: 0644]
client/src/app/+admin/video-abuses/video-abuse-list/moderation-comment-modal.component.ts [new file with mode: 0644]
client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.scss
client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts
client/src/app/shared/video-abuse/video-abuse.service.ts
client/src/app/videos/+video-watch/modal/video-report.component.scss
shared/models/videos/index.ts

index 04b7ec5c1c724c776128fced562cdf712318f5c0..23ca5a6b3a13bfaaf62dd3fb715922a78e261a3d 100644 (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,
index 2d11dd7a0e76eb6bfc5f40683992a3086199fc64..47291918dffd91643a05689a816061c8bd60d2ee 100644 (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;
 }
index 01c24d860a737397d120ee276310fc7e1b334c88..da7176e522d1471af20632ebd2193a8c1b3f4958 100644 (file)
@@ -1 +1,2 @@
 export * from './video-abuse-list.component'
+export * from './moderation-comment-modal.component'
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/video-abuses/video-abuse-list/moderation-comment-modal.component.html
new file mode 100644 (file)
index 0000000..3a8424f
--- /dev/null
@@ -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>
\ No newline at end of file
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/moderation-comment-modal.component.scss b/client/src/app/+admin/video-abuses/video-abuse-list/moderation-comment-modal.component.scss
new file mode 100644 (file)
index 0000000..afcdb9a
--- /dev/null
@@ -0,0 +1,6 @@
+@import 'variables';
+@import 'mixins';
+
+textarea {
+  @include peertube-textarea(100%, 100px);
+}
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/video-abuses/video-abuse-list/moderation-comment-modal.component.ts
new file mode 100644 (file)
index 0000000..7e8af6e
--- /dev/null
@@ -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)
+      )
+  }
+
+}
index 8111e5f73f0b306c9db083edf021cdb730087c90..08501d872d6b365c1f664b848c067203951edde9 100644 (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>
\ No newline at end of file
index 6a4762650abbe9180b7cbca27beaee98befb5ee2..951a3fc92d9af9f5264d5af3ad8e12a3d67468c1 100644 (file)
@@ -1,6 +1,6 @@
-/deep/ a {
+@import '_variables';
+@import '_mixins';
 
-  &, &:hover, &:active, &:focus {
-    color: #000;
-  }
-}
+.moderation-comment-label {
+  font-weight: $font-semibold;
+}
\ No newline at end of file
index 6ddebff7e8263bc9e6acd35207b137c6e83351de..a850c0ec2cedd53cf832769a9ce583cc098b3662 100644 (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(
index 774e6a488bce479a9c1ea80f9e8af9ef30aeb7bf..6e98066118176661732c11e88674ce68bec5d5d8 100644 (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.')
+      }
+    }
   }
 }
index 6fab3ef43e32456ba86facaf02e1413644b6b660..61b7e1b9880dfbbdeacd1e3b6f229229eaf6af7c 100644 (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))
+               )
+  }}
index 84562f15cf24c3b83ecbed0a2d1da194eb982bf9..afcdb9a16f31ceb4db00a0714dd1c7059538b839 100644 (file)
@@ -2,5 +2,5 @@
 @import 'mixins';
 
 textarea {
-  @include peertube-textarea(100%, 60px);
+  @include peertube-textarea(100%, 100px);
 }
index 2d7de2a0d08d708cdd5f3b92692312959df0099f..02bf2b84248057b4d2ac2f8167b695ede78cb94c 100644 (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'