Merge pull request #4321 from mempool/mononaut/ssr

Angular Universal SSR
This commit is contained in:
softsimon 2024-03-22 20:01:30 +09:00 committed by GitHub
commit b4d0b75557
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
94 changed files with 3390 additions and 300 deletions

View file

@ -280,6 +280,56 @@
}
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/mempool/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json"
},
"configurations": {
"production": {
"outputHashing": "media",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"sourceMap": false,
"localize": true,
"optimization": false
}
}
},
"serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server",
"options": {
"browserTarget": "mempool:build",
"serverTarget": "mempool:server"
},
"configurations": {
"production": {
"browserTarget": "mempool:build:production",
"serverTarget": "mempool:server:production",
"optimization": false,
"sourceMap": true
}
}
},
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"browserTarget": "mempool:build:production",
"serverTarget": "mempool:server:production",
"routes": [
"/"
]
},
"configurations": {
"production": {}
}
},
"cypress-run": {
"builder": "@cypress/schematic:cypress",
"options": {

View file

@ -71,7 +71,7 @@ const newConfig = `(function (window) {
window.__env.${obj.key} = ${typeof obj.value === 'string' ? `'${obj.value}'` : obj.value};`, '')}
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
}(this));`;
}((typeof global !== 'undefined') ? global : this));`;
const newConfigTemplate = `(function (window) {
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}

File diff suppressed because it is too large Load diff

View file

@ -48,6 +48,9 @@
"prettier": "prettier --write \"src/app/**/*.{js,json,css,scss,less,md,ts,html,component.html}\"",
"e2e": "npm run generate-config && npm run ng -- e2e",
"e2e:ci": "npm run cypress:run:ci",
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
"serve:ssr": "npm run generate-config && node server.run.js",
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
@ -61,18 +64,18 @@
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^16.2.0",
"@angular/animations": "^16.2.2",
"@angular/cli": "^16.2.0",
"@angular/common": "^16.2.2",
"@angular/compiler": "^16.2.2",
"@angular/core": "^16.2.2",
"@angular/forms": "^16.2.2",
"@angular/localize": "^16.2.2",
"@angular/platform-browser": "^16.2.2",
"@angular/platform-browser-dynamic": "^16.2.2",
"@angular/platform-server": "^16.2.2",
"@angular/router": "^16.2.2",
"@angular-devkit/build-angular": "^16.1.1",
"@angular/animations": "^16.1.1",
"@angular/cli": "^16.1.1",
"@angular/common": "^16.1.1",
"@angular/compiler": "^16.1.1",
"@angular/core": "^16.1.1",
"@angular/forms": "^16.1.1",
"@angular/localize": "^16.1.1",
"@angular/platform-browser": "^16.1.1",
"@angular/platform-browser-dynamic": "^16.1.1",
"@angular/platform-server": "^16.1.1",
"@angular/router": "^16.1.1",
"@fortawesome/angular-fontawesome": "~0.13.0",
"@fortawesome/fontawesome-common-types": "~6.5.1",
"@fortawesome/fontawesome-svg-core": "~6.5.1",
@ -96,14 +99,17 @@
"zone.js": "~0.13.1"
},
"devDependencies": {
"@angular/compiler-cli": "^16.1.5",
"@angular/language-service": "^16.1.5",
"@angular/compiler-cli": "^16.1.1",
"@angular/language-service": "^16.1.1",
"@nguniversal/builders": "16.1.1",
"@nguniversal/express-engine": "16.1.1",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"eslint": "^8.31.0",
"http-proxy-middleware": "~2.0.6",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"ts-node": "~10.9.1",
"typescript": "~4.9.3"
},

105
frontend/server.run.ts Normal file
View file

@ -0,0 +1,105 @@
import 'zone.js/dist/zone-node';
import './src/resources/config.js';
import * as domino from 'domino';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
const {readFileSync, existsSync} = require('fs');
const {createProxyMiddleware} = require('http-proxy-middleware');
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
const win = domino.createWindow(template);
// @ts-ignore
win.__env = global.__env;
// @ts-ignore
win.matchMedia = () => {
return {
matches: true
};
};
// @ts-ignore
win.setTimeout = (fn) => { fn(); };
win.document.body.scrollTo = (() => {});
// @ts-ignore
global['window'] = win;
global['document'] = win.document;
// @ts-ignore
global['history'] = { state: { } };
global['localStorage'] = {
getItem: () => '',
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => '',
};
/**
* Return the list of supported and actually active locales
*/
function getActiveLocales() {
const angularConfig = JSON.parse(readFileSync('angular.json', 'utf8'));
const supportedLocales = [
angularConfig.projects.mempool.i18n.sourceLocale,
...Object.keys(angularConfig.projects.mempool.i18n.locales),
];
return supportedLocales.filter(locale => locale === 'en-US' && existsSync(`./dist/mempool/server/${locale}`));
// return supportedLocales.filter(locale => existsSync(`./dist/mempool/server/${locale}`));
}
function app() {
const server = express();
// proxy websocket
server.get('/api/v1/ws', createProxyMiddleware({
target: 'ws://localhost:4200/api/v1/ws',
changeOrigin: true,
ws: true,
logLevel: 'debug'
}));
// proxy API to nginx
server.get('/api/**', createProxyMiddleware({
// @ts-ignore
target: win.__env.NGINX_PROTOCOL + '://' + win.__env.NGINX_HOSTNAME + ':' + win.__env.NGINX_PORT,
changeOrigin: true,
}));
server.get('/resources/**', express.static('./src'));
// map / and /en to en-US
const defaultLocale = 'en-US';
console.log(`serving default locale: ${defaultLocale}`);
const appServerModule = require(`./dist/mempool/server/${defaultLocale}/main.js`);
server.use('/', appServerModule.app(defaultLocale));
server.use('/en', appServerModule.app(defaultLocale));
// map each locale to its localized main.js
getActiveLocales().forEach(locale => {
console.log('serving locale:', locale);
const appServerModule = require(`./dist/mempool/server/${locale}/main.js`);
// map everything to itself
server.use(`/${locale}`, appServerModule.app(locale));
});
return server;
}
function run() {
const port = process.env.PORT || 4000;
// Start up the Node server
app().listen(port, () => {
console.log(`Node Express server listening on port ${port}`);
});
}
run();

112
frontend/server.ts Normal file
View file

@ -0,0 +1,112 @@
import 'zone.js/dist/zone-node';
import './src/resources/config.js';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
import * as domino from 'domino';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';
import { ResizeObserver } from './shims';
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
const win = domino.createWindow(template);
// @ts-ignore
win.__env = global.__env;
// @ts-ignore
win.matchMedia = (media) => {
return {
media,
matches: true,
};
};
// @ts-ignore
win.setTimeout = (fn) => { fn(); };
win.document.body.scrollTo = (() => {});
win['ResizeObserver'] = ResizeObserver;
// @ts-ignore
global['window'] = win;
// @ts-ignore
global['document'] = win.document;
// @ts-ignore
global['history'] = { state: { } };
// @ts-ignore
Object.defineProperty(global, 'navigator', {
value: win.navigator,
writable: true
});
global['localStorage'] = {
getItem: () => '',
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => '',
};
// The Express app is exported so that it can be used by serverless Functions.
export function app(locale: string): express.Express {
const server = express();
const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`);
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// static file handler so we send HTTP 404 to nginx
server.get('/**.(css|js|json|ico|webmanifest|png|jpg|jpeg|svg|mp4)*', express.static(distFolder, { maxAge: '1y', fallthrough: false }));
// handle page routes
server.get('/**', getLocalizedSSR(indexHtml));
return server;
}
function getLocalizedSSR(indexHtml) {
return (req, res) => {
res.render(indexHtml, {
req,
providers: [
{ provide: APP_BASE_HREF, useValue: req.baseUrl }
]
});
}
}
// only used for development mode
function run(): void {
const port = process.env.PORT || 4000;
// Start up the Node server
const server = app('en-US');
server.listen(port, () => {
console.log(`Node Express server listening on port ${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';

7
frontend/shims.ts Normal file
View file

@ -0,0 +1,7 @@
export class ResizeObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
}

View file

@ -2,6 +2,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ZONE_SERVICE } from './injection-tokens';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './components/app/app.component';
import { ElectrsApiService } from './services/electrs-api.service';
@ -13,6 +14,7 @@ import { WebsocketService } from './services/websocket.service';
import { AudioService } from './services/audio.service';
import { SeoService } from './services/seo.service';
import { OpenGraphService } from './services/opengraph.service';
import { ZoneService } from './services/zone-shim.service';
import { SharedModule } from './shared/shared.module';
import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
@ -42,7 +44,8 @@ const providers = [
CapAddressPipe,
AppPreloadingStrategy,
ServicesApiServices,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true },
{ provide: ZONE_SERVICE, useClass: ZoneService },
];
@NgModule({

View file

@ -1,20 +1,23 @@
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { ServerModule } from '@angular/platform-server';
import { ZONE_SERVICE } from './injection-tokens';
import { AppModule } from './app.module';
import { AppComponent } from './components/app/app.component';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { ZoneService } from './services/zone.service';
@NgModule({
imports: [
AppModule,
ServerModule,
ServerTransferStateModule,
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true },
{ provide: ZONE_SERVICE, useClass: ZoneService },
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
export class AppServerModule {}

View file

@ -45,10 +45,10 @@
</form>
</div>
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View file

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
import { EChartsOption } from 'echarts';
import { EChartsOption } from '../../../graphs/echarts';
import { Observable, Subscription, combineLatest, fromEvent, share } from 'rxjs';
import { startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '../../../services/seo.service';
@ -11,6 +11,7 @@ import { MiningService } from '../../../services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
import { StateService } from '../../../services/state.service';
@Component({
selector: 'app-acceleration-fees-graph',
@ -59,6 +60,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
private storageService: StorageService,
private miningService: MiningService,
private route: ActivatedRoute,
public stateService: StateService,
private cd: ChangeDetectorRef,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
@ -176,10 +178,10 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
padding: [10, 0, 0, 0],
},
type: 'time',
boundaryGap: false,
boundaryGap: [0, 0],
axisLine: { onZero: true },
axisLabel: {
formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)),
formatter: (val): string => formatterXAxisTimeCategory(this.locale, this.timespan, val),
align: 'center',
fontSize: 11,
lineHeight: 12,

View file

@ -42,7 +42,7 @@
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<div class="mempool-block-wrapper">
<div class="mempool-block-wrapper" *ngIf="webGlEnabled">
<app-mempool-block-overview [index]="0" [overrideColors]="getAcceleratorColor"></app-mempool-block-overview>
</div>
</div>
@ -66,16 +66,6 @@
</div>
</div>
<!-- Avg block fees graph -->
<!-- <div class="col" style="margin-bottom: 1.47rem">
<div class="card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<app-block-fee-rates-graph [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-block-fee-rates-graph>
<div class="mt-1"><a [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" fragment="1m" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div> -->
<!-- Recent Accelerations List -->
<div class="col">
<div class="card list-card">

View file

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, PLATFORM_ID } from '@angular/core';
import { SeoService } from '../../../services/seo.service';
import { OpenGraphService } from '../../../services/opengraph.service';
import { WebsocketService } from '../../../services/websocket.service';
@ -10,6 +10,7 @@ import { hexToColor } from '../../block-overview-graph/utils';
import TxView from '../../block-overview-graph/tx-view';
import { feeLevels, mempoolFeeColors } from '../../../app.constants';
import { ServicesApiServices } from '../../../services/services-api.service';
import { detectWebGL } from '../../../shared/graphs.utils';
const acceleratedColor: Color = hexToColor('8F5FF6');
const normalColors = mempoolFeeColors.map(hex => hexToColor(hex + '5F'));
@ -30,6 +31,7 @@ export class AcceleratorDashboardComponent implements OnInit {
pendingAccelerations$: Observable<Acceleration[]>;
minedAccelerations$: Observable<Acceleration[]>;
loadingBlocks: boolean = true;
webGlEnabled = true;
graphHeight: number = 300;
@ -39,7 +41,9 @@ export class AcceleratorDashboardComponent implements OnInit {
private websocketService: WebsocketService,
private serviceApiServices: ServicesApiServices,
private stateService: StateService,
@Inject(PLATFORM_ID) private platformId: Object,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Accelerator Dashboard`);
this.ogService.setManualOgImage('accelerator.jpg');
}
@ -48,7 +52,7 @@ export class AcceleratorDashboardComponent implements OnInit {
this.onResize();
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
this.pendingAccelerations$ = interval(30000).pipe(
this.pendingAccelerations$ = (this.stateService.isBrowser ? interval(30000) : of(null)).pipe(
startWith(true),
switchMap(() => {
return this.serviceApiServices.getAccelerations$().pipe(

View file

@ -8,7 +8,7 @@
</div>
<ng-container *ngIf="!error">
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">

View file

@ -62,10 +62,10 @@
</div>
</div>
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { Observable, combineLatest, of } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
@ -55,7 +55,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
private miningService: MiningService,
private stateService: StateService,
public stateService: StateService,
private router: Router,
private zone: NgZone,
private route: ActivatedRoute,
@ -209,7 +209,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
prepareChartOptions(data, weightMode) {
this.chartOptions = {
color: this.widget ? ['#6b6b6b', new graphic.LinearGradient(0, 0, 0, 0.65, [
color: this.widget ? ['#6b6b6b', new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
{ offset: 0, color: '#F4511E' },
{ offset: 0.25, color: '#FB8C00' },
{ offset: 0.5, color: '#FFB300' },
@ -282,7 +282,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
legend: (this.widget || data.series.length === 0) ? undefined : {
padding: [10, 75],
data: data.legends,
selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? {
selected: JSON.parse(this.storageService.getValue('fee_rates_legend') || 'null') ?? {
'Min': true,
'10th': true,
'25th': true,

View file

@ -36,10 +36,10 @@
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -12,6 +12,7 @@ import { MiningService } from '../../services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-block-fees-graph',
@ -54,6 +55,7 @@ export class BlockFeesGraphComponent implements OnInit {
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
private miningService: MiningService,
public stateService: StateService,
private route: ActivatedRoute,
private fiatShortenerPipe: FiatShortenerPipe,
private fiatCurrencyPipe: FiatCurrencyPipe,

View file

@ -45,10 +45,10 @@
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -52,7 +52,7 @@ export class BlockHealthGraphComponent implements OnInit {
private storageService: StorageService,
private zone: NgZone,
private route: ActivatedRoute,
private stateService: StateService,
public stateService: StateService,
private router: Router,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });

View file

@ -1,7 +1,7 @@
<div class="grid-align" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
<div class="block-overview-graph">
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
<canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>

View file

@ -83,9 +83,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
constructor(
readonly ngZone: NgZone,
readonly elRef: ElementRef,
private stateService: StateService,
public stateService: StateService,
) {
this.webGlEnabled = detectWebGL();
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
this.searchText = text;
@ -94,13 +94,15 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
ngAfterViewInit(): void {
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl');
if (this.canvas) {
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl');
if (this.gl) {
this.initCanvas();
this.resizeCanvas();
if (this.gl) {
this.initCanvas();
this.resizeCanvas();
}
}
}
@ -142,8 +144,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
cancelAnimationFrame(this.animationFrameRequest);
clearTimeout(this.animationHeartBeat);
}
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
if (this.canvas) {
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
}
}
clear(direction): void {
@ -209,6 +213,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
initCanvas(): void {
if (!this.canvas || !this.gl) {
return;
}
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
@ -262,24 +270,26 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
this.displayWidth = window.devicePixelRatio * this.cssWidth;
this.displayHeight = window.devicePixelRatio * this.cssHeight;
this.canvas.nativeElement.width = this.displayWidth;
this.canvas.nativeElement.height = this.displayHeight;
if (this.gl) {
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
}
if (this.scene) {
this.scene.resize({ width: this.displayWidth, height: this.displayHeight, animate: false });
this.start();
} else {
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,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
if (this.canvas) {
this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
this.displayWidth = window.devicePixelRatio * this.cssWidth;
this.displayHeight = window.devicePixelRatio * this.cssHeight;
this.canvas.nativeElement.width = this.displayWidth;
this.canvas.nativeElement.height = this.displayHeight;
if (this.gl) {
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
}
if (this.scene) {
this.scene.resize({ width: this.displayWidth, height: this.displayHeight, animate: false });
this.start();
} else {
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,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
colorFunction: this.getColorFunction() });
this.start();
this.start();
}
}
}
@ -406,6 +416,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@HostListener('pointerup', ['$event'])
onClick(event) {
if (!this.canvas) {
return;
}
if (event.target === this.canvas.nativeElement && event.pointerType === 'touch') {
this.setPreviewTx(event.offsetX, event.offsetY, true);
} else if (event.target === this.canvas.nativeElement) {
@ -417,6 +430,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
if (!this.canvas) {
return;
}
if (event.target === this.canvas.nativeElement) {
this.setPreviewTx(event.offsetX, event.offsetY, false);
} else {

View file

@ -37,10 +37,10 @@
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -12,6 +12,7 @@ import { StorageService } from '../../services/storage.service';
import { ActivatedRoute } from '@angular/router';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-block-rewards-graph',
@ -54,6 +55,7 @@ export class BlockRewardsGraphComponent implements OnInit {
private formBuilder: UntypedFormBuilder,
private miningService: MiningService,
private storageService: StorageService,
public stateService: StateService,
private route: ActivatedRoute,
private fiatShortenerPipe: FiatShortenerPipe,
private fiatCurrencyPipe: FiatCurrencyPipe,

View file

@ -44,10 +44,10 @@
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -10,6 +10,7 @@ import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { download, formatterXAxis } from '../../shared/graphs.utils';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-block-sizes-weights-graph',
@ -52,6 +53,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
private miningService: MiningService,
public stateService: StateService,
private route: ActivatedRoute,
) {
}

View file

@ -109,7 +109,7 @@
<div class="col-sm chart-container" *ngIf="webGlEnabled && !showAudit">
<app-block-overview-graph
#blockGraphActual
[isLoading]="isLoadingOverview"
[isLoading]="!stateService.isBrowser || isLoadingOverview"
[resolution]="86"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
@ -229,7 +229,7 @@
<div class="col-sm audit-col" [class.mobile]="isMobile">
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container></h3>
<div class="block-graph-wrapper">
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
<app-block-overview-graph #blockGraphProjected [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
@ -244,7 +244,7 @@
<div class="col-sm audit-col" *ngIf="!isMobile">
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
<div class="block-graph-wrapper">
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
<app-block-overview-graph #blockGraphActual [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>

View file

@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { Component, OnInit, OnDestroy, ViewChildren, QueryList, Inject, PLATFORM_ID, ChangeDetectorRef } from '@angular/core';
import { Location } from '@angular/common';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
@ -108,8 +108,10 @@ export class BlockComponent implements OnInit, OnDestroy {
private priceService: PriceService,
private cacheService: CacheService,
private servicesApiService: ServicesApiServices,
private cd: ChangeDetectorRef,
@Inject(PLATFORM_ID) private platformId: Object,
) {
this.webGlEnabled = detectWebGL();
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
}
ngOnInit() {
@ -318,6 +320,7 @@ export class BlockComponent implements OnInit, OnDestroy {
}
this.transactions = transactions;
this.isLoadingTransactions = false;
this.cd.markForCheck();
},
(error) => {
this.error = error;
@ -471,6 +474,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.isLoadingOverview = false;
this.setupBlockGraphs();
this.cd.markForCheck();
});
this.oobSubscription = block$.pipe(

View file

@ -33,7 +33,7 @@
<div *ngIf="indexingAvailable" class="tooltip-custom">
<a class="clear-link" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]">
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
onError="this.onerror=null; this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
<span class="pool-name">{{ block.extras.pool.name }}</span>
</a>
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>

View file

@ -1,6 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { Subscription, tap, timer } from 'rxjs';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
@Component({
@ -33,17 +32,20 @@ export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
}
ngOnInit(): void {
this.timeSubscription = timer(0, 250).pipe(
tap(() => {
this.updateTime();
})
).subscribe();
this.blocksSubscription = this.stateService.blocks$
.subscribe((blocks) => {
this.blockTimes = blocks.map(block => [block.height, new Date(block.timestamp * 1000)]);
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
this.updateSegments();
});
if (this.stateService.isBrowser) {
this.timeSubscription = timer(0, 250).pipe(
tap(() => {
console.log('face tick');
this.updateTime();
})
).subscribe();
this.blocksSubscription = this.stateService.blocks$
.subscribe((blocks) => {
this.blockTimes = blocks.map(block => [block.height, new Date(block.timestamp * 1000)]);
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
this.updateSegments();
});
}
}
ngOnChanges(): void {
@ -54,7 +56,9 @@ export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
}
ngOnDestroy(): void {
this.timeSubscription.unsubscribe();
if (this.timeSubscription) {
this.timeSubscription.unsubscribe();
}
}
updateTime(): void {

View file

@ -110,8 +110,8 @@ export class ClockComponent implements OnInit {
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
const windowWidth = this.limitWidth || window.innerWidth;
const windowHeight = this.limitHeight || window.innerHeight;
const windowWidth = this.limitWidth || window.innerWidth || 800;
const windowHeight = this.limitHeight || window.innerHeight || 800;
this.chainWidth = windowWidth;
this.chainHeight = Math.max(60, windowHeight / 8);
this.clockSize = Math.min(800, windowWidth, windowHeight - (1.4 * this.chainHeight));

View file

@ -95,7 +95,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
private apiService: ApiService,
private bytesPipe: BytesPipe,
) {
this.webGlEnabled = detectWebGL();
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
}
ngOnInit(): void {

View file

@ -1,5 +1,5 @@
<div class="fee-distribution-chart" *ngIf="mempoolVsizeFeesOptions; else loadingFees">
<div echarts [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>
<div class="fee-distribution-chart" *ngIf="mempoolVsizeFeesOptions && stateService.isBrowser; else loadingFees">
<div *browserOnly echarts [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>
</div>
<ng-template #loadingFees>

View file

@ -36,7 +36,7 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
};
constructor(
private stateService: StateService,
public stateService: StateService,
private vbytesPipe: VbytesPipe,
) { }

View file

@ -54,10 +54,10 @@
</form>
</div>
<div [class]="!widget ? 'chart' : 'chart-widget'" [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -60,7 +60,7 @@ export class HashrateChartComponent implements OnInit {
private storageService: StorageService,
private miningService: MiningService,
private route: ActivatedRoute,
private stateService: StateService
public stateService: StateService
) {
}
@ -326,7 +326,7 @@ export class HashrateChartComponent implements OnInit {
},
},
],
selected: JSON.parse(this.storageService.getValue('hashrate_difficulty_legend')) ?? {
selected: JSON.parse(this.storageService?.getValue('hashrate_difficulty_legend') || 'null') ?? {
'$localize`:@@79a9dc5b1caca3cbeb1733a19515edacc5fc7920:Hashrate`': true,
'$localize`::Difficulty`': this.network === '',
'$localize`Hashrate (MA)`': true,

View file

@ -31,10 +31,10 @@
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -10,6 +10,7 @@ import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service';
import { download } from '../../shared/graphs.utils';
import { ActivatedRoute } from '@angular/router';
import { StateService } from '../../services/state.service';
interface Hashrate {
timestamp: number;
@ -60,6 +61,7 @@ export class HashrateChartPoolsComponent implements OnInit {
private cd: ChangeDetectorRef,
private storageService: StorageService,
private miningService: MiningService,
public stateService: StateService,
private route: ActivatedRoute,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });

View file

@ -1,6 +1,6 @@
<div class="echarts" echarts [initOpts]="mempoolStatsChartInitOption" [options]="mempoolStatsChartOption" (chartRendered)="rendered()"
<div class="echarts" *browserOnly echarts [initOpts]="mempoolStatsChartInitOption" [options]="mempoolStatsChartOption" (chartRendered)="rendered()"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -48,7 +48,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
constructor(
@Inject(LOCALE_ID) private locale: string,
private storageService: StorageService,
private stateService: StateService,
public stateService: StateService,
) { }
ngOnInit() {

View file

@ -1,4 +1,4 @@
<div class="echarts" echarts [initOpts]="pegsChartInitOption" [options]="pegsChartOptions" (chartRendered)="rendered()"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="echarts" *browserOnly echarts [initOpts]="pegsChartInitOption" [options]="pegsChartOptions" (chartRendered)="rendered()"></div>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -1,6 +1,7 @@
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
import { formatDate, formatNumber } from '@angular/common';
import { EChartsOption } from '../../graphs/echarts';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-lbtc-pegs-graph',
@ -32,6 +33,7 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
};
constructor(
public stateService: StateService,
@Inject(LOCALE_ID) private locale: string,
) { }

View file

@ -1,4 +1,4 @@
<div class="echarts" echarts [initOpts]="ratioChartInitOptions" [options]="ratioChartOptions" (chartRendered)="rendered()"></div>
<div class="echarts" *browserOnly echarts [initOpts]="ratioChartInitOptions" [options]="ratioChartOptions" (chartRendered)="rendered()"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -1,6 +1,6 @@
<app-block-overview-graph
#blockGraph
[isLoading]="isLoading$ | async"
[isLoading]="(isLoading$ | async) || !stateService.isBrowser"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="timeLtr ? 'right' : 'left'"

View file

@ -1,4 +1,5 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Inject, PLATFORM_ID, ChangeDetectorRef } from '@angular/core';
import { detectWebGL } from '../../shared/graphs.utils';
import { StateService } from '../../services/state.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap, map, tap, filter } from 'rxjs/operators';
@ -29,8 +30,9 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
private seoService: SeoService,
private websocketService: WebsocketService,
private cd: ChangeDetectorRef,
@Inject(PLATFORM_ID) private platformId: Object,
) {
this.webGlEnabled = detectWebGL();
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
}
ngOnInit(): void {
@ -93,9 +95,3 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
this.previewTx = event;
}
}
function detectWebGL() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
return (gl && gl instanceof WebGLRenderingContext);
}

View file

@ -1,4 +1,4 @@
<div echarts class="echarts" (chartInit)="onChartReady($event)" (chartRendered)="rendered()" [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div *browserOnly echarts class="echarts" (chartInit)="onChartReady($event)" (chartRendered)="rendered()" [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -59,7 +59,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
private vbytesPipe: VbytesPipe,
private wubytesPipe: WuBytesPipe,
private amountShortenerPipe: AmountShortenerPipe,
private stateService: StateService,
public stateService: StateService,
private storageService: StorageService,
@Inject(LOCALE_ID) private locale: string,
) { }

View file

@ -76,11 +76,11 @@
</div>
<div [class]="!widget ? '' : 'pb-0'" class="container pb-lg-0">
<div [class]="widget ? 'chart-widget' : 'chart'" [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class]="widget ? 'chart-widget' : 'chart'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>
@ -102,7 +102,7 @@
<tr *ngFor="let pool of miningStats.pools">
<td class="d-none d-md-table-cell">{{ pool.rank }}</td>
<td class="text-right">
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.src = '/resources/mining-pools/default.svg'">
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.onerror=null; this.src = '/resources/mining-pools/default.svg'">
</td>
<td class="pool-name"><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
<td class="" *ngIf="this.miningWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{

View file

@ -41,7 +41,7 @@ export class PoolRankingComponent implements OnInit {
miningStatsObservable$: Observable<MiningStats>;
constructor(
private stateService: StateService,
public stateService: StateService,
private storageService: StorageService,
private formBuilder: UntypedFormBuilder,
private miningService: MiningService,

View file

@ -25,7 +25,7 @@
</div>
</div>
<div class="row hash-chart full-width-row">
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartFinished)="onChartReady()"></div>
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartFinished)="onChartReady()"></div>
</div>
</div>

View file

@ -6,7 +6,7 @@
<div *ngIf="poolStats$ | async as poolStats; else loadingMain">
<div style="display:flex" class="mb-3">
<img width="50" height="50" src="{{ poolStats['logo'] }}" [alt]="poolStats.pool.name + ' mining pool logo'"
onError="this.src = '/resources/mining-pools/default.svg'" class="mr-3">
onError="this.onerror=null; this.src = '/resources/mining-pools/default.svg'" class="mr-3">
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
</div>
@ -168,8 +168,8 @@
</ng-template>
<!-- Hashrate chart -->
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -35,4 +35,10 @@
</div>
</div>
<ng-container *serverOnly>
<!-- disgusting hack to apply an initial scroll to server-side rendered blockchain bar -->
<img *ngIf="!stateService.isLiquid()" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="pixel" style="visibility: hidden; position: absolute;" onload="(() => { const b = document.getElementById('blockchain-container'); const d = document.getElementById('divider'); if (b && d) { b.scrollLeft = d.getBoundingClientRect().x - (window.innerWidth * (window.innerWidth >= 768 ? 0.5 : 0.95)); }})()">
<img *ngIf="stateService.isLiquid()" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="pixel" style="visibility: hidden; position: absolute;" onload="(() => { const b = document.getElementById('blockchain-container'); const d = document.getElementById('divider'); if (b && d) { b.scrollLeft = d.getBoundingClientRect().x - (window.innerWidth >= 768 ? 420 : (window.innerWidth * 0.5)); }})()">
</ng-container>
<router-outlet></router-outlet>

View file

@ -59,7 +59,7 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
hasMenu = false;
constructor(
private stateService: StateService,
public stateService: StateService,
private cd: ChangeDetectorRef,
) {
this.isiOS = ['iPhone','iPod','iPad'].includes((navigator as any)?.userAgentData?.platform || navigator.platform);

View file

@ -1,4 +1,4 @@
import { Component, OnInit, AfterViewInit, OnDestroy, HostListener, ViewChild, ElementRef } from '@angular/core';
import { Component, OnInit, AfterViewInit, OnDestroy, HostListener, ViewChild, ElementRef, Inject, ChangeDetectorRef } from '@angular/core';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import {
@ -28,6 +28,7 @@ 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';
import { ZONE_SERVICE } from '../../injection-tokens';
interface Pool {
id: number;
@ -101,7 +102,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
inputIndex: number;
outputIndex: number;
graphExpanded: boolean = false;
graphWidth: number = 1000;
graphWidth: number = 1068;
graphHeight: number = 360;
inOutLimit: number = 150;
maxInOut: number = 0;
@ -141,6 +142,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private priceService: PriceService,
private storageService: StorageService,
private enterpriseService: EnterpriseService,
private cd: ChangeDetectorRef,
@Inject(ZONE_SERVICE) private zoneService: any,
) {}
ngOnInit() {
@ -356,7 +359,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
});
this.subscription = this.route.paramMap
this.subscription = this.zoneService.wrapObservable(this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
const urlMatch = (params.get('id') || '').split(':');
@ -430,7 +433,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
return of(tx);
})
)
))
.subscribe((tx: Transaction) => {
if (!tx) {
this.fetchCachedTx$.next(this.txId);
@ -503,6 +506,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
).subscribe();
setTimeout(() => { this.applyFragment(); }, 0);
this.cd.detectChanges();
},
(error) => {
this.error = error;
@ -785,9 +790,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
@HostListener('window:resize', ['$event'])
setGraphSize(): void {
this.isMobile = window.innerWidth < 850;
if (this.graphContainer?.nativeElement) {
if (this.graphContainer?.nativeElement && this.stateService.isBrowser) {
setTimeout(() => {
if (this.graphContainer?.nativeElement) {
if (this.graphContainer?.nativeElement?.clientWidth) {
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
} else {
setTimeout(() => { this.setGraphSize(); }, 1);

View file

@ -1,5 +1,13 @@
<div class="bowtie-graph">
<svg *ngIf="inputs && outputs" class="bowtie" [class.rtl]="dir === 'rtl'" [attr.height]="(height + 10) + 'px'" [attr.width]="width + 'px'">
<svg
*ngIf="inputs && outputs"
class="bowtie"
[class.rtl]="dir === 'rtl'"
[attr.height]="(height + 10) + 'px'"
[attr.width]="stateService.isBrowser ? (width + 'px') : '100%'"
[attr.viewBox]="stateService.isBrowser ? null : ('0 0 ' + width + ' ' + (height + 10))"
[attr.preserveAspectRatio]="stateService.isBrowser ? null : 'none'"
>
<defs>
<marker id="input-arrow" viewBox="-5 -5 10 10"
refX="0" refY="0"

View file

@ -101,7 +101,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
constructor(
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
private stateService: StateService,
public stateService: StateService,
private electrsApiService: ElectrsApiService,
private assetsService: AssetsService,
@Inject(LOCALE_ID) private locale: string,

View file

@ -1,7 +1,7 @@
<div class="container-xl dashboard-container" *ngIf="(network$ | async) !== 'liquid'; else liquidDashboard">
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
<ng-container *ngIf="(network$ | async) !== 'liquidtestnet'">
<ng-container>
<div class="col card-wrapper">
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
<div class="card">
@ -29,7 +29,7 @@
</label>
</div>
</div>
<div class="mempool-block-wrapper">
<div class="mempool-block-wrapper" *ngIf="webGlEnabled">
<app-mempool-block-overview
[index]="0"
[resolution]="goggleResolution"
@ -45,12 +45,12 @@
<div class="card-body">
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
<h5 class="card-title mt-3" i18n="dashboard.incoming-transactions">Incoming Transactions</h5>
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats">
<app-incoming-transactions-graph
[height]="incomingGraphHeight"
[left]="50"
[right]="20"
[data]="mempoolStats.value?.weightPerSecond"
[data]="mempoolStats?.weightPerSecond"
[windowPreferenceOverride]="'2h'"
></app-incoming-transactions-graph>
</div>

View file

@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs';
import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, FederationUtxo, OptimizedMempoolStats, PegsVolume, RecentPeg } from '../interfaces/node-api.interface';
@ -8,6 +8,7 @@ import { StateService } from '../services/state.service';
import { WebsocketService } from '../services/websocket.service';
import { SeoService } from '../services/seo.service';
import { ActiveFilter, FilterMode, toFlags } from '../shared/filters.utils';
import { detectWebGL } from '../shared/graphs.utils';
interface MempoolBlocksData {
blocks: number;
@ -67,6 +68,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
currency: string;
incomingGraphHeight: number = 300;
lbtcPegGraphHeight: number = 360;
webGlEnabled = true;
private lastPegBlockUpdate: number = 0;
private lastPegAmount: string = '';
private lastReservesBlockUpdate: number = 0;
@ -88,8 +90,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
public stateService: StateService,
private apiService: ApiService,
private websocketService: WebsocketService,
private seoService: SeoService
) { }
private seoService: SeoService,
@Inject(PLATFORM_ID) private platformId: Object,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
}
ngAfterViewInit(): void {
this.stateService.focusSearchInputDesktop();
@ -241,7 +246,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
return null;
}
}),
share(),
shareReplay(1),
);
if (this.stateService.network === 'liquid') {

View file

@ -0,0 +1,3 @@
import { InjectionToken } from '@angular/core';
export const ZONE_SERVICE = new InjectionToken('ZONE_TASK');

View file

@ -8,6 +8,7 @@ import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesP
providedIn: 'root'
})
export class LightningApiService {
private apiBaseUrl: string; // base URL is protocol, hostname, and port
private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
@ -16,6 +17,10 @@ export class LightningApiService {
private httpClient: HttpClient,
private stateService: StateService,
) {
this.apiBaseUrl = ''; // use relative URL by default
if (!stateService.isBrowser) { // except when inside AU SSR process
this.apiBaseUrl = this.stateService.env.NGINX_PROTOCOL + '://' + this.stateService.env.NGINX_HOSTNAME + ':' + this.stateService.env.NGINX_PORT;
}
this.apiBasePath = ''; // assume mainnet by default
this.stateService.networkChanged$.subscribe((network) => {
if (network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND) {
@ -66,15 +71,15 @@ export class LightningApiService {
}
getNode$(publicKey: string): Observable<any> {
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
}
getNodeGroup$(name: string): Observable<any[]> {
return this.httpClient.get<any[]>(this.apiBasePath + '/api/v1/lightning/nodes/group/' + name);
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/group/' + name);
}
getChannel$(shortId: string): Observable<any> {
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/channels/' + shortId);
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/' + shortId);
}
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
@ -84,57 +89,57 @@ export class LightningApiService {
.set('status', status)
;
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/channels', { params, observe: 'response' });
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels', { params, observe: 'response' });
}
getLatestStatistics$(): Observable<any> {
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/statistics/latest');
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/statistics/latest');
}
listNodeStats$(publicKey: string): Observable<any> {
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
}
getNodeFeeHistogram$(publicKey: string): Observable<any> {
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram');
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram');
}
getNodesRanking$(): Observable<INodesRanking> {
return this.httpClient.get<INodesRanking>(this.apiBasePath + '/api/v1/lightning/nodes/rankings');
return this.httpClient.get<INodesRanking>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings');
}
listChannelStats$(publicKey: string): Observable<any> {
return this.httpClient.get<any>(this.apiBasePath + '/channels/' + publicKey + '/statistics');
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/channels/' + publicKey + '/statistics');
}
listStatistics$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any>(
this.apiBasePath + '/api/v1/lightning/statistics' +
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/statistics' +
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
);
}
getTopNodesByCapacity$(): Observable<ITopNodesPerCapacity[]> {
return this.httpClient.get<ITopNodesPerCapacity[]>(
this.apiBasePath + '/api/v1/lightning/nodes/rankings/liquidity'
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/liquidity'
);
}
getTopNodesByChannels$(): Observable<ITopNodesPerChannels[]> {
return this.httpClient.get<ITopNodesPerChannels[]>(
this.apiBasePath + '/api/v1/lightning/nodes/rankings/connectivity'
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/connectivity'
);
}
getPenaltyClosedChannels$(): Observable<IChannel[]> {
return this.httpClient.get<IChannel[]>(
this.apiBasePath + '/api/v1/lightning/penalties'
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/penalties'
);
}
getOldestNodes$(): Observable<IOldestNodes[]> {
return this.httpClient.get<IOldestNodes[]>(
this.apiBasePath + '/api/v1/lightning/nodes/rankings/age'
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/age'
);
}
}

View file

@ -1,5 +1,5 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, OnInit } from '@angular/core';
import { Observable, merge } from 'rxjs';
import { share } from 'rxjs/operators';
import { INodesRanking, INodesStatistics } from '../../interfaces/node-api.interface';
import { SeoService } from '../../services/seo.service';
@ -24,6 +24,7 @@ export class LightningDashboardComponent implements OnInit, AfterViewInit {
private seoService: SeoService,
private ogService: OpenGraphService,
private stateService: StateService,
private cd: ChangeDetectorRef,
) { }
ngOnInit(): void {
@ -35,6 +36,12 @@ export class LightningDashboardComponent implements OnInit, AfterViewInit {
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
if (!this.stateService.isBrowser) {
merge(this.nodesRanking$, this.statistics$).subscribe(() => {
this.cd.markForCheck();
});
}
}
ngAfterViewInit(): void {

View file

@ -1,7 +1,7 @@
<div class="full-container">
<h2 i18n="lightning.node-fee-distribution">Fee distribution</h2>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View file

@ -1,5 +1,7 @@
.full-container {
position: relative;
margin-top: 25px;
margin-bottom: 25px;
min-height: 100%;
}
min-height: 450px;
}

View file

@ -5,6 +5,7 @@ import { download } from '../../shared/graphs.utils';
import { LightningApiService } from '../lightning-api.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-node-fee-chart',
@ -33,6 +34,7 @@ export class NodeFeeChartComponent implements OnInit {
constructor(
@Inject(LOCALE_ID) public locale: string,
private lightningApiService: LightningApiService,
public stateService: StateService,
private activatedRoute: ActivatedRoute,
private amountShortenerPipe: AmountShortenerPipe,
) {

View file

@ -1,7 +1,7 @@
<div class="full-container">
<div [class]="!widget ? 'chart' : 'chart-widget'" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -8,6 +8,7 @@ import { StorageService } from '../../services/storage.service';
import { download } from '../../shared/graphs.utils';
import { LightningApiService } from '../lightning-api.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-node-statistics-chart',
@ -48,6 +49,7 @@ export class NodeStatisticsChartComponent implements OnInit {
@Inject(LOCALE_ID) public locale: string,
private lightningApiService: LightningApiService,
private storageService: StorageService,
public stateService: StateService,
private activatedRoute: ActivatedRoute,
) {
}

View file

@ -1,12 +1,13 @@
<div class="map-wrapper" [class]="style" *ngIf="style !== 'graph'">
<ng-container *ngIf="channelsObservable | async">
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
<div class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div class="chart" [class]="style" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
</div>
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
</div>
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading && !disableSpinner">
<div class="text-center loading-spinner" [class]="style" *ngIf="(!stateService.isBrowser || isLoading) && !disableSpinner">
<div class="spinner-border text-light"></div>
</div>
</ng-container>
@ -21,8 +22,10 @@
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
</div>
<div *ngIf="channelsObservable | async" class="chart-graph" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
</div>
<ng-container *ngIf="channelsObservable | async">
<div class="chart-graph" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
</div>
</ng-container>
</div>

View file

@ -33,7 +33,6 @@
min-height: 400px;
margin-top: 25px;
margin-bottom: 25px;
min-height: 100%;
}
.full-container.widget {
height: 250px;

View file

@ -45,7 +45,7 @@ export class NodesChannelsMap implements OnInit {
constructor(
private seoService: SeoService,
private apiService: ApiService,
private stateService: StateService,
public stateService: StateService,
private assetsService: AssetsService,
private router: Router,
private zone: NgZone,

View file

@ -1,9 +1,11 @@
<div *ngIf="channelsObservable$ | async" style="min-height: 455px">
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
<div echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
<div class="node-channels-container">
<div *ngIf="channelsObservable$ | async" style="min-height: 455px">
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
<div *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
</div>
</div>
<div *ngIf="!stateService.isBrowser || isLoading" class="text-center loading-spinner">
<div class="spinner-border text-light"></div>
</div>
</div>
<div *ngIf="isLoading" class="text-center loading-spinner">
<div class="spinner-border text-light"></div>
</div>

View file

@ -1,5 +1,13 @@
.node-channels-container {
position: relative;
}
.loading-spinner {
min-height: 455px;
position: absolute;
top: 0;
left: 0;
right: 0;
width: 100%;
z-index: 100;
}

View file

@ -33,7 +33,7 @@ export class NodeChannels implements OnChanges {
private amountShortenerPipe: AmountShortenerPipe,
private zone: NgZone,
private router: Router,
private stateService: StateService,
public stateService: StateService,
) {}
ngOnChanges(): void {

View file

@ -7,8 +7,13 @@
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
</div>
<div *ngIf="observable$ | async" class="chart" [class]="widget ? 'widget' : ''" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
<ng-container *ngIf="observable$ | async">
<div class="chart" [class]="widget ? 'widget' : ''" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
</div>
</ng-container>
<div class="text-center loading-spinner" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View file

@ -63,3 +63,13 @@
.chart.widget {
padding: 0px;
}
.loading-spinner {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
@media (max-width: 767.98px) {
top: 550px;
}
}

View file

@ -26,6 +26,7 @@ export class NodesMap implements OnInit, OnChanges {
inputNodes$: BehaviorSubject<any>;
nodes$: Observable<any>;
observable$: Observable<any>;
isLoading: boolean = true;
chartInstance = undefined;
chartOptions: EChartsOption = {};
@ -37,7 +38,7 @@ export class NodesMap implements OnInit, OnChanges {
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
private apiService: ApiService,
private stateService: StateService,
public stateService: StateService,
private assetsService: AssetsService,
private router: Router,
private zone: NgZone,
@ -226,6 +227,7 @@ export class NodesMap implements OnInit, OnChanges {
},
]
};
this.isLoading = false;
}
onChartInit(ec) {
@ -235,6 +237,10 @@ export class NodesMap implements OnInit, OnChanges {
this.chartInstance = ec;
this.chartInstance.on('finished', () => {
this.isLoading = false;
});
this.chartInstance.on('click', (e) => {
if (e.data) {
this.zone.run(() => {

View file

@ -35,8 +35,8 @@
</form>
</div>
<div [class]="!widget ? 'chart' : 'chart-widget'" [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -11,6 +11,7 @@ import { SeoService } from '../../services/seo.service';
import { LightningApiService } from '../lightning-api.service';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
import { isMobile } from '../../shared/common.utils';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-nodes-networks-chart',
@ -58,6 +59,7 @@ export class NodesNetworksChartComponent implements OnInit, OnChanges {
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
private miningService: MiningService,
public stateService: StateService,
private amountShortenerPipe: AmountShortenerPipe,
) {
}

View file

@ -12,12 +12,12 @@
<div class="container pb-lg-0">
<div class="pb-lg-5">
<div class="chart w-100" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div class="chart w-100" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -37,7 +37,7 @@ export class NodesPerCountryChartComponent implements OnInit {
private seoService: SeoService,
private amountShortenerPipe: AmountShortenerPipe,
private zone: NgZone,
private stateService: StateService,
public stateService: StateService,
private router: Router,
) {
}

View file

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs';
import { ApiService } from '../../services/api.service';
@ -27,6 +27,7 @@ export class NodesPerCountry implements OnInit {
constructor(
private apiService: ApiService,
private seoService: SeoService,
private cd: ChangeDetectorRef,
private route: ActivatedRoute,
) {
for (let i = 0; i < this.pageSize; ++i) {
@ -94,7 +95,10 @@ export class NodesPerCountry implements OnInit {
ispCount: Object.keys(isps).length
};
}),
tap(() => this.isLoading = false),
tap(() => {
this.isLoading = false
this.cd.markForCheck();
}),
share()
);

View file

@ -39,14 +39,10 @@
</div>
<div *ngIf="!indexingInProgress else indexing" [class]="!widget ? '' : 'pb-0'" class="container pb-lg-0">
<div [class]="widget ? 'chart-widget' : 'chart'" [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class]="widget ? 'chart-widget' : 'chart'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
<div class="d-flex justify-content-md-end toggle" *ngIf="!widget">
<app-toggle [textLeft]="'Sort by nodes'" [textRight]="'capacity'" [checked]="true" (toggleStatusChanged)="onGroupToggleStatusChanged($event)"></app-toggle>
</div>
@ -74,6 +70,9 @@
</tbody>
</table>
</div>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>
<ng-template #loadingReward>

View file

@ -44,7 +44,7 @@ export class NodesPerISPChartComponent implements OnInit {
private amountShortenerPipe: AmountShortenerPipe,
private router: Router,
private zone: NgZone,
private stateService: StateService,
public stateService: StateService,
) {
}

View file

@ -42,9 +42,9 @@
</form>
</div>
<div [class]="!widget ? 'chart' : 'chart-widget'" [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View file

@ -11,6 +11,7 @@ import { download } from '../../shared/graphs.utils';
import { LightningApiService } from '../lightning-api.service';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
import { isMobile } from '../../shared/common.utils';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-lightning-statistics-chart',
@ -55,6 +56,7 @@ export class LightningStatisticsChartComponent implements OnInit, OnChanges {
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
private miningService: MiningService,
public stateService: StateService,
private amountShortenerPipe: AmountShortenerPipe,
) {
}

View file

@ -1,5 +1,5 @@
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { TransferState, makeStateKey } from '@angular/platform-browser';
@ -17,14 +17,18 @@ export class HttpCacheInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.isBrowser && request.method === 'GET') {
const cachedResponse = this.transferState.get<any>(makeStateKey(request.url), null);
if (cachedResponse) {
const { response, headers } = this.transferState.get<any>(makeStateKey(request.url), null) || {};
if (response) {
const httpHeaders = new HttpHeaders();
for (const [k,v] of Object.entries(headers)) {
httpHeaders.set(k,v as string[]);
}
const modifiedResponse = new HttpResponse<any>({
headers: cachedResponse.headers,
body: cachedResponse.body,
status: cachedResponse.status,
statusText: cachedResponse.statusText,
url: cachedResponse.url
headers: httpHeaders,
body: response.body,
status: response.status,
statusText: response.statusText,
url: response.url
});
this.transferState.remove(makeStateKey(request.url));
return of(modifiedResponse);
@ -35,7 +39,11 @@ export class HttpCacheInterceptor implements HttpInterceptor {
.pipe(tap((event: HttpEvent<any>) => {
if (!this.isBrowser && event instanceof HttpResponse) {
let keyId = request.url.split('/').slice(3).join('/');
this.transferState.set<any>(makeStateKey('/' + keyId), event);
const headers = {};
for (const k of event.headers.keys()) {
headers[k] = event.headers.getAll(k);
}
this.transferState.set<any>(makeStateKey('/' + keyId), { response: event, headers });
}
}));
}

View file

@ -3,10 +3,10 @@ import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { WebsocketResponse } from '../interfaces/websocket.interface';
import { StateService } from './state.service';
import { Transaction } from '../interfaces/electrs.interface';
import { Subscription } from 'rxjs';
import { firstValueFrom, Subscription } from 'rxjs';
import { ApiService } from './api.service';
import { take } from 'rxjs/operators';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { TransferState, makeStateKey } from '@angular/core';
import { CacheService } from './cache.service';
import { uncompressDeltaChange, uncompressTx } from '../shared/common.utils';
@ -57,8 +57,12 @@ export class WebsocketService {
this.network = this.stateService.network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND ? '' : this.stateService.network;
this.websocketSubject = webSocket<WebsocketResponse>(this.webSocketUrl.replace('{network}', this.network ? '/' + this.network : ''));
const theInitData = this.transferState.get<any>(initData, null);
const { response: theInitData } = this.transferState.get<any>(initData, null) || {};
if (theInitData) {
if (theInitData.body.blocks) {
theInitData.body.blocks = theInitData.body.blocks.reverse();
}
this.stateService.isLoadingWebSocket$.next(false);
this.handleResponse(theInitData.body);
this.startSubscription(false, true);
} else {
@ -223,6 +227,7 @@ export class WebsocketService {
}
startTrackRbfSummary() {
this.initRbfSummary();
this.websocketSubject.next({ 'track-rbf-summary': true });
this.isTrackingRbfSummary = true;
}
@ -445,4 +450,30 @@ export class WebsocketService {
this.websocketSubject.next({'refresh-blocks': true});
}
}
async initRbfSummary(): Promise<void> {
if (!this.stateService.isBrowser) {
const rbfList = await firstValueFrom(this.apiService.getRbfList$(false));
if (rbfList) {
const rbfSummary = rbfList.slice(0, 6).map(rbfTree => {
let oldFee = 0;
let oldVsize = 0;
for (const replaced of rbfTree.replaces) {
oldFee += replaced.tx.fee;
oldVsize += replaced.tx.vsize;
}
return {
txid: rbfTree.tx.txid,
mined: !!rbfTree.tx.mined,
fullRbf: !!rbfTree.tx.fullRbf,
oldFee,
oldVsize,
newFee: rbfTree.tx.fee,
newVsize: rbfTree.tx.vsize,
};
});
this.stateService.rbfLatestSummary$.next(rbfSummary);
}
}
}
}

View file

@ -0,0 +1,14 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ZoneService {
constructor() { }
wrapObservable<T>(obs: Observable<T>): Observable<T> {
return obs;
}
}

View file

@ -0,0 +1,60 @@
import { ApplicationRef, Injectable, NgZone } from '@angular/core';
import { Observable, Subscriber } from 'rxjs';
// global Zone object provided by zone.js
declare const Zone: any;
@Injectable({
providedIn: 'root'
})
export class ZoneService {
constructor(
private ngZone: NgZone,
private appRef: ApplicationRef,
) { }
wrapObservable<T>(obs: Observable<T>): Observable<T> {
return new Observable((subscriber: Subscriber<T>) => {
let task: any;
this.ngZone.run(() => {
task = Zone.current.scheduleMacroTask('wrapObservable', () => {}, {}, () => {}, () => {});
});
const subscription = obs.subscribe(
value => {
subscriber.next(value);
if (task) {
this.ngZone.run(() => {
this.appRef.tick();
});
task.invoke();
}
},
err => {
subscriber.error(err);
if (task) {
this.appRef.tick();
task.invoke();
}
},
() => {
subscriber.complete();
if (task) {
this.appRef.tick();
task.invoke();
}
}
);
return () => {
subscription.unsubscribe();
if (task) {
this.appRef.tick();
task.invoke();
}
};
});
}
}

View file

@ -0,0 +1,19 @@
import { Directive, TemplateRef, ViewContainerRef, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Directive({
selector: '[browserOnly]'
})
export class BrowserOnlyDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
@Inject(PLATFORM_ID) private platformId: Object
) {
if (isPlatformBrowser(this.platformId)) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}

View file

@ -0,0 +1,19 @@
import { Directive, TemplateRef, ViewContainerRef, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformServer } from '@angular/common';
@Directive({
selector: '[serverOnly]'
})
export class ServerOnlyDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
@Inject(PLATFORM_ID) private platformId: Object
) {
if (isPlatformServer(this.platformId)) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}

View file

@ -323,7 +323,7 @@ export function hasTouchScreen(): boolean {
// @ts-ignore
hasTouchScreen = navigator.msMaxTouchPoints > 0;
} else {
const mQ = matchMedia?.('(pointer:coarse)');
const mQ = window.matchMedia?.('(pointer:coarse)');
if (mQ?.media === '(pointer:coarse)') {
hasTouchScreen = !!mQ.matches;
} else if ('orientation' in window) {

View file

@ -33,6 +33,8 @@ import { ReactiveFormsModule } from '@angular/forms';
import { LanguageSelectorComponent } from '../components/language-selector/language-selector.component';
import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component';
import { RateUnitSelectorComponent } from '../components/rate-unit-selector/rate-unit-selector.component';
import { BrowserOnlyDirective } from './directives/browser-only.directive';
import { ServerOnlyDirective } from './directives/server-only.directive';
import { ColoredPriceDirective } from './directives/colored-price.directive';
import { NoSanitizePipe } from './pipes/no-sanitize.pipe';
import { MempoolBlocksComponent } from '../components/mempool-blocks/mempool-blocks.component';
@ -132,6 +134,8 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
FeeRoundingPipe,
FiatCurrencyPipe,
ColoredPriceDirective,
BrowserOnlyDirective,
ServerOnlyDirective,
BlockchainComponent,
BlockViewComponent,
EightBlocksComponent,
@ -264,6 +268,8 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
Decimal2HexPipe,
FeeRoundingPipe,
ColoredPriceDirective,
BrowserOnlyDirective,
ServerOnlyDirective,
NoSanitizePipe,
BlockchainComponent,
MempoolBlocksComponent,

View file

@ -0,0 +1,11 @@
import '@angular/localize/init';
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
export { AppServerModule } from './app/app.server.module';
export { renderModule } from '@angular/platform-server';

View file

@ -0,0 +1,18 @@
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "./out-tsc/server",
"target": "ES2022",
"sourceMap": true,
"types": [
"node"
]
},
"files": [
"src/main.server.ts",
"server.ts"
],
"angularCompilerOptions": {
"entryModule": "./src/app/app.server.module#AppServerModule"
}
}