diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-06-22 13:00:39 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-22 13:00:39 +0200 |
commit | 1ebddadd0704812a4600c39cabe2268321e88331 (patch) | |
tree | 1cc8560e5b63e9976aa5411ba800a62cfe7b8ea9 /client/src | |
parent | 07aea1a2642fc9868cb01e30c322514029d5b95a (diff) | |
download | PeerTube-1ebddadd0704812a4600c39cabe2268321e88331.tar.gz PeerTube-1ebddadd0704812a4600c39cabe2268321e88331.tar.zst PeerTube-1ebddadd0704812a4600c39cabe2268321e88331.zip |
predefined report reasons & improved reporter UI (#2842)
- added `startAt` and `endAt` optional timestamps to help pin down reported sections of a video
- added predefined report reasons
- added video player with report modal
Diffstat (limited to 'client/src')
17 files changed, 302 insertions, 61 deletions
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss index ba68cf6f6..0ec420af9 100644 --- a/client/src/app/+admin/moderation/moderation.component.scss +++ b/client/src/app/+admin/moderation/moderation.component.scss | |||
@@ -42,6 +42,20 @@ | |||
42 | } | 42 | } |
43 | } | 43 | } |
44 | 44 | ||
45 | p-calendar { | ||
46 | display: block; | ||
47 | |||
48 | ::ng-deep { | ||
49 | .ui-widget-content { | ||
50 | min-width: 400px; | ||
51 | } | ||
52 | |||
53 | input { | ||
54 | @include peertube-input-text(100%); | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | |||
45 | .screenratio { | 59 | .screenratio { |
46 | div { | 60 | div { |
47 | @include miniature-thumbnail; | 61 | @include miniature-thumbnail; |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html index 453a282d1..5512bb1de 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html | |||
@@ -57,6 +57,22 @@ | |||
57 | <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span> | 57 | <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span> |
58 | </div> | 58 | </div> |
59 | 59 | ||
60 | <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex"> | ||
61 | <span class="col-3"></span> | ||
62 | <span class="col-9"> | ||
63 | <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" *ngFor="let reason of getPredefinedReasons()"> | ||
64 | <div>{{ reason.label }}</div> | ||
65 | </a> | ||
66 | </span> | ||
67 | </div> | ||
68 | |||
69 | <div *ngIf="videoAbuse.startAt" class="mt-2 d-flex"> | ||
70 | <span class="col-3 moderation-expanded-label" i18n>Reported part</span> | ||
71 | <span class="col-9"> | ||
72 | {{ startAt }}<ng-container *ngIf="videoAbuse.endAt"> - {{ endAt }}</ng-container> | ||
73 | </span> | ||
74 | </div> | ||
75 | |||
60 | <div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment"> | 76 | <div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment"> |
61 | <span class="col-3 moderation-expanded-label" i18n>Note</span> | 77 | <span class="col-3 moderation-expanded-label" i18n>Note</span> |
62 | <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span> | 78 | <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span> |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts index d9cb19845..13485124f 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { Account } from '@app/shared/account/account.model' | ||
3 | import { Actor } from '@app/shared/actor/actor.model' | 2 | import { Actor } from '@app/shared/actor/actor.model' |
3 | import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model' | ||
4 | import { ProcessedVideoAbuse } from './video-abuse-list.component' | 4 | import { ProcessedVideoAbuse } from './video-abuse-list.component' |
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
6 | import { durationToString } from '@app/shared/misc/utils' | ||
5 | 7 | ||
6 | @Component({ | 8 | @Component({ |
7 | selector: 'my-video-abuse-details', | 9 | selector: 'my-video-abuse-details', |
@@ -11,6 +13,39 @@ import { ProcessedVideoAbuse } from './video-abuse-list.component' | |||
11 | export class VideoAbuseDetailsComponent { | 13 | export class VideoAbuseDetailsComponent { |
12 | @Input() videoAbuse: ProcessedVideoAbuse | 14 | @Input() videoAbuse: ProcessedVideoAbuse |
13 | 15 | ||
16 | private predefinedReasonsTranslations: { [key in VideoAbusePredefinedReasonsString]: string } | ||
17 | |||
18 | constructor ( | ||
19 | private i18n: I18n | ||
20 | ) { | ||
21 | this.predefinedReasonsTranslations = { | ||
22 | violentOrRepulsive: this.i18n('Violent or Repulsive'), | ||
23 | hatefulOrAbusive: this.i18n('Hateful or Abusive'), | ||
24 | spamOrMisleading: this.i18n('Spam or Misleading'), | ||
25 | privacy: this.i18n('Privacy'), | ||
26 | rights: this.i18n('Rights'), | ||
27 | serverRules: this.i18n('Server rules'), | ||
28 | thumbnails: this.i18n('Thumbnails'), | ||
29 | captions: this.i18n('Captions') | ||
30 | } | ||
31 | } | ||
32 | |||
33 | get startAt () { | ||
34 | return durationToString(this.videoAbuse.startAt) | ||
35 | } | ||
36 | |||
37 | get endAt () { | ||
38 | return durationToString(this.videoAbuse.endAt) | ||
39 | } | ||
40 | |||
41 | getPredefinedReasons () { | ||
42 | if (!this.videoAbuse.predefinedReasons) return [] | ||
43 | return this.videoAbuse.predefinedReasons.map(r => ({ | ||
44 | id: r, | ||
45 | label: this.predefinedReasonsTranslations[r] | ||
46 | })) | ||
47 | } | ||
48 | |||
14 | switchToDefaultAvatar ($event: Event) { | 49 | switchToDefaultAvatar ($event: Event) { |
15 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() | 50 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() |
16 | } | 51 | } |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts index a36acc2ab..d7f5beef3 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts | |||
@@ -11,13 +11,13 @@ import { ModerationCommentModalComponent } from './moderation-comment-modal.comp | |||
11 | import { Video } from '../../../shared/video/video.model' | 11 | import { Video } from '../../../shared/video/video.model' |
12 | import { MarkdownService } from '@app/shared/renderer' | 12 | import { MarkdownService } from '@app/shared/renderer' |
13 | import { Actor } from '@app/shared/actor/actor.model' | 13 | import { Actor } from '@app/shared/actor/actor.model' |
14 | import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils' | 14 | import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' |
15 | import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' | ||
16 | import { DomSanitizer } from '@angular/platform-browser' | 15 | import { DomSanitizer } from '@angular/platform-browser' |
17 | import { BlocklistService } from '@app/shared/blocklist' | 16 | import { BlocklistService } from '@app/shared/blocklist' |
18 | import { VideoService } from '@app/shared/video/video.service' | 17 | import { VideoService } from '@app/shared/video/video.service' |
19 | import { ActivatedRoute, Params, Router } from '@angular/router' | 18 | import { ActivatedRoute, Params, Router } from '@angular/router' |
20 | import { filter } from 'rxjs/operators' | 19 | import { filter } from 'rxjs/operators' |
20 | import { environment } from 'src/environments/environment' | ||
21 | 21 | ||
22 | export type ProcessedVideoAbuse = VideoAbuse & { | 22 | export type ProcessedVideoAbuse = VideoAbuse & { |
23 | moderationCommentHtml?: string, | 23 | moderationCommentHtml?: string, |
@@ -259,12 +259,15 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV | |||
259 | } | 259 | } |
260 | 260 | ||
261 | getVideoEmbed (videoAbuse: VideoAbuse) { | 261 | getVideoEmbed (videoAbuse: VideoAbuse) { |
262 | const absoluteAPIUrl = getAbsoluteAPIUrl() | 262 | return buildVideoEmbed( |
263 | const embedUrl = buildVideoLink({ | 263 | buildVideoLink({ |
264 | baseUrl: absoluteAPIUrl + '/videos/embed/' + videoAbuse.video.uuid, | 264 | baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`, |
265 | warningTitle: false | 265 | title: false, |
266 | }) | 266 | warningTitle: false, |
267 | return buildVideoEmbed(embedUrl) | 267 | startTime: videoAbuse.startAt, |
268 | stopTime: videoAbuse.endAt | ||
269 | }) | ||
270 | ) | ||
268 | } | 271 | } |
269 | 272 | ||
270 | switchToDefaultAvatar ($event: Event) { | 273 | switchToDefaultAvatar ($event: Event) { |
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts index cd6db1f3c..78558851a 100644 --- a/client/src/app/shared/rest/rest.service.ts +++ b/client/src/app/shared/rest/rest.service.ts | |||
@@ -46,7 +46,7 @@ export class RestService { | |||
46 | addObjectParams (params: HttpParams, object: { [ name: string ]: any }) { | 46 | addObjectParams (params: HttpParams, object: { [ name: string ]: any }) { |
47 | for (const name of Object.keys(object)) { | 47 | for (const name of Object.keys(object)) { |
48 | const value = object[name] | 48 | const value = object[name] |
49 | if (!value) continue | 49 | if (value === undefined || value === null) continue |
50 | 50 | ||
51 | if (Array.isArray(value) && value.length !== 0) { | 51 | if (Array.isArray(value) && value.length !== 0) { |
52 | for (const v of value) params = params.append(name, v) | 52 | for (const v of value) params = params.append(name, v) |
@@ -93,7 +93,7 @@ export class RestService { | |||
93 | 93 | ||
94 | return t | 94 | return t |
95 | }) | 95 | }) |
96 | .filter(t => !!t) | 96 | .filter(t => !!t || t === 0) |
97 | 97 | ||
98 | if (matchedTokens.length === 0) continue | 98 | if (matchedTokens.length === 0) continue |
99 | 99 | ||
@@ -103,7 +103,7 @@ export class RestService { | |||
103 | } | 103 | } |
104 | 104 | ||
105 | return { | 105 | return { |
106 | search: searchTokens.join(' '), | 106 | search: searchTokens.join(' ') || undefined, |
107 | 107 | ||
108 | ...additionalFilters | 108 | ...additionalFilters |
109 | } | 109 | } |
diff --git a/client/src/app/shared/video-abuse/video-abuse.service.ts b/client/src/app/shared/video-abuse/video-abuse.service.ts index 700a30239..43f4674b1 100644 --- a/client/src/app/shared/video-abuse/video-abuse.service.ts +++ b/client/src/app/shared/video-abuse/video-abuse.service.ts | |||
@@ -3,9 +3,10 @@ import { HttpClient, HttpParams } from '@angular/common/http' | |||
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { SortMeta } from 'primeng/api' | 4 | import { SortMeta } from 'primeng/api' |
5 | import { Observable } from 'rxjs' | 5 | import { Observable } from 'rxjs' |
6 | import { ResultList, VideoAbuse, VideoAbuseUpdate, VideoAbuseState } from '../../../../../shared' | 6 | import { ResultList, VideoAbuse, VideoAbuseCreate, VideoAbuseState, VideoAbuseUpdate } from '../../../../../shared' |
7 | import { environment } from '../../../environments/environment' | 7 | import { environment } from '../../../environments/environment' |
8 | import { RestExtractor, RestPagination, RestService } from '../rest' | 8 | import { RestExtractor, RestPagination, RestService } from '../rest' |
9 | import { omit } from 'lodash-es' | ||
9 | 10 | ||
10 | @Injectable() | 11 | @Injectable() |
11 | export class VideoAbuseService { | 12 | export class VideoAbuseService { |
@@ -51,7 +52,8 @@ export class VideoAbuseService { | |||
51 | } | 52 | } |
52 | }, | 53 | }, |
53 | searchReporter: { prefix: 'reporter:' }, | 54 | searchReporter: { prefix: 'reporter:' }, |
54 | searchReportee: { prefix: 'reportee:' } | 55 | searchReportee: { prefix: 'reportee:' }, |
56 | predefinedReason: { prefix: 'tag:' } | ||
55 | }) | 57 | }) |
56 | 58 | ||
57 | params = this.restService.addObjectParams(params, filters) | 59 | params = this.restService.addObjectParams(params, filters) |
@@ -63,9 +65,10 @@ export class VideoAbuseService { | |||
63 | ) | 65 | ) |
64 | } | 66 | } |
65 | 67 | ||
66 | reportVideo (id: number, reason: string) { | 68 | reportVideo (parameters: { id: number } & VideoAbuseCreate) { |
67 | const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse' | 69 | const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + parameters.id + '/abuse' |
68 | const body = { reason } | 70 | |
71 | const body = omit(parameters, [ 'id' ]) | ||
69 | 72 | ||
70 | return this.authHttp.post(url, body) | 73 | return this.authHttp.post(url, body) |
71 | .pipe( | 74 | .pipe( |
diff --git a/client/src/app/shared/video/modals/video-block.component.html b/client/src/app/shared/video/modals/video-block.component.html index a8dd30b5e..5e73d66c5 100644 --- a/client/src/app/shared/video/modals/video-block.component.html +++ b/client/src/app/shared/video/modals/video-block.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Blocklist video</h4> | 3 | <h4 i18n class="modal-title">Block video "{{ video.name }}"</h4> |
4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
@@ -9,7 +9,7 @@ | |||
9 | <form novalidate [formGroup]="form" (ngSubmit)="block()"> | 9 | <form novalidate [formGroup]="form" (ngSubmit)="block()"> |
10 | <div class="form-group"> | 10 | <div class="form-group"> |
11 | <textarea | 11 | <textarea |
12 | i18n-placeholder placeholder="Reason..." formControlName="reason" | 12 | i18n-placeholder placeholder="Please describe the reason..." formControlName="reason" |
13 | [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control" | 13 | [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control" |
14 | ></textarea> | 14 | ></textarea> |
15 | <div *ngIf="formErrors.reason" class="form-error"> | 15 | <div *ngIf="formErrors.reason" class="form-error"> |
diff --git a/client/src/app/shared/video/modals/video-report.component.html b/client/src/app/shared/video/modals/video-report.component.html index e336b6660..d6beb6d2a 100644 --- a/client/src/app/shared/video/modals/video-report.component.html +++ b/client/src/app/shared/video/modals/video-report.component.html | |||
@@ -1,38 +1,97 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Report video</h4> | 3 | <h4 i18n class="modal-title">Report video "{{ video.name }}"</h4> |
4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
8 | <form novalidate [formGroup]="form" (ngSubmit)="report()"> | ||
8 | 9 | ||
9 | <div i18n class="information"> | 10 | <div class="row"> |
10 | Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemoteVideo()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>. | 11 | <div class="col-5 form-group"> |
11 | </div> | 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 | <div class="form-group" *ngFor="let reason of predefinedReasons"> | ||
18 | <my-peertube-checkbox formControlName="{{reason.id}}" labelText="{{reason.label}}"> | ||
19 | <ng-template *ngIf="reason.help" ptTemplate="help"> | ||
20 | <div [innerHTML]="reason.help"></div> | ||
21 | </ng-template> | ||
22 | <ng-container *ngIf="reason.description" ngProjectAs="description"> | ||
23 | <div [innerHTML]="reason.description"></div> | ||
24 | </ng-container> | ||
25 | </my-peertube-checkbox> | ||
26 | </div> | ||
27 | </ng-container> | ||
28 | </div> | ||
12 | 29 | ||
13 | <form novalidate [formGroup]="form" (ngSubmit)="report()"> | ||
14 | <div class="form-group"> | ||
15 | <textarea | ||
16 | i18n-placeholder placeholder="Reason..." formControlName="reason" | ||
17 | [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control" | ||
18 | ></textarea> | ||
19 | <div *ngIf="formErrors.reason" class="form-error"> | ||
20 | {{ formErrors.reason }} | ||
21 | </div> | ||
22 | </div> | 30 | </div> |
23 | 31 | ||
24 | <div class="form-group inputs"> | 32 | <div class="col-7"> |
25 | <input | 33 | <div class="row justify-content-center"> |
26 | type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" | 34 | <div class="col-12 col-lg-9 mb-2"> |
27 | (click)="hide()" (key.enter)="hide()" | 35 | <div class="screenratio"> |
28 | > | 36 | <div [innerHTML]="embedHtml"></div> |
37 | </div> | ||
38 | </div> | ||
39 | </div> | ||
40 | |||
41 | <div class="mb-1 start-at" formGroupName="timestamp"> | ||
42 | <my-peertube-checkbox | ||
43 | formControlName="hasStart" | ||
44 | i18n-labelText labelText="Start at" | ||
45 | ></my-peertube-checkbox> | ||
46 | |||
47 | <my-timestamp-input | ||
48 | [timestamp]="timestamp.startAt" | ||
49 | [maxTimestamp]="video.duration" | ||
50 | formControlName="startAt" | ||
51 | inputName="startAt" | ||
52 | > | ||
53 | </my-timestamp-input> | ||
54 | </div> | ||
55 | |||
56 | <div class="mb-3 stop-at" formGroupName="timestamp" *ngIf="timestamp.hasStart"> | ||
57 | <my-peertube-checkbox | ||
58 | formControlName="hasEnd" | ||
59 | i18n-labelText labelText="Stop at" | ||
60 | ></my-peertube-checkbox> | ||
29 | 61 | ||
30 | <input | 62 | <my-timestamp-input |
31 | type="submit" i18n-value value="Submit" class="action-button-submit" | 63 | [timestamp]="timestamp.endAt" |
32 | [disabled]="!form.valid" | 64 | [maxTimestamp]="video.duration" |
33 | > | 65 | formControlName="endAt" |
66 | inputName="endAt" | ||
67 | > | ||
68 | </my-timestamp-input> | ||
69 | </div> | ||
70 | |||
71 | <div i18n class="information"> | ||
72 | Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemoteVideo()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>. | ||
73 | </div> | ||
74 | |||
75 | <div class="form-group"> | ||
76 | <textarea | ||
77 | i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus | ||
78 | [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control" | ||
79 | ></textarea> | ||
80 | <div *ngIf="formErrors.reason" class="form-error"> | ||
81 | {{ formErrors.reason }} | ||
82 | </div> | ||
83 | </div> | ||
34 | </div> | 84 | </div> |
35 | </form> | 85 | </div> |
36 | 86 | ||
87 | <div class="form-group inputs"> | ||
88 | <input | ||
89 | type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" | ||
90 | (click)="hide()" (key.enter)="hide()" | ||
91 | > | ||
92 | <input type="submit" i18n-value value="Submit" class="action-button-submit" [disabled]="!form.valid"> | ||
93 | </div> | ||
94 | |||
95 | </form> | ||
37 | </div> | 96 | </div> |
38 | </ng-template> | 97 | </ng-template> |
diff --git a/client/src/app/shared/video/modals/video-report.component.scss b/client/src/app/shared/video/modals/video-report.component.scss index 4713660a2..b2606cbd8 100644 --- a/client/src/app/shared/video/modals/video-report.component.scss +++ b/client/src/app/shared/video/modals/video-report.component.scss | |||
@@ -8,3 +8,20 @@ | |||
8 | textarea { | 8 | textarea { |
9 | @include peertube-textarea(100%, 100px); | 9 | @include peertube-textarea(100%, 100px); |
10 | } | 10 | } |
11 | |||
12 | .start-at, | ||
13 | .stop-at { | ||
14 | width: 300px; | ||
15 | display: flex; | ||
16 | align-items: center; | ||
17 | |||
18 | my-timestamp-input { | ||
19 | margin-left: 10px; | ||
20 | } | ||
21 | } | ||
22 | |||
23 | .screenratio { | ||
24 | @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { | ||
25 | left: 0; | ||
26 | }; | ||
27 | } | ||
diff --git a/client/src/app/shared/video/modals/video-report.component.ts b/client/src/app/shared/video/modals/video-report.component.ts index 988fa03d4..c2d441bba 100644 --- a/client/src/app/shared/video/modals/video-report.component.ts +++ b/client/src/app/shared/video/modals/video-report.component.ts | |||
@@ -8,6 +8,10 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | |||
8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
9 | import { VideoAbuseService } from '@app/shared/video-abuse' | 9 | import { VideoAbuseService } from '@app/shared/video-abuse' |
10 | import { Video } from '@app/shared/video/video.model' | 10 | import { Video } from '@app/shared/video/video.model' |
11 | import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' | ||
12 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' | ||
13 | import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model' | ||
14 | import { mapValues, pickBy } from 'lodash-es' | ||
11 | 15 | ||
12 | @Component({ | 16 | @Component({ |
13 | selector: 'my-video-report', | 17 | selector: 'my-video-report', |
@@ -20,6 +24,8 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
20 | @ViewChild('modal', { static: true }) modal: NgbModal | 24 | @ViewChild('modal', { static: true }) modal: NgbModal |
21 | 25 | ||
22 | error: string = null | 26 | error: string = null |
27 | predefinedReasons: { id: VideoAbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] | ||
28 | embedHtml: SafeHtml | ||
23 | 29 | ||
24 | private openedModal: NgbModalRef | 30 | private openedModal: NgbModalRef |
25 | 31 | ||
@@ -29,6 +35,7 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
29 | private videoAbuseValidatorsService: VideoAbuseValidatorsService, | 35 | private videoAbuseValidatorsService: VideoAbuseValidatorsService, |
30 | private videoAbuseService: VideoAbuseService, | 36 | private videoAbuseService: VideoAbuseService, |
31 | private notifier: Notifier, | 37 | private notifier: Notifier, |
38 | private sanitizer: DomSanitizer, | ||
32 | private i18n: I18n | 39 | private i18n: I18n |
33 | ) { | 40 | ) { |
34 | super() | 41 | super() |
@@ -46,14 +53,82 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
46 | return '' | 53 | return '' |
47 | } | 54 | } |
48 | 55 | ||
56 | get timestamp () { | ||
57 | return this.form.get('timestamp').value | ||
58 | } | ||
59 | |||
60 | getVideoEmbed () { | ||
61 | return this.sanitizer.bypassSecurityTrustHtml( | ||
62 | buildVideoEmbed( | ||
63 | buildVideoLink({ | ||
64 | baseUrl: this.video.embedUrl, | ||
65 | title: false, | ||
66 | warningTitle: false | ||
67 | }) | ||
68 | ) | ||
69 | ) | ||
70 | } | ||
71 | |||
49 | ngOnInit () { | 72 | ngOnInit () { |
50 | this.buildForm({ | 73 | this.buildForm({ |
51 | reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON | 74 | reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON, |
75 | predefinedReasons: mapValues(videoAbusePredefinedReasonsMap, r => null), | ||
76 | timestamp: { | ||
77 | hasStart: null, | ||
78 | startAt: null, | ||
79 | hasEnd: null, | ||
80 | endAt: null | ||
81 | } | ||
52 | }) | 82 | }) |
83 | |||
84 | this.predefinedReasons = [ | ||
85 | { | ||
86 | id: 'violentOrRepulsive', | ||
87 | label: this.i18n('Violent or repulsive'), | ||
88 | help: this.i18n('Contains offensive, violent, or coarse language or iconography.') | ||
89 | }, | ||
90 | { | ||
91 | id: 'hatefulOrAbusive', | ||
92 | label: this.i18n('Hateful or abusive'), | ||
93 | help: this.i18n('Contains abusive, racist or sexist language or iconography.') | ||
94 | }, | ||
95 | { | ||
96 | id: 'spamOrMisleading', | ||
97 | label: this.i18n('Spam, ad or false news'), | ||
98 | help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.') | ||
99 | }, | ||
100 | { | ||
101 | id: 'privacy', | ||
102 | label: this.i18n('Privacy breach or doxxing'), | ||
103 | 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).') | ||
104 | }, | ||
105 | { | ||
106 | id: 'rights', | ||
107 | label: this.i18n('Intellectual property violation'), | ||
108 | help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.') | ||
109 | }, | ||
110 | { | ||
111 | id: 'serverRules', | ||
112 | label: this.i18n('Breaks server rules'), | ||
113 | 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.') | ||
114 | }, | ||
115 | { | ||
116 | id: 'thumbnails', | ||
117 | label: this.i18n('Thumbnails'), | ||
118 | help: this.i18n('The above can only be seen in thumbnails.') | ||
119 | }, | ||
120 | { | ||
121 | id: 'captions', | ||
122 | label: this.i18n('Captions'), | ||
123 | help: this.i18n('The above can only be seen in captions (please describe which).') | ||
124 | } | ||
125 | ] | ||
126 | |||
127 | this.embedHtml = this.getVideoEmbed() | ||
53 | } | 128 | } |
54 | 129 | ||
55 | show () { | 130 | show () { |
56 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false }) | 131 | this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' }) |
57 | } | 132 | } |
58 | 133 | ||
59 | hide () { | 134 | hide () { |
@@ -62,17 +137,24 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
62 | } | 137 | } |
63 | 138 | ||
64 | report () { | 139 | report () { |
65 | const reason = this.form.value['reason'] | 140 | const reason = this.form.get('reason').value |
141 | const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as VideoAbusePredefinedReasonsString[] | ||
142 | const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value | ||
66 | 143 | ||
67 | this.videoAbuseService.reportVideo(this.video.id, reason) | 144 | this.videoAbuseService.reportVideo({ |
68 | .subscribe( | 145 | id: this.video.id, |
69 | () => { | 146 | reason, |
70 | this.notifier.success(this.i18n('Video reported.')) | 147 | predefinedReasons, |
71 | this.hide() | 148 | startAt: hasStart && startAt ? startAt : undefined, |
72 | }, | 149 | endAt: hasEnd && endAt ? endAt : undefined |
150 | }).subscribe( | ||
151 | () => { | ||
152 | this.notifier.success(this.i18n('Video reported.')) | ||
153 | this.hide() | ||
154 | }, | ||
73 | 155 | ||
74 | err => this.notifier.error(err.message) | 156 | err => this.notifier.error(err.message) |
75 | ) | 157 | ) |
76 | } | 158 | } |
77 | 159 | ||
78 | isRemoteVideo () { | 160 | isRemoteVideo () { |
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 16e43cbd8..dc5f45626 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -7,6 +7,7 @@ import { peertubeTranslate, ServerConfig } from '../../../../../shared/models' | |||
7 | import { Actor } from '@app/shared/actor/actor.model' | 7 | import { Actor } from '@app/shared/actor/actor.model' |
8 | import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' | 8 | import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' |
9 | import { AuthUser } from '@app/core' | 9 | import { AuthUser } from '@app/core' |
10 | import { environment } from '../../../environments/environment' | ||
10 | 11 | ||
11 | export class Video implements VideoServerModel { | 12 | export class Video implements VideoServerModel { |
12 | byVideoChannel: string | 13 | byVideoChannel: string |
@@ -111,7 +112,7 @@ export class Video implements VideoServerModel { | |||
111 | this.previewUrl = hash.previewUrl || (absoluteAPIUrl + hash.previewPath) | 112 | this.previewUrl = hash.previewUrl || (absoluteAPIUrl + hash.previewPath) |
112 | 113 | ||
113 | this.embedPath = hash.embedPath | 114 | this.embedPath = hash.embedPath |
114 | this.embedUrl = hash.embedUrl || (absoluteAPIUrl + hash.embedPath) | 115 | this.embedUrl = hash.embedUrl || (environment.embedUrl + hash.embedPath) |
115 | 116 | ||
116 | this.url = hash.url | 117 | this.url = hash.url |
117 | 118 | ||
diff --git a/client/src/environments/environment.e2e.ts b/client/src/environments/environment.e2e.ts index 7c00e8d4f..7724d27c9 100644 --- a/client/src/environments/environment.e2e.ts +++ b/client/src/environments/environment.e2e.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export const environment = { | 1 | export const environment = { |
2 | production: false, | 2 | production: false, |
3 | hmr: false, | 3 | hmr: false, |
4 | apiUrl: 'http://localhost:9001' | 4 | apiUrl: 'http://localhost:9001', |
5 | embedUrl: 'http://localhost:9001/videos/embed' | ||
5 | } | 6 | } |
diff --git a/client/src/environments/environment.hmr.ts b/client/src/environments/environment.hmr.ts index 853e20803..72eed45e5 100644 --- a/client/src/environments/environment.hmr.ts +++ b/client/src/environments/environment.hmr.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export const environment = { | 1 | export const environment = { |
2 | production: false, | 2 | production: false, |
3 | hmr: true, | 3 | hmr: true, |
4 | apiUrl: '' | 4 | apiUrl: '', |
5 | embedUrl: 'http://localhost:9000/videos/embed' | ||
5 | } | 6 | } |
diff --git a/client/src/environments/environment.prod.ts b/client/src/environments/environment.prod.ts index d5dfe5573..368aa1389 100644 --- a/client/src/environments/environment.prod.ts +++ b/client/src/environments/environment.prod.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export const environment = { | 1 | export const environment = { |
2 | production: true, | 2 | production: true, |
3 | hmr: false, | 3 | hmr: false, |
4 | apiUrl: '' | 4 | apiUrl: '', |
5 | embedUrl: '/videos/embed' | ||
5 | } | 6 | } |
diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts index b6bc784b5..60f5d9450 100644 --- a/client/src/environments/environment.ts +++ b/client/src/environments/environment.ts | |||
@@ -11,5 +11,6 @@ import 'core-js/features/reflect' | |||
11 | export const environment = { | 11 | export const environment = { |
12 | production: false, | 12 | production: false, |
13 | hmr: false, | 13 | hmr: false, |
14 | apiUrl: 'http://localhost:9000' | 14 | apiUrl: 'http://localhost:9000', |
15 | embedUrl: 'http://localhost:9000/videos/embed' | ||
15 | } | 16 | } |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index eb80ea0e3..6a1deac76 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -804,10 +804,12 @@ | |||
804 | } | 804 | } |
805 | 805 | ||
806 | @mixin chip { | 806 | @mixin chip { |
807 | --chip-radius: 5rem; | ||
808 | --chip-padding: .2rem .4rem; | ||
807 | $avatar-height: 1.2rem; | 809 | $avatar-height: 1.2rem; |
808 | 810 | ||
809 | align-items: center; | 811 | align-items: center; |
810 | border-radius: 5rem; | 812 | border-radius: var(--chip-radius); |
811 | display: inline-flex; | 813 | display: inline-flex; |
812 | font-size: 90%; | 814 | font-size: 90%; |
813 | color: pvar(--mainForegroundColor); | 815 | color: pvar(--mainForegroundColor); |
@@ -816,12 +818,17 @@ | |||
816 | margin: .1rem; | 818 | margin: .1rem; |
817 | max-width: 320px; | 819 | max-width: 320px; |
818 | overflow: hidden; | 820 | overflow: hidden; |
819 | padding: .2rem .4rem; | 821 | padding: var(--chip-padding); |
820 | text-decoration: none; | 822 | text-decoration: none; |
821 | text-overflow: ellipsis; | 823 | text-overflow: ellipsis; |
822 | vertical-align: middle; | 824 | vertical-align: middle; |
823 | white-space: nowrap; | 825 | white-space: nowrap; |
824 | 826 | ||
827 | &.rectangular { | ||
828 | --chip-radius: .2rem; | ||
829 | --chip-padding: .2rem .3rem; | ||
830 | } | ||
831 | |||
825 | .avatar { | 832 | .avatar { |
826 | margin-left: -.4rem; | 833 | margin-left: -.4rem; |
827 | margin-right: .2rem; | 834 | margin-right: .2rem; |
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss index 1fc744e67..bdeff8f9a 100644 --- a/client/src/sass/player/peertube-skin.scss +++ b/client/src/sass/player/peertube-skin.scss | |||
@@ -86,7 +86,7 @@ body { | |||
86 | } | 86 | } |
87 | 87 | ||
88 | &.focus-visible, &:hover { | 88 | &.focus-visible, &:hover { |
89 | background-color: var(--mainColor); | 89 | background-color: var(--mainColor, dimgray); |
90 | } | 90 | } |
91 | 91 | ||
92 | } | 92 | } |