mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
Merge pull request #6510 from mempool/mononaut/cluster-frontend-mobile-fixes
This commit is contained in:
commit
6ff86a34bc
9 changed files with 216 additions and 93 deletions
|
|
@ -48,10 +48,12 @@
|
|||
[class.inactive]="outline.chunkIndex !== effectiveActiveChunk"
|
||||
[attr.d]="outline.path"
|
||||
[ngbTooltip]="chunkLabelTooltip"
|
||||
[disableTooltip]="isMobile"
|
||||
container="body"
|
||||
placement="top"
|
||||
(mouseenter)="onChunkEnter(outline.chunkIndex)"
|
||||
(mouseleave)="onChunkLeave()" />
|
||||
(mouseleave)="onChunkLeave()"
|
||||
(click)="onChunkClick(outline.chunkIndex, $event)" />
|
||||
|
||||
<text *ngFor="let outline of chunkOutlines; trackBy: trackByChunkIndex"
|
||||
class="chunk-label"
|
||||
|
|
@ -59,10 +61,12 @@
|
|||
[attr.x]="outline.labelX" [attr.y]="outline.labelY"
|
||||
text-anchor="middle"
|
||||
[ngbTooltip]="chunkLabelTooltip"
|
||||
[disableTooltip]="isMobile"
|
||||
container="body"
|
||||
placement="top"
|
||||
(mouseenter)="onChunkEnter(outline.chunkIndex)"
|
||||
(mouseleave)="onChunkLeave()">
|
||||
(mouseleave)="onChunkLeave()"
|
||||
(click)="onChunkClick(outline.chunkIndex, $event)">
|
||||
{{ outline.feerate | feeRounding }} sat/vB
|
||||
</text>
|
||||
</ng-container>
|
||||
|
|
@ -76,7 +80,8 @@
|
|||
stroke-width="12"
|
||||
(mouseenter)="onEdgeEnter(i, $event)"
|
||||
(mousemove)="onEdgeMove($event)"
|
||||
(mouseleave)="onEdgeLeave()" />
|
||||
(mouseleave)="onEdgeLeave()"
|
||||
(click)="onEdgeClick(i, $event)" />
|
||||
<path class="edge-line"
|
||||
[class.highlighted]="edge.highlighted"
|
||||
[class.ancestor]="edge.highlightKind === 'ancestor'"
|
||||
|
|
@ -96,7 +101,7 @@
|
|||
(mouseenter)="onNodeEnter(node, $event)"
|
||||
(mousemove)="onNodeMove($event)"
|
||||
(mouseleave)="onNodeLeave()"
|
||||
(click)="onNodeClick(node)">
|
||||
(click)="onNodeClick(node, $event)">
|
||||
<rect class="node-rect"
|
||||
[class.current]="node.isCurrent"
|
||||
[class.hovered]="node.hovered"
|
||||
|
|
@ -120,7 +125,7 @@
|
|||
|
||||
<div #tooltip
|
||||
class="cluster-tooltip"
|
||||
*ngIf="hoverNode || hoverEdge"
|
||||
*ngIf="!isMobile && (hoverNode || hoverEdge)"
|
||||
[style.left.px]="tooltipPosition.x"
|
||||
[style.top.px]="tooltipPosition.y">
|
||||
<ng-container *ngIf="hoverNode">
|
||||
|
|
@ -154,3 +159,39 @@
|
|||
<div><span class="swatch descendant"></span><span i18n="cluster.child|child">child</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="cluster-mobile-panel"
|
||||
*ngIf="isMobile && (hoverNode || hoverEdge)"
|
||||
(click)="$event.stopPropagation()">
|
||||
<ng-container *ngIf="hoverNode">
|
||||
<div class="tx-id-row">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, hoverNode.tx.txid]">{{ hoverNode.tx.txid | shortenString }}</a>
|
||||
<span class="this-tx-badge" *ngIf="hoverNode.isCurrent" i18n="cluster.this-transaction|this transaction">this transaction</span>
|
||||
</div>
|
||||
<div class="tooltip-body">
|
||||
<table class="stats">
|
||||
<tr>
|
||||
<th i18n="transaction.fee|Transaction fee">Fee</th>
|
||||
<td>{{ hoverNode.tx.fee | number }} <span class="unit" i18n="shared.sats">sats</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th i18n="transaction.vsize|Virtual size">Size</th>
|
||||
<td><span [innerHTML]="hoverNode.tx.weight / 4 | vbytes: 2"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
|
||||
<td>{{ hoverNode.feerate | feeRounding }} <span class="unit">sat/vB</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="legend">
|
||||
<div><span class="swatch ancestor"></span><span i18n="cluster.parents|parents">parents</span></div>
|
||||
<div><span class="swatch descendant"></span><span i18n="cluster.children|children">children</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="legend edge-legend" *ngIf="!hoverNode && hoverEdge">
|
||||
<div><span class="swatch ancestor"></span><span i18n="cluster.parent|parent">parent</span></div>
|
||||
<div><span class="swatch descendant"></span><span i18n="cluster.child|child">child</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -144,17 +144,14 @@
|
|||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cluster-tooltip {
|
||||
position: absolute;
|
||||
.cluster-tooltip,
|
||||
.cluster-mobile-panel {
|
||||
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.5);
|
||||
color: var(--tooltip-grey);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
max-width: 360px;
|
||||
white-space: nowrap;
|
||||
|
||||
.tx-id-row {
|
||||
display: flex;
|
||||
|
|
@ -225,3 +222,15 @@
|
|||
&.descendant { background: var(--cluster-descendant-color); }
|
||||
}
|
||||
}
|
||||
|
||||
.cluster-tooltip {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
max-width: 360px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cluster-mobile-panel {
|
||||
margin-top: 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
|
|||
@Input() cluster: { txs: CpfpClusterTx[]; chunks: CpfpClusterChunk[]; chunkIndex: number };
|
||||
@Input() txid: string;
|
||||
@Input() preview = false;
|
||||
@Input() isMobile = false;
|
||||
|
||||
@ViewChild('graphContainer', { static: true }) graphContainer: ElementRef;
|
||||
@ViewChild('tooltip') tooltipElement: ElementRef;
|
||||
|
|
@ -125,8 +126,17 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
|
|||
}
|
||||
|
||||
onNodeEnter(node: RenderedNode, event: MouseEvent): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.applyNodeHighlight(node);
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private applyNodeHighlight(node: RenderedNode): void {
|
||||
this.hoverNode = node;
|
||||
this.hoverEdge = null;
|
||||
this.hoverChunkIndex = null;
|
||||
this.applyEffectiveChunk();
|
||||
this.clearHighlights();
|
||||
node.hovered = true;
|
||||
for (const edge of this.edges) {
|
||||
|
|
@ -140,58 +150,63 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
|
|||
edge.highlightKind = 'ancestor';
|
||||
}
|
||||
}
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onNodeMove(event: MouseEvent): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onNodeLeave(): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.hoverNode = null;
|
||||
this.clearHighlights();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onEdgeEnter(edgeIndex: number, event: MouseEvent): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.applyEdgeHighlight(edgeIndex);
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private applyEdgeHighlight(edgeIndex: number): void {
|
||||
this.clearHighlights();
|
||||
this.hoverNode = null;
|
||||
this.hoverChunkIndex = null;
|
||||
this.applyEffectiveChunk();
|
||||
const edge = this.edges[edgeIndex];
|
||||
edge.highlighted = true;
|
||||
edge.highlightKind = 'direct';
|
||||
this.nodes[edge.parentIndex].relation = 'ancestor';
|
||||
this.nodes[edge.childIndex].relation = 'descendant';
|
||||
this.hoverEdge = edge;
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onEdgeMove(event: MouseEvent): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.updateTooltipPosition(event);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onEdgeLeave(): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.hoverEdge = null;
|
||||
this.clearHighlights();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onChunkEnter(chunkIndex: number): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.hoverChunkIndex = chunkIndex;
|
||||
this.applyEffectiveChunk();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onChunkLeave(): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.preview || this.isMobile) { return; }
|
||||
this.hoverChunkIndex = null;
|
||||
this.applyEffectiveChunk();
|
||||
this.cd.markForCheck();
|
||||
|
|
@ -212,13 +227,61 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
|
|||
}
|
||||
}
|
||||
|
||||
onNodeClick(node: RenderedNode): void {
|
||||
onNodeClick(node: RenderedNode, event: MouseEvent): void {
|
||||
if (this.preview) { return; }
|
||||
if (this.isMobile) {
|
||||
event.stopPropagation();
|
||||
if (this.hoverNode?.index === node.index) {
|
||||
this.clearMobileSelection();
|
||||
} else {
|
||||
this.applyNodeHighlight(node);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const network = this.stateService.network;
|
||||
const prefix = network && network !== 'mainnet' ? `/${network}` : '';
|
||||
this.router.navigate([prefix + '/tx/', node.tx.txid]);
|
||||
}
|
||||
|
||||
onEdgeClick(edgeIndex: number, event: MouseEvent): void {
|
||||
if (this.preview || !this.isMobile) { return; }
|
||||
event.stopPropagation();
|
||||
const edge = this.edges[edgeIndex];
|
||||
if (this.hoverEdge === edge) {
|
||||
this.clearMobileSelection();
|
||||
} else {
|
||||
this.applyEdgeHighlight(edgeIndex);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
onChunkClick(chunkIndex: number, event: MouseEvent): void {
|
||||
if (this.preview || !this.isMobile) { return; }
|
||||
event.stopPropagation();
|
||||
this.hoverNode = null;
|
||||
this.hoverEdge = null;
|
||||
this.clearHighlights();
|
||||
this.hoverChunkIndex = chunkIndex;
|
||||
this.applyEffectiveChunk();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
onBackgroundClick(): void {
|
||||
if (this.preview || !this.isMobile) { return; }
|
||||
this.clearMobileSelection();
|
||||
}
|
||||
|
||||
private clearMobileSelection(): void {
|
||||
this.hoverNode = null;
|
||||
this.hoverEdge = null;
|
||||
this.hoverChunkIndex = null;
|
||||
this.clearHighlights();
|
||||
this.applyEffectiveChunk();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private clearHighlights(): void {
|
||||
for (const node of this.nodes) {
|
||||
node.hovered = false;
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ const PREVIEW_DIMENSIONS: RenderDimensions = {
|
|||
};
|
||||
|
||||
const PREVIEW_VIEWPORT_HEIGHT = 48;
|
||||
const PREVIEW_SAFE_INSET = 16;
|
||||
const OUTLINE_PAD = 6;
|
||||
|
||||
export function renderLayout(layout: GridLayout, params: RenderParams): RenderResult {
|
||||
|
|
@ -175,7 +176,7 @@ export function renderLayout(layout: GridLayout, params: RenderParams): RenderRe
|
|||
let cellW: number;
|
||||
if (params.preview) {
|
||||
const activeCols = activeChunkColCount(layout, params.activeChunkIndex);
|
||||
const denom = Math.max(1, activeCols);
|
||||
const denom = Math.max(1, activeCols + 1);
|
||||
cellW = Math.max(dim.minCellW, Math.min(dim.maxCellW,
|
||||
(params.containerWidth - dim.marginX * 2) / denom));
|
||||
} else if (layout.cols > 0) {
|
||||
|
|
@ -222,15 +223,21 @@ export function renderLayout(layout: GridLayout, params: RenderParams): RenderRe
|
|||
activeMaxY = Math.max(activeMaxY, n.rectY + n.height);
|
||||
}
|
||||
}
|
||||
const pad = dim.marginX;
|
||||
const pad = dim.marginX + PREVIEW_SAFE_INSET;
|
||||
const chunkFits = isFinite(activeMinX) && (activeMaxX - activeMinX) + 2 * pad <= vbWidth;
|
||||
const vbX = chunkFits
|
||||
let vbX = chunkFits
|
||||
? (activeMinX + activeMaxX) / 2 - vbWidth / 2
|
||||
: selected.x - vbWidth / 2;
|
||||
if (!chunkFits && isFinite(activeMinX)) {
|
||||
vbX = Math.max(activeMinX - pad, Math.min(activeMaxX + pad - vbWidth, vbX));
|
||||
}
|
||||
const chunkFitsVertically = isFinite(activeMinY) && (activeMaxY - activeMinY) + 2 * dim.marginY <= vbHeight;
|
||||
const vbY = chunkFitsVertically
|
||||
let vbY = chunkFitsVertically
|
||||
? (activeMinY + activeMaxY) / 2 - vbHeight / 2
|
||||
: selected.y - vbHeight / 2;
|
||||
if (!chunkFitsVertically && isFinite(activeMinY)) {
|
||||
vbY = Math.max(activeMinY - dim.marginY, Math.min(activeMaxY + dim.marginY - vbHeight, vbY));
|
||||
}
|
||||
|
||||
return {
|
||||
nodes, edges, chunkOutlines,
|
||||
|
|
|
|||
|
|
@ -248,13 +248,14 @@
|
|||
<ng-template #clusterPreviewButton let-cluster="cluster" let-txid="txid">
|
||||
<div class="cluster-preview-inline" role="button" tabindex="0"
|
||||
[ngbTooltip]="clusterPreviewTooltip"
|
||||
[disableTooltip]="isMobile"
|
||||
placement="bottom"
|
||||
tooltipClass="cluster-preview-ngb-tooltip"
|
||||
container="body"
|
||||
(click)="toggleCpfp()"
|
||||
(keydown.enter)="toggleCpfp()"
|
||||
(keydown.space)="toggleCpfp(); $event.preventDefault()">
|
||||
<app-cluster-diagram class="cluster-preview-diagram" [cluster]="cluster" [txid]="txid" [preview]="true"></app-cluster-diagram>
|
||||
<app-cluster-diagram class="cluster-preview-diagram" [cluster]="cluster" [txid]="txid" [preview]="true" [isMobile]="isMobile"></app-cluster-diagram>
|
||||
<fa-icon class="cluster-preview-expand" [icon]="['fas', cpfpMode ? 'compress' : 'expand']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
<ng-template #clusterPreviewTooltip>
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@
|
|||
<ng-container *ngIf="cpfpInfo?.cluster; else legacyCpfpRaw">
|
||||
<div class="box">
|
||||
<app-cluster-diagram
|
||||
[cluster]="cpfpInfo.cluster" [txid]="transaction.txid">
|
||||
[cluster]="cpfpInfo.cluster" [txid]="transaction.txid" [isMobile]="isMobile">
|
||||
</app-cluster-diagram>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -248,4 +248,4 @@
|
|||
</h3>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||
this.seoService.setTitle($localize`:@@d7f92e6fe26fba6fff568cbdae5db4a5c8c6a55c:Preview Transaction`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.preview-tx:Preview a transaction to the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network using the transaction's raw hex data.`);
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
this.cpfpMode = this.isCpfpParamEnabled(this.route.snapshot.queryParams['cpfp']);
|
||||
this.cpfpMode = this.route.snapshot.queryParams['cpfp'] === 'true';
|
||||
this.pushTxForm = this.formBuilder.group({
|
||||
txRaw: ['', Validators.required],
|
||||
});
|
||||
|
|
@ -380,10 +380,6 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
private isCpfpParamEnabled(cpfpParam: string | undefined): boolean {
|
||||
return cpfpParam === 'true' || cpfpParam === 'advanced' || cpfpParam === 'simple';
|
||||
}
|
||||
|
||||
setupGraph() {
|
||||
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.transaction?.vin?.length || 1, this.transaction?.vout?.length + 1 || 1));
|
||||
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@
|
|||
|
||||
<!-- CPFP Details -->
|
||||
<ng-container *ngIf="cpfpMode && hasCpfp">
|
||||
<span id="cluster" #cluster class="anchor"></span>
|
||||
<br>
|
||||
<div class="title float-start">
|
||||
<h2 *ngIf="cpfpInfo?.cluster && !isAcceleration" i18n="transaction.cluster|Cluster">Cluster</h2>
|
||||
|
|
@ -93,7 +92,7 @@
|
|||
<ng-container *ngIf="cpfpInfo?.cluster && !isAcceleration; else legacyCpfp">
|
||||
<div class="box">
|
||||
<app-cluster-diagram
|
||||
[cluster]="cpfpInfo.cluster" [txid]="tx.txid">
|
||||
[cluster]="cpfpInfo.cluster" [txid]="tx.txid" [isMobile]="isMobile">
|
||||
</app-cluster-diagram>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -394,4 +393,4 @@
|
|||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -173,6 +173,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
graphContainer: ElementRef;
|
||||
private txList: TransactionsListComponent;
|
||||
private fragmentAnchor: string | null = null;
|
||||
private scrolledFragmentAnchor: string | null = null;
|
||||
private firstFragmentScroll = true;
|
||||
|
||||
@ViewChild('txList')
|
||||
set txListSetter(component: TransactionsListComponent | undefined) {
|
||||
|
|
@ -197,13 +200,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewChild('cluster')
|
||||
set clusterAnchor(element: ElementRef | null | undefined) {
|
||||
if (element) {
|
||||
setTimeout(() => { this.applyFragment(); }, 0);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
|
|
@ -228,7 +224,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
ngOnInit() {
|
||||
this.enterpriseService.page();
|
||||
this.isDetailsOpen = this.route.snapshot.queryParams['showDetails'] === 'true';
|
||||
this.cpfpMode = this.isCpfpParamEnabled(this.route.snapshot.queryParams['cpfp']);
|
||||
this.cpfpMode = this.route.snapshot.queryParams['cpfp'] === 'true';
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.forceAccelerationSummary = !!urlParams.get('cash_request_id');
|
||||
|
|
@ -609,7 +605,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: this.fragmentParams.toString(),
|
||||
fragment: this.formatFragment(this.fragmentParams),
|
||||
});
|
||||
} else {
|
||||
this.txId = urlMatch[0];
|
||||
|
|
@ -620,7 +616,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.fragmentParams.delete('vin');
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: this.fragmentParams.toString(),
|
||||
fragment: this.formatFragment(this.fragmentParams),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -630,7 +626,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
if (window.innerWidth <= 767.98) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
|
||||
queryParamsHandling: 'merge',
|
||||
preserveFragment: true,
|
||||
fragment: this.formatFragment(this.fragmentParams, this.fragmentAnchor),
|
||||
queryParams: { mode: 'details' },
|
||||
replaceUrl: true,
|
||||
});
|
||||
|
|
@ -888,7 +884,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
relativeTo: this.route,
|
||||
queryParams: { showDetails: this.isDetailsOpen ? 'true' : null },
|
||||
queryParamsHandling: 'merge',
|
||||
preserveFragment: true,
|
||||
fragment: this.formatFragment(this.fragmentParams),
|
||||
replaceUrl: true,
|
||||
});
|
||||
this.txList?.setDetailsOpen(this.isDetailsOpen);
|
||||
|
|
@ -1081,6 +1077,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
resetTransaction() {
|
||||
this.firstLoad = false;
|
||||
this.firstFragmentScroll = this.fragmentAnchor !== null;
|
||||
this.scrolledFragmentAnchor = null;
|
||||
this.gotInitialPosition = false;
|
||||
this.error = undefined;
|
||||
this.tx = null;
|
||||
|
|
@ -1108,7 +1106,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.auditStatus = null;
|
||||
this.accelerationPositions = null;
|
||||
this.isDetailsOpen = this.route.snapshot.queryParams['showDetails'] === 'true';
|
||||
this.cpfpMode = this.isCpfpParamEnabled(this.route.snapshot.queryParams['cpfp']);
|
||||
this.cpfpMode = this.route.snapshot.queryParams['cpfp'] === 'true';
|
||||
document.body.scrollTo(0, 0);
|
||||
this.isAcceleration = false;
|
||||
this.isAccelerated$.next(this.isAcceleration);
|
||||
|
|
@ -1128,41 +1126,26 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
toggleCpfp() {
|
||||
this.cpfpMode = !this.cpfpMode;
|
||||
if (this.cpfpInfo?.cluster) {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { cpfp: this.cpfpMode ? 'true' : null },
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: this.getCpfpFragment(),
|
||||
replaceUrl: true,
|
||||
});
|
||||
} else {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { cpfp: this.cpfpMode ? 'simple' : null },
|
||||
queryParamsHandling: 'merge',
|
||||
preserveFragment: true,
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { cpfp: this.cpfpMode ? 'true' : null },
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: this.formatFragment(this.fragmentParams),
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
|
||||
private isCpfpParamEnabled(cpfpParam: string | undefined): boolean {
|
||||
return cpfpParam === 'true' || cpfpParam === 'advanced' || cpfpParam === 'simple';
|
||||
}
|
||||
|
||||
private getCpfpFragment(): string | null {
|
||||
const currentParams = new URLSearchParams(this.fragmentParams?.toString() || this.route.snapshot.fragment || '');
|
||||
const fragmentParams = new URLSearchParams();
|
||||
if (this.cpfpMode) {
|
||||
fragmentParams.set('cluster', '');
|
||||
}
|
||||
for (const [key, value] of currentParams.entries()) {
|
||||
if (key !== 'cluster') {
|
||||
fragmentParams.set(key, value);
|
||||
private formatFragment(fragmentParams: URLSearchParams, anchor: string | null = null): string | null {
|
||||
const params = new URLSearchParams(fragmentParams.toString());
|
||||
for (const [key, value] of Array.from(params.entries())) {
|
||||
if (value === '') {
|
||||
params.delete(key);
|
||||
}
|
||||
}
|
||||
return fragmentParams.toString() || null;
|
||||
if (anchor) {
|
||||
params.set(anchor, '');
|
||||
}
|
||||
return params.toString() || null;
|
||||
}
|
||||
|
||||
toggleGraph() {
|
||||
|
|
@ -1172,7 +1155,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
relativeTo: this.route,
|
||||
queryParams: { showFlow: showFlow },
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: 'flow'
|
||||
fragment: this.formatFragment(this.fragmentParams, showFlow ? 'flow' : null)
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1192,11 +1175,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
// simulate normal anchor fragment behavior
|
||||
applyFragment(): void {
|
||||
const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === '');
|
||||
if (anchor?.length) {
|
||||
const anchorElement = document.getElementById(anchor[0]);
|
||||
if (this.fragmentAnchor && this.scrolledFragmentAnchor !== this.fragmentAnchor) {
|
||||
const anchorElement = document.getElementById(this.fragmentAnchor);
|
||||
if (anchorElement) {
|
||||
anchorElement.scrollIntoView({ behavior: 'smooth' });
|
||||
anchorElement.scrollIntoView({ behavior: this.firstFragmentScroll ? 'auto' : 'smooth' });
|
||||
this.firstFragmentScroll = false;
|
||||
this.scrolledFragmentAnchor = this.fragmentAnchor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1205,12 +1189,35 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
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;
|
||||
const inputIndex = (!isNaN(vin) && vin >= 0) ? vin : null;
|
||||
const outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
|
||||
const selectionChanged = inputIndex !== this.inputIndex || outputIndex !== this.outputIndex;
|
||||
const anchor = Array.from(this.fragmentParams.entries()).find(([, value]) => value === '')?.[0] || null;
|
||||
this.inputIndex = inputIndex;
|
||||
this.outputIndex = outputIndex;
|
||||
if (this.fragmentParams.has('accelerate')) {
|
||||
this.forceAccelerationSummary = true;
|
||||
}
|
||||
setTimeout(() => { this.applyFragment(); }, 0);
|
||||
if (!anchor && !this.fragmentAnchor) {
|
||||
this.firstFragmentScroll = false;
|
||||
}
|
||||
if (selectionChanged && anchor) {
|
||||
this.scrolledFragmentAnchor = null;
|
||||
}
|
||||
if (anchor !== this.fragmentAnchor) {
|
||||
this.fragmentAnchor = anchor;
|
||||
this.scrolledFragmentAnchor = null;
|
||||
if (!this.fragmentAnchor) {
|
||||
this.firstFragmentScroll = false;
|
||||
}
|
||||
}
|
||||
if (this.scrolledFragmentAnchor !== this.fragmentAnchor) {
|
||||
if (this.fragmentAnchor) {
|
||||
setTimeout(() => { this.applyFragment(); }, 0);
|
||||
} else {
|
||||
this.firstFragmentScroll = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setHasAccelerationDetails(hasDetails: boolean): void {
|
||||
|
|
@ -1237,20 +1244,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
onAccelerationCompleted(): void {
|
||||
this.router.navigate([], { fragment: null, queryParamsHandling: 'merge' });
|
||||
this.router.navigate([], { fragment: this.formatFragment(this.fragmentParams), queryParamsHandling: 'merge' });
|
||||
this.accelerationFlowCompleted = true;
|
||||
this.forceAccelerationSummary = false;
|
||||
}
|
||||
|
||||
closeAccelerator(): void {
|
||||
this.router.navigate([], { fragment: null, queryParamsHandling: 'merge' });
|
||||
this.router.navigate([], { fragment: this.formatFragment(this.fragmentParams), queryParamsHandling: 'merge' });
|
||||
this.hideAccelerationSummary = true;
|
||||
this.forceAccelerationSummary = false;
|
||||
this.storageService.setValue('hide-accelerator-pref', 'true');
|
||||
}
|
||||
|
||||
openAccelerator(): void {
|
||||
this.router.navigate([], { fragment: 'accelerate', queryParamsHandling: 'merge' });
|
||||
this.router.navigate([], { fragment: this.formatFragment(this.fragmentParams, 'accelerate'), queryParamsHandling: 'merge' });
|
||||
this.accelerationFlowCompleted = false;
|
||||
this.hideAccelerationSummary = false;
|
||||
this.storageService.setValue('hide-accelerator-pref', 'false');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue