Lazy load all routes
[oweals/peertube.git] / client / src / app / +search / search.component.ts
1 import { forkJoin, of, Subscription } from 'rxjs'
2 import { Component, OnDestroy, OnInit } from '@angular/core'
3 import { ActivatedRoute, Router } from '@angular/router'
4 import { AuthService, ComponentPagination, HooksService, Notifier, ServerService, User, UserService } from '@app/core'
5 import { immutableAssign } from '@app/helpers'
6 import { Video, VideoChannel } from '@app/shared/shared-main'
7 import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
8 import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
9 import { MetaService } from '@ngx-meta/core'
10 import { I18n } from '@ngx-translate/i18n-polyfill'
11 import { SearchTargetType, ServerConfig } from '@shared/models'
12
13 @Component({
14   selector: 'my-search',
15   styleUrls: [ './search.component.scss' ],
16   templateUrl: './search.component.html'
17 })
18 export class SearchComponent implements OnInit, OnDestroy {
19   results: (Video | VideoChannel)[] = []
20
21   pagination: ComponentPagination = {
22     currentPage: 1,
23     itemsPerPage: 10, // Only for videos, use another variable for channels
24     totalItems: null
25   }
26   advancedSearch: AdvancedSearch = new AdvancedSearch()
27   isSearchFilterCollapsed = true
28   currentSearch: string
29
30   videoDisplayOptions: MiniatureDisplayOptions = {
31     date: true,
32     views: true,
33     by: true,
34     avatar: false,
35     privacyLabel: false,
36     privacyText: false,
37     state: false,
38     blacklistInfo: false
39   }
40
41   errorMessage: string
42   serverConfig: ServerConfig
43
44   userMiniature: User
45
46   private subActivatedRoute: Subscription
47   private isInitialLoad = false // set to false to show the search filters on first arrival
48   private firstSearch = true
49
50   private channelsPerPage = 2
51
52   private lastSearchTarget: SearchTargetType
53
54   constructor (
55     private i18n: I18n,
56     private route: ActivatedRoute,
57     private router: Router,
58     private metaService: MetaService,
59     private notifier: Notifier,
60     private searchService: SearchService,
61     private authService: AuthService,
62     private userService: UserService,
63     private hooks: HooksService,
64     private serverService: ServerService
65   ) { }
66
67   ngOnInit () {
68     this.serverService.getConfig()
69       .subscribe(config => this.serverConfig = config)
70
71     this.subActivatedRoute = this.route.queryParams.subscribe(
72       async queryParams => {
73         const querySearch = queryParams['search']
74         const searchTarget = queryParams['searchTarget']
75
76         // Search updated, reset filters
77         if (this.currentSearch !== querySearch || searchTarget !== this.advancedSearch.searchTarget) {
78           this.resetPagination()
79           this.advancedSearch.reset()
80
81           this.currentSearch = querySearch || undefined
82           this.updateTitle()
83         }
84
85         this.advancedSearch = new AdvancedSearch(queryParams)
86         if (!this.advancedSearch.searchTarget) {
87           this.advancedSearch.searchTarget = await this.serverService.getDefaultSearchTarget()
88         }
89
90         // Don't hide filters if we have some of them AND the user just came on the webpage
91         this.isSearchFilterCollapsed = this.isInitialLoad === false || !this.advancedSearch.containsValues()
92         this.isInitialLoad = false
93
94         this.search()
95       },
96
97       err => this.notifier.error(err.text)
98     )
99
100     this.userService.getAnonymousOrLoggedUser()
101       .subscribe(user => this.userMiniature = user)
102
103     this.hooks.runAction('action:search.init', 'search')
104   }
105
106   ngOnDestroy () {
107     if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe()
108   }
109
110   isVideoChannel (d: VideoChannel | Video): d is VideoChannel {
111     return d instanceof VideoChannel
112   }
113
114   isVideo (v: VideoChannel | Video): v is Video {
115     return v instanceof Video
116   }
117
118   isUserLoggedIn () {
119     return this.authService.isLoggedIn()
120   }
121
122   search () {
123     forkJoin([
124       this.getVideosObs(),
125       this.getVideoChannelObs()
126     ]).subscribe(
127       ([videosResult, videoChannelsResult]) => {
128         this.results = this.results
129           .concat(videoChannelsResult.data)
130           .concat(videosResult.data)
131
132         this.pagination.totalItems = videosResult.total + videoChannelsResult.total
133         this.lastSearchTarget = this.advancedSearch.searchTarget
134
135         // Focus on channels if there are no enough videos
136         if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) {
137           this.resetPagination()
138           this.firstSearch = false
139
140           this.channelsPerPage = 10
141           this.search()
142         }
143
144         this.firstSearch = false
145       },
146
147       err => {
148         if (this.advancedSearch.searchTarget !== 'search-index') {
149           this.notifier.error(err.message)
150           return
151         }
152
153         this.notifier.error(
154           this.i18n('Search index is unavailable. Retrying with instance results instead.'),
155           this.i18n('Search error')
156         )
157         this.advancedSearch.searchTarget = 'local'
158         this.search()
159       }
160     )
161   }
162
163   onNearOfBottom () {
164     // Last page
165     if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
166
167     this.pagination.currentPage += 1
168     this.search()
169   }
170
171   onFiltered () {
172     this.resetPagination()
173
174     this.updateUrlFromAdvancedSearch()
175   }
176
177   numberOfFilters () {
178     return this.advancedSearch.size()
179   }
180
181   // Add VideoChannel for typings, but the template already checks "video" argument is a video
182   removeVideoFromArray (video: Video | VideoChannel) {
183     this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id)
184   }
185
186   getChannelUrl (channel: VideoChannel) {
187     if (this.advancedSearch.searchTarget === 'search-index' && channel.url) {
188       const remoteUriConfig = this.serverConfig.search.remoteUri
189
190       // Redirect on the external instance if not allowed to fetch remote data
191       const externalRedirect = (!this.authService.isLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users
192       const fromPath = window.location.pathname + window.location.search
193
194       return [ '/search/lazy-load-channel', { url: channel.url, externalRedirect, fromPath } ]
195     }
196
197     return [ '/video-channels', channel.nameWithHost ]
198   }
199
200   hideActions () {
201     return this.lastSearchTarget === 'search-index'
202   }
203
204   private resetPagination () {
205     this.pagination.currentPage = 1
206     this.pagination.totalItems = null
207     this.channelsPerPage = 2
208
209     this.results = []
210   }
211
212   private updateTitle () {
213     const suffix = this.currentSearch ? ' ' + this.currentSearch : ''
214     this.metaService.setTitle(this.i18n('Search') + suffix)
215   }
216
217   private updateUrlFromAdvancedSearch () {
218     const search = this.currentSearch || undefined
219
220     this.router.navigate([], {
221       relativeTo: this.route,
222       queryParams: Object.assign({}, this.advancedSearch.toUrlObject(), { search })
223     })
224   }
225
226   private getVideosObs () {
227     const params = {
228       search: this.currentSearch,
229       componentPagination: this.pagination,
230       advancedSearch: this.advancedSearch
231     }
232
233     return this.hooks.wrapObsFun(
234       this.searchService.searchVideos.bind(this.searchService),
235       params,
236       'search',
237       'filter:api.search.videos.list.params',
238       'filter:api.search.videos.list.result'
239     )
240   }
241
242   private getVideoChannelObs () {
243     if (!this.currentSearch) return of({ data: [], total: 0 })
244
245     const params = {
246       search: this.currentSearch,
247       componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }),
248       searchTarget: this.advancedSearch.searchTarget
249     }
250
251     return this.hooks.wrapObsFun(
252       this.searchService.searchVideoChannels.bind(this.searchService),
253       params,
254       'search',
255       'filter:api.search.video-channels.list.params',
256       'filter:api.search.video-channels.list.result'
257     )
258   }
259 }