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