2023-11-01 17:47:20 +09:00
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' ;
2023-07-24 10:18:00 +09:00
import { EventType , NavigationStart , Router } from '@angular/router' ;
2022-09-21 17:23:45 +02:00
import { AssetsService } from '../../services/assets.service' ;
2023-12-31 23:23:53 +01:00
import { Env , StateService } from '../../services/state.service' ;
2022-10-14 05:09:25 +04:00
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' ;
2022-05-15 14:47:55 +04:00
import { SearchResultsComponent } from './search-results/search-results.component' ;
2023-12-31 23:23:53 +01:00
import { Network , findOtherNetworks , getRegex , getTargetUrl , needBaseModuleChange } from '../../shared/regex.utils' ;
2020-02-16 22:15:07 +07:00
@Component ( {
selector : 'app-search-form' ,
templateUrl : './search-form.component.html' ,
styleUrls : [ './search-form.component.scss' ] ,
2022-05-15 14:47:55 +04:00
changeDetection : ChangeDetectionStrategy.OnPush ,
2020-02-16 22:15:07 +07:00
} )
export class SearchFormComponent implements OnInit {
2023-11-01 17:47:20 +09:00
@Input ( ) hamburgerOpen = false ;
2023-12-31 23:23:53 +01:00
env : Env ;
2020-05-09 20:37:50 +07:00
network = '' ;
2020-05-28 01:01:35 +07:00
assets : object = { } ;
2020-11-22 16:03:23 +07:00
isSearching = false ;
2022-08-30 11:46:37 +02:00
isTypeaheading $ = new BehaviorSubject < boolean > ( false ) ;
2022-05-15 14:47:55 +04:00
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 ;
}
}
2020-02-16 22:15:07 +07:00
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 > ( ) ;
2022-05-15 14:47:55 +04:00
@Output ( ) searchTriggered = new EventEmitter ( ) ;
@ViewChild ( 'searchResults' ) searchResults : SearchResultsComponent ;
2022-10-14 05:09:25 +04:00
@HostListener ( 'keydown' , [ '$event' ] ) keydown ( $event ) : void {
2022-05-15 14:47:55 +04:00
this . handleKeyDown ( $event ) ;
}
2022-02-27 18:53:16 +03:00
2023-07-24 10:18:00 +09:00
@ViewChild ( 'searchInput' ) searchInput : ElementRef ;
2020-02-16 22:15:07 +07:00
constructor (
2022-11-28 11:55:23 +09:00
private formBuilder : UntypedFormBuilder ,
2020-02-16 22:15:07 +07:00
private router : Router ,
2020-05-02 12:36:35 +07:00
private assetsService : AssetsService ,
2020-05-09 20:37:50 +07:00
private stateService : StateService ,
2020-07-24 22:37:35 +07:00
private electrsApiService : ElectrsApiService ,
2022-05-15 14:47:55 +04:00
private apiService : ApiService ,
2021-12-30 02:30:46 +04:00
private relativeUrlPipe : RelativeUrlPipe ,
2023-07-24 10:18:00 +09:00
private elementRef : ElementRef
) {
}
2022-08-28 00:07:13 +09:00
2022-10-14 05:09:25 +04:00
ngOnInit ( ) : void {
2023-12-31 23:23:53 +01:00
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
} ) ;
2020-05-09 20:37:50 +07:00
2023-07-24 10:18:00 +09:00
this . router . events . subscribe ( ( e : NavigationStart ) = > { // Reset search focus when changing page
2023-07-25 15:03:39 +09:00
if ( this . searchInput && e . type === EventType . NavigationStart ) {
2023-07-24 10:18:00 +09:00
this . searchInput . nativeElement . blur ( ) ;
}
} ) ;
2023-07-25 15:03:39 +09:00
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 ) {
2023-07-24 10:18:00 +09:00
this . searchInput . nativeElement . focus ( ) ;
}
} ) ;
2020-05-09 20:37:50 +07:00
2020-02-16 22:15:07 +07:00
this . searchForm = this . formBuilder . group ( {
searchText : [ '' , Validators . required ] ,
} ) ;
2021-05-13 03:01:47 +04:00
2021-12-27 22:54:45 +04:00
if ( this . network === 'liquid' || this . network === 'liquidtestnet' ) {
2020-05-05 15:26:23 +07:00
this . assetsService . getAssetsMinimalJson $
. subscribe ( ( assets ) = > {
this . assets = assets ;
} ) ;
}
2020-02-16 22:15:07 +07:00
2022-10-14 05:09:25 +04:00
const searchText $ = this . searchForm . get ( 'searchText' ) . valueChanges
. pipe (
map ( ( text ) = > {
return text . trim ( ) ;
} ) ,
2023-07-12 10:11:04 +09:00
tap ( ( text ) = > {
this . stateService . searchText $ . next ( text ) ;
} ) ,
2022-10-14 05:09:25 +04:00
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 ) {
2022-05-15 14:47:55 +04:00
return zip (
this . electrsApiService . getAddressesByPrefix $ ( text ) . pipe ( catchError ( ( ) = > of ( [ ] ) ) ) ,
2022-10-14 05:09:25 +04:00
[ { nodes : [ ] , channels : [ ] } ] ,
) ;
}
return zip (
this . electrsApiService . getAddressesByPrefix $ ( text ) . pipe ( catchError ( ( ) = > of ( [ ] ) ) ) ,
this . apiService . lightningSearch $ ( text ) . pipe ( catchError ( ( ) = > of ( {
nodes : [ ] ,
channels : [ ] ,
} ) ) ) ,
) ;
} ) ,
2023-01-29 21:18:44 +04:00
map ( ( result : any [ ] ) = > {
return result ;
} ) ,
tap ( ( ) = > {
2022-10-14 05:09:25 +04:00
this . isTypeaheading $ . next ( false ) ;
} )
) ;
this . typeAhead $ = combineLatest (
[
searchText $ ,
searchResults $ . pipe (
startWith ( [
[ ] ,
{
nodes : [ ] ,
channels : [ ] ,
}
] ) )
]
) . pipe (
map ( ( latestData ) = > {
2023-01-29 21:18:44 +04:00
let searchText = latestData [ 0 ] ;
2022-10-14 05:09:25 +04:00
if ( ! searchText . length ) {
return {
searchText : '' ,
hashQuickMatch : false ,
blockHeight : false ,
txId : false ,
address : false ,
2023-12-31 23:23:53 +01:00
otherNetworks : [ ] ,
2022-10-14 05:09:25 +04:00
addresses : [ ] ,
2022-05-15 19:22:14 +04:00
nodes : [ ] ,
channels : [ ] ,
2024-01-22 11:45:07 +01:00
liquidAsset : [ ] ,
2022-10-14 05:09:25 +04:00
} ;
}
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' ;
2024-01-03 13:23:25 +01:00
2023-12-13 19:22:46 +01:00
const matchesBlockHeight = this . regexBlockheight . test ( searchText ) && parseInt ( searchText ) <= this . stateService . latestBlockHeight ;
2024-01-03 13:23:25 +01:00
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 ;
2022-10-14 05:09:25 +04:00
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 ) ;
2024-01-12 18:04:14 +01:00
const otherNetworks = findOtherNetworks ( searchText , this . network as any || 'mainnet' , this . env ) ;
2024-01-22 11:45:07 +01:00
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 , '-' ) ;
}
2022-05-15 14:47:55 +04:00
return {
2022-10-14 05:09:25 +04:00
searchText : searchText ,
2023-11-30 19:04:14 +01:00
hashQuickMatch : + ( matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress || matchesUnixTimestamp || matchesDateTime ) ,
2022-10-14 05:09:25 +04:00
blockHeight : matchesBlockHeight ,
2023-11-30 19:04:14 +01:00
dateTime : matchesDateTime ,
unixTimestamp : matchesUnixTimestamp ,
2022-10-14 05:09:25 +04:00
txId : matchesTxId ,
blockHash : matchesBlockHash ,
address : matchesAddress ,
2023-12-31 23:23:53 +01:00
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 ,
2022-10-14 05:09:25 +04:00
nodes : lightningResults.nodes ,
channels : lightningResults.channels ,
2024-01-22 11:45:07 +01:00
liquidAsset : liquidAsset ,
2022-05-15 14:47:55 +04:00
} ;
2021-05-13 03:01:47 +04:00
} )
) ;
2022-05-15 14:47:55 +04:00
}
2022-10-14 05:09:25 +04:00
handleKeyDown ( $event ) : void {
2022-05-15 14:47:55 +04:00
this . searchResults . handleKeyDown ( $event ) ;
}
2021-05-13 03:01:47 +04:00
2022-10-14 05:09:25 +04:00
itemSelected ( ) : void {
2020-07-25 17:52:41 +07:00
setTimeout ( ( ) = > this . search ( ) ) ;
}
2022-10-14 05:09:25 +04:00
selectedResult ( result : any ) : void {
2022-05-15 14:47:55 +04:00
if ( typeof result === 'string' ) {
2022-07-06 16:07:21 +02:00
this . search ( result ) ;
2023-12-13 19:22:46 +01:00
} else if ( typeof result === 'number' && result <= this . stateService . latestBlockHeight ) {
2022-09-12 19:20:22 +02:00
this . navigate ( '/block/' , result . toString ( ) ) ;
2022-05-15 14:47:55 +04:00
} else if ( result . alias ) {
this . navigate ( '/lightning/node/' , result . public_key ) ;
} else if ( result . short_id ) {
this . navigate ( '/lightning/channel/' , result . id ) ;
2023-12-31 23:23:53 +01:00
} else if ( result . network ) {
2024-01-12 18:04:14 +01:00
if ( result . isNetworkAvailable ) {
this . navigate ( '/address/' , result . address , undefined , result . network ) ;
} else {
this . searchForm . setValue ( {
searchText : '' ,
} ) ;
this . isSearching = false ;
}
2022-05-15 14:47:55 +04:00
}
}
2022-10-14 05:09:25 +04:00
search ( result? : string ) : void {
2022-07-06 16:07:21 +02:00
const searchText = result || this . searchForm . value . searchText . trim ( ) ;
2020-02-16 22:15:07 +07:00
if ( searchText ) {
2020-11-22 16:03:23 +07:00
this . isSearching = true ;
2022-09-04 21:31:02 +09:00
2023-05-10 19:57:58 -06:00
if ( ! this . regexTransaction . test ( searchText ) && this . regexAddress . test ( searchText ) ) {
2021-07-06 19:55:01 +03:00
this . navigate ( '/address/' , searchText ) ;
2023-12-13 19:22:46 +01:00
} else if ( this . regexBlockhash . test ( searchText ) ) {
2020-11-22 16:03:23 +07:00
this . navigate ( '/block/' , searchText ) ;
2023-12-13 19:22:46 +01:00
} else if ( this . regexBlockheight . test ( searchText ) ) {
parseInt ( searchText ) <= this . stateService . latestBlockHeight ? this . navigate ( '/block/' , searchText ) : this . isSearching = false ;
2020-02-19 23:50:23 +07:00
} else if ( this . regexTransaction . test ( searchText ) ) {
2021-10-19 23:24:12 +04:00
const matches = this . regexTransaction . exec ( searchText ) ;
2021-12-27 22:54:45 +04:00
if ( this . network === 'liquid' || this . network === 'liquidtestnet' ) {
2024-01-22 11:45:07 +01:00
if ( this . assets [ matches [ 0 ] ] ) {
this . navigate ( '/assets/asset/' , matches [ 0 ] ) ;
2020-11-22 16:03:23 +07:00
}
2024-01-22 11:45:07 +01:00
this . electrsApiService . getAsset $ ( matches [ 0 ] )
2020-11-22 16:03:23 +07:00
. subscribe (
2024-01-22 11:45:07 +01:00
( ) = > { this . navigate ( '/assets/asset/' , matches [ 0 ] ) ; } ,
2021-09-25 14:37:54 +04:00
( ) = > {
2024-01-22 11:45:07 +01:00
this . electrsApiService . getBlock $ ( matches [ 0 ] )
2021-09-25 14:37:54 +04:00
. subscribe (
2024-01-22 11:45:07 +01:00
( block ) = > { this . navigate ( '/block/' , matches [ 0 ] , { state : { data : { block } } } ) ; } ,
2021-10-19 23:24:12 +04:00
( ) = > { this . navigate ( '/tx/' , matches [ 0 ] ) ; } ) ;
2021-09-25 14:37:54 +04:00
}
2020-11-22 16:03:23 +07:00
) ;
2020-05-02 12:36:35 +07:00
} else {
2021-10-19 23:24:12 +04:00
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
2024-01-12 17:21:07 +01:00
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 ; }
) ;
2020-02-19 23:50:23 +07:00
} else {
2022-08-30 22:36:13 +02:00
this . searchResults . searchButtonClick ( ) ;
2020-11-22 16:03:23 +07:00
this . isSearching = false ;
2020-02-16 22:15:07 +07:00
}
}
}
2020-11-22 16:03:23 +07:00
2022-09-04 21:31:02 +09:00
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 ) ) {
2023-12-31 23:23:53 +01:00
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 ;
}
2020-11-22 16:03:23 +07:00
}
2020-02-16 22:15:07 +07:00
}