Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,25 @@
[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"
[class.inactive]="outline.chunkIndex !== effectiveActiveChunk"
[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>
Expand All @@ -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'"
Expand All @@ -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"
Expand All @@ -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">
Expand Down Expand Up @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
Expand All @@ -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;
Expand Down
15 changes: 11 additions & 4 deletions frontend/src/app/components/cluster-diagram/cluster-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ <h2 *ngIf="!cpfpInfo?.cluster" i18n="transaction.related-transactions|CPFP List"
<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>
Expand Down Expand Up @@ -248,4 +248,4 @@ <h3>
</h3>
</div>
}
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -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],
});
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading