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