mirror of https://github.com/Chocobozzz/PeerTube
Add abuse message management in admin
parent
edbc932546
commit
441e453ae5
|
@ -46,6 +46,7 @@
|
||||||
<th i18n>Video/Comment/Account</th>
|
<th i18n>Video/Comment/Account</th>
|
||||||
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
<th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
|
<th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||||
|
<th i18n style="width: 80px;">Messages</th>
|
||||||
<th style="width: 150px;"></th>
|
<th style="width: 150px;"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -157,6 +158,12 @@
|
||||||
<span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
|
<span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)">
|
||||||
|
{{ abuse.countMessages }}
|
||||||
|
|
||||||
|
<my-global-icon iconName="message-circle"></my-global-icon>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
<my-action-dropdown
|
<my-action-dropdown
|
||||||
[ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
|
[ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
|
||||||
|
@ -187,3 +194,4 @@
|
||||||
</p-table>
|
</p-table>
|
||||||
|
|
||||||
<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
|
<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
|
||||||
|
<my-abuse-message-modal #abuseMessagesModal (countMessagesUpdated)="onCountMessagesUpdated($event)"></my-abuse-message-modal>
|
||||||
|
|
|
@ -21,3 +21,12 @@
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.abuse-messages {
|
||||||
|
my-global-icon {
|
||||||
|
width: 22px;
|
||||||
|
margin-left: 3px;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,17 +8,17 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router'
|
import { ActivatedRoute, Params, Router } from '@angular/router'
|
||||||
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
|
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||||
import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
|
import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
|
||||||
import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
|
import { AbuseService, BlocklistService, VideoBlockService, AbuseMessageModalComponent } from '@app/shared/shared-moderation'
|
||||||
import { VideoCommentService } from '@app/shared/shared-video-comment'
|
import { VideoCommentService } from '@app/shared/shared-video-comment'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { Abuse, AbuseState } from '@shared/models'
|
import { AdminAbuse, AbuseState } from '@shared/models'
|
||||||
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
||||||
|
|
||||||
const logger = debug('peertube:moderation:AbuseListComponent')
|
const logger = debug('peertube:moderation:AbuseListComponent')
|
||||||
|
|
||||||
// Don't use an abuse model because we need external services to compute some properties
|
// Don't use an abuse model because we need external services to compute some properties
|
||||||
// And this model is only used in this component
|
// And this model is only used in this component
|
||||||
export type ProcessedAbuse = Abuse & {
|
export type ProcessedAbuse = AdminAbuse & {
|
||||||
moderationCommentHtml?: string,
|
moderationCommentHtml?: string,
|
||||||
reasonHtml?: string
|
reasonHtml?: string
|
||||||
embedHtml?: SafeHtml
|
embedHtml?: SafeHtml
|
||||||
|
@ -31,8 +31,8 @@ export type ProcessedAbuse = Abuse & {
|
||||||
truncatedCommentHtml?: string
|
truncatedCommentHtml?: string
|
||||||
commentHtml?: string
|
commentHtml?: string
|
||||||
|
|
||||||
video: Abuse['video'] & {
|
video: AdminAbuse['video'] & {
|
||||||
channel: Abuse['video']['channel'] & {
|
channel: AdminAbuse['video']['channel'] & {
|
||||||
ownerAccount: Account
|
ownerAccount: Account
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,7 @@ export type ProcessedAbuse = Abuse & {
|
||||||
})
|
})
|
||||||
export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit {
|
export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit {
|
||||||
@ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
|
@ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
|
||||||
|
@ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
|
||||||
|
|
||||||
abuses: ProcessedAbuse[] = []
|
abuses: ProcessedAbuse[] = []
|
||||||
totalRecords = 0
|
totalRecords = 0
|
||||||
|
@ -104,7 +105,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
return 'AbuseListComponent'
|
return 'AbuseListComponent'
|
||||||
}
|
}
|
||||||
|
|
||||||
openModerationCommentModal (abuse: Abuse) {
|
openModerationCommentModal (abuse: AdminAbuse) {
|
||||||
this.moderationCommentModal.openModal(abuse)
|
this.moderationCommentModal.openModal(abuse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,19 +133,19 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
}
|
}
|
||||||
/* END Table filter functions */
|
/* END Table filter functions */
|
||||||
|
|
||||||
isAbuseAccepted (abuse: Abuse) {
|
isAbuseAccepted (abuse: AdminAbuse) {
|
||||||
return abuse.state.id === AbuseState.ACCEPTED
|
return abuse.state.id === AbuseState.ACCEPTED
|
||||||
}
|
}
|
||||||
|
|
||||||
isAbuseRejected (abuse: Abuse) {
|
isAbuseRejected (abuse: AdminAbuse) {
|
||||||
return abuse.state.id === AbuseState.REJECTED
|
return abuse.state.id === AbuseState.REJECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideoUrl (abuse: Abuse) {
|
getVideoUrl (abuse: AdminAbuse) {
|
||||||
return Video.buildClientUrl(abuse.video.uuid)
|
return Video.buildClientUrl(abuse.video.uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommentUrl (abuse: Abuse) {
|
getCommentUrl (abuse: AdminAbuse) {
|
||||||
return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
|
return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,7 +153,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
return '/accounts/' + abuse.flaggedAccount.nameWithHost
|
return '/accounts/' + abuse.flaggedAccount.nameWithHost
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideoEmbed (abuse: Abuse) {
|
getVideoEmbed (abuse: AdminAbuse) {
|
||||||
return buildVideoEmbed(
|
return buildVideoEmbed(
|
||||||
buildVideoLink({
|
buildVideoLink({
|
||||||
baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
|
baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
|
||||||
|
@ -168,7 +169,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
|
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAbuse (abuse: Abuse) {
|
async removeAbuse (abuse: AdminAbuse) {
|
||||||
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
|
||||||
|
|
||||||
|
@ -182,7 +183,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAbuseState (abuse: Abuse, state: AbuseState) {
|
updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
|
||||||
this.abuseService.updateAbuse(abuse, { state })
|
this.abuseService.updateAbuse(abuse, { state })
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => this.loadData(),
|
() => this.loadData(),
|
||||||
|
@ -191,10 +192,25 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
|
||||||
|
const abuse = this.abuses.find(a => a.id === event.abuseId)
|
||||||
|
|
||||||
|
if (!abuse) {
|
||||||
|
console.error('Cannot find abuse %d.', event.abuseId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
abuse.countMessages = event.countMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
openAbuseMessagesModal (abuse: AdminAbuse) {
|
||||||
|
this.abuseMessagesModal.openModal(abuse)
|
||||||
|
}
|
||||||
|
|
||||||
protected loadData () {
|
protected loadData () {
|
||||||
logger('Load data.')
|
logger('Load data.')
|
||||||
|
|
||||||
return this.abuseService.getAbuses({
|
return this.abuseService.getAdminAbuses({
|
||||||
pagination: this.pagination,
|
pagination: this.pagination,
|
||||||
sort: this.sort,
|
sort: this.sort,
|
||||||
search: this.search
|
search: this.search
|
||||||
|
@ -257,7 +273,11 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
|
||||||
handler: abuse => this.removeAbuse(abuse)
|
handler: abuse => this.removeAbuse(abuse)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Add note'),
|
label: this.i18n('Messages'),
|
||||||
|
handler: abuse => this.openAbuseMessagesModal(abuse)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.i18n('Add internal note'),
|
||||||
handler: abuse => this.openModerationCommentModal(abuse),
|
handler: abuse => this.openModerationCommentModal(abuse),
|
||||||
isDisplayed: abuse => !abuse.moderationComment
|
isDisplayed: abuse => !abuse.moderationComment
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { AbuseService } from '@app/shared/shared-moderation'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { Abuse } from '@shared/models'
|
import { AdminAbuse } from '@shared/models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-moderation-comment-modal',
|
selector: 'my-moderation-comment-modal',
|
||||||
|
@ -16,7 +16,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
|
||||||
@ViewChild('modal', { static: true }) modal: NgbModal
|
@ViewChild('modal', { static: true }) modal: NgbModal
|
||||||
@Output() commentUpdated = new EventEmitter<string>()
|
@Output() commentUpdated = new EventEmitter<string>()
|
||||||
|
|
||||||
private abuseToComment: Abuse
|
private abuseToComment: AdminAbuse
|
||||||
private openedModal: NgbModalRef
|
private openedModal: NgbModalRef
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
@ -36,7 +36,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
openModal (abuseToComment: Abuse) {
|
openModal (abuseToComment: AdminAbuse) {
|
||||||
this.abuseToComment = abuseToComment
|
this.abuseToComment = abuseToComment
|
||||||
this.openedModal = this.modalService.open(this.modal, { centered: true })
|
this.openedModal = this.modalService.open(this.modal, { centered: true })
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { BuildFormValidator } from './form-validator.service'
|
||||||
export class AbuseValidatorsService {
|
export class AbuseValidatorsService {
|
||||||
readonly ABUSE_REASON: BuildFormValidator
|
readonly ABUSE_REASON: BuildFormValidator
|
||||||
readonly ABUSE_MODERATION_COMMENT: BuildFormValidator
|
readonly ABUSE_MODERATION_COMMENT: BuildFormValidator
|
||||||
|
readonly ABUSE_MESSAGE: BuildFormValidator
|
||||||
|
|
||||||
constructor (private i18n: I18n) {
|
constructor (private i18n: I18n) {
|
||||||
this.ABUSE_REASON = {
|
this.ABUSE_REASON = {
|
||||||
|
@ -26,5 +27,14 @@ export class AbuseValidatorsService {
|
||||||
'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.')
|
'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.ABUSE_MESSAGE = {
|
||||||
|
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
|
||||||
|
MESSAGES: {
|
||||||
|
'required': this.i18n('Abuse message is required.'),
|
||||||
|
'minlength': this.i18n('Abuse message must be at least 2 characters long.'),
|
||||||
|
'maxlength': this.i18n('Abuse message cannot be more than 3000 characters long.')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,8 +64,7 @@ const icons = {
|
||||||
'go': require('!!raw-loader?!../../../assets/images/feather/arrow-up-right.svg').default,
|
'go': require('!!raw-loader?!../../../assets/images/feather/arrow-up-right.svg').default,
|
||||||
'cross': require('!!raw-loader?!../../../assets/images/feather/x.svg').default,
|
'cross': require('!!raw-loader?!../../../assets/images/feather/x.svg').default,
|
||||||
'tick': require('!!raw-loader?!../../../assets/images/feather/check.svg').default,
|
'tick': require('!!raw-loader?!../../../assets/images/feather/check.svg').default,
|
||||||
'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default,
|
'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default
|
||||||
'columns': require('!!raw-loader?!../../../assets/images/feather/columns.svg').default
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GlobalIconName = keyof typeof icons
|
export type GlobalIconName = keyof typeof icons
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<ng-template #modal>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 i18n class="modal-title">Messages</h4>
|
||||||
|
|
||||||
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="messages" #messagesBlock>
|
||||||
|
<div
|
||||||
|
*ngFor="let message of abuseMessages"
|
||||||
|
class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="author">{{ message.account.name }}</div>
|
||||||
|
|
||||||
|
<div class="bubble">
|
||||||
|
<div class="content">{{ message.message }}</div>
|
||||||
|
<div class="date">{{ message.createdAt | date }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form novalidate [formGroup]="form" (ngSubmit)="addMessage()">
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea formControlName="message" ngbAutofocus [ngClass]="{ 'input-error': formErrors['message'] }" class="form-control"></textarea>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.message" class="form-error">
|
||||||
|
{{ formErrors.message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group inputs">
|
||||||
|
<input type="submit" i18n-value value="Add message" class="action-button-submit" [disabled]="!form.valid || sendingMessage">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,57 @@
|
||||||
|
@import 'variables';
|
||||||
|
@import 'mixins';
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin: 20px 20px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
@include peertube-textarea(100%, 70px);
|
||||||
|
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: scroll;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-block {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
max-width: 60%;
|
||||||
|
|
||||||
|
.author {
|
||||||
|
color: var(--greyForegroundColor);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
color: var(--mainForegroundColor);
|
||||||
|
background-color: var(--greyBackgroundColor);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
|
||||||
|
&.by-me {
|
||||||
|
color: var(--mainForegroundColor);
|
||||||
|
background-color: var(--secondaryColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.by-moderator {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--mainColor);
|
||||||
|
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--greyForegroundColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { Component, ElementRef, EventEmitter, Output, ViewChild, OnInit } from '@angular/core'
|
||||||
|
import { Notifier, AuthService } from '@app/core'
|
||||||
|
import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { AbuseMessage, UserAbuse } from '@shared/models'
|
||||||
|
import { AbuseService } from './abuse.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-abuse-message-modal',
|
||||||
|
templateUrl: './abuse-message-modal.component.html',
|
||||||
|
styleUrls: [ './abuse-message-modal.component.scss' ]
|
||||||
|
})
|
||||||
|
export class AbuseMessageModalComponent extends FormReactive implements OnInit {
|
||||||
|
@ViewChild('modal', { static: true }) modal: NgbModal
|
||||||
|
@ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef
|
||||||
|
|
||||||
|
@Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>()
|
||||||
|
|
||||||
|
abuseMessages: AbuseMessage[] = []
|
||||||
|
textareaMessage: string
|
||||||
|
sendingMessage = false
|
||||||
|
|
||||||
|
private openedModal: NgbModalRef
|
||||||
|
private abuse: UserAbuse
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
protected formValidatorService: FormValidatorService,
|
||||||
|
private abuseValidatorsService: AbuseValidatorsService,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private auth: AuthService,
|
||||||
|
private notifier: Notifier,
|
||||||
|
private i18n: I18n,
|
||||||
|
private abuseService: AbuseService
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.buildForm({
|
||||||
|
message: this.abuseValidatorsService.ABUSE_MESSAGE
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
openModal (abuse: UserAbuse) {
|
||||||
|
this.abuse = abuse
|
||||||
|
|
||||||
|
this.openedModal = this.modalService.open(this.modal, { centered: true })
|
||||||
|
|
||||||
|
this.loadMessages()
|
||||||
|
}
|
||||||
|
|
||||||
|
hide () {
|
||||||
|
this.abuseMessages = []
|
||||||
|
this.openedModal.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage () {
|
||||||
|
this.sendingMessage = true
|
||||||
|
|
||||||
|
this.abuseService.addAbuseMessage(this.abuse, this.form.value['message'])
|
||||||
|
.subscribe(
|
||||||
|
() => {
|
||||||
|
this.form.reset()
|
||||||
|
this.sendingMessage = false
|
||||||
|
this.countMessagesUpdated.emit({ abuseId: this.abuse.id, countMessages: this.abuseMessages.length + 1 })
|
||||||
|
|
||||||
|
this.loadMessages()
|
||||||
|
},
|
||||||
|
|
||||||
|
err => {
|
||||||
|
this.sendingMessage = false
|
||||||
|
console.error(err)
|
||||||
|
this.notifier.error('Sorry but you cannot send this message. Please retry later')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteMessage (abuseMessage: AbuseMessage) {
|
||||||
|
this.abuseService.deleteAbuseMessage(this.abuse, abuseMessage)
|
||||||
|
.subscribe(
|
||||||
|
() => {
|
||||||
|
this.countMessagesUpdated.emit({ abuseId: this.abuse.id, countMessages: this.abuseMessages.length - 1 })
|
||||||
|
|
||||||
|
this.abuseMessages = this.abuseMessages.filter(m => m.id !== abuseMessage.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
err => this.notifier.error(err.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isMessageByMe (abuseMessage: AbuseMessage) {
|
||||||
|
return this.auth.getUser().account.id === abuseMessage.account.id
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadMessages () {
|
||||||
|
this.abuseService.listAbuseMessages(this.abuse)
|
||||||
|
.subscribe(
|
||||||
|
res => {
|
||||||
|
this.abuseMessages = res.data
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.messagesBlock) return
|
||||||
|
|
||||||
|
const element = this.messagesBlock.nativeElement as HTMLElement
|
||||||
|
element.scrollIntoView({ block: 'end', inline: 'nearest' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
err => this.notifier.error(err.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import { catchError, map } from 'rxjs/operators'
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
||||||
import { Abuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList } from '@shared/models'
|
import { AdminAbuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList, UserAbuse, AbuseMessage } from '@shared/models'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
|
||||||
|
@ -20,11 +20,11 @@ export class AbuseService {
|
||||||
private restExtractor: RestExtractor
|
private restExtractor: RestExtractor
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
getAbuses (options: {
|
getAdminAbuses (options: {
|
||||||
pagination: RestPagination,
|
pagination: RestPagination,
|
||||||
sort: SortMeta,
|
sort: SortMeta,
|
||||||
search?: string
|
search?: string
|
||||||
}): Observable<ResultList<Abuse>> {
|
}): Observable<ResultList<AdminAbuse>> {
|
||||||
const { pagination, sort, search } = options
|
const { pagination, sort, search } = options
|
||||||
const url = AbuseService.BASE_ABUSE_URL
|
const url = AbuseService.BASE_ABUSE_URL
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ export class AbuseService {
|
||||||
params = this.restService.addObjectParams(params, filters)
|
params = this.restService.addObjectParams(params, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<Abuse>>(url, { params })
|
return this.authHttp.get<ResultList<AdminAbuse>>(url, { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
)
|
)
|
||||||
|
@ -79,7 +79,7 @@ export class AbuseService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) {
|
updateAbuse (abuse: AdminAbuse, abuseUpdate: AbuseUpdate) {
|
||||||
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
|
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
|
||||||
|
|
||||||
return this.authHttp.put(url, abuseUpdate)
|
return this.authHttp.put(url, abuseUpdate)
|
||||||
|
@ -89,7 +89,7 @@ export class AbuseService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAbuse (abuse: Abuse) {
|
removeAbuse (abuse: AdminAbuse) {
|
||||||
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
|
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
|
||||||
|
|
||||||
return this.authHttp.delete(url)
|
return this.authHttp.delete(url)
|
||||||
|
@ -99,6 +99,35 @@ export class AbuseService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addAbuseMessage (abuse: UserAbuse, message: string) {
|
||||||
|
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id + '/messages'
|
||||||
|
|
||||||
|
return this.authHttp.post(url, { message })
|
||||||
|
.pipe(
|
||||||
|
map(this.restExtractor.extractDataBool),
|
||||||
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
listAbuseMessages (abuse: UserAbuse) {
|
||||||
|
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id + '/messages'
|
||||||
|
|
||||||
|
return this.authHttp.get<ResultList<AbuseMessage>>(url)
|
||||||
|
.pipe(
|
||||||
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAbuseMessage (abuse: UserAbuse, abuseMessage: AbuseMessage) {
|
||||||
|
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id + '/messages/' + abuseMessage.id
|
||||||
|
|
||||||
|
return this.authHttp.delete(url)
|
||||||
|
.pipe(
|
||||||
|
map(this.restExtractor.extractDataBool),
|
||||||
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
getPrefefinedReasons (type: AbuseFilter) {
|
getPrefefinedReasons (type: AbuseFilter) {
|
||||||
let reasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [
|
let reasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export * from './report-modals'
|
export * from './report-modals'
|
||||||
|
|
||||||
|
export * from './abuse-message-modal.component'
|
||||||
export * from './abuse.service'
|
export * from './abuse.service'
|
||||||
export * from './account-block.model'
|
export * from './account-block.model'
|
||||||
export * from './account-blocklist.component'
|
export * from './account-blocklist.component'
|
||||||
|
|
|
@ -4,15 +4,16 @@ import { SharedFormModule } from '../shared-forms/shared-form.module'
|
||||||
import { SharedGlobalIconModule } from '../shared-icons'
|
import { SharedGlobalIconModule } from '../shared-icons'
|
||||||
import { SharedMainModule } from '../shared-main/shared-main.module'
|
import { SharedMainModule } from '../shared-main/shared-main.module'
|
||||||
import { SharedVideoCommentModule } from '../shared-video-comment'
|
import { SharedVideoCommentModule } from '../shared-video-comment'
|
||||||
|
import { AbuseMessageModalComponent } from './abuse-message-modal.component'
|
||||||
import { AbuseService } from './abuse.service'
|
import { AbuseService } from './abuse.service'
|
||||||
import { BatchDomainsModalComponent } from './batch-domains-modal.component'
|
import { BatchDomainsModalComponent } from './batch-domains-modal.component'
|
||||||
import { BlocklistService } from './blocklist.service'
|
import { BlocklistService } from './blocklist.service'
|
||||||
import { BulkService } from './bulk.service'
|
import { BulkService } from './bulk.service'
|
||||||
|
import { AccountReportComponent, CommentReportComponent, VideoReportComponent } from './report-modals'
|
||||||
import { UserBanModalComponent } from './user-ban-modal.component'
|
import { UserBanModalComponent } from './user-ban-modal.component'
|
||||||
import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
|
import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
|
||||||
import { VideoBlockComponent } from './video-block.component'
|
import { VideoBlockComponent } from './video-block.component'
|
||||||
import { VideoBlockService } from './video-block.service'
|
import { VideoBlockService } from './video-block.service'
|
||||||
import { VideoReportComponent, AccountReportComponent, CommentReportComponent } from './report-modals'
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -29,7 +30,8 @@ import { VideoReportComponent, AccountReportComponent, CommentReportComponent }
|
||||||
VideoReportComponent,
|
VideoReportComponent,
|
||||||
BatchDomainsModalComponent,
|
BatchDomainsModalComponent,
|
||||||
CommentReportComponent,
|
CommentReportComponent,
|
||||||
AccountReportComponent
|
AccountReportComponent,
|
||||||
|
AbuseMessageModalComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -39,7 +41,8 @@ import { VideoReportComponent, AccountReportComponent, CommentReportComponent }
|
||||||
VideoReportComponent,
|
VideoReportComponent,
|
||||||
BatchDomainsModalComponent,
|
BatchDomainsModalComponent,
|
||||||
CommentReportComponent,
|
CommentReportComponent,
|
||||||
AccountReportComponent
|
AccountReportComponent,
|
||||||
|
AbuseMessageModalComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-circle"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
After Width: | Height: | Size: 428 B |
|
@ -32,6 +32,7 @@ body {
|
||||||
--secondaryColor: #{$secondary-color};
|
--secondaryColor: #{$secondary-color};
|
||||||
|
|
||||||
--greyForegroundColor: #{$grey-foreground-color};
|
--greyForegroundColor: #{$grey-foreground-color};
|
||||||
|
--greyBackgroundColor: #{$grey-background-color};
|
||||||
|
|
||||||
--menuBackgroundColor: #{$menu-background};
|
--menuBackgroundColor: #{$menu-background};
|
||||||
--menuForegroundColor: #{$menu-color};
|
--menuForegroundColor: #{$menu-color};
|
||||||
|
|
|
@ -92,6 +92,7 @@ $variables: (
|
||||||
--secondaryColor: var(--secondaryColor),
|
--secondaryColor: var(--secondaryColor),
|
||||||
|
|
||||||
--greyForegroundColor: var(--greyForegroundColor),
|
--greyForegroundColor: var(--greyForegroundColor),
|
||||||
|
--greyBackgroundColor: var(--greyBackgroundColor),
|
||||||
|
|
||||||
--menuBackgroundColor: var(--menuBackgroundColor),
|
--menuBackgroundColor: var(--menuBackgroundColor),
|
||||||
--menuForegroundColor: var(--menuForegroundColor),
|
--menuForegroundColor: var(--menuForegroundColor),
|
||||||
|
|
|
@ -94,6 +94,8 @@ export class AbuseMessageModel extends Model<AbuseMessageModel> {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
|
||||||
byModerator: this.byModerator,
|
byModerator: this.byModerator,
|
||||||
message: this.message,
|
message: this.message,
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ export interface AbuseMessage {
|
||||||
id: number
|
id: number
|
||||||
message: string
|
message: string
|
||||||
byModerator: boolean
|
byModerator: boolean
|
||||||
|
createdAt: Date | string
|
||||||
|
|
||||||
account: AccountSummary
|
account: AccountSummary
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue