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