diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index c61fd0e12..bb36928b0 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -302,8 +302,9 @@ export class BlockComponent implements OnInit, OnDestroy { throttleTime(300, asyncScheduler, { leading: true, trailing: true }), shareReplay(1) ); - this.transactionSubscription = block$.pipe( - switchMap((block) => this.electrsApiService.getBlockTransactions$(block.id) + 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; @@ -592,19 +593,7 @@ export class BlockComponent implements OnInit, OnDestroy { this.transactions = null; this.transactionsError = null; target.scrollIntoView(); // works for chrome - - this.electrsApiService.getBlockTransactions$(this.block.id, start) - .pipe( - catchError((err) => { - this.transactionsError = err; - return of([]); - }) - ) - .subscribe((transactions) => { - this.transactions = transactions; - this.isLoadingTransactions = false; - target.scrollIntoView(); // works for firefox - }); + this.router.navigate([], { queryParams: { page: page }, queryParamsHandling: 'merge' }); } toggleShowDetails() { diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.ts b/frontend/src/app/components/blocks-list/blocks-list.component.ts index f451cb3a2..94afb6509 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.ts +++ b/frontend/src/app/components/blocks-list/blocks-list.component.ts @@ -1,6 +1,7 @@ -import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core'; -import { BehaviorSubject, combineLatest, Observable, timer, of } from 'rxjs'; -import { delayWhen, map, retryWhen, scan, switchMap, tap } from 'rxjs/operators'; +import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, Inject, LOCALE_ID } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, combineLatest, Observable, timer, of, Subscription } from 'rxjs'; +import { debounceTime, delayWhen, filter, map, retryWhen, scan, skip, switchMap, tap, throttleTime } from 'rxjs/operators'; import { BlockExtended } from '../../interfaces/node-api.interface'; import { ApiService } from '../../services/api.service'; import { StateService } from '../../services/state.service'; @@ -25,6 +26,7 @@ export class BlocksList implements OnInit { auditAvailable = false; isLoading = true; fromBlockHeight = undefined; + lastBlockHeightFetched = -1; paginationMaxSize: number; page = 1; lastPage = 1; @@ -33,6 +35,10 @@ export class BlocksList implements OnInit { fromHeightSubject: BehaviorSubject = new BehaviorSubject(this.fromBlockHeight); skeletonLines: number[] = []; lastBlockHeight = -1; + blocksCountInitialized$: BehaviorSubject = new BehaviorSubject(false); + blocksCountInitializedSubscription: Subscription; + keyNavigationSubscription: Subscription; + dir: 'rtl' | 'ltr' = 'ltr'; constructor( private apiService: ApiService, @@ -41,8 +47,14 @@ export class BlocksList implements OnInit { private cd: ChangeDetectorRef, private seoService: SeoService, private ogService: OpenGraphService, + private route: ActivatedRoute, + private router: Router, + @Inject(LOCALE_ID) private locale: string, ) { this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool'; + if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) { + this.dir = 'rtl'; + } } ngOnInit(): void { @@ -52,6 +64,34 @@ export class BlocksList implements OnInit { if (!this.widget) { this.websocketService.want(['blocks']); + this.blocksCountInitializedSubscription = combineLatest([this.blocksCountInitialized$, this.route.queryParams]).pipe( + filter(([blocksCountInitialized, _]) => blocksCountInitialized), + tap(([_, params]) => { + this.page = +params['page'] || 1; + this.page === 1 ? this.fromHeightSubject.next(undefined) : this.fromHeightSubject.next((this.blocksCount - 1) - (this.page - 1) * 15); + this.cd.markForCheck(); + }) + ).subscribe(); + + this.keyNavigationSubscription = this.stateService.keyNavigation$ + .pipe( + tap((event) => { + this.isLoading = true; + const prevKey = this.dir === 'ltr' ? 'ArrowLeft' : 'ArrowRight'; + const nextKey = this.dir === 'ltr' ? 'ArrowRight' : 'ArrowLeft'; + if (event.key === prevKey && this.page > 1) { + this.page--; + this.cd.markForCheck(); + } + if (event.key === nextKey && this.page * 15 < this.blocksCount) { + this.page++; + this.cd.markForCheck(); + } + }), + throttleTime(1000, undefined, { leading: true, trailing: true }), + ).subscribe(() => { + this.pageChange(this.page); + }); } this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; @@ -70,13 +110,16 @@ export class BlocksList implements OnInit { this.blocks$ = combineLatest([ this.fromHeightSubject.pipe( + filter(fromBlockHeight => fromBlockHeight !== this.lastBlockHeightFetched), switchMap((fromBlockHeight) => { this.isLoading = true; + this.lastBlockHeightFetched = fromBlockHeight; return this.apiService.getBlocks$(this.page === 1 ? undefined : fromBlockHeight) .pipe( tap(blocks => { if (this.blocksCount === undefined) { this.blocksCount = blocks[0].height + 1; + this.blocksCountInitialized$.next(true); } this.isLoading = false; this.lastBlockHeight = Math.max(...blocks.map(o => o.height)); @@ -138,7 +181,7 @@ export class BlocksList implements OnInit { } pageChange(page: number): void { - this.fromHeightSubject.next((this.blocksCount - 1) - (page - 1) * 15); + this.router.navigate([], { queryParams: { page: page } }); } trackByBlock(index: number, block: BlockExtended): number { @@ -148,4 +191,9 @@ export class BlocksList implements OnInit { isEllipsisActive(e): boolean { return (e.offsetWidth < e.scrollWidth); } + + ngOnDestroy(): void { + this.blocksCountInitializedSubscription?.unsubscribe(); + this.keyNavigationSubscription?.unsubscribe(); + } } diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts index d8c60113f..b818dff78 100644 --- a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts +++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts @@ -1,6 +1,7 @@ -import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; -import { BehaviorSubject, Observable, Subject, combineLatest, of, timer } from 'rxjs'; -import { delayWhen, filter, map, share, shareReplay, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Component, OnInit, ChangeDetectionStrategy, Input, Inject, LOCALE_ID, ChangeDetectorRef } from '@angular/core'; +import { BehaviorSubject, Observable, Subject, Subscription, combineLatest, of, timer } from 'rxjs'; +import { delayWhen, filter, map, share, shareReplay, switchMap, take, takeUntil, tap, throttleTime } from 'rxjs/operators'; import { ApiService } from '../../../services/api.service'; import { Env, StateService } from '../../../services/state.service'; import { AuditStatus, CurrentPegs, RecentPeg } from '../../../interfaces/node-api.interface'; @@ -29,20 +30,31 @@ export class RecentPegsListComponent implements OnInit { lastReservesBlockUpdate: number = 0; currentPeg$: Observable; pegsCount$: Observable; + pegsCount: number; startingIndexSubject: BehaviorSubject = new BehaviorSubject(0); currentIndex: number = 0; lastPegBlockUpdate: number = 0; lastPegAmount: string = ''; isLoad: boolean = true; + queryParamSubscription: Subscription; + keyNavigationSubscription: Subscription; + dir: 'rtl' | 'ltr' = 'ltr'; private destroy$ = new Subject(); - + constructor( private apiService: ApiService, + private cd: ChangeDetectorRef, public stateService: StateService, private websocketService: WebsocketService, - private seoService: SeoService + private seoService: SeoService, + private route: ActivatedRoute, + private router: Router, + @Inject(LOCALE_ID) private locale: string, ) { + if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) { + this.dir = 'rtl'; + } } ngOnInit(): void { @@ -53,6 +65,34 @@ export class RecentPegsListComponent implements OnInit { if (!this.widget) { this.seoService.setTitle($localize`:@@a8b0889ea1b41888f1e247f2731cc9322198ca04:Recent Peg-In / Out's`); this.websocketService.want(['blocks']); + + this.queryParamSubscription = this.route.queryParams.pipe( + tap((params) => { + this.page = +params['page'] || 1; + this.startingIndexSubject.next((this.page - 1) * 15); + }), + ).subscribe(); + + this.keyNavigationSubscription = this.stateService.keyNavigation$ + .pipe( + tap((event) => { + this.isLoading = true; + const prevKey = this.dir === 'ltr' ? 'ArrowLeft' : 'ArrowRight'; + const nextKey = this.dir === 'ltr' ? 'ArrowRight' : 'ArrowLeft'; + if (event.key === prevKey && this.page > 1) { + this.page--; + this.cd.markForCheck(); + } + if (event.key === nextKey && this.page < this.pegsCount / this.pageSize) { + this.page++; + this.cd.markForCheck(); + } + }), + throttleTime(1000, undefined, { leading: true, trailing: true }), + ).subscribe(() => { + this.pageChange(this.page); + }); + this.auditStatus$ = this.stateService.blocks$.pipe( takeUntil(this.destroy$), throttleTime(40000), @@ -99,7 +139,10 @@ export class RecentPegsListComponent implements OnInit { tap(() => this.isPegCountLoading = true), switchMap(_ => this.apiService.pegsCount$()), map((data) => data.pegs_count), - tap(() => this.isPegCountLoading = false), + tap((pegsCount) => { + this.isPegCountLoading = false; + this.pegsCount = pegsCount; + }), share() ); @@ -122,18 +165,19 @@ export class RecentPegsListComponent implements OnInit { tap(() => this.isLoading = false), share() ); - + } } ngOnDestroy(): void { this.destroy$.next(1); this.destroy$.complete(); + this.queryParamSubscription?.unsubscribe(); + this.keyNavigationSubscription?.unsubscribe(); } pageChange(page: number): void { - this.startingIndexSubject.next((page - 1) * 15); - this.page = page; + this.router.navigate([], { queryParams: { page: page } }); } }