Merge branch 'master' into simon/node-alias-fulltext-search
This commit is contained in:
commit
4727b4bade
@ -77,13 +77,19 @@
|
||||
},
|
||||
"LIGHTNING": {
|
||||
"ENABLED": false,
|
||||
"BACKEND": "lnd"
|
||||
"BACKEND": "lnd",
|
||||
"STATS_REFRESH_INTERVAL": 600,
|
||||
"GRAPH_REFRESH_INTERVAL": 600,
|
||||
"LOGGER_UPDATE_INTERVAL": 30
|
||||
},
|
||||
"LND": {
|
||||
"TLS_CERT_PATH": "tls.cert",
|
||||
"MACAROON_PATH": "readonly.macaroon",
|
||||
"REST_API_URL": "https://localhost:8080"
|
||||
},
|
||||
"CLIGHTNING": {
|
||||
"SOCKET": "lightning-rpc"
|
||||
},
|
||||
"SOCKS5PROXY": {
|
||||
"ENABLED": false,
|
||||
"USE_ONION": true,
|
||||
|
@ -88,5 +88,21 @@
|
||||
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
|
||||
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
|
||||
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
|
||||
},
|
||||
"LIGHTNING": {
|
||||
"ENABLED": "__LIGHTNING_ENABLED__",
|
||||
"BACKEND": "__LIGHTNING_BACKEND__",
|
||||
"TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
|
||||
"STATS_REFRESH_INTERVAL": 600,
|
||||
"GRAPH_REFRESH_INTERVAL": 600,
|
||||
"LOGGER_UPDATE_INTERVAL": 30
|
||||
},
|
||||
"LND": {
|
||||
"TLS_CERT_PATH": "",
|
||||
"MACAROON_PATH": "",
|
||||
"REST_API_URL": "https://localhost:8080"
|
||||
},
|
||||
"CLIGHTNING": {
|
||||
"SOCKET": "__CLIGHTNING_SOCKET__"
|
||||
}
|
||||
}
|
||||
|
@ -297,19 +297,24 @@ class NodesApi {
|
||||
|
||||
if (!ispList[isp1]) {
|
||||
ispList[isp1] = {
|
||||
id: channel.isp1ID,
|
||||
id: channel.isp1ID.toString(),
|
||||
capacity: 0,
|
||||
channels: 0,
|
||||
nodes: {},
|
||||
};
|
||||
} else if (ispList[isp1].id.indexOf(channel.isp1ID) === -1) {
|
||||
ispList[isp1].id += ',' + channel.isp1ID.toString();
|
||||
}
|
||||
|
||||
if (!ispList[isp2]) {
|
||||
ispList[isp2] = {
|
||||
id: channel.isp2ID,
|
||||
id: channel.isp2ID.toString(),
|
||||
capacity: 0,
|
||||
channels: 0,
|
||||
nodes: {},
|
||||
};
|
||||
} else if (ispList[isp2].id.indexOf(channel.isp2ID) === -1) {
|
||||
ispList[isp2].id += ',' + channel.isp2ID.toString();
|
||||
}
|
||||
|
||||
ispList[isp1].capacity += channel.capacity;
|
||||
@ -386,9 +391,10 @@ class NodesApi {
|
||||
public async $getNodesPerCountry(countryId: string) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
||||
geo_names_city.names as city
|
||||
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
||||
geo_names_city.names as city, geo_names_country.names as country,
|
||||
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
|
||||
FROM node_stats
|
||||
JOIN (
|
||||
SELECT public_key, MAX(added) as last_added
|
||||
@ -396,15 +402,19 @@ class NodesApi {
|
||||
GROUP BY public_key
|
||||
) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added
|
||||
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||
JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
||||
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
|
||||
WHERE geo_names_country.id = ?
|
||||
ORDER BY capacity DESC
|
||||
`;
|
||||
|
||||
const [rows]: any = await DB.query(query, [countryId]);
|
||||
for (let i = 0; i < rows.length; ++i) {
|
||||
rows[i].country = JSON.parse(rows[i].country);
|
||||
rows[i].city = JSON.parse(rows[i].city);
|
||||
rows[i].subdivision = JSON.parse(rows[i].subdivision);
|
||||
}
|
||||
return rows;
|
||||
} catch (e) {
|
||||
@ -418,7 +428,8 @@ class NodesApi {
|
||||
const query = `
|
||||
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
||||
geo_names_city.names as city, geo_names_country.names as country
|
||||
geo_names_city.names as city, geo_names_country.names as country,
|
||||
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
|
||||
FROM node_stats
|
||||
JOIN (
|
||||
SELECT public_key, MAX(added) as last_added
|
||||
@ -426,8 +437,10 @@ class NodesApi {
|
||||
GROUP BY public_key
|
||||
) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added
|
||||
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||
JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
||||
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
|
||||
WHERE nodes.as_number IN (?)
|
||||
ORDER BY capacity DESC
|
||||
`;
|
||||
@ -436,6 +449,7 @@ class NodesApi {
|
||||
for (let i = 0; i < rows.length; ++i) {
|
||||
rows[i].country = JSON.parse(rows[i].country);
|
||||
rows[i].city = JSON.parse(rows[i].city);
|
||||
rows[i].subdivision = JSON.parse(rows[i].subdivision);
|
||||
}
|
||||
return rows;
|
||||
} catch (e) {
|
||||
|
@ -36,6 +36,7 @@ interface IConfig {
|
||||
TOPOLOGY_FOLDER: string;
|
||||
STATS_REFRESH_INTERVAL: number;
|
||||
GRAPH_REFRESH_INTERVAL: number;
|
||||
LOGGER_UPDATE_INTERVAL: number;
|
||||
};
|
||||
LND: {
|
||||
TLS_CERT_PATH: string;
|
||||
@ -191,6 +192,7 @@ const defaults: IConfig = {
|
||||
'TOPOLOGY_FOLDER': '',
|
||||
'STATS_REFRESH_INTERVAL': 600,
|
||||
'GRAPH_REFRESH_INTERVAL': 600,
|
||||
'LOGGER_UPDATE_INTERVAL': 30,
|
||||
},
|
||||
'LND': {
|
||||
'TLS_CERT_PATH': '',
|
||||
|
@ -95,11 +95,19 @@ class NetworkSyncService {
|
||||
*/
|
||||
private async $updateChannelsList(channels: ILightningApi.Channel[]): Promise<void> {
|
||||
try {
|
||||
const [closedChannelsRaw]: any[] = await DB.query(`SELECT id FROM channels WHERE status = 2`);
|
||||
const closedChannels = {};
|
||||
for (const closedChannel of closedChannelsRaw) {
|
||||
closedChannels[Common.channelShortIdToIntegerId(closedChannel.id)] = true;
|
||||
}
|
||||
|
||||
let progress = 0;
|
||||
|
||||
const graphChannelsIds: string[] = [];
|
||||
for (const channel of channels) {
|
||||
await channelsApi.$saveChannel(channel);
|
||||
if (!closedChannels[channel.channel_id]) {
|
||||
await channelsApi.$saveChannel(channel);
|
||||
}
|
||||
graphChannelsIds.push(channel.channel_id);
|
||||
++progress;
|
||||
|
||||
|
@ -350,3 +350,68 @@ Corresponding `docker-compose.yml` overrides:
|
||||
PRICE_DATA_SERVER_CLEARNET_URL: ""
|
||||
...
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
"LIGHTNING": {
|
||||
"ENABLED": false
|
||||
"BACKEND": "lnd"
|
||||
"TOPOLOGY_FOLDER": ""
|
||||
"STATS_REFRESH_INTERVAL": 600
|
||||
"GRAPH_REFRESH_INTERVAL": 600
|
||||
"LOGGER_UPDATE_INTERVAL": 30
|
||||
}
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
api:
|
||||
environment:
|
||||
LIGHTNING_ENABLED: false
|
||||
LIGHTNING_BACKEND: "lnd"
|
||||
LIGHTNING_TOPOLOGY_FOLDER: ""
|
||||
LIGHTNING_STATS_REFRESH_INTERVAL: 600
|
||||
LIGHTNING_GRAPH_REFRESH_INTERVAL: 600
|
||||
LIGHTNING_LOGGER_UPDATE_INTERVAL: 30
|
||||
...
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
"LND": {
|
||||
"TLS_CERT_PATH": ""
|
||||
"MACAROON_PATH": ""
|
||||
"REST_API_URL": "https://localhost:8080"
|
||||
}
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
api:
|
||||
environment:
|
||||
LND_TLS_CERT_PATH: ""
|
||||
LND_MACAROON_PATH: ""
|
||||
LND_REST_API_URL: "https://localhost:8080"
|
||||
...
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
"CLN": {
|
||||
"SOCKET": ""
|
||||
}
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
api:
|
||||
environment:
|
||||
CLN_SOCKET: ""
|
||||
...
|
||||
```
|
||||
|
@ -91,6 +91,22 @@ __EXTERNAL_DATA_SERVER_LIQUID_ONION__=${EXTERNAL_DATA_SERVER_LIQUID_ONION:=http:
|
||||
__EXTERNAL_DATA_SERVER_BISQ_URL__=${EXTERNAL_DATA_SERVER_BISQ_URL:=https://bisq.markets/api}
|
||||
__EXTERNAL_DATA_SERVER_BISQ_ONION__=${EXTERNAL_DATA_SERVER_BISQ_ONION:=http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api}
|
||||
|
||||
# LIGHTNING
|
||||
__LIGHTNING_ENABLED__=${LIGHTNING_ENABLED:=false}
|
||||
__LIGHTNING_BACKEND__=${LIGHTNING_BACKEND:="lnd"}
|
||||
__LIGHTNING_TOPOLOGY_FOLDER__=${LIGHTNING_TOPOLOGY_FOLDER:=""}
|
||||
__LIGHTNING_STATS_REFRESH_INTERVAL__=${LIGHTNING_STATS_REFRESH_INTERVAL:=600}
|
||||
__LIGHTNING_GRAPH_REFRESH_INTERVAL__=${LIGHTNING_GRAPH_REFRESH_INTERVAL:=600}
|
||||
__LIGHTNING_LOGGER_UPDATE_INTERVAL__=${LIGHTNING_LOGGER_UPDATE_INTERVAL:=30}
|
||||
|
||||
# LND
|
||||
__LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""}
|
||||
__LND_MACAROON_PATH__=${LND_MACAROON_PATH:=""}
|
||||
__LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"}
|
||||
|
||||
# CLN
|
||||
__CLN_SOCKET__=${CLN_SOCKET:=""}
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
|
||||
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
|
||||
@ -173,4 +189,20 @@ sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_ONION__!${__EXTERNAL_DATA_SERVER_LIQUID_
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_URL__!${__EXTERNAL_DATA_SERVER_BISQ_URL__}!g" mempool-config.json
|
||||
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_ONION__!${__EXTERNAL_DATA_SERVER_BISQ_ONION__}!g" mempool-config.json
|
||||
|
||||
# LIGHTNING
|
||||
sed -i "s!__LIGHTNING_ENABLED__!${__LIGHTNING_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__LIGHTNING_BACKEND__!${__LIGHTNING_BACKEND__}!g" mempool-config.json
|
||||
sed -i "s!__LIGHTNING_TOPOLOGY_FOLDER__!${__LIGHTNING_TOPOLOGY_FOLDER__}!g" mempool-config.json
|
||||
sed -i "s!__LIGHTNING_STATS_REFRESH_INTERVAL__!${__LIGHTNING_STATS_REFRESH_INTERVAL__}!g" mempool-config.json
|
||||
sed -i "s!__LIGHTNING_GRAPH_REFRESH_INTERVAL__!${__LIGHTNING_GRAPH_REFRESH_INTERVAL__}!g" mempool-config.json
|
||||
sed -i "s!__LIGHTNING_LOGGER_UPDATE_INTERVAL__!${__LIGHTNING_LOGGER_UPDATE_INTERVAL__}!g" mempool-config.json
|
||||
|
||||
# LND
|
||||
sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json
|
||||
sed -i "s!__LND_MACAROON_PATH__!${__LND_MACAROON_PATH__}!g" mempool-config.json
|
||||
sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json
|
||||
|
||||
# CLN
|
||||
sed -i "s!__CLN_SOCKET__!${__CLN_SOCKET__}!g" mempool-config.json
|
||||
|
||||
node /backend/dist/index.js
|
||||
|
@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
|
||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { OpenGraphService } from 'src/app/services/opengraph.service';
|
||||
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
|
||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
|
||||
|
@ -42,24 +42,10 @@
|
||||
<app-fiat [value]="node.avgCapacity" digitsInfo="1.0-0"></app-fiat>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="node.country && node.city && node.subdivision">
|
||||
<tr *ngIf="node.geolocation">
|
||||
<td i18n="location">Location</td>
|
||||
<td>
|
||||
<span>{{ node.city.en }}, {{ node.subdivision.en }}</span>
|
||||
<br>
|
||||
<a class="d-flex align-items-center" [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
|
||||
<span class="link">{{ node.country.en }}</span>
|
||||
|
||||
<span class="flag">{{ node.flag }}</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="node.country && !node.city">
|
||||
<td i18n="location">Location</td>
|
||||
<td>
|
||||
<a [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
|
||||
{{ node.country.en }} {{ node.flag }}
|
||||
</a>
|
||||
<app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -3,9 +3,9 @@ import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node',
|
||||
@ -58,7 +58,6 @@ export class NodeComponent implements OnInit {
|
||||
} else if (socket.indexOf('onion') > -1) {
|
||||
label = 'Tor';
|
||||
}
|
||||
node.flag = getFlagEmoji(node.iso_code);
|
||||
socketsObject.push({
|
||||
label: label,
|
||||
socket: node.public_key + '@' + socket,
|
||||
@ -66,6 +65,19 @@ export class NodeComponent implements OnInit {
|
||||
}
|
||||
node.socketsObject = socketsObject;
|
||||
node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
|
||||
|
||||
if (!node?.country && !node?.city &&
|
||||
!node?.subdivision && !node?.iso) {
|
||||
node.geolocation = null;
|
||||
} else {
|
||||
node.geolocation = <GeolocationData>{
|
||||
country: node.country?.en,
|
||||
city: node.city?.en,
|
||||
subdivision: node.subdivision?.en,
|
||||
iso: node.iso_code,
|
||||
};
|
||||
}
|
||||
|
||||
return node;
|
||||
}),
|
||||
catchError(err => {
|
||||
|
@ -1,14 +1,16 @@
|
||||
<div [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
|
||||
|
||||
<div *ngIf="style === 'graph'" class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
|
||||
<div *ngIf="channelsObservable | async">
|
||||
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
|
||||
<div *ngIf="style === 'graph'" class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
|
||||
</div>
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"
|
||||
(chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
|
||||
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, HostListener, Input, Output, EventEmitter, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, NgZone, OnInit } from '@angular/core';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { Observable, switchMap, tap, zip } from 'rxjs';
|
||||
@ -16,14 +16,14 @@ import { isMobile } from 'src/app/shared/common.utils';
|
||||
styleUrls: ['./nodes-channels-map.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
export class NodesChannelsMap implements OnInit {
|
||||
@Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph';
|
||||
@Input() publicKey: string | undefined;
|
||||
@Input() channel: any[] = [];
|
||||
@Input() fitContainer = false;
|
||||
@Output() readyEvent = new EventEmitter();
|
||||
|
||||
observable$: Observable<any>;
|
||||
channelsObservable: Observable<any>;
|
||||
|
||||
center: number[] | undefined;
|
||||
zoom: number | undefined;
|
||||
@ -31,6 +31,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
channelOpacity = 0.1;
|
||||
channelColor = '#466d9d';
|
||||
channelCurve = 0;
|
||||
nodeSize = 4;
|
||||
|
||||
chartInstance = undefined;
|
||||
chartOptions: EChartsOption = {};
|
||||
@ -49,8 +50,6 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.center = this.style === 'widget' ? [0, 40] : [0, 5];
|
||||
this.zoom = 1.3;
|
||||
@ -65,8 +64,12 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
if (this.style === 'graph') {
|
||||
this.seoService.setTitle($localize`Lightning nodes channels world map`);
|
||||
}
|
||||
|
||||
if (['nodepage', 'channelpage'].includes(this.style)) {
|
||||
this.nodeSize = 8;
|
||||
}
|
||||
|
||||
this.observable$ = this.activatedRoute.paramMap
|
||||
this.channelsObservable = this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
return zip(
|
||||
@ -170,15 +173,8 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
prepareChartOptions(nodes, channels) {
|
||||
let title: object;
|
||||
if (channels.length === 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: $localize`No geolocation data available`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
this.chartOptions = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
@ -214,7 +210,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
data: nodes,
|
||||
coordinateSystem: 'geo',
|
||||
geoIndex: 0,
|
||||
symbolSize: 4,
|
||||
symbolSize: this.nodeSize,
|
||||
tooltip: {
|
||||
show: true,
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
|
@ -10,6 +10,7 @@ import { download } from 'src/app/shared/graphs.utils';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { isMobile } from 'src/app/shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-networks-chart',
|
||||
@ -108,19 +109,19 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions(data, maxYAxis) {
|
||||
prepareChartOptions(data, maxYAxis): void {
|
||||
let title: object;
|
||||
if (data.tor_nodes.length === 0) {
|
||||
if (!this.widget && data.tor_nodes.length === 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`,
|
||||
text: $localize`Indexing in progess`,
|
||||
left: 'center',
|
||||
top: 'top',
|
||||
top: 'center',
|
||||
};
|
||||
} else if (this.widget) {
|
||||
} else if (data.tor_nodes.length > 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
@ -140,11 +141,11 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
height: this.widget ? 100 : undefined,
|
||||
top: this.widget ? 10 : 40,
|
||||
bottom: this.widget ? 0 : 70,
|
||||
right: (this.isMobile() && this.widget) ? 35 : this.right,
|
||||
left: (this.isMobile() && this.widget) ? 40 :this.left,
|
||||
right: (isMobile() && this.widget) ? 35 : this.right,
|
||||
left: (isMobile() && this.widget) ? 40 :this.left,
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile() || !this.widget,
|
||||
show: !isMobile() || !this.widget,
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
@ -157,7 +158,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: (ticks) => {
|
||||
formatter: (ticks): string => {
|
||||
let total = 0;
|
||||
const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
let tooltip = `<b style="color: white; margin-left: 2px">${date}</b><br>`;
|
||||
@ -180,7 +181,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
},
|
||||
xAxis: data.tor_nodes.length === 0 ? undefined : {
|
||||
type: 'time',
|
||||
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
|
||||
splitNumber: (isMobile() || this.widget) ? 5 : 10,
|
||||
axisLabel: {
|
||||
hideOverlap: true,
|
||||
}
|
||||
@ -372,7 +373,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
onChartInit(ec): void {
|
||||
if (this.chartInstance !== undefined) {
|
||||
return;
|
||||
}
|
||||
@ -384,11 +385,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
onSaveChart() {
|
||||
onSaveChart(): void {
|
||||
// @ts-ignore
|
||||
const prevBottom = this.chartOptions.grid.bottom;
|
||||
const now = new Date();
|
||||
|
@ -9,7 +9,7 @@ import { StateService } from 'src/app/services/state.service';
|
||||
import { download } from 'src/app/shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
|
||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-per-country-chart',
|
||||
|
@ -36,7 +36,7 @@
|
||||
{{ node.channels }}
|
||||
</td>
|
||||
<td class="city text-right text-truncate">
|
||||
{{ node?.city?.en ?? '-' }}
|
||||
<app-geolocation [data]="node.geolocation" [type]="'list-country'"></app-geolocation>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -3,7 +3,8 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
|
||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-per-country',
|
||||
@ -29,6 +30,16 @@ export class NodesPerCountry implements OnInit {
|
||||
name: response.country.en,
|
||||
flag: getFlagEmoji(this.route.snapshot.params.country)
|
||||
};
|
||||
|
||||
for (const i in response.nodes) {
|
||||
response.nodes[i].geolocation = <GeolocationData>{
|
||||
country: response.nodes[i].country?.en,
|
||||
city: response.nodes[i].city?.en,
|
||||
subdivision: response.nodes[i].subdivision?.en,
|
||||
iso: response.nodes[i].iso_code,
|
||||
};
|
||||
}
|
||||
|
||||
this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`);
|
||||
return response.nodes;
|
||||
})
|
||||
|
@ -154,7 +154,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: () => {
|
||||
return `<b style="color: white">${isp[1]} (${isp[6]}%)</b><br>` +
|
||||
return `<b style="color: white">${isp[1]} (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)</b><br>` +
|
||||
$localize`${isp[4].toString()} nodes<br>` +
|
||||
$localize`${this.amountShortenerPipe.transform(isp[2] / 100000000, 2)} BTC`
|
||||
;
|
||||
|
@ -33,7 +33,7 @@
|
||||
{{ node.channels }}
|
||||
</td>
|
||||
<td class="city text-right text-truncate">
|
||||
{{ node?.city?.en ?? '-' }}
|
||||
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-per-isp',
|
||||
@ -29,6 +30,16 @@ export class NodesPerISP implements OnInit {
|
||||
id: this.route.snapshot.params.isp
|
||||
};
|
||||
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
|
||||
|
||||
for (const i in response.nodes) {
|
||||
response.nodes[i].geolocation = <GeolocationData>{
|
||||
country: response.nodes[i].country?.en,
|
||||
city: response.nodes[i].city?.en,
|
||||
subdivision: response.nodes[i].subdivision?.en,
|
||||
iso: response.nodes[i].iso_code,
|
||||
};
|
||||
}
|
||||
|
||||
return response.nodes;
|
||||
})
|
||||
);
|
||||
|
@ -48,4 +48,10 @@
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="widget && (capacityObservable$ | async) as stats">
|
||||
<div *ngIf="stats.days === 0" class="indexing-message d-flex" i18n="lightning.indexing-in-progress">
|
||||
Indexing in progress
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
@ -131,4 +131,13 @@
|
||||
display: block;
|
||||
max-width: 80px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
|
||||
.indexing-message {
|
||||
position: absolute;
|
||||
font-size: 15px;
|
||||
color: grey;
|
||||
font-weight: bold;
|
||||
margin-left: calc(50% - 85px);
|
||||
margin-top: -10px;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
@ -10,6 +10,7 @@ import { MiningService } from 'src/app/services/mining.service';
|
||||
import { download } from 'src/app/shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { isMobile } from 'src/app/shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lightning-statistics-chart',
|
||||
@ -96,12 +97,13 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
}),
|
||||
);
|
||||
}),
|
||||
)
|
||||
share(),
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
prepareChartOptions(data): void {
|
||||
let title: object;
|
||||
if (data.channel_count.length === 0) {
|
||||
if (!this.widget && data.channel_count.length === 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
@ -111,7 +113,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
} else if (this.widget) {
|
||||
} else if (data.channel_count.length > 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
@ -138,11 +140,11 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
height: this.widget ? 100 : undefined,
|
||||
top: this.widget ? 10 : 40,
|
||||
bottom: this.widget ? 0 : 70,
|
||||
right: (this.isMobile() && this.widget) ? 35 : this.right,
|
||||
left: (this.isMobile() && this.widget) ? 40 :this.left,
|
||||
right: (isMobile() && this.widget) ? 35 : this.right,
|
||||
left: (isMobile() && this.widget) ? 40 :this.left,
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile(),
|
||||
show: !isMobile(),
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
@ -155,7 +157,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: (ticks) => {
|
||||
formatter: (ticks): string => {
|
||||
let sizeString = '';
|
||||
let weightString = '';
|
||||
|
||||
@ -169,16 +171,18 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
|
||||
const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
|
||||
let tooltip = `<b style="color: white; margin-left: 18px">${date}</b><br>
|
||||
const tooltip = `
|
||||
<b style="color: white; margin-left: 18px">${date}</b><br>
|
||||
<span>${sizeString}</span><br>
|
||||
<span>${weightString}</span>`;
|
||||
<span>${weightString}</span>
|
||||
`;
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
},
|
||||
xAxis: data.channel_count.length === 0 ? undefined : {
|
||||
type: 'time',
|
||||
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
|
||||
splitNumber: (isMobile() || this.widget) ? 5 : 10,
|
||||
axisLabel: {
|
||||
hideOverlap: true,
|
||||
}
|
||||
@ -315,7 +319,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
onChartInit(ec): void {
|
||||
if (this.chartInstance !== undefined) {
|
||||
return;
|
||||
}
|
||||
@ -327,11 +331,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
onSaveChart() {
|
||||
onSaveChart(): void {
|
||||
// @ts-ignore
|
||||
const prevBottom = this.chartOptions.grid.bottom;
|
||||
const now = new Date();
|
||||
|
@ -1,3 +1,120 @@
|
||||
export function isMobile() {
|
||||
export function isMobile(): boolean {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
export function getFlagEmoji(countryCode): string {
|
||||
if (!countryCode) {
|
||||
return '';
|
||||
}
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => 127397 + char.charCodeAt());
|
||||
return String.fromCodePoint(...codePoints);
|
||||
}
|
||||
|
||||
// https://gist.github.com/calebgrove/c285a9510948b633aa47
|
||||
export function convertRegion(input, to: 'name' | 'abbreviated'): string {
|
||||
if (!input) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const states = [
|
||||
['Alabama', 'AL'],
|
||||
['Alaska', 'AK'],
|
||||
['American Samoa', 'AS'],
|
||||
['Arizona', 'AZ'],
|
||||
['Arkansas', 'AR'],
|
||||
['Armed Forces Americas', 'AA'],
|
||||
['Armed Forces Europe', 'AE'],
|
||||
['Armed Forces Pacific', 'AP'],
|
||||
['California', 'CA'],
|
||||
['Colorado', 'CO'],
|
||||
['Connecticut', 'CT'],
|
||||
['Delaware', 'DE'],
|
||||
['District Of Columbia', 'DC'],
|
||||
['Florida', 'FL'],
|
||||
['Georgia', 'GA'],
|
||||
['Guam', 'GU'],
|
||||
['Hawaii', 'HI'],
|
||||
['Idaho', 'ID'],
|
||||
['Illinois', 'IL'],
|
||||
['Indiana', 'IN'],
|
||||
['Iowa', 'IA'],
|
||||
['Kansas', 'KS'],
|
||||
['Kentucky', 'KY'],
|
||||
['Louisiana', 'LA'],
|
||||
['Maine', 'ME'],
|
||||
['Marshall Islands', 'MH'],
|
||||
['Maryland', 'MD'],
|
||||
['Massachusetts', 'MA'],
|
||||
['Michigan', 'MI'],
|
||||
['Minnesota', 'MN'],
|
||||
['Mississippi', 'MS'],
|
||||
['Missouri', 'MO'],
|
||||
['Montana', 'MT'],
|
||||
['Nebraska', 'NE'],
|
||||
['Nevada', 'NV'],
|
||||
['New Hampshire', 'NH'],
|
||||
['New Jersey', 'NJ'],
|
||||
['New Mexico', 'NM'],
|
||||
['New York', 'NY'],
|
||||
['North Carolina', 'NC'],
|
||||
['North Dakota', 'ND'],
|
||||
['Northern Mariana Islands', 'NP'],
|
||||
['Ohio', 'OH'],
|
||||
['Oklahoma', 'OK'],
|
||||
['Oregon', 'OR'],
|
||||
['Pennsylvania', 'PA'],
|
||||
['Puerto Rico', 'PR'],
|
||||
['Rhode Island', 'RI'],
|
||||
['South Carolina', 'SC'],
|
||||
['South Dakota', 'SD'],
|
||||
['Tennessee', 'TN'],
|
||||
['Texas', 'TX'],
|
||||
['US Virgin Islands', 'VI'],
|
||||
['Utah', 'UT'],
|
||||
['Vermont', 'VT'],
|
||||
['Virginia', 'VA'],
|
||||
['Washington', 'WA'],
|
||||
['West Virginia', 'WV'],
|
||||
['Wisconsin', 'WI'],
|
||||
['Wyoming', 'WY'],
|
||||
];
|
||||
|
||||
// So happy that Canada and the US have distinct abbreviations
|
||||
const provinces = [
|
||||
['Alberta', 'AB'],
|
||||
['British Columbia', 'BC'],
|
||||
['Manitoba', 'MB'],
|
||||
['New Brunswick', 'NB'],
|
||||
['Newfoundland', 'NF'],
|
||||
['Northwest Territory', 'NT'],
|
||||
['Nova Scotia', 'NS'],
|
||||
['Nunavut', 'NU'],
|
||||
['Ontario', 'ON'],
|
||||
['Prince Edward Island', 'PE'],
|
||||
['Quebec', 'QC'],
|
||||
['Saskatchewan', 'SK'],
|
||||
['Yukon', 'YT'],
|
||||
];
|
||||
|
||||
const regions = states.concat(provinces);
|
||||
|
||||
let i; // Reusable loop variable
|
||||
if (to == 'abbreviated') {
|
||||
input = input.replace(/\w\S*/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); });
|
||||
for (i = 0; i < regions.length; i++) {
|
||||
if (regions[i][0] == input) {
|
||||
return (regions[i][1]);
|
||||
}
|
||||
}
|
||||
} else if (to == 'name') {
|
||||
input = input.toUpperCase();
|
||||
for (i = 0; i < regions.length; i++) {
|
||||
if (regions[i][1] == input) {
|
||||
return (regions[i][0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
<span [innerHTML]="formattedLocation"></span>
|
@ -0,0 +1,83 @@
|
||||
import { Component, Input, OnChanges } from '@angular/core';
|
||||
import { convertRegion, getFlagEmoji } from '../../common.utils';
|
||||
|
||||
export interface GeolocationData {
|
||||
country: string;
|
||||
city: string;
|
||||
subdivision: string;
|
||||
iso: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-geolocation',
|
||||
templateUrl: './geolocation.component.html',
|
||||
styleUrls: ['./geolocation.component.scss']
|
||||
})
|
||||
export class GeolocationComponent implements OnChanges {
|
||||
@Input() data: GeolocationData;
|
||||
@Input() type: 'node' | 'list-isp' | 'list-country';
|
||||
|
||||
formattedLocation: string = '';
|
||||
|
||||
ngOnChanges(): void {
|
||||
const city = this.data.city ? this.data.city : '';
|
||||
const subdivisionLikeCity = this.data.city === this.data.subdivision;
|
||||
let subdivision = this.data.subdivision;
|
||||
|
||||
if (['US', 'CA'].includes(this.data.iso) === false || (this.type === 'node' && subdivisionLikeCity)) {
|
||||
this.data.subdivision = undefined;
|
||||
} else if (['list-isp', 'list-country'].includes(this.type) === true) {
|
||||
subdivision = convertRegion(this.data.subdivision, 'abbreviated');
|
||||
}
|
||||
|
||||
if (this.type === 'list-country') {
|
||||
if (this.data.city) {
|
||||
this.formattedLocation += ' ' + city;
|
||||
if (this.data.subdivision) {
|
||||
this.formattedLocation += ', ' + subdivision;
|
||||
}
|
||||
} else {
|
||||
this.formattedLocation += '-';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.type === 'list-isp') {
|
||||
this.formattedLocation = getFlagEmoji(this.data.iso);
|
||||
if (this.data.city) {
|
||||
this.formattedLocation += ' ' + city;
|
||||
if (this.data.subdivision) {
|
||||
this.formattedLocation += ', ' + subdivision;
|
||||
}
|
||||
} else {
|
||||
this.formattedLocation += ' ' + this.data.country;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.type === 'node') {
|
||||
const city = this.data.city ? this.data.city : '';
|
||||
|
||||
// City
|
||||
this.formattedLocation = `${city}`;
|
||||
|
||||
// ,Subdivision
|
||||
if (this.formattedLocation.length > 0 && !subdivisionLikeCity) {
|
||||
this.formattedLocation += ', ';
|
||||
}
|
||||
if (!subdivisionLikeCity) {
|
||||
this.formattedLocation += `${subdivision}`;
|
||||
}
|
||||
|
||||
// <br>[flag] County
|
||||
if (this.data?.country.length ?? 0 > 0) {
|
||||
if ((this.formattedLocation?.length ?? 0 > 0) && !subdivisionLikeCity) {
|
||||
this.formattedLocation += '<br>';
|
||||
} else if (this.data.city) {
|
||||
this.formattedLocation += ', ';
|
||||
}
|
||||
this.formattedLocation += `${this.data.country} ${getFlagEmoji(this.data.iso)}`;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
@ -91,13 +91,3 @@ export function detectWebGL() {
|
||||
return (gl && gl instanceof WebGLRenderingContext);
|
||||
}
|
||||
|
||||
export function getFlagEmoji(countryCode) {
|
||||
if (!countryCode) {
|
||||
return '';
|
||||
}
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => 127397 + char.charCodeAt());
|
||||
return String.fromCodePoint(...codePoints);
|
||||
}
|
||||
|
@ -82,6 +82,7 @@ import { SatsComponent } from './components/sats/sats.component';
|
||||
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
|
||||
import { TimestampComponent } from './components/timestamp/timestamp.component';
|
||||
import { ToggleComponent } from './components/toggle/toggle.component';
|
||||
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -158,6 +159,7 @@ import { ToggleComponent } from './components/toggle/toggle.component';
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
ToggleComponent,
|
||||
GeolocationComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -261,6 +263,7 @@ import { ToggleComponent } from './components/toggle/toggle.component';
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
ToggleComponent,
|
||||
GeolocationComponent,
|
||||
]
|
||||
})
|
||||
export class SharedModule {
|
||||
|
Loading…
x
Reference in New Issue
Block a user