aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-06-22 13:00:39 +0200
committerGitHub <noreply@github.com>2020-06-22 13:00:39 +0200
commit1ebddadd0704812a4600c39cabe2268321e88331 (patch)
tree1cc8560e5b63e9976aa5411ba800a62cfe7b8ea9 /client/src
parent07aea1a2642fc9868cb01e30c322514029d5b95a (diff)
downloadPeerTube-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')
-rw-r--r--client/src/app/+admin/moderation/moderation.component.scss14
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html16
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts37
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts19
-rw-r--r--client/src/app/shared/rest/rest.service.ts6
-rw-r--r--client/src/app/shared/video-abuse/video-abuse.service.ts13
-rw-r--r--client/src/app/shared/video/modals/video-block.component.html4
-rw-r--r--client/src/app/shared/video/modals/video-report.component.html105
-rw-r--r--client/src/app/shared/video/modals/video-report.component.scss17
-rw-r--r--client/src/app/shared/video/modals/video-report.component.ts104
-rw-r--r--client/src/app/shared/video/video.model.ts3
-rw-r--r--client/src/environments/environment.e2e.ts3
-rw-r--r--client/src/environments/environment.hmr.ts3
-rw-r--r--client/src/environments/environment.prod.ts3
-rw-r--r--client/src/environments/environment.ts3
-rw-r--r--client/src/sass/include/_mixins.scss11
-rw-r--r--client/src/sass/player/peertube-skin.scss2
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
45p-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 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { Account } from '@app/shared/account/account.model'
3import { Actor } from '@app/shared/actor/actor.model' 2import { Actor } from '@app/shared/actor/actor.model'
3import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model'
4import { ProcessedVideoAbuse } from './video-abuse-list.component' 4import { ProcessedVideoAbuse } from './video-abuse-list.component'
5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { 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'
11export class VideoAbuseDetailsComponent { 13export 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
11import { Video } from '../../../shared/video/video.model' 11import { Video } from '../../../shared/video/video.model'
12import { MarkdownService } from '@app/shared/renderer' 12import { MarkdownService } from '@app/shared/renderer'
13import { Actor } from '@app/shared/actor/actor.model' 13import { Actor } from '@app/shared/actor/actor.model'
14import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils' 14import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
15import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
16import { DomSanitizer } from '@angular/platform-browser' 15import { DomSanitizer } from '@angular/platform-browser'
17import { BlocklistService } from '@app/shared/blocklist' 16import { BlocklistService } from '@app/shared/blocklist'
18import { VideoService } from '@app/shared/video/video.service' 17import { VideoService } from '@app/shared/video/video.service'
19import { ActivatedRoute, Params, Router } from '@angular/router' 18import { ActivatedRoute, Params, Router } from '@angular/router'
20import { filter } from 'rxjs/operators' 19import { filter } from 'rxjs/operators'
20import { environment } from 'src/environments/environment'
21 21
22export type ProcessedVideoAbuse = VideoAbuse & { 22export 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'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { SortMeta } from 'primeng/api' 4import { SortMeta } from 'primeng/api'
5import { Observable } from 'rxjs' 5import { Observable } from 'rxjs'
6import { ResultList, VideoAbuse, VideoAbuseUpdate, VideoAbuseState } from '../../../../../shared' 6import { ResultList, VideoAbuse, VideoAbuseCreate, VideoAbuseState, VideoAbuseUpdate } from '../../../../../shared'
7import { environment } from '../../../environments/environment' 7import { environment } from '../../../environments/environment'
8import { RestExtractor, RestPagination, RestService } from '../rest' 8import { RestExtractor, RestPagination, RestService } from '../rest'
9import { omit } from 'lodash-es'
9 10
10@Injectable() 11@Injectable()
11export class VideoAbuseService { 12export 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 @@
8textarea { 8textarea {
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'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { VideoAbuseService } from '@app/shared/video-abuse' 9import { VideoAbuseService } from '@app/shared/video-abuse'
10import { Video } from '@app/shared/video/video.model' 10import { Video } from '@app/shared/video/video.model'
11import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
12import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
13import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model'
14import { 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'
7import { Actor } from '@app/shared/actor/actor.model' 7import { Actor } from '@app/shared/actor/actor.model'
8import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model' 8import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
9import { AuthUser } from '@app/core' 9import { AuthUser } from '@app/core'
10import { environment } from '../../../environments/environment'
10 11
11export class Video implements VideoServerModel { 12export 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 @@
1export const environment = { 1export 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 @@
1export const environment = { 1export 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 @@
1export const environment = { 1export 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'
11export const environment = { 11export 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 }