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