Merge pull request #2639 from mempool/simon/instant-search-results
Handle instant block, txid and address search
This commit is contained in:
		
						commit
						596c7afecb
					
				| @ -3,8 +3,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |||||||
| import { Router } from '@angular/router'; | import { Router } from '@angular/router'; | ||||||
| import { AssetsService } from '../../services/assets.service'; | import { AssetsService } from '../../services/assets.service'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| import { Observable, of, Subject, zip, BehaviorSubject } from 'rxjs'; | import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs'; | ||||||
| import { debounceTime, distinctUntilChanged, switchMap, catchError, map } from 'rxjs/operators'; | import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith,  tap } from 'rxjs/operators'; | ||||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||||
| import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | ||||||
| import { ApiService } from '../../services/api.service'; | import { ApiService } from '../../services/api.service'; | ||||||
| @ -24,7 +24,7 @@ export class SearchFormComponent implements OnInit { | |||||||
|   typeAhead$: Observable<any>; |   typeAhead$: Observable<any>; | ||||||
|   searchForm: FormGroup; |   searchForm: FormGroup; | ||||||
| 
 | 
 | ||||||
|   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})$/; |   regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/; | ||||||
|   regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/; |   regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/; | ||||||
|   regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/; |   regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/; | ||||||
|   regexBlockheight = /^[0-9]{1,9}$/; |   regexBlockheight = /^[0-9]{1,9}$/; | ||||||
| @ -33,7 +33,7 @@ export class SearchFormComponent implements OnInit { | |||||||
| 
 | 
 | ||||||
|   @Output() searchTriggered = new EventEmitter(); |   @Output() searchTriggered = new EventEmitter(); | ||||||
|   @ViewChild('searchResults') searchResults: SearchResultsComponent; |   @ViewChild('searchResults') searchResults: SearchResultsComponent; | ||||||
|   @HostListener('keydown', ['$event']) keydown($event) { |   @HostListener('keydown', ['$event']) keydown($event): void { | ||||||
|     this.handleKeyDown($event); |     this.handleKeyDown($event); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -47,7 +47,7 @@ export class SearchFormComponent implements OnInit { | |||||||
|     private relativeUrlPipe: RelativeUrlPipe, |     private relativeUrlPipe: RelativeUrlPipe, | ||||||
|   ) { } |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit() { |   ngOnInit(): void { | ||||||
|     this.stateService.networkChanged$.subscribe((network) => this.network = network); |     this.stateService.networkChanged$.subscribe((network) => this.network = network); | ||||||
| 
 | 
 | ||||||
|     this.searchForm = this.formBuilder.group({ |     this.searchForm = this.formBuilder.group({ | ||||||
| @ -61,70 +61,111 @@ export class SearchFormComponent implements OnInit { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.typeAhead$ = this.searchForm.get('searchText').valueChanges |     const searchText$ = this.searchForm.get('searchText').valueChanges | ||||||
|       .pipe( |     .pipe( | ||||||
|         map((text) => { |       map((text) => { | ||||||
|           if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { |         if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { | ||||||
|             return text.substr(1); |           return text.substr(1); | ||||||
|           } |         } | ||||||
|           return text.trim(); |         return text.trim(); | ||||||
|         }), |       }), | ||||||
|         debounceTime(200), |       distinctUntilChanged(), | ||||||
|         distinctUntilChanged(), |     ); | ||||||
|         switchMap((text) => { | 
 | ||||||
|           if (!text.length) { |     const searchResults$ = searchText$.pipe( | ||||||
|             return of([ |       debounceTime(200), | ||||||
|               '', |       switchMap((text) => { | ||||||
|               [], |         if (!text.length) { | ||||||
|               { |           return of([ | ||||||
|                 nodes: [], |             [], | ||||||
|                 channels: [], |             { nodes: [], channels: [] } | ||||||
|               } |           ]); | ||||||
|             ]); |         } | ||||||
|           } |         this.isTypeaheading$.next(true); | ||||||
|           this.isTypeaheading$.next(true); |         if (!this.stateService.env.LIGHTNING) { | ||||||
|           if (!this.stateService.env.LIGHTNING) { |  | ||||||
|             return zip( |  | ||||||
|               of(text), |  | ||||||
|               this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), |  | ||||||
|               [{ nodes: [], channels: [] }], |  | ||||||
|               of(this.regexBlockheight.test(text)), |  | ||||||
|             ); |  | ||||||
|           } |  | ||||||
|           return zip( |           return zip( | ||||||
|             of(text), |  | ||||||
|             this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), |             this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), | ||||||
|             this.apiService.lightningSearch$(text).pipe(catchError(() => of({ |             [{ nodes: [], channels: [] }], | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |         return zip( | ||||||
|  |           this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), | ||||||
|  |           this.apiService.lightningSearch$(text).pipe(catchError(() => of({ | ||||||
|  |             nodes: [], | ||||||
|  |             channels: [], | ||||||
|  |           }))), | ||||||
|  |         ); | ||||||
|  |       }), | ||||||
|  |       tap((result: any[]) => { | ||||||
|  |         this.isTypeaheading$.next(false); | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     this.typeAhead$ = combineLatest( | ||||||
|  |       [ | ||||||
|  |         searchText$, | ||||||
|  |         searchResults$.pipe( | ||||||
|  |         startWith([ | ||||||
|  |           [], | ||||||
|  |           { | ||||||
|  |             nodes: [], | ||||||
|  |             channels: [], | ||||||
|  |           } | ||||||
|  |         ])) | ||||||
|  |       ] | ||||||
|  |       ).pipe( | ||||||
|  |         map((latestData) => { | ||||||
|  |           const searchText = latestData[0]; | ||||||
|  |           if (!searchText.length) { | ||||||
|  |             return { | ||||||
|  |               searchText: '', | ||||||
|  |               hashQuickMatch: false, | ||||||
|  |               blockHeight: false, | ||||||
|  |               txId: false, | ||||||
|  |               address: false, | ||||||
|  |               addresses: [], | ||||||
|               nodes: [], |               nodes: [], | ||||||
|               channels: [], |               channels: [], | ||||||
|             }))), |             }; | ||||||
|           ); |  | ||||||
|         }), |  | ||||||
|         map((result: any[]) => { |  | ||||||
|           this.isTypeaheading$.next(false); |  | ||||||
|           if (this.network === 'bisq') { |  | ||||||
|             return result[0].map((address: string) => 'B' + address); |  | ||||||
|           } |           } | ||||||
|  | 
 | ||||||
|  |           const result = latestData[1]; | ||||||
|  |           const addressPrefixSearchResults = result[0]; | ||||||
|  |           const lightningResults = result[1]; | ||||||
|  | 
 | ||||||
|  |           if (this.network === 'bisq') { | ||||||
|  |             return searchText.map((address: string) => 'B' + address); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           const matchesBlockHeight = this.regexBlockheight.test(searchText); | ||||||
|  |           const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText); | ||||||
|  |           const matchesBlockHash = this.regexBlockhash.test(searchText); | ||||||
|  |           const matchesAddress = this.regexAddress.test(searchText); | ||||||
|  | 
 | ||||||
|           return { |           return { | ||||||
|             searchText: result[0], |             searchText: searchText, | ||||||
|             blockHeight: this.regexBlockheight.test(result[0]) ? [parseInt(result[0], 10)] : [], |             hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress), | ||||||
|             addresses: result[1], |             blockHeight: matchesBlockHeight, | ||||||
|             nodes: result[2].nodes, |             txId: matchesTxId, | ||||||
|             channels: result[2].channels, |             blockHash: matchesBlockHash, | ||||||
|             totalResults: result[1].length + result[2].nodes.length + result[2].channels.length, |             address: matchesAddress, | ||||||
|  |             addresses: addressPrefixSearchResults, | ||||||
|  |             nodes: lightningResults.nodes, | ||||||
|  |             channels: lightningResults.channels, | ||||||
|           }; |           }; | ||||||
|         }) |         }) | ||||||
|       ); |       ); | ||||||
|   } |   } | ||||||
|   handleKeyDown($event) { | 
 | ||||||
|  |   handleKeyDown($event): void { | ||||||
|     this.searchResults.handleKeyDown($event); |     this.searchResults.handleKeyDown($event); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   itemSelected() { |   itemSelected(): void { | ||||||
|     setTimeout(() => this.search()); |     setTimeout(() => this.search()); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   selectedResult(result: any) { |   selectedResult(result: any): void { | ||||||
|     if (typeof result === 'string') { |     if (typeof result === 'string') { | ||||||
|       this.search(result); |       this.search(result); | ||||||
|     } else if (typeof result === 'number') { |     } else if (typeof result === 'number') { | ||||||
| @ -136,7 +177,7 @@ export class SearchFormComponent implements OnInit { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   search(result?: string) { |   search(result?: string): void { | ||||||
|     const searchText = result || this.searchForm.value.searchText.trim(); |     const searchText = result || this.searchForm.value.searchText.trim(); | ||||||
|     if (searchText) { |     if (searchText) { | ||||||
|       this.isSearching = true; |       this.isSearching = true; | ||||||
| @ -170,7 +211,7 @@ export class SearchFormComponent implements OnInit { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   navigate(url: string, searchText: string, extras?: any) { |   navigate(url: string, searchText: string, extras?: any): void { | ||||||
|     this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras); |     this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras); | ||||||
|     this.searchTriggered.emit(); |     this.searchTriggered.emit(); | ||||||
|     this.searchForm.setValue({ |     this.searchForm.setValue({ | ||||||
|  | |||||||
| @ -1,14 +1,32 @@ | |||||||
| <div class="dropdown-menu show" *ngIf="results" [hidden]="!results.blockHeight.length && !results.addresses.length && !results.nodes.length && !results.channels.length"> | <div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.addresses.length && !results.nodes.length && !results.channels.length"> | ||||||
|   <ng-template [ngIf]="results.blockHeight.length"> |   <ng-template [ngIf]="results.blockHeight"> | ||||||
|     <div class="card-title">Bitcoin Block Height</div> |     <div class="card-title">Bitcoin Block Height</div> | ||||||
|     <button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item"> |     <button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item"> | ||||||
|       Go to "{{ results.searchText }}" |       Go to "{{ results.searchText }}" | ||||||
|     </button> |     </button> | ||||||
|   </ng-template> |   </ng-template> | ||||||
|  |   <ng-template [ngIf]="results.txId"> | ||||||
|  |     <div class="card-title">Bitcoin Transaction</div> | ||||||
|  |     <button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item"> | ||||||
|  |       Go to "{{ results.searchText | shortenString : 13 }}" | ||||||
|  |     </button> | ||||||
|  |   </ng-template> | ||||||
|  |   <ng-template [ngIf]="results.address"> | ||||||
|  |     <div class="card-title">Bitcoin Address</div> | ||||||
|  |     <button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item"> | ||||||
|  |       Go to "{{ results.searchText | shortenString : isMobile ? 20 : 30 }}" | ||||||
|  |     </button> | ||||||
|  |   </ng-template> | ||||||
|  |   <ng-template [ngIf]="results.blockHash"> | ||||||
|  |     <div class="card-title">Bitcoin Block</div> | ||||||
|  |     <button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item"> | ||||||
|  |       Go to "{{ results.searchText | shortenString : 13 }}" | ||||||
|  |     </button> | ||||||
|  |   </ng-template> | ||||||
|   <ng-template [ngIf]="results.addresses.length"> |   <ng-template [ngIf]="results.addresses.length"> | ||||||
|     <div class="card-title" *ngIf="stateService.env.LIGHTNING">Bitcoin Addresses</div> |     <div class="card-title">Bitcoin Addresses</div> | ||||||
|     <ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index"> |     <ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index"> | ||||||
|       <button (click)="clickItem(results.blockHeight.length + i)" [class.active]="(results.blockHeight.length + i) === activeIdx" type="button" role="option" class="dropdown-item"> |       <button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" type="button" role="option" class="dropdown-item"> | ||||||
|         <ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight> |         <ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight> | ||||||
|       </button> |       </button> | ||||||
|     </ng-template> |     </ng-template> | ||||||
| @ -16,7 +34,7 @@ | |||||||
|   <ng-template [ngIf]="results.nodes.length"> |   <ng-template [ngIf]="results.nodes.length"> | ||||||
|     <div class="card-title">Lightning Nodes</div> |     <div class="card-title">Lightning Nodes</div> | ||||||
|     <ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index"> |     <ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index"> | ||||||
|       <button (click)="clickItem(results.blockHeight.length + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.blockHeight.length + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item"> |       <button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item"> | ||||||
|         <ngb-highlight [result]="node.alias" [term]="results.searchText"></ngb-highlight>  <span class="symbol">{{ node.public_key | shortenString : 10 }}</span> |         <ngb-highlight [result]="node.alias" [term]="results.searchText"></ngb-highlight>  <span class="symbol">{{ node.public_key | shortenString : 10 }}</span> | ||||||
|       </button> |       </button> | ||||||
|     </ng-template> |     </ng-template> | ||||||
| @ -24,7 +42,7 @@ | |||||||
|   <ng-template [ngIf]="results.channels.length"> |   <ng-template [ngIf]="results.channels.length"> | ||||||
|     <div class="card-title">Lightning Channels</div> |     <div class="card-title">Lightning Channels</div> | ||||||
|     <ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index"> |     <ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index"> | ||||||
|       <button (click)="clickItem(results.blockHeight.length + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2"  [class.active]="results.blockHeight.length + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item"> |       <button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2"  [class.active]="results.hashQuickMatch + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item"> | ||||||
|         <ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight>  <span class="symbol">{{ channel.id }}</span> |         <ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight>  <span class="symbol">{{ channel.id }}</span> | ||||||
|       </button> |       </button> | ||||||
|     </ng-template> |     </ng-template> | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ export class SearchResultsComponent implements OnChanges { | |||||||
|   ngOnChanges() { |   ngOnChanges() { | ||||||
|     this.activeIdx = 0; |     this.activeIdx = 0; | ||||||
|     if (this.results) { |     if (this.results) { | ||||||
|       this.resultsFlattened = [...this.results.blockHeight, ...this.results.addresses, ...this.results.nodes, ...this.results.channels]; |       this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.nodes, ...this.results.channels]; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user