]>
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 C |
16 | import { GlobalIconName } from '@app/shared/shared-icons' |
17 | import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@shared/core-utils' | |
18 | import { ResultList, UserRight, VideoSortField } from '@shared/models' | |
19 | import { Syndication, Video } from '../shared-main' | |
20 | import { VideoFilters, VideoFilterScope } from './video-filters.model' | |
21 | import { MiniatureDisplayOptions } from './video-miniature.component' | |
22 | ||
23 | const logger = debug('peertube:videos:VideosListComponent') | |
24 | ||
25 | export type HeaderAction = { | |
26 | iconName: GlobalIconName | |
27 | label: string | |
28 | justIcon?: boolean | |
29 | routerLink?: string | |
30 | href?: string | |
31 | click?: (e: Event) => void | |
32 | } | |
33 | ||
34 | enum 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 | }) | |
49 | export 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 | } |