diff --git a/frontend/src/app/components/blockchain/blockchain.component.html b/frontend/src/app/components/blockchain/blockchain.component.html index 0c4a1cbb7..2c3f1ad8c 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.html +++ b/frontend/src/app/components/blockchain/blockchain.component.html @@ -1,9 +1,9 @@
-
+
- + diff --git a/frontend/src/app/components/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index 63ca22626..135a8b842 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.scss +++ b/frontend/src/app/components/blockchain/blockchain.component.scss @@ -26,43 +26,14 @@ position: absolute; left: 0; top: 75px; - transform: translateX(50vw); + --divider-offset: 50vw; + --mempool-offset: 0px; + transform: translateX(calc(var(--divider-offset) + var(--mempool-offset))); } -.position-container.liquid, .position-container.liquidtestnet { - transform: translateX(420px); -} - -@media (min-width: 768px) { - .blockchain-wrapper.time-ltr { - .position-container.liquid, .position-container.liquidtestnet { - transform: translateX(calc(100vw - 420px)); - } - } -} - -@media (max-width: 767.98px) { - .blockchain-wrapper { - .position-container { - transform: translateX(95vw); - } - .position-container.liquid, .position-container.liquidtestnet { - transform: translateX(50vw); - } - .position-container.loading { - transform: translateX(50vw); - } - } - .blockchain-wrapper.time-ltr { - .position-container { - transform: translateX(5vw); - } - .position-container.liquid, .position-container.liquidtestnet { - transform: translateX(50vw); - } - .position-container.loading { - transform: translateX(50vw); - } +.blockchain-wrapper.time-ltr { + .position-container { + transform: translateX(calc(100vw - var(--divider-offset) - var(--mempool-offset))); } } diff --git a/frontend/src/app/components/blockchain/blockchain.component.ts b/frontend/src/app/components/blockchain/blockchain.component.ts index ab9875a4c..5eb2ed481 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.ts +++ b/frontend/src/app/components/blockchain/blockchain.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, HostListener, ChangeDetectorRef } from '@angular/core'; import { firstValueFrom, Subscription } from 'rxjs'; import { StateService } from '../../services/state.service'; @@ -13,43 +13,95 @@ export class BlockchainComponent implements OnInit, OnDestroy { @Input() pageIndex: number; @Input() blocksPerPage: number = 8; @Input() minScrollWidth: number = 0; + @Input() scrollableMempool: boolean = false; + + @Output() mempoolOffsetChange: EventEmitter = new EventEmitter(); network: string; timeLtrSubscription: Subscription; timeLtr: boolean = this.stateService.timeLtr.value; ltrTransitionEnabled = false; + flipping = false; connectionStateSubscription: Subscription; loadingTip: boolean = true; connected: boolean = true; + dividerOffset: number = 0; + mempoolOffset: number = 0; + constructor( public stateService: StateService, + private cd: ChangeDetectorRef, ) {} - ngOnInit() { + ngOnInit(): void { + this.onResize(); this.network = this.stateService.network; this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { this.timeLtr = !!ltr; }); this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => { this.connected = (state === 2); - }) - firstValueFrom(this.stateService.chainTip$).then(tip => { + }); + firstValueFrom(this.stateService.chainTip$).then(() => { this.loadingTip = false; }); } - ngOnDestroy() { + ngOnDestroy(): void { this.timeLtrSubscription.unsubscribe(); this.connectionStateSubscription.unsubscribe(); } - trackByPageFn(index: number, item: { index: number }) { + trackByPageFn(index: number, item: { index: number }): number { return item.index; } - toggleTimeDirection() { - this.ltrTransitionEnabled = true; - this.stateService.timeLtr.next(!this.timeLtr); + toggleTimeDirection(): void { + this.ltrTransitionEnabled = false; + const prevOffset = this.mempoolOffset; + this.mempoolOffset = 0; + this.mempoolOffsetChange.emit(0); + setTimeout(() => { + this.ltrTransitionEnabled = true; + this.flipping = true; + this.stateService.timeLtr.next(!this.timeLtr); + setTimeout(() => { + this.ltrTransitionEnabled = false; + this.flipping = false; + this.mempoolOffset = prevOffset; + this.mempoolOffsetChange.emit(this.mempoolOffset); + }, 1000); + }, 0); + this.cd.markForCheck(); + } + + onMempoolWidthChange(width): void { + if (this.flipping) { + return; + } + this.mempoolOffset = Math.max(0, width - this.dividerOffset); + this.cd.markForCheck(); + setTimeout(() => { + this.mempoolOffsetChange.emit(this.mempoolOffset); + }, 0); + } + + @HostListener('window:resize', ['$event']) + onResize(): void { + if (window.innerWidth >= 768) { + if (this.stateService.isLiquid()) { + this.dividerOffset = 420; + } else { + this.dividerOffset = window.innerWidth * 0.5; + } + } else { + if (this.stateService.isLiquid()) { + this.dividerOffset = window.innerWidth * 0.5; + } else { + this.dividerOffset = window.innerWidth * 0.95; + } + } + this.cd.markForCheck(); } } diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index a72d24899..bc3633be0 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -1,9 +1,9 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core'; import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs'; import { MempoolBlock } from '../../interfaces/websocket.interface'; import { StateService } from '../../services/state.service'; import { Router } from '@angular/router'; -import { take, map, switchMap } from 'rxjs/operators'; +import { take, map, switchMap, tap } from 'rxjs/operators'; import { feeLevels, mempoolFeeColors } from '../../app.constants'; import { specialBlocks } from '../../app.constants'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; @@ -29,6 +29,9 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { @Input() count: number = null; @Input() spotlight: number = 0; @Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`; + @Input() allBlocks: boolean = false; + + @Output() widthChange: EventEmitter = new EventEmitter(); specialBlocks = specialBlocks; mempoolBlocks: MempoolBlock[] = []; @@ -147,7 +150,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { this.updateMempoolBlockStyles(); this.calculateTransactionPosition(); + return this.mempoolBlocks; + }), + tap(() => { + this.cd.markForCheck(); + this.widthChange.emit(this.containerOffset + this.mempoolBlocks.length * this.blockOffset); }) ); @@ -257,7 +265,10 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { reduceEmptyBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] { const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2; - const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding))); + let blocksAmount = this.stateService.env.MEMPOOL_BLOCKS_AMOUNT; + if (!this.allBlocks) { + blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding))); + } while (blocks.length < blocksAmount) { blocks.push({ blockSize: 0, @@ -277,10 +288,10 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] { const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2; - let blocksAmount; + let blocksAmount = this.stateService.env.MEMPOOL_BLOCKS_AMOUNT; if (this.count) { blocksAmount = 8; - } else { + } else if (!this.allBlocks) { blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding))); } while (blocks.length > blocksAmount) { diff --git a/frontend/src/app/components/start/start.component.html b/frontend/src/app/components/start/start.component.html index ec4bf4805..5cf7b4fd9 100644 --- a/frontend/src/app/components/start/start.component.html +++ b/frontend/src/app/components/start/start.component.html @@ -18,7 +18,7 @@ (dragstart)="onDragStart($event)" (scroll)="onScroll($event)" > - +
diff --git a/frontend/src/app/components/start/start.component.ts b/frontend/src/app/components/start/start.component.ts index 3bd0c086e..22d3d6350 100644 --- a/frontend/src/app/components/start/start.component.ts +++ b/frontend/src/app/components/start/start.component.ts @@ -1,6 +1,6 @@ import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input } from '@angular/core'; import { Subscription } from 'rxjs'; -import { StateService } from '../../services/state.service'; +import { MarkBlockState, StateService } from '../../services/state.service'; import { specialBlocks } from '../../app.constants'; @Component({ @@ -24,6 +24,7 @@ export class StartComponent implements OnInit, OnDestroy { chainTipSubscription: Subscription; chainTip: number = -1; tipIsSet: boolean = false; + lastMark: MarkBlockState; markBlockSubscription: Subscription; blockCounterSubscription: Subscription; @ViewChild('blockchainContainer') blockchainContainer: ElementRef; @@ -40,10 +41,11 @@ export class StartComponent implements OnInit, OnDestroy { minScrollWidth: number; pageIndex: number = 0; pages: any[] = []; - pendingMark: number | void = null; + pendingMark: number | null = null; lastUpdate: number = 0; lastMouseX: number; velocity: number = 0; + mempoolOffset: number = 0; constructor( private stateService: StateService, @@ -70,19 +72,40 @@ export class StartComponent implements OnInit, OnDestroy { this.chainTip = height; this.tipIsSet = true; this.updatePages(); - if (this.pendingMark != null) { - this.scrollToBlock(this.pendingMark); - this.pendingMark = null; - } + this.applyPendingMarkArrow(); }); this.markBlockSubscription = this.stateService.markBlock$.subscribe((mark) => { + let blockHeight; + let newMark = true; if (mark?.blockHeight != null) { + if (this.lastMark?.blockHeight === mark.blockHeight) { + newMark = false; + } + blockHeight = mark.blockHeight; + } else if (mark?.mempoolBlockIndex != null) { + if (this.lastMark?.mempoolBlockIndex === mark.mempoolBlockIndex || (mark.txid && this.lastMark?.txid === mark.txid)) { + newMark = false; + } + blockHeight = -1 - mark.mempoolBlockIndex; + } else if (mark?.mempoolPosition?.block != null) { + if (this.lastMark?.txid === mark.txid) { + newMark = false; + } + blockHeight = -1 - mark.mempoolPosition.block; + } + this.lastMark = mark; + if (blockHeight != null) { if (this.tipIsSet) { - if (!this.blockInViewport(mark.blockHeight)) { - this.scrollToBlock(mark.blockHeight); + let scrollToHeight = blockHeight; + if (blockHeight < 0) { + scrollToHeight = this.chainTip - blockHeight; } - } else { - this.pendingMark = mark.blockHeight; + if (newMark && !this.blockInViewport(scrollToHeight)) { + this.scrollToBlock(scrollToHeight); + } + } + if (!this.tipIsSet || (blockHeight < 0 && !this.mempoolOffset)) { + this.pendingMark = blockHeight; } } }); @@ -117,6 +140,24 @@ export class StartComponent implements OnInit, OnDestroy { }); } + onMempoolOffsetChange(offset): void { + const delta = offset - this.mempoolOffset; + this.addConvertedScrollOffset(delta); + this.mempoolOffset = offset; + this.applyPendingMarkArrow(); + } + + applyPendingMarkArrow(): void { + if (this.pendingMark != null) { + if (this.pendingMark < 0) { + this.scrollToBlock(this.chainTip - this.pendingMark); + } else { + this.scrollToBlock(this.pendingMark); + } + this.pendingMark = null; + } + } + @HostListener('window:resize', ['$event']) onResize(): void { this.isMobile = window.innerWidth <= 767.98; @@ -350,7 +391,7 @@ export class StartComponent implements OnInit, OnDestroy { resetScroll(): void { this.scrollToBlock(this.chainTip); - this.blockchainContainer.nativeElement.scrollLeft = 0; + this.setScrollLeft(0); } getPageIndexOf(height: number): number { @@ -368,9 +409,17 @@ export class StartComponent implements OnInit, OnDestroy { getConvertedScrollOffset(): number { if (this.timeLtr) { - return -this.blockchainContainer?.nativeElement?.scrollLeft || 0; + return -(this.blockchainContainer?.nativeElement?.scrollLeft || 0) - this.mempoolOffset; } else { - return this.blockchainContainer?.nativeElement?.scrollLeft || 0; + return (this.blockchainContainer?.nativeElement?.scrollLeft || 0) - this.mempoolOffset; + } + } + + setScrollLeft(offset: number): void { + if (this.timeLtr) { + this.blockchainContainer.nativeElement.scrollLeft = offset - this.mempoolOffset; + } else { + this.blockchainContainer.nativeElement.scrollLeft = offset + this.mempoolOffset; } } @@ -388,7 +437,7 @@ export class StartComponent implements OnInit, OnDestroy { ngOnDestroy() { if (this.blockchainContainer?.nativeElement) { // clean up scroll position to prevent caching wrong scroll in Firefox - this.blockchainContainer.nativeElement.scrollLeft = 0; + this.setScrollLeft(0); } this.timeLtrSubscription.unsubscribe(); this.chainTipSubscription.unsubscribe(); diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index e7fbaa913..bbf679dcf 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -241,6 +241,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.mempoolPosition = txPosition.position; if (this.tx && !this.tx.status.confirmed) { this.stateService.markBlock$.next({ + txid: txPosition.txid, mempoolPosition: this.mempoolPosition }); this.txInBlockIndex = this.mempoolPosition.block; @@ -360,6 +361,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } else { if (tx.cpfpChecked) { this.stateService.markBlock$.next({ + txid: tx.txid, txFeePerVSize: tx.effectiveFeePerVsize, mempoolPosition: this.mempoolPosition, }); diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index fb3b37e05..738893bbc 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -8,8 +8,9 @@ import { isPlatformBrowser } from '@angular/common'; import { map, scan, shareReplay, tap } from 'rxjs/operators'; import { StorageService } from './storage.service'; -interface MarkBlockState { +export interface MarkBlockState { blockHeight?: number; + txid?: string; mempoolBlockIndex?: number; txFeePerVSize?: number; mempoolPosition?: MempoolPosition;