Merge pull request #4407 from mempool/mononaut/8chain
Standalone multi-block view page
This commit is contained in:
		
						commit
						671540af78
					
				| @ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; | |||||||
| import { Routes, RouterModule } from '@angular/router'; | import { Routes, RouterModule } from '@angular/router'; | ||||||
| import { AppPreloadingStrategy } from './app.preloading-strategy' | import { AppPreloadingStrategy } from './app.preloading-strategy' | ||||||
| import { BlockViewComponent } from './components/block-view/block-view.component'; | import { BlockViewComponent } from './components/block-view/block-view.component'; | ||||||
|  | import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component'; | ||||||
| import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component'; | import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component'; | ||||||
| import { ClockComponent } from './components/clock/clock.component'; | import { ClockComponent } from './components/clock/clock.component'; | ||||||
| import { StatusViewComponent } from './components/status-view/status-view.component'; | import { StatusViewComponent } from './components/status-view/status-view.component'; | ||||||
| @ -124,6 +125,10 @@ let routes: Routes = [ | |||||||
|     path: 'view/mempool-block/:index', |     path: 'view/mempool-block/:index', | ||||||
|     component: MempoolBlockViewComponent, |     component: MempoolBlockViewComponent, | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     path: 'view/blocks', | ||||||
|  |     component: EightBlocksComponent, | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     path: 'status', |     path: 'status', | ||||||
|     data: { networks: ['bitcoin', 'liquid'] }, |     data: { networks: ['bitcoin', 'liquid'] }, | ||||||
|  | |||||||
| @ -20,6 +20,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|   @Input() blockLimit: number; |   @Input() blockLimit: number; | ||||||
|   @Input() orientation = 'left'; |   @Input() orientation = 'left'; | ||||||
|   @Input() flip = true; |   @Input() flip = true; | ||||||
|  |   @Input() animationDuration: number = 1000; | ||||||
|  |   @Input() animationOffset: number | null = null; | ||||||
|   @Input() disableSpinner = false; |   @Input() disableSpinner = false; | ||||||
|   @Input() mirrorTxid: string | void; |   @Input() mirrorTxid: string | void; | ||||||
|   @Input() unavailable: boolean = false; |   @Input() unavailable: boolean = false; | ||||||
| @ -141,9 +143,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void { |   replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void { | ||||||
|     if (this.scene) { |     if (this.scene) { | ||||||
|       this.scene.replace(transactions || [], direction, sort); |       this.scene.replace(transactions || [], direction, sort, startTime); | ||||||
|       this.start(); |       this.start(); | ||||||
|       this.updateSearchHighlight(); |       this.updateSearchHighlight(); | ||||||
|     } |     } | ||||||
| @ -226,7 +228,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|     } else { |     } else { | ||||||
|       this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, |       this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, | ||||||
|         blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, |         blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, | ||||||
|         highlighting: this.auditHighlighting }); |         highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset }); | ||||||
|       this.start(); |       this.start(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -9,6 +9,9 @@ export default class BlockScene { | |||||||
|   txs: { [key: string]: TxView }; |   txs: { [key: string]: TxView }; | ||||||
|   orientation: string; |   orientation: string; | ||||||
|   flip: boolean; |   flip: boolean; | ||||||
|  |   animationDuration: number = 1000; | ||||||
|  |   configAnimationOffset: number | null; | ||||||
|  |   animationOffset: number; | ||||||
|   highlightingEnabled: boolean; |   highlightingEnabled: boolean; | ||||||
|   width: number; |   width: number; | ||||||
|   height: number; |   height: number; | ||||||
| @ -23,11 +26,11 @@ export default class BlockScene { | |||||||
|   animateUntil = 0; |   animateUntil = 0; | ||||||
|   dirty: boolean; |   dirty: boolean; | ||||||
| 
 | 
 | ||||||
|   constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }: |   constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }: | ||||||
|       { width: number, height: number, resolution: number, blockLimit: number, |       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, | ||||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } |         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } | ||||||
|   ) { |   ) { | ||||||
|     this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }); |     this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { |   resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { | ||||||
| @ -36,6 +39,7 @@ export default class BlockScene { | |||||||
|     this.gridSize = this.width / this.gridWidth; |     this.gridSize = this.width / this.gridWidth; | ||||||
|     this.unitPadding =  Math.max(1, Math.floor(this.gridSize / 5)); |     this.unitPadding =  Math.max(1, Math.floor(this.gridSize / 5)); | ||||||
|     this.unitWidth = this.gridSize - (this.unitPadding * 2); |     this.unitWidth = this.gridSize - (this.unitPadding * 2); | ||||||
|  |     this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset; | ||||||
| 
 | 
 | ||||||
|     this.dirty = true; |     this.dirty = true; | ||||||
|     if (this.initialised && this.scene) { |     if (this.initialised && this.scene) { | ||||||
| @ -90,8 +94,8 @@ export default class BlockScene { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Animate new block entering scene
 |   // Animate new block entering scene
 | ||||||
|   enter(txs: TransactionStripped[], direction) { |   enter(txs: TransactionStripped[], direction, startTime?: number) { | ||||||
|     this.replace(txs, direction); |     this.replace(txs, direction, false, startTime); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Animate block leaving scene
 |   // Animate block leaving scene
 | ||||||
| @ -108,8 +112,7 @@ export default class BlockScene { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Reset layout and replace with new set of transactions
 |   // Reset layout and replace with new set of transactions
 | ||||||
|   replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true): void { |   replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true, startTime: number = performance.now()): void { | ||||||
|     const startTime = performance.now(); |  | ||||||
|     const nextIds = {}; |     const nextIds = {}; | ||||||
|     const remove = []; |     const remove = []; | ||||||
|     txs.forEach(tx => { |     txs.forEach(tx => { | ||||||
| @ -133,7 +136,7 @@ export default class BlockScene { | |||||||
|       removed.forEach(tx => { |       removed.forEach(tx => { | ||||||
|         tx.destroy(); |         tx.destroy(); | ||||||
|       }); |       }); | ||||||
|     }, 1000); |     }, (startTime - performance.now()) + this.animationDuration + 1000); | ||||||
| 
 | 
 | ||||||
|     this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); |     this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); | ||||||
| 
 | 
 | ||||||
| @ -147,7 +150,7 @@ export default class BlockScene { | |||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.updateAll(startTime, 200, direction); |     this.updateAll(startTime, 50, direction); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { |   update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { | ||||||
| @ -214,10 +217,13 @@ export default class BlockScene { | |||||||
|     this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value)); |     this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }: |   private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }: | ||||||
|       { width: number, height: number, resolution: number, blockLimit: number, |       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, | ||||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } |         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } | ||||||
|   ): void { |   ): void { | ||||||
|  |     this.animationDuration = animationDuration || 1000; | ||||||
|  |     this.configAnimationOffset = animationOffset; | ||||||
|  |     this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset; | ||||||
|     this.orientation = orientation; |     this.orientation = orientation; | ||||||
|     this.flip = flip; |     this.flip = flip; | ||||||
|     this.vertexArray = vertexArray; |     this.vertexArray = vertexArray; | ||||||
| @ -261,8 +267,8 @@ export default class BlockScene { | |||||||
|       this.applyTxUpdate(tx, { |       this.applyTxUpdate(tx, { | ||||||
|         display: { |         display: { | ||||||
|           position: { |           position: { | ||||||
|             x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4, |             x: tx.screenPosition.x + (direction === 'right' ? -this.width - this.animationOffset : (direction === 'left' ? this.width + this.animationOffset : 0)), | ||||||
|             y: tx.screenPosition.y + (direction === 'up' ? -this.height : (direction === 'down' ? this.height : 0)) * 1.4, |             y: tx.screenPosition.y + (direction === 'up' ? -this.height - this.animationOffset : (direction === 'down' ? this.height + this.animationOffset : 0)), | ||||||
|             s: tx.screenPosition.s |             s: tx.screenPosition.s | ||||||
|           }, |           }, | ||||||
|           color: txColor, |           color: txColor, | ||||||
| @ -275,7 +281,7 @@ export default class BlockScene { | |||||||
|           position: tx.screenPosition, |           position: tx.screenPosition, | ||||||
|           color: txColor |           color: txColor | ||||||
|         }, |         }, | ||||||
|         duration: animate ? 1000 : 1, |         duration: animate ? this.animationDuration : 1, | ||||||
|         start: startTime, |         start: startTime, | ||||||
|         delay: animate ? delay : 0, |         delay: animate ? delay : 0, | ||||||
|       }); |       }); | ||||||
| @ -284,8 +290,8 @@ export default class BlockScene { | |||||||
|         display: { |         display: { | ||||||
|           position: tx.screenPosition |           position: tx.screenPosition | ||||||
|         }, |         }, | ||||||
|         duration: animate ? 1000 : 0, |         duration: animate ? this.animationDuration : 0, | ||||||
|         minDuration: animate ? 500 : 0, |         minDuration: animate ? (this.animationDuration / 2) : 0, | ||||||
|         start: startTime, |         start: startTime, | ||||||
|         delay: animate ? delay : 0, |         delay: animate ? delay : 0, | ||||||
|         adjust: animate |         adjust: animate | ||||||
| @ -322,11 +328,11 @@ export default class BlockScene { | |||||||
|       this.applyTxUpdate(tx, { |       this.applyTxUpdate(tx, { | ||||||
|         display: { |         display: { | ||||||
|           position: { |           position: { | ||||||
|             x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4, |             x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)), | ||||||
|             y: tx.screenPosition.y + (direction === 'up' ? this.height : (direction === 'down' ? -this.height : 0)) * 1.4, |             y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)), | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         duration: 1000, |         duration: this.animationDuration, | ||||||
|         start: startTime, |         start: startTime, | ||||||
|         delay: 50 |         delay: 50 | ||||||
|       }); |       }); | ||||||
|  | |||||||
| @ -0,0 +1,24 @@ | |||||||
|  | <div class="blocks" [class.wrap]="wrapBlocks"> | ||||||
|  |   <ng-container *ngFor="let i of blockIndices"> | ||||||
|  |     <div class="block-wrapper" [style]="wrapperStyle"> | ||||||
|  |       <div class="block-container" [style]="containerStyle"> | ||||||
|  |         <app-block-overview-graph | ||||||
|  |           #blockGraph | ||||||
|  |           [isLoading]="false" | ||||||
|  |           [resolution]="resolution" | ||||||
|  |           [blockLimit]="stateService.blockVSize" | ||||||
|  |           [orientation]="'top'" | ||||||
|  |           [flip]="false" | ||||||
|  |           [animationDuration]="animationDuration" | ||||||
|  |           [animationOffset]="animationOffset" | ||||||
|  |           [disableSpinner]="true" | ||||||
|  |           (txClickEvent)="onTxClick($event)" | ||||||
|  |         ></app-block-overview-graph> | ||||||
|  |         <div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange> | ||||||
|  |           <h1 class="height">{{ blockInfo[i].height }}</h1> | ||||||
|  |           <h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </ng-container> | ||||||
|  | </div> | ||||||
| @ -0,0 +1,69 @@ | |||||||
|  | .blocks { | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  |   min-width: 100vw; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   flex-wrap: nowrap; | ||||||
|  |   justify-content: flex-start; | ||||||
|  |   align-items: flex-start; | ||||||
|  |   align-content: flex-start; | ||||||
|  | 
 | ||||||
|  |   &.wrap { | ||||||
|  |     flex-wrap: wrap; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .block-wrapper { | ||||||
|  |     flex-grow: 0; | ||||||
|  |     flex-shrink: 0; | ||||||
|  |     position: relative; | ||||||
|  |     --block-width: 1080px; | ||||||
|  | 
 | ||||||
|  |     .info { | ||||||
|  |       position: absolute; | ||||||
|  |       left: 8%; | ||||||
|  |       top: 8%; | ||||||
|  |       right: 8%; | ||||||
|  |       bottom: 8%; | ||||||
|  |       height: 84%; | ||||||
|  |       width: 84%; | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       font-weight: 700; | ||||||
|  |       font-size: calc(var(--block-width) * 0.03); | ||||||
|  |       text-shadow: 0 0 calc(var(--block-width) * 0.05) black; | ||||||
|  | 
 | ||||||
|  |       h1 { | ||||||
|  |         font-size: 6em; | ||||||
|  |         line-height: 1; | ||||||
|  |         margin-bottom: calc(var(--block-width) * 0.03); | ||||||
|  |       } | ||||||
|  |       h2 { | ||||||
|  |         font-size: 1.8em; | ||||||
|  |         line-height: 1; | ||||||
|  |         margin-bottom: calc(var(--block-width) * 0.03); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       .hash { | ||||||
|  |         font-family: monospace; | ||||||
|  |         word-wrap: break-word; | ||||||
|  |         font-size: 1.4em; | ||||||
|  |         line-height: 1; | ||||||
|  |         margin-bottom: calc(var(--block-width) * 0.03); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       .mined-by { | ||||||
|  |         position: absolute; | ||||||
|  |         bottom: 0; | ||||||
|  |         margin: auto; | ||||||
|  |         text-align: center; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .block-container { | ||||||
|  |     overflow: hidden; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,253 @@ | |||||||
|  | import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core'; | ||||||
|  | import { ActivatedRoute, Router } from '@angular/router'; | ||||||
|  | import { catchError, startWith } from 'rxjs/operators'; | ||||||
|  | import { Subject, Subscription, of } from 'rxjs'; | ||||||
|  | import { StateService } from '../../services/state.service'; | ||||||
|  | import { WebsocketService } from '../../services/websocket.service'; | ||||||
|  | import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | ||||||
|  | import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; | ||||||
|  | import { ApiService } from '../../services/api.service'; | ||||||
|  | import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component'; | ||||||
|  | import { detectWebGL } from '../../shared/graphs.utils'; | ||||||
|  | import { animate, style, transition, trigger } from '@angular/animations'; | ||||||
|  | import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe'; | ||||||
|  | 
 | ||||||
|  | function bestFitResolution(min, max, n): number { | ||||||
|  |   const target = (min + max) / 2; | ||||||
|  |   let bestScore = Infinity; | ||||||
|  |   let best = null; | ||||||
|  |   for (let i = min; i <= max; i++) { | ||||||
|  |     const remainder = (n % i); | ||||||
|  |     if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) { | ||||||
|  |       bestScore = remainder; | ||||||
|  |       best = i; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return best; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface BlockInfo extends BlockExtended { | ||||||
|  |   timeString: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-eight-blocks', | ||||||
|  |   templateUrl: './eight-blocks.component.html', | ||||||
|  |   styleUrls: ['./eight-blocks.component.scss'], | ||||||
|  |   animations: [ | ||||||
|  |     trigger('infoChange', [ | ||||||
|  |       transition(':enter', [ | ||||||
|  |         style({ opacity: 0 }), | ||||||
|  |         animate('1000ms', style({ opacity: 1 })), | ||||||
|  |       ]), | ||||||
|  |       transition(':leave', [ | ||||||
|  |         animate('1000ms 500ms', style({ opacity: 0 })) | ||||||
|  |       ]) | ||||||
|  |     ]), | ||||||
|  |   ], | ||||||
|  | }) | ||||||
|  | export class EightBlocksComponent implements OnInit, OnDestroy { | ||||||
|  |   network = ''; | ||||||
|  |   latestBlocks: BlockExtended[] = []; | ||||||
|  |   isLoadingTransactions = true; | ||||||
|  |   strippedTransactions: { [height: number]: TransactionStripped[] } = {}; | ||||||
|  |   webGlEnabled = true; | ||||||
|  |   hoverTx: string | null = null; | ||||||
|  | 
 | ||||||
|  |   blocksSubscription: Subscription; | ||||||
|  |   cacheBlocksSubscription: Subscription; | ||||||
|  |   networkChangedSubscription: Subscription; | ||||||
|  |   queryParamsSubscription: Subscription; | ||||||
|  |   graphChangeSubscription: Subscription; | ||||||
|  | 
 | ||||||
|  |   numBlocks: number = 8; | ||||||
|  |   blockIndices: number[] = [...Array(8).keys()]; | ||||||
|  |   autofit: boolean = false; | ||||||
|  |   padding: number = 0; | ||||||
|  |   wrapBlocks: boolean = false; | ||||||
|  |   blockWidth: number = 1080; | ||||||
|  |   animationDuration: number = 2000; | ||||||
|  |   animationOffset: number = 0; | ||||||
|  |   stagger: number = 0; | ||||||
|  |   testing: boolean = true; | ||||||
|  |   testHeight: number = 800000; | ||||||
|  |   testShiftTimeout: number; | ||||||
|  | 
 | ||||||
|  |   showInfo: boolean = true; | ||||||
|  |   blockInfo: BlockInfo[] = []; | ||||||
|  | 
 | ||||||
|  |   wrapperStyle = { | ||||||
|  |     '--block-width': '1080px', | ||||||
|  |     width: '1080px', | ||||||
|  |     maxWidth: '1080px', | ||||||
|  |     padding: '', | ||||||
|  |   }; | ||||||
|  |   containerStyle = {}; | ||||||
|  |   resolution: number = 86; | ||||||
|  | 
 | ||||||
|  |   @ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     private route: ActivatedRoute, | ||||||
|  |     private router: Router, | ||||||
|  |     public stateService: StateService, | ||||||
|  |     private websocketService: WebsocketService, | ||||||
|  |     private apiService: ApiService, | ||||||
|  |     private bytesPipe: BytesPipe, | ||||||
|  |   ) { | ||||||
|  |     this.webGlEnabled = detectWebGL(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngOnInit(): void { | ||||||
|  |     this.websocketService.want(['blocks']); | ||||||
|  |     this.network = this.stateService.network; | ||||||
|  | 
 | ||||||
|  |     this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { | ||||||
|  |       this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8; | ||||||
|  |       this.blockIndices = [...Array(this.numBlocks).keys()]; | ||||||
|  |       this.autofit = params.autofit !== 'false'; | ||||||
|  |       this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 10; | ||||||
|  |       this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540; | ||||||
|  |       this.wrapBlocks = params.wrap !== 'false'; | ||||||
|  |       this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0; | ||||||
|  |       this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000; | ||||||
|  |       this.animationOffset = this.padding * 2; | ||||||
|  | 
 | ||||||
|  |       if (this.autofit) { | ||||||
|  |         this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2); | ||||||
|  |       } else { | ||||||
|  |         this.resolution = 86; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this.wrapperStyle = { | ||||||
|  |         '--block-width': this.blockWidth + 'px', | ||||||
|  |         width: this.blockWidth + 'px', | ||||||
|  |         maxWidth: this.blockWidth + 'px', | ||||||
|  |         padding: (this.padding || 0) +'px 0px', | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       if (params.test === 'true') { | ||||||
|  |         if (this.blocksSubscription) { | ||||||
|  |           this.blocksSubscription.unsubscribe(); | ||||||
|  |         } | ||||||
|  |         this.blocksSubscription = (new Subject<BlockExtended[]>()).subscribe((blocks) => { | ||||||
|  |           this.handleNewBlock(blocks.slice(0, this.numBlocks)); | ||||||
|  |         }); | ||||||
|  |         this.shiftTestBlocks(); | ||||||
|  |       } else if (!this.blocksSubscription) { | ||||||
|  |         this.blocksSubscription = this.stateService.blocks$ | ||||||
|  |           .subscribe((blocks) => { | ||||||
|  |             this.handleNewBlock(blocks.slice(0, this.numBlocks)); | ||||||
|  |           }); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     this.setupBlockGraphs(); | ||||||
|  | 
 | ||||||
|  |     this.networkChangedSubscription = this.stateService.networkChanged$ | ||||||
|  |       .subscribe((network) => this.network = network); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngAfterViewInit(): void { | ||||||
|  |     this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => { | ||||||
|  |       this.setupBlockGraphs(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngOnDestroy(): void { | ||||||
|  |     this.stateService.markBlock$.next({}); | ||||||
|  |     if (this.blocksSubscription) { | ||||||
|  |       this.blocksSubscription?.unsubscribe(); | ||||||
|  |     } | ||||||
|  |     this.cacheBlocksSubscription?.unsubscribe(); | ||||||
|  |     this.networkChangedSubscription?.unsubscribe(); | ||||||
|  |     this.queryParamsSubscription?.unsubscribe(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   shiftTestBlocks(): void { | ||||||
|  |     const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => { | ||||||
|  |       sub.unsubscribe(); | ||||||
|  |       this.handleNewBlock(result.slice(0, this.numBlocks)); | ||||||
|  |       this.testHeight++; | ||||||
|  |       clearTimeout(this.testShiftTimeout); | ||||||
|  |       this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async handleNewBlock(blocks: BlockExtended[]): Promise<void> { | ||||||
|  |     const readyPromises: Promise<TransactionStripped[]>[] = []; | ||||||
|  |     const previousBlocks = this.latestBlocks; | ||||||
|  |     const newHeights = {}; | ||||||
|  |     this.latestBlocks = blocks; | ||||||
|  |     for (const block of blocks) { | ||||||
|  |       newHeights[block.height] = true; | ||||||
|  |       if (!this.strippedTransactions[block.height]) { | ||||||
|  |         readyPromises.push(new Promise((resolve) => { | ||||||
|  |           const subscription = this.apiService.getStrippedBlockTransactions$(block.id).pipe( | ||||||
|  |             catchError(() => { | ||||||
|  |               return of([]); | ||||||
|  |             }), | ||||||
|  |           ).subscribe((transactions) => { | ||||||
|  |             this.strippedTransactions[block.height] = transactions; | ||||||
|  |             subscription.unsubscribe(); | ||||||
|  |             resolve(transactions); | ||||||
|  |           }); | ||||||
|  |         })); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     await Promise.allSettled(readyPromises); | ||||||
|  |     this.updateBlockGraphs(blocks); | ||||||
|  | 
 | ||||||
|  |     // free up old transactions
 | ||||||
|  |     previousBlocks.forEach(block => { | ||||||
|  |       if (!newHeights[block.height]) { | ||||||
|  |         delete this.strippedTransactions[block.height]; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   updateBlockGraphs(blocks): void { | ||||||
|  |     const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0); | ||||||
|  |     if (this.blockGraphs) { | ||||||
|  |       this.blockGraphs.forEach((graph, index) => { | ||||||
|  |         graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index)); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     this.showInfo = false; | ||||||
|  |     setTimeout(() => { | ||||||
|  |       this.blockInfo = blocks.map(block => { | ||||||
|  |         return { | ||||||
|  |           ...block, | ||||||
|  |           timeString: (new Date(block.timestamp * 1000)).toLocaleTimeString(), | ||||||
|  |         }; | ||||||
|  |       }); | ||||||
|  |       this.showInfo = true; | ||||||
|  |     }, 1600);  // Should match the animation time.
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   setupBlockGraphs(): void { | ||||||
|  |     if (this.blockGraphs) { | ||||||
|  |       this.blockGraphs.forEach((graph, index) => { | ||||||
|  |         graph.destroy(); | ||||||
|  |         graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   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'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   onTxHover(txid: string): void { | ||||||
|  |     if (txid && txid.length) { | ||||||
|  |       this.hoverTx = txid; | ||||||
|  |     } else { | ||||||
|  |       this.hoverTx = null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -237,7 +237,7 @@ | |||||||
|       <div class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig"> |       <div class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig"> | ||||||
|         <div class="progress"> |         <div class="progress"> | ||||||
|           <div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }"> </div> |           <div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }"> </div> | ||||||
|           <div class="progress-text">‎<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div> |           <div class="progress-text">‎<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : false : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ export class BytesPipe implements PipeTransform { | |||||||
|         'TB': {max: Number.MAX_SAFE_INTEGER, prev: 'GB'} |         'TB': {max: Number.MAX_SAFE_INTEGER, prev: 'GB'} | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     transform(input: any, decimal: number = 0, from: ByteUnit = 'B', to?: ByteUnit, sigfigs?: number): any { |     transform(input: any, decimal: number = 0, from: ByteUnit = 'B', to?: ByteUnit, plaintext = false, sigfigs?: number): any { | ||||||
| 
 | 
 | ||||||
|         if (!(isNumberFinite(input) && |         if (!(isNumberFinite(input) && | ||||||
|                 isNumberFinite(decimal) && |                 isNumberFinite(decimal) && | ||||||
| @ -42,7 +42,7 @@ export class BytesPipe implements PipeTransform { | |||||||
| 
 | 
 | ||||||
|             const result = numberFormat(BytesPipe.calculateResult(format, bytes)); |             const result = numberFormat(BytesPipe.calculateResult(format, bytes)); | ||||||
| 
 | 
 | ||||||
|             return BytesPipe.formatResult(result, to); |             return BytesPipe.formatResult(result, to, plaintext); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         for (const key in BytesPipe.formats) { |         for (const key in BytesPipe.formats) { | ||||||
| @ -51,14 +51,18 @@ export class BytesPipe implements PipeTransform { | |||||||
| 
 | 
 | ||||||
|                 const result = numberFormat(BytesPipe.calculateResult(format, bytes)); |                 const result = numberFormat(BytesPipe.calculateResult(format, bytes)); | ||||||
| 
 | 
 | ||||||
|                 return BytesPipe.formatResult(result, key); |                 return BytesPipe.formatResult(result, key, plaintext); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static formatResult(result: string, unit: string): string { |     static formatResult(result: string, unit: string, plaintext): string { | ||||||
|  |         if (plaintext) { | ||||||
|  |             return `${result} ${unit}`; | ||||||
|  |         } else { | ||||||
|             return `${result} <span class="symbol">${unit}</span>`; |             return `${result} <span class="symbol">${unit}</span>`; | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     static calculateResult(format: { max: number, prev?: ByteUnit }, bytes: number) { |     static calculateResult(format: { max: number, prev?: ByteUnit }, bytes: number) { | ||||||
|         const prev = format.prev ? BytesPipe.formats[format.prev] : undefined; |         const prev = format.prev ? BytesPipe.formats[format.prev] : undefined; | ||||||
|  | |||||||
| @ -87,6 +87,7 @@ import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/ac | |||||||
| import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component'; | import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component'; | ||||||
| 
 | 
 | ||||||
| import { BlockViewComponent } from '../components/block-view/block-view.component'; | import { BlockViewComponent } from '../components/block-view/block-view.component'; | ||||||
|  | import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component'; | ||||||
| import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component'; | import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component'; | ||||||
| import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; | import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; | ||||||
| import { ClockchainComponent } from '../components/clockchain/clockchain.component'; | import { ClockchainComponent } from '../components/clockchain/clockchain.component'; | ||||||
| @ -126,6 +127,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | |||||||
|     ColoredPriceDirective, |     ColoredPriceDirective, | ||||||
|     BlockchainComponent, |     BlockchainComponent, | ||||||
|     BlockViewComponent, |     BlockViewComponent, | ||||||
|  |     EightBlocksComponent, | ||||||
|     MempoolBlockViewComponent, |     MempoolBlockViewComponent, | ||||||
|     MempoolBlocksComponent, |     MempoolBlocksComponent, | ||||||
|     BlockchainBlocksComponent, |     BlockchainBlocksComponent, | ||||||
| @ -179,6 +181,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | |||||||
|     CalculatorComponent, |     CalculatorComponent, | ||||||
|     BitcoinsatoshisPipe, |     BitcoinsatoshisPipe, | ||||||
|     BlockViewComponent, |     BlockViewComponent, | ||||||
|  |     EightBlocksComponent, | ||||||
|     MempoolBlockViewComponent, |     MempoolBlockViewComponent, | ||||||
|     MempoolBlockOverviewComponent, |     MempoolBlockOverviewComponent, | ||||||
|     ClockchainComponent, |     ClockchainComponent, | ||||||
| @ -202,6 +205,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | |||||||
|     FontAwesomeModule, |     FontAwesomeModule, | ||||||
|   ], |   ], | ||||||
|   providers: [ |   providers: [ | ||||||
|  |     BytesPipe, | ||||||
|     VbytesPipe, |     VbytesPipe, | ||||||
|     WuBytesPipe, |     WuBytesPipe, | ||||||
|     RelativeUrlPipe, |     RelativeUrlPipe, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user