Add migrations for abuse messages
This commit is contained in:
parent
594d3e48d8
commit
d573926e9b
|
@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
|
||||||
newFollow: this.i18n('You or your channel(s) has a new follower'),
|
newFollow: this.i18n('You or your channel(s) has a new follower'),
|
||||||
commentMention: this.i18n('Someone mentioned you in video comments'),
|
commentMention: this.i18n('Someone mentioned you in video comments'),
|
||||||
newInstanceFollower: this.i18n('Your instance has a new follower'),
|
newInstanceFollower: this.i18n('Your instance has a new follower'),
|
||||||
autoInstanceFollowing: this.i18n('Your instance auto followed another instance')
|
autoInstanceFollowing: this.i18n('Your instance auto followed another instance'),
|
||||||
|
abuseNewMessage: this.i18n('An abuse received a new message'),
|
||||||
|
abuseStateChange: this.i18n('One of your abuse has been accepted or rejected by moderators')
|
||||||
}
|
}
|
||||||
this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
|
this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
|
||||||
|
|
||||||
|
|
|
@ -3,19 +3,29 @@ import { LinkifierService } from './linkifier.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HtmlRendererService {
|
export class HtmlRendererService {
|
||||||
|
private sanitizeHtml: typeof import ('sanitize-html')
|
||||||
|
|
||||||
constructor (private linkifier: LinkifierService) {
|
constructor (private linkifier: LinkifierService) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async convertToBr (text: string) {
|
||||||
|
await this.loadSanitizeHtml()
|
||||||
|
|
||||||
|
const html = text.replace(/\r?\n/g, '<br />')
|
||||||
|
|
||||||
|
return this.sanitizeHtml(html, {
|
||||||
|
allowedTags: [ 'br' ]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async toSafeHtml (text: string) {
|
async toSafeHtml (text: string) {
|
||||||
// FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
|
await this.loadSanitizeHtml()
|
||||||
const sanitizeHtml: typeof import ('sanitize-html') = (await import('sanitize-html') as any).default
|
|
||||||
|
|
||||||
// Convert possible markdown to html
|
// Convert possible markdown to html
|
||||||
const html = this.linkifier.linkify(text)
|
const html = this.linkifier.linkify(text)
|
||||||
|
|
||||||
return sanitizeHtml(html, {
|
return this.sanitizeHtml(html, {
|
||||||
allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
|
allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
|
||||||
allowedSchemes: [ 'http', 'https' ],
|
allowedSchemes: [ 'http', 'https' ],
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
|
@ -37,4 +47,9 @@ export class HtmlRendererService {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async loadSanitizeHtml () {
|
||||||
|
// FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
|
||||||
|
this.sanitizeHtml = (await import('sanitize-html') as any).default
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
<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 i18n style="width: 80px;">Messages</th>
|
||||||
|
<th i18n *ngIf="isAdminView()" style="width: 100px;">Internal note</th>
|
||||||
<th style="width: 150px;"></th>
|
<th style="width: 150px;"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -144,13 +145,11 @@
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
<td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
|
<td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
|
||||||
|
|
||||||
<td class="c-hand abuse-states" [pRowToggler]="abuse">
|
<td class="c-hand abuse-states" [pRowToggler]="abuse">
|
||||||
<span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
|
<span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
|
||||||
<span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
|
<span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></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)">
|
<td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)">
|
||||||
|
@ -161,6 +160,10 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td *ngIf="isAdminView()" class="internal-note" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment">
|
||||||
|
{{ abuse.moderationComment }}
|
||||||
|
</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"
|
||||||
|
|
|
@ -278,7 +278,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
|
||||||
isDisplayed: abuse => this.isLocalAbuse(abuse)
|
isDisplayed: abuse => this.isLocalAbuse(abuse)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Update note'),
|
label: this.i18n('Update internal note'),
|
||||||
handler: abuse => this.openModerationCommentModal(abuse),
|
handler: abuse => this.openModerationCommentModal(abuse),
|
||||||
isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
|
isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="messages" #messagesBlock>
|
<div class="messages">
|
||||||
<div
|
<div
|
||||||
*ngFor="let message of abuseMessages"
|
*ngFor="let message of abuseMessages"
|
||||||
class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }"
|
class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }"
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
<div class="author">{{ message.account.name }}</div>
|
<div class="author">{{ message.account.name }}</div>
|
||||||
|
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<div class="content">{{ message.message }}</div>
|
<div class="content" [innerHTML]="message.messageHtml"></div>
|
||||||
<div class="date">{{ message.createdAt | date }}</div>
|
<div class="date">{{ message.createdAt | date }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,6 +20,7 @@ textarea {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-messages {
|
.no-messages {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
|
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
|
||||||
import { AuthService, Notifier } from '@app/core'
|
import { AuthService, Notifier, HtmlRendererService } from '@app/core'
|
||||||
import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
||||||
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'
|
||||||
|
@ -14,13 +14,12 @@ import { AbuseService } from '../shared-moderation'
|
||||||
})
|
})
|
||||||
export class AbuseMessageModalComponent extends FormReactive implements OnInit {
|
export class AbuseMessageModalComponent extends FormReactive implements OnInit {
|
||||||
@ViewChild('modal', { static: true }) modal: NgbModal
|
@ViewChild('modal', { static: true }) modal: NgbModal
|
||||||
@ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef
|
|
||||||
|
|
||||||
@Input() isAdminView: boolean
|
@Input() isAdminView: boolean
|
||||||
|
|
||||||
@Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>()
|
@Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>()
|
||||||
|
|
||||||
abuseMessages: AbuseMessage[] = []
|
abuseMessages: (AbuseMessage & { messageHtml: string })[] = []
|
||||||
textareaMessage: string
|
textareaMessage: string
|
||||||
sendingMessage = false
|
sendingMessage = false
|
||||||
noResults = false
|
noResults = false
|
||||||
|
@ -33,6 +32,7 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
|
||||||
private abuseValidatorsService: AbuseValidatorsService,
|
private abuseValidatorsService: AbuseValidatorsService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private i18n: I18n,
|
private i18n: I18n,
|
||||||
|
private htmlRenderer: HtmlRendererService,
|
||||||
private auth: AuthService,
|
private auth: AuthService,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private abuseService: AbuseService
|
private abuseService: AbuseService
|
||||||
|
@ -108,15 +108,21 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
|
||||||
private loadMessages () {
|
private loadMessages () {
|
||||||
this.abuseService.listAbuseMessages(this.abuse)
|
this.abuseService.listAbuseMessages(this.abuse)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
res => {
|
async res => {
|
||||||
this.abuseMessages = res.data
|
this.abuseMessages = []
|
||||||
|
|
||||||
|
for (const m of res.data) {
|
||||||
|
this.abuseMessages.push(Object.assign(m, {
|
||||||
|
messageHtml: await this.htmlRenderer.convertToBr(m.message)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
this.noResults = this.abuseMessages.length === 0
|
this.noResults = this.abuseMessages.length === 0
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.messagesBlock) return
|
// Don't use ViewChild: it is not supported inside a ng-template
|
||||||
|
const messagesBlock = document.querySelector('.messages')
|
||||||
const element = this.messagesBlock.nativeElement as HTMLElement
|
messagesBlock.scroll(0, messagesBlock.scrollHeight)
|
||||||
element.scrollIntoView({ block: 'end', inline: 'nearest' })
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
|
import {
|
||||||
|
AbuseState,
|
||||||
|
ActorInfo,
|
||||||
|
FollowState,
|
||||||
|
UserNotification as UserNotificationServer,
|
||||||
|
UserNotificationType,
|
||||||
|
VideoInfo,
|
||||||
|
UserRight
|
||||||
|
} from '@shared/models'
|
||||||
import { Actor } from '../account/actor.model'
|
import { Actor } from '../account/actor.model'
|
||||||
import { ActorInfo, Avatar, FollowState, UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '@shared/models'
|
import { AuthUser } from '@app/core'
|
||||||
|
|
||||||
export class UserNotification implements UserNotificationServer {
|
export class UserNotification implements UserNotificationServer {
|
||||||
id: number
|
id: number
|
||||||
|
@ -27,6 +36,7 @@ export class UserNotification implements UserNotificationServer {
|
||||||
|
|
||||||
abuse?: {
|
abuse?: {
|
||||||
id: number
|
id: number
|
||||||
|
state: AbuseState
|
||||||
|
|
||||||
video?: VideoInfo
|
video?: VideoInfo
|
||||||
|
|
||||||
|
@ -69,13 +79,14 @@ export class UserNotification implements UserNotificationServer {
|
||||||
videoUrl?: string
|
videoUrl?: string
|
||||||
commentUrl?: any[]
|
commentUrl?: any[]
|
||||||
abuseUrl?: string
|
abuseUrl?: string
|
||||||
|
abuseQueryParams?: { [id: string]: string } = {}
|
||||||
videoAutoBlacklistUrl?: string
|
videoAutoBlacklistUrl?: string
|
||||||
accountUrl?: string
|
accountUrl?: string
|
||||||
videoImportIdentifier?: string
|
videoImportIdentifier?: string
|
||||||
videoImportUrl?: string
|
videoImportUrl?: string
|
||||||
instanceFollowUrl?: string
|
instanceFollowUrl?: string
|
||||||
|
|
||||||
constructor (hash: UserNotificationServer) {
|
constructor (hash: UserNotificationServer, user: AuthUser) {
|
||||||
this.id = hash.id
|
this.id = hash.id
|
||||||
this.type = hash.type
|
this.type = hash.type
|
||||||
this.read = hash.read
|
this.read = hash.read
|
||||||
|
@ -122,12 +133,25 @@ export class UserNotification implements UserNotificationServer {
|
||||||
|
|
||||||
case UserNotificationType.NEW_ABUSE_FOR_MODERATORS:
|
case UserNotificationType.NEW_ABUSE_FOR_MODERATORS:
|
||||||
this.abuseUrl = '/admin/moderation/abuses/list'
|
this.abuseUrl = '/admin/moderation/abuses/list'
|
||||||
|
this.abuseQueryParams.search = '#' + this.abuse.id
|
||||||
|
|
||||||
if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video)
|
if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video)
|
||||||
else if (this.abuse.comment) this.commentUrl = this.buildCommentUrl(this.abuse.comment)
|
else if (this.abuse.comment) this.commentUrl = this.buildCommentUrl(this.abuse.comment)
|
||||||
else if (this.abuse.account) this.accountUrl = this.buildAccountUrl(this.abuse.account)
|
else if (this.abuse.account) this.accountUrl = this.buildAccountUrl(this.abuse.account)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case UserNotificationType.ABUSE_STATE_CHANGE:
|
||||||
|
this.abuseUrl = '/my-account/abuses'
|
||||||
|
this.abuseQueryParams.search = '#' + this.abuse.id
|
||||||
|
break
|
||||||
|
|
||||||
|
case UserNotificationType.ABUSE_NEW_MESSAGE:
|
||||||
|
this.abuseUrl = user.hasRight(UserRight.MANAGE_ABUSES)
|
||||||
|
? '/admin/moderation/abuses/list'
|
||||||
|
: '/my-account/abuses'
|
||||||
|
this.abuseQueryParams.search = '#' + this.abuse.id
|
||||||
|
break
|
||||||
|
|
||||||
case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
|
case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
|
||||||
this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
|
this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
|
||||||
// Backward compatibility where we did not assign videoBlacklist to this type of notification before
|
// Backward compatibility where we did not assign videoBlacklist to this type of notification before
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { catchError, map, tap } from 'rxjs/operators'
|
import { catchError, map, tap } 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 { ComponentPaginationLight, RestExtractor, RestService, User, UserNotificationSocket } from '@app/core'
|
import { ComponentPaginationLight, RestExtractor, RestService, User, UserNotificationSocket, AuthService } from '@app/core'
|
||||||
import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models'
|
import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models'
|
||||||
import { environment } from '../../../../environments/environment'
|
import { environment } from '../../../../environments/environment'
|
||||||
import { UserNotification } from './user-notification.model'
|
import { UserNotification } from './user-notification.model'
|
||||||
|
@ -14,6 +14,7 @@ export class UserNotificationService {
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private authHttp: HttpClient,
|
private authHttp: HttpClient,
|
||||||
|
private auth: AuthService,
|
||||||
private restExtractor: RestExtractor,
|
private restExtractor: RestExtractor,
|
||||||
private restService: RestService,
|
private restService: RestService,
|
||||||
private userNotificationSocket: UserNotificationSocket
|
private userNotificationSocket: UserNotificationSocket
|
||||||
|
@ -84,6 +85,6 @@ export class UserNotificationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatNotification (notification: UserNotificationServer) {
|
private formatNotification (notification: UserNotificationServer) {
|
||||||
return new UserNotification(notification)
|
return new UserNotification(notification, this.auth.getUser())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,20 +46,38 @@
|
||||||
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
|
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
|
||||||
|
|
||||||
<div class="message" *ngIf="notification.videoUrl" i18n>
|
<div class="message" *ngIf="notification.videoUrl" i18n>
|
||||||
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.abuse.video.name }}</a>
|
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.abuse.video.name }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message" *ngIf="notification.commentUrl" i18n>
|
<div class="message" *ngIf="notification.commentUrl" i18n>
|
||||||
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new comment abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.abuse.comment.video.name }}</a>
|
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">A new comment abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.abuse.comment.video.name }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message" *ngIf="notification.accountUrl" i18n>
|
<div class="message" *ngIf="notification.accountUrl" i18n>
|
||||||
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new account abuse</a> has been created on account <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.abuse.account.displayName }}</a>
|
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">A new account abuse</a> has been created on account <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.abuse.account.displayName }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Deleted entity associated to the abuse -->
|
<!-- Deleted entity associated to the abuse -->
|
||||||
<div class="message" *ngIf="!notification.videoUrl && !notification.commentUrl && !notification.accountUrl" i18n>
|
<div class="message" *ngIf="!notification.videoUrl && !notification.commentUrl && !notification.accountUrl" i18n>
|
||||||
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new abuse</a> has been created
|
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">A new abuse</a> has been created
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="UserNotificationType.ABUSE_STATE_CHANGE">
|
||||||
|
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
|
||||||
|
|
||||||
|
<div class="message" i18n>
|
||||||
|
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">Your abuse {{ notification.abuse.id }}</a> has been
|
||||||
|
<ng-container *ngIf="isAccepted(notification)">accepted</ng-container>
|
||||||
|
<ng-container *ngIf="!isAccepted(notification)">rejected</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="UserNotificationType.ABUSE_NEW_MESSAGE">
|
||||||
|
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
|
||||||
|
|
||||||
|
<div class="message" i18n>
|
||||||
|
<a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">Abuse {{ notification.abuse.id }}</a> has a new message
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||||
import { ComponentPagination, hasMoreItems, Notifier } from '@app/core'
|
import { ComponentPagination, hasMoreItems, Notifier } from '@app/core'
|
||||||
import { UserNotificationType } from '@shared/models'
|
import { UserNotificationType, AbuseState } from '@shared/models'
|
||||||
import { UserNotification } from './user-notification.model'
|
import { UserNotification } from './user-notification.model'
|
||||||
import { UserNotificationService } from './user-notification.service'
|
import { UserNotificationService } from './user-notification.service'
|
||||||
|
|
||||||
|
@ -116,4 +116,8 @@ export class UserNotificationsComponent implements OnInit {
|
||||||
this.sortField = column
|
this.sortField = column
|
||||||
this.loadNotifications(true)
|
this.loadNotifications(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAccepted (notification: UserNotification) {
|
||||||
|
return notification.abuse.state === AbuseState.ACCEPTED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ cp "./tsconfig.json" "./dist"
|
||||||
|
|
||||||
npm run tsc -- --incremental --sourceMap
|
npm run tsc -- --incremental --sourceMap
|
||||||
cp -r ./server/static ./server/assets ./dist/server
|
cp -r ./server/static ./server/assets ./dist/server
|
||||||
|
cp -r "./server/lib/emails" "./dist/server/lib"
|
||||||
|
|
||||||
NODE_ENV=test node node_modules/.bin/concurrently -k \
|
NODE_ENV=test node node_modules/.bin/concurrently -k \
|
||||||
"node_modules/.bin/nodemon --delay 1 --watch ./dist dist/server" \
|
"node_modules/.bin/nodemon --delay 1 --watch ./dist dist/server" \
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 520
|
const LAST_MIGRATION_VERSION = 525
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
sequelize: Sequelize.Sequelize
|
||||||
|
}): Promise<void> {
|
||||||
|
await utils.sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "abuseMessage" (
|
||||||
|
"id" serial,
|
||||||
|
"message" text NOT NULL,
|
||||||
|
"byModerator" boolean NOT NULL,
|
||||||
|
"accountId" integer REFERENCES "account" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
"abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
"createdAt" timestamp WITH time zone NOT NULL,
|
||||||
|
"updatedAt" timestamp WITH time zone NOT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
const notificationSettingColumns = [ 'abuseStateChange', 'abuseNewMessage' ]
|
||||||
|
|
||||||
|
for (const column of notificationSettingColumns) {
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
defaultValue: null,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
await utils.queryInterface.addColumn('userNotificationSetting', column, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const query = 'UPDATE "userNotificationSetting" SET "abuseStateChange" = 3, "abuseNewMessage" = 3'
|
||||||
|
await utils.sequelize.query(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const column of notificationSettingColumns) {
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
defaultValue: null,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import { isTestInstance, root } from '../helpers/core-utils'
|
||||||
import { bunyanLogger, logger } from '../helpers/logger'
|
import { bunyanLogger, logger } from '../helpers/logger'
|
||||||
import { CONFIG, isEmailEnabled } from '../initializers/config'
|
import { CONFIG, isEmailEnabled } from '../initializers/config'
|
||||||
import { WEBSERVER } from '../initializers/constants'
|
import { WEBSERVER } from '../initializers/constants'
|
||||||
import { MAbuseFull, MAbuseMessage, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
|
import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
|
||||||
import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
|
import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
|
||||||
import { JobQueue } from './job-queue'
|
import { JobQueue } from './job-queue'
|
||||||
|
|
||||||
|
@ -362,9 +362,11 @@ class Emailer {
|
||||||
? 'Report #' + abuse.id + ' has been accepted'
|
? 'Report #' + abuse.id + ' has been accepted'
|
||||||
: 'Report #' + abuse.id + ' has been rejected'
|
: 'Report #' + abuse.id + ' has been rejected'
|
||||||
|
|
||||||
|
const abuseUrl = WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
|
||||||
|
|
||||||
const action = {
|
const action = {
|
||||||
text,
|
text,
|
||||||
url: WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
|
url: abuseUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailPayload: EmailPayload = {
|
const emailPayload: EmailPayload = {
|
||||||
|
@ -374,6 +376,7 @@ class Emailer {
|
||||||
locals: {
|
locals: {
|
||||||
action,
|
action,
|
||||||
abuseId: abuse.id,
|
abuseId: abuse.id,
|
||||||
|
abuseUrl,
|
||||||
isAccepted: abuse.state === AbuseState.ACCEPTED
|
isAccepted: abuse.state === AbuseState.ACCEPTED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -381,15 +384,24 @@ class Emailer {
|
||||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||||
}
|
}
|
||||||
|
|
||||||
addAbuseNewMessageNotification (to: string[], options: { target: 'moderator' | 'reporter', abuse: MAbuseFull, message: MAbuseMessage }) {
|
addAbuseNewMessageNotification (
|
||||||
const { abuse, target, message } = options
|
to: string[],
|
||||||
|
options: {
|
||||||
|
target: 'moderator' | 'reporter'
|
||||||
|
abuse: MAbuseFull
|
||||||
|
message: MAbuseMessage
|
||||||
|
accountMessage: MAccountDefault
|
||||||
|
}) {
|
||||||
|
const { abuse, target, message, accountMessage } = options
|
||||||
|
|
||||||
|
const text = 'New message on report #' + abuse.id
|
||||||
|
const abuseUrl = target === 'moderator'
|
||||||
|
? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
|
||||||
|
: WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
|
||||||
|
|
||||||
const text = 'New message on abuse #' + abuse.id
|
|
||||||
const action = {
|
const action = {
|
||||||
text,
|
text,
|
||||||
url: target === 'moderator'
|
url: abuseUrl
|
||||||
? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
|
|
||||||
: WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailPayload: EmailPayload = {
|
const emailPayload: EmailPayload = {
|
||||||
|
@ -397,7 +409,9 @@ class Emailer {
|
||||||
to,
|
to,
|
||||||
subject: text,
|
subject: text,
|
||||||
locals: {
|
locals: {
|
||||||
|
abuseId: abuse.id,
|
||||||
abuseUrl: action.url,
|
abuseUrl: action.url,
|
||||||
|
messageAccountName: accountMessage.getDisplayName(),
|
||||||
messageText: message.message,
|
messageText: message.message,
|
||||||
action
|
action
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ extends ../common/greetings
|
||||||
include ../common/mixins.pug
|
include ../common/mixins.pug
|
||||||
|
|
||||||
block title
|
block title
|
||||||
| New abuse message
|
| New message on abuse report
|
||||||
|
|
||||||
block content
|
block content
|
||||||
p
|
p
|
||||||
| A new message was created on #[a(href=WEBSERVER.URL) abuse ##{abuseId} on #{WEBSERVER.HOST}]
|
| A new message by #{messageAccountName} was posted on #[a(href=abuseUrl) abuse report ##{abuseId}] on #{WEBSERVER.HOST}
|
||||||
blockquote #{messageText}
|
blockquote #{messageText}
|
||||||
br(style="display: none;")
|
br(style="display: none;")
|
||||||
|
|
|
@ -2,8 +2,8 @@ extends ../common/greetings
|
||||||
include ../common/mixins.pug
|
include ../common/mixins.pug
|
||||||
|
|
||||||
block title
|
block title
|
||||||
| Abuse state changed
|
| Abuse report state changed
|
||||||
|
|
||||||
block content
|
block content
|
||||||
p
|
p
|
||||||
| #[a(href=abuseUrl) Your abuse ##{abuseId} on #{WEBSERVER.HOST}] has been #{isAccepted ? 'accepted' : 'rejected'}
|
| #[a(href=abuseUrl) Your abuse report ##{abuseId}] on #{WEBSERVER.HOST} has been #{isAccepted ? 'accepted' : 'rejected'}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../type
|
||||||
import { isBlockedByServerOrAccount } from './blocklist'
|
import { isBlockedByServerOrAccount } from './blocklist'
|
||||||
import { Emailer } from './emailer'
|
import { Emailer } from './emailer'
|
||||||
import { PeerTubeSocket } from './peertube-socket'
|
import { PeerTubeSocket } from './peertube-socket'
|
||||||
|
import { AccountModel } from '@server/models/account/account'
|
||||||
|
|
||||||
class Notifier {
|
class Notifier {
|
||||||
|
|
||||||
|
@ -137,7 +138,7 @@ class Notifier {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyOnAbuseMessage (abuse: MAbuseFull, message: AbuseMessageModel): void {
|
notifyOnAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage): void {
|
||||||
this.notifyOfNewAbuseMessage(abuse, message)
|
this.notifyOfNewAbuseMessage(abuse, message)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
logger.error('Cannot notify on new abuse %d message.', abuse.id, { err })
|
logger.error('Cannot notify on new abuse %d message.', abuse.id, { err })
|
||||||
|
@ -436,6 +437,8 @@ class Notifier {
|
||||||
const url = this.getAbuseUrl(abuse)
|
const url = this.getAbuseUrl(abuse)
|
||||||
logger.info('Notifying reporter and moderators of new abuse message on %s.', url)
|
logger.info('Notifying reporter and moderators of new abuse message on %s.', url)
|
||||||
|
|
||||||
|
const accountMessage = await AccountModel.load(message.accountId)
|
||||||
|
|
||||||
function settingGetter (user: MUserWithNotificationSetting) {
|
function settingGetter (user: MUserWithNotificationSetting) {
|
||||||
return user.NotificationSetting.abuseNewMessage
|
return user.NotificationSetting.abuseNewMessage
|
||||||
}
|
}
|
||||||
|
@ -452,11 +455,11 @@ class Notifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
function emailSenderReporter (emails: string[]) {
|
function emailSenderReporter (emails: string[]) {
|
||||||
return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message })
|
return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message, accountMessage })
|
||||||
}
|
}
|
||||||
|
|
||||||
function emailSenderModerators (emails: string[]) {
|
function emailSenderModerators (emails: string[]) {
|
||||||
return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message })
|
return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message, accountMessage })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildReporterOptions () {
|
async function buildReporterOptions () {
|
||||||
|
|
Loading…
Reference in New Issue