]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - client/src/app/header/search-typeahead.component.ts
Improve search typeahead performance and use native events
[github/Chocobozzz/PeerTube.git] / client / src / app / header / search-typeahead.component.ts
index d12a9682e0dcb11aa276c5a87dd09a8b7a5a4bd6..c265f2c83d16777777621a8ba3205adffbed7471 100644 (file)
-import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core'
-import { Router, NavigationEnd } from '@angular/router'
-import { AuthService } from '@app/core'
-import { I18n } from '@ngx-translate/i18n-polyfill'
-import { filter } from 'rxjs/operators'
-import { ListKeyManager, ListKeyManagerOption } from '@angular/cdk/a11y'
+import {
+  Component,
+  AfterViewInit,
+  OnInit,
+  OnDestroy,
+  QueryList,
+  ViewChild,
+  ElementRef
+} from '@angular/core'
+import { Router, Params, ActivatedRoute } from '@angular/router'
+import { AuthService, ServerService } from '@app/core'
+import { first, tap } from 'rxjs/operators'
+import { ListKeyManager } from '@angular/cdk/a11y'
 import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'
+import { SuggestionComponent, Result } from './suggestion.component'
+import { of } from 'rxjs'
+import { getParameterByName } from '@app/shared/misc/utils'
+import { ServerConfig } from '@shared/models'
 
 @Component({
   selector: 'my-search-typeahead',
   templateUrl: './search-typeahead.component.html',
   styleUrls: [ './search-typeahead.component.scss' ]
 })
-export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
-  @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef
-  @ViewChild('optionsList', { static: true }) optionsList: ElementRef
+export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit {
+  @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement>
 
   hasChannel = false
   inChannel = false
-  keyboardEventsManager: ListKeyManager<ListKeyManagerOption>
+  newSearch = true
 
-  searchInput: HTMLInputElement
-  URIPolicy: 'only-followed' | 'any' = 'any'
+  search = ''
+  serverConfig: ServerConfig
 
-  URIPolicyText: string
-  inAllText: string
   inThisChannelText: string
 
+  keyboardEventsManager: ListKeyManager<SuggestionComponent>
   results: any[] = []
 
   constructor (
     private authService: AuthService,
     private router: Router,
-    private i18n: I18n
-  ) {
-    this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content from its URL, or if your instance only allows doing so for instances it follows.')
-    this.inAllText = this.i18n('In all PeerTube')
-    this.inThisChannelText = this.i18n('In this channel')
-  }
+    private route: ActivatedRoute,
+    private serverService: ServerService
+  ) {}
 
   ngOnInit () {
-    this.router.events
-      .pipe(filter(event => event instanceof NavigationEnd))
-      .subscribe((event: NavigationEnd) => {
-        this.hasChannel = event.url.startsWith('/videos/watch')
-        this.inChannel = event.url.startsWith('/video-channels')
-        this.computeResults()
-      })
+    this.serverService.getConfig()
+      .subscribe(config => this.serverConfig = config)
+  }
+
+  ngOnDestroy () {
+    if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
   }
 
   ngAfterViewInit () {
-    this.searchInput = this.contentWrapper.nativeElement.childNodes[0]
-    this.searchInput.addEventListener('input', this.computeResults.bind(this))
+    this.search = getParameterByName('search', window.location.href) || ''
+  }
+
+  get activeResult () {
+    return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result
+  }
+
+  get showInstructions () {
+    return !this.search
+  }
+
+  get showHelp () {
+    return this.search && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false
+  }
+
+  get anyURI () {
+    if (!this.serverConfig) return false
+    return this.authService.isLoggedIn()
+      ? this.serverConfig.search.remoteUri.users
+      : this.serverConfig.search.remoteUri.anonymous
   }
 
-  get hasSearch () {
-    return !!this.searchInput && !!this.searchInput.value
+  onSearchChange () {
+    this.computeResults()
   }
 
   computeResults () {
-    let results = [
-      {
-        text: 'MaĆ®tre poney',
-        type: 'channel'
-      }
-    ]
+    this.newSearch = true
+    let results: Result[] = []
 
-    if (this.hasSearch) {
+    if (this.search) {
       results = [
+        /* Channel search is still unimplemented. Uncomment when it is.
         {
-          text: this.searchInput.value,
+          text: this.search,
           type: 'search-channel'
         },
+        */
+        {
+          text: this.search,
+          type: 'search-instance',
+          default: true
+        },
+        /* Global search is still unimplemented. Uncomment when it is.
         {
-          text: this.searchInput.value,
+          text: this.search,
           type: 'search-global'
         },
+        */
         ...results
       ]
     }
 
     this.results = results.filter(
-      result => {
+      (result: Result) => {
         // if we're not in a channel or one of its videos/playlits, show all channel-related results
         if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel')
         // if we're in a channel, show all channel-related results except for the channel redirection itself
-        if (this.inChannel) return !(result.type === 'channel')
+        if (this.inChannel) return result.type !== 'channel'
+        // all other result types are kept
         return true
       }
     )
   }
 
-  isUserLoggedIn () {
-    return this.authService.isLoggedIn()
+  setEventItems (event: { items: QueryList<SuggestionComponent>, index?: number }) {
+    event.items.forEach(e => {
+      if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) {
+        this.keyboardEventsManager.activeItem.active = true
+      } else {
+        e.active = false
+      }
+    })
+  }
+
+  initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) {
+    if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
+    this.keyboardEventsManager = new ListKeyManager(event.items)
+    if (event.index !== undefined) {
+      this.keyboardEventsManager.setActiveItem(event.index)
+    } else {
+      this.keyboardEventsManager.setFirstItemActive()
+    }
+    this.keyboardEventsManager.change.subscribe(
+      _ => this.setEventItems(event)
+    )
   }
 
-  handleKeyUp (event: KeyboardEvent) {
+  handleKeyUp (event: KeyboardEvent, indexSelected?: number) {
     event.stopImmediatePropagation()
     if (this.keyboardEventsManager) {
       if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
-        // passing the event to key manager so we get a change fired
         this.keyboardEventsManager.onKeydown(event)
         return false
       } else if (event.keyCode === ENTER) {
-        // when we hit enter, the keyboardManager should call the selectItem method of the `ListItemComponent`
-        // this.keyboardEventsManager.activeItem
+        this.newSearch = false
+        this.doSearch()
         return false
       }
     }
   }
+
+  doSearch () {
+    const queryParams: Params = {}
+
+    if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
+      Object.assign(queryParams, this.route.snapshot.queryParams)
+    }
+
+    Object.assign(queryParams, { search: this.search })
+
+    const o = this.authService.isLoggedIn()
+      ? this.loadUserLanguagesIfNeeded(queryParams)
+      : of(true)
+
+    o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
+  }
+
+  private loadUserLanguagesIfNeeded (queryParams: any) {
+    if (queryParams && queryParams.languageOneOf) return of(queryParams)
+
+    return this.authService.userInformationLoaded
+               .pipe(
+                 first(),
+                 tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
+               )
+  }
 }