diff --git a/frontend/cypress/e2e/liquid/liquid.spec.ts b/frontend/cypress/e2e/liquid/liquid.spec.ts index 8548059bb..b355af0d2 100644 --- a/frontend/cypress/e2e/liquid/liquid.spec.ts +++ b/frontend/cypress/e2e/liquid/liquid.spec.ts @@ -45,6 +45,7 @@ describe('Liquid', () => { it('loads a specific block page', () => { cy.visit(`${basePath}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); }); diff --git a/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts b/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts index a96b0700c..54e355ce8 100644 --- a/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts +++ b/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts @@ -46,7 +46,8 @@ describe('Liquid Testnet', () => { }); it('loads a specific block page', () => { - cy.visit(`${basePath}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`); + cy.visit(`${basePath}/block/fb4cbcbff3993ca4bf8caf657d55a23db5ed4ab1cfa33c489303c2e04e1c38e0`); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); }); diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index 5032144f8..edceeecf4 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -103,6 +103,7 @@ describe('Mainnet', () => { it('check op_return tx tooltip', () => { cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover'); cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter'); @@ -111,6 +112,7 @@ describe('Mainnet', () => { it('check op_return coinbase tooltip', () => { cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover'); cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter'); @@ -283,6 +285,7 @@ describe('Mainnet', () => { it('loads genesis block and keypress arrow right', () => { cy.viewport('macbook-16'); cy.visit('/block/0'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.waitForPageIdle(); @@ -295,6 +298,7 @@ describe('Mainnet', () => { it('loads genesis block and keypress arrow left', () => { cy.viewport('macbook-16'); cy.visit('/block/0'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.waitForPageIdle(); @@ -323,6 +327,7 @@ describe('Mainnet', () => { it('loads genesis block and click on the arrow left', () => { cy.viewport('macbook-16'); cy.visit('/block/0'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.waitForPageIdle(); cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); @@ -439,6 +444,7 @@ describe('Mainnet', () => { describe('blocks', () => { it('shows empty blocks properly', () => { cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.waitForPageIdle(); cy.get('h2').invoke('text').should('equal', '1 transaction'); @@ -446,6 +452,7 @@ describe('Mainnet', () => { it('expands and collapses the block details', () => { cy.visit('/block/0'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.waitForPageIdle(); cy.get('.btn.btn-outline-info').click().then(() => { @@ -458,6 +465,7 @@ describe('Mainnet', () => { }); it('shows blocks with no pagination', () => { cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.waitForPageIdle(); cy.get('.block-tx-title h2').invoke('text').should('equal', '19 transactions'); @@ -467,6 +475,7 @@ describe('Mainnet', () => { it('supports pagination on the block screen', () => { // 41 txs cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.get('.pagination-container a').invoke('text').then((text1) => { cy.get('.active + li').first().click().then(() => { @@ -482,6 +491,7 @@ describe('Mainnet', () => { it('shows blocks pagination with 5 pages (desktop)', () => { cy.viewport(760, 800); cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => { + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.waitForPageIdle(); }); @@ -493,6 +503,7 @@ describe('Mainnet', () => { it('shows blocks pagination with 3 pages (mobile)', () => { cy.viewport(669, 800); cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => { + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.waitForPageIdle(); }); diff --git a/frontend/cypress/e2e/signet/signet.spec.ts b/frontend/cypress/e2e/signet/signet.spec.ts index 03cfb3480..11c47d14d 100644 --- a/frontend/cypress/e2e/signet/signet.spec.ts +++ b/frontend/cypress/e2e/signet/signet.spec.ts @@ -95,12 +95,14 @@ describe('Signet', () => { describe('blocks', () => { it('shows empty blocks properly', () => { cy.visit('/signet/block/00000133d54e4589f6436703b067ec23209e0a21b8a9b12f57d0592fd85f7a42'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.get('h2').invoke('text').should('equal', '1 transaction'); }); it('expands and collapses the block details', () => { cy.visit('/signet/block/0'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.get('.btn.btn-outline-info').click().then(() => { cy.get('#details').should('be.visible'); @@ -113,6 +115,7 @@ describe('Signet', () => { it('shows blocks with no pagination', () => { cy.visit('/signet/block/00000078f920a96a69089877b934ce7fd009ab55e3170920a021262cb258e7cc'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.get('h2').invoke('text').should('equal', '13 transactions'); cy.get('ul.pagination').first().children().should('have.length', 5); @@ -121,6 +124,7 @@ describe('Signet', () => { it('supports pagination on the block screen', () => { // 43 txs cy.visit('/signet/block/00000094bd52f73bdbfc4bece3a94c21fec2dc968cd54210496e69e4059d66a6'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.get('.header-bg.box > a').invoke('text').then((text1) => { cy.get('.active + li').first().click().then(() => { diff --git a/frontend/cypress/e2e/testnet4/testnet4.spec.ts b/frontend/cypress/e2e/testnet4/testnet4.spec.ts index 4e2b6e3fa..c67d2414b 100644 --- a/frontend/cypress/e2e/testnet4/testnet4.spec.ts +++ b/frontend/cypress/e2e/testnet4/testnet4.spec.ts @@ -95,12 +95,14 @@ describe('Testnet4', () => { describe('blocks', () => { it('shows empty blocks properly', () => { cy.visit('/testnet4/block/0'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.get('h2').invoke('text').should('equal', '1 transaction'); }); it('expands and collapses the block details', () => { cy.visit('/testnet4/block/0'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.get('.btn.btn-outline-info').click().then(() => { cy.get('#details').should('be.visible'); @@ -113,6 +115,7 @@ describe('Testnet4', () => { it('shows blocks with no pagination', () => { cy.visit('/testnet4/block/000000000066e8b6cc78a93f8989587f5819624bae2eb1c05f535cadded19f99'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.get('h2').invoke('text').should('equal', '18 transactions'); cy.get('ul.pagination').first().children().should('have.length', 5); @@ -121,6 +124,7 @@ describe('Testnet4', () => { it('supports pagination on the block screen', () => { // 48 txs cy.visit('/testnet4/block/000000000000006982d53f8273bdff21dafc380c292eabc669b5ab6d732311c3'); + cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); cy.waitForSkeletonGone(); cy.get('.header-bg.box > a').invoke('text').then((text1) => { cy.get('.active + li').first().click().then(() => { diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 65d484520..f9d5d9474 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -12,6 +12,7 @@ import { PriceService } from './services/price.service'; import { EnterpriseService } from './services/enterprise.service'; import { WebsocketService } from './services/websocket.service'; import { AudioService } from './services/audio.service'; +import { PreloadService } from './services/preload.service'; import { SeoService } from './services/seo.service'; import { OpenGraphService } from './services/opengraph.service'; import { ZoneService } from './services/zone-shim.service'; @@ -46,6 +47,7 @@ const providers = [ CapAddressPipe, AppPreloadingStrategy, ServicesApiServices, + PreloadService, { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }, { provide: ZONE_SERVICE, useClass: ZoneService }, ]; diff --git a/frontend/src/app/components/block/block-transactions.component.html b/frontend/src/app/components/block/block-transactions.component.html new file mode 100644 index 000000000..788995475 --- /dev/null +++ b/frontend/src/app/components/block/block-transactions.component.html @@ -0,0 +1,53 @@ +
+

+ + {{ i }} transaction + {{ i }} transactions +

+ +
+
+ + + + +
+ + Error loading data. + +
+
+
+ + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+ + + +
+
+
+
+
+ + +
+ +
+
+ + diff --git a/frontend/src/app/components/block/block-transactions.component.scss b/frontend/src/app/components/block/block-transactions.component.scss new file mode 100644 index 000000000..d1ade512b --- /dev/null +++ b/frontend/src/app/components/block/block-transactions.component.scss @@ -0,0 +1,37 @@ +.block-tx-title { + display: flex; + justify-content: space-between; + flex-direction: column; + margin-top: -15px; + position: relative; + @media (min-width: 550px) { + margin-top: 1rem; + flex-direction: row; + } + h2 { + line-height: 1; + margin: 0; + position: relative; + padding-bottom: 10px; + @media (min-width: 550px) { + padding-bottom: 0px; + align-self: end; + } + } +} + +.tx-skeleton { + margin-top: 10px; + margin-bottom: 10px; + .header-bg { + &:first-child { + padding: 10px; + margin-bottom: 10px; + } + &:nth-child(2) { + .row { + height: 107px; + } + } + } +} diff --git a/frontend/src/app/components/block/block-transactions.component.ts b/frontend/src/app/components/block/block-transactions.component.ts new file mode 100644 index 000000000..c0cda6c4f --- /dev/null +++ b/frontend/src/app/components/block/block-transactions.component.ts @@ -0,0 +1,74 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { StateService } from '../../services/state.service'; +import { Transaction, Vout } from '../../interfaces/electrs.interface'; +import { Observable, Subscription, catchError, combineLatest, map, of, startWith, switchMap, tap } from 'rxjs'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ElectrsApiService } from '../../services/electrs-api.service'; +import { PreloadService } from '../../services/preload.service'; + +@Component({ + selector: 'app-block-transactions', + templateUrl: './block-transactions.component.html', + styleUrl: './block-transactions.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BlockTransactionsComponent implements OnInit { + @Input() txCount: number; + @Input() timestamp: number; + @Input() blockHash: string; + @Input() previousBlockHash: string; + @Input() block$: Observable; + @Input() paginationMaxSize: number; + @Output() blockReward = new EventEmitter(); + + itemsPerPage = this.stateService.env.ITEMS_PER_PAGE; + page = 1; + + transactions$: Observable; + isLoadingTransactions = true; + transactionsError: any = null; + transactionSubscription: Subscription; + txsLoadingStatus$: Observable; + nextBlockTxListSubscription: Subscription; + + constructor( + private stateService: StateService, + private route: ActivatedRoute, + private router: Router, + private electrsApiService: ElectrsApiService, + ) { } + + ngOnInit(): void { + this.transactions$ = combineLatest([this.block$, this.route.queryParams]).pipe( + tap(([_, queryParams]) => { + this.page = +queryParams['page'] || 1; + }), + switchMap(([block, _]) => this.electrsApiService.getBlockTransactions$(block.id, (this.page - 1) * this.itemsPerPage) + .pipe( + startWith(null), + catchError((err) => { + this.transactionsError = err; + return of([]); + })) + ), + tap((transactions: Transaction[]) => { + // The block API doesn't contain the block rewards on Liquid + if (this.stateService.isLiquid() && transactions && transactions[0] && transactions[0].vin[0].is_coinbase) { + const blockReward = transactions[0].vout.reduce((acc: number, curr: Vout) => acc + curr.value, 0) / 100000000; + this.blockReward.emit(blockReward); + } + }) + ); + + this.txsLoadingStatus$ = this.route.paramMap + .pipe( + switchMap(() => this.stateService.loadingIndicators$), + map((indicators) => indicators['blocktxs-' + this.blockHash] !== undefined ? indicators['blocktxs-' + this.blockHash] : 0) + ); + } + + pageChange(page: number, target: HTMLElement): void { + target.scrollIntoView(); // works for chrome + this.router.navigate([], { queryParams: { page: page }, queryParamsHandling: 'merge' }); + } +} diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index d2f84116c..c83459375 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -325,53 +325,39 @@ >Details -
-

- - {{ i }} transaction - {{ i }} transactions -

- - -
-
- - - - -
- - Error loading data. - -
-
-
- - -
- - + @defer (on viewport) { + + } @placeholder { +
+
+

+ + {{ i }} transaction + {{ i }} transactions +

+ +
+
+
+
-
-
-
+
- - -
-
-
- - -
-
- +
+
+
+ +
+
+ + + +
- - + }

@@ -382,12 +368,6 @@ - -
- -
-
-
diff --git a/frontend/src/app/components/block/block.component.scss b/frontend/src/app/components/block/block.component.scss index 70f388e73..af3d60c56 100644 --- a/frontend/src/app/components/block/block.component.scss +++ b/frontend/src/app/components/block/block.component.scss @@ -21,25 +21,6 @@ } } -.qr-wrapper { - background-color: var(--fg); - padding: 10px; - padding-bottom: 5px; - display: inline-block; -} - -.qrcode-col { - text-align: center; -} - -.qrcode-col > div { - margin: 20px auto 5px; - @media (min-width: 768px) { - text-align: center; - margin: auto; - } -} - .fiat { display: block; font-size: 13px; @@ -100,19 +81,7 @@ h1 { } } -.address-link { - line-height: 26px; - margin-left: 0px; - top: 14px; - position: relative; - display: flex; - flex-direction: row; - @media (min-width: 768px) { - line-height: 38px; - } -} - -.row{ +.row { flex-direction: column; @media (min-width: 768px) { flex-direction: row; @@ -140,28 +109,6 @@ h1 { margin-right: .5em; } -.block-tx-title { - display: flex; - justify-content: space-between; - flex-direction: column; - margin-top: -15px; - position: relative; - @media (min-width: 550px) { - margin-top: 1rem; - flex-direction: row; - } - h2 { - line-height: 1; - margin: 0; - position: relative; - padding-bottom: 10px; - @media (min-width: 550px) { - padding-bottom: 0px; - align-self: end; - } - } -} - .grow { flex-grow: 1; } @@ -204,22 +151,6 @@ h1 { } } -.tx-skeleton { - margin-top: 10px; - margin-bottom: 10px; - .header-bg { - &:first-child { - padding: 10px; - margin-bottom: 10px; - } - &:nth-child(2) { - .row { - height: 107px; - } - } - } -} - .chart-container{ margin: 20px auto; @media (min-width: 768px) { @@ -303,3 +234,41 @@ h1 { .graph-col { flex-grow: 1.11; } + +.block-tx-title { + display: flex; + justify-content: space-between; + flex-direction: column; + margin-top: -15px; + position: relative; + @media (min-width: 550px) { + margin-top: 1rem; + flex-direction: row; + } + h2 { + line-height: 1; + margin: 0; + position: relative; + padding-bottom: 10px; + @media (min-width: 550px) { + padding-bottom: 0px; + align-self: end; + } + } +} + +.tx-skeleton { + margin-top: 10px; + margin-bottom: 10px; + .header-bg { + &:first-child { + padding: 10px; + margin-bottom: 10px; + } + &:nth-child(2) { + .row { + height: 107px; + } + } + } +} diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 9b0dc0d05..d762a879e 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -1,15 +1,14 @@ -import { Component, OnInit, OnDestroy, ViewChildren, QueryList, Inject, PLATFORM_ID, ChangeDetectorRef } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core'; import { Location } from '@angular/common'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ElectrsApiService } from '../../services/electrs-api.service'; import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter } from 'rxjs/operators'; -import { Transaction, Vout } from '../../interfaces/electrs.interface'; import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs'; import { StateService } from '../../services/state.service'; import { SeoService } from '../../services/seo.service'; import { WebsocketService } from '../../services/websocket.service'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; -import { AccelerationInfo, BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; +import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; import { ApiService } from '../../services/api.service'; import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; import { detectWebGL } from '../../shared/graphs.utils'; @@ -17,6 +16,7 @@ import { seoDescriptionNetwork } from '../../shared/common.utils'; import { PriceService, Price } from '../../services/price.service'; import { CacheService } from '../../services/cache.service'; import { ServicesApiServices } from '../../services/services-api.service'; +import { PreloadService } from '../../services/preload.service'; @Component({ selector: 'app-block', @@ -42,23 +42,17 @@ export class BlockComponent implements OnInit, OnDestroy { isLoadingBlock = true; latestBlock: BlockExtended; latestBlocks: BlockExtended[] = []; - transactions: Transaction[]; oobFees: number = 0; - isLoadingTransactions = true; strippedTransactions: TransactionStripped[]; overviewTransitionDirection: string; isLoadingOverview = true; error: any; blockSubsidy: number; fees: number; - paginationMaxSize: number; - page = 1; - itemsPerPage: number; - txsLoadingStatus$: Observable; + block$: Observable; showDetails = false; showPreviousBlocklink = true; showNextBlocklink = true; - transactionsError: any = null; overviewError: any = null; webGlEnabled = true; auditParamEnabled: boolean = false; @@ -69,20 +63,16 @@ export class BlockComponent implements OnInit, OnDestroy { isMobile = window.innerWidth <= 767.98; hoverTx: string; numMissing: number = 0; + paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; numUnexpected: number = 0; mode: 'projected' | 'actual' = 'projected'; - transactionSubscription: Subscription; overviewSubscription: Subscription; - auditSubscription: Subscription; keyNavigationSubscription: Subscription; blocksSubscription: Subscription; cacheBlocksSubscription: Subscription; networkChangedSubscription: Subscription; queryParamsSubscription: Subscription; - nextBlockSubscription: Subscription = undefined; - nextBlockSummarySubscription: Subscription = undefined; - nextBlockTxListSubscription: Subscription = undefined; timeLtrSubscription: Subscription; timeLtr: boolean; childChangeSubscription: Subscription; @@ -109,16 +99,14 @@ export class BlockComponent implements OnInit, OnDestroy { private cacheService: CacheService, private servicesApiService: ServicesApiServices, private cd: ChangeDetectorRef, - @Inject(PLATFORM_ID) private platformId: Object, + private preloadService: PreloadService, ) { this.webGlEnabled = this.stateService.isBrowser && detectWebGL(); } - ngOnInit() { + ngOnInit(): void { this.websocketService.want(['blocks', 'mempool-blocks']); - this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; this.network = this.stateService.network; - this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE; this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { this.timeLtr = !!ltr; @@ -139,12 +127,6 @@ export class BlockComponent implements OnInit, OnDestroy { }); } - this.txsLoadingStatus$ = this.route.paramMap - .pipe( - switchMap(() => this.stateService.loadingIndicators$), - map((indicators) => indicators['blocktxs-' + this.blockHash] !== undefined ? indicators['blocktxs-' + this.blockHash] : 0) - ); - this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block) => { this.loadedCacheBlock(block); }); @@ -172,11 +154,10 @@ export class BlockComponent implements OnInit, OnDestroy { } }); - const block$ = this.route.paramMap.pipe( + this.block$ = this.route.paramMap.pipe( switchMap((params: ParamMap) => { const blockHash: string = params.get('id') || ''; this.block = undefined; - this.page = 1; this.error = undefined; this.fees = undefined; this.oobFees = 0; @@ -254,16 +235,11 @@ export class BlockComponent implements OnInit, OnDestroy { } }), tap((block: BlockExtended) => { - if (block.height > 0) { - // Preload previous block summary (execute the http query so the response will be cached) - this.unsubscribeNextBlockSubscriptions(); - setTimeout(() => { - this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe(); - this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe(); - if (this.auditSupported) { - this.apiService.getBlockAudit$(block.previousblockhash); - } - }, 100); + if (block.previousblockhash) { + this.preloadService.block$.next(block.previousblockhash); + if (this.auditSupported) { + this.preloadService.blockAudit$.next(block.previousblockhash); + } } this.updateAuditAvailableFromBlockHeight(block.height); this.block = block; @@ -288,9 +264,6 @@ export class BlockComponent implements OnInit, OnDestroy { this.fees = block.extras.reward / 100000000 - this.blockSubsidy; } this.stateService.markBlock$.next({ blockHeight: this.blockHeight }); - this.isLoadingTransactions = true; - this.transactions = null; - this.transactionsError = null; this.isLoadingOverview = true; this.overviewError = null; @@ -304,31 +277,8 @@ export class BlockComponent implements OnInit, OnDestroy { throttleTime(300, asyncScheduler, { leading: true, trailing: true }), shareReplay(1) ); - this.transactionSubscription = combineLatest([block$, this.route.queryParams]).pipe( - tap(([_, queryParams]) => this.page = +queryParams['page'] || 1), - switchMap(([block, _]) => this.electrsApiService.getBlockTransactions$(block.id, (this.page - 1) * this.itemsPerPage) - .pipe( - catchError((err) => { - this.transactionsError = err; - return of([]); - })) - ), - ) - .subscribe((transactions: Transaction[]) => { - if (this.fees === undefined && transactions[0]) { - this.fees = transactions[0].vout.reduce((acc: number, curr: Vout) => acc + curr.value, 0) / 100000000 - this.blockSubsidy; - } - this.transactions = transactions; - this.isLoadingTransactions = false; - this.cd.markForCheck(); - }, - (error) => { - this.error = error; - this.isLoadingBlock = false; - this.isLoadingOverview = false; - }); - this.overviewSubscription = block$.pipe( + this.overviewSubscription = this.block$.pipe( switchMap((block) => { return forkJoin([ this.apiService.getStrippedBlockTransactions$(block.id) @@ -498,14 +448,14 @@ export class BlockComponent implements OnInit, OnDestroy { this.cd.markForCheck(); }); - this.oobSubscription = block$.pipe( + this.oobSubscription = this.block$.pipe( filter(() => this.stateService.env.PUBLIC_ACCELERATIONS === true && this.stateService.network === ''), switchMap((block) => this.apiService.getAccelerationsByHeight$(block.height) .pipe( map(accelerations => { return { block, accelerations }; }), - catchError((err) => { + catchError(() => { return of({ block, accelerations: [] }); })) ), @@ -560,7 +510,7 @@ export class BlockComponent implements OnInit, OnDestroy { if (this.priceSubscription) { this.priceSubscription.unsubscribe(); } - this.priceSubscription = combineLatest([this.stateService.fiatCurrency$, block$]).pipe( + this.priceSubscription = combineLatest([this.stateService.fiatCurrency$, this.block$]).pipe( switchMap(([currency, block]) => { return this.priceService.getBlockPrice$(block.timestamp, true, currency).pipe( tap((price) => { @@ -577,52 +527,27 @@ export class BlockComponent implements OnInit, OnDestroy { }); } - ngOnDestroy() { + ngOnDestroy(): void { this.stateService.markBlock$.next({}); - this.transactionSubscription?.unsubscribe(); this.overviewSubscription?.unsubscribe(); - this.auditSubscription?.unsubscribe(); this.keyNavigationSubscription?.unsubscribe(); this.blocksSubscription?.unsubscribe(); this.cacheBlocksSubscription?.unsubscribe(); this.networkChangedSubscription?.unsubscribe(); this.queryParamsSubscription?.unsubscribe(); this.timeLtrSubscription?.unsubscribe(); - this.auditSubscription?.unsubscribe(); - this.unsubscribeNextBlockSubscriptions(); this.childChangeSubscription?.unsubscribe(); this.priceSubscription?.unsubscribe(); this.oobSubscription?.unsubscribe(); } - unsubscribeNextBlockSubscriptions() { - if (this.nextBlockSubscription !== undefined) { - this.nextBlockSubscription.unsubscribe(); - } - if (this.nextBlockSummarySubscription !== undefined) { - this.nextBlockSummarySubscription.unsubscribe(); - } - if (this.nextBlockTxListSubscription !== undefined) { - this.nextBlockTxListSubscription.unsubscribe(); - } - } - // TODO - Refactor this.fees/this.reward for liquid because it is not // used anymore on Bitcoin networks (we use block.extras directly) - setBlockSubsidy() { + setBlockSubsidy(): void { this.blockSubsidy = 0; } - pageChange(page: number, target: HTMLElement) { - const start = (page - 1) * this.itemsPerPage; - this.isLoadingTransactions = true; - this.transactions = null; - this.transactionsError = null; - target.scrollIntoView(); // works for chrome - this.router.navigate([], { queryParams: { page: page }, queryParamsHandling: 'merge' }); - } - - toggleShowDetails() { + toggleShowDetails(): void { if (this.showDetails) { this.showDetails = false; this.router.navigate([], { @@ -654,7 +579,7 @@ export class BlockComponent implements OnInit, OnDestroy { return this.block && this.block.height > 681393 && (new Date().getTime() / 1000) < 1628640000; } - navigateToPreviousBlock() { + navigateToPreviousBlock(): void { if (!this.block) { return; } @@ -663,13 +588,13 @@ export class BlockComponent implements OnInit, OnDestroy { block ? block.id : this.block.previousblockhash], { state: { data: { block, blockHeight: this.nextBlockHeight - 2 } } }); } - navigateToNextBlock() { + navigateToNextBlock(): void { const block = this.latestBlocks.find((b) => b.height === this.nextBlockHeight); this.router.navigate([this.relativeUrlPipe.transform('/block/'), block ? block.id : this.nextBlockHeight], { state: { data: { block, blockHeight: this.nextBlockHeight } } }); } - setNextAndPreviousBlockLink(){ + setNextAndPreviousBlockLink(): void { if (this.latestBlock) { if (!this.blockHeight){ this.showPreviousBlocklink = false; @@ -701,11 +626,12 @@ export class BlockComponent implements OnInit, OnDestroy { } } - onResize(event: any): void { - const isMobile = event.target.innerWidth <= 767.98; + onResize(event: Event): void { + const target = event.target as Window; + const isMobile = target.innerWidth <= 767.98; const changed = isMobile !== this.isMobile; this.isMobile = isMobile; - this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5; + this.paginationMaxSize = target.innerWidth < 670 ? 3 : 5; if (changed) { this.changeMode(this.mode); @@ -747,11 +673,11 @@ export class BlockComponent implements OnInit, OnDestroy { this.stateService.hideAudit.next(this.auditModeEnabled); this.route.queryParams.subscribe(params => { - let queryParams = { ...params }; + const queryParams = { ...params }; delete queryParams['audit']; let newUrl = this.router.url.split('?')[0]; - let queryString = new URLSearchParams(queryParams).toString(); + const queryString = new URLSearchParams(queryParams).toString(); if (queryString) { newUrl += '?' + queryString; } @@ -829,4 +755,10 @@ export class BlockComponent implements OnInit, OnDestroy { this.block.canonical = block.id; } } + + updateBlockReward(blockReward: number): void { + if (this.fees === undefined) { + this.fees = blockReward; + } + } } \ No newline at end of file diff --git a/frontend/src/app/components/block/block.module.ts b/frontend/src/app/components/block/block.module.ts index d6991c68a..661e52dcf 100644 --- a/frontend/src/app/components/block/block.module.ts +++ b/frontend/src/app/components/block/block.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; import { BlockComponent } from './block.component'; +import { BlockTransactionsComponent } from './block-transactions.component'; import { SharedModule } from '../../shared/shared.module'; const routes: Routes = [ @@ -32,6 +33,7 @@ export class BlockRoutingModule { } ], declarations: [ BlockComponent, + BlockTransactionsComponent, ] }) export class BlockModule { } diff --git a/frontend/src/app/services/preload.service.ts b/frontend/src/app/services/preload.service.ts new file mode 100644 index 000000000..386d4deb4 --- /dev/null +++ b/frontend/src/app/services/preload.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { ElectrsApiService } from '../services/electrs-api.service'; +import { Subject, debounceTime, switchMap } from 'rxjs'; +import { ApiService } from './api.service'; + +@Injectable({ + providedIn: 'root' +}) +export class PreloadService { + block$ = new Subject; + blockAudit$ = new Subject; + debounceTime = 250; + + constructor( + private electrsApiService: ElectrsApiService, + private apiService: ApiService, + ) { + this.block$ + .pipe( + debounceTime(this.debounceTime), + switchMap((blockHash) => this.electrsApiService.getBlockTransactions$(blockHash)) + ) + .subscribe(); + + this.blockAudit$ + .pipe( + debounceTime(this.debounceTime), + switchMap((blockHash) => this.apiService.getBlockAudit$(blockHash)) + ) + .subscribe(); + } + +}