From ae9439a991d13bd0c7425d6cff950bc7be852b22 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 11 Oct 2022 20:54:17 +0000 Subject: [PATCH] vin/vout selection syntax via url fragments --- .../transaction/transaction.component.html | 2 - .../transaction/transaction.component.ts | 55 +++++++++++++++---- .../transactions-list.component.html | 4 +- .../tx-bowtie-graph.component.ts | 39 +++++++++---- frontend/tsconfig.base.json | 3 +- 5 files changed, 74 insertions(+), 29 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 86930bcc7..ec0e824c8 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -210,8 +210,6 @@ [network]="network" [tooltip]="true" [inputIndex]="inputIndex" [outputIndex]="outputIndex" - (selectInput)="selectInput($event)" - (selectOutput)="selectOutput($event)" > diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 2be549569..0235dd887 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -18,6 +18,7 @@ import { ApiService } from '../../services/api.service'; import { SeoService } from '../../services/seo.service'; import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface'; import { LiquidUnblinding } from './liquid-ublinding'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; @Component({ selector: 'app-transaction', @@ -40,6 +41,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { txReplacedSubscription: Subscription; blocksSubscription: Subscription; queryParamsSubscription: Subscription; + urlFragmentSubscription: Subscription; + fragmentParams: URLSearchParams; rbfTransaction: undefined | Transaction; cpfpInfo: CpfpInfo | null; showCpfpDetails = false; @@ -67,6 +70,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { constructor( private route: ActivatedRoute, private router: Router, + private relativeUrlPipe: RelativeUrlPipe, private electrsApiService: ElectrsApiService, private stateService: StateService, private websocketService: WebsocketService, @@ -93,6 +97,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { map((da) => da.timeAvg) ); + this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => { + this.fragmentParams = new URLSearchParams(fragment || ''); + const vin = parseInt(this.fragmentParams.get('vin'), 10); + const vout = parseInt(this.fragmentParams.get('vout'), 10); + this.inputIndex = (!isNaN(vin) && vin >= 0) ? vin : null; + this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null; + }); + this.fetchCpfpSubscription = this.fetchCpfp$ .pipe( switchMap((txId) => @@ -132,13 +144,29 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { switchMap((params: ParamMap) => { const urlMatch = (params.get('id') || '').split(':'); if (urlMatch.length === 2 && urlMatch[1].length === 64) { - this.inputIndex = parseInt(urlMatch[0], 10); - this.outputIndex = null; + const vin = parseInt(urlMatch[0], 10); this.txId = urlMatch[1]; + // rewrite legacy vin syntax + if (!isNaN(vin)) { + this.fragmentParams.set('vin', vin.toString()); + this.fragmentParams.delete('vout'); + } + this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], { + queryParamsHandling: 'merge', + fragment: this.fragmentParams.toString(), + }); } else { this.txId = urlMatch[0]; - this.outputIndex = urlMatch[1] === undefined ? null : parseInt(urlMatch[1], 10); - this.inputIndex = null; + const vout = parseInt(urlMatch[1], 10); + if (urlMatch.length > 1 && !isNaN(vout)) { + // rewrite legacy vout syntax + this.fragmentParams.set('vout', vout.toString()); + this.fragmentParams.delete('vin'); + this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], { + queryParamsHandling: 'merge', + fragment: this.fragmentParams.toString(), + }); + } } this.seoService.setTitle( $localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:` @@ -222,6 +250,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.fetchCpfp$.next(this.tx.txid); } } + setTimeout(() => { this.applyFragment(); }, 0); }, (error) => { this.error = error; @@ -359,14 +388,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.graphExpanded = false; } - selectInput(input) { - this.inputIndex = input; - this.outputIndex = null; - } - - selectOutput(output) { - this.outputIndex = output; - this.inputIndex = null; + // simulate normal anchor fragment behavior + applyFragment(): void { + const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === ''); + if (anchor) { + const anchorElement = document.getElementById(anchor[0]); + if (anchorElement) { + anchorElement.scrollIntoView(); + } + } } @HostListener('window:resize', ['$event']) @@ -383,6 +413,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.blocksSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe(); this.flowPrefSubscription.unsubscribe(); + this.urlFragmentSubscription.unsubscribe(); this.leaveTransaction(); } } diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index e53c54a7a..a808eacd1 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -43,7 +43,7 @@ - + @@ -220,7 +220,7 @@ - + diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts index 9d29500f0..5fadc7572 100644 --- a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Input, Output, EventEmitter, OnChanges, HostListener } from '@angular/core'; +import { Component, OnInit, Input, OnChanges, HostListener } from '@angular/core'; import { StateService } from '../../services/state.service'; import { Outspend, Transaction } from '../../interfaces/electrs.interface'; import { Router } from '@angular/router'; @@ -43,9 +43,6 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { @Input() inputIndex: number; @Input() outputIndex: number; - @Output() selectInput = new EventEmitter(); - @Output() selectOutput = new EventEmitter(); - inputData: Xput[]; outputData: Xput[]; inputs: SvgLine[]; @@ -369,23 +366,41 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { if (side === 'input') { const input = this.tx.vin[index]; if (input && input.txid && input.vout != null) { - this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid + ':' + input.vout], { + this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid], { queryParamsHandling: 'merge', - fragment: 'flow' + fragment: (new URLSearchParams({ + flow: '', + vout: input.vout.toString(), + })).toString(), + }); + } else if (index != null) { + this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.tx.txid], { + queryParamsHandling: 'merge', + fragment: (new URLSearchParams({ + flow: '', + vin: index.toString(), + })).toString(), }); - } else { - this.selectInput.emit(index); } } else { const output = this.tx.vout[index]; const outspend = this.outspends[index]; if (output && outspend && outspend.spent && outspend.txid) { - this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.vin + ':' + outspend.txid], { + this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.txid], { queryParamsHandling: 'merge', - fragment: 'flow' + fragment: (new URLSearchParams({ + flow: '', + vin: outspend.vin.toString(), + })).toString(), + }); + } else if (index != null) { + this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.tx.txid], { + queryParamsHandling: 'merge', + fragment: (new URLSearchParams({ + flow: '', + vout: index.toString(), + })).toString(), }); - } else { - this.selectOutput.emit(index); } } } diff --git a/frontend/tsconfig.base.json b/frontend/tsconfig.base.json index b12d6068c..c3676addb 100644 --- a/frontend/tsconfig.base.json +++ b/frontend/tsconfig.base.json @@ -16,7 +16,8 @@ ], "lib": [ "es2018", - "dom" + "dom", + "dom.iterable" ] }, "angularCompilerOptions": {