]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/app/shared/shared-video-miniature/videos-list.component.ts
Display avatar in video miniatures (#4823)
[github/Chocobozzz/PeerTube.git] / client / src / app / shared / shared-video-miniature / videos-list.component.ts
CommitLineData
dd24f1bb
C
1import * as debug from 'debug'
2import { fromEvent, Observable, Subject, Subscription } from 'rxjs'
3import { debounceTime, switchMap } from 'rxjs/operators'
4import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'
5import { ActivatedRoute } from '@angular/router'
2c102aac 6import {
7 AuthService,
8 ComponentPaginationLight,
9 Notifier,
10 PeerTubeRouterService,
11 ScreenService,
12 ServerService,
13 User,
14 UserService
15} from '@app/core'
dd24f1bb
C
16import { GlobalIconName } from '@app/shared/shared-icons'
17import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@shared/core-utils'
18import { ResultList, UserRight, VideoSortField } from '@shared/models'
19import { Syndication, Video } from '../shared-main'
20import { VideoFilters, VideoFilterScope } from './video-filters.model'
21import { MiniatureDisplayOptions } from './video-miniature.component'
22
23const logger = debug('peertube:videos:VideosListComponent')
24
25export type HeaderAction = {
26 iconName: GlobalIconName
27 label: string
28 justIcon?: boolean
29 routerLink?: string
30 href?: string
31 click?: (e: Event) => void
32}
33
34enum GroupDate {
35 UNKNOWN = 0,
36 TODAY = 1,
37 YESTERDAY = 2,
38 THIS_WEEK = 3,
39 THIS_MONTH = 4,
40 LAST_MONTH = 5,
41 OLDER = 6
42}
43
44@Component({
45 selector: 'my-videos-list',
46 templateUrl: './videos-list.component.html',
47 styleUrls: [ './videos-list.component.scss' ]
48})
49export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
50 @Input() getVideosObservableFunction: (pagination: ComponentPaginationLight, filters: VideoFilters) => Observable<ResultList<Video>>
51 @Input() getSyndicationItemsFunction: (filters: VideoFilters) => Promise<Syndication[]> | Syndication[]
52 @Input() baseRouteBuilderFunction: (filters: VideoFilters) => string[]
53
54 @Input() title: string
55 @Input() titleTooltip: string
56 @Input() displayTitle = true
57
58 @Input() defaultSort: VideoSortField
59 @Input() defaultScope: VideoFilterScope = 'federated'
60 @Input() displayFilters = false
61 @Input() displayModerationBlock = false
62
63 @Input() loadUserVideoPreferences = false
64
65 @Input() displayAsRow = false
66 @Input() displayVideoActions = true
67 @Input() groupByDate = false
68
69 @Input() headerActions: HeaderAction[] = []
70
1b206245
C
71 @Input() hideScopeFilter = false
72
2c102aac 73 @Input() displayOptions: MiniatureDisplayOptions
dd24f1bb
C
74
75 @Input() disabled = false
76
77 @Output() filtersChanged = new EventEmitter<VideoFilters>()
78
79 videos: Video[] = []
80 filters: VideoFilters
81 syndicationItems: Syndication[]
82
83 onDataSubject = new Subject<any[]>()
84 hasDoneFirstQuery = false
85
86 userMiniature: User
87
2c102aac 88 private defaultDisplayOptions: MiniatureDisplayOptions = {
89 date: true,
90 views: true,
91 by: true,
92 avatar: false,
93 privacyLabel: true,
94 privacyText: false,
95 state: false,
96 blacklistInfo: false
97 }
dd24f1bb
C
98 private routeSub: Subscription
99 private userSub: Subscription
100 private resizeSub: Subscription
101
102 private pagination: ComponentPaginationLight = {
103 currentPage: 1,
104 itemsPerPage: 25
105 }
106
107 private groupedDateLabels: { [id in GroupDate]: string }
108 private groupedDates: { [id: number]: GroupDate } = {}
109
110 private lastQueryLength: number
111
112 constructor (
113 private notifier: Notifier,
114 private authService: AuthService,
115 private userService: UserService,
116 private route: ActivatedRoute,
117 private screenService: ScreenService,
2c102aac 118 private peertubeRouter: PeerTubeRouterService,
119 private serverService: ServerService
dd24f1bb
C
120 ) {
121
122 }
123
124 ngOnInit () {
1b206245
C
125 const hiddenFilters = this.hideScopeFilter
126 ? [ 'scope' ]
127 : []
128
129 this.filters = new VideoFilters(this.defaultSort, this.defaultScope, hiddenFilters)
dd24f1bb
C
130 this.filters.load({ ...this.route.snapshot.queryParams, scope: this.defaultScope })
131
132 this.groupedDateLabels = {
133 [GroupDate.UNKNOWN]: null,
134 [GroupDate.TODAY]: $localize`Today`,
135 [GroupDate.YESTERDAY]: $localize`Yesterday`,
136 [GroupDate.THIS_WEEK]: $localize`This week`,
137 [GroupDate.THIS_MONTH]: $localize`This month`,
138 [GroupDate.LAST_MONTH]: $localize`Last month`,
139 [GroupDate.OLDER]: $localize`Older`
140 }
141
142 this.resizeSub = fromEvent(window, 'resize')
143 .pipe(debounceTime(500))
144 .subscribe(() => this.calcPageSizes())
145
146 this.calcPageSizes()
147
148 this.userService.getAnonymousOrLoggedUser()
149 .subscribe(user => {
150 this.userMiniature = user
151
152 if (this.loadUserVideoPreferences) {
153 this.loadUserSettings(user)
154 }
155
156 this.scheduleOnFiltersChanged(false)
157
158 this.subscribeToAnonymousUpdate()
159 this.subscribeToSearchChange()
160 })
161
162 // Display avatar in mobile view
163 if (this.screenService.isInMobileView()) {
164 this.displayOptions.avatar = true
165 }
166 }
167
168 ngOnDestroy () {
169 if (this.resizeSub) this.resizeSub.unsubscribe()
170 if (this.routeSub) this.routeSub.unsubscribe()
171 if (this.userSub) this.userSub.unsubscribe()
172 }
173
174 ngOnChanges (changes: SimpleChanges) {
2c102aac 175 if (changes['displayOptions'] || !this.displayOptions) {
176 this.displayOptions = {
177 ...this.defaultDisplayOptions,
178 avatar: this.serverService.getHTMLConfig().client.videos.miniature.displayAuthorAvatar,
179 ...changes['displayOptions']
180 }
181 }
182
dd24f1bb
C
183 if (!this.filters) return
184
185 let updated = false
186
187 if (changes['defaultScope']) {
188 updated = true
189 this.filters.setDefaultScope(this.defaultScope)
190 }
191
192 if (changes['defaultSort']) {
193 updated = true
194 this.filters.setDefaultSort(this.defaultSort)
195 }
196
197 if (!updated) return
198
199 const customizedByUser = this.hasBeenCustomizedByUser()
200
201 if (!customizedByUser) {
202 if (this.loadUserVideoPreferences) {
203 this.loadUserSettings(this.userMiniature)
204 }
205
206 this.filters.reset('scope')
207 this.filters.reset('sort')
208 }
209
210 this.scheduleOnFiltersChanged(customizedByUser)
211 }
212
213 videoById (_index: number, video: Video) {
214 return video.id
215 }
216
217 onNearOfBottom () {
218 if (this.disabled) return
219
220 // No more results
221 if (this.lastQueryLength !== undefined && this.lastQueryLength < this.pagination.itemsPerPage) return
222
223 this.pagination.currentPage += 1
224
225 this.loadMoreVideos()
226 }
227
228 loadMoreVideos (reset = false) {
c356907b
C
229 if (reset) this.hasDoneFirstQuery = false
230
dd24f1bb
C
231 this.getVideosObservableFunction(this.pagination, this.filters)
232 .subscribe({
233 next: ({ data }) => {
234 this.hasDoneFirstQuery = true
235 this.lastQueryLength = data.length
236
237 if (reset) this.videos = []
238 this.videos = this.videos.concat(data)
239
240 if (this.groupByDate) this.buildGroupedDateLabels()
241
242 this.onDataSubject.next(data)
243 },
244
245 error: err => {
246 const message = $localize`Cannot load more videos. Try again later.`
247
248 console.error(message, { err })
249 this.notifier.error(message)
250 }
251 })
252 }
253
254 reloadVideos () {
255 this.pagination.currentPage = 1
256 this.loadMoreVideos(true)
257 }
258
259 removeVideoFromArray (video: Video) {
260 this.videos = this.videos.filter(v => v.id !== video.id)
261 }
262
263 buildGroupedDateLabels () {
264 let currentGroupedDate: GroupDate = GroupDate.UNKNOWN
265
266 const periods = [
267 {
268 value: GroupDate.TODAY,
269 validator: (d: Date) => isToday(d)
270 },
271 {
272 value: GroupDate.YESTERDAY,
273 validator: (d: Date) => isYesterday(d)
274 },
275 {
276 value: GroupDate.THIS_WEEK,
277 validator: (d: Date) => isLastWeek(d)
278 },
279 {
280 value: GroupDate.THIS_MONTH,
281 validator: (d: Date) => isThisMonth(d)
282 },
283 {
284 value: GroupDate.LAST_MONTH,
285 validator: (d: Date) => isLastMonth(d)
286 },
287 {
288 value: GroupDate.OLDER,
289 validator: () => true
290 }
291 ]
292
293 for (const video of this.videos) {
294 const publishedDate = video.publishedAt
295
296 for (let i = 0; i < periods.length; i++) {
297 const period = periods[i]
298
299 if (currentGroupedDate <= period.value && period.validator(publishedDate)) {
300
301 if (currentGroupedDate !== period.value) {
302 currentGroupedDate = period.value
303 this.groupedDates[video.id] = currentGroupedDate
304 }
305
306 break
307 }
308 }
309 }
310 }
311
312 getCurrentGroupedDateLabel (video: Video) {
313 if (this.groupByDate === false) return undefined
314
315 return this.groupedDateLabels[this.groupedDates[video.id]]
316 }
317
318 scheduleOnFiltersChanged (customizedByUser: boolean) {
319 // We'll reload videos, but avoid weird UI effect
320 this.videos = []
321
322 setTimeout(() => this.onFiltersChanged(customizedByUser))
323 }
324
325 onFiltersChanged (customizedByUser: boolean) {
326 logger('Running on filters changed')
327
328 this.updateUrl(customizedByUser)
329
330 this.filters.triggerChange()
331
332 this.reloadSyndicationItems()
333 this.reloadVideos()
334 }
335
336 protected enableAllFilterIfPossible () {
337 if (!this.authService.isLoggedIn()) return
338
339 this.authService.userInformationLoaded
340 .subscribe(() => {
341 const user = this.authService.getUser()
342 this.displayModerationBlock = user.hasRight(UserRight.SEE_ALL_VIDEOS)
343 })
344 }
345
346 private calcPageSizes () {
347 if (this.screenService.isInMobileView()) {
348 this.pagination.itemsPerPage = 5
349 }
350 }
351
352 private loadUserSettings (user: User) {
353 this.filters.setNSFWPolicy(user.nsfwPolicy)
354
355 // Don't reset language filter if we don't want to refresh the component
356 if (!this.hasBeenCustomizedByUser()) {
357 this.filters.load({ languageOneOf: user.videoLanguages })
358 }
359 }
360
361 private reloadSyndicationItems () {
362 Promise.resolve(this.getSyndicationItemsFunction(this.filters))
363 .then(items => {
364 if (!items || items.length === 0) this.syndicationItems = undefined
365 else this.syndicationItems = items
366 })
367 .catch(err => console.error('Cannot get syndication items.', err))
368 }
369
370 private updateUrl (customizedByUser: boolean) {
371 const baseQuery = this.filters.toUrlObject()
372
373 // Set or reset customized by user query param
374 const queryParams = customizedByUser || this.hasBeenCustomizedByUser()
375 ? { ...baseQuery, c: customizedByUser }
376 : baseQuery
377
378 logger('Will inject %O in URL query', queryParams)
379
380 const baseRoute = this.baseRouteBuilderFunction
381 ? this.baseRouteBuilderFunction(this.filters)
382 : []
383
384 const pathname = window.location.pathname
385
386 const baseRouteChanged = baseRoute.length !== 0 &&
387 pathname !== '/' && // Exclude special '/' case, we'll be redirected without component change
388 baseRoute.length !== 0 && pathname !== baseRoute.join('/')
389
390 if (baseRouteChanged || Object.keys(baseQuery).length !== 0 || customizedByUser) {
391 this.peertubeRouter.silentNavigate(baseRoute, queryParams)
392 }
393
394 this.filtersChanged.emit(this.filters)
395 }
396
397 private hasBeenCustomizedByUser () {
398 return this.route.snapshot.queryParams['c'] === 'true'
399 }
400
401 private subscribeToAnonymousUpdate () {
402 this.userSub = this.userService.listenAnonymousUpdate()
403 .pipe(switchMap(() => this.userService.getAnonymousOrLoggedUser()))
404 .subscribe(user => {
405 if (this.loadUserVideoPreferences) {
406 this.loadUserSettings(user)
407 }
408
409 if (this.hasDoneFirstQuery) {
410 this.reloadVideos()
411 }
412 })
413 }
414
415 private subscribeToSearchChange () {
416 this.routeSub = this.route.queryParams.subscribe(param => {
417 if (!param['search']) return
418
419 this.filters.load({ search: param['search'] })
420 this.onFiltersChanged(true)
421 })
422 }
423}