diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2d34bb03..51ddb7855 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" strategy: matrix: - node: ["18", "20"] + node: ["20", "21"] flavor: ["dev", "prod"] fail-fast: false runs-on: "ubuntu-latest" @@ -160,7 +160,7 @@ jobs: if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" strategy: matrix: - node: ["18", "20"] + node: ["20", "21"] flavor: ["dev", "prod"] fail-fast: false runs-on: "ubuntu-latest" diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index 55a5585cc..919130c53 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -100,6 +100,5 @@ jobs: --cache-to "type=local,dest=/tmp/.buildx-cache" \ --platform linux/amd64,linux/arm64 \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ - --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ --output "type=registry" ./${{ matrix.service }}/ \ --build-arg commitHash=$SHORT_SHA diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 4ccf58645..28ca38152 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -40,6 +40,7 @@ class Blocks { private quarterEpochBlockTime: number | null = null; private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise)[] = []; + private classifyingBlocks: boolean = false; private mainLoopTimeout: number = 120000; @@ -568,6 +569,11 @@ class Blocks { * [INDEXING] Index transaction classification flags for Goggles */ public async $classifyBlocks(): Promise { + if (this.classifyingBlocks) { + return; + } + this.classifyingBlocks = true; + // classification requires an esplora backend if (!Common.gogglesIndexingEnabled() || config.MEMPOOL.BACKEND !== 'esplora') { return; @@ -679,6 +685,8 @@ class Blocks { indexedThisRun = 0; } } + + this.classifyingBlocks = false; } /** diff --git a/backend/src/api/redis-cache.ts b/backend/src/api/redis-cache.ts index edfd2142b..d19d73a7f 100644 --- a/backend/src/api/redis-cache.ts +++ b/backend/src/api/redis-cache.ts @@ -19,45 +19,90 @@ class RedisCache { private client; private connected = false; private schemaVersion = 1; + private redisConfig: any; + private pauseFlush: boolean = false; private cacheQueue: MempoolTransactionExtended[] = []; + private removeQueue: string[] = []; + private rbfCacheQueue: { type: string, txid: string, value: any }[] = []; + private rbfRemoveQueue: { type: string, txid: string }[] = []; private txFlushLimit: number = 10000; constructor() { if (config.REDIS.ENABLED) { - const redisConfig = { + this.redisConfig = { socket: { path: config.REDIS.UNIX_SOCKET_PATH }, database: NetworkDB[config.MEMPOOL.NETWORK], }; - this.client = createClient(redisConfig); - this.client.on('error', (e) => { - logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`); - }); this.$ensureConnected(); + setInterval(() => { this.$ensureConnected(); }, 10000); } } - private async $ensureConnected(): Promise { + private async $ensureConnected(): Promise { if (!this.connected && config.REDIS.ENABLED) { - return this.client.connect().then(async () => { - this.connected = true; - logger.info(`Redis client connected`); - const version = await this.client.get('schema_version'); - if (version !== this.schemaVersion) { - // schema changed - // perform migrations or flush DB if necessary - logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`); - await this.client.set('schema_version', this.schemaVersion); - } - }); + try { + this.client = createClient(this.redisConfig); + this.client.on('error', async (e) => { + logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`); + this.connected = false; + await this.client.disconnect(); + }); + await this.client.connect().then(async () => { + try { + const version = await this.client.get('schema_version'); + this.connected = true; + if (version !== this.schemaVersion) { + // schema changed + // perform migrations or flush DB if necessary + logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`); + await this.client.set('schema_version', this.schemaVersion); + } + logger.info(`Redis client connected`); + return true; + } catch (e) { + this.connected = false; + logger.warn('Failed to connect to Redis'); + return false; + } + }); + await this.$onConnected(); + return true; + } catch (e) { + logger.warn('Error connecting to Redis: ' + (e instanceof Error ? e.message : e)); + return false; + } + } else { + try { + // test connection + await this.client.get('schema_version'); + return true; + } catch (e) { + logger.warn('Lost connection to Redis: ' + (e instanceof Error ? e.message : e)); + logger.warn('Attempting to reconnect in 10 seconds'); + this.connected = false; + return false; + } } } - async $updateBlocks(blocks: BlockExtended[]) { + private async $onConnected(): Promise { + await this.$flushTransactions(); + await this.$removeTransactions([]); + await this.$flushRbfQueues(); + } + + async $updateBlocks(blocks: BlockExtended[]): Promise { + if (!config.REDIS.ENABLED) { + return; + } + if (!this.connected) { + logger.warn(`Failed to update blocks in Redis cache: Redis is not connected`); + return; + } try { - await this.$ensureConnected(); await this.client.set('blocks', JSON.stringify(blocks)); logger.debug(`Saved latest blocks to Redis cache`); } catch (e) { @@ -65,9 +110,15 @@ class RedisCache { } } - async $updateBlockSummaries(summaries: BlockSummary[]) { + async $updateBlockSummaries(summaries: BlockSummary[]): Promise { + if (!config.REDIS.ENABLED) { + return; + } + if (!this.connected) { + logger.warn(`Failed to update block summaries in Redis cache: Redis is not connected`); + return; + } try { - await this.$ensureConnected(); await this.client.set('block-summaries', JSON.stringify(summaries)); logger.debug(`Saved latest block summaries to Redis cache`); } catch (e) { @@ -75,30 +126,35 @@ class RedisCache { } } - async $addTransaction(tx: MempoolTransactionExtended) { + async $addTransaction(tx: MempoolTransactionExtended): Promise { + if (!config.REDIS.ENABLED) { + return; + } this.cacheQueue.push(tx); if (this.cacheQueue.length >= this.txFlushLimit) { - await this.$flushTransactions(); + if (!this.pauseFlush) { + await this.$flushTransactions(); + } } } - async $flushTransactions() { - const success = await this.$addTransactions(this.cacheQueue); - if (success) { - logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`); - this.cacheQueue = []; - } else { - logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`); + async $flushTransactions(): Promise { + if (!config.REDIS.ENABLED) { + return; + } + if (!this.cacheQueue.length) { + return; + } + if (!this.connected) { + logger.warn(`Failed to add ${this.cacheQueue.length} transactions to Redis cache: Redis not connected`); + return; } - } - private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise { - if (!newTransactions.length) { - return true; - } + this.pauseFlush = false; + + const toAdd = this.cacheQueue.slice(0, this.txFlushLimit); try { - await this.$ensureConnected(); - const msetData = newTransactions.map(tx => { + const msetData = toAdd.map(tx => { const minified: any = { ...tx }; delete minified.hex; for (const vin of minified.vin) { @@ -112,30 +168,53 @@ class RedisCache { return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)]; }); await this.client.MSET(msetData); - return true; + // successful, remove transactions from cache queue + this.cacheQueue = this.cacheQueue.slice(toAdd.length); + logger.debug(`Saved ${toAdd.length} transactions to Redis cache, ${this.cacheQueue.length} left in queue`); } catch (e) { - logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`); - return false; + logger.warn(`Failed to add ${toAdd.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`); + this.pauseFlush = true; } } - async $removeTransactions(transactions: string[]) { - try { - await this.$ensureConnected(); + async $removeTransactions(transactions: string[]): Promise { + if (!config.REDIS.ENABLED) { + return; + } + const toRemove = this.removeQueue.concat(transactions); + this.removeQueue = []; + let failed: string[] = []; + let numRemoved = 0; + if (this.connected) { const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE; - for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) { - const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength); - await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`)); - logger.debug(`Deleted ${slice.length} transactions from the Redis cache`); + for (let i = 0; i < Math.ceil(toRemove.length / sliceLength); i++) { + const slice = toRemove.slice(i * sliceLength, (i + 1) * sliceLength); + try { + await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`)); + numRemoved+= sliceLength; + logger.debug(`Deleted ${slice.length} transactions from the Redis cache`); + } catch (e) { + logger.warn(`Failed to remove ${slice.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`); + failed = failed.concat(slice); + } } - } catch (e) { - logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`); + // concat instead of replace, in case more txs have been added in the meantime + this.removeQueue = this.removeQueue.concat(failed); + } else { + this.removeQueue = this.removeQueue.concat(toRemove); } } async $setRbfEntry(type: string, txid: string, value: any): Promise { + if (!config.REDIS.ENABLED) { + return; + } + if (!this.connected) { + this.rbfCacheQueue.push({ type, txid, value }); + logger.warn(`Failed to set RBF ${type} in Redis cache: Redis is not connected`); + return; + } try { - await this.$ensureConnected(); await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value)); } catch (e) { logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`); @@ -143,17 +222,55 @@ class RedisCache { } async $removeRbfEntry(type: string, txid: string): Promise { + if (!config.REDIS.ENABLED) { + return; + } + if (!this.connected) { + this.rbfRemoveQueue.push({ type, txid }); + logger.warn(`Failed to remove RBF ${type} from Redis cache: Redis is not connected`); + return; + } try { - await this.$ensureConnected(); await this.client.unlink(`rbf:${type}:${txid}`); } catch (e) { logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`); } } - async $getBlocks(): Promise { + private async $flushRbfQueues(): Promise { + if (!config.REDIS.ENABLED) { + return; + } + if (!this.connected) { + return; + } + try { + const toAdd = this.rbfCacheQueue; + this.rbfCacheQueue = []; + for (const { type, txid, value } of toAdd) { + await this.$setRbfEntry(type, txid, value); + } + logger.debug(`Saved ${toAdd.length} queued RBF entries to the Redis cache`); + const toRemove = this.rbfRemoveQueue; + this.rbfRemoveQueue = []; + for (const { type, txid } of toRemove) { + await this.$removeRbfEntry(type, txid); + } + logger.debug(`Removed ${toRemove.length} queued RBF entries from the Redis cache`); + } catch (e) { + logger.warn(`Failed to flush RBF cache event queues after reconnecting to Redis: ${e instanceof Error ? e.message : e}`); + } + } + + async $getBlocks(): Promise { + if (!config.REDIS.ENABLED) { + return []; + } + if (!this.connected) { + logger.warn(`Failed to retrieve blocks from Redis cache: Redis is not connected`); + return []; + } try { - await this.$ensureConnected(); const json = await this.client.get('blocks'); return JSON.parse(json); } catch (e) { @@ -163,8 +280,14 @@ class RedisCache { } async $getBlockSummaries(): Promise { + if (!config.REDIS.ENABLED) { + return []; + } + if (!this.connected) { + logger.warn(`Failed to retrieve blocks from Redis cache: Redis is not connected`); + return []; + } try { - await this.$ensureConnected(); const json = await this.client.get('block-summaries'); return JSON.parse(json); } catch (e) { @@ -174,10 +297,16 @@ class RedisCache { } async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> { + if (!config.REDIS.ENABLED) { + return {}; + } + if (!this.connected) { + logger.warn(`Failed to retrieve mempool from Redis cache: Redis is not connected`); + return {}; + } const start = Date.now(); const mempool = {}; try { - await this.$ensureConnected(); const mempoolList = await this.scanKeys('mempool:tx:*'); for (const tx of mempoolList) { mempool[tx.key] = tx.value; @@ -191,8 +320,14 @@ class RedisCache { } async $getRbfEntries(type: string): Promise { + if (!config.REDIS.ENABLED) { + return []; + } + if (!this.connected) { + logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: Redis is not connected`); + return []; + } try { - await this.$ensureConnected(); const rbfEntries = await this.scanKeys(`rbf:${type}:*`); return rbfEntries; } catch (e) { @@ -201,7 +336,10 @@ class RedisCache { } } - async $loadCache() { + async $loadCache(): Promise { + if (!config.REDIS.ENABLED) { + return; + } logger.info('Restoring mempool and blocks data from Redis cache'); // Load block data const loadedBlocks = await this.$getBlocks(); @@ -226,7 +364,7 @@ class RedisCache { }); } - private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }) { + private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }): void { for (const tx of Object.values(mempool)) { for (const vin of tx.vin) { if (vin.scriptsig) { diff --git a/backend/src/indexer.ts b/backend/src/indexer.ts index 90b4a59e6..dcb91d010 100644 --- a/backend/src/indexer.ts +++ b/backend/src/indexer.ts @@ -185,7 +185,8 @@ class Indexer { await blocks.$generateCPFPDatabase(); await blocks.$generateAuditStats(); await auditReplicator.$sync(); - await blocks.$classifyBlocks(); + // do not wait for classify blocks to finish + blocks.$classifyBlocks(); } catch (e) { this.indexerRunning = false; logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e)); diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 054f26fe6..009040889 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -416,7 +416,7 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.

- This program incorporates software and other components licensed from third parties. See the full list of Third-Party Licenses for legal notices from those projects. + This program incorporates software and other components licensed from third parties. See the full list of Third-Party Licenses for legal notices from those projects.

Trademark Notice
@@ -429,10 +429,6 @@

- -
diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html index 2d2c9c3f3..b4488d33d 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html +++ b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html @@ -129,7 +129,7 @@ - mempool.space fee + Accelerator Service Fee +{{ estimate.mempoolBaseFee | number }} @@ -141,7 +141,7 @@ - Transaction vsize fee + Transaction Size Surcharge +{{ estimate.vsizeFee | number }} diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss index 9ad50fc30..81a79eb67 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.scss @@ -11,6 +11,7 @@ text-align: left; min-width: 320px; pointer-events: none; + z-index: 11; &.clickable { pointer-events: all; diff --git a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html index 215b5c68a..1461b0c59 100644 --- a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html +++ b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.html @@ -46,15 +46,13 @@
Next Halving
- - {{ i }} blocks - {{ i }} block + {{ timeUntilHalving | date }}
- {{ timeUntilHalving | date }} +
diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index f7b91e151..a6f43909a 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -163,7 +163,7 @@ export class PoolRankingComponent implements OnInit { const i = pool.blockCount.toString(); if (this.miningWindowPreference === '24h') { return `${pool.name} (${pool.share}%)
` + - pool.lastEstimatedHashrate.toString() + ' PH/s' + + pool.lastEstimatedHashrate.toString() + ' ' + miningStats.miningUnits.hashrateUnit + `
` + $localize`${ i }:INTERPOLATION: blocks`; } else { return `${pool.name} (${pool.share}%)
` + @@ -201,7 +201,7 @@ export class PoolRankingComponent implements OnInit { const i = totalBlockOther.toString(); if (this.miningWindowPreference === '24h') { return `` + $localize`Other (${percentage})` + `
` + - totalEstimatedHashrateOther.toString() + ' PH/s' + + totalEstimatedHashrateOther.toString() + ' ' + miningStats.miningUnits.hashrateUnit + `
` + $localize`${ i }:INTERPOLATION: blocks`; } else { return `` + $localize`Other (${percentage})` + `
` + diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 589b48869..60797a9a1 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -26,6 +26,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi import { Price, PriceService } from '../../services/price.service'; import { isFeatureActive } from '../../bitcoin.utils'; import { ServicesApiServices } from '../../services/services-api.service'; +import { EnterpriseService } from '../../services/enterprise.service'; @Component({ selector: 'app-transaction', @@ -116,12 +117,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { private servicesApiService: ServicesApiServices, private seoService: SeoService, private priceService: PriceService, - private storageService: StorageService + private storageService: StorageService, + private enterpriseService: EnterpriseService, ) {} ngOnInit() { this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; + this.enterpriseService.page(); + this.websocketService.want(['blocks', 'mempool-blocks']); this.stateService.networkChanged$.subscribe( (network) => { @@ -527,6 +531,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { if (!this.txId) { return; } + this.enterpriseService.goal(8); this.showAccelerationSummary = true && this.acceleratorAvailable; this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview; return false; diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts index 5e655b584..bb9e21c4b 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts @@ -88,7 +88,7 @@ export class NodesMap implements OnInit, OnChanges { node.public_key, node.alias, node.capacity, - node.active_channel_count, + node.channels, node.country, node.iso_code, ]); diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html index 9318e925b..be7737894 100644 --- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html +++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html @@ -64,8 +64,8 @@ Channels Location - - + + {{ node.alias }} @@ -116,5 +116,10 @@ + + +
diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.scss b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.scss index 25e4cf7f3..97d42298c 100644 --- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.scss +++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.scss @@ -22,14 +22,14 @@ .timestamp-first { width: 20%; - @media (max-width: 576px) { + @media (max-width: 1060px) { display: none } } .timestamp-update { width: 16%; - @media (max-width: 576px) { + @media (max-width: 1060px) { display: none } } @@ -50,7 +50,7 @@ .city { max-width: 150px; - @media (max-width: 576px) { + @media (max-width: 675px) { display: none } } \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts index 19dd999ee..4035b62d4 100644 --- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts +++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { map, Observable, share } from 'rxjs'; +import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs'; import { ApiService } from '../../services/api.service'; import { SeoService } from '../../services/seo.service'; import { getFlagEmoji } from '../../shared/common.utils'; @@ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation export class NodesPerCountry implements OnInit { nodes$: Observable; country: {name: string, flag: string}; + nodesPagination$: Observable; + startingIndexSubject: BehaviorSubject = new BehaviorSubject(0); + page = 1; + pageSize = 15; + maxSize = window.innerWidth <= 767.98 ? 3 : 5; + isLoading = true; skeletonLines: number[] = []; @@ -23,7 +29,7 @@ export class NodesPerCountry implements OnInit { private seoService: SeoService, private route: ActivatedRoute, ) { - for (let i = 0; i < 20; ++i) { + for (let i = 0; i < this.pageSize; ++i) { this.skeletonLines.push(i); } } @@ -31,6 +37,7 @@ export class NodesPerCountry implements OnInit { ngOnInit(): void { this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country) .pipe( + tap(() => this.isLoading = true), map(response => { this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`); this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-country:Explore all the Lightning nodes hosted in ${response.country.en} and see an overview of each node's capacity, number of open channels, and more.`); @@ -87,11 +94,21 @@ export class NodesPerCountry implements OnInit { ispCount: Object.keys(isps).length }; }), + tap(() => this.isLoading = false), share() ); + + this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe( + map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize)) + ); } trackByPublicKey(index: number, node: any): string { return node.public_key; } + + pageChange(page: number): void { + this.startingIndexSubject.next((page - 1) * this.pageSize); + this.page = page; + } } diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html index 3daafe4db..865d2d2dd 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html @@ -61,8 +61,8 @@ Channels Location - - + + {{ node.alias }} @@ -113,5 +113,10 @@ + + + diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss index b829c5b59..b043d36f8 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss @@ -24,7 +24,7 @@ .timestamp-first { width: 20%; - @media (max-width: 576px) { + @media (max-width: 1060px) { display: none } } @@ -32,7 +32,7 @@ .timestamp-update { width: 16%; - @media (max-width: 576px) { + @media (max-width: 1060px) { display: none } } @@ -56,7 +56,7 @@ .city { max-width: 150px; - @media (max-width: 576px) { + @media (max-width: 675px) { display: none } } diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts index d4f27975c..f6c61a9f6 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { map, Observable, share } from 'rxjs'; +import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs'; import { ApiService } from '../../services/api.service'; import { SeoService } from '../../services/seo.service'; import { getFlagEmoji } from '../../shared/common.utils'; @@ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation export class NodesPerISP implements OnInit { nodes$: Observable; isp: {name: string, id: number}; + nodesPagination$: Observable; + startingIndexSubject: BehaviorSubject = new BehaviorSubject(0); + page = 1; + pageSize = 15; + maxSize = window.innerWidth <= 767.98 ? 3 : 5; + isLoading = true; skeletonLines: number[] = []; @@ -23,7 +29,7 @@ export class NodesPerISP implements OnInit { private seoService: SeoService, private route: ActivatedRoute, ) { - for (let i = 0; i < 20; ++i) { + for (let i = 0; i < this.pageSize; ++i) { this.skeletonLines.push(i); } } @@ -31,6 +37,7 @@ export class NodesPerISP implements OnInit { ngOnInit(): void { this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp) .pipe( + tap(() => this.isLoading = true), map(response => { this.isp = { name: response.isp, @@ -77,11 +84,21 @@ export class NodesPerISP implements OnInit { topCountry: topCountry, }; }), + tap(() => this.isLoading = false), share() ); + + this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe( + map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize)) + ); } trackByPublicKey(index: number, node: any): string { return node.public_key; } + + pageChange(page: number): void { + this.startingIndexSubject.next((page - 1) * this.pageSize); + this.page = page; + } } diff --git a/frontend/src/app/services/enterprise.service.ts b/frontend/src/app/services/enterprise.service.ts index 7e69af223..d1e3624f9 100644 --- a/frontend/src/app/services/enterprise.service.ts +++ b/frontend/src/app/services/enterprise.service.ts @@ -139,6 +139,14 @@ export class EnterpriseService { this.getMatomo()?.trackGoal(id); } + page() { + const matomo = this.getMatomo(); + if (matomo) { + matomo.setCustomUrl(this.getCustomUrl()); + matomo.trackPageView(); + } + } + private getCustomUrl(): string { let url = window.location.origin + '/'; let route = this.activatedRoute; diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index 47b5d9835..271279589 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -77,6 +77,7 @@

Terms of Service

Privacy Policy

Trademark Policy

+

Third-party Licenses