Compare commits
19 Commits
translatio
...
mononaut/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd300578b6 | ||
|
|
9fc3a9130b | ||
|
|
601f0ea671 | ||
|
|
341da85c77 | ||
|
|
0d8f63feff | ||
|
|
e7af43efa2 | ||
|
|
aca2f2ec7d | ||
|
|
803b005880 | ||
|
|
204d54b189 | ||
|
|
c248544fe8 | ||
|
|
b65d00f289 | ||
|
|
f77dc68ec7 | ||
|
|
c4ec50b771 | ||
|
|
8529b99675 | ||
|
|
cd02d89235 | ||
|
|
4dcbccd9b2 | ||
|
|
6a4aeaf7ed | ||
|
|
6432f72664 | ||
|
|
f6ab2caaf9 |
@@ -119,7 +119,11 @@ class RbfCache {
|
||||
|
||||
|
||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||
if ( !newTxExtended
|
||||
|| !replaced?.length
|
||||
|| this.txs.has(newTxExtended.txid)
|
||||
|| !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ class ServicesRoutes {
|
||||
public initRoutes(app: Application): void {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/custom/config', this.$getCustomConfig)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -22,6 +23,11 @@ class ServicesRoutes {
|
||||
handleError(req, res, 500, 'Failed to get wallet');
|
||||
}
|
||||
}
|
||||
|
||||
// serve a blank custom config file by default
|
||||
private async $getCustomConfig(req: Request, res: Response): Promise<void> {
|
||||
res.status(200).contentType('application/javascript').send('');
|
||||
}
|
||||
}
|
||||
|
||||
export default new ServicesRoutes();
|
||||
|
||||
@@ -478,25 +478,30 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
extendSummary(summary) {
|
||||
let extendedSummary = summary.slice();
|
||||
const extendedSummary = summary.slice();
|
||||
|
||||
// Add a point at today's date to make the graph end at the current time
|
||||
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
|
||||
extendedSummary.reverse();
|
||||
|
||||
let oneHour = 60 * 60;
|
||||
let maxTime = Date.now() / 1000;
|
||||
|
||||
const oneHour = 60 * 60;
|
||||
// Fill gaps longer than interval
|
||||
for (let i = 0; i < extendedSummary.length - 1; i++) {
|
||||
let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);
|
||||
if (extendedSummary[i].time > maxTime) {
|
||||
extendedSummary[i].time = maxTime - 30;
|
||||
}
|
||||
maxTime = extendedSummary[i].time;
|
||||
const hours = Math.floor((extendedSummary[i].time - extendedSummary[i + 1].time) / oneHour);
|
||||
if (hours > 1) {
|
||||
for (let j = 1; j < hours; j++) {
|
||||
let newTime = extendedSummary[i].time + oneHour * j;
|
||||
const newTime = extendedSummary[i].time - oneHour * j;
|
||||
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
|
||||
}
|
||||
i += hours - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return extendedSummary.reverse();
|
||||
return extendedSummary;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export class AppComponent implements OnInit {
|
||||
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
handleKeyboardEvents(event: KeyboardEvent) {
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
||||
return;
|
||||
}
|
||||
// prevent arrow key horizontal scrolling
|
||||
|
||||
@@ -281,9 +281,11 @@
|
||||
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<span class="title-link">
|
||||
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/wallet/' + widget.props.wallet | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
|
||||
</span>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||
</a>
|
||||
<app-address-transactions-widget [addressSummary$]="walletSummary$"></app-address-transactions-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
[height]="tx?.status?.block_height"
|
||||
[replaced]="replaced"
|
||||
[removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
|
||||
[cached]="isCached"
|
||||
></app-confirmations>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
|
||||
<div class="title-address">
|
||||
<h1 i18n="shared.wallet">Wallet</h1>
|
||||
<h1>{{ walletName }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
@@ -74,6 +74,36 @@
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="title-tx">
|
||||
<h2 class="text-left" i18n="address.transactions">Transactions</h2>
|
||||
</div>
|
||||
|
||||
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [addresses]="addressStrings" (loadMore)="loadMore()"></app-transactions-list>
|
||||
|
||||
<div class="text-center">
|
||||
<ng-template [ngIf]="isLoadingTransactions">
|
||||
<div class="header-bg box">
|
||||
<div class="row" style="height: 107px;">
|
||||
<div class="col-sm">
|
||||
<span class="skeleton-loader"></span>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<span class="skeleton-loader"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="retryLoadMore">
|
||||
<br>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="loadMore()"><fa-icon [icon]="['fas', 'redo-alt']" [fixedWidth]="true"></fa-icon></button>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-template #loadingTemplate>
|
||||
|
||||
<div class="box" *ngIf="!error; else errorTemplate">
|
||||
|
||||
@@ -9,6 +9,8 @@ import { of, Observable, Subscription } from 'rxjs';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||
import { WalletAddress } from '@interfaces/node-api.interface';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
|
||||
class WalletStats implements ChainStats {
|
||||
addresses: string[];
|
||||
@@ -24,6 +26,7 @@ class WalletStats implements ChainStats {
|
||||
acc.funded_txo_sum += stat.funded_txo_sum;
|
||||
acc.spent_txo_count += stat.spent_txo_count;
|
||||
acc.spent_txo_sum += stat.spent_txo_sum;
|
||||
acc.tx_count += stat.tx_count;
|
||||
return acc;
|
||||
}, {
|
||||
funded_txo_count: 0,
|
||||
@@ -109,12 +112,17 @@ export class WalletComponent implements OnInit, OnDestroy {
|
||||
addressStrings: string[] = [];
|
||||
walletName: string;
|
||||
isLoadingWallet = true;
|
||||
isLoadingTransactions = true;
|
||||
transactions: Transaction[];
|
||||
totalTransactionCount: number;
|
||||
retryLoadMore = false;
|
||||
wallet$: Observable<Record<string, WalletAddress>>;
|
||||
walletAddresses$: Observable<Record<string, Address>>;
|
||||
walletSummary$: Observable<AddressTxSummary[]>;
|
||||
walletStats$: Observable<WalletStats>;
|
||||
error: any;
|
||||
walletSubscription: Subscription;
|
||||
transactionSubscription: Subscription;
|
||||
|
||||
collapseAddresses: boolean = true;
|
||||
|
||||
@@ -129,6 +137,8 @@ export class WalletComponent implements OnInit, OnDestroy {
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private audioService: AudioService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
@@ -172,6 +182,21 @@ export class WalletComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
switchMap(initial => this.stateService.walletTransactions$.pipe(
|
||||
startWith(null),
|
||||
tap((transactions) => {
|
||||
if (!transactions?.length) {
|
||||
return;
|
||||
}
|
||||
for (const transaction of transactions) {
|
||||
const tx = this.transactions.find((t) => t.txid === transaction.txid);
|
||||
if (tx) {
|
||||
tx.status = transaction.status;
|
||||
} else {
|
||||
this.transactions.unshift(transaction);
|
||||
}
|
||||
}
|
||||
this.transactions = this.transactions.slice();
|
||||
this.audioService.playSound('magic');
|
||||
}),
|
||||
scan((wallet, walletTransactions) => {
|
||||
for (const tx of (walletTransactions || [])) {
|
||||
const funded: Record<string, number> = {};
|
||||
@@ -267,8 +292,57 @@ export class WalletComponent implements OnInit, OnDestroy {
|
||||
return stats;
|
||||
}, walletStats),
|
||||
);
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
this.transactionSubscription = this.wallet$.pipe(
|
||||
switchMap(wallet => {
|
||||
const addresses = Object.keys(wallet).map(addr => this.normalizeAddress(addr));
|
||||
return this.electrsApiService.getAddressesTransactions$(addresses);
|
||||
}),
|
||||
map(transactions => {
|
||||
// only confirmed transactions supported for now
|
||||
return transactions.filter(tx => tx.status.confirmed).sort((a, b) => b.status.block_height - a.status.block_height);
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.log(error);
|
||||
this.error = error;
|
||||
this.seoService.logSoft404();
|
||||
this.isLoadingWallet = false;
|
||||
return of([]);
|
||||
})
|
||||
).subscribe((transactions: Transaction[] | null) => {
|
||||
if (!transactions) {
|
||||
return;
|
||||
}
|
||||
this.transactions = transactions;
|
||||
this.isLoadingTransactions = false;
|
||||
});
|
||||
}
|
||||
|
||||
loadMore(): void {
|
||||
if (this.isLoadingTransactions || this.fullyLoaded) {
|
||||
return;
|
||||
}
|
||||
this.isLoadingTransactions = true;
|
||||
this.retryLoadMore = false;
|
||||
this.electrsApiService.getAddressesTransactions$(this.addressStrings, this.transactions[this.transactions.length - 1].txid)
|
||||
.subscribe((transactions: Transaction[]) => {
|
||||
if (transactions && transactions.length) {
|
||||
this.transactions = this.transactions.concat(transactions.sort((a, b) => b.status.block_height - a.status.block_height));
|
||||
} else {
|
||||
this.fullyLoaded = true;
|
||||
}
|
||||
this.isLoadingTransactions = false;
|
||||
},
|
||||
(error) => {
|
||||
this.isLoadingTransactions = false;
|
||||
this.retryLoadMore = true;
|
||||
// In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page.
|
||||
if (error.status === 422) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
|
||||
@@ -299,5 +373,6 @@ export class WalletComponent implements OnInit, OnDestroy {
|
||||
ngOnDestroy(): void {
|
||||
this.websocketService.stopTrackingWallet();
|
||||
this.walletSubscription.unsubscribe();
|
||||
this.transactionSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface";
|
||||
import { AddressTxSummary, Block, ChainStats } from "./electrs.interface";
|
||||
|
||||
export interface OptimizedMempoolStats {
|
||||
added: number;
|
||||
|
||||
@@ -142,12 +142,16 @@ export class ElectrsApiService {
|
||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
||||
}
|
||||
|
||||
getAddressesTransactions$(addresses: string[], txid?: string): Observable<Transaction[]> {
|
||||
getAddressesTransactions$(addresses: string[], txid?: string): Observable<Transaction[]> {
|
||||
let params = new HttpParams();
|
||||
if (txid) {
|
||||
params = params.append('after_txid', txid);
|
||||
}
|
||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs?addresses=${addresses.join(',')}`, { params });
|
||||
return this.httpClient.post<Transaction[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs',
|
||||
addresses,
|
||||
{ params }
|
||||
);
|
||||
}
|
||||
|
||||
getAddressSummary$(address: string, txid?: string): Observable<AddressTxSummary[]> {
|
||||
@@ -163,7 +167,7 @@ export class ElectrsApiService {
|
||||
if (txid) {
|
||||
params = params.append('after_txid', txid);
|
||||
}
|
||||
return this.httpClient.get<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs/summary?addresses=${addresses.join(',')}`, { params });
|
||||
return this.httpClient.post<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs/summary', addresses, { params });
|
||||
}
|
||||
|
||||
getScriptHashTransactions$(script: string, txid?: string): Observable<Transaction[]> {
|
||||
@@ -182,7 +186,7 @@ export class ElectrsApiService {
|
||||
params = params.append('after_txid', txid);
|
||||
}
|
||||
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
|
||||
switchMap(scriptHashes => this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs?scripthashes=${scriptHashes.join(',')}`, { params })),
|
||||
switchMap(scriptHashes => this.httpClient.post<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs', scriptHashes, { params })),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,7 +216,7 @@ export class ElectrsApiService {
|
||||
params = params.append('after_txid', txid);
|
||||
}
|
||||
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
|
||||
switchMap(scriptHashes => this.httpClient.get<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs/summary?scripthashes=${scriptHashes.join(',')}`, { params })),
|
||||
switchMap(scriptHashes => this.httpClient.post<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs/summary', scriptHashes, { params })),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DOCUMENT } from '@angular/common';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Customization, StateService } from '@app/services/state.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@@ -51,6 +51,8 @@ export class EnterpriseService {
|
||||
if (this.stateService.env.customize?.branding) {
|
||||
const info = this.stateService.env.customize?.branding;
|
||||
this.insertMatomo(info.site_id);
|
||||
this.setFavicons(this.stateService.env.customize);
|
||||
this.seoService.setCustomMeta(this.stateService.env.customize);
|
||||
this.seoService.setEnterpriseTitle(info.title, true);
|
||||
this.info$.next(info);
|
||||
} else {
|
||||
@@ -67,6 +69,50 @@ export class EnterpriseService {
|
||||
}
|
||||
}
|
||||
|
||||
setFavicons(customize: Customization): void {
|
||||
const enterprise = customize.enterprise;
|
||||
const head = this.document.getElementsByTagName('head')[0];
|
||||
|
||||
const faviconLinks = [
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
sizes: '180x180',
|
||||
href: `/resources/${enterprise}/favicons/apple-touch-icon.png`
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '32x32',
|
||||
href: `/resources/${enterprise}/favicons/favicon-32x32.png`
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '16x16',
|
||||
href: `/resources/${enterprise}/favicons/favicon-16x16.png`
|
||||
},
|
||||
{
|
||||
rel: 'manifest',
|
||||
href: `/resources/${enterprise}/favicons/site.webmanifest`
|
||||
},
|
||||
{
|
||||
rel: 'shortcut icon',
|
||||
href: `/resources/${enterprise}/favicons/favicon.ico`
|
||||
}
|
||||
];
|
||||
|
||||
faviconLinks.forEach(linkInfo => {
|
||||
let link = this.document.querySelector(`link[rel="${linkInfo.rel}"]${linkInfo.sizes ? `[sizes="${linkInfo.sizes}"]` : ''}`) as HTMLLinkElement;
|
||||
if (!link) {
|
||||
link = this.document.createElement('link');
|
||||
head.appendChild(link);
|
||||
}
|
||||
Object.entries(linkInfo).forEach(([attr, value]) => {
|
||||
link.setAttribute(attr, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
insertMatomo(siteId?: number): void {
|
||||
let statsUrl = '//stats.mempool.space/';
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ import { LanguageService } from '@app/services/language.service';
|
||||
export class OpenGraphService {
|
||||
network = '';
|
||||
defaultImageUrl = '';
|
||||
defaultImageType = 'image/png';
|
||||
defaultImageWidth = '1000';
|
||||
defaultImageHeight = '500';
|
||||
previewLoadingEvents = {};
|
||||
previewLoadingCount = 0;
|
||||
|
||||
@@ -25,12 +28,17 @@ export class OpenGraphService {
|
||||
) {
|
||||
// save og:image tag from original template
|
||||
const initialOgImageTag = metaService.getTag("property='og:image'");
|
||||
this.defaultImageUrl = initialOgImageTag?.content || 'https://mempool.space/resources/previews/mempool-space-preview.jpg';
|
||||
this.defaultImageUrl = (this.stateService.env.customize?.meta?.image?.src ? this.stateService.env.customize.meta.image.src : initialOgImageTag?.content) || 'https://mempool.space/resources/previews/mempool-space-preview.jpg';
|
||||
this.defaultImageType = (this.stateService.env.customize?.meta?.image?.type ? this.stateService.env.customize.meta.image.type : 'image/png');
|
||||
this.defaultImageWidth = (this.stateService.env.customize?.meta?.image?.width ? this.stateService.env.customize.meta.image.width : '1000');
|
||||
this.defaultImageHeight = (this.stateService.env.customize?.meta?.image?.height ? this.stateService.env.customize.meta.image.height : '500');
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
map(() => this.activatedRoute),
|
||||
map(route => {
|
||||
while (route.firstChild) route = route.firstChild;
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
return route;
|
||||
}),
|
||||
filter(route => route.outlet === 'primary'),
|
||||
@@ -45,7 +53,7 @@ export class OpenGraphService {
|
||||
|
||||
// expose routing method to global scope, so we can access it from the unfurler
|
||||
window['ogService'] = {
|
||||
loadPage: (path) => { return this.loadPage(path) }
|
||||
loadPage: (path) => { return this.loadPage(path); }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,9 +70,9 @@ export class OpenGraphService {
|
||||
clearOgImage() {
|
||||
this.metaService.updateTag({ property: 'og:image', content: this.defaultImageUrl });
|
||||
this.metaService.updateTag({ name: 'twitter:image', content: this.defaultImageUrl });
|
||||
this.metaService.updateTag({ property: 'og:image:type', content: 'image/png' });
|
||||
this.metaService.updateTag({ property: 'og:image:width', content: '1000' });
|
||||
this.metaService.updateTag({ property: 'og:image:height', content: '500' });
|
||||
this.metaService.updateTag({ property: 'og:image:type', content: this.defaultImageType });
|
||||
this.metaService.updateTag({ property: 'og:image:width', content: this.defaultImageWidth });
|
||||
this.metaService.updateTag({ property: 'og:image:height', content: this.defaultImageHeight });
|
||||
}
|
||||
|
||||
setManualOgImage(imageFilename) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { Title, Meta } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { filter, map, switchMap } from 'rxjs';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Customization, StateService } from '@app/services/state.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -23,13 +23,17 @@ export class SeoService {
|
||||
private activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
// save original meta tags
|
||||
this.baseDescription = metaService.getTag('name=\'description\'')?.content || this.baseDescription;
|
||||
this.baseTitle = titleService.getTitle()?.split(' - ')?.[0] || this.baseTitle;
|
||||
try {
|
||||
const canonicalUrl = new URL(this.canonicalLink?.href || '');
|
||||
this.baseDomain = canonicalUrl?.host;
|
||||
} catch (e) {
|
||||
// leave as default
|
||||
this.baseDescription = this.stateService.env.customize?.meta?.description || metaService.getTag('name=\'description\'')?.content || this.baseDescription;
|
||||
this.baseTitle = this.stateService.env.customize?.meta?.title || titleService.getTitle()?.split(' - ')?.[0] || this.baseTitle;
|
||||
if (this.stateService.env.customize?.domains?.length) {
|
||||
this.baseDomain = this.stateService.env.customize.domains[0];
|
||||
} else {
|
||||
try {
|
||||
const canonicalUrl = new URL(this.canonicalLink?.href || '');
|
||||
this.baseDomain = canonicalUrl?.host;
|
||||
} catch (e) {
|
||||
// leave as default
|
||||
}
|
||||
}
|
||||
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
@@ -70,6 +74,22 @@ export class SeoService {
|
||||
this.resetTitle();
|
||||
}
|
||||
|
||||
setCustomMeta(customize: Customization) {
|
||||
if (!customize.meta) {
|
||||
return;
|
||||
}
|
||||
this.metaService.updateTag({ name: 'description', content: customize.meta.description});
|
||||
this.metaService.updateTag({ name: 'twitter:description', content: customize.meta.description});
|
||||
this.metaService.updateTag({ property: 'og:description', content: customize.meta.description});
|
||||
this.metaService.updateTag({ name: 'twitter:image', content: customize.meta.image.src});
|
||||
this.metaService.updateTag({ property: 'og:image', content: customize.meta.image.src});
|
||||
this.metaService.updateTag({ property: 'og:image:type', content: customize.meta.image.type});
|
||||
this.metaService.updateTag({ property: 'og:image:width', content: customize.meta.image.width});
|
||||
this.metaService.updateTag({ property: 'og:image:height', content: customize.meta.image.height});
|
||||
const domain = customize.domains?.[0] || window.location.hostname;
|
||||
this.metaService.updateTag({ name: 'twitter:domain', content: domain});
|
||||
}
|
||||
|
||||
setDescription(newDescription: string): void {
|
||||
this.metaService.updateTag({ name: 'description', content: newDescription});
|
||||
this.metaService.updateTag({ name: 'twitter:description', content: newDescription});
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface MarkBlockState {
|
||||
export interface ILoadingIndicators { [name: string]: number; }
|
||||
|
||||
export interface Customization {
|
||||
domains: string[];
|
||||
theme: string;
|
||||
enterprise?: string;
|
||||
branding: {
|
||||
@@ -33,6 +34,16 @@ export interface Customization {
|
||||
footer_img?: string;
|
||||
rounded_corner: boolean;
|
||||
},
|
||||
meta: {
|
||||
title: string;
|
||||
description: string;
|
||||
image: {
|
||||
src: string;
|
||||
type: string;
|
||||
width: string;
|
||||
height: string;
|
||||
};
|
||||
};
|
||||
dashboard: {
|
||||
widgets: {
|
||||
component: string;
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
|
||||
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && removed">
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && (removed || cached)">
|
||||
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !removed">
|
||||
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !(removed || cached)">
|
||||
<button type="button" class="btn btn-sm btn-danger no-cursor {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
||||
</ng-template>
|
||||
@@ -12,6 +12,7 @@ export class ConfirmationsComponent implements OnChanges {
|
||||
@Input() height: number;
|
||||
@Input() replaced: boolean = false;
|
||||
@Input() removed: boolean = false;
|
||||
@Input() cached: boolean = false;
|
||||
@Input() hideUnconfirmed: boolean = false;
|
||||
@Input() buttonClass: string = '';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="utf-8">
|
||||
<title>mempool - Bitcoin Explorer</title>
|
||||
<script src="/resources/config.js"></script>
|
||||
<script src="/resources/customize.js"></script>
|
||||
<script src="/api/v1/services/custom/config"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
|
||||
|
||||
@@ -1567,7 +1567,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="bdb8bbb38e4ca3c73e19dc4167fbe4aec316f818" datatype="html">
|
||||
<source>Total Bid Boost</source>
|
||||
<target>Total frais ajoutés</target>
|
||||
<target>Augmentation totale des frais</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
@@ -1728,7 +1728,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="57cde27765d527a0d9195212fa5a7ce06408c827" datatype="html">
|
||||
<source>Bid Boost</source>
|
||||
<target>Frais ajoutés</target>
|
||||
<target>Augmentation des frais</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/accelerations-list/accelerations-list.component.html</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
@@ -6739,7 +6739,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="date-base.just-now" datatype="html">
|
||||
<source>Just now</source>
|
||||
<target>À l'instant</target>
|
||||
<target>Juste maintenant</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
|
||||
<context context-type="linenumber">111</context>
|
||||
@@ -9847,7 +9847,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="8e623d3cfecb7c560c114390db53c1f430ffd0de" datatype="html">
|
||||
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> confirmation</source>
|
||||
<target><x id="INTERPOLATION" equiv-text="{{ i }}"/> confirmation</target>
|
||||
<target><x id="INTERPOLATION" equiv-text="{{ i }}"/>confirmation</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/components/confirmations/confirmations.component.html</context>
|
||||
<context context-type="linenumber">4</context>
|
||||
|
||||
@@ -140,7 +140,8 @@ location @mempool-api-v1-cache-normal {
|
||||
proxy_cache_valid 200 2s;
|
||||
proxy_redirect off;
|
||||
|
||||
expires 2s;
|
||||
# cache for 2 seconds on server, but send expires -1 so browser doesn't cache
|
||||
expires -1;
|
||||
}
|
||||
|
||||
location @mempool-api-v1-cache-disabled {
|
||||
|
||||
Reference in New Issue
Block a user