From 580ac889df3e67171520fa40b0ddb892d79db4ac Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 17 Oct 2024 08:25:40 +0000 Subject: [PATCH] multiblock interactivity --- .../block-overview-graph.component.ts | 4 +- .../block-overview-graph/block-scene.ts | 8 +- .../block-overview-multi.component.ts | 153 ++++++++++++++++++ .../eight-mempool.component.html | 1 + .../eight-mempool/eight-mempool.component.ts | 12 +- 5 files changed, 172 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index b83f0e36a..5c2939471 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -553,7 +553,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On x: cssX, y: cssY }; - const selected = this.scene.getTxAt({ x, y }); + const selected = this.scene.getTxAt({ x, y: this.displayHeight - y }); const currentPreview = this.selectedTx || this.hoverTx; if (selected !== currentPreview) { @@ -627,7 +627,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On if (this.scene) { const x = cssX * window.devicePixelRatio; const y = cssY * window.devicePixelRatio; - const selected = this.scene.getTxAt({ x, y }); + const selected = this.scene.getTxAt({ x, y: this.displayHeight - y }); if (selected && selected.txid) { this.txClickEvent.emit({ tx: selected, keyModifier }); } diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index 2901f90fd..63c6f07da 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -228,7 +228,11 @@ export default class BlockScene { getTxAt(position: Position): TxView | void { if (this.layout) { const gridPosition = this.screenToGrid(position); - return this.layout.getTx(gridPosition); + if (gridPosition.x >= 0 && gridPosition.x < this.gridWidth && gridPosition.y >= 0 && gridPosition.y < this.gridHeight) { + return this.layout.getTx(gridPosition); + } else { + return null; + } } else { return null; } @@ -465,7 +469,7 @@ export default class BlockScene { private screenToGrid(position: Position): Position { let x = position.x - this.x; - let y = this.height - (position.y - this.y); + let y = position.y - this.y; let t; switch (this.orientation) { diff --git a/frontend/src/app/components/block-overview-multi/block-overview-multi.component.ts b/frontend/src/app/components/block-overview-multi/block-overview-multi.component.ts index 63c6f5aae..858b9fe1e 100644 --- a/frontend/src/app/components/block-overview-multi/block-overview-multi.component.ts +++ b/frontend/src/app/components/block-overview-multi/block-overview-multi.component.ts @@ -523,6 +523,141 @@ export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, On } } + @HostListener('document:click', ['$event']) + clickAway(event) { + if (!this.elRef.nativeElement.contains(event.target)) { + const currentPreview = this.selectedTx || this.hoverTx; + if (currentPreview) { + for (const scene of this.scenes) { + if (scene) { + scene.setHover(currentPreview, false); + } + } + this.start(); + } + this.hoverTx = null; + this.selectedTx = null; + this.onTxHover(null); + } + } + + @HostListener('pointerup', ['$event']) + onClick(event) { + if (!this.canvas) { + return; + } + if (event.target === this.canvas.nativeElement && event.pointerType === 'touch') { + this.setPreviewTx(event.offsetX, event.offsetY, true); + } else if (event.target === this.canvas.nativeElement) { + const keyMod = event.shiftKey || event.ctrlKey || event.metaKey; + const middleClick = event.which === 2 || event.button === 1; + this.onTxClick(event.offsetX, event.offsetY, keyMod || middleClick); + } + } + + @HostListener('pointermove', ['$event']) + onPointerMove(event) { + if (!this.canvas) { + return; + } + if (event.target === this.canvas.nativeElement) { + this.setPreviewTx(event.offsetX, event.offsetY, false); + } else { + this.onPointerLeave(event); + } + } + + @HostListener('pointerleave', ['$event']) + onPointerLeave(event) { + if (event.pointerType !== 'touch') { + this.setPreviewTx(-1, -1, true); + } + } + + setPreviewTx(cssX: number, cssY: number, clicked: boolean = false) { + const x = cssX * window.devicePixelRatio; + const y = cssY * window.devicePixelRatio; + if (!this.selectedTx || clicked) { + this.tooltipPosition = { + x: cssX, + y: cssY + }; + const currentPreview = this.selectedTx || this.hoverTx; + let selected; + for (const scene of this.scenes) { + if (scene) { + selected = scene.getTxAt({ x, y: this.displayHeight - y }); + if (selected) { + break; + } + } + } + + if (selected !== currentPreview) { + if (currentPreview) { + for (const scene of this.scenes) { + if (scene) { + scene.setHover(currentPreview, false); + break; + } + } + this.start(); + } + if (selected) { + for (const scene of this.scenes) { + if (scene) { + scene.setHover(selected, true); + break; + } + } + this.start(); + if (clicked) { + this.selectedTx = selected; + } else { + this.hoverTx = selected; + this.onTxHover(this.hoverTx ? this.hoverTx.txid : null); + } + } else { + if (clicked) { + this.selectedTx = null; + } + this.hoverTx = null; + this.onTxHover(null); + } + } else if (clicked) { + if (selected === this.selectedTx) { + this.hoverTx = this.selectedTx; + this.selectedTx = null; + this.onTxHover(this.hoverTx ? this.hoverTx.txid : null); + } else { + this.selectedTx = selected; + } + } + } + } + + updateSearchHighlight(): void { + if (this.highlightTx && this.highlightTx.txid !== this.searchText) { + for (const scene of this.scenes) { + if (scene) { + scene.setHighlight(this.highlightTx, false); + } + } + this.start(); + } else if (this.searchText && this.searchText.length === 64) { + for (const scene of this.scenes) { + if (scene) { + const highlightTx = scene.txs[this.searchText]; + if (highlightTx) { + scene.setHighlight(highlightTx, true); + this.highlightTx = highlightTx; + this.start(); + } + } + } + } + } + setHighlightingEnabled(enabled: boolean): void { for (const scene of this.scenes) { scene.setHighlighting(enabled); @@ -530,6 +665,24 @@ export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, On this.start(); } + onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) { + for (const scene of this.scenes) { + if (scene) { + const x = cssX * window.devicePixelRatio; + const y = cssY * window.devicePixelRatio; + const selected = scene.getTxAt({ x, y: this.displayHeight - y }); + if (selected && selected.txid) { + this.txClickEvent.emit({ tx: selected, keyModifier }); + return; + } + } + } + } + + onTxHover(hoverId: string) { + this.txHoverEvent.emit(hoverId); + } + getColorFunction(): ((tx: TxView) => Color) { if (this.overrideColors) { return this.overrideColors; diff --git a/frontend/src/app/components/eight-mempool/eight-mempool.component.html b/frontend/src/app/components/eight-mempool/eight-mempool.component.html index eebd3a120..7931689b4 100644 --- a/frontend/src/app/components/eight-mempool/eight-mempool.component.html +++ b/frontend/src/app/components/eight-mempool/eight-mempool.component.html @@ -11,4 +11,5 @@ [animationDuration]="animationDuration" [animationOffset]="animationOffset" [disableSpinner]="true" + (txClickEvent)="onTxClick($event)" > \ No newline at end of file diff --git a/frontend/src/app/components/eight-mempool/eight-mempool.component.ts b/frontend/src/app/components/eight-mempool/eight-mempool.component.ts index fe6b47496..954871109 100644 --- a/frontend/src/app/components/eight-mempool/eight-mempool.component.ts +++ b/frontend/src/app/components/eight-mempool/eight-mempool.component.ts @@ -12,6 +12,7 @@ import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe'; import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component'; import { CacheService } from '../../services/cache.service'; import { isMempoolDelta, MempoolBlockDelta } from '../../interfaces/websocket.interface'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; function bestFitResolution(min, max, n): number { const target = (min + max) / 2; @@ -53,7 +54,6 @@ export class EightMempoolComponent implements OnInit, OnDestroy { queryParamsSubscription: Subscription; graphChangeSubscription: Subscription; blockSub: Subscription; - mempoolBlockSub: Subscription; chainDirection: string = 'right'; poolDirection: string = 'left'; @@ -178,10 +178,18 @@ export class EightMempoolComponent implements OnInit, OnDestroy { .subscribe((network) => this.network = network); } + onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void { + const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`); + if (!event.keyModifier) { + this.router.navigate([url]); + } else { + window.open(url, '_blank'); + } + } + ngOnDestroy(): void { this.stateService.markBlock$.next({}); this.blockSub.unsubscribe(); - this.mempoolBlockSub.unsubscribe(); this.networkChangedSubscription?.unsubscribe(); this.queryParamsSubscription?.unsubscribe(); }