6c8b8efee30f549898792919574178f988662d9f
[oweals/peertube.git] / client / src / app / header / search-typeahead.component.ts
1 import { of } from 'rxjs'
2 import { first, tap, delay } from 'rxjs/operators'
3 import { ListKeyManager } from '@angular/cdk/a11y'
4 import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren, AfterViewChecked } from '@angular/core'
5 import { ActivatedRoute, Params, Router } from '@angular/router'
6 import { AuthService, ServerService } from '@app/core'
7 import { ServerConfig } from '@shared/models'
8 import { SearchTargetType } from '@shared/models/search/search-target-query.model'
9 import { SuggestionComponent, SuggestionPayload, SuggestionPayloadType } from './suggestion.component'
10
11 @Component({
12   selector: 'my-search-typeahead',
13   templateUrl: './search-typeahead.component.html',
14   styleUrls: [ './search-typeahead.component.scss' ]
15 })
16 export class SearchTypeaheadComponent implements OnInit, AfterViewInit, AfterViewChecked, OnDestroy {
17   @ViewChildren(SuggestionComponent) suggestionItems: QueryList<SuggestionComponent>
18
19   hasChannel = false
20   inChannel = false
21   areSuggestionsOpened = true
22
23   search = ''
24   serverConfig: ServerConfig
25
26   inThisChannelText: string
27
28   keyboardEventsManager: ListKeyManager<SuggestionComponent>
29   results: SuggestionPayload[] = []
30
31   activeSearch: SuggestionPayloadType
32
33   private scheduleKeyboardEventsInit = false
34
35   constructor (
36     private authService: AuthService,
37     private router: Router,
38     private route: ActivatedRoute,
39     private serverService: ServerService
40   ) {}
41
42   ngOnInit () {
43     this.route.queryParams
44       .pipe(first(params => this.isOnSearch() && params.search !== undefined && params.search !== null))
45       .subscribe(params => this.search = params.search)
46   }
47
48   ngAfterViewInit () {
49     this.serverService.getConfig()
50       .subscribe(config => {
51         this.serverConfig = config
52
53         this.computeTypeahead()
54
55         this.serverService.configReloaded
56           .subscribe(config => {
57             this.serverConfig = config
58             this.computeTypeahead()
59           })
60       })
61   }
62
63   ngAfterViewChecked () {
64     if (this.scheduleKeyboardEventsInit && !this.keyboardEventsManager) {
65       // Avoid ExpressionChangedAfterItHasBeenCheckedError errors
66       setTimeout(() => this.initKeyboardEventsManager(), 0)
67     }
68   }
69
70   ngOnDestroy () {
71     if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
72   }
73
74   areInstructionsDisplayed () {
75     return !this.search
76   }
77
78   showSearchGlobalHelp () {
79     return this.search && this.areSuggestionsOpened && this.keyboardEventsManager?.activeItem?.result?.type === 'search-index'
80   }
81
82   canSearchAnyURI () {
83     if (!this.serverConfig) return false
84
85     return this.authService.isLoggedIn()
86       ? this.serverConfig.search.remoteUri.users
87       : this.serverConfig.search.remoteUri.anonymous
88   }
89
90   onSearchChange () {
91     this.computeTypeahead()
92   }
93
94   initKeyboardEventsManager () {
95     if (this.keyboardEventsManager) return
96
97     this.keyboardEventsManager = new ListKeyManager(this.suggestionItems)
98
99     const activeIndex = this.suggestionItems.toArray().findIndex(i => i.result.default === true)
100     if (activeIndex === -1) {
101       console.error('Cannot find active index.', { suggestionItems: this.suggestionItems })
102     }
103
104     this.updateItemsState(activeIndex)
105
106     this.keyboardEventsManager.change.subscribe(
107       _ => this.updateItemsState()
108     )
109   }
110
111   computeTypeahead () {
112     const searchIndexConfig = this.serverConfig.search.searchIndex
113
114     if (!this.activeSearch) {
115       if (searchIndexConfig.enabled && searchIndexConfig.isDefaultSearch) {
116         this.activeSearch = 'search-instance'
117       } else {
118         this.activeSearch = 'search-index'
119       }
120     }
121
122     this.areSuggestionsOpened = true
123     this.results = []
124
125     if (!this.search) return
126
127     if (searchIndexConfig.enabled === false || searchIndexConfig.disableLocalSearch !== true) {
128       this.results.push({
129         text: this.search,
130         type: 'search-instance',
131         default: this.activeSearch === 'search-instance'
132       })
133     }
134
135     if (searchIndexConfig.enabled) {
136       this.results.push({
137         text: this.search,
138         type: 'search-index',
139         default: this.activeSearch === 'search-index'
140       })
141     }
142
143     this.scheduleKeyboardEventsInit = true
144   }
145
146   updateItemsState (index?: number) {
147     if (index !== undefined) {
148       this.keyboardEventsManager.setActiveItem(index)
149     }
150
151     for (const item of this.suggestionItems) {
152       if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === item) {
153         item.active = true
154         this.activeSearch = item.result.type
155         continue
156       }
157
158       item.active = false
159     }
160   }
161
162   onSuggestionlicked (payload: SuggestionPayload) {
163     this.doSearch(this.buildSearchTarget(payload))
164   }
165
166   onSuggestionHover (index: number) {
167     this.updateItemsState(index)
168   }
169
170   handleKey (event: KeyboardEvent) {
171     if (!this.keyboardEventsManager) return
172
173     switch (event.key) {
174       case 'ArrowDown':
175       case 'ArrowUp':
176         event.stopPropagation()
177
178         this.keyboardEventsManager.onKeydown(event)
179         break
180     }
181   }
182
183   isOnSearch () {
184     return window.location.pathname === '/search'
185   }
186
187   doSearch (searchTarget?: SearchTargetType) {
188     this.areSuggestionsOpened = false
189     const queryParams: Params = {}
190
191     if (this.isOnSearch() && this.route.snapshot.queryParams) {
192       Object.assign(queryParams, this.route.snapshot.queryParams)
193     }
194
195     if (!searchTarget) {
196       searchTarget = this.buildSearchTarget(this.keyboardEventsManager.activeItem.result)
197     }
198
199     Object.assign(queryParams, { search: this.search, searchTarget })
200
201     const o = this.authService.isLoggedIn()
202       ? this.loadUserLanguagesIfNeeded(queryParams)
203       : of(true)
204
205     o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
206   }
207
208   private loadUserLanguagesIfNeeded (queryParams: any) {
209     if (queryParams && queryParams.languageOneOf) return of(queryParams)
210
211     return this.authService.userInformationLoaded
212                .pipe(
213                  first(),
214                  tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
215                )
216   }
217
218   private buildSearchTarget (result: SuggestionPayload): SearchTargetType {
219     if (result.type === 'search-index') {
220       return 'search-index'
221     }
222
223     return 'local'
224   }
225 }