Lazy load all routes
[oweals/peertube.git] / client / src / app / +videos / +video-watch / comment / video-comments.component.ts
1 import { Subject, Subscription } from 'rxjs'
2 import { Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
3 import { ActivatedRoute } from '@angular/router'
4 import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifier, User } from '@app/core'
5 import { HooksService } from '@app/core/plugins/hooks.service'
6 import { Syndication, VideoDetails } from '@app/shared/shared-main'
7 import { I18n } from '@ngx-translate/i18n-polyfill'
8 import { VideoCommentThreadTree } from './video-comment-thread-tree.model'
9 import { VideoComment } from './video-comment.model'
10 import { VideoCommentService } from './video-comment.service'
11
12 @Component({
13   selector: 'my-video-comments',
14   templateUrl: './video-comments.component.html',
15   styleUrls: ['./video-comments.component.scss']
16 })
17 export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
18   @ViewChild('commentHighlightBlock') commentHighlightBlock: ElementRef
19   @Input() video: VideoDetails
20   @Input() user: User
21
22   @Output() timestampClicked = new EventEmitter<number>()
23
24   comments: VideoComment[] = []
25   highlightedThread: VideoComment
26   sort = '-createdAt'
27   componentPagination: ComponentPagination = {
28     currentPage: 1,
29     itemsPerPage: 10,
30     totalItems: null
31   }
32   inReplyToCommentId: number
33   threadComments: { [ id: number ]: VideoCommentThreadTree } = {}
34   threadLoading: { [ id: number ]: boolean } = {}
35
36   syndicationItems: Syndication[] = []
37
38   onDataSubject = new Subject<any[]>()
39
40   private sub: Subscription
41
42   constructor (
43     private authService: AuthService,
44     private notifier: Notifier,
45     private confirmService: ConfirmService,
46     private videoCommentService: VideoCommentService,
47     private activatedRoute: ActivatedRoute,
48     private i18n: I18n,
49     private hooks: HooksService
50   ) {}
51
52   ngOnInit () {
53     // Find highlighted comment in params
54     this.sub = this.activatedRoute.params.subscribe(
55       params => {
56         if (params['threadId']) {
57           const highlightedThreadId = +params['threadId']
58           this.processHighlightedThread(highlightedThreadId)
59         }
60       }
61     )
62   }
63
64   ngOnChanges (changes: SimpleChanges) {
65     if (changes['video']) {
66       this.resetVideo()
67     }
68   }
69
70   ngOnDestroy () {
71     if (this.sub) this.sub.unsubscribe()
72   }
73
74   viewReplies (commentId: number, highlightThread = false) {
75     this.threadLoading[commentId] = true
76
77     const params = {
78       videoId: this.video.id,
79       threadId: commentId
80     }
81
82     const obs = this.hooks.wrapObsFun(
83       this.videoCommentService.getVideoThreadComments.bind(this.videoCommentService),
84       params,
85       'video-watch',
86       'filter:api.video-watch.video-thread-replies.list.params',
87       'filter:api.video-watch.video-thread-replies.list.result'
88     )
89
90     obs.subscribe(
91         res => {
92           this.threadComments[commentId] = res
93           this.threadLoading[commentId] = false
94           this.hooks.runAction('action:video-watch.video-thread-replies.loaded', 'video-watch', { data: res })
95
96           if (highlightThread) {
97             this.highlightedThread = new VideoComment(res.comment)
98
99             // Scroll to the highlighted thread
100             setTimeout(() => this.commentHighlightBlock.nativeElement.scrollIntoView(), 0)
101           }
102         },
103
104         err => this.notifier.error(err.message)
105       )
106   }
107
108   loadMoreThreads () {
109     const params = {
110       videoId: this.video.id,
111       componentPagination: this.componentPagination,
112       sort: this.sort
113     }
114
115     const obs = this.hooks.wrapObsFun(
116       this.videoCommentService.getVideoCommentThreads.bind(this.videoCommentService),
117       params,
118       'video-watch',
119       'filter:api.video-watch.video-threads.list.params',
120       'filter:api.video-watch.video-threads.list.result'
121     )
122
123     obs.subscribe(
124       res => {
125         this.comments = this.comments.concat(res.data)
126         this.componentPagination.totalItems = res.total
127
128         this.onDataSubject.next(res.data)
129         this.hooks.runAction('action:video-watch.video-threads.loaded', 'video-watch', { data: this.componentPagination })
130       },
131
132       err => this.notifier.error(err.message)
133     )
134   }
135
136   onCommentThreadCreated (comment: VideoComment) {
137     this.comments.unshift(comment)
138   }
139
140   onWantedToReply (comment: VideoComment) {
141     this.inReplyToCommentId = comment.id
142   }
143
144   onResetReply () {
145     this.inReplyToCommentId = undefined
146   }
147
148   onThreadCreated (commentTree: VideoCommentThreadTree) {
149     this.viewReplies(commentTree.comment.id)
150   }
151
152   handleSortChange (sort: string) {
153     if (this.sort === sort) return
154
155     this.sort = sort
156     this.resetVideo()
157   }
158
159   handleTimestampClicked (timestamp: number) {
160     this.timestampClicked.emit(timestamp)
161   }
162
163   async onWantedToDelete (commentToDelete: VideoComment) {
164     let message = 'Do you really want to delete this comment?'
165
166     if (commentToDelete.isLocal || this.video.isLocal) {
167       message += this.i18n(' The deletion will be sent to remote instances so they can reflect the change.')
168     } else {
169       message += this.i18n(' It is a remote comment, so the deletion will only be effective on your instance.')
170     }
171
172     const res = await this.confirmService.confirm(message, this.i18n('Delete'))
173     if (res === false) return
174
175     this.videoCommentService.deleteVideoComment(commentToDelete.videoId, commentToDelete.id)
176       .subscribe(
177         () => {
178           if (this.highlightedThread?.id === commentToDelete.id) {
179             commentToDelete = this.comments.find(c => c.id === commentToDelete.id)
180
181             this.highlightedThread = undefined
182           }
183
184           // Mark the comment as deleted
185           this.softDeleteComment(commentToDelete)
186         },
187
188         err => this.notifier.error(err.message)
189       )
190   }
191
192   isUserLoggedIn () {
193     return this.authService.isLoggedIn()
194   }
195
196   onNearOfBottom () {
197     if (hasMoreItems(this.componentPagination)) {
198       this.componentPagination.currentPage++
199       this.loadMoreThreads()
200     }
201   }
202
203   private softDeleteComment (comment: VideoComment) {
204     comment.isDeleted = true
205     comment.deletedAt = new Date()
206     comment.text = ''
207     comment.account = null
208   }
209
210   private resetVideo () {
211     if (this.video.commentsEnabled === true) {
212       // Reset all our fields
213       this.highlightedThread = null
214       this.comments = []
215       this.threadComments = {}
216       this.threadLoading = {}
217       this.inReplyToCommentId = undefined
218       this.componentPagination.currentPage = 1
219       this.componentPagination.totalItems = null
220
221       this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid)
222       this.loadMoreThreads()
223     }
224   }
225
226   private processHighlightedThread (highlightedThreadId: number) {
227     this.highlightedThread = this.comments.find(c => c.id === highlightedThreadId)
228
229     const highlightThread = true
230     this.viewReplies(highlightedThreadId, highlightThread)
231   }
232 }