396a875c872c0f6f63ae98814887e9ce74b12da0
[oweals/peertube.git] / client / src / app / header / search-typeahead.component.ts
1 import { Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild } from '@angular/core'
2 import { ActivatedRoute, Params, Router } from '@angular/router'
3 import { AuthService, ServerService } from '@app/core'
4 import { first, tap } from 'rxjs/operators'
5 import { ListKeyManager } from '@angular/cdk/a11y'
6 import { Result, SuggestionComponent } from './suggestion.component'
7 import { of } from 'rxjs'
8 import { ServerConfig } from '@shared/models'
9
10 @Component({
11   selector: 'my-search-typeahead',
12   templateUrl: './search-typeahead.component.html',
13   styleUrls: [ './search-typeahead.component.scss' ]
14 })
15 export class SearchTypeaheadComponent implements OnInit, OnDestroy {
16   @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement>
17
18   hasChannel = false
19   inChannel = false
20   newSearch = true
21
22   search = ''
23   serverConfig: ServerConfig
24
25   inThisChannelText: string
26
27   keyboardEventsManager: ListKeyManager<SuggestionComponent>
28   results: Result[] = []
29
30   constructor (
31     private authService: AuthService,
32     private router: Router,
33     private route: ActivatedRoute,
34     private serverService: ServerService
35   ) {}
36
37   ngOnInit () {
38     this.route.queryParams
39       .pipe(first(params => params.search !== undefined && params.search !== null))
40       .subscribe(params => this.search = params.search)
41     this.serverService.getConfig()
42       .subscribe(config => this.serverConfig = config)
43   }
44
45   ngOnDestroy () {
46     if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
47   }
48
49   get activeResult () {
50     return this.keyboardEventsManager?.activeItem?.result
51   }
52
53   get areInstructionsDisplayed () {
54     return !this.search
55   }
56
57   get showHelp () {
58     return this.search && this.newSearch && this.activeResult?.type === 'search-global'
59   }
60
61   get canSearchAnyURI () {
62     if (!this.serverConfig) return false
63     return this.authService.isLoggedIn()
64       ? this.serverConfig.search.remoteUri.users
65       : this.serverConfig.search.remoteUri.anonymous
66   }
67
68   onSearchChange () {
69     this.computeResults()
70   }
71
72   computeResults () {
73     this.newSearch = true
74     let results: Result[] = []
75
76     if (this.search) {
77       results = [
78         /* Channel search is still unimplemented. Uncomment when it is.
79         {
80           text: this.search,
81           type: 'search-channel'
82         },
83         */
84         {
85           text: this.search,
86           type: 'search-instance',
87           default: true
88         },
89         /* Global search is still unimplemented. Uncomment when it is.
90         {
91           text: this.search,
92           type: 'search-global'
93         },
94         */
95         ...results
96       ]
97     }
98
99     this.results = results.filter(
100       (result: Result) => {
101         // if we're not in a channel or one of its videos/playlits, show all channel-related results
102         if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel')
103         // if we're in a channel, show all channel-related results except for the channel redirection itself
104         if (this.inChannel) return result.type !== 'channel'
105         // all other result types are kept
106         return true
107       }
108     )
109   }
110
111   setEventItems (event: { items: QueryList<SuggestionComponent>, index?: number }) {
112     event.items.forEach(e => {
113       if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) {
114         this.keyboardEventsManager.activeItem.active = true
115       } else {
116         e.active = false
117       }
118     })
119   }
120
121   initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) {
122     if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
123
124     this.keyboardEventsManager = new ListKeyManager(event.items)
125
126     if (event.index !== undefined) {
127       this.keyboardEventsManager.setActiveItem(event.index)
128     } else {
129       this.keyboardEventsManager.setFirstItemActive()
130     }
131
132     this.keyboardEventsManager.change.subscribe(
133       _ => this.setEventItems(event)
134     )
135   }
136
137   handleKeyUp (event: KeyboardEvent) {
138     event.stopImmediatePropagation()
139     if (!this.keyboardEventsManager) return
140
141     switch (event.key) {
142       case 'ArrowDown':
143       case 'ArrowUp':
144         this.keyboardEventsManager.onKeydown(event)
145         break
146       case 'Enter':
147         this.newSearch = false
148         this.doSearch()
149         break
150     }
151   }
152
153   doSearch () {
154     const queryParams: Params = {}
155
156     if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
157       Object.assign(queryParams, this.route.snapshot.queryParams)
158     }
159
160     Object.assign(queryParams, { search: this.search })
161
162     const o = this.authService.isLoggedIn()
163       ? this.loadUserLanguagesIfNeeded(queryParams)
164       : of(true)
165
166     o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
167   }
168
169   private loadUserLanguagesIfNeeded (queryParams: any) {
170     if (queryParams && queryParams.languageOneOf) return of(queryParams)
171
172     return this.authService.userInformationLoaded
173                .pipe(
174                  first(),
175                  tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
176                )
177   }
178 }