From d33c12cdee3bd2ae0708a1dde3c7e3767360063c Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 6 Feb 2022 03:31:50 +0400 Subject: [PATCH] Asset search --- .../assets-nav/assets-nav.component.html | 2 +- .../assets/assets-nav/assets-nav.component.ts | 78 ++++++++++++- .../app/components/assets/assets.component.ts | 103 +++--------------- frontend/src/app/services/assets.service.ts | 27 ++++- 4 files changed, 118 insertions(+), 92 deletions(-) diff --git a/frontend/src/app/components/assets/assets-nav/assets-nav.component.html b/frontend/src/app/components/assets/assets-nav/assets-nav.component.html index 9ee7ac72a..73d391254 100644 --- a/frontend/src/app/components/assets/assets-nav/assets-nav.component.html +++ b/frontend/src/app/components/assets/assets-nav/assets-nav.component.html @@ -15,7 +15,7 @@
- +
diff --git a/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts b/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts index e1b31372b..d7280d50b 100644 --- a/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts +++ b/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts @@ -1,5 +1,15 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; +import { merge, Observable, of, Subject } from 'rxjs'; +import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; +import { AssetExtended } from 'src/app/interfaces/electrs.interface'; +import { AssetsService } from 'src/app/services/assets.service'; +import { SeoService } from 'src/app/services/seo.service'; +import { StateService } from 'src/app/services/state.service'; +import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; +import { environment } from 'src/environments/environment'; @Component({ selector: 'app-assets-nav', @@ -7,16 +17,78 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; styleUrls: ['./assets-nav.component.scss'] }) export class AssetsNavComponent implements OnInit { - activeTab = 0; + @ViewChild('instance', {static: true}) instance: NgbTypeahead; + nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; searchForm: FormGroup; + assetsCache: AssetExtended[]; + + typeaheadSearchFn: ((text: Observable) => Observable); + formatterFn = (asset: AssetExtended) => asset.name + ' (' + asset.ticker + ')'; + focus$ = new Subject(); + click$ = new Subject(); + + itemsPerPage = 15; constructor( private formBuilder: FormBuilder, + private seoService: SeoService, + private router: Router, + private assetsService: AssetsService, + private stateService: StateService, + private relativeUrlPipe: RelativeUrlPipe, ) { } ngOnInit(): void { + this.typeaheadSearchFn = this.typeaheadSearch; + this.searchForm = this.formBuilder.group({ - searchText: [{ value: '', disabled: true }, Validators.required] + searchText: [{ value: '', disabled: false }, Validators.required] + }); + } + + typeaheadSearch = (text$: Observable) => { + const debouncedText$ = text$.pipe( + distinctUntilChanged() + ); + const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen())); + const inputFocus$ = this.focus$; + + return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$) + .pipe( + switchMap((searchText) => { + if (!searchText.length) { + return of([]); + } + return this.assetsService.getAssetsJson$.pipe( + map((assets) => { + if (searchText.length ) { + const filteredAssets = assets.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1 + || (asset.ticker || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1 + || (asset.entity && asset.entity.domain || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1); + assets = filteredAssets; + return filteredAssets.slice(0, this.itemsPerPage); + } else { + return assets.slice(0, this.itemsPerPage); + } + }) + ) + }), + ); + } + + itemSelected() { + setTimeout(() => this.search()); + } + + search() { + const searchText = this.searchForm.value.searchText; + this.navigate('/assets/asset/', searchText.asset_id); + } + + navigate(url: string, searchText: string, extras?: any) { + this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras); + this.searchForm.setValue({ + searchText: '', }); } diff --git a/frontend/src/app/components/assets/assets.component.ts b/frontend/src/app/components/assets/assets.component.ts index d5cbd10ab..542765033 100644 --- a/frontend/src/app/components/assets/assets.component.ts +++ b/frontend/src/app/components/assets/assets.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { AssetsService } from 'src/app/services/assets.service'; import { environment } from 'src/environments/environment'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { distinctUntilChanged, map, filter, mergeMap, tap, take } from 'rxjs/operators'; +import { FormGroup } from '@angular/forms'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; -import { merge, combineLatest, Observable } from 'rxjs'; +import { combineLatest, Observable } from 'rxjs'; import { AssetExtended } from 'src/app/interfaces/electrs.interface'; import { SeoService } from 'src/app/services/seo.service'; import { StateService } from 'src/app/services/state.service'; @@ -32,7 +32,6 @@ export class AssetsComponent implements OnInit { constructor( private assetsService: AssetsService, - private formBuilder: FormBuilder, private route: ActivatedRoute, private router: Router, private seoService: SeoService, @@ -43,56 +42,20 @@ export class AssetsComponent implements OnInit { this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`); this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10); - this.searchForm = this.formBuilder.group({ - searchText: [{ value: '', disabled: true }, Validators.required] - }); - this.assets$ = combineLatest([ this.assetsService.getAssetsJson$, this.route.queryParams, ]) - .pipe( - take(1), - mergeMap(([assets, qp]) => { - this.assets = Object.values(assets); - if (this.stateService.network === 'liquid') { - // @ts-ignore - this.assets.push({ - name: 'Liquid Bitcoin', - ticker: 'L-BTC', - asset_id: this.nativeAssetId, - }); - } else if (this.stateService.network === 'liquidtestnet') { - // @ts-ignore - this.assets.push({ - name: 'Test Liquid Bitcoin', - ticker: 'tL-BTC', - asset_id: this.nativeAssetId, - }); - } + .pipe( + take(1), + switchMap(([assets, qp]) => { + this.assets = assets; - this.assets = this.assets.sort((a: any, b: any) => a.name.localeCompare(b.name)); - this.assetsCache = this.assets; - this.searchForm.get('searchText').enable(); - - if (qp.search) { - this.searchForm.get('searchText').setValue(qp.search, { emitEvent: false }); - } - - return merge( - this.searchForm.get('searchText').valueChanges - .pipe( - distinctUntilChanged(), - tap((text) => { - this.page = 1; - this.searchTextChanged(text); - }) - ), - this.route.queryParams + return this.route.queryParams .pipe( filter((queryParams) => { const newPage = parseInt(queryParams.page, 10); - if (newPage !== this.page || queryParams.search !== this.searchForm.get('searchText').value) { + if (newPage !== this.page) { return true; } return false; @@ -104,38 +67,19 @@ export class AssetsComponent implements OnInit { } else { this.page = 1; } - if (this.searchForm.get('searchText').value !== (queryParams.search || '')) { - this.searchTextChanged(queryParams.search); - } - if (queryParams.search) { - this.searchForm.get('searchText').setValue(queryParams.search, { emitEvent: false }); - return queryParams.search; - } return ''; }) - ), - ); - }), - map((searchText) => { - const start = (this.page - 1) * this.itemsPerPage; - if (searchText.length ) { - const filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1 - || (asset.ticker || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1); - this.assets = filteredAssets; - return filteredAssets.slice(start, this.itemsPerPage + start); - } else { - this.assets = this.assetsCache; + ); + }), + map(() => { + const start = (this.page - 1) * this.itemsPerPage; return this.assets.slice(start, this.itemsPerPage + start); - } - }) - ); + }) + ); } pageChange(page: number) { - const queryParams = { page: page, search: this.searchForm.get('searchText').value }; - if (queryParams.search === '') { - queryParams.search = null; - } + const queryParams = { page: page }; if (queryParams.page === 1) { queryParams.page = null; } @@ -147,21 +91,6 @@ export class AssetsComponent implements OnInit { }); } - searchTextChanged(text: string) { - const queryParams = { search: text, page: 1 }; - if (queryParams.search === '') { - queryParams.search = null; - } - if (queryParams.page === 1) { - queryParams.page = null; - } - this.router.navigate([], { - relativeTo: this.route, - queryParams: queryParams, - queryParamsHandling: 'merge', - }); - } - trackByAsset(index: number, asset: any) { return asset.asset_id; } diff --git a/frontend/src/app/services/assets.service.ts b/frontend/src/app/services/assets.service.ts index 260a48b7b..d5693abf8 100644 --- a/frontend/src/app/services/assets.service.ts +++ b/frontend/src/app/services/assets.service.ts @@ -3,12 +3,16 @@ import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map, shareReplay, switchMap } from 'rxjs/operators'; import { StateService } from './state.service'; +import { environment } from 'src/environments/environment'; +import { AssetExtended } from '../interfaces/electrs.interface'; @Injectable({ providedIn: 'root' }) export class AssetsService { - getAssetsJson$: Observable; + nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; + + getAssetsJson$: Observable; getAssetsMinimalJson$: Observable; getMiningPools$: Observable; @@ -24,6 +28,27 @@ export class AssetsService { this.getAssetsJson$ = this.stateService.networkChanged$ .pipe( switchMap(() => this.httpClient.get(`${apiBaseUrl}/resources/assets${this.stateService.network === 'liquidtestnet' ? '-testnet' : ''}.json`)), + map((rawAssets) => { + const assets: AssetExtended[] = Object.values(rawAssets); + + if (this.stateService.network === 'liquid') { + // @ts-ignore + assets.push({ + name: 'Liquid Bitcoin', + ticker: 'L-BTC', + asset_id: this.nativeAssetId, + }); + } else if (this.stateService.network === 'liquidtestnet') { + // @ts-ignore + assets.push({ + name: 'Test Liquid Bitcoin', + ticker: 'tL-BTC', + asset_id: this.nativeAssetId, + }); + } + + return assets.sort((a: any, b: any) => a.name.localeCompare(b.name)); + }), shareReplay(1), ); this.getAssetsMinimalJson$ = this.stateService.networkChanged$