diff options
18 files changed, 189 insertions, 43 deletions
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts index 8562e564b..89a04c078 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts | |||
@@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit { | |||
42 | newFollow: this.i18n('You or your channel(s) has a new follower'), | 42 | newFollow: this.i18n('You or your channel(s) has a new follower'), |
43 | commentMention: this.i18n('Someone mentioned you in video comments'), | 43 | commentMention: this.i18n('Someone mentioned you in video comments'), |
44 | newInstanceFollower: this.i18n('Your instance has a new follower'), | 44 | newInstanceFollower: this.i18n('Your instance has a new follower'), |
45 | autoInstanceFollowing: this.i18n('Your instance auto followed another instance') | 45 | autoInstanceFollowing: this.i18n('Your instance auto followed another instance'), |
46 | abuseNewMessage: this.i18n('An abuse received a new message'), | ||
47 | abuseStateChange: this.i18n('One of your abuse has been accepted or rejected by moderators') | ||
46 | } | 48 | } |
47 | this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] | 49 | this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] |
48 | 50 | ||
diff --git a/client/src/app/core/renderer/html-renderer.service.ts b/client/src/app/core/renderer/html-renderer.service.ts index f0527c759..302d92ed9 100644 --- a/client/src/app/core/renderer/html-renderer.service.ts +++ b/client/src/app/core/renderer/html-renderer.service.ts | |||
@@ -3,19 +3,29 @@ import { LinkifierService } from './linkifier.service' | |||
3 | 3 | ||
4 | @Injectable() | 4 | @Injectable() |
5 | export class HtmlRendererService { | 5 | export class HtmlRendererService { |
6 | private sanitizeHtml: typeof import ('sanitize-html') | ||
6 | 7 | ||
7 | constructor (private linkifier: LinkifierService) { | 8 | constructor (private linkifier: LinkifierService) { |
8 | 9 | ||
9 | } | 10 | } |
10 | 11 | ||
12 | async convertToBr (text: string) { | ||
13 | await this.loadSanitizeHtml() | ||
14 | |||
15 | const html = text.replace(/\r?\n/g, '<br />') | ||
16 | |||
17 | return this.sanitizeHtml(html, { | ||
18 | allowedTags: [ 'br' ] | ||
19 | }) | ||
20 | } | ||
21 | |||
11 | async toSafeHtml (text: string) { | 22 | async toSafeHtml (text: string) { |
12 | // FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function | 23 | await this.loadSanitizeHtml() |
13 | const sanitizeHtml: typeof import ('sanitize-html') = (await import('sanitize-html') as any).default | ||
14 | 24 | ||
15 | // Convert possible markdown to html | 25 | // Convert possible markdown to html |
16 | const html = this.linkifier.linkify(text) | 26 | const html = this.linkifier.linkify(text) |
17 | 27 | ||
18 | return sanitizeHtml(html, { | 28 | return this.sanitizeHtml(html, { |
19 | allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ], | 29 | allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ], |
20 | allowedSchemes: [ 'http', 'https' ], | 30 | allowedSchemes: [ 'http', 'https' ], |
21 | allowedAttributes: { | 31 | allowedAttributes: { |
@@ -37,4 +47,9 @@ export class HtmlRendererService { | |||
37 | } | 47 | } |
38 | }) | 48 | }) |
39 | } | 49 | } |
50 | |||
51 | private async loadSanitizeHtml () { | ||
52 | // FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function | ||
53 | this.sanitizeHtml = (await import('sanitize-html') as any).default | ||
54 | } | ||
40 | } | 55 | } |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html index 17b3742d6..d90b93fff 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html | |||
@@ -42,6 +42,7 @@ | |||
42 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> | 42 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> |
43 | <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> | 43 | <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> |
44 | <th i18n style="width: 80px;">Messages</th> | 44 | <th i18n style="width: 80px;">Messages</th> |
45 | <th i18n *ngIf="isAdminView()" style="width: 100px;">Internal note</th> | ||
45 | <th style="width: 150px;"></th> | 46 | <th style="width: 150px;"></th> |
46 | </tr> | 47 | </tr> |
47 | </ng-template> | 48 | </ng-template> |
@@ -144,13 +145,11 @@ | |||
144 | 145 | ||
145 | </ng-container> | 146 | </ng-container> |
146 | 147 | ||
147 | |||
148 | <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td> | 148 | <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td> |
149 | 149 | ||
150 | <td class="c-hand abuse-states" [pRowToggler]="abuse"> | 150 | <td class="c-hand abuse-states" [pRowToggler]="abuse"> |
151 | <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span> | 151 | <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span> |
152 | <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span> | 152 | <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span> |
153 | <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span> | ||
154 | </td> | 153 | </td> |
155 | 154 | ||
156 | <td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)"> | 155 | <td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)"> |
@@ -161,6 +160,10 @@ | |||
161 | </ng-container> | 160 | </ng-container> |
162 | </td> | 161 | </td> |
163 | 162 | ||
163 | <td *ngIf="isAdminView()" class="internal-note" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment"> | ||
164 | {{ abuse.moderationComment }} | ||
165 | </td> | ||
166 | |||
164 | <td class="action-cell"> | 167 | <td class="action-cell"> |
165 | <my-action-dropdown | 168 | <my-action-dropdown |
166 | [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" | 169 | [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body" |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index 1d17c9ec9..21d2ea47d 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts | |||
@@ -278,7 +278,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV | |||
278 | isDisplayed: abuse => this.isLocalAbuse(abuse) | 278 | isDisplayed: abuse => this.isLocalAbuse(abuse) |
279 | }, | 279 | }, |
280 | { | 280 | { |
281 | label: this.i18n('Update note'), | 281 | label: this.i18n('Update internal note'), |
282 | handler: abuse => this.openModerationCommentModal(abuse), | 282 | handler: abuse => this.openModerationCommentModal(abuse), |
283 | isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment | 283 | isDisplayed: abuse => this.isAdminView() && !!abuse.moderationComment |
284 | }, | 284 | }, |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html index cb965b71d..17e9ce4cf 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.html | |||
@@ -9,7 +9,7 @@ | |||
9 | </div> | 9 | </div> |
10 | 10 | ||
11 | <div class="modal-body"> | 11 | <div class="modal-body"> |
12 | <div class="messages" #messagesBlock> | 12 | <div class="messages"> |
13 | <div | 13 | <div |
14 | *ngFor="let message of abuseMessages" | 14 | *ngFor="let message of abuseMessages" |
15 | class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }" | 15 | class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }" |
@@ -18,7 +18,7 @@ | |||
18 | <div class="author">{{ message.account.name }}</div> | 18 | <div class="author">{{ message.account.name }}</div> |
19 | 19 | ||
20 | <div class="bubble"> | 20 | <div class="bubble"> |
21 | <div class="content">{{ message.message }}</div> | 21 | <div class="content" [innerHTML]="message.messageHtml"></div> |
22 | <div class="date">{{ message.createdAt | date }}</div> | 22 | <div class="date">{{ message.createdAt | date }}</div> |
23 | </div> | 23 | </div> |
24 | </div> | 24 | </div> |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss index 4dd025fc4..4163722dd 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss +++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.scss | |||
@@ -20,6 +20,7 @@ textarea { | |||
20 | display: flex; | 20 | display: flex; |
21 | flex-direction: column; | 21 | flex-direction: column; |
22 | overflow-y: scroll; | 22 | overflow-y: scroll; |
23 | max-height: 50vh; | ||
23 | } | 24 | } |
24 | 25 | ||
25 | .no-messages { | 26 | .no-messages { |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts index 03f5ad735..6686d91f4 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { AuthService, Notifier } from '@app/core' | 2 | import { AuthService, Notifier, HtmlRendererService } from '@app/core' |
3 | import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 3 | import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
@@ -14,13 +14,12 @@ import { AbuseService } from '../shared-moderation' | |||
14 | }) | 14 | }) |
15 | export class AbuseMessageModalComponent extends FormReactive implements OnInit { | 15 | export class AbuseMessageModalComponent extends FormReactive implements OnInit { |
16 | @ViewChild('modal', { static: true }) modal: NgbModal | 16 | @ViewChild('modal', { static: true }) modal: NgbModal |
17 | @ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef | ||
18 | 17 | ||
19 | @Input() isAdminView: boolean | 18 | @Input() isAdminView: boolean |
20 | 19 | ||
21 | @Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>() | 20 | @Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>() |
22 | 21 | ||
23 | abuseMessages: AbuseMessage[] = [] | 22 | abuseMessages: (AbuseMessage & { messageHtml: string })[] = [] |
24 | textareaMessage: string | 23 | textareaMessage: string |
25 | sendingMessage = false | 24 | sendingMessage = false |
26 | noResults = false | 25 | noResults = false |
@@ -33,6 +32,7 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit { | |||
33 | private abuseValidatorsService: AbuseValidatorsService, | 32 | private abuseValidatorsService: AbuseValidatorsService, |
34 | private modalService: NgbModal, | 33 | private modalService: NgbModal, |
35 | private i18n: I18n, | 34 | private i18n: I18n, |
35 | private htmlRenderer: HtmlRendererService, | ||
36 | private auth: AuthService, | 36 | private auth: AuthService, |
37 | private notifier: Notifier, | 37 | private notifier: Notifier, |
38 | private abuseService: AbuseService | 38 | private abuseService: AbuseService |
@@ -108,15 +108,21 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit { | |||
108 | private loadMessages () { | 108 | private loadMessages () { |
109 | this.abuseService.listAbuseMessages(this.abuse) | 109 | this.abuseService.listAbuseMessages(this.abuse) |
110 | .subscribe( | 110 | .subscribe( |
111 | res => { | 111 | async res => { |
112 | this.abuseMessages = res.data | 112 | this.abuseMessages = [] |
113 | |||
114 | for (const m of res.data) { | ||
115 | this.abuseMessages.push(Object.assign(m, { | ||
116 | messageHtml: await this.htmlRenderer.convertToBr(m.message) | ||
117 | })) | ||
118 | } | ||
119 | |||
113 | this.noResults = this.abuseMessages.length === 0 | 120 | this.noResults = this.abuseMessages.length === 0 |
114 | 121 | ||
115 | setTimeout(() => { | 122 | setTimeout(() => { |
116 | if (!this.messagesBlock) return | 123 | // Don't use ViewChild: it is not supported inside a ng-template |
117 | 124 | const messagesBlock = document.querySelector('.messages') | |
118 | const element = this.messagesBlock.nativeElement as HTMLElement | 125 | messagesBlock.scroll(0, messagesBlock.scrollHeight) |
119 | element.scrollIntoView({ block: 'end', inline: 'nearest' }) | ||
120 | }) | 126 | }) |
121 | }, | 127 | }, |
122 | 128 | ||
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts index 61b48a806..a068daaac 100644 --- a/client/src/app/shared/shared-main/users/user-notification.model.ts +++ b/client/src/app/shared/shared-main/users/user-notification.model.ts | |||
@@ -1,5 +1,14 @@ | |||
1 | import { | ||
2 | AbuseState, | ||
3 | ActorInfo, | ||
4 | FollowState, | ||
5 | UserNotification as UserNotificationServer, | ||
6 | UserNotificationType, | ||
7 | VideoInfo, | ||
8 | UserRight | ||
9 | } from '@shared/models' | ||
1 | import { Actor } from '../account/actor.model' | 10 | import { Actor } from '../account/actor.model' |
2 | import { ActorInfo, Avatar, FollowState, UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '@shared/models' | 11 | import { AuthUser } from '@app/core' |
3 | 12 | ||
4 | export class UserNotification implements UserNotificationServer { | 13 | export class UserNotification implements UserNotificationServer { |
5 | id: number | 14 | id: number |
@@ -27,6 +36,7 @@ export class UserNotification implements UserNotificationServer { | |||
27 | 36 | ||
28 | abuse?: { | 37 | abuse?: { |
29 | id: number | 38 | id: number |
39 | state: AbuseState | ||
30 | 40 | ||
31 | video?: VideoInfo | 41 | video?: VideoInfo |
32 | 42 | ||
@@ -69,13 +79,14 @@ export class UserNotification implements UserNotificationServer { | |||
69 | videoUrl?: string | 79 | videoUrl?: string |
70 | commentUrl?: any[] | 80 | commentUrl?: any[] |
71 | abuseUrl?: string | 81 | abuseUrl?: string |
82 | abuseQueryParams?: { [id: string]: string } = {} | ||
72 | videoAutoBlacklistUrl?: string | 83 | videoAutoBlacklistUrl?: string |
73 | accountUrl?: string | 84 | accountUrl?: string |
74 | videoImportIdentifier?: string | 85 | videoImportIdentifier?: string |
75 | videoImportUrl?: string | 86 | videoImportUrl?: string |
76 | instanceFollowUrl?: string | 87 | instanceFollowUrl?: string |
77 | 88 | ||
78 | constructor (hash: UserNotificationServer) { | 89 | constructor (hash: UserNotificationServer, user: AuthUser) { |
79 | this.id = hash.id | 90 | this.id = hash.id |
80 | this.type = hash.type | 91 | this.type = hash.type |
81 | this.read = hash.read | 92 | this.read = hash.read |
@@ -122,12 +133,25 @@ export class UserNotification implements UserNotificationServer { | |||
122 | 133 | ||
123 | case UserNotificationType.NEW_ABUSE_FOR_MODERATORS: | 134 | case UserNotificationType.NEW_ABUSE_FOR_MODERATORS: |
124 | this.abuseUrl = '/admin/moderation/abuses/list' | 135 | this.abuseUrl = '/admin/moderation/abuses/list' |
136 | this.abuseQueryParams.search = '#' + this.abuse.id | ||
125 | 137 | ||
126 | if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video) | 138 | if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video) |
127 | else if (this.abuse.comment) this.commentUrl = this.buildCommentUrl(this.abuse.comment) | 139 | else if (this.abuse.comment) this.commentUrl = this.buildCommentUrl(this.abuse.comment) |
128 | else if (this.abuse.account) this.accountUrl = this.buildAccountUrl(this.abuse.account) | 140 | else if (this.abuse.account) this.accountUrl = this.buildAccountUrl(this.abuse.account) |
129 | break | 141 | break |
130 | 142 | ||
143 | case UserNotificationType.ABUSE_STATE_CHANGE: | ||
144 | this.abuseUrl = '/my-account/abuses' | ||
145 | this.abuseQueryParams.search = '#' + this.abuse.id | ||
146 | break | ||
147 | |||
148 | case UserNotificationType.ABUSE_NEW_MESSAGE: | ||
149 | this.abuseUrl = user.hasRight(UserRight.MANAGE_ABUSES) | ||
150 | ? '/admin/moderation/abuses/list' | ||
151 | : '/my-account/abuses' | ||
152 | this.abuseQueryParams.search = '#' + this.abuse.id | ||
153 | break | ||
154 | |||
131 | case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS: | 155 | case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS: |
132 | this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list' | 156 | this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list' |
133 | // Backward compatibility where we did not assign videoBlacklist to this type of notification before | 157 | // Backward compatibility where we did not assign videoBlacklist to this type of notification before |
diff --git a/client/src/app/shared/shared-main/users/user-notification.service.ts b/client/src/app/shared/shared-main/users/user-notification.service.ts index ecc66ecdb..7b9dc34be 100644 --- a/client/src/app/shared/shared-main/users/user-notification.service.ts +++ b/client/src/app/shared/shared-main/users/user-notification.service.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { catchError, map, tap } from 'rxjs/operators' | 1 | import { catchError, map, tap } from 'rxjs/operators' |
2 | import { HttpClient, HttpParams } from '@angular/common/http' | 2 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { ComponentPaginationLight, RestExtractor, RestService, User, UserNotificationSocket } from '@app/core' | 4 | import { ComponentPaginationLight, RestExtractor, RestService, User, UserNotificationSocket, AuthService } from '@app/core' |
5 | import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models' | 5 | import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models' |
6 | import { environment } from '../../../../environments/environment' | 6 | import { environment } from '../../../../environments/environment' |
7 | import { UserNotification } from './user-notification.model' | 7 | import { UserNotification } from './user-notification.model' |
@@ -14,6 +14,7 @@ export class UserNotificationService { | |||
14 | 14 | ||
15 | constructor ( | 15 | constructor ( |
16 | private authHttp: HttpClient, | 16 | private authHttp: HttpClient, |
17 | private auth: AuthService, | ||
17 | private restExtractor: RestExtractor, | 18 | private restExtractor: RestExtractor, |
18 | private restService: RestService, | 19 | private restService: RestService, |
19 | private userNotificationSocket: UserNotificationSocket | 20 | private userNotificationSocket: UserNotificationSocket |
@@ -84,6 +85,6 @@ export class UserNotificationService { | |||
84 | } | 85 | } |
85 | 86 | ||
86 | private formatNotification (notification: UserNotificationServer) { | 87 | private formatNotification (notification: UserNotificationServer) { |
87 | return new UserNotification(notification) | 88 | return new UserNotification(notification, this.auth.getUser()) |
88 | } | 89 | } |
89 | } | 90 | } |
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html index 8127ae979..a56a0859b 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.html +++ b/client/src/app/shared/shared-main/users/user-notifications.component.html | |||
@@ -46,20 +46,38 @@ | |||
46 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> | 46 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> |
47 | 47 | ||
48 | <div class="message" *ngIf="notification.videoUrl" i18n> | 48 | <div class="message" *ngIf="notification.videoUrl" i18n> |
49 | <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> | 49 | <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> |
50 | </div> | 50 | </div> |
51 | 51 | ||
52 | <div class="message" *ngIf="notification.commentUrl" i18n> | 52 | <div class="message" *ngIf="notification.commentUrl" i18n> |
53 | <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> | 53 | <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> |
54 | </div> | 54 | </div> |
55 | 55 | ||
56 | <div class="message" *ngIf="notification.accountUrl" i18n> | 56 | <div class="message" *ngIf="notification.accountUrl" i18n> |
57 | <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> | 57 | <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> |
58 | </div> | 58 | </div> |
59 | 59 | ||
60 | <!-- Deleted entity associated to the abuse --> | 60 | <!-- Deleted entity associated to the abuse --> |
61 | <div class="message" *ngIf="!notification.videoUrl && !notification.commentUrl && !notification.accountUrl" i18n> | 61 | <div class="message" *ngIf="!notification.videoUrl && !notification.commentUrl && !notification.accountUrl" i18n> |
62 | <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new abuse</a> has been created | 62 | <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">A new abuse</a> has been created |
63 | </div> | ||
64 | </ng-container> | ||
65 | |||
66 | <ng-container *ngSwitchCase="UserNotificationType.ABUSE_STATE_CHANGE"> | ||
67 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> | ||
68 | |||
69 | <div class="message" i18n> | ||
70 | <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">Your abuse {{ notification.abuse.id }}</a> has been | ||
71 | <ng-container *ngIf="isAccepted(notification)">accepted</ng-container> | ||
72 | <ng-container *ngIf="!isAccepted(notification)">rejected</ng-container> | ||
73 | </div> | ||
74 | </ng-container> | ||
75 | |||
76 | <ng-container *ngSwitchCase="UserNotificationType.ABUSE_NEW_MESSAGE"> | ||
77 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> | ||
78 | |||
79 | <div class="message" i18n> | ||
80 | <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl" [queryParams]="notification.abuseQueryParams">Abuse {{ notification.abuse.id }}</a> has a new message | ||
63 | </div> | 81 | </div> |
64 | </ng-container> | 82 | </ng-container> |
65 | 83 | ||
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.ts b/client/src/app/shared/shared-main/users/user-notifications.component.ts index 7518dbdd0..387c49d94 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.ts +++ b/client/src/app/shared/shared-main/users/user-notifications.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Subject } from 'rxjs' | 1 | import { Subject } from 'rxjs' |
2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
3 | import { ComponentPagination, hasMoreItems, Notifier } from '@app/core' | 3 | import { ComponentPagination, hasMoreItems, Notifier } from '@app/core' |
4 | import { UserNotificationType } from '@shared/models' | 4 | import { UserNotificationType, AbuseState } from '@shared/models' |
5 | import { UserNotification } from './user-notification.model' | 5 | import { UserNotification } from './user-notification.model' |
6 | import { UserNotificationService } from './user-notification.service' | 6 | import { UserNotificationService } from './user-notification.service' |
7 | 7 | ||
@@ -116,4 +116,8 @@ export class UserNotificationsComponent implements OnInit { | |||
116 | this.sortField = column | 116 | this.sortField = column |
117 | this.loadNotifications(true) | 117 | this.loadNotifications(true) |
118 | } | 118 | } |
119 | |||
120 | isAccepted (notification: UserNotification) { | ||
121 | return notification.abuse.state === AbuseState.ACCEPTED | ||
122 | } | ||
119 | } | 123 | } |
diff --git a/scripts/dev/server.sh b/scripts/dev/server.sh index 680ca3d79..5aac470eb 100755 --- a/scripts/dev/server.sh +++ b/scripts/dev/server.sh | |||
@@ -21,6 +21,7 @@ cp "./tsconfig.json" "./dist" | |||
21 | 21 | ||
22 | npm run tsc -- --incremental --sourceMap | 22 | npm run tsc -- --incremental --sourceMap |
23 | cp -r ./server/static ./server/assets ./dist/server | 23 | cp -r ./server/static ./server/assets ./dist/server |
24 | cp -r "./server/lib/emails" "./dist/server/lib" | ||
24 | 25 | ||
25 | NODE_ENV=test node node_modules/.bin/concurrently -k \ | 26 | NODE_ENV=test node node_modules/.bin/concurrently -k \ |
26 | "node_modules/.bin/nodemon --delay 1 --watch ./dist dist/server" \ | 27 | "node_modules/.bin/nodemon --delay 1 --watch ./dist dist/server" \ |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index a40a22395..ca6c2a7ff 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -23,7 +23,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
23 | 23 | ||
24 | // --------------------------------------------------------------------------- | 24 | // --------------------------------------------------------------------------- |
25 | 25 | ||
26 | const LAST_MIGRATION_VERSION = 520 | 26 | const LAST_MIGRATION_VERSION = 525 |
27 | 27 | ||
28 | // --------------------------------------------------------------------------- | 28 | // --------------------------------------------------------------------------- |
29 | 29 | ||
diff --git a/server/initializers/migrations/0525-abuse-messages.ts b/server/initializers/migrations/0525-abuse-messages.ts new file mode 100644 index 000000000..c8fd7cbcf --- /dev/null +++ b/server/initializers/migrations/0525-abuse-messages.ts | |||
@@ -0,0 +1,54 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | await utils.sequelize.query(` | ||
9 | CREATE TABLE IF NOT EXISTS "abuseMessage" ( | ||
10 | "id" serial, | ||
11 | "message" text NOT NULL, | ||
12 | "byModerator" boolean NOT NULL, | ||
13 | "accountId" integer REFERENCES "account" ("id") ON DELETE SET NULL ON UPDATE CASCADE, | ||
14 | "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE, | ||
15 | "createdAt" timestamp WITH time zone NOT NULL, | ||
16 | "updatedAt" timestamp WITH time zone NOT NULL, | ||
17 | PRIMARY KEY ("id") | ||
18 | ); | ||
19 | `) | ||
20 | |||
21 | const notificationSettingColumns = [ 'abuseStateChange', 'abuseNewMessage' ] | ||
22 | |||
23 | for (const column of notificationSettingColumns) { | ||
24 | const data = { | ||
25 | type: Sequelize.INTEGER, | ||
26 | defaultValue: null, | ||
27 | allowNull: true | ||
28 | } | ||
29 | await utils.queryInterface.addColumn('userNotificationSetting', column, data) | ||
30 | } | ||
31 | |||
32 | { | ||
33 | const query = 'UPDATE "userNotificationSetting" SET "abuseStateChange" = 3, "abuseNewMessage" = 3' | ||
34 | await utils.sequelize.query(query) | ||
35 | } | ||
36 | |||
37 | for (const column of notificationSettingColumns) { | ||
38 | const data = { | ||
39 | type: Sequelize.INTEGER, | ||
40 | defaultValue: null, | ||
41 | allowNull: false | ||
42 | } | ||
43 | await utils.queryInterface.changeColumn('userNotificationSetting', column, data) | ||
44 | } | ||
45 | } | ||
46 | |||
47 | function down (options) { | ||
48 | throw new Error('Not implemented.') | ||
49 | } | ||
50 | |||
51 | export { | ||
52 | up, | ||
53 | down | ||
54 | } | ||
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 9c49aa2f6..25b0aaedd 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -11,7 +11,7 @@ import { isTestInstance, root } from '../helpers/core-utils' | |||
11 | import { bunyanLogger, logger } from '../helpers/logger' | 11 | import { bunyanLogger, logger } from '../helpers/logger' |
12 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 12 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
13 | import { WEBSERVER } from '../initializers/constants' | 13 | import { WEBSERVER } from '../initializers/constants' |
14 | import { MAbuseFull, MAbuseMessage, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' | 14 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' |
15 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' | 15 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' |
16 | import { JobQueue } from './job-queue' | 16 | import { JobQueue } from './job-queue' |
17 | 17 | ||
@@ -362,9 +362,11 @@ class Emailer { | |||
362 | ? 'Report #' + abuse.id + ' has been accepted' | 362 | ? 'Report #' + abuse.id + ' has been accepted' |
363 | : 'Report #' + abuse.id + ' has been rejected' | 363 | : 'Report #' + abuse.id + ' has been rejected' |
364 | 364 | ||
365 | const abuseUrl = WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id | ||
366 | |||
365 | const action = { | 367 | const action = { |
366 | text, | 368 | text, |
367 | url: WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id | 369 | url: abuseUrl |
368 | } | 370 | } |
369 | 371 | ||
370 | const emailPayload: EmailPayload = { | 372 | const emailPayload: EmailPayload = { |
@@ -374,6 +376,7 @@ class Emailer { | |||
374 | locals: { | 376 | locals: { |
375 | action, | 377 | action, |
376 | abuseId: abuse.id, | 378 | abuseId: abuse.id, |
379 | abuseUrl, | ||
377 | isAccepted: abuse.state === AbuseState.ACCEPTED | 380 | isAccepted: abuse.state === AbuseState.ACCEPTED |
378 | } | 381 | } |
379 | } | 382 | } |
@@ -381,15 +384,24 @@ class Emailer { | |||
381 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 384 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
382 | } | 385 | } |
383 | 386 | ||
384 | addAbuseNewMessageNotification (to: string[], options: { target: 'moderator' | 'reporter', abuse: MAbuseFull, message: MAbuseMessage }) { | 387 | addAbuseNewMessageNotification ( |
385 | const { abuse, target, message } = options | 388 | to: string[], |
389 | options: { | ||
390 | target: 'moderator' | 'reporter' | ||
391 | abuse: MAbuseFull | ||
392 | message: MAbuseMessage | ||
393 | accountMessage: MAccountDefault | ||
394 | }) { | ||
395 | const { abuse, target, message, accountMessage } = options | ||
396 | |||
397 | const text = 'New message on report #' + abuse.id | ||
398 | const abuseUrl = target === 'moderator' | ||
399 | ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id | ||
400 | : WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id | ||
386 | 401 | ||
387 | const text = 'New message on abuse #' + abuse.id | ||
388 | const action = { | 402 | const action = { |
389 | text, | 403 | text, |
390 | url: target === 'moderator' | 404 | url: abuseUrl |
391 | ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id | ||
392 | : WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id | ||
393 | } | 405 | } |
394 | 406 | ||
395 | const emailPayload: EmailPayload = { | 407 | const emailPayload: EmailPayload = { |
@@ -397,7 +409,9 @@ class Emailer { | |||
397 | to, | 409 | to, |
398 | subject: text, | 410 | subject: text, |
399 | locals: { | 411 | locals: { |
412 | abuseId: abuse.id, | ||
400 | abuseUrl: action.url, | 413 | abuseUrl: action.url, |
414 | messageAccountName: accountMessage.getDisplayName(), | ||
401 | messageText: message.message, | 415 | messageText: message.message, |
402 | action | 416 | action |
403 | } | 417 | } |
diff --git a/server/lib/emails/abuse-new-message/html.pug b/server/lib/emails/abuse-new-message/html.pug index a4180aba1..0841775d2 100644 --- a/server/lib/emails/abuse-new-message/html.pug +++ b/server/lib/emails/abuse-new-message/html.pug | |||
@@ -2,10 +2,10 @@ extends ../common/greetings | |||
2 | include ../common/mixins.pug | 2 | include ../common/mixins.pug |
3 | 3 | ||
4 | block title | 4 | block title |
5 | | New abuse message | 5 | | New message on abuse report |
6 | 6 | ||
7 | block content | 7 | block content |
8 | p | 8 | p |
9 | | A new message was created on #[a(href=WEBSERVER.URL) abuse ##{abuseId} on #{WEBSERVER.HOST}] | 9 | | A new message by #{messageAccountName} was posted on #[a(href=abuseUrl) abuse report ##{abuseId}] on #{WEBSERVER.HOST} |
10 | blockquote #{messageText} | 10 | blockquote #{messageText} |
11 | br(style="display: none;") | 11 | br(style="display: none;") |
diff --git a/server/lib/emails/abuse-state-change/html.pug b/server/lib/emails/abuse-state-change/html.pug index a94c8521d..ca89a2f05 100644 --- a/server/lib/emails/abuse-state-change/html.pug +++ b/server/lib/emails/abuse-state-change/html.pug | |||
@@ -2,8 +2,8 @@ extends ../common/greetings | |||
2 | include ../common/mixins.pug | 2 | include ../common/mixins.pug |
3 | 3 | ||
4 | block title | 4 | block title |
5 | | Abuse state changed | 5 | | Abuse report state changed |
6 | 6 | ||
7 | block content | 7 | block content |
8 | p | 8 | p |
9 | | #[a(href=abuseUrl) Your abuse ##{abuseId} on #{WEBSERVER.HOST}] has been #{isAccepted ? 'accepted' : 'rejected'} | 9 | | #[a(href=abuseUrl) Your abuse report ##{abuseId}] on #{WEBSERVER.HOST} has been #{isAccepted ? 'accepted' : 'rejected'} |
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 5c50fcf01..9c2f16c27 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts | |||
@@ -24,6 +24,7 @@ import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../type | |||
24 | import { isBlockedByServerOrAccount } from './blocklist' | 24 | import { isBlockedByServerOrAccount } from './blocklist' |
25 | import { Emailer } from './emailer' | 25 | import { Emailer } from './emailer' |
26 | import { PeerTubeSocket } from './peertube-socket' | 26 | import { PeerTubeSocket } from './peertube-socket' |
27 | import { AccountModel } from '@server/models/account/account' | ||
27 | 28 | ||
28 | class Notifier { | 29 | class Notifier { |
29 | 30 | ||
@@ -137,7 +138,7 @@ class Notifier { | |||
137 | }) | 138 | }) |
138 | } | 139 | } |
139 | 140 | ||
140 | notifyOnAbuseMessage (abuse: MAbuseFull, message: AbuseMessageModel): void { | 141 | notifyOnAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage): void { |
141 | this.notifyOfNewAbuseMessage(abuse, message) | 142 | this.notifyOfNewAbuseMessage(abuse, message) |
142 | .catch(err => { | 143 | .catch(err => { |
143 | logger.error('Cannot notify on new abuse %d message.', abuse.id, { err }) | 144 | logger.error('Cannot notify on new abuse %d message.', abuse.id, { err }) |
@@ -436,6 +437,8 @@ class Notifier { | |||
436 | const url = this.getAbuseUrl(abuse) | 437 | const url = this.getAbuseUrl(abuse) |
437 | logger.info('Notifying reporter and moderators of new abuse message on %s.', url) | 438 | logger.info('Notifying reporter and moderators of new abuse message on %s.', url) |
438 | 439 | ||
440 | const accountMessage = await AccountModel.load(message.accountId) | ||
441 | |||
439 | function settingGetter (user: MUserWithNotificationSetting) { | 442 | function settingGetter (user: MUserWithNotificationSetting) { |
440 | return user.NotificationSetting.abuseNewMessage | 443 | return user.NotificationSetting.abuseNewMessage |
441 | } | 444 | } |
@@ -452,11 +455,11 @@ class Notifier { | |||
452 | } | 455 | } |
453 | 456 | ||
454 | function emailSenderReporter (emails: string[]) { | 457 | function emailSenderReporter (emails: string[]) { |
455 | return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message }) | 458 | return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message, accountMessage }) |
456 | } | 459 | } |
457 | 460 | ||
458 | function emailSenderModerators (emails: string[]) { | 461 | function emailSenderModerators (emails: string[]) { |
459 | return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message }) | 462 | return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message, accountMessage }) |
460 | } | 463 | } |
461 | 464 | ||
462 | async function buildReporterOptions () { | 465 | async function buildReporterOptions () { |