aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared')
-rw-r--r--client/src/app/shared/shared-main/account/actor.model.ts6
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts10
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html15
-rw-r--r--client/src/app/shared/shared-moderation/comment-report.component.scss11
-rw-r--r--client/src/app/shared/shared-moderation/index.ts3
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/account-report.component.ts94
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts (renamed from client/src/app/shared/shared-moderation/comment-report.component.ts)17
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/index.ts3
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/report.component.html (renamed from client/src/app/shared/shared-moderation/comment-report.component.html)4
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/report.component.scss (renamed from client/src/app/shared/shared-moderation/video-report.component.scss)0
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.html (renamed from client/src/app/shared/shared-moderation/video-report.component.html)2
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.ts (renamed from client/src/app/shared/shared-moderation/video-report.component.ts)10
-rw-r--r--client/src/app/shared/shared-moderation/shared-moderation.module.ts15
-rw-r--r--client/src/app/shared/shared-video-comment/index.ts5
-rw-r--r--client/src/app/shared/shared-video-comment/shared-video-comment.module.ts19
-rw-r--r--client/src/app/shared/shared-video-comment/video-comment-thread-tree.model.ts7
-rw-r--r--client/src/app/shared/shared-video-comment/video-comment.model.ts48
-rw-r--r--client/src/app/shared/shared-video-comment/video-comment.service.ts149
18 files changed, 382 insertions, 36 deletions
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 9ec6dbab1..bda88bdee 100644
--- a/client/src/app/shared/shared-main/account/actor.model.ts
+++ b/client/src/app/shared/shared-main/account/actor.model.ts
@@ -14,6 +14,8 @@ export abstract class Actor implements ActorServer {
14 14
15 avatarUrl: string 15 avatarUrl: string
16 16
17 isLocal: boolean
18
17 static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) { 19 static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) {
18 if (actor?.avatar?.url) return actor.avatar.url 20 if (actor?.avatar?.url) return actor.avatar.url
19 21
@@ -52,6 +54,10 @@ export abstract class Actor implements ActorServer {
52 54
53 this.avatar = hash.avatar 55 this.avatar = hash.avatar
54 56
57 const absoluteAPIUrl = getAbsoluteAPIUrl()
58 const thisHost = new URL(absoluteAPIUrl).host
59 this.isLocal = this.host.trim() === thisHost
60
55 this.updateComputedAttributes() 61 this.updateComputedAttributes()
56 } 62 }
57 63
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index a137f8c62..61b48a806 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -34,7 +34,9 @@ export class UserNotification implements UserNotificationServer {
34 threadId: number 34 threadId: number
35 35
36 video: { 36 video: {
37 id: number
37 uuid: string 38 uuid: string
39 name: string
38 } 40 }
39 } 41 }
40 42
@@ -115,13 +117,15 @@ export class UserNotification implements UserNotificationServer {
115 case UserNotificationType.COMMENT_MENTION: 117 case UserNotificationType.COMMENT_MENTION:
116 if (!this.comment) break 118 if (!this.comment) break
117 this.accountUrl = this.buildAccountUrl(this.comment.account) 119 this.accountUrl = this.buildAccountUrl(this.comment.account)
118 this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ] 120 this.commentUrl = this.buildCommentUrl(this.comment)
119 break 121 break
120 122
121 case UserNotificationType.NEW_ABUSE_FOR_MODERATORS: 123 case UserNotificationType.NEW_ABUSE_FOR_MODERATORS:
122 this.abuseUrl = '/admin/moderation/abuses/list' 124 this.abuseUrl = '/admin/moderation/abuses/list'
123 125
124 if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video) 126 if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video)
127 else if (this.abuse.comment) this.commentUrl = this.buildCommentUrl(this.abuse.comment)
128 else if (this.abuse.account) this.accountUrl = this.buildAccountUrl(this.abuse.account)
125 break 129 break
126 130
127 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS: 131 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
@@ -190,6 +194,10 @@ export class UserNotification implements UserNotificationServer {
190 return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName 194 return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
191 } 195 }
192 196
197 private buildCommentUrl (comment: { video: { uuid: string }, threadId: number }) {
198 return [ this.buildVideoUrl(comment.video), { threadId: comment.threadId } ]
199 }
200
193 private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) { 201 private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) {
194 actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor) 202 actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor)
195 } 203 }
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html
index 2b341af2c..8127ae979 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.html
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.html
@@ -45,9 +45,22 @@
45 <ng-container *ngSwitchCase="UserNotificationType.NEW_ABUSE_FOR_MODERATORS"> 45 <ng-container *ngSwitchCase="UserNotificationType.NEW_ABUSE_FOR_MODERATORS">
46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
47 47
48 <div class="message" i18n> 48 <div class="message" *ngIf="notification.videoUrl" i18n>
49 <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.abuse.video.name }}</a> 49 <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.abuse.video.name }}</a>
50 </div> 50 </div>
51
52 <div class="message" *ngIf="notification.commentUrl" i18n>
53 <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new comment abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.abuse.comment.video.name }}</a>
54 </div>
55
56 <div class="message" *ngIf="notification.accountUrl" i18n>
57 <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new account abuse</a> has been created on account <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.abuse.account.displayName }}</a>
58 </div>
59
60 <!-- Deleted entity associated to the abuse -->
61 <div class="message" *ngIf="!notification.videoUrl && !notification.commentUrl && !notification.accountUrl" i18n>
62 <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new abuse</a> has been created
63 </div>
51 </ng-container> 64 </ng-container>
52 65
53 <ng-container *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS"> 66 <ng-container *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS">
diff --git a/client/src/app/shared/shared-moderation/comment-report.component.scss b/client/src/app/shared/shared-moderation/comment-report.component.scss
deleted file mode 100644
index 17a33d3a2..000000000
--- a/client/src/app/shared/shared-moderation/comment-report.component.scss
+++ /dev/null
@@ -1,11 +0,0 @@
1@import 'variables';
2@import 'mixins';
3
4.information {
5 margin-bottom: 20px;
6}
7
8textarea {
9 @include peertube-textarea(100%, 100px);
10}
11
diff --git a/client/src/app/shared/shared-moderation/index.ts b/client/src/app/shared/shared-moderation/index.ts
index d6c4a10be..41c910ffe 100644
--- a/client/src/app/shared/shared-moderation/index.ts
+++ b/client/src/app/shared/shared-moderation/index.ts
@@ -1,3 +1,5 @@
1export * from './report-modals'
2
1export * from './abuse.service' 3export * from './abuse.service'
2export * from './account-block.model' 4export * from './account-block.model'
3export * from './account-blocklist.component' 5export * from './account-blocklist.component'
@@ -9,5 +11,4 @@ export * from './user-ban-modal.component'
9export * from './user-moderation-dropdown.component' 11export * from './user-moderation-dropdown.component'
10export * from './video-block.component' 12export * from './video-block.component'
11export * from './video-block.service' 13export * from './video-block.service'
12export * from './video-report.component'
13export * from './shared-moderation.module' 14export * from './shared-moderation.module'
diff --git a/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
new file mode 100644
index 000000000..78ca934c7
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
@@ -0,0 +1,94 @@
1import { mapValues, pickBy } from 'lodash-es'
2import { Component, Input, OnInit, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core'
4import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { Account } from '@app/shared/shared-main'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
8import { I18n } from '@ngx-translate/i18n-polyfill'
9import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models'
10import { AbuseService } from '../abuse.service'
11
12@Component({
13 selector: 'my-account-report',
14 templateUrl: './report.component.html',
15 styleUrls: [ './report.component.scss' ]
16})
17export class AccountReportComponent extends FormReactive implements OnInit {
18 @Input() account: Account = null
19
20 @ViewChild('modal', { static: true }) modal: NgbModal
21
22 error: string = null
23 predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
24 modalTitle: string
25
26 private openedModal: NgbModalRef
27
28 constructor (
29 protected formValidatorService: FormValidatorService,
30 private modalService: NgbModal,
31 private abuseValidatorsService: AbuseValidatorsService,
32 private abuseService: AbuseService,
33 private notifier: Notifier,
34 private i18n: I18n
35 ) {
36 super()
37 }
38
39 get currentHost () {
40 return window.location.host
41 }
42
43 get originHost () {
44 if (this.isRemote()) {
45 return this.account.host
46 }
47
48 return ''
49 }
50
51 ngOnInit () {
52 this.modalTitle = this.i18n('Report {{displayName}}', { displayName: this.account.displayName })
53
54 this.buildForm({
55 reason: this.abuseValidatorsService.ABUSE_REASON,
56 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null)
57 })
58
59 this.predefinedReasons = this.abuseService.getPrefefinedReasons('account')
60 }
61
62 show () {
63 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' })
64 }
65
66 hide () {
67 this.openedModal.close()
68 this.openedModal = null
69 }
70
71 report () {
72 const reason = this.form.get('reason').value
73 const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[]
74
75 this.abuseService.reportVideo({
76 reason,
77 predefinedReasons,
78 account: {
79 id: this.account.id
80 }
81 }).subscribe(
82 () => {
83 this.notifier.success(this.i18n('Account reported.'))
84 this.hide()
85 },
86
87 err => this.notifier.error(err.message)
88 )
89 }
90
91 isRemote () {
92 return !this.account.isLocal
93 }
94}
diff --git a/client/src/app/shared/shared-moderation/comment-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
index 5db4b2dc1..00d7b8d34 100644
--- a/client/src/app/shared/shared-moderation/comment-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
@@ -1,28 +1,27 @@
1import { mapValues, pickBy } from 'lodash-es' 1import { mapValues, pickBy } from 'lodash-es'
2import { Component, Input, OnInit, ViewChild } from '@angular/core' 2import { Component, Input, OnInit, ViewChild } from '@angular/core'
3import { SafeHtml } from '@angular/platform-browser'
4import { VideoComment } from '@app/+videos/+video-watch/comment/video-comment.model'
5import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
6import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { VideoComment } from '@app/shared/shared-video-comment'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { I18n } from '@ngx-translate/i18n-polyfill' 8import { I18n } from '@ngx-translate/i18n-polyfill'
10import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models' 9import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models'
11import { AbuseService } from './abuse.service' 10import { AbuseService } from '../abuse.service'
12 11
13@Component({ 12@Component({
14 selector: 'my-comment-report', 13 selector: 'my-comment-report',
15 templateUrl: './comment-report.component.html', 14 templateUrl: './report.component.html',
16 styleUrls: [ './comment-report.component.scss' ] 15 styleUrls: [ './report.component.scss' ]
17}) 16})
18export class CommentReportComponent extends FormReactive implements OnInit { 17export class CommentReportComponent extends FormReactive implements OnInit {
19 @Input() comment: VideoComment = null 18 @Input() comment: VideoComment = null
20 19
21 @ViewChild('modal', { static: true }) modal: NgbModal 20 @ViewChild('modal', { static: true }) modal: NgbModal
22 21
22 modalTitle: string
23 error: string = null 23 error: string = null
24 predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [] 24 predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
25 embedHtml: SafeHtml
26 25
27 private openedModal: NgbModalRef 26 private openedModal: NgbModalRef
28 27
@@ -42,7 +41,7 @@ export class CommentReportComponent extends FormReactive implements OnInit {
42 } 41 }
43 42
44 get originHost () { 43 get originHost () {
45 if (this.isRemoteComment()) { 44 if (this.isRemote()) {
46 return this.comment.account.host 45 return this.comment.account.host
47 } 46 }
48 47
@@ -50,6 +49,8 @@ export class CommentReportComponent extends FormReactive implements OnInit {
50 } 49 }
51 50
52 ngOnInit () { 51 ngOnInit () {
52 this.modalTitle = this.i18n('Report comment')
53
53 this.buildForm({ 54 this.buildForm({
54 reason: this.abuseValidatorsService.ABUSE_REASON, 55 reason: this.abuseValidatorsService.ABUSE_REASON,
55 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null) 56 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null)
@@ -87,7 +88,7 @@ export class CommentReportComponent extends FormReactive implements OnInit {
87 ) 88 )
88 } 89 }
89 90
90 isRemoteComment () { 91 isRemote () {
91 return !this.comment.isLocal 92 return !this.comment.isLocal
92 } 93 }
93} 94}
diff --git a/client/src/app/shared/shared-moderation/report-modals/index.ts b/client/src/app/shared/shared-moderation/report-modals/index.ts
new file mode 100644
index 000000000..f3c4058ae
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/report-modals/index.ts
@@ -0,0 +1,3 @@
1export * from './account-report.component'
2export * from './comment-report.component'
3export * from './video-report.component'
diff --git a/client/src/app/shared/shared-moderation/comment-report.component.html b/client/src/app/shared/shared-moderation/report-modals/report.component.html
index 1105b3788..bda62312f 100644
--- a/client/src/app/shared/shared-moderation/comment-report.component.html
+++ b/client/src/app/shared/shared-moderation/report-modals/report.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">Report comment</h4> 3 <h4 class="modal-title">{{ modalTitle }}</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
@@ -34,7 +34,7 @@
34 34
35 <div class="col-7"> 35 <div class="col-7">
36 <div i18n class="information"> 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>. 37 Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemote()"> and will be forwarded to the comment origin ({{ originHost }}) too</ng-container>.
38 </div> 38 </div>
39 39
40 <div class="form-group"> 40 <div class="form-group">
diff --git a/client/src/app/shared/shared-moderation/video-report.component.scss b/client/src/app/shared/shared-moderation/report-modals/report.component.scss
index b2606cbd8..b2606cbd8 100644
--- a/client/src/app/shared/shared-moderation/video-report.component.scss
+++ b/client/src/app/shared/shared-moderation/report-modals/report.component.scss
diff --git a/client/src/app/shared/shared-moderation/video-report.component.html b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
index b724ecb18..4947088d1 100644
--- a/client/src/app/shared/shared-moderation/video-report.component.html
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
@@ -72,7 +72,7 @@
72 </div> 72 </div>
73 73
74 <div i18n class="information"> 74 <div i18n class="information">
75 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>. 75 Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemote()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>.
76 </div> 76 </div>
77 77
78 <div class="form-group"> 78 <div class="form-group">
diff --git a/client/src/app/shared/shared-moderation/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
index 26e7b62ba..7d53ea3c9 100644
--- a/client/src/app/shared/shared-moderation/video-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
@@ -8,13 +8,13 @@ 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 { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models' 10import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models'
11import { Video } from '../shared-main' 11import { Video } from '../../shared-main'
12import { AbuseService } from './abuse.service' 12import { AbuseService } from '../abuse.service'
13 13
14@Component({ 14@Component({
15 selector: 'my-video-report', 15 selector: 'my-video-report',
16 templateUrl: './video-report.component.html', 16 templateUrl: './video-report.component.html',
17 styleUrls: [ './video-report.component.scss' ] 17 styleUrls: [ './report.component.scss' ]
18}) 18})
19export class VideoReportComponent extends FormReactive implements OnInit { 19export class VideoReportComponent extends FormReactive implements OnInit {
20 @Input() video: Video = null 20 @Input() video: Video = null
@@ -44,7 +44,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
44 } 44 }
45 45
46 get originHost () { 46 get originHost () {
47 if (this.isRemoteVideo()) { 47 if (this.isRemote()) {
48 return this.video.account.host 48 return this.video.account.host
49 } 49 }
50 50
@@ -116,7 +116,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
116 ) 116 )
117 } 117 }
118 118
119 isRemoteVideo () { 119 isRemote () {
120 return !this.video.isLocal 120 return !this.video.isLocal
121 } 121 }
122} 122}
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 ff4021a33..8fa9ee794 100644
--- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts
+++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
@@ -3,22 +3,23 @@ import { NgModule } from '@angular/core'
3import { SharedFormModule } from '../shared-forms/shared-form.module' 3import { SharedFormModule } from '../shared-forms/shared-form.module'
4import { SharedGlobalIconModule } from '../shared-icons' 4import { SharedGlobalIconModule } from '../shared-icons'
5import { SharedMainModule } from '../shared-main/shared-main.module' 5import { SharedMainModule } from '../shared-main/shared-main.module'
6import { SharedVideoCommentModule } from '../shared-video-comment'
7import { AbuseService } from './abuse.service'
6import { BatchDomainsModalComponent } from './batch-domains-modal.component' 8import { BatchDomainsModalComponent } from './batch-domains-modal.component'
7import { BlocklistService } from './blocklist.service' 9import { BlocklistService } from './blocklist.service'
8import { BulkService } from './bulk.service' 10import { BulkService } from './bulk.service'
9import { UserBanModalComponent } from './user-ban-modal.component' 11import { UserBanModalComponent } from './user-ban-modal.component'
10import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' 12import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
11import { AbuseService } from './abuse.service'
12import { VideoBlockComponent } from './video-block.component' 13import { VideoBlockComponent } from './video-block.component'
13import { VideoBlockService } from './video-block.service' 14import { VideoBlockService } from './video-block.service'
14import { VideoReportComponent } from './video-report.component' 15import { VideoReportComponent, AccountReportComponent, CommentReportComponent } from './report-modals'
15import { CommentReportComponent } from './comment-report.component'
16 16
17@NgModule({ 17@NgModule({
18 imports: [ 18 imports: [
19 SharedMainModule, 19 SharedMainModule,
20 SharedFormModule, 20 SharedFormModule,
21 SharedGlobalIconModule 21 SharedGlobalIconModule,
22 SharedVideoCommentModule
22 ], 23 ],
23 24
24 declarations: [ 25 declarations: [
@@ -27,7 +28,8 @@ import { CommentReportComponent } from './comment-report.component'
27 VideoBlockComponent, 28 VideoBlockComponent,
28 VideoReportComponent, 29 VideoReportComponent,
29 BatchDomainsModalComponent, 30 BatchDomainsModalComponent,
30 CommentReportComponent 31 CommentReportComponent,
32 AccountReportComponent
31 ], 33 ],
32 34
33 exports: [ 35 exports: [
@@ -36,7 +38,8 @@ import { CommentReportComponent } from './comment-report.component'
36 VideoBlockComponent, 38 VideoBlockComponent,
37 VideoReportComponent, 39 VideoReportComponent,
38 BatchDomainsModalComponent, 40 BatchDomainsModalComponent,
39 CommentReportComponent 41 CommentReportComponent,
42 AccountReportComponent
40 ], 43 ],
41 44
42 providers: [ 45 providers: [
diff --git a/client/src/app/shared/shared-video-comment/index.ts b/client/src/app/shared/shared-video-comment/index.ts
new file mode 100644
index 000000000..b1195f232
--- /dev/null
+++ b/client/src/app/shared/shared-video-comment/index.ts
@@ -0,0 +1,5 @@
1export * from './video-comment.service'
2export * from './video-comment.model'
3export * from './video-comment-thread-tree.model'
4
5export * from './shared-video-comment.module'
diff --git a/client/src/app/shared/shared-video-comment/shared-video-comment.module.ts b/client/src/app/shared/shared-video-comment/shared-video-comment.module.ts
new file mode 100644
index 000000000..41b329861
--- /dev/null
+++ b/client/src/app/shared/shared-video-comment/shared-video-comment.module.ts
@@ -0,0 +1,19 @@
1
2import { NgModule } from '@angular/core'
3import { SharedMainModule } from '../shared-main/shared-main.module'
4import { VideoCommentService } from './video-comment.service'
5
6@NgModule({
7 imports: [
8 SharedMainModule
9 ],
10
11 declarations: [ ],
12
13 exports: [ ],
14
15 providers: [
16 VideoCommentService
17 ]
18})
19export class SharedVideoCommentModule { }
diff --git a/client/src/app/shared/shared-video-comment/video-comment-thread-tree.model.ts b/client/src/app/shared/shared-video-comment/video-comment-thread-tree.model.ts
new file mode 100644
index 000000000..7c2aaeadd
--- /dev/null
+++ b/client/src/app/shared/shared-video-comment/video-comment-thread-tree.model.ts
@@ -0,0 +1,7 @@
1import { VideoCommentThreadTree as VideoCommentThreadTreeServerModel } from '@shared/models'
2import { VideoComment } from './video-comment.model'
3
4export class VideoCommentThreadTree implements VideoCommentThreadTreeServerModel {
5 comment: VideoComment
6 children: VideoCommentThreadTree[]
7}
diff --git a/client/src/app/shared/shared-video-comment/video-comment.model.ts b/client/src/app/shared/shared-video-comment/video-comment.model.ts
new file mode 100644
index 000000000..e85443196
--- /dev/null
+++ b/client/src/app/shared/shared-video-comment/video-comment.model.ts
@@ -0,0 +1,48 @@
1import { getAbsoluteAPIUrl } from '@app/helpers'
2import { Actor } from '@app/shared/shared-main'
3import { Account as AccountInterface, VideoComment as VideoCommentServerModel } from '@shared/models'
4
5export class VideoComment implements VideoCommentServerModel {
6 id: number
7 url: string
8 text: string
9 threadId: number
10 inReplyToCommentId: number
11 videoId: number
12 createdAt: Date | string
13 updatedAt: Date | string
14 deletedAt: Date | string
15 isDeleted: boolean
16 account: AccountInterface
17 totalRepliesFromVideoAuthor: number
18 totalReplies: number
19 by: string
20 accountAvatarUrl: string
21
22 isLocal: boolean
23
24 constructor (hash: VideoCommentServerModel) {
25 this.id = hash.id
26 this.url = hash.url
27 this.text = hash.text
28 this.threadId = hash.threadId
29 this.inReplyToCommentId = hash.inReplyToCommentId
30 this.videoId = hash.videoId
31 this.createdAt = new Date(hash.createdAt.toString())
32 this.updatedAt = new Date(hash.updatedAt.toString())
33 this.deletedAt = hash.deletedAt ? new Date(hash.deletedAt.toString()) : null
34 this.isDeleted = hash.isDeleted
35 this.account = hash.account
36 this.totalRepliesFromVideoAuthor = hash.totalRepliesFromVideoAuthor
37 this.totalReplies = hash.totalReplies
38
39 if (this.account) {
40 this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host)
41 this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account)
42
43 const absoluteAPIUrl = getAbsoluteAPIUrl()
44 const thisHost = new URL(absoluteAPIUrl).host
45 this.isLocal = this.account.host.trim() === thisHost
46 }
47 }
48}
diff --git a/client/src/app/shared/shared-video-comment/video-comment.service.ts b/client/src/app/shared/shared-video-comment/video-comment.service.ts
new file mode 100644
index 000000000..81c65aa38
--- /dev/null
+++ b/client/src/app/shared/shared-video-comment/video-comment.service.ts
@@ -0,0 +1,149 @@
1import { Observable } from 'rxjs'
2import { catchError, map } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core'
5import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
6import { objectLineFeedToHtml } from '@app/helpers'
7import {
8 FeedFormat,
9 ResultList,
10 VideoComment as VideoCommentServerModel,
11 VideoCommentCreate,
12 VideoCommentThreadTree as VideoCommentThreadTreeServerModel
13} from '@shared/models'
14import { environment } from '../../../environments/environment'
15import { VideoCommentThreadTree } from './video-comment-thread-tree.model'
16import { VideoComment } from './video-comment.model'
17
18@Injectable()
19export class VideoCommentService {
20 private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
21 private static BASE_FEEDS_URL = environment.apiUrl + '/feeds/video-comments.'
22
23 constructor (
24 private authHttp: HttpClient,
25 private restExtractor: RestExtractor,
26 private restService: RestService
27 ) {}
28
29 addCommentThread (videoId: number | string, comment: VideoCommentCreate) {
30 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads'
31 const normalizedComment = objectLineFeedToHtml(comment, 'text')
32
33 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
34 .pipe(
35 map(data => this.extractVideoComment(data.comment)),
36 catchError(err => this.restExtractor.handleError(err))
37 )
38 }
39
40 addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) {
41 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId
42 const normalizedComment = objectLineFeedToHtml(comment, 'text')
43
44 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
45 .pipe(
46 map(data => this.extractVideoComment(data.comment)),
47 catchError(err => this.restExtractor.handleError(err))
48 )
49 }
50
51 getVideoCommentThreads (parameters: {
52 videoId: number | string,
53 componentPagination: ComponentPaginationLight,
54 sort: string
55 }): Observable<ResultList<VideoComment>> {
56 const { videoId, componentPagination, sort } = parameters
57
58 const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
59
60 let params = new HttpParams()
61 params = this.restService.addRestGetParams(params, pagination, sort)
62
63 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads'
64 return this.authHttp.get<ResultList<VideoComment>>(url, { params })
65 .pipe(
66 map(result => this.extractVideoComments(result)),
67 catchError(err => this.restExtractor.handleError(err))
68 )
69 }
70
71 getVideoThreadComments (parameters: {
72 videoId: number | string,
73 threadId: number
74 }): Observable<VideoCommentThreadTree> {
75 const { videoId, threadId } = parameters
76 const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}`
77
78 return this.authHttp
79 .get<VideoCommentThreadTreeServerModel>(url)
80 .pipe(
81 map(tree => this.extractVideoCommentTree(tree)),
82 catchError(err => this.restExtractor.handleError(err))
83 )
84 }
85
86 deleteVideoComment (videoId: number | string, commentId: number) {
87 const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comments/${commentId}`
88
89 return this.authHttp
90 .delete(url)
91 .pipe(
92 map(this.restExtractor.extractDataBool),
93 catchError(err => this.restExtractor.handleError(err))
94 )
95 }
96
97 getVideoCommentsFeeds (videoUUID?: string) {
98 const feeds = [
99 {
100 format: FeedFormat.RSS,
101 label: 'rss 2.0',
102 url: VideoCommentService.BASE_FEEDS_URL + FeedFormat.RSS.toLowerCase()
103 },
104 {
105 format: FeedFormat.ATOM,
106 label: 'atom 1.0',
107 url: VideoCommentService.BASE_FEEDS_URL + FeedFormat.ATOM.toLowerCase()
108 },
109 {
110 format: FeedFormat.JSON,
111 label: 'json 1.0',
112 url: VideoCommentService.BASE_FEEDS_URL + FeedFormat.JSON.toLowerCase()
113 }
114 ]
115
116 if (videoUUID !== undefined) {
117 for (const feed of feeds) {
118 feed.url += '?videoId=' + videoUUID
119 }
120 }
121
122 return feeds
123 }
124
125 private extractVideoComment (videoComment: VideoCommentServerModel) {
126 return new VideoComment(videoComment)
127 }
128
129 private extractVideoComments (result: ResultList<VideoCommentServerModel>) {
130 const videoCommentsJson = result.data
131 const totalComments = result.total
132 const comments: VideoComment[] = []
133
134 for (const videoCommentJson of videoCommentsJson) {
135 comments.push(new VideoComment(videoCommentJson))
136 }
137
138 return { data: comments, total: totalComments }
139 }
140
141 private extractVideoCommentTree (tree: VideoCommentThreadTreeServerModel) {
142 if (!tree) return tree as VideoCommentThreadTree
143
144 tree.comment = new VideoComment(tree.comment)
145 tree.children.forEach(c => this.extractVideoCommentTree(c))
146
147 return tree as VideoCommentThreadTree
148 }
149}