mempool/frontend/src/app/components/search-form/search-form.component.ts

308 lines
12 KiB
TypeScript
Raw Normal View History

import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef, Input } from '@angular/core';
2022-11-28 11:55:23 +09:00
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { EventType, NavigationStart, Router } from '@angular/router';
2022-09-21 17:23:45 +02:00
import { AssetsService } from '../../services/assets.service';
import { Env, StateService } from '../../services/state.service';
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith, tap } from 'rxjs/operators';
2022-09-21 17:23:45 +02:00
import { ElectrsApiService } from '../../services/electrs-api.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { ApiService } from '../../services/api.service';
import { SearchResultsComponent } from './search-results/search-results.component';
import { Network, findOtherNetworks, getRegex, getTargetUrl, needBaseModuleChange } from '../../shared/regex.utils';
@Component({
selector: 'app-search-form',
templateUrl: './search-form.component.html',
styleUrls: ['./search-form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchFormComponent implements OnInit {
@Input() hamburgerOpen = false;
env: Env;
network = '';
2020-05-28 01:01:35 +07:00
assets: object = {};
isSearching = false;
isTypeaheading$ = new BehaviorSubject<boolean>(false);
typeAhead$: Observable<any>;
2022-11-28 11:55:23 +09:00
searchForm: UntypedFormGroup;
2022-09-12 17:31:36 +02:00
dropdownHidden = false;
@HostListener('document:click', ['$event'])
onDocumentClick(event) {
if (this.elementRef.nativeElement.contains(event.target)) {
this.dropdownHidden = false;
} else {
this.dropdownHidden = true;
}
}
2023-07-19 16:46:02 +09:00
regexAddress = getRegex('address', 'mainnet'); // Default to mainnet
2023-12-30 19:19:07 +01:00
regexBlockhash = getRegex('blockhash', 'mainnet');
2023-07-19 16:46:02 +09:00
regexTransaction = getRegex('transaction');
regexBlockheight = getRegex('blockheight');
2023-12-30 19:19:07 +01:00
regexDate = getRegex('date');
regexUnixTimestamp = getRegex('timestamp');
2022-08-28 00:07:13 +09:00
2020-07-24 22:37:35 +07:00
focus$ = new Subject<string>();
click$ = new Subject<string>();
@Output() searchTriggered = new EventEmitter();
@ViewChild('searchResults') searchResults: SearchResultsComponent;
@HostListener('keydown', ['$event']) keydown($event): void {
this.handleKeyDown($event);
}
@ViewChild('searchInput') searchInput: ElementRef;
constructor(
2022-11-28 11:55:23 +09:00
private formBuilder: UntypedFormBuilder,
private router: Router,
2020-05-02 12:36:35 +07:00
private assetsService: AssetsService,
private stateService: StateService,
2020-07-24 22:37:35 +07:00
private electrsApiService: ElectrsApiService,
private apiService: ApiService,
private relativeUrlPipe: RelativeUrlPipe,
private elementRef: ElementRef
) {
}
2022-08-28 00:07:13 +09:00
ngOnInit(): void {
this.env = this.stateService.env;
2022-08-28 00:07:13 +09:00
this.stateService.networkChanged$.subscribe((network) => {
this.network = network;
// TODO: Eventually change network type here from string to enum of consts
2022-09-04 21:53:52 +09:00
this.regexAddress = getRegex('address', network as any || 'mainnet');
2023-12-30 19:19:07 +01:00
this.regexBlockhash = getRegex('blockhash', network as any || 'mainnet');
2022-08-28 00:07:13 +09:00
});
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
if (this.searchInput && e.type === EventType.NavigationStart) {
this.searchInput.nativeElement.blur();
}
});
this.stateService.searchFocus$.subscribe(() => {
if (!this.searchInput) { // Try again a bit later once the view is properly initialized
setTimeout(() => this.searchInput.nativeElement.focus(), 100);
} else if (this.searchInput) {
this.searchInput.nativeElement.focus();
}
});
this.searchForm = this.formBuilder.group({
searchText: ['', Validators.required],
});
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
this.assetsService.getAssetsMinimalJson$
.subscribe((assets) => {
this.assets = assets;
});
}
const searchText$ = this.searchForm.get('searchText').valueChanges
.pipe(
map((text) => {
return text.trim();
}),
tap((text) => {
this.stateService.searchText$.next(text);
}),
distinctUntilChanged(),
);
const searchResults$ = searchText$.pipe(
debounceTime(200),
switchMap((text) => {
if (!text.length) {
return of([
[],
{ nodes: [], channels: [] }
]);
}
this.isTypeaheading$.next(true);
if (!this.stateService.env.LIGHTNING) {
return zip(
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
[{ nodes: [], channels: [] }],
);
}
return zip(
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
this.apiService.lightningSearch$(text).pipe(catchError(() => of({
nodes: [],
channels: [],
}))),
);
}),
map((result: any[]) => {
return result;
}),
tap(() => {
this.isTypeaheading$.next(false);
})
);
this.typeAhead$ = combineLatest(
[
searchText$,
searchResults$.pipe(
startWith([
[],
{
nodes: [],
channels: [],
}
]))
]
).pipe(
map((latestData) => {
let searchText = latestData[0];
if (!searchText.length) {
return {
searchText: '',
hashQuickMatch: false,
blockHeight: false,
txId: false,
address: false,
otherNetworks: [],
addresses: [],
2022-05-15 19:22:14 +04:00
nodes: [],
channels: [],
liquidAsset: [],
};
}
const result = latestData[1];
const addressPrefixSearchResults = result[0];
const lightningResults = result[1];
2024-03-17 18:22:38 +09:00
// Do not show date and timestamp results for liquid
2024-05-06 15:40:32 +00:00
const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'testnet4' || this.network === 'signet';
const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight;
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date' && new Date(searchText).getTime() <= Date.now() && isNetworkBitcoin;
const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText) && parseInt(searchText) <= Math.floor(Date.now() / 1000) && isNetworkBitcoin;
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
const matchesBlockHash = this.regexBlockhash.test(searchText);
2024-03-17 18:22:38 +09:00
const matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
const otherNetworks = findOtherNetworks(searchText, this.network as any || 'mainnet', this.env);
const liquidAsset = this.assets ? (this.assets[searchText] || []) : [];
2024-03-17 18:22:38 +09:00
2023-11-30 19:04:14 +01:00
if (matchesDateTime && searchText.indexOf('/') !== -1) {
searchText = searchText.replace(/\//g, '-');
}
return {
searchText: searchText,
2023-11-30 19:04:14 +01:00
hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress || matchesUnixTimestamp || matchesDateTime),
blockHeight: matchesBlockHeight,
2023-11-30 19:04:14 +01:00
dateTime: matchesDateTime,
unixTimestamp: matchesUnixTimestamp,
txId: matchesTxId,
blockHash: matchesBlockHash,
address: matchesAddress,
addresses: matchesAddress && addressPrefixSearchResults.length === 1 && searchText === addressPrefixSearchResults[0] ? [] : addressPrefixSearchResults, // If there is only one address and it matches the search text, don't show it in the dropdown
otherNetworks: otherNetworks,
nodes: lightningResults.nodes,
channels: lightningResults.channels,
liquidAsset: liquidAsset,
};
})
);
}
handleKeyDown($event): void {
this.searchResults.handleKeyDown($event);
}
itemSelected(): void {
setTimeout(() => this.search());
}
selectedResult(result: any): void {
if (typeof result === 'string') {
2022-07-06 16:07:21 +02:00
this.search(result);
} else if (typeof result === 'number' && result <= this.stateService.latestBlockHeight) {
2022-09-12 19:20:22 +02:00
this.navigate('/block/', result.toString());
} else if (result.alias) {
this.navigate('/lightning/node/', result.public_key);
} else if (result.short_id) {
this.navigate('/lightning/channel/', result.id);
} else if (result.network) {
if (result.isNetworkAvailable) {
this.navigate('/address/', result.address, undefined, result.network);
} else {
this.searchForm.setValue({
searchText: '',
});
this.isSearching = false;
}
}
}
search(result?: string): void {
2022-07-06 16:07:21 +02:00
const searchText = result || this.searchForm.value.searchText.trim();
if (searchText) {
this.isSearching = true;
if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) {
this.navigate('/address/', searchText);
} else if (this.regexBlockhash.test(searchText)) {
this.navigate('/block/', searchText);
} else if (this.regexBlockheight.test(searchText)) {
parseInt(searchText) <= this.stateService.latestBlockHeight ? this.navigate('/block/', searchText) : this.isSearching = false;
} else if (this.regexTransaction.test(searchText)) {
const matches = this.regexTransaction.exec(searchText);
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
if (this.assets[matches[0]]) {
this.navigate('/assets/asset/', matches[0]);
}
this.electrsApiService.getAsset$(matches[0])
.subscribe(
() => { this.navigate('/assets/asset/', matches[0]); },
() => {
this.electrsApiService.getBlock$(matches[0])
.subscribe(
(block) => { this.navigate('/block/', matches[0], { state: { data: { block } } }); },
() => { this.navigate('/tx/', matches[0]); });
}
);
2020-05-02 12:36:35 +07:00
} else {
this.navigate('/tx/', matches[0]);
2020-05-02 12:36:35 +07:00
}
2023-11-30 19:04:14 +01:00
} else if (this.regexDate.test(searchText) || this.regexUnixTimestamp.test(searchText)) {
let timestamp: number;
this.regexDate.test(searchText) ? timestamp = Math.floor(new Date(searchText).getTime() / 1000) : timestamp = searchText;
2023-12-30 19:19:07 +01:00
// Check if timestamp is too far in the future or before the genesis block
if (timestamp > Math.floor(Date.now() / 1000)) {
2023-12-30 19:19:07 +01:00
this.isSearching = false;
return;
}
2023-11-30 19:04:14 +01:00
this.apiService.getBlockDataFromTimestamp$(timestamp).subscribe(
(data) => { this.navigate('/block/', data.hash); },
(error) => { console.log(error); this.isSearching = false; }
);
} else {
2022-08-30 22:36:13 +02:00
this.searchResults.searchButtonClick();
this.isSearching = false;
}
}
}
navigate(url: string, searchText: string, extras?: any, swapNetwork?: string) {
2024-03-17 18:22:38 +09:00
if (needBaseModuleChange(this.env.BASE_MODULE as 'liquid' | 'mempool', swapNetwork as Network)) {
window.location.href = getTargetUrl(swapNetwork as Network, searchText, this.env);
} else {
this.router.navigate([this.relativeUrlPipe.transform(url, swapNetwork), searchText], extras);
this.searchTriggered.emit();
this.searchForm.setValue({
searchText: '',
});
this.isSearching = false;
}
}
}