diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 260efcafe..69c78fc83 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -74,12 +74,14 @@ let routes: Routes = [ children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', component: StartComponent, + data: { networkSpecific: true }, children: [ { path: ':id', @@ -90,6 +92,7 @@ let routes: Routes = [ { path: 'block', component: StartComponent, + data: { networkSpecific: true }, children: [ { path: ':id', @@ -102,6 +105,7 @@ let routes: Routes = [ }, { path: 'block-audit', + data: { networkSpecific: true }, children: [ { path: ':id', @@ -121,12 +125,13 @@ let routes: Routes = [ { path: 'lightning', loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule), - data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true }, + data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] }, }, ], }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -185,11 +190,13 @@ let routes: Routes = [ children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -200,6 +207,7 @@ let routes: Routes = [ }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -213,6 +221,7 @@ let routes: Routes = [ }, { path: 'block-audit', + data: { networkSpecific: true }, children: [ { path: ':id', @@ -230,12 +239,14 @@ let routes: Routes = [ }, { path: 'lightning', + data: { networks: ['bitcoin'] }, loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) }, ], }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -291,11 +302,13 @@ let routes: Routes = [ children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -306,6 +319,7 @@ let routes: Routes = [ }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -319,6 +333,7 @@ let routes: Routes = [ }, { path: 'block-audit', + data: { networkSpecific: true }, children: [ { path: ':id', @@ -336,6 +351,7 @@ let routes: Routes = [ }, { path: 'lightning', + data: { networks: ['bitcoin'] }, loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) }, ], @@ -359,6 +375,7 @@ let routes: Routes = [ }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -422,11 +439,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -437,6 +456,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -450,18 +470,22 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'assets', + data: { networks: ['liquid'] }, component: AssetsNavComponent, children: [ { path: 'all', + data: { networks: ['liquid'] }, component: AssetsComponent, }, { path: 'asset/:id', + data: { networkSpecific: true }, component: AssetComponent }, { path: 'group/:id', + data: { networkSpecific: true }, component: AssetGroupComponent }, { @@ -482,6 +506,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -532,11 +557,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -547,6 +574,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -560,22 +588,27 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'assets', + data: { networks: ['liquid'] }, component: AssetsNavComponent, children: [ { path: 'featured', + data: { networkSpecific: true }, component: AssetsFeaturedComponent, }, { path: 'all', + data: { networks: ['liquid'] }, component: AssetsComponent, }, { path: 'asset/:id', + data: { networkSpecific: true }, component: AssetComponent }, { path: 'group/:id', + data: { networkSpecific: true }, component: AssetGroupComponent }, { @@ -609,6 +642,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'status', + data: { networks: ['bitcoin', 'liquid']}, component: StatusViewComponent }, { diff --git a/frontend/src/app/bisq/bisq.routing.module.ts b/frontend/src/app/bisq/bisq.routing.module.ts index f7385ae63..11acdca2a 100644 --- a/frontend/src/app/bisq/bisq.routing.module.ts +++ b/frontend/src/app/bisq/bisq.routing.module.ts @@ -20,14 +20,17 @@ const routes: Routes = [ }, { path: 'markets', + data: { networks: ['bisq'] }, component: BisqDashboardComponent, }, { path: 'transactions', + data: { networks: ['bisq'] }, component: BisqTransactionsComponent }, { path: 'market/:pair', + data: { networkSpecific: true }, component: BisqMarketComponent, }, { @@ -36,6 +39,7 @@ const routes: Routes = [ }, { path: 'tx/:id', + data: { networkSpecific: true }, component: BisqTransactionComponent }, { @@ -45,14 +49,17 @@ const routes: Routes = [ }, { path: 'block/:id', + data: { networkSpecific: true }, component: BisqBlockComponent, }, { path: 'address/:id', + data: { networkSpecific: true }, component: BisqAddressComponent, }, { path: 'stats', + data: { networks: ['bisq'] }, component: BisqStatsComponent, }, { diff --git a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.html b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.html index d07f9d60c..2054f1a5d 100644 --- a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.html +++ b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.html @@ -44,13 +44,13 @@
- Mainnet - Signet - Testnet + Mainnet + Signet + Testnet - Bisq - Liquid - Liquid Testnet + Bisq + Liquid + Liquid Testnet
diff --git a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts index d69b37d3d..941d9e21e 100644 --- a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts +++ b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts @@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service'; import { Observable } from 'rxjs'; import { LanguageService } from '../../services/language.service'; import { EnterpriseService } from '../../services/enterprise.service'; +import { NavigationService } from '../../services/navigation.service'; @Component({ selector: 'app-bisq-master-page', @@ -15,17 +16,23 @@ export class BisqMasterPageComponent implements OnInit { env: Env; isMobile = window.innerWidth <= 767.98; urlLanguage: string; + networkPaths: { [network: string]: string }; constructor( private stateService: StateService, private languageService: LanguageService, private enterpriseService: EnterpriseService, + private navigationService: NavigationService, ) { } ngOnInit() { this.env = this.stateService.env; this.connectionState$ = this.stateService.connectionState$; this.urlLanguage = this.languageService.getLanguageForUrl(); + this.navigationService.subnetPaths.subscribe((paths) => { + console.log('network paths updated...'); + this.networkPaths = paths; + }); } collapse(): void { diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html index 7f22fd465..17f371202 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html @@ -49,13 +49,13 @@
- Mainnet - Signet - Testnet + Mainnet + Signet + Testnet - Bisq - Liquid - Liquid Testnet + Bisq + Liquid + Liquid Testnet
diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts index d78cd457e..c57673529 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts @@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service'; import { merge, Observable, of} from 'rxjs'; import { LanguageService } from '../../services/language.service'; import { EnterpriseService } from '../../services/enterprise.service'; +import { NavigationService } from '../../services/navigation.service'; @Component({ selector: 'app-liquid-master-page', @@ -17,11 +18,13 @@ export class LiquidMasterPageComponent implements OnInit { officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; network$: Observable; urlLanguage: string; + networkPaths: { [network: string]: string }; constructor( private stateService: StateService, private languageService: LanguageService, private enterpriseService: EnterpriseService, + private navigationService: NavigationService, ) { } ngOnInit() { @@ -29,6 +32,10 @@ export class LiquidMasterPageComponent implements OnInit { this.connectionState$ = this.stateService.connectionState$; this.network$ = merge(of(''), this.stateService.networkChanged$); this.urlLanguage = this.languageService.getLanguageForUrl(); + this.navigationService.subnetPaths.subscribe((paths) => { + console.log('network paths updated...'); + this.networkPaths = paths; + }); } collapse(): void { diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 1c28d5dce..5c365f0f9 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -22,13 +22,13 @@
- Mainnet - Signet - Testnet + Mainnet + Signet + Testnet - Bisq - Liquid - Liquid Testnet + Bisq + Liquid + Liquid Testnet
diff --git a/frontend/src/app/components/master-page/master-page.component.ts b/frontend/src/app/components/master-page/master-page.component.ts index 994f0412f..8f7b4fecc 100644 --- a/frontend/src/app/components/master-page/master-page.component.ts +++ b/frontend/src/app/components/master-page/master-page.component.ts @@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service'; import { Observable, merge, of } from 'rxjs'; import { LanguageService } from '../../services/language.service'; import { EnterpriseService } from '../../services/enterprise.service'; +import { NavigationService } from '../../services/navigation.service'; @Component({ selector: 'app-master-page', @@ -18,11 +19,13 @@ export class MasterPageComponent implements OnInit { officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; urlLanguage: string; subdomain = ''; + networkPaths: { [network: string]: string }; constructor( public stateService: StateService, private languageService: LanguageService, private enterpriseService: EnterpriseService, + private navigationService: NavigationService, ) { } ngOnInit() { @@ -31,6 +34,10 @@ export class MasterPageComponent implements OnInit { this.network$ = merge(of(''), this.stateService.networkChanged$); this.urlLanguage = this.languageService.getLanguageForUrl(); this.subdomain = this.enterpriseService.getSubdomain(); + this.navigationService.subnetPaths.subscribe((paths) => { + console.log('network paths updated...'); + this.networkPaths = paths; + }); } collapse(): void { diff --git a/frontend/src/app/docs/docs.routing.module.ts b/frontend/src/app/docs/docs.routing.module.ts index 52eb54f2e..0f01016de 100644 --- a/frontend/src/app/docs/docs.routing.module.ts +++ b/frontend/src/app/docs/docs.routing.module.ts @@ -39,6 +39,7 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' || }, { path: 'faq', + data: { networks: ['bitcoin'] }, component: DocsComponent }, { diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts index 57ef6cef7..bf0e0a0a7 100644 --- a/frontend/src/app/graphs/graphs.routing.module.ts +++ b/frontend/src/app/graphs/graphs.routing.module.ts @@ -37,10 +37,12 @@ const routes: Routes = [ children: [ { path: 'mining/pool/:slug', + data: { networks: ['bitcoin'] }, component: PoolComponent, }, { path: 'mining', + data: { networks: ['bitcoin'] }, component: StartComponent, children: [ { @@ -51,6 +53,7 @@ const routes: Routes = [ }, { path: 'mempool-block/:id', + data: { networks: ['bitcoin', 'liquid'] }, component: StartComponent, children: [ { @@ -61,62 +64,77 @@ const routes: Routes = [ }, { path: 'graphs', + data: { networks: ['bitcoin', 'liquid'] }, component: GraphsComponent, children: [ { path: 'mempool', + data: { networks: ['bitcoin', 'liquid'] }, component: StatisticsComponent, }, { path: 'mining/hashrate-difficulty', + data: { networks: ['bitcoin'] }, component: HashrateChartComponent, }, { path: 'mining/pools-dominance', + data: { networks: ['bitcoin'] }, component: HashrateChartPoolsComponent, }, { path: 'mining/pools', + data: { networks: ['bitcoin'] }, component: PoolRankingComponent, }, { path: 'mining/block-fees', + data: { networks: ['bitcoin'] }, component: BlockFeesGraphComponent, }, { path: 'mining/block-rewards', + data: { networks: ['bitcoin'] }, component: BlockRewardsGraphComponent, }, { path: 'mining/block-fee-rates', + data: { networks: ['bitcoin'] }, component: BlockFeeRatesGraphComponent, }, { path: 'mining/block-sizes-weights', + data: { networks: ['bitcoin'] }, component: BlockSizesWeightsGraphComponent, }, { path: 'lightning/nodes-networks', + data: { networks: ['bitcoin'] }, component: NodesNetworksChartComponent, }, { path: 'lightning/capacity', + data: { networks: ['bitcoin'] }, component: LightningStatisticsChartComponent, }, { path: 'lightning/nodes-per-isp', + data: { networks: ['bitcoin'] }, component: NodesPerISPChartComponent, }, { path: 'lightning/nodes-per-country', + data: { networks: ['bitcoin'] }, component: NodesPerCountryChartComponent, }, { path: 'lightning/nodes-map', + data: { networks: ['bitcoin'] }, component: NodesMap, }, { path: 'lightning/nodes-channels-map', + data: { networks: ['bitcoin'] }, component: NodesChannelsMap, }, { @@ -125,6 +143,7 @@ const routes: Routes = [ }, { path: 'mining/block-prediction', + data: { networks: ['bitcoin'] }, component: BlockPredictionGraphComponent, }, ] @@ -141,6 +160,7 @@ const routes: Routes = [ }, { path: 'tv', + data: { networks: ['bitcoin', 'liquid'] }, component: TelevisionComponent }, ]; diff --git a/frontend/src/app/lightning/lightning.routing.module.ts b/frontend/src/app/lightning/lightning.routing.module.ts index e2121f8e8..79c3bc297 100644 --- a/frontend/src/app/lightning/lightning.routing.module.ts +++ b/frontend/src/app/lightning/lightning.routing.module.ts @@ -21,10 +21,12 @@ const routes: Routes = [ }, { path: 'node/:public_key', + data: { networkSpecific: true }, component: NodeComponent, }, { path: 'channel/:short_id', + data: { networkSpecific: true }, component: ChannelComponent, }, { diff --git a/frontend/src/app/services/navigation.service.ts b/frontend/src/app/services/navigation.service.ts new file mode 100644 index 000000000..661a8c38f --- /dev/null +++ b/frontend/src/app/services/navigation.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@angular/core'; +import { Router, ActivatedRoute, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router'; +import { BehaviorSubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StateService } from './state.service'; + +const networkModules = { + bitcoin: { + subnets: [ + { name: 'mainnet', path: '' }, + { name: 'testnet', path: '/testnet' }, + { name: 'signet', path: '/signet' }, + ], + }, + liquid: { + subnets: [ + { name: 'liquid', path: '' }, + { name: 'liquidtestnet', path: '/testnet' }, + ], + }, + bisq: { + subnets: [ + { name: 'bisq', path: '' }, + ], + }, +}; +const networks = Object.keys(networkModules); + +@Injectable({ + providedIn: 'root' +}) +export class NavigationService { + subnetPaths = new BehaviorSubject>({}); + + constructor( + private stateService: StateService, + private router: Router, + ) { + this.router.events.pipe( + filter(event => event instanceof NavigationEnd), + map(() => this.router.routerState.snapshot.root), + ).subscribe((state) => { + this.updateSubnetPaths(state); + }); + } + + // For each network (bitcoin/liquid/bisq), find and save the longest url path compatible with the current route + updateSubnetPaths(root: ActivatedRouteSnapshot): void { + let path = ''; + const networkPaths = {}; + let route = root; + // traverse the router state tree until all network paths are set, or we reach the end of the tree + while (!networks.reduce((acc, network) => acc && !!networkPaths[network], true) && route) { + // 'networkSpecific' paths may correspond to valid routes on other networks, but aren't directly compatible + // (e.g. we shouldn't link a mainnet transaction page to the same txid on testnet or liquid) + if (route.data?.networkSpecific) { + networks.forEach(network => { + if (networkPaths[network] == null) { + networkPaths[network] = path; + } + }); + } + // null or empty networks list is shorthand for "compatible with every network" + if (route.data?.networks?.length) { + // if the list is non-empty, only those networks are compatible + networks.forEach(network => { + if (!route.data.networks.includes(network)) { + if (networkPaths[network] == null) { + networkPaths[network] = path; + } + } + }); + } + if (route.url?.length) { + path = [path, ...route.url.map(segment => segment.path).filter(path => { + return path.length && !['testnet', 'signet'].includes(path); + })].join('/'); + } + route = route.firstChild; + } + + const subnetPaths = {}; + Object.entries(networkModules).forEach(([key, network]) => { + network.subnets.forEach(subnet => { + subnetPaths[subnet.name] = subnet.path + (networkPaths[key] != null ? networkPaths[key] : path); + }); + }); + this.subnetPaths.next(subnetPaths); + } +}