diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 6346c837c..98ad9de95 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -24,7 +24,7 @@ export class AppComponent implements OnInit { txId: ['', Validators.pattern('^[a-fA-F0-9]{64}$')], }); - this.memPoolService.isOffline + this.memPoolService.isOffline$ .subscribe((state) => { this.isOffline = state; }); @@ -42,7 +42,7 @@ export class AppComponent implements OnInit { } else { this.router.navigate(['/tx/', txId]); } - this.memPoolService.txIdSearch.next(txId); + this.memPoolService.txIdSearch$.next(txId); this.searchForm.setValue({ txId: '', }); diff --git a/frontend/src/app/blockchain-blocks/block-modal/block-modal.component.ts b/frontend/src/app/blockchain-blocks/block-modal/block-modal.component.ts index fe2bf5da6..b38874f17 100644 --- a/frontend/src/app/blockchain-blocks/block-modal/block-modal.component.ts +++ b/frontend/src/app/blockchain-blocks/block-modal/block-modal.component.ts @@ -56,7 +56,7 @@ export class BlockModalComponent implements OnInit { ] }; - this.memPoolService.conversions + this.memPoolService.conversions$ .subscribe((conversions) => { this.conversions = conversions; }); diff --git a/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts index 61b34ebdc..aef5b4c7e 100644 --- a/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts @@ -1,21 +1,33 @@ -import { Component, Input } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { IBlock } from '../blockchain/interfaces'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { BlockModalComponent } from './block-modal/block-modal.component'; +import { MemPoolService } from '../services/mem-pool.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-blockchain-blocks', templateUrl: './blockchain-blocks.component.html', styleUrls: ['./blockchain-blocks.component.scss'] }) -export class BlockchainBlocksComponent { - - @Input() blocks: IBlock[]; +export class BlockchainBlocksComponent implements OnInit, OnDestroy { + blocks: IBlock[] = []; + blocksSubscription: Subscription; constructor( private modalService: NgbModal, + private memPoolService: MemPoolService, ) { } + ngOnInit() { + this.blocksSubscription = this.memPoolService.blocks$ + .subscribe((block) => this.blocks.unshift(block)); + } + + ngOnDestroy() { + this.blocksSubscription.unsubscribe(); + } + getTimeSinceMined(block: IBlock): string { const minutes = ((new Date().getTime()) - (new Date(block.time * 1000).getTime())) / 1000 / 60; if (minutes >= 120) { diff --git a/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.scss b/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.scss index 489fe8628..dc5ce18c4 100644 --- a/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.scss +++ b/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.scss @@ -9,12 +9,6 @@ font-weight: bold; } -.blocks-container { - position: absolute; - top: 0px; - left: 40px; -} - .projected-blocks-container { position: absolute; top: 0px; diff --git a/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.ts b/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.ts index ff6f3bfac..0bfe5fc31 100644 --- a/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.ts +++ b/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.ts @@ -1,21 +1,33 @@ -import { Component, Input } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { IProjectedBlock, IBlock } from '../blockchain/interfaces'; import { ProjectedBlockModalComponent } from './projected-block-modal/projected-block-modal.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { MemPoolService } from '../services/mem-pool.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-blockchain-projected-blocks', templateUrl: './blockchain-projected-blocks.component.html', styleUrls: ['./blockchain-projected-blocks.component.scss'] }) -export class BlockchainProjectedBlocksComponent { - - @Input() projectedBlocks: IProjectedBlock[]; +export class BlockchainProjectedBlocksComponent implements OnInit, OnDestroy { + projectedBlocks: IProjectedBlock[]; + subscription: Subscription; constructor( private modalService: NgbModal, + private memPoolService: MemPoolService, ) { } + ngOnInit() { + this.subscription = this.memPoolService.projectedBlocks$ + .subscribe((projectedblocks) => this.projectedBlocks = projectedblocks); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + trackByProjectedFn(index: number) { return index; } diff --git a/frontend/src/app/blockchain-projected-blocks/projected-block-modal/projected-block-modal.component.ts b/frontend/src/app/blockchain-projected-blocks/projected-block-modal/projected-block-modal.component.ts index d66340337..3bf267b77 100644 --- a/frontend/src/app/blockchain-projected-blocks/projected-block-modal/projected-block-modal.component.ts +++ b/frontend/src/app/blockchain-projected-blocks/projected-block-modal/projected-block-modal.component.ts @@ -57,7 +57,7 @@ export class ProjectedBlockModalComponent implements OnInit { ] }; - this.memPoolService.conversions + this.memPoolService.conversions$ .subscribe((conversions) => { this.conversions = conversions; }); diff --git a/frontend/src/app/blockchain/blockchain.component.html b/frontend/src/app/blockchain/blockchain.component.html index f18cb433c..7f73b2a3e 100644 --- a/frontend/src/app/blockchain/blockchain.component.html +++ b/frontend/src/app/blockchain/blockchain.component.html @@ -1,9 +1,9 @@ -
{{ txIdShort }} | |||
Fees: | -{{ tx?.fee }} BTC | +Fee: | +{{ tx?.fee * 100000000 | number }} sats ({{ conversions.USD * tx?.fee | currency:'USD':'symbol':'1.2-2' }}) |
Fee per vByte: | diff --git a/frontend/src/app/tx-bubble/tx-bubble.component.scss b/frontend/src/app/tx-bubble/tx-bubble.component.scss index 4c759c4c1..105c30991 100644 --- a/frontend/src/app/tx-bubble/tx-bubble.component.scss +++ b/frontend/src/app/tx-bubble/tx-bubble.component.scss @@ -63,3 +63,7 @@ .txBubble .arrow-top-left.txBubbleText::after { left: 20%; } + +.green-color { + color: #3bcc49; +} diff --git a/frontend/src/app/tx-bubble/tx-bubble.component.ts b/frontend/src/app/tx-bubble/tx-bubble.component.ts index 6e5b86c42..92cf2f0a9 100644 --- a/frontend/src/app/tx-bubble/tx-bubble.component.ts +++ b/frontend/src/app/tx-bubble/tx-bubble.component.ts @@ -1,26 +1,144 @@ -import { Component, OnInit, Input, OnChanges } from '@angular/core'; -import { ITransaction } from '../blockchain/interfaces'; +import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; +import { ITransaction, IProjectedBlock } from '../blockchain/interfaces'; +import { Subscription } from 'rxjs'; +import { ITxTracking, MemPoolService } from '../services/mem-pool.service'; @Component({ selector: 'app-tx-bubble', templateUrl: './tx-bubble.component.html', styleUrls: ['./tx-bubble.component.scss'] }) -export class TxBubbleComponent implements OnChanges { - @Input() tx: ITransaction | null = null; - @Input() txTrackingBlockHeight = 0; - @Input() latestBlockHeight = 0; - @Input() arrowPosition: 'top' | 'right' | 'bottom' | 'top-right' | 'top-left' = 'top'; +export class TxBubbleComponent implements OnInit, OnDestroy { + tx: ITransaction | null = null; + txTrackingBlockHeight = 0; + latestBlockHeight = 0; + arrowPosition: 'top' | 'right' | 'bottom' | 'top-right' | 'top-left' = 'top'; + + txTrackingSubscription: Subscription; + projectedBlocksSubscription: Subscription; + blocksSubscription: Subscription; + + projectedBlocks: IProjectedBlock[] = []; txIdShort = ''; confirmations = 0; + conversions: any; - constructor() { } + txBubbleStyle: any = { + 'position': 'absolute', + 'top': '425px', + 'visibility': 'hidden', + }; - ngOnChanges() { - if (this.tx) { - this.txIdShort = this.tx.txid.substring(0, 6) + '...' + this.tx.txid.substring(this.tx.txid.length - 6); + txTrackingLoading = false; + txTrackingEnabled = false; + txTrackingTx: ITransaction | null = null; + txShowTxNotFound = false; + + txBubbleArrowPosition = 'top'; + + @HostListener('window:resize', ['$event']) + onResize(event: Event) { + this.moveTxBubbleToPosition(); + } + + constructor( + private memPoolService: MemPoolService, + ) { } + + ngOnInit() { + this.txTrackingSubscription = this.memPoolService.txTracking$ + .subscribe((response: ITxTracking) => { + this.txTrackingBlockHeight = response.blockHeight; + this.txTrackingEnabled = response.enabled; + if (response.tx) { + this.tx = response.tx; + } + if (this.txTrackingEnabled) { + setTimeout(() => this.moveTxBubbleToPosition()); + } + if (this.txShowTxNotFound) { + setTimeout(() => { this.txShowTxNotFound = false; }, 2000); + } + if (this.tx) { + this.txIdShort = this.tx.txid.substring(0, 6) + '...' + this.tx.txid.substring(this.tx.txid.length - 6); + } + if (this.latestBlockHeight) { + this.confirmations = (this.latestBlockHeight - this.txTrackingBlockHeight) + 1; + } + }); + + this.projectedBlocksSubscription = this.memPoolService.projectedBlocks$ + .subscribe((projectedblocks) => this.projectedBlocks = projectedblocks); + + this.blocksSubscription = this.memPoolService.blocks$ + .subscribe((block) => { + this.latestBlockHeight = block.height; + if (this.txTrackingBlockHeight) { + this.confirmations = (this.latestBlockHeight - this.txTrackingBlockHeight) + 1; + } + }); + + this.memPoolService.conversions$ + .subscribe((conversions) => { + this.conversions = conversions; + }); + } + + ngOnDestroy() { + this.projectedBlocksSubscription.unsubscribe(); + this.txTrackingSubscription.unsubscribe(); + this.blocksSubscription.unsubscribe(); + } + + moveTxBubbleToPosition() { + let element: HTMLElement | null = null; + if (this.txTrackingBlockHeight === 0) { + const index = this.projectedBlocks.findIndex((pB) => pB.hasMytx); + if (index > -1) { + element = document.getElementById('projected-block-' + index); + } else { + return; + } + } else { + element = document.getElementById('bitcoin-block-' + this.txTrackingBlockHeight); + } + + this.txBubbleStyle['visibility'] = 'visible'; + this.txBubbleStyle['position'] = 'absolute'; + + if (!element) { + if (window.innerWidth <= 768) { + this.txBubbleArrowPosition = 'bottom'; + this.txBubbleStyle['left'] = window.innerWidth / 2 - 50 + 'px'; + this.txBubbleStyle['bottom'] = '270px'; + this.txBubbleStyle['top'] = 'inherit'; + this.txBubbleStyle['position'] = 'fixed'; + } else { + this.txBubbleStyle['left'] = window.innerWidth - 220 + 'px'; + this.txBubbleArrowPosition = 'right'; + this.txBubbleStyle['top'] = '425px'; + } + } else { + this.txBubbleArrowPosition = 'top'; + const domRect: DOMRect | ClientRect = element.getBoundingClientRect(); + this.txBubbleStyle['left'] = domRect.left - 50 + 'px'; + this.txBubbleStyle['top'] = domRect.top + 140 + window.scrollY + 'px'; + + if (domRect.left + 100 > window.innerWidth) { + this.txBubbleStyle['left'] = window.innerWidth - 220 + 'px'; + this.txBubbleArrowPosition = 'right'; + } else if (domRect.left + 220 > window.innerWidth) { + this.txBubbleStyle['left'] = window.innerWidth - 240 + 'px'; + this.txBubbleArrowPosition = 'top-right'; + } else { + this.txBubbleStyle['left'] = domRect.left + 15 + 'px'; + } + + if (domRect.left < 86) { + this.txBubbleArrowPosition = 'top-left'; + this.txBubbleStyle['left'] = 125 + 'px'; + } } - this.confirmations = (this.latestBlockHeight - this.txTrackingBlockHeight) + 1; } }