diff options
Diffstat (limited to 'client/src')
19 files changed, 480 insertions, 158 deletions
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index 87ed33a45..4345d1945 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts | |||
@@ -47,8 +47,8 @@ export class AdminComponent implements OnInit { | |||
47 | 47 | ||
48 | if (this.hasAbusesRight()) { | 48 | if (this.hasAbusesRight()) { |
49 | moderationItems.children.push({ | 49 | moderationItems.children.push({ |
50 | label: this.i18n('Video reports'), | 50 | label: this.i18n('Reports'), |
51 | routerLink: '/admin/moderation/video-abuses/list', | 51 | routerLink: '/admin/moderation/abuses/list', |
52 | iconName: 'flag' | 52 | iconName: 'flag' |
53 | }) | 53 | }) |
54 | } | 54 | } |
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html index d031ea8ed..cba9cfb73 100644 --- a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html +++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html | |||
@@ -3,10 +3,13 @@ | |||
3 | <div class="col-8"> | 3 | <div class="col-8"> |
4 | 4 | ||
5 | <!-- report metadata --> | 5 | <!-- report metadata --> |
6 | <div class="d-flex"> | 6 | <div class="d-flex" *ngIf="abuse.reporterAccount"> |
7 | <span class="col-3 moderation-expanded-label" i18n>Reporter</span> | 7 | <span class="col-3 moderation-expanded-label" i18n>Reporter</span> |
8 | |||
8 | <span class="col-9 moderation-expanded-text"> | 9 | <span class="col-9 moderation-expanded-text"> |
9 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" class="chip"> | 10 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" |
11 | class="chip" | ||
12 | > | ||
10 | <img | 13 | <img |
11 | class="avatar" | 14 | class="avatar" |
12 | [src]="abuse.reporterAccount.avatar?.path" | 15 | [src]="abuse.reporterAccount.avatar?.path" |
@@ -17,27 +20,35 @@ | |||
17 | <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span> | 20 | <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span> |
18 | </div> | 21 | </div> |
19 | </a> | 22 | </a> |
20 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n> | 23 | |
24 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" | ||
25 | class="ml-auto text-muted abuse-details-links" i18n | ||
26 | > | ||
21 | {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> | 27 | {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> |
22 | </a> | 28 | </a> |
23 | </span> | 29 | </span> |
24 | </div> | 30 | </div> |
25 | 31 | ||
26 | <div class="d-flex"> | 32 | <div class="d-flex" *ngIf="abuse.flaggedAccount"> |
27 | <span class="col-3 moderation-expanded-label" i18n>Reportee</span> | 33 | <span class="col-3 moderation-expanded-label" i18n>Reportee</span> |
28 | <span class="col-9 moderation-expanded-text"> | 34 | <span class="col-9 moderation-expanded-text"> |
29 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.video.channel.ownerAccount.displayName + '"' }" class="chip"> | 35 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" |
36 | class="chip" | ||
37 | > | ||
30 | <img | 38 | <img |
31 | class="avatar" | 39 | class="avatar" |
32 | [src]="abuse.video.channel.ownerAccount?.avatar?.path" | 40 | [src]="abuse.flaggedAccount?.avatar?.path" |
33 | (error)="switchToDefaultAvatar($event)" | 41 | (error)="switchToDefaultAvatar($event)" |
34 | alt="Avatar" | 42 | alt="Avatar" |
35 | > | 43 | > |
36 | <div> | 44 | <div> |
37 | <span class="text-muted">{{ abuse.video.channel.ownerAccount ? abuse.video.channel.ownerAccount.nameWithHost : '' }}</span> | 45 | <span class="text-muted">{{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}</span> |
38 | </div> | 46 | </div> |
39 | </a> | 47 | </a> |
40 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.video.channel.ownerAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n> | 48 | |
49 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" | ||
50 | class="ml-auto text-muted abuse-details-links" i18n | ||
51 | > | ||
41 | {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> | 52 | {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span> |
42 | </a> | 53 | </a> |
43 | </span> | 54 | </span> |
@@ -45,7 +56,7 @@ | |||
45 | 56 | ||
46 | <div class="d-flex" *ngIf="abuse.updatedAt"> | 57 | <div class="d-flex" *ngIf="abuse.updatedAt"> |
47 | <span class="col-3 moderation-expanded-label" i18n>Updated</span> | 58 | <span class="col-3 moderation-expanded-label" i18n>Updated</span> |
48 | <time class="col-9 moderation-expanded-text video-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time> | 59 | <time class="col-9 moderation-expanded-text abuse-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time> |
49 | </div> | 60 | </div> |
50 | 61 | ||
51 | <!-- report text --> | 62 | <!-- report text --> |
@@ -60,34 +71,45 @@ | |||
60 | <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex"> | 71 | <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex"> |
61 | <span class="col-3"></span> | 72 | <span class="col-3"></span> |
62 | <span class="col-9"> | 73 | <span class="col-9"> |
63 | <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" *ngFor="let reason of getPredefinedReasons()"> | 74 | <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ '/admin/moderation/abuses/list' ]" |
75 | [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" | ||
76 | > | ||
64 | <div>{{ reason.label }}</div> | 77 | <div>{{ reason.label }}</div> |
65 | </a> | 78 | </a> |
66 | </span> | 79 | </span> |
67 | </div> | 80 | </div> |
68 | 81 | ||
69 | <div *ngIf="abuse.startAt" class="mt-2 d-flex"> | 82 | <div *ngIf="abuse.video?.startAt" class="mt-2 d-flex"> |
70 | <span class="col-3 moderation-expanded-label" i18n>Reported part</span> | 83 | <span class="col-3 moderation-expanded-label" i18n>Reported part</span> |
71 | <span class="col-9"> | 84 | <span class="col-9"> |
72 | {{ startAt }}<ng-container *ngIf="abuse.endAt"> - {{ endAt }}</ng-container> | 85 | {{ startAt }}<ng-container *ngIf="abuse.video.endAt"> - {{ endAt }}</ng-container> |
73 | </span> | 86 | </span> |
74 | </div> | 87 | </div> |
75 | 88 | ||
76 | <div class="mt-3 d-flex" *ngIf="abuse.moderationComment"> | 89 | <div class="mt-3 d-flex" *ngIf="abuse.moderationComment"> |
77 | <span class="col-3 moderation-expanded-label" i18n>Note</span> | 90 | <span class="col-3 moderation-expanded-label" i18n>Note</span> |
78 | <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.moderationCommentHtml"></span> | 91 | <span class="col-9 moderation-expanded-text d-block" [innerHTML]="abuse.moderationCommentHtml"></span> |
79 | </div> | 92 | </div> |
80 | 93 | ||
81 | </div> | 94 | </div> |
82 | 95 | ||
83 | <!-- report right part (video details) --> | 96 | <!-- report right part (video/comment details) --> |
84 | <div class="col-4"> | 97 | <div class="col-4"> |
85 | <div class="screenratio"> | 98 | <div *ngIf="abuse.video" class="screenratio"> |
86 | <div *ngIf="abuse.video.deleted || abuse.video.blacklisted"> | 99 | <div> |
87 | <span i18n *ngIf="abuse.video.deleted">The video was deleted</span> | 100 | <span i18n *ngIf="abuse.video.deleted">The video was deleted</span> |
88 | <span i18n *ngIf="!abuse.video.deleted">The video was blocked</span> | 101 | <span i18n *ngIf="!abuse.video.deleted">The video was blocked</span> |
89 | </div> | 102 | </div> |
103 | |||
90 | <div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div> | 104 | <div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div> |
91 | </div> | 105 | </div> |
106 | |||
107 | <div *ngIf="abuse.comment" class="comment-html"> | ||
108 | <div> | ||
109 | <strong i18n>Comment:</strong> | ||
110 | </div> | ||
111 | |||
112 | <div [innerHTML]="abuse.commentHtml"></div> | ||
113 | </div> | ||
92 | </div> | 114 | </div> |
93 | </div> | 115 | </div> |
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts index 8f87630b8..fb0f65764 100644 --- a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts +++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts | |||
@@ -31,15 +31,16 @@ export class AbuseDetailsComponent { | |||
31 | } | 31 | } |
32 | 32 | ||
33 | get startAt () { | 33 | get startAt () { |
34 | return durationToString(this.abuse.startAt) | 34 | return durationToString(this.abuse.video.startAt) |
35 | } | 35 | } |
36 | 36 | ||
37 | get endAt () { | 37 | get endAt () { |
38 | return durationToString(this.abuse.endAt) | 38 | return durationToString(this.abuse.video.endAt) |
39 | } | 39 | } |
40 | 40 | ||
41 | getPredefinedReasons () { | 41 | getPredefinedReasons () { |
42 | if (!this.abuse.predefinedReasons) return [] | 42 | if (!this.abuse.predefinedReasons) return [] |
43 | |||
43 | return this.abuse.predefinedReasons.map(r => ({ | 44 | return this.abuse.predefinedReasons.map(r => ({ |
44 | id: r, | 45 | id: r, |
45 | label: this.predefinedReasonsTranslations[r] | 46 | label: this.predefinedReasonsTranslations[r] |
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html index 333438269..1ad73e38a 100644 --- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html | |||
@@ -38,7 +38,7 @@ | |||
38 | <tr> <!-- header --> | 38 | <tr> <!-- header --> |
39 | <th style="width: 40px;"></th> | 39 | <th style="width: 40px;"></th> |
40 | <th style="width: 20%;" pResizableColumn i18n>Reporter</th> | 40 | <th style="width: 20%;" pResizableColumn i18n>Reporter</th> |
41 | <th i18n>Video</th> | 41 | <th i18n>Video/Comment/Account</th> |
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 style="width: 150px;"></th> | 44 | <th style="width: 150px;"></th> |
@@ -54,7 +54,7 @@ | |||
54 | </td> | 54 | </td> |
55 | 55 | ||
56 | <td> | 56 | <td> |
57 | <a [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | 57 | <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> |
58 | <div class="chip two-lines"> | 58 | <div class="chip two-lines"> |
59 | <img | 59 | <img |
60 | class="avatar" | 60 | class="avatar" |
@@ -64,54 +64,73 @@ | |||
64 | > | 64 | > |
65 | <div> | 65 | <div> |
66 | {{ abuse.reporterAccount.displayName }} | 66 | {{ abuse.reporterAccount.displayName }} |
67 | <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span> | 67 | <span>{{ abuse.reporterAccount.nameWithHost }}</span> |
68 | </div> | 68 | </div> |
69 | </div> | 69 | </div> |
70 | </a> | 70 | </a> |
71 | |||
72 | <span i18n *ngIf="!abuse.reporterAccount"> | ||
73 | Deleted account | ||
74 | </span> | ||
71 | </td> | 75 | </td> |
72 | 76 | ||
73 | <td *ngIf="!abuse.video.deleted"> | 77 | <ng-container *ngIf="abuse.video"> |
74 | <a [href]="getVideoUrl(abuse)" class="video-table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer"> | 78 | |
75 | <div class="video-table-video"> | 79 | <td *ngIf="!abuse.video.deleted"> |
76 | <div class="video-table-video-image"> | 80 | <a [href]="getVideoUrl(abuse)" class="table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer"> |
77 | <img [src]="abuse.video.thumbnailPath"> | 81 | <div class="table-video"> |
78 | <span | 82 | <div class="table-video-image"> |
79 | class="video-table-video-image-label" *ngIf="abuse.count > 1" | 83 | <img [src]="abuse.video.thumbnailPath"> |
80 | i18n-title title="This video has been reported multiple times." | 84 | <span |
81 | > | 85 | class="table-video-image-label" *ngIf="abuse.count > 1" |
82 | {{ abuse.nth }}/{{ abuse.count }} | 86 | i18n-title title="This video has been reported multiple times." |
83 | </span> | 87 | > |
88 | {{ abuse.nth }}/{{ abuse.count }} | ||
89 | </span> | ||
90 | </div> | ||
91 | |||
92 | <div class="table-video-text"> | ||
93 | <div> | ||
94 | <span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span> | ||
95 | <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span> | ||
96 | {{ abuse.video.name }} | ||
97 | </div> | ||
98 | <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div> | ||
99 | </div> | ||
84 | </div> | 100 | </div> |
85 | <div class="video-table-video-text"> | 101 | </a> |
102 | </td> | ||
103 | |||
104 | <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse"> | ||
105 | <div class="table-video" i18n-title title="Video was deleted"> | ||
106 | <div class="table-video-image"> | ||
107 | <span i18n>Deleted</span> | ||
108 | </div> | ||
109 | |||
110 | <div class="table-video-text"> | ||
86 | <div> | 111 | <div> |
87 | <span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span> | ||
88 | <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span> | ||
89 | {{ abuse.video.name }} | 112 | {{ abuse.video.name }} |
113 | <span class="glyphicon glyphicon-trash"></span> | ||
90 | </div> | 114 | </div> |
91 | <div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div> | 115 | <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div> |
92 | </div> | 116 | </div> |
93 | </div> | 117 | </div> |
94 | </a> | 118 | </td> |
95 | </td> | 119 | </ng-container> |
96 | 120 | ||
97 | <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse"> | 121 | <ng-container *ngIf="abuse.comment"> |
98 | <div class="video-table-video" i18n-title title="Video was deleted"> | 122 | <td> |
99 | <div class="video-table-video-image"> | 123 | <a [href]="getCommentUrl(abuse)" [innerHTML]="abuse.truncatedCommentHtml" class="table-comment-link" |
100 | <span i18n>Deleted</span> | 124 | [title]="abuse.comment.video.name" target="_blank" rel="noopener noreferrer" |
101 | </div> | 125 | ></a> |
102 | <div class="video-table-video-text"> | 126 | |
103 | <div> | 127 | <div class="comment-flagged-account" *ngIf="abuse.flaggedAccount">by {{ abuse.flaggedAccount.displayName }}</div> |
104 | {{ abuse.video.name }} | 128 | </td> |
105 | <span class="glyphicon glyphicon-trash"></span> | 129 | </ng-container> |
106 | </div> | ||
107 | <div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div> | ||
108 | </div> | ||
109 | </div> | ||
110 | </td> | ||
111 | 130 | ||
112 | <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td> | 131 | <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td> |
113 | 132 | ||
114 | <td class="c-hand video-abuse-states" [pRowToggler]="abuse"> | 133 | <td class="c-hand abuse-states" [pRowToggler]="abuse"> |
115 | <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span> | 134 | <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span> |
116 | <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span> | 135 | <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span> |
117 | <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span> | 136 | <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span> |
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss index 8eee15b64..c22f98c47 100644 --- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss | |||
@@ -10,7 +10,7 @@ | |||
10 | @include disable-default-a-behaviour; | 10 | @include disable-default-a-behaviour; |
11 | } | 11 | } |
12 | 12 | ||
13 | .video-abuse-states .glyphicon-comment { | 13 | .abuse-states .glyphicon-comment { |
14 | margin-left: 0.5rem; | 14 | margin-left: 0.5rem; |
15 | } | 15 | } |
16 | 16 | ||
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts index 427ec4d5d..1ea61ed37 100644 --- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts +++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts | |||
@@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api' | |||
2 | import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' | 2 | import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' |
3 | import { environment } from 'src/environments/environment' | 3 | import { environment } from 'src/environments/environment' |
4 | import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core' | 4 | import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core' |
5 | import { DomSanitizer } from '@angular/platform-browser' | 5 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' |
6 | import { ActivatedRoute, Params, Router } from '@angular/router' | 6 | import { ActivatedRoute, Params, Router } from '@angular/router' |
7 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' | 7 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' |
8 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 8 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
@@ -10,15 +10,20 @@ import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/s | |||
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { Abuse, AbuseState } from '@shared/models' | 11 | import { Abuse, AbuseState } from '@shared/models' |
12 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' | 12 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' |
13 | import truncate from 'lodash-es/truncate' | ||
13 | 14 | ||
14 | export type ProcessedAbuse = Abuse & { | 15 | export type ProcessedAbuse = Abuse & { |
15 | moderationCommentHtml?: string, | 16 | moderationCommentHtml?: string, |
16 | reasonHtml?: string | 17 | reasonHtml?: string |
17 | embedHtml?: string | 18 | embedHtml?: SafeHtml |
18 | updatedAt?: Date | 19 | updatedAt?: Date |
19 | 20 | ||
20 | // override bare server-side definitions with rich client-side definitions | 21 | // override bare server-side definitions with rich client-side definitions |
21 | reporterAccount: Account | 22 | reporterAccount?: Account |
23 | flaggedAccount?: Account | ||
24 | |||
25 | truncatedCommentHtml?: string | ||
26 | commentHtml?: string | ||
22 | 27 | ||
23 | video: Abuse['video'] & { | 28 | video: Abuse['video'] & { |
24 | channel: Abuse['video']['channel'] & { | 29 | channel: Abuse['video']['channel'] & { |
@@ -92,11 +97,11 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
92 | { | 97 | { |
93 | label: this.i18n('Actions for the video'), | 98 | label: this.i18n('Actions for the video'), |
94 | isHeader: true, | 99 | isHeader: true, |
95 | isDisplayed: abuse => !abuse.video.deleted | 100 | isDisplayed: abuse => abuse.video && !abuse.video.deleted |
96 | }, | 101 | }, |
97 | { | 102 | { |
98 | label: this.i18n('Block video'), | 103 | label: this.i18n('Block video'), |
99 | isDisplayed: abuse => !abuse.video.deleted && !abuse.video.blacklisted, | 104 | isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted, |
100 | handler: abuse => { | 105 | handler: abuse => { |
101 | this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true) | 106 | this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true) |
102 | .subscribe( | 107 | .subscribe( |
@@ -112,7 +117,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
112 | }, | 117 | }, |
113 | { | 118 | { |
114 | label: this.i18n('Unblock video'), | 119 | label: this.i18n('Unblock video'), |
115 | isDisplayed: abuse => !abuse.video.deleted && abuse.video.blacklisted, | 120 | isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted, |
116 | handler: abuse => { | 121 | handler: abuse => { |
117 | this.videoBlocklistService.unblockVideo(abuse.video.id) | 122 | this.videoBlocklistService.unblockVideo(abuse.video.id) |
118 | .subscribe( | 123 | .subscribe( |
@@ -128,7 +133,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
128 | }, | 133 | }, |
129 | { | 134 | { |
130 | label: this.i18n('Delete video'), | 135 | label: this.i18n('Delete video'), |
131 | isDisplayed: abuse => !abuse.video.deleted, | 136 | isDisplayed: abuse => abuse.video && !abuse.video.deleted, |
132 | handler: async abuse => { | 137 | handler: async abuse => { |
133 | const res = await this.confirmService.confirm( | 138 | const res = await this.confirmService.confirm( |
134 | this.i18n('Do you really want to delete this video?'), | 139 | this.i18n('Do you really want to delete this video?'), |
@@ -152,10 +157,12 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
152 | [ | 157 | [ |
153 | { | 158 | { |
154 | label: this.i18n('Actions for the reporter'), | 159 | label: this.i18n('Actions for the reporter'), |
155 | isHeader: true | 160 | isHeader: true, |
161 | isDisplayed: abuse => !!abuse.reporterAccount | ||
156 | }, | 162 | }, |
157 | { | 163 | { |
158 | label: this.i18n('Mute reporter'), | 164 | label: this.i18n('Mute reporter'), |
165 | isDisplayed: abuse => !!abuse.reporterAccount, | ||
159 | handler: async abuse => { | 166 | handler: async abuse => { |
160 | const account = abuse.reporterAccount as Account | 167 | const account = abuse.reporterAccount as Account |
161 | 168 | ||
@@ -175,7 +182,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
175 | }, | 182 | }, |
176 | { | 183 | { |
177 | label: this.i18n('Mute server'), | 184 | label: this.i18n('Mute server'), |
178 | isDisplayed: abuse => !abuse.reporterAccount.userId, | 185 | isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId, |
179 | handler: async abuse => { | 186 | handler: async abuse => { |
180 | this.blocklistService.blockServerByInstance(abuse.reporterAccount.host) | 187 | this.blocklistService.blockServerByInstance(abuse.reporterAccount.host) |
181 | .subscribe( | 188 | .subscribe( |
@@ -231,7 +238,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
231 | const queryParams: Params = {} | 238 | const queryParams: Params = {} |
232 | if (search) Object.assign(queryParams, { search }) | 239 | if (search) Object.assign(queryParams, { search }) |
233 | 240 | ||
234 | this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams }) | 241 | this.router.navigate([ '/admin/moderation/abuses/list' ], { queryParams }) |
235 | } | 242 | } |
236 | 243 | ||
237 | resetTableFilter () { | 244 | resetTableFilter () { |
@@ -253,6 +260,10 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
253 | return Video.buildClientUrl(abuse.video.uuid) | 260 | return Video.buildClientUrl(abuse.video.uuid) |
254 | } | 261 | } |
255 | 262 | ||
263 | getCommentUrl (abuse: Abuse) { | ||
264 | return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId | ||
265 | } | ||
266 | |||
256 | getVideoEmbed (abuse: Abuse) { | 267 | getVideoEmbed (abuse: Abuse) { |
257 | return buildVideoEmbed( | 268 | return buildVideoEmbed( |
258 | buildVideoLink({ | 269 | buildVideoLink({ |
@@ -300,23 +311,45 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn | |||
300 | }).subscribe( | 311 | }).subscribe( |
301 | async resultList => { | 312 | async resultList => { |
302 | this.totalRecords = resultList.total | 313 | this.totalRecords = resultList.total |
303 | const abuses = [] | ||
304 | 314 | ||
305 | for (const abuse of resultList.data) { | 315 | this.abuses = [] |
306 | Object.assign(abuse, { | 316 | |
307 | reasonHtml: await this.toHtml(abuse.reason), | 317 | for (const a of resultList.data) { |
308 | moderationCommentHtml: await this.toHtml(abuse.moderationComment), | 318 | const abuse = a as ProcessedAbuse |
309 | embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)), | 319 | |
310 | reporterAccount: new Account(abuse.reporterAccount) | 320 | abuse.reasonHtml = await this.toHtml(abuse.reason) |
311 | }) | 321 | abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment) |
322 | |||
323 | if (abuse.video) { | ||
324 | abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)) | ||
325 | |||
326 | if (abuse.video.channel?.ownerAccount) { | ||
327 | abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) | ||
328 | } | ||
329 | } | ||
330 | |||
331 | if (abuse.comment) { | ||
332 | if (abuse.comment.deleted) { | ||
333 | abuse.truncatedCommentHtml = abuse.commentHtml = this.i18n('Deleted comment') | ||
334 | } else { | ||
335 | const truncated = truncate(abuse.comment.text, { length: 100 }) | ||
336 | abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true) | ||
337 | abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true) | ||
338 | } | ||
339 | } | ||
340 | |||
341 | if (abuse.reporterAccount) { | ||
342 | abuse.reporterAccount = new Account(abuse.reporterAccount) | ||
343 | } | ||
344 | |||
345 | if (abuse.flaggedAccount) { | ||
346 | abuse.flaggedAccount = new Account(abuse.flaggedAccount) | ||
347 | } | ||
312 | 348 | ||
313 | if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount) | ||
314 | if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt | 349 | if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt |
315 | 350 | ||
316 | abuses.push(abuse as ProcessedAbuse) | 351 | this.abuses.push(abuse) |
317 | } | 352 | } |
318 | |||
319 | this.abuses = abuses | ||
320 | }, | 353 | }, |
321 | 354 | ||
322 | err => this.notifier.error(err.message) | 355 | err => this.notifier.error(err.message) |
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss index 0ec420af9..f73c71dc5 100644 --- a/client/src/app/+admin/moderation/moderation.component.scss +++ b/client/src/app/+admin/moderation/moderation.component.scss | |||
@@ -25,18 +25,18 @@ | |||
25 | vertical-align: top; | 25 | vertical-align: top; |
26 | text-align: right; | 26 | text-align: right; |
27 | } | 27 | } |
28 | 28 | ||
29 | .moderation-expanded-text { | 29 | .moderation-expanded-text { |
30 | display: inline-flex; | 30 | display: inline-flex; |
31 | word-wrap: break-word; | 31 | word-wrap: break-word; |
32 | 32 | ||
33 | ::ng-deep p:last-child { | 33 | ::ng-deep p:last-child { |
34 | margin-bottom: 0px !important; | 34 | margin-bottom: 0px !important; |
35 | } | 35 | } |
36 | } | 36 | } |
37 | } | 37 | } |
38 | 38 | ||
39 | .video-table-states { | 39 | .table-states { |
40 | & > :not(:first-child) { | 40 | & > :not(:first-child) { |
41 | margin-left: .4rem; | 41 | margin-left: .4rem; |
42 | } | 42 | } |
@@ -59,6 +59,7 @@ p-calendar { | |||
59 | .screenratio { | 59 | .screenratio { |
60 | div { | 60 | div { |
61 | @include miniature-thumbnail; | 61 | @include miniature-thumbnail; |
62 | |||
62 | display: inline-flex; | 63 | display: inline-flex; |
63 | justify-content: center; | 64 | justify-content: center; |
64 | align-items: center; | 65 | align-items: center; |
@@ -72,6 +73,11 @@ p-calendar { | |||
72 | }; | 73 | }; |
73 | } | 74 | } |
74 | 75 | ||
76 | .comment-html { | ||
77 | background-color: #ececec; | ||
78 | padding: 10px; | ||
79 | } | ||
80 | |||
75 | .chip { | 81 | .chip { |
76 | @include chip; | 82 | @include chip; |
77 | } | 83 | } |
@@ -83,16 +89,32 @@ my-action-dropdown.show { | |||
83 | } | 89 | } |
84 | 90 | ||
85 | 91 | ||
86 | .video-table-video-link { | 92 | .table-video-link { |
87 | @include disable-outline; | 93 | @include disable-outline; |
94 | |||
88 | position: relative; | 95 | position: relative; |
89 | top: 3px; | 96 | top: 3px; |
90 | } | 97 | } |
91 | 98 | ||
92 | .video-table-video { | 99 | .table-comment-link { |
100 | @include disable-outline; | ||
101 | |||
102 | color: var(--mainForegroundColor); | ||
103 | |||
104 | ::ng-deep p:last-child { | ||
105 | margin: 0; | ||
106 | } | ||
107 | } | ||
108 | |||
109 | .comment-flagged-account { | ||
110 | font-size: 11px; | ||
111 | color: var(--greyForegroundColor); | ||
112 | } | ||
113 | |||
114 | .table-video { | ||
93 | display: inline-flex; | 115 | display: inline-flex; |
94 | 116 | ||
95 | .video-table-video-image { | 117 | .table-video-image { |
96 | @include miniature-thumbnail; | 118 | @include miniature-thumbnail; |
97 | 119 | ||
98 | $image-height: 45px; | 120 | $image-height: 45px; |
@@ -118,7 +140,7 @@ my-action-dropdown.show { | |||
118 | color: pvar(--inputPlaceholderColor); | 140 | color: pvar(--inputPlaceholderColor); |
119 | } | 141 | } |
120 | 142 | ||
121 | .video-table-video-image-label { | 143 | .table-video-image-label { |
122 | @include static-thumbnail-overlay; | 144 | @include static-thumbnail-overlay; |
123 | position: absolute; | 145 | position: absolute; |
124 | border-radius: 3px; | 146 | border-radius: 3px; |
@@ -130,7 +152,7 @@ my-action-dropdown.show { | |||
130 | } | 152 | } |
131 | } | 153 | } |
132 | 154 | ||
133 | .video-table-video-text { | 155 | .table-video-text { |
134 | display: inline-flex; | 156 | display: inline-flex; |
135 | flex-direction: column; | 157 | flex-direction: column; |
136 | justify-content: center; | 158 | justify-content: center; |
@@ -145,7 +167,8 @@ my-action-dropdown.show { | |||
145 | } | 167 | } |
146 | 168 | ||
147 | div + div { | 169 | div + div { |
148 | font-size: 80%; | 170 | color: var(--greyForegroundColor); |
171 | font-size: 11px; | ||
149 | } | 172 | } |
150 | } | 173 | } |
151 | } | 174 | } |
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts index 1e207e5e8..8a31a54dc 100644 --- a/client/src/app/+admin/moderation/moderation.routes.ts +++ b/client/src/app/+admin/moderation/moderation.routes.ts | |||
@@ -33,7 +33,7 @@ export const ModerationRoutes: Routes = [ | |||
33 | data: { | 33 | data: { |
34 | userRight: UserRight.MANAGE_ABUSES, | 34 | userRight: UserRight.MANAGE_ABUSES, |
35 | meta: { | 35 | meta: { |
36 | title: 'Video reports' | 36 | title: 'Reports' |
37 | } | 37 | } |
38 | } | 38 | } |
39 | }, | 39 | }, |
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/comment/video-comment.component.html index 002de57e4..f02ea549a 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment.component.html +++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.html | |||
@@ -45,6 +45,7 @@ | |||
45 | <div *ngIf="isRemovableByUser()" (click)="onWantToDelete()" class="comment-action-delete" i18n>Delete</div> | 45 | <div *ngIf="isRemovableByUser()" (click)="onWantToDelete()" class="comment-action-delete" i18n>Delete</div> |
46 | 46 | ||
47 | <my-user-moderation-dropdown | 47 | <my-user-moderation-dropdown |
48 | [prependActions]="prependModerationActions" | ||
48 | buttonSize="small" [account]="commentAccount" [user]="commentUser" i18n-label label="Options" placement="bottom-left auto" | 49 | buttonSize="small" [account]="commentAccount" [user]="commentUser" i18n-label label="Options" placement="bottom-left auto" |
49 | ></my-user-moderation-dropdown> | 50 | ></my-user-moderation-dropdown> |
50 | </div> | 51 | </div> |
@@ -93,3 +94,7 @@ | |||
93 | </div> | 94 | </div> |
94 | </div> | 95 | </div> |
95 | </div> | 96 | </div> |
97 | |||
98 | <ng-container *ngIf="prependModerationActions"> | ||
99 | <my-comment-report #commentReportModal [comment]="comment"></my-comment-report> | ||
100 | </ng-container> | ||
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts index 27846c1ad..2a4a6e737 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts +++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts | |||
@@ -1,7 +1,10 @@ | |||
1 | import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' | 1 | |
2 | import { Component, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { MarkdownService, Notifier, UserService } from '@app/core' | 3 | import { MarkdownService, Notifier, UserService } from '@app/core' |
3 | import { AuthService } from '@app/core/auth' | 4 | import { AuthService } from '@app/core/auth' |
4 | import { Account, Actor, Video } from '@app/shared/shared-main' | 5 | import { Account, Actor, DropdownAction, Video } from '@app/shared/shared-main' |
6 | import { CommentReportComponent } from '@app/shared/shared-moderation/comment-report.component' | ||
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
5 | import { User, UserRight } from '@shared/models' | 8 | import { User, UserRight } from '@shared/models' |
6 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' | 9 | import { VideoCommentThreadTree } from './video-comment-thread-tree.model' |
7 | import { VideoComment } from './video-comment.model' | 10 | import { VideoComment } from './video-comment.model' |
@@ -12,6 +15,8 @@ import { VideoComment } from './video-comment.model' | |||
12 | styleUrls: ['./video-comment.component.scss'] | 15 | styleUrls: ['./video-comment.component.scss'] |
13 | }) | 16 | }) |
14 | export class VideoCommentComponent implements OnInit, OnChanges { | 17 | export class VideoCommentComponent implements OnInit, OnChanges { |
18 | @ViewChild('commentReportModal') commentReportModal: CommentReportComponent | ||
19 | |||
15 | @Input() video: Video | 20 | @Input() video: Video |
16 | @Input() comment: VideoComment | 21 | @Input() comment: VideoComment |
17 | @Input() parentComments: VideoComment[] = [] | 22 | @Input() parentComments: VideoComment[] = [] |
@@ -26,6 +31,8 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
26 | @Output() resetReply = new EventEmitter() | 31 | @Output() resetReply = new EventEmitter() |
27 | @Output() timestampClicked = new EventEmitter<number>() | 32 | @Output() timestampClicked = new EventEmitter<number>() |
28 | 33 | ||
34 | prependModerationActions: DropdownAction<any>[] | ||
35 | |||
29 | sanitizedCommentHTML = '' | 36 | sanitizedCommentHTML = '' |
30 | newParentComments: VideoComment[] = [] | 37 | newParentComments: VideoComment[] = [] |
31 | 38 | ||
@@ -33,6 +40,7 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
33 | commentUser: User | 40 | commentUser: User |
34 | 41 | ||
35 | constructor ( | 42 | constructor ( |
43 | private i18n: I18n, | ||
36 | private markdownService: MarkdownService, | 44 | private markdownService: MarkdownService, |
37 | private authService: AuthService, | 45 | private authService: AuthService, |
38 | private userService: UserService, | 46 | private userService: UserService, |
@@ -127,5 +135,20 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
127 | } else { | 135 | } else { |
128 | this.comment.account = null | 136 | this.comment.account = null |
129 | } | 137 | } |
138 | |||
139 | if (this.isUserLoggedIn()) { | ||
140 | this.prependModerationActions = [ | ||
141 | { | ||
142 | label: this.i18n('Report comment'), | ||
143 | handler: () => this.showReportModal() | ||
144 | } | ||
145 | ] | ||
146 | } else { | ||
147 | this.prependModerationActions = undefined | ||
148 | } | ||
149 | } | ||
150 | |||
151 | private showReportModal () { | ||
152 | this.commentReportModal.show() | ||
130 | } | 153 | } |
131 | } | 154 | } |
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts index 0fa161ce6..9ec6dbab1 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts | |||
@@ -46,8 +46,10 @@ export abstract class Actor implements ActorServer { | |||
46 | this.host = hash.host | 46 | this.host = hash.host |
47 | this.followingCount = hash.followingCount | 47 | this.followingCount = hash.followingCount |
48 | this.followersCount = hash.followersCount | 48 | this.followersCount = hash.followersCount |
49 | this.createdAt = new Date(hash.createdAt.toString()) | 49 | |
50 | this.updatedAt = new Date(hash.updatedAt.toString()) | 50 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) |
51 | if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) | ||
52 | |||
51 | this.avatar = hash.avatar | 53 | this.avatar = hash.avatar |
52 | 54 | ||
53 | this.updateComputedAttributes() | 55 | this.updateComputedAttributes() |
diff --git a/client/src/app/shared/shared-moderation/abuse.service.ts b/client/src/app/shared/shared-moderation/abuse.service.ts index f45018d5c..95ac16955 100644 --- a/client/src/app/shared/shared-moderation/abuse.service.ts +++ b/client/src/app/shared/shared-moderation/abuse.service.ts | |||
@@ -5,18 +5,20 @@ import { catchError, map } from 'rxjs/operators' | |||
5 | import { HttpClient, HttpParams } from '@angular/common/http' | 5 | import { HttpClient, HttpParams } from '@angular/common/http' |
6 | import { Injectable } from '@angular/core' | 6 | import { Injectable } from '@angular/core' |
7 | import { RestExtractor, RestPagination, RestService } from '@app/core' | 7 | import { RestExtractor, RestPagination, RestService } from '@app/core' |
8 | import { AbuseUpdate, ResultList, Abuse, AbuseCreate, AbuseState } from '@shared/models' | 8 | import { Abuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList } from '@shared/models' |
9 | import { environment } from '../../../environments/environment' | 9 | import { environment } from '../../../environments/environment' |
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
10 | 11 | ||
11 | @Injectable() | 12 | @Injectable() |
12 | export class AbuseService { | 13 | export class AbuseService { |
13 | private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses' | 14 | private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses' |
14 | 15 | ||
15 | constructor ( | 16 | constructor ( |
17 | private i18n: I18n, | ||
16 | private authHttp: HttpClient, | 18 | private authHttp: HttpClient, |
17 | private restService: RestService, | 19 | private restService: RestService, |
18 | private restExtractor: RestExtractor | 20 | private restExtractor: RestExtractor |
19 | ) {} | 21 | ) { } |
20 | 22 | ||
21 | getAbuses (options: { | 23 | getAbuses (options: { |
22 | pagination: RestPagination, | 24 | pagination: RestPagination, |
@@ -24,7 +26,7 @@ export class AbuseService { | |||
24 | search?: string | 26 | search?: string |
25 | }): Observable<ResultList<Abuse>> { | 27 | }): Observable<ResultList<Abuse>> { |
26 | const { pagination, sort, search } = options | 28 | const { pagination, sort, search } = options |
27 | const url = AbuseService.BASE_ABUSE_URL + 'abuse' | 29 | const url = AbuseService.BASE_ABUSE_URL |
28 | 30 | ||
29 | let params = new HttpParams() | 31 | let params = new HttpParams() |
30 | params = this.restService.addRestGetParams(params, pagination, sort) | 32 | params = this.restService.addRestGetParams(params, pagination, sort) |
@@ -60,39 +62,93 @@ export class AbuseService { | |||
60 | } | 62 | } |
61 | 63 | ||
62 | return this.authHttp.get<ResultList<Abuse>>(url, { params }) | 64 | return this.authHttp.get<ResultList<Abuse>>(url, { params }) |
63 | .pipe( | 65 | .pipe( |
64 | catchError(res => this.restExtractor.handleError(res)) | 66 | catchError(res => this.restExtractor.handleError(res)) |
65 | ) | 67 | ) |
66 | } | 68 | } |
67 | 69 | ||
68 | reportVideo (parameters: AbuseCreate) { | 70 | reportVideo (parameters: AbuseCreate) { |
69 | const url = AbuseService.BASE_ABUSE_URL | 71 | const url = AbuseService.BASE_ABUSE_URL |
70 | 72 | ||
71 | const body = omit(parameters, [ 'id' ]) | 73 | const body = omit(parameters, ['id']) |
72 | 74 | ||
73 | return this.authHttp.post(url, body) | 75 | return this.authHttp.post(url, body) |
74 | .pipe( | 76 | .pipe( |
75 | map(this.restExtractor.extractDataBool), | 77 | map(this.restExtractor.extractDataBool), |
76 | catchError(res => this.restExtractor.handleError(res)) | 78 | catchError(res => this.restExtractor.handleError(res)) |
77 | ) | 79 | ) |
78 | } | 80 | } |
79 | 81 | ||
80 | updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) { | 82 | updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) { |
81 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id | 83 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id |
82 | 84 | ||
83 | return this.authHttp.put(url, abuseUpdate) | 85 | return this.authHttp.put(url, abuseUpdate) |
84 | .pipe( | 86 | .pipe( |
85 | map(this.restExtractor.extractDataBool), | 87 | map(this.restExtractor.extractDataBool), |
86 | catchError(res => this.restExtractor.handleError(res)) | 88 | catchError(res => this.restExtractor.handleError(res)) |
87 | ) | 89 | ) |
88 | } | 90 | } |
89 | 91 | ||
90 | removeAbuse (abuse: Abuse) { | 92 | removeAbuse (abuse: Abuse) { |
91 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id | 93 | const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id |
92 | 94 | ||
93 | return this.authHttp.delete(url) | 95 | return this.authHttp.delete(url) |
94 | .pipe( | 96 | .pipe( |
95 | map(this.restExtractor.extractDataBool), | 97 | map(this.restExtractor.extractDataBool), |
96 | catchError(res => this.restExtractor.handleError(res)) | 98 | catchError(res => this.restExtractor.handleError(res)) |
97 | ) | 99 | ) |
98 | }} | 100 | } |
101 | |||
102 | getPrefefinedReasons (type: AbuseFilter) { | ||
103 | let reasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [ | ||
104 | { | ||
105 | id: 'violentOrRepulsive', | ||
106 | label: this.i18n('Violent or repulsive'), | ||
107 | help: this.i18n('Contains offensive, violent, or coarse language or iconography.') | ||
108 | }, | ||
109 | { | ||
110 | id: 'hatefulOrAbusive', | ||
111 | label: this.i18n('Hateful or abusive'), | ||
112 | help: this.i18n('Contains abusive, racist or sexist language or iconography.') | ||
113 | }, | ||
114 | { | ||
115 | id: 'spamOrMisleading', | ||
116 | label: this.i18n('Spam, ad or false news'), | ||
117 | help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.') | ||
118 | }, | ||
119 | { | ||
120 | id: 'privacy', | ||
121 | label: this.i18n('Privacy breach or doxxing'), | ||
122 | help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).') | ||
123 | }, | ||
124 | { | ||
125 | id: 'rights', | ||
126 | label: this.i18n('Intellectual property violation'), | ||
127 | help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.') | ||
128 | }, | ||
129 | { | ||
130 | id: 'serverRules', | ||
131 | label: this.i18n('Breaks server rules'), | ||
132 | description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.') | ||
133 | } | ||
134 | ] | ||
135 | |||
136 | if (type === 'video') { | ||
137 | reasons = reasons.concat([ | ||
138 | { | ||
139 | id: 'thumbnails', | ||
140 | label: this.i18n('Thumbnails'), | ||
141 | help: this.i18n('The above can only be seen in thumbnails.') | ||
142 | }, | ||
143 | { | ||
144 | id: 'captions', | ||
145 | label: this.i18n('Captions'), | ||
146 | help: this.i18n('The above can only be seen in captions (please describe which).') | ||
147 | } | ||
148 | ]) | ||
149 | } | ||
150 | |||
151 | return reasons | ||
152 | } | ||
153 | |||
154 | } | ||
diff --git a/client/src/app/shared/shared-moderation/comment-report.component.html b/client/src/app/shared/shared-moderation/comment-report.component.html new file mode 100644 index 000000000..1105b3788 --- /dev/null +++ b/client/src/app/shared/shared-moderation/comment-report.component.html | |||
@@ -0,0 +1,62 @@ | |||
1 | <ng-template #modal> | ||
2 | <div class="modal-header"> | ||
3 | <h4 i18n class="modal-title">Report comment</h4> | ||
4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
5 | </div> | ||
6 | |||
7 | <div class="modal-body"> | ||
8 | <form novalidate [formGroup]="form" (ngSubmit)="report()"> | ||
9 | |||
10 | <div class="row"> | ||
11 | <div class="col-5 form-group"> | ||
12 | |||
13 | <label i18n for="reportPredefinedReasons">What is the issue?</label> | ||
14 | |||
15 | <div class="ml-2 mt-2 d-flex flex-column"> | ||
16 | <ng-container formGroupName="predefinedReasons"> | ||
17 | |||
18 | <div class="form-group" *ngFor="let reason of predefinedReasons"> | ||
19 | <my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label"> | ||
20 | <ng-template *ngIf="reason.help" ptTemplate="help"> | ||
21 | <div [innerHTML]="reason.help"></div> | ||
22 | </ng-template> | ||
23 | |||
24 | <ng-container *ngIf="reason.description" ngProjectAs="description"> | ||
25 | <div [innerHTML]="reason.description"></div> | ||
26 | </ng-container> | ||
27 | </my-peertube-checkbox> | ||
28 | </div> | ||
29 | |||
30 | </ng-container> | ||
31 | </div> | ||
32 | |||
33 | </div> | ||
34 | |||
35 | <div class="col-7"> | ||
36 | <div i18n class="information"> | ||
37 | Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemoteComment()"> and will be forwarded to the comment origin ({{ originHost }}) too</ng-container>. | ||
38 | </div> | ||
39 | |||
40 | <div class="form-group"> | ||
41 | <textarea | ||
42 | i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus | ||
43 | [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control" | ||
44 | ></textarea> | ||
45 | <div *ngIf="formErrors.reason" class="form-error"> | ||
46 | {{ formErrors.reason }} | ||
47 | </div> | ||
48 | </div> | ||
49 | </div> | ||
50 | </div> | ||
51 | |||
52 | <div class="form-group inputs"> | ||
53 | <input | ||
54 | type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" | ||
55 | (click)="hide()" (key.enter)="hide()" | ||
56 | > | ||
57 | <input type="submit" i18n-value value="Submit" class="action-button-submit" [disabled]="!form.valid"> | ||
58 | </div> | ||
59 | |||
60 | </form> | ||
61 | </div> | ||
62 | </ng-template> | ||
diff --git a/client/src/app/shared/shared-moderation/comment-report.component.scss b/client/src/app/shared/shared-moderation/comment-report.component.scss new file mode 100644 index 000000000..17a33d3a2 --- /dev/null +++ b/client/src/app/shared/shared-moderation/comment-report.component.scss | |||
@@ -0,0 +1,11 @@ | |||
1 | @import 'variables'; | ||
2 | @import 'mixins'; | ||
3 | |||
4 | .information { | ||
5 | margin-bottom: 20px; | ||
6 | } | ||
7 | |||
8 | textarea { | ||
9 | @include peertube-textarea(100%, 100px); | ||
10 | } | ||
11 | |||
diff --git a/client/src/app/shared/shared-moderation/comment-report.component.ts b/client/src/app/shared/shared-moderation/comment-report.component.ts new file mode 100644 index 000000000..5db4b2dc1 --- /dev/null +++ b/client/src/app/shared/shared-moderation/comment-report.component.ts | |||
@@ -0,0 +1,93 @@ | |||
1 | import { mapValues, pickBy } from 'lodash-es' | ||
2 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | ||
3 | import { SafeHtml } from '@angular/platform-browser' | ||
4 | import { VideoComment } from '@app/+videos/+video-watch/comment/video-comment.model' | ||
5 | import { Notifier } from '@app/core' | ||
6 | import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' | ||
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
10 | import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models' | ||
11 | import { AbuseService } from './abuse.service' | ||
12 | |||
13 | @Component({ | ||
14 | selector: 'my-comment-report', | ||
15 | templateUrl: './comment-report.component.html', | ||
16 | styleUrls: [ './comment-report.component.scss' ] | ||
17 | }) | ||
18 | export class CommentReportComponent extends FormReactive implements OnInit { | ||
19 | @Input() comment: VideoComment = null | ||
20 | |||
21 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
22 | |||
23 | error: string = null | ||
24 | predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] | ||
25 | embedHtml: SafeHtml | ||
26 | |||
27 | private openedModal: NgbModalRef | ||
28 | |||
29 | constructor ( | ||
30 | protected formValidatorService: FormValidatorService, | ||
31 | private modalService: NgbModal, | ||
32 | private abuseValidatorsService: AbuseValidatorsService, | ||
33 | private abuseService: AbuseService, | ||
34 | private notifier: Notifier, | ||
35 | private i18n: I18n | ||
36 | ) { | ||
37 | super() | ||
38 | } | ||
39 | |||
40 | get currentHost () { | ||
41 | return window.location.host | ||
42 | } | ||
43 | |||
44 | get originHost () { | ||
45 | if (this.isRemoteComment()) { | ||
46 | return this.comment.account.host | ||
47 | } | ||
48 | |||
49 | return '' | ||
50 | } | ||
51 | |||
52 | ngOnInit () { | ||
53 | this.buildForm({ | ||
54 | reason: this.abuseValidatorsService.ABUSE_REASON, | ||
55 | predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null) | ||
56 | }) | ||
57 | |||
58 | this.predefinedReasons = this.abuseService.getPrefefinedReasons('comment') | ||
59 | } | ||
60 | |||
61 | show () { | ||
62 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' }) | ||
63 | } | ||
64 | |||
65 | hide () { | ||
66 | this.openedModal.close() | ||
67 | this.openedModal = null | ||
68 | } | ||
69 | |||
70 | report () { | ||
71 | const reason = this.form.get('reason').value | ||
72 | const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[] | ||
73 | |||
74 | this.abuseService.reportVideo({ | ||
75 | reason, | ||
76 | predefinedReasons, | ||
77 | comment: { | ||
78 | id: this.comment.id | ||
79 | } | ||
80 | }).subscribe( | ||
81 | () => { | ||
82 | this.notifier.success(this.i18n('Comment reported.')) | ||
83 | this.hide() | ||
84 | }, | ||
85 | |||
86 | err => this.notifier.error(err.message) | ||
87 | ) | ||
88 | } | ||
89 | |||
90 | isRemoteComment () { | ||
91 | return !this.comment.isLocal | ||
92 | } | ||
93 | } | ||
diff --git a/client/src/app/shared/shared-moderation/shared-moderation.module.ts b/client/src/app/shared/shared-moderation/shared-moderation.module.ts index 742193e58..ff4021a33 100644 --- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts +++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts | |||
@@ -12,6 +12,7 @@ import { AbuseService } from './abuse.service' | |||
12 | import { VideoBlockComponent } from './video-block.component' | 12 | import { VideoBlockComponent } from './video-block.component' |
13 | import { VideoBlockService } from './video-block.service' | 13 | import { VideoBlockService } from './video-block.service' |
14 | import { VideoReportComponent } from './video-report.component' | 14 | import { VideoReportComponent } from './video-report.component' |
15 | import { CommentReportComponent } from './comment-report.component' | ||
15 | 16 | ||
16 | @NgModule({ | 17 | @NgModule({ |
17 | imports: [ | 18 | imports: [ |
@@ -25,7 +26,8 @@ import { VideoReportComponent } from './video-report.component' | |||
25 | UserModerationDropdownComponent, | 26 | UserModerationDropdownComponent, |
26 | VideoBlockComponent, | 27 | VideoBlockComponent, |
27 | VideoReportComponent, | 28 | VideoReportComponent, |
28 | BatchDomainsModalComponent | 29 | BatchDomainsModalComponent, |
30 | CommentReportComponent | ||
29 | ], | 31 | ], |
30 | 32 | ||
31 | exports: [ | 33 | exports: [ |
@@ -33,7 +35,8 @@ import { VideoReportComponent } from './video-report.component' | |||
33 | UserModerationDropdownComponent, | 35 | UserModerationDropdownComponent, |
34 | VideoBlockComponent, | 36 | VideoBlockComponent, |
35 | VideoReportComponent, | 37 | VideoReportComponent, |
36 | BatchDomainsModalComponent | 38 | BatchDomainsModalComponent, |
39 | CommentReportComponent | ||
37 | ], | 40 | ], |
38 | 41 | ||
39 | providers: [ | 42 | providers: [ |
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts index d3c37f082..78c2658df 100644 --- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts | |||
@@ -16,6 +16,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
16 | 16 | ||
17 | @Input() user: User | 17 | @Input() user: User |
18 | @Input() account: Account | 18 | @Input() account: Account |
19 | @Input() prependActions: DropdownAction<{ user: User, account: Account }>[] | ||
19 | 20 | ||
20 | @Input() buttonSize: 'normal' | 'small' = 'normal' | 21 | @Input() buttonSize: 'normal' | 'small' = 'normal' |
21 | @Input() placement = 'left-top left-bottom auto' | 22 | @Input() placement = 'left-top left-bottom auto' |
@@ -250,6 +251,12 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
250 | private buildActions () { | 251 | private buildActions () { |
251 | this.userActions = [] | 252 | this.userActions = [] |
252 | 253 | ||
254 | if (this.prependActions) { | ||
255 | this.userActions = [ | ||
256 | this.prependActions | ||
257 | ] | ||
258 | } | ||
259 | |||
253 | if (this.authService.isLoggedIn()) { | 260 | if (this.authService.isLoggedIn()) { |
254 | const authUser = this.authService.getUser() | 261 | const authUser = this.authService.getUser() |
255 | 262 | ||
diff --git a/client/src/app/shared/shared-moderation/video-report.component.html b/client/src/app/shared/shared-moderation/video-report.component.html index d6beb6d2a..b724ecb18 100644 --- a/client/src/app/shared/shared-moderation/video-report.component.html +++ b/client/src/app/shared/shared-moderation/video-report.component.html | |||
@@ -14,16 +14,19 @@ | |||
14 | 14 | ||
15 | <div class="ml-2 mt-2 d-flex flex-column"> | 15 | <div class="ml-2 mt-2 d-flex flex-column"> |
16 | <ng-container formGroupName="predefinedReasons"> | 16 | <ng-container formGroupName="predefinedReasons"> |
17 | |||
17 | <div class="form-group" *ngFor="let reason of predefinedReasons"> | 18 | <div class="form-group" *ngFor="let reason of predefinedReasons"> |
18 | <my-peertube-checkbox formControlName="{{reason.id}}" labelText="{{reason.label}}"> | 19 | <my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label"> |
19 | <ng-template *ngIf="reason.help" ptTemplate="help"> | 20 | <ng-template *ngIf="reason.help" ptTemplate="help"> |
20 | <div [innerHTML]="reason.help"></div> | 21 | <div [innerHTML]="reason.help"></div> |
21 | </ng-template> | 22 | </ng-template> |
23 | |||
22 | <ng-container *ngIf="reason.description" ngProjectAs="description"> | 24 | <ng-container *ngIf="reason.description" ngProjectAs="description"> |
23 | <div [innerHTML]="reason.description"></div> | 25 | <div [innerHTML]="reason.description"></div> |
24 | </ng-container> | 26 | </ng-container> |
25 | </my-peertube-checkbox> | 27 | </my-peertube-checkbox> |
26 | </div> | 28 | </div> |
29 | |||
27 | </ng-container> | 30 | </ng-container> |
28 | </div> | 31 | </div> |
29 | 32 | ||
@@ -73,7 +76,7 @@ | |||
73 | </div> | 76 | </div> |
74 | 77 | ||
75 | <div class="form-group"> | 78 | <div class="form-group"> |
76 | <textarea | 79 | <textarea |
77 | i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus | 80 | i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus |
78 | [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control" | 81 | [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control" |
79 | ></textarea> | 82 | ></textarea> |
diff --git a/client/src/app/shared/shared-moderation/video-report.component.ts b/client/src/app/shared/shared-moderation/video-report.component.ts index 7977e4cca..26e7b62ba 100644 --- a/client/src/app/shared/shared-moderation/video-report.component.ts +++ b/client/src/app/shared/shared-moderation/video-report.component.ts | |||
@@ -79,48 +79,7 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
79 | } | 79 | } |
80 | }) | 80 | }) |
81 | 81 | ||
82 | this.predefinedReasons = [ | 82 | this.predefinedReasons = this.abuseService.getPrefefinedReasons('video') |
83 | { | ||
84 | id: 'violentOrRepulsive', | ||
85 | label: this.i18n('Violent or repulsive'), | ||
86 | help: this.i18n('Contains offensive, violent, or coarse language or iconography.') | ||
87 | }, | ||
88 | { | ||
89 | id: 'hatefulOrAbusive', | ||
90 | label: this.i18n('Hateful or abusive'), | ||
91 | help: this.i18n('Contains abusive, racist or sexist language or iconography.') | ||
92 | }, | ||
93 | { | ||
94 | id: 'spamOrMisleading', | ||
95 | label: this.i18n('Spam, ad or false news'), | ||
96 | help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.') | ||
97 | }, | ||
98 | { | ||
99 | id: 'privacy', | ||
100 | label: this.i18n('Privacy breach or doxxing'), | ||
101 | help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).') | ||
102 | }, | ||
103 | { | ||
104 | id: 'rights', | ||
105 | label: this.i18n('Intellectual property violation'), | ||
106 | help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.') | ||
107 | }, | ||
108 | { | ||
109 | id: 'serverRules', | ||
110 | label: this.i18n('Breaks server rules'), | ||
111 | description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.') | ||
112 | }, | ||
113 | { | ||
114 | id: 'thumbnails', | ||
115 | label: this.i18n('Thumbnails'), | ||
116 | help: this.i18n('The above can only be seen in thumbnails.') | ||
117 | }, | ||
118 | { | ||
119 | id: 'captions', | ||
120 | label: this.i18n('Captions'), | ||
121 | help: this.i18n('The above can only be seen in captions (please describe which).') | ||
122 | } | ||
123 | ] | ||
124 | 83 | ||
125 | this.embedHtml = this.getVideoEmbed() | 84 | this.embedHtml = this.getVideoEmbed() |
126 | } | 85 | } |