diff --git a/frontend/src/app/components/qrcode/qrcode.component.ts b/frontend/src/app/components/qrcode/qrcode.component.ts index 07f2703fc..923e66fd8 100644 --- a/frontend/src/app/components/qrcode/qrcode.component.ts +++ b/frontend/src/app/components/qrcode/qrcode.component.ts @@ -21,7 +21,7 @@ export class QrcodeComponent implements AfterViewInit { ) { } ngOnChanges() { - if (!this.canvas.nativeElement) { + if (!this.canvas || !this.canvas.nativeElement) { return; } this.render(); diff --git a/frontend/src/app/components/search-form/search-form.component.html b/frontend/src/app/components/search-form/search-form.component.html index 422dfaa62..417414b58 100644 --- a/frontend/src/app/components/search-form/search-form.component.html +++ b/frontend/src/app/components/search-form/search-form.component.html @@ -1,7 +1,10 @@
- + + + +
diff --git a/frontend/src/app/components/search-form/search-form.component.scss b/frontend/src/app/components/search-form/search-form.component.scss index f316c3aa7..448cb28b3 100644 --- a/frontend/src/app/components/search-form/search-form.component.scss +++ b/frontend/src/app/components/search-form/search-form.component.scss @@ -32,6 +32,7 @@ form { } .search-box-container { + position: relative; width: 100%; @media (min-width: 768px) { min-width: 400px; @@ -48,4 +49,4 @@ form { .btn { width: 100px; } -} +} \ No newline at end of file diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index d83975c50..3914918ad 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -1,41 +1,40 @@ -import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild } from '@angular/core'; +import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { AssetsService } from 'src/app/services/assets.service'; import { StateService } from 'src/app/services/state.service'; -import { Observable, of, Subject, merge } from 'rxjs'; +import { Observable, of, Subject, merge, zip } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap, filter, catchError, map } from 'rxjs/operators'; import { ElectrsApiService } from 'src/app/services/electrs-api.service'; -import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { ShortenStringPipe } from 'src/app/shared/pipes/shorten-string-pipe/shorten-string.pipe'; +import { ApiService } from 'src/app/services/api.service'; +import { SearchResultsComponent } from './search-results/search-results.component'; @Component({ selector: 'app-search-form', templateUrl: './search-form.component.html', styleUrls: ['./search-form.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SearchFormComponent implements OnInit { network = ''; assets: object = {}; isSearching = false; - typeaheadSearchFn: ((text: Observable) => Observable); - + typeAhead$: Observable; searchForm: FormGroup; - isMobile = (window.innerWidth <= 767.98); - @Output() searchTriggered = new EventEmitter(); regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/; regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/; regexTransaction = /^([a-fA-F0-9]{64}):?(\d+)?$/; regexBlockheight = /^[0-9]+$/; - - @ViewChild('instance', {static: true}) instance: NgbTypeahead; focus$ = new Subject(); click$ = new Subject(); - formatterFn = (address: string) => this.shortenStringPipe.transform(address, this.isMobile ? 33 : 40); + @Output() searchTriggered = new EventEmitter(); + @ViewChild('searchResults') searchResults: SearchResultsComponent; + @HostListener('keydown', ['$event']) keydown($event) { + this.handleKeyDown($event); + } constructor( private formBuilder: FormBuilder, @@ -43,12 +42,11 @@ export class SearchFormComponent implements OnInit { private assetsService: AssetsService, private stateService: StateService, private electrsApiService: ElectrsApiService, + private apiService: ApiService, private relativeUrlPipe: RelativeUrlPipe, - private shortenStringPipe: ShortenStringPipe, ) { } ngOnInit() { - this.typeaheadSearchFn = this.typeaheadSearch; this.stateService.networkChanged$.subscribe((network) => this.network = network); this.searchForm = this.formBuilder.group({ @@ -61,43 +59,63 @@ export class SearchFormComponent implements OnInit { this.assets = assets; }); } - } - typeaheadSearch = (text$: Observable) => { - const debouncedText$ = text$.pipe( - map((text) => { - if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { - return text.substr(1); - } - return text; - }), - debounceTime(200), - distinctUntilChanged() - ); - const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen())); - const inputFocus$ = this.focus$; - - return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$) + this.typeAhead$ = this.searchForm.get('searchText').valueChanges .pipe( + map((text) => { + if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { + return text.substr(1); + } + return text; + }), + debounceTime(300), + distinctUntilChanged(), switchMap((text) => { if (!text.length) { - return of([]); + return of([ + [], + { + nodes: [], + channels: [], + } + ]); } - return this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))); + return zip( + this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), + this.apiService.lightningSearch$(text), + ); }), - map((result: string[]) => { + map((result: any[]) => { if (this.network === 'bisq') { - return result.map((address: string) => 'B' + address); + return result[0].map((address: string) => 'B' + address); } - return result; + return { + addresses: result[0], + nodes: result[1].nodes, + channels: result[1].channels, + totalResults: result[0].length + result[1].nodes.length + result[1].channels.length, + }; }) ); - } + } + handleKeyDown($event) { + this.searchResults.handleKeyDown($event); + } itemSelected() { setTimeout(() => this.search()); } + selectedResult(result: any) { + if (typeof result === 'string') { + this.navigate('/address/', result); + } else if (result.alias) { + this.navigate('/lightning/node/', result.public_key); + } else if (result.short_id) { + this.navigate('/lightning/channel/', result.id); + } + } + search() { const searchText = this.searchForm.value.searchText.trim(); if (searchText) { diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.html b/frontend/src/app/components/search-form/search-results/search-results.component.html new file mode 100644 index 000000000..e3e3e5212 --- /dev/null +++ b/frontend/src/app/components/search-form/search-results/search-results.component.html @@ -0,0 +1,26 @@ + diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.scss b/frontend/src/app/components/search-form/search-results/search-results.component.scss new file mode 100644 index 000000000..094865bb6 --- /dev/null +++ b/frontend/src/app/components/search-form/search-results/search-results.component.scss @@ -0,0 +1,16 @@ +.card-title { + color: #4a68b9; + font-size: 10px; + margin-bottom: 4px; + font-size: 1rem; + + margin-left: 10px; +} + +.dropdown-menu { + position: absolute; + top: 42px; + left: 0px; + box-shadow: 0.125rem 0.125rem 0.25rem rgba(0,0,0,0.075); + width: 100%; +} diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.ts b/frontend/src/app/components/search-form/search-results/search-results.component.ts new file mode 100644 index 000000000..0ce88fe04 --- /dev/null +++ b/frontend/src/app/components/search-form/search-results/search-results.component.ts @@ -0,0 +1,68 @@ +import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; + +@Component({ + selector: 'app-search-results', + templateUrl: './search-results.component.html', + styleUrls: ['./search-results.component.scss'], +}) +export class SearchResultsComponent implements OnChanges { + @Input() results: any = {}; + @Input() searchTerm = ''; + @Output() selectedResult = new EventEmitter(); + + isMobile = (window.innerWidth <= 767.98); + resultsFlattened = []; + activeIdx = 0; + focusFirst = true; + + constructor() { } + + ngOnChanges() { + this.activeIdx = 0; + if (this.results) { + this.resultsFlattened = [...this.results.addresses, ...this.results.nodes, ...this.results.channels]; + } + } + + handleKeyDown(event: KeyboardEvent) { + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + this.next(); + break; + case 'ArrowUp': + event.preventDefault(); + this.prev(); + break; + case 'Enter': + event.preventDefault(); + this.selectedResult.emit(this.resultsFlattened[this.activeIdx]); + this.results = null; + break; + } + } + + clickItem(id: number) { + this.selectedResult.emit(this.resultsFlattened[id]); + this.results = null; + } + + next() { + if (this.activeIdx === this.resultsFlattened.length - 1) { + this.activeIdx = this.focusFirst ? (this.activeIdx + 1) % this.resultsFlattened.length : -1; + } else { + this.activeIdx++; + } + } + + prev() { + if (this.activeIdx < 0) { + this.activeIdx = this.resultsFlattened.length - 1; + } else if (this.activeIdx === 0) { + this.activeIdx = this.focusFirst ? this.resultsFlattened.length - 1 : -1; + } else { + this.activeIdx--; + } + } + +} diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index ab0a742cf..0ea41a2bb 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -5,7 +5,7 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter import { ElectrsApiService } from '../../services/electrs-api.service'; import { environment } from 'src/environments/environment'; import { AssetsService } from 'src/app/services/assets.service'; -import { map, tap, switchMap } from 'rxjs/operators'; +import { filter, map, tap, switchMap } from 'rxjs/operators'; import { BlockExtended } from 'src/app/interfaces/node-api.interface'; import { ApiService } from 'src/app/services/api.service'; @@ -78,6 +78,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { ), this.refreshChannels$ .pipe( + filter(() => this.stateService.env.LIGHTNING), switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)), map((channels) => { this.channels = channels; diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index 066d37a70..fe6d44e42 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -3,10 +3,10 @@ - - - - + + + + @@ -15,18 +15,18 @@ - - - @@ -34,22 +34,22 @@ - - - - - - -
Node AliasNode IDStatusFee RateCapacityNode IDStatusFee RateCapacity Channel ID
{{ channel.alias_left || '?' }} + {{ channel.node1_public_key | shortenString : 10 }} + Inactive Active Closed + {{ channel.node1_fee_rate / 10000 | number }}% {{ channel.alias_right || '?' }} + {{ channel.node2_public_key | shortenString : 10 }} + Inactive Active Closed + {{ channel.node2_fee_rate / 10000 | number }}% + @@ -66,13 +66,13 @@ + + + diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index f3c0d6153..5842a1b9d 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -47,7 +47,6 @@ export class NodeComponent implements OnInit { socket: node.public_key + '@' + socket, }); } - console.log(socketsObject); node.socketsObject = socketsObject; return node; }),