aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared
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/app/shared
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/app/shared')
-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
7 files changed, 207 insertions, 45 deletions
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