From 9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Tue, 4 Feb 2020 16:44:53 +0100 Subject: Improve search typeahead performance and use native events --- client/src/app/+accounts/accounts.component.html | 15 ++-- .../+video-channels/video-channels.component.html | 24 +++--- .../+video-channels/video-channels.component.scss | 20 +++++ client/src/app/header/header.component.html | 8 +- client/src/app/header/header.component.scss | 24 ------ client/src/app/header/header.component.ts | 16 +--- .../src/app/header/search-typeahead.component.html | 21 ++++-- .../src/app/header/search-typeahead.component.scss | 30 +++++++- .../src/app/header/search-typeahead.component.ts | 86 ++++++++-------------- client/src/app/header/suggestion.component.html | 12 +-- client/src/app/header/suggestion.component.ts | 18 +---- client/src/app/header/suggestions.component.html | 6 ++ client/src/app/header/suggestions.component.ts | 14 +--- client/src/app/shared/angular/highlight.pipe.ts | 74 ++++++++++--------- client/src/sass/application.scss | 5 +- client/src/sass/bootstrap.scss | 2 +- client/src/sass/include/_bootstrap-variables.scss | 5 +- client/src/sass/include/_mixins.scss | 13 +++- client/src/sass/include/_variables.scss | 2 + 19 files changed, 189 insertions(+), 206 deletions(-) create mode 100644 client/src/app/header/suggestions.component.html diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index b982fba9a..6a76393b9 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html @@ -7,14 +7,13 @@
{{ account.displayName }}
-
{{ account.nameWithHost }} - - - +
+ {{ account.nameWithHost }} +
Banned Muted diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html index 735a8f2c8..1087de113 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html @@ -7,25 +7,29 @@
{{ videoChannel.displayName }}
-
{{ videoChannel.nameWithHost }} +
+ {{ videoChannel.nameWithHost }}
+
-
- Manage - -
+
+ Manage +
-
{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
- - Created by {{ videoChannel.ownerBy }} - Owner account avatar - +
+
{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
+ + + Created by {{ videoChannel.ownerBy }} + Owner account avatar + +
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss index 50b69e7ac..aa26a7e7b 100644 --- a/client/src/app/+video-channels/video-channels.component.scss +++ b/client/src/app/+video-channels/video-channels.component.scss @@ -8,6 +8,23 @@ width: 100%; } + .actor-info { + display: grid !important; + grid-template-columns: 1fr auto; + grid-template-rows: 1fr auto / 1fr auto; + grid-template-areas: "name buttons" + "lower buttons"; + + @media screen and (max-width: #{map-get($grid-breakpoints, lg)}) { + grid-template-areas: "name name" + "lower buttons"; + } + } + + .actor-names { + grid-area: name; + } + .actor-name { flex-grow: 1; @@ -25,6 +42,9 @@ margin-left: auto; margin-top: 20px; + grid-row: buttons-start / span buttons-end; + grid-column: buttons-start; + a { @include peertube-button-outline; line-height: 1.8; diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html index 8e1d24ea8..49e219187 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html @@ -1,10 +1,4 @@ - - - - + diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss index 2f0a407fc..91b390773 100644 --- a/client/src/app/header/header.component.scss +++ b/client/src/app/header/header.component.scss @@ -5,30 +5,6 @@ my-search-typeahead { margin-right: 15px; } -#search-video { - @include peertube-input-text($search-input-width); - padding-left: 10px; - padding-right: 40px; // For the search icon - font-size: 14px; - - &::placeholder { - color: var(--inputPlaceholderColor); - } -} - -.icon.icon-search { - @include icon(25px); - height: 21px; - - background-color: var(--mainForegroundColor); - mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%; - - // yolo - position: absolute; - margin-left: -35px; - margin-top: 5px; -} - .upload-button { @include peertube-button-link; @include orange-button; diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts index d9311c554..cce76b0d1 100644 --- a/client/src/app/header/header.component.ts +++ b/client/src/app/header/header.component.ts @@ -1,5 +1,4 @@ -import { Component, OnInit } from '@angular/core' -import { I18n } from '@ngx-translate/i18n-polyfill' +import { Component } from '@angular/core' @Component({ selector: 'my-header', @@ -7,15 +6,4 @@ import { I18n } from '@ngx-translate/i18n-polyfill' styleUrls: [ './header.component.scss' ] }) -export class HeaderComponent implements OnInit { - searchValue = '' - ariaLabelTextForSearch = '' - - constructor ( - private i18n: I18n - ) {} - - ngOnInit () { - this.ariaLabelTextForSearch = this.i18n('Search videos, channels') - } -} +export class HeaderComponent {} diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html index 2623ba337..428246585 100644 --- a/client/src/app/header/search-typeahead.component.html +++ b/client/src/app/header/search-typeahead.component.html @@ -1,9 +1,13 @@ -
- +
+ +
- +
@@ -11,7 +15,7 @@
- using {{ globalSearchIndex }} + using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}
@@ -20,13 +24,14 @@
-
+
- - {URIPolicy, select, only-followed {only followed instances} other {any instance}} - + + any instance + only followed instances +
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss index 6d7511c70..a55e78326 100644 --- a/client/src/app/header/search-typeahead.component.scss +++ b/client/src/app/header/search-typeahead.component.scss @@ -3,6 +3,30 @@ @import '_bootstrap-variables'; @import '~bootstrap/scss/mixins/_breakpoints'; +#search-video { + @include peertube-input-text($search-input-width); + padding-left: 10px; + padding-right: 40px; // For the search icon + font-size: 14px; + + &::placeholder { + color: var(--inputPlaceholderColor); + } +} + +.icon.icon-search { + @include icon(25px); + height: 21px; + + background-color: var(--mainForegroundColor); + mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%; + + // yolo + position: absolute; + margin-left: -35px; + margin-top: 5px; +} + .jump-to-suggestions { top: 100%; left: 0; @@ -42,7 +66,7 @@ my-suggestions ::ng-deep ul { } #typeahead-container { - ::ng-deep input { + input { border: 1px solid var(--mainBackgroundColor) !important; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px; flex-grow: 1; @@ -56,12 +80,12 @@ my-suggestions ::ng-deep ul { @media screen and (max-width: $small-view) { flex: 1; - ::ng-deep input { + input { width: unset; } } - ::ng-deep span { + span { right: 10px; } diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts index 9b414bc56..c265f2c83 100644 --- a/client/src/app/header/search-typeahead.component.ts +++ b/client/src/app/header/search-typeahead.component.ts @@ -1,16 +1,15 @@ import { Component, - ViewChild, - ElementRef, AfterViewInit, OnInit, OnDestroy, - QueryList + QueryList, + ViewChild, + ElementRef } from '@angular/core' -import { Router, NavigationEnd, Params, ActivatedRoute } from '@angular/router' +import { Router, Params, ActivatedRoute } from '@angular/router' import { AuthService, ServerService } from '@app/core' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { filter, first, tap, map } from 'rxjs/operators' +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' @@ -24,19 +23,16 @@ import { ServerConfig } from '@shared/models' styleUrls: [ './search-typeahead.component.scss' ] }) export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit { - @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef + @ViewChild('searchVideo', { static: true }) searchInput: ElementRef hasChannel = false inChannel = false newSearch = true - searchInput: HTMLInputElement + search = '' serverConfig: ServerConfig - URIPolicyText: string - inAllText: string inThisChannelText: string - globalSearchIndex = 'https://index.joinpeertube.org' keyboardEventsManager: ListKeyManager results: any[] = [] @@ -45,30 +41,10 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni private authService: AuthService, private router: Router, private route: ActivatedRoute, - private serverService: ServerService, - private i18n: I18n - ) { - this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content, 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 serverService: ServerService + ) {} ngOnInit () { - this.router.events - .pipe(filter(e => e instanceof NavigationEnd)) - .subscribe((event: NavigationEnd) => { - this.hasChannel = event.url.startsWith('/videos/watch') - this.inChannel = event.url.startsWith('/video-channels') - this.computeResults() - }) - - this.router.events - .pipe( - filter(e => e instanceof NavigationEnd), - map(() => getParameterByName('search', window.location.href)) - ) - .subscribe(searchQuery => this.searchInput.value = searchQuery || '') - this.serverService.getConfig() .subscribe(config => this.serverConfig = config) } @@ -78,53 +54,52 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni } ngAfterViewInit () { - this.searchInput = this.contentWrapper.nativeElement.childNodes[0] - this.searchInput.addEventListener('input', this.computeResults.bind(this)) - this.searchInput.addEventListener('keyup', this.handleKeyUp.bind(this)) - } - - get hasSearch () { - return !!this.searchInput && !!this.searchInput.value + 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.hasSearch && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false + return this.search && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false } - get URIPolicy (): 'only-followed' | 'any' { - return ( - this.authService.isLoggedIn() - ? this.serverConfig.search.remoteUri.users - : this.serverConfig.search.remoteUri.anonymous - ) - ? 'any' - : 'only-followed' + get anyURI () { + if (!this.serverConfig) return false + return this.authService.isLoggedIn() + ? this.serverConfig.search.remoteUri.users + : this.serverConfig.search.remoteUri.anonymous + } + + onSearchChange () { + this.computeResults() } computeResults () { 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.searchInput.value, + 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' }, */ @@ -137,7 +112,8 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni // 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 } ) @@ -187,7 +163,7 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni Object.assign(queryParams, this.route.snapshot.queryParams) } - Object.assign(queryParams, { search: this.searchInput.value }) + Object.assign(queryParams, { search: this.search }) const o = this.authService.isLoggedIn() ? this.loadUserLanguagesIfNeeded(queryParams) diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html index 894cacb95..edde2023a 100644 --- a/client/src/app/header/suggestion.component.html +++ b/client/src/app/header/suggestion.component.html @@ -9,15 +9,9 @@
- - {{ inThisChannelText }} - - - {{ inThisInstanceText }} - - - {{ inAllText }} - + In this channel + In this instance + In the vidiverse
diff --git a/client/src/app/header/suggestion.component.ts b/client/src/app/header/suggestion.component.ts index bdcb3e03f..69641b511 100644 --- a/client/src/app/header/suggestion.component.ts +++ b/client/src/app/header/suggestion.component.ts @@ -1,6 +1,5 @@ -import { Input, Component, Output, EventEmitter, OnInit } from '@angular/core' +import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core' import { RouterLink } from '@angular/router' -import { I18n } from '@ngx-translate/i18n-polyfill' import { ListKeyManagerOption } from '@angular/cdk/a11y' export type Result = { @@ -13,28 +12,17 @@ export type Result = { @Component({ selector: 'my-suggestion', templateUrl: './suggestion.component.html', - styleUrls: [ './suggestion.component.scss' ] + styleUrls: [ './suggestion.component.scss' ], + changeDetection: ChangeDetectionStrategy.OnPush }) export class SuggestionComponent implements OnInit, ListKeyManagerOption { @Input() result: Result @Input() highlight: string @Output() selected = new EventEmitter() - inAllText: string - inThisChannelText: string - inThisInstanceText: string - disabled = false active = false - constructor ( - private i18n: I18n - ) { - this.inAllText = this.i18n('In the vidiverse') - this.inThisChannelText = this.i18n('In this channel') - this.inThisInstanceText = this.i18n('In this instance') - } - getLabel () { return this.result.text } diff --git a/client/src/app/header/suggestions.component.html b/client/src/app/header/suggestions.component.html new file mode 100644 index 000000000..8d017d78d --- /dev/null +++ b/client/src/app/header/suggestions.component.html @@ -0,0 +1,6 @@ +
    +
  • + +
  • +
\ No newline at end of file diff --git a/client/src/app/header/suggestions.component.ts b/client/src/app/header/suggestions.component.ts index fac7fe2f9..ee3ef73c2 100644 --- a/client/src/app/header/suggestions.component.ts +++ b/client/src/app/header/suggestions.component.ts @@ -1,16 +1,10 @@ -import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren } from '@angular/core' +import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core' import { SuggestionComponent } from './suggestion.component' @Component({ selector: 'my-suggestions', - template: ` -
    -
  • - -
  • -
- ` + templateUrl: './suggestions.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class SuggestionsComponent implements AfterViewInit { @Input() results: any[] @@ -20,7 +14,7 @@ export class SuggestionsComponent implements AfterViewInit { ngAfterViewInit () { this.listItems.changes.subscribe( - val => this.init.emit({ items: this.listItems }) + _ => this.init.emit({ items: this.listItems }) ) } diff --git a/client/src/app/shared/angular/highlight.pipe.ts b/client/src/app/shared/angular/highlight.pipe.ts index 4199d833e..e219b3823 100644 --- a/client/src/app/shared/angular/highlight.pipe.ts +++ b/client/src/app/shared/angular/highlight.pipe.ts @@ -5,48 +5,50 @@ import { SafeHtml } from '@angular/platform-browser' @Pipe({ name: 'highlight' }) export class HighlightPipe implements PipeTransform { /* use this for single match search */ - static SINGLE_MATCH: string = "Single-Match" + static SINGLE_MATCH = 'Single-Match' /* use this for single match search with a restriction that target should start with search string */ - static SINGLE_AND_STARTS_WITH_MATCH: string = "Single-And-StartsWith-Match" + static SINGLE_AND_STARTS_WITH_MATCH = 'Single-And-StartsWith-Match' /* use this for global search */ - static MULTI_MATCH: string = "Multi-Match" + static MULTI_MATCH = 'Multi-Match' - constructor() {} - transform( + // tslint:disable-next-line:no-empty + constructor () {} + + transform ( contentString: string = null, stringToHighlight: string = null, - option: string = "Single-And-StartsWith-Match", - caseSensitive: boolean = false, - highlightStyleName: string = "search-highlight" + option = 'Single-And-StartsWith-Match', + caseSensitive = false, + highlightStyleName = 'search-highlight' ): SafeHtml { - if (stringToHighlight && contentString && option) { - let regex: any = "" - let caseFlag: string = !caseSensitive ? "i" : "" - switch (option) { - case "Single-Match": { - regex = new RegExp(stringToHighlight, caseFlag) - break - } - case "Single-And-StartsWith-Match": { - regex = new RegExp("^" + stringToHighlight, caseFlag) - break - } - case "Multi-Match": { - regex = new RegExp(stringToHighlight, "g" + caseFlag) - break - } - default: { - // default will be a global case-insensitive match - regex = new RegExp(stringToHighlight, "gi") - } - } - const replaced = contentString.replace( - regex, - (match) => `${match}` - ) - return replaced - } else { - return contentString + if (stringToHighlight && contentString && option) { + let regex: any = '' + const caseFlag: string = !caseSensitive ? 'i' : '' + switch (option) { + case 'Single-Match': { + regex = new RegExp(stringToHighlight, caseFlag) + break + } + case 'Single-And-StartsWith-Match': { + regex = new RegExp("^" + stringToHighlight, caseFlag) + break + } + case 'Multi-Match': { + regex = new RegExp(stringToHighlight, 'g' + caseFlag) + break + } + default: { + // default will be a global case-insensitive match + regex = new RegExp(stringToHighlight, 'gi') + } } + const replaced = contentString.replace( + regex, + (match) => `${match}` + ) + return replaced + } else { + return contentString + } } } diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index 6bf345789..560414e90 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss @@ -1,5 +1,6 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/'; +@import '_bootstrap-variables'; @import '_variables'; @import '_mixins'; @@ -234,7 +235,7 @@ table { } } -@media screen and (max-width: 1600px) { +@media screen and (max-width: #{map-get($grid-breakpoints, xxl)}) { .main-col { &.expanded { .margin-content { @@ -245,7 +246,7 @@ table { } } -@media screen and (max-width: 900px) { +@media screen and (max-width: #{map-get($grid-breakpoints, lg)}) { .main-col { &.expanded { .margin-content { diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss index be5879b50..e10b84596 100644 --- a/client/src/sass/bootstrap.scss +++ b/client/src/sass/bootstrap.scss @@ -52,7 +52,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/'; } -@media screen and (min-width: 768px) { +@media screen and (min-width: #{map-get($grid-breakpoints, md)}) { .modal:before { vertical-align: middle; content: " "; diff --git a/client/src/sass/include/_bootstrap-variables.scss b/client/src/sass/include/_bootstrap-variables.scss index 7f413836b..b3ab0eb2b 100644 --- a/client/src/sass/include/_bootstrap-variables.scss +++ b/client/src/sass/include/_bootstrap-variables.scss @@ -13,8 +13,9 @@ $grid-breakpoints: ( md: 768px, // Large screen / desktop lg: 900px, - // Extra large screen / wide desktop - xl: 1200px + // Extra large screens / wide desktops + xl: 1200px, + xxl: 1600px ); $container-max-widths: ( diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index ed2cacdd1..4766e4490 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss @@ -445,7 +445,6 @@ @mixin actor-owner { @include disable-default-a-behaviour; - display: inline-table; font-size: 13px; margin-top: 4px; color: var(--mainForegroundColor); @@ -488,14 +487,15 @@ .actor-names { display: flex; align-items: center; + flex-wrap: wrap; .actor-display-name { font-size: 23px; font-weight: $font-bold; + margin-right: 7px; } .actor-name { - margin-left: 7px; position: relative; top: 3px; font-size: 14px; @@ -503,6 +503,10 @@ } } + .actor-lower { + grid-area: lower; + } + .actor-followers { font-size: 15px; } @@ -522,6 +526,11 @@ margin-bottom: 0; text-transform: uppercase; font-weight: 600; + font-size: 110%; + + @media screen and (max-width: $mobile-view) { + font-size: 130%; + } } } } diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index d0e1a8c9c..d8db3f3f8 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss @@ -1,3 +1,5 @@ +@import '_bootstrap-variables'; + $small-view: 800px; $mobile-view: 500px; -- cgit v1.2.3