aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-02-04 16:44:53 +0100
committerRigel Kent <sendmemail@rigelk.eu>2020-02-13 16:35:24 +0100
commit9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110 (patch)
treef38e6f83a9d892a99f930c0a25b1a405e679cd4a
parentece3029bd99a76b3c48a1cc8c58914c5cf61f106 (diff)
downloadPeerTube-9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110.tar.gz
PeerTube-9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110.tar.zst
PeerTube-9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110.zip
Improve search typeahead performance and use native events
-rw-r--r--client/src/app/+accounts/accounts.component.html15
-rw-r--r--client/src/app/+video-channels/video-channels.component.html24
-rw-r--r--client/src/app/+video-channels/video-channels.component.scss20
-rw-r--r--client/src/app/header/header.component.html8
-rw-r--r--client/src/app/header/header.component.scss24
-rw-r--r--client/src/app/header/header.component.ts16
-rw-r--r--client/src/app/header/search-typeahead.component.html21
-rw-r--r--client/src/app/header/search-typeahead.component.scss30
-rw-r--r--client/src/app/header/search-typeahead.component.ts86
-rw-r--r--client/src/app/header/suggestion.component.html12
-rw-r--r--client/src/app/header/suggestion.component.ts18
-rw-r--r--client/src/app/header/suggestions.component.html6
-rw-r--r--client/src/app/header/suggestions.component.ts14
-rw-r--r--client/src/app/shared/angular/highlight.pipe.ts74
-rw-r--r--client/src/sass/application.scss5
-rw-r--r--client/src/sass/bootstrap.scss2
-rw-r--r--client/src/sass/include/_bootstrap-variables.scss5
-rw-r--r--client/src/sass/include/_mixins.scss13
-rw-r--r--client/src/sass/include/_variables.scss2
19 files changed, 189 insertions, 206 deletions
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 @@
7 <div class="actor-info"> 7 <div class="actor-info">
8 <div class="actor-names"> 8 <div class="actor-names">
9 <div class="actor-display-name">{{ account.displayName }}</div> 9 <div class="actor-display-name">{{ account.displayName }}</div>
10 <div class="actor-name">{{ account.nameWithHost }} 10 <div class="actor-name">
11 11 <span>{{ account.nameWithHost }}</span>
12 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" 12 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
13 class="btn btn-outline-secondary btn-sm copy-button" 13 class="btn btn-outline-secondary btn-sm copy-button"
14 > 14 >
15 <span class="glyphicon glyphicon-copy"></span> 15 <span class="glyphicon glyphicon-copy"></span>
16 </button> 16 </button>
17
18 </div> 17 </div>
19 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> 18 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
20 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> 19 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
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 @@
7 <div class="actor-info"> 7 <div class="actor-info">
8 <div class="actor-names"> 8 <div class="actor-names">
9 <div class="actor-display-name">{{ videoChannel.displayName }}</div> 9 <div class="actor-display-name">{{ videoChannel.displayName }}</div>
10 <div class="actor-name">{{ videoChannel.nameWithHost }} 10 <div class="actor-name">
11 <span>{{ videoChannel.nameWithHost }}</span>
11 <button [cdkCopyToClipboard]="videoChannel.nameWithHost" (click)="activateCopiedMessage()" 12 <button [cdkCopyToClipboard]="videoChannel.nameWithHost" (click)="activateCopiedMessage()"
12 class="btn btn-outline-secondary btn-sm copy-button" 13 class="btn btn-outline-secondary btn-sm copy-button"
13 > 14 >
14 <span class="glyphicon glyphicon-copy"></span> 15 <span class="glyphicon glyphicon-copy"></span>
15 </button> 16 </button>
16 </div> 17 </div>
18 </div>
17 19
18 <div class="right-buttons"> 20 <div class="right-buttons">
19 <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a> 21 <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a>
20 <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> 22 <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
21 </div>
22 </div> 23 </div>
23 <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
24 24
25 <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> 25 <div class="actor-lower">
26 <span i18n>Created by {{ videoChannel.ownerBy }}</span> 26 <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
27 <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> 27
28 </a> 28 <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
29 <span i18n>Created by {{ videoChannel.ownerBy }}</span>
30 <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
31 </a>
32 </div>
29 </div> 33 </div>
30 </div> 34 </div>
31 35
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 @@
8 width: 100%; 8 width: 100%;
9 } 9 }
10 10
11 .actor-info {
12 display: grid !important;
13 grid-template-columns: 1fr auto;
14 grid-template-rows: 1fr auto / 1fr auto;
15 grid-template-areas: "name buttons"
16 "lower buttons";
17
18 @media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
19 grid-template-areas: "name name"
20 "lower buttons";
21 }
22 }
23
24 .actor-names {
25 grid-area: name;
26 }
27
11 .actor-name { 28 .actor-name {
12 flex-grow: 1; 29 flex-grow: 1;
13 30
@@ -25,6 +42,9 @@
25 margin-left: auto; 42 margin-left: auto;
26 margin-top: 20px; 43 margin-top: 20px;
27 44
45 grid-row: buttons-start / span buttons-end;
46 grid-column: buttons-start;
47
28 a { 48 a {
29 @include peertube-button-outline; 49 @include peertube-button-outline;
30 line-height: 1.8; 50 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 @@
1<my-search-typeahead class="w-100 d-flex justify-content-end"> 1<my-search-typeahead class="w-100 d-flex justify-content-end"></my-search-typeahead>
2 <input
3 type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch"
4 i18n-placeholder placeholder="Search videos, channels…" [(ngModel)]="searchValue"
5 >
6 <span class="icon icon-search"></span>
7</my-search-typeahead>
8 2
9<a class="upload-button" routerLink="/videos/upload"> 3<a class="upload-button" routerLink="/videos/upload">
10 <my-global-icon iconName="upload"></my-global-icon> 4 <my-global-icon iconName="upload"></my-global-icon>
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 {
5 margin-right: 15px; 5 margin-right: 15px;
6} 6}
7 7
8#search-video {
9 @include peertube-input-text($search-input-width);
10 padding-left: 10px;
11 padding-right: 40px; // For the search icon
12 font-size: 14px;
13
14 &::placeholder {
15 color: var(--inputPlaceholderColor);
16 }
17}
18
19.icon.icon-search {
20 @include icon(25px);
21 height: 21px;
22
23 background-color: var(--mainForegroundColor);
24 mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
25
26 // yolo
27 position: absolute;
28 margin-left: -35px;
29 margin-top: 5px;
30}
31
32.upload-button { 8.upload-button {
33 @include peertube-button-link; 9 @include peertube-button-link;
34 @include orange-button; 10 @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 @@
1import { Component, OnInit } from '@angular/core' 1import { Component } from '@angular/core'
2import { I18n } from '@ngx-translate/i18n-polyfill'
3 2
4@Component({ 3@Component({
5 selector: 'my-header', 4 selector: 'my-header',
@@ -7,15 +6,4 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
7 styleUrls: [ './header.component.scss' ] 6 styleUrls: [ './header.component.scss' ]
8}) 7})
9 8
10export class HeaderComponent implements OnInit { 9export class HeaderComponent {}
11 searchValue = ''
12 ariaLabelTextForSearch = ''
13
14 constructor (
15 private i18n: I18n
16 ) {}
17
18 ngOnInit () {
19 this.ariaLabelTextForSearch = this.i18n('Search videos, channels')
20 }
21}
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 @@
1<div class="d-inline-flex position-relative" id="typeahead-container" #contentWrapper> 1<div class="d-inline-flex position-relative" id="typeahead-container">
2 <ng-content></ng-content> 2 <input
3 type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…"
4 [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keyup)="handleKeyUp($event)"
5 >
6 <span class="icon icon-search" (click)="doSearch()"></span>
3 7
4 <div class="position-absolute jump-to-suggestions"> 8 <div class="position-absolute jump-to-suggestions">
5 <!-- suggestions --> 9 <!-- suggestions -->
6 <my-suggestions *ngIf="hasSearch && newSearch" [results]="results" [highlight]="searchInput.value" (init)="initKeyboardEventsManager($event)" #suggestions></my-suggestions> 10 <my-suggestions *ngIf="search && newSearch" [results]="results" [highlight]="search" (init)="initKeyboardEventsManager($event)"></my-suggestions>
7 11
8 <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion --> 12 <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
9 <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden"> 13 <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden">
@@ -11,7 +15,7 @@
11 <div class="d-flex justify-content-between"> 15 <div class="d-flex justify-content-between">
12 <label class="small-title" i18n>Global search</label> 16 <label class="small-title" i18n>Global search</label>
13 <div class="advanced-search-status text-muted"> 17 <div class="advanced-search-status text-muted">
14 <span class="mr-1" i18n>using {{ globalSearchIndex }}</span> 18 <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}</span>
15 <i class="glyphicon glyphicon-globe"></i> 19 <i class="glyphicon glyphicon-globe"></i>
16 </div> 20 </div>
17 </div> 21 </div>
@@ -20,13 +24,14 @@
20 </div> 24 </div>
21 25
22 <!-- search instructions, when search input is empty --> 26 <!-- search instructions, when search input is empty -->
23 <div *ngIf="!hasSearch" id="typeahead-instructions" class="overflow-hidden"> 27 <div *ngIf="showInstructions" id="typeahead-instructions" class="overflow-hidden">
24 <div class="d-flex justify-content-between"> 28 <div class="d-flex justify-content-between">
25 <label class="small-title" i18n>Advanced search</label> 29 <label class="small-title" i18n>Advanced search</label>
26 <div class="advanced-search-status c-help"> 30 <div class="advanced-search-status c-help">
27 <span [ngClass]="URIPolicy === 'any' ? 'text-success' : 'text-muted'" [title]="URIPolicyText"> 31 <span [ngClass]="anyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
28 <span class="mr-1" i18n>{URIPolicy, select, only-followed {only followed instances} other {any instance}} </span> 32 <span *ngIf="anyURI" class="mr-1" i18n>any instance</span>
29 <i [ngClass]="URIPolicy === 'any' ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i> 33 <span *ngIf="!anyURI" class="mr-1" i18n>only followed instances</span>
34 <i [ngClass]="anyURI ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
30 </span> 35 </span>
31 </div> 36 </div>
32 </div> 37 </div>
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 @@
3@import '_bootstrap-variables'; 3@import '_bootstrap-variables';
4@import '~bootstrap/scss/mixins/_breakpoints'; 4@import '~bootstrap/scss/mixins/_breakpoints';
5 5
6#search-video {
7 @include peertube-input-text($search-input-width);
8 padding-left: 10px;
9 padding-right: 40px; // For the search icon
10 font-size: 14px;
11
12 &::placeholder {
13 color: var(--inputPlaceholderColor);
14 }
15}
16
17.icon.icon-search {
18 @include icon(25px);
19 height: 21px;
20
21 background-color: var(--mainForegroundColor);
22 mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
23
24 // yolo
25 position: absolute;
26 margin-left: -35px;
27 margin-top: 5px;
28}
29
6.jump-to-suggestions { 30.jump-to-suggestions {
7 top: 100%; 31 top: 100%;
8 left: 0; 32 left: 0;
@@ -42,7 +66,7 @@ my-suggestions ::ng-deep ul {
42} 66}
43 67
44#typeahead-container { 68#typeahead-container {
45 ::ng-deep input { 69 input {
46 border: 1px solid var(--mainBackgroundColor) !important; 70 border: 1px solid var(--mainBackgroundColor) !important;
47 box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px; 71 box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
48 flex-grow: 1; 72 flex-grow: 1;
@@ -56,12 +80,12 @@ my-suggestions ::ng-deep ul {
56 @media screen and (max-width: $small-view) { 80 @media screen and (max-width: $small-view) {
57 flex: 1; 81 flex: 1;
58 82
59 ::ng-deep input { 83 input {
60 width: unset; 84 width: unset;
61 } 85 }
62 } 86 }
63 87
64 ::ng-deep span { 88 span {
65 right: 10px; 89 right: 10px;
66 } 90 }
67 91
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 @@
1import { 1import {
2 Component, 2 Component,
3 ViewChild,
4 ElementRef,
5 AfterViewInit, 3 AfterViewInit,
6 OnInit, 4 OnInit,
7 OnDestroy, 5 OnDestroy,
8 QueryList 6 QueryList,
7 ViewChild,
8 ElementRef
9} from '@angular/core' 9} from '@angular/core'
10import { Router, NavigationEnd, Params, ActivatedRoute } from '@angular/router' 10import { Router, Params, ActivatedRoute } from '@angular/router'
11import { AuthService, ServerService } from '@app/core' 11import { AuthService, ServerService } from '@app/core'
12import { I18n } from '@ngx-translate/i18n-polyfill' 12import { first, tap } from 'rxjs/operators'
13import { filter, first, tap, map } from 'rxjs/operators'
14import { ListKeyManager } from '@angular/cdk/a11y' 13import { ListKeyManager } from '@angular/cdk/a11y'
15import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes' 14import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'
16import { SuggestionComponent, Result } from './suggestion.component' 15import { SuggestionComponent, Result } from './suggestion.component'
@@ -24,19 +23,16 @@ import { ServerConfig } from '@shared/models'
24 styleUrls: [ './search-typeahead.component.scss' ] 23 styleUrls: [ './search-typeahead.component.scss' ]
25}) 24})
26export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit { 25export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit {
27 @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef 26 @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement>
28 27
29 hasChannel = false 28 hasChannel = false
30 inChannel = false 29 inChannel = false
31 newSearch = true 30 newSearch = true
32 31
33 searchInput: HTMLInputElement 32 search = ''
34 serverConfig: ServerConfig 33 serverConfig: ServerConfig
35 34
36 URIPolicyText: string
37 inAllText: string
38 inThisChannelText: string 35 inThisChannelText: string
39 globalSearchIndex = 'https://index.joinpeertube.org'
40 36
41 keyboardEventsManager: ListKeyManager<SuggestionComponent> 37 keyboardEventsManager: ListKeyManager<SuggestionComponent>
42 results: any[] = [] 38 results: any[] = []
@@ -45,30 +41,10 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
45 private authService: AuthService, 41 private authService: AuthService,
46 private router: Router, 42 private router: Router,
47 private route: ActivatedRoute, 43 private route: ActivatedRoute,
48 private serverService: ServerService, 44 private serverService: ServerService
49 private i18n: I18n 45 ) {}
50 ) {
51 this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content, or if your instance only allows doing so for instances it follows.')
52 this.inAllText = this.i18n('In all PeerTube')
53 this.inThisChannelText = this.i18n('In this channel')
54 }
55 46
56 ngOnInit () { 47 ngOnInit () {
57 this.router.events
58 .pipe(filter(e => e instanceof NavigationEnd))
59 .subscribe((event: NavigationEnd) => {
60 this.hasChannel = event.url.startsWith('/videos/watch')
61 this.inChannel = event.url.startsWith('/video-channels')
62 this.computeResults()
63 })
64
65 this.router.events
66 .pipe(
67 filter(e => e instanceof NavigationEnd),
68 map(() => getParameterByName('search', window.location.href))
69 )
70 .subscribe(searchQuery => this.searchInput.value = searchQuery || '')
71
72 this.serverService.getConfig() 48 this.serverService.getConfig()
73 .subscribe(config => this.serverConfig = config) 49 .subscribe(config => this.serverConfig = config)
74 } 50 }
@@ -78,53 +54,52 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
78 } 54 }
79 55
80 ngAfterViewInit () { 56 ngAfterViewInit () {
81 this.searchInput = this.contentWrapper.nativeElement.childNodes[0] 57 this.search = getParameterByName('search', window.location.href) || ''
82 this.searchInput.addEventListener('input', this.computeResults.bind(this))
83 this.searchInput.addEventListener('keyup', this.handleKeyUp.bind(this))
84 }
85
86 get hasSearch () {
87 return !!this.searchInput && !!this.searchInput.value
88 } 58 }
89 59
90 get activeResult () { 60 get activeResult () {
91 return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result 61 return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result
92 } 62 }
93 63
64 get showInstructions () {
65 return !this.search
66 }
67
94 get showHelp () { 68 get showHelp () {
95 return this.hasSearch && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false 69 return this.search && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false
96 } 70 }
97 71
98 get URIPolicy (): 'only-followed' | 'any' { 72 get anyURI () {
99 return ( 73 if (!this.serverConfig) return false
100 this.authService.isLoggedIn() 74 return this.authService.isLoggedIn()
101 ? this.serverConfig.search.remoteUri.users 75 ? this.serverConfig.search.remoteUri.users
102 : this.serverConfig.search.remoteUri.anonymous 76 : this.serverConfig.search.remoteUri.anonymous
103 ) 77 }
104 ? 'any' 78
105 : 'only-followed' 79 onSearchChange () {
80 this.computeResults()
106 } 81 }
107 82
108 computeResults () { 83 computeResults () {
109 this.newSearch = true 84 this.newSearch = true
110 let results: Result[] = [] 85 let results: Result[] = []
111 86
112 if (this.hasSearch) { 87 if (this.search) {
113 results = [ 88 results = [
114 /* Channel search is still unimplemented. Uncomment when it is. 89 /* Channel search is still unimplemented. Uncomment when it is.
115 { 90 {
116 text: this.searchInput.value, 91 text: this.search,
117 type: 'search-channel' 92 type: 'search-channel'
118 }, 93 },
119 */ 94 */
120 { 95 {
121 text: this.searchInput.value, 96 text: this.search,
122 type: 'search-instance', 97 type: 'search-instance',
123 default: true 98 default: true
124 }, 99 },
125 /* Global search is still unimplemented. Uncomment when it is. 100 /* Global search is still unimplemented. Uncomment when it is.
126 { 101 {
127 text: this.searchInput.value, 102 text: this.search,
128 type: 'search-global' 103 type: 'search-global'
129 }, 104 },
130 */ 105 */
@@ -137,7 +112,8 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
137 // if we're not in a channel or one of its videos/playlits, show all channel-related results 112 // if we're not in a channel or one of its videos/playlits, show all channel-related results
138 if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') 113 if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel')
139 // if we're in a channel, show all channel-related results except for the channel redirection itself 114 // if we're in a channel, show all channel-related results except for the channel redirection itself
140 if (this.inChannel) return !(result.type === 'channel') 115 if (this.inChannel) return result.type !== 'channel'
116 // all other result types are kept
141 return true 117 return true
142 } 118 }
143 ) 119 )
@@ -187,7 +163,7 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
187 Object.assign(queryParams, this.route.snapshot.queryParams) 163 Object.assign(queryParams, this.route.snapshot.queryParams)
188 } 164 }
189 165
190 Object.assign(queryParams, { search: this.searchInput.value }) 166 Object.assign(queryParams, { search: this.search })
191 167
192 const o = this.authService.isLoggedIn() 168 const o = this.authService.isLoggedIn()
193 ? this.loadUserLanguagesIfNeeded(queryParams) 169 ? 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 @@
9 <div class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target" [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight"></div> 9 <div class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target" [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight"></div>
10 10
11 <div *ngIf="result.type !== 'channel' && result.type !== 'suggestion'" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6"> 11 <div *ngIf="result.type !== 'channel' && result.type !== 'suggestion'" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6">
12 <span *ngIf="result.type === 'search-channel'" [attr.aria-label]="inThisChannelText"> 12 <span *ngIf="result.type === 'search-channel'" i18n>In this channel</span>
13 {{ inThisChannelText }} 13 <span *ngIf="result.type === 'search-instance'" i18n>In this instance</span>
14 </span> 14 <span *ngIf="result.type === 'search-global'" i18n>In the vidiverse</span>
15 <span *ngIf="result.type === 'search-instance'" [attr.aria-label]="inThisInstanceText">
16 {{ inThisInstanceText }}
17 </span>
18 <span *ngIf="result.type === 'search-global'" [attr.aria-label]="inAllText">
19 {{ inAllText }}
20 </span>
21 <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span> 15 <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
22 </div> 16 </div>
23 17
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 @@
1import { Input, Component, Output, EventEmitter, OnInit } from '@angular/core' 1import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core'
2import { RouterLink } from '@angular/router' 2import { RouterLink } from '@angular/router'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { ListKeyManagerOption } from '@angular/cdk/a11y' 3import { ListKeyManagerOption } from '@angular/cdk/a11y'
5 4
6export type Result = { 5export type Result = {
@@ -13,28 +12,17 @@ export type Result = {
13@Component({ 12@Component({
14 selector: 'my-suggestion', 13 selector: 'my-suggestion',
15 templateUrl: './suggestion.component.html', 14 templateUrl: './suggestion.component.html',
16 styleUrls: [ './suggestion.component.scss' ] 15 styleUrls: [ './suggestion.component.scss' ],
16 changeDetection: ChangeDetectionStrategy.OnPush
17}) 17})
18export class SuggestionComponent implements OnInit, ListKeyManagerOption { 18export class SuggestionComponent implements OnInit, ListKeyManagerOption {
19 @Input() result: Result 19 @Input() result: Result
20 @Input() highlight: string 20 @Input() highlight: string
21 @Output() selected = new EventEmitter() 21 @Output() selected = new EventEmitter()
22 22
23 inAllText: string
24 inThisChannelText: string
25 inThisInstanceText: string
26
27 disabled = false 23 disabled = false
28 active = false 24 active = false
29 25
30 constructor (
31 private i18n: I18n
32 ) {
33 this.inAllText = this.i18n('In the vidiverse')
34 this.inThisChannelText = this.i18n('In this channel')
35 this.inThisInstanceText = this.i18n('In this instance')
36 }
37
38 getLabel () { 26 getLabel () {
39 return this.result.text 27 return this.result.text
40 } 28 }
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 @@
1<ul role="listbox" class="p-0 m-0">
2 <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5"
3 role="option" aria-selected="true" (mouseenter)="hoverItem(i)">
4 <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion>
5 </li>
6</ul> \ 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 @@
1import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren } from '@angular/core' 1import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core'
2import { SuggestionComponent } from './suggestion.component' 2import { SuggestionComponent } from './suggestion.component'
3 3
4@Component({ 4@Component({
5 selector: 'my-suggestions', 5 selector: 'my-suggestions',
6 template: ` 6 templateUrl: './suggestions.component.html',
7 <ul role="listbox" class="p-0 m-0"> 7 changeDetection: ChangeDetectionStrategy.OnPush
8 <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5"
9 role="option" aria-selected="true" (mouseenter)="hoverItem(i)">
10 <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion>
11 </li>
12 </ul>
13 `
14}) 8})
15export class SuggestionsComponent implements AfterViewInit { 9export class SuggestionsComponent implements AfterViewInit {
16 @Input() results: any[] 10 @Input() results: any[]
@@ -20,7 +14,7 @@ export class SuggestionsComponent implements AfterViewInit {
20 14
21 ngAfterViewInit () { 15 ngAfterViewInit () {
22 this.listItems.changes.subscribe( 16 this.listItems.changes.subscribe(
23 val => this.init.emit({ items: this.listItems }) 17 _ => this.init.emit({ items: this.listItems })
24 ) 18 )
25 } 19 }
26 20
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'
5@Pipe({ name: 'highlight' }) 5@Pipe({ name: 'highlight' })
6export class HighlightPipe implements PipeTransform { 6export class HighlightPipe implements PipeTransform {
7 /* use this for single match search */ 7 /* use this for single match search */
8 static SINGLE_MATCH: string = "Single-Match" 8 static SINGLE_MATCH = 'Single-Match'
9 /* use this for single match search with a restriction that target should start with search string */ 9 /* use this for single match search with a restriction that target should start with search string */
10 static SINGLE_AND_STARTS_WITH_MATCH: string = "Single-And-StartsWith-Match" 10 static SINGLE_AND_STARTS_WITH_MATCH = 'Single-And-StartsWith-Match'
11 /* use this for global search */ 11 /* use this for global search */
12 static MULTI_MATCH: string = "Multi-Match" 12 static MULTI_MATCH = 'Multi-Match'
13 13
14 constructor() {} 14 // tslint:disable-next-line:no-empty
15 transform( 15 constructor () {}
16
17 transform (
16 contentString: string = null, 18 contentString: string = null,
17 stringToHighlight: string = null, 19 stringToHighlight: string = null,
18 option: string = "Single-And-StartsWith-Match", 20 option = 'Single-And-StartsWith-Match',
19 caseSensitive: boolean = false, 21 caseSensitive = false,
20 highlightStyleName: string = "search-highlight" 22 highlightStyleName = 'search-highlight'
21 ): SafeHtml { 23 ): SafeHtml {
22 if (stringToHighlight && contentString && option) { 24 if (stringToHighlight && contentString && option) {
23 let regex: any = "" 25 let regex: any = ''
24 let caseFlag: string = !caseSensitive ? "i" : "" 26 const caseFlag: string = !caseSensitive ? 'i' : ''
25 switch (option) { 27 switch (option) {
26 case "Single-Match": { 28 case 'Single-Match': {
27 regex = new RegExp(stringToHighlight, caseFlag) 29 regex = new RegExp(stringToHighlight, caseFlag)
28 break 30 break
29 } 31 }
30 case "Single-And-StartsWith-Match": { 32 case 'Single-And-StartsWith-Match': {
31 regex = new RegExp("^" + stringToHighlight, caseFlag) 33 regex = new RegExp("^" + stringToHighlight, caseFlag)
32 break 34 break
33 } 35 }
34 case "Multi-Match": { 36 case 'Multi-Match': {
35 regex = new RegExp(stringToHighlight, "g" + caseFlag) 37 regex = new RegExp(stringToHighlight, 'g' + caseFlag)
36 break 38 break
37 } 39 }
38 default: { 40 default: {
39 // default will be a global case-insensitive match 41 // default will be a global case-insensitive match
40 regex = new RegExp(stringToHighlight, "gi") 42 regex = new RegExp(stringToHighlight, 'gi')
41 } 43 }
42 }
43 const replaced = contentString.replace(
44 regex,
45 (match) => `<span class="${highlightStyleName}">${match}</span>`
46 )
47 return replaced
48 } else {
49 return contentString
50 } 44 }
45 const replaced = contentString.replace(
46 regex,
47 (match) => `<span class="${highlightStyleName}">${match}</span>`
48 )
49 return replaced
50 } else {
51 return contentString
52 }
51 } 53 }
52} 54}
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 @@
1$icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/'; 1$icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
2 2
3@import '_bootstrap-variables';
3@import '_variables'; 4@import '_variables';
4@import '_mixins'; 5@import '_mixins';
5 6
@@ -234,7 +235,7 @@ table {
234 } 235 }
235} 236}
236 237
237@media screen and (max-width: 1600px) { 238@media screen and (max-width: #{map-get($grid-breakpoints, xxl)}) {
238 .main-col { 239 .main-col {
239 &.expanded { 240 &.expanded {
240 .margin-content { 241 .margin-content {
@@ -245,7 +246,7 @@ table {
245 } 246 }
246} 247}
247 248
248@media screen and (max-width: 900px) { 249@media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
249 .main-col { 250 .main-col {
250 &.expanded { 251 &.expanded {
251 .margin-content { 252 .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/';
52} 52}
53 53
54 54
55@media screen and (min-width: 768px) { 55@media screen and (min-width: #{map-get($grid-breakpoints, md)}) {
56 .modal:before { 56 .modal:before {
57 vertical-align: middle; 57 vertical-align: middle;
58 content: " "; 58 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: (
13 md: 768px, 13 md: 768px,
14 // Large screen / desktop 14 // Large screen / desktop
15 lg: 900px, 15 lg: 900px,
16 // Extra large screen / wide desktop 16 // Extra large screens / wide desktops
17 xl: 1200px 17 xl: 1200px,
18 xxl: 1600px
18); 19);
19 20
20$container-max-widths: ( 21$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 @@
445@mixin actor-owner { 445@mixin actor-owner {
446 @include disable-default-a-behaviour; 446 @include disable-default-a-behaviour;
447 447
448 display: inline-table;
449 font-size: 13px; 448 font-size: 13px;
450 margin-top: 4px; 449 margin-top: 4px;
451 color: var(--mainForegroundColor); 450 color: var(--mainForegroundColor);
@@ -488,14 +487,15 @@
488 .actor-names { 487 .actor-names {
489 display: flex; 488 display: flex;
490 align-items: center; 489 align-items: center;
490 flex-wrap: wrap;
491 491
492 .actor-display-name { 492 .actor-display-name {
493 font-size: 23px; 493 font-size: 23px;
494 font-weight: $font-bold; 494 font-weight: $font-bold;
495 margin-right: 7px;
495 } 496 }
496 497
497 .actor-name { 498 .actor-name {
498 margin-left: 7px;
499 position: relative; 499 position: relative;
500 top: 3px; 500 top: 3px;
501 font-size: 14px; 501 font-size: 14px;
@@ -503,6 +503,10 @@
503 } 503 }
504 } 504 }
505 505
506 .actor-lower {
507 grid-area: lower;
508 }
509
506 .actor-followers { 510 .actor-followers {
507 font-size: 15px; 511 font-size: 15px;
508 } 512 }
@@ -522,6 +526,11 @@
522 margin-bottom: 0; 526 margin-bottom: 0;
523 text-transform: uppercase; 527 text-transform: uppercase;
524 font-weight: 600; 528 font-weight: 600;
529 font-size: 110%;
530
531 @media screen and (max-width: $mobile-view) {
532 font-size: 130%;
533 }
525 } 534 }
526 } 535 }
527} 536}
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 @@
1@import '_bootstrap-variables';
2
1$small-view: 800px; 3$small-view: 800px;
2$mobile-view: 500px; 4$mobile-view: 500px;
3 5