mirror of
https://github.com/mempool/mempool.git
synced 2025-10-05 21:53:06 +02:00
Support for sighash display preferences
This commit is contained in:
@@ -147,7 +147,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<app-transactions-list #txList [transactions]="[transaction]" [transactionPage]="true" [txPreview]="true" [signatures]="true"></app-transactions-list>
|
<app-transactions-list #txList [transactions]="[transaction]" [transactionPage]="true" [txPreview]="true" forceSignaturesMode="all"></app-transactions-list>
|
||||||
|
|
||||||
<div class="title text-left">
|
<div class="title text-left">
|
||||||
<h2 i18n="transaction.details|Transaction Details">Details</h2>
|
<h2 i18n="transaction.details|Transaction Details">Details</h2>
|
||||||
|
@@ -55,21 +55,21 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
@if (signatures) {
|
@if (tx['_showSignatures']) {
|
||||||
<td class="sig-td">
|
<td class="sig-td">
|
||||||
<div class="sig-stack">
|
<div class="sig-stack">
|
||||||
@if (tx['_sigs'][vindex].length === 0) {
|
@if (tx['_sigs'][vindex].length === 0 && signaturesMode === 'all') {
|
||||||
<span class="sig sig-key sig-no-lock" title="unsigned">
|
<span class="sig sig-key sig-no-lock" ngbTooltip="unsigned">
|
||||||
<fa-icon [icon]="['fas', 'lock-open']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'lock-open']" [fixedWidth]="true"></fa-icon>
|
||||||
</span>
|
</span>
|
||||||
} @else {
|
} @else if (showSig(tx['_sigs'][vindex])) {
|
||||||
@for (sig of tx['_sigs'][vindex].slice(0, 7); track sig.signature; let idx = $index) {
|
@for (sig of tx['_sigs'][vindex].slice(0, 7); track sig.signature; let idx = $index) {
|
||||||
@if (idx < 7) {
|
@if (idx < 7) {
|
||||||
<span
|
<span
|
||||||
class="sig sig-key sighash-{{sig.sighash}}"
|
class="sig sig-key sighash-{{sig.sighash}}"
|
||||||
(mouseenter)="showSigInfo(i, vindex, sig)"
|
(mouseenter)="showSigInfo(i, vindex, sig)"
|
||||||
(mouseleave)="hideSigInfo()"
|
(mouseleave)="hideSigInfo()"
|
||||||
[title]="sighashLabels[sig.sighash]"
|
[ngbTooltip]="sighashLabels[sig.sighash]"
|
||||||
[class.hovered]="selectedSig && selectedSig.txIndex === i && selectedSig.vindex === vindex && selectedSig.sig === sig"
|
[class.hovered]="selectedSig && selectedSig.txIndex === i && selectedSig.vindex === vindex && selectedSig.sig === sig"
|
||||||
>
|
>
|
||||||
<fa-icon [icon]="['fas', 'key']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'key']" [fixedWidth]="true"></fa-icon>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (tx['_sigs'][vindex].length > 7) {
|
@if (tx['_sigs'][vindex].length > 7) {
|
||||||
<span class="sig sig-key sig-overflow">
|
<span class="sig sig-overflow">
|
||||||
+{{ tx['_sigs'][vindex].length - 7 }}
|
+{{ tx['_sigs'][vindex].length - 7 }}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
'sigged': selectedSig && selectedSig.txIndex === i && sigHighlights.vin[vindex],
|
'sigged': selectedSig && selectedSig.txIndex === i && sigHighlights.vin[vindex],
|
||||||
}">
|
}">
|
||||||
<td></td>
|
<td></td>
|
||||||
@if (signatures) {
|
@if (tx['_showSignatures']) {
|
||||||
<td></td>
|
<td></td>
|
||||||
}
|
}
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="(showDetails$ | async) === true">
|
<tr *ngIf="(showDetails$ | async) === true">
|
||||||
<td [attr.colspan]="signatures ? 4 : 3" class="details-container" >
|
<td [attr.colspan]="tx['_showSignatures'] ? 4 : 3" class="details-container" >
|
||||||
<table class="table table-striped table-fixed table-borderless details-table mb-3">
|
<table class="table table-striped table-fixed table-borderless details-table mb-3">
|
||||||
<tbody>
|
<tbody>
|
||||||
<ng-template [ngIf]="vin.scriptsig">
|
<ng-template [ngIf]="vin.scriptsig">
|
||||||
@@ -174,12 +174,12 @@
|
|||||||
<td style="text-align: left;">
|
<td style="text-align: left;">
|
||||||
<ng-container *ngFor="let witness of vin.witness; index as windex">
|
<ng-container *ngFor="let witness of vin.witness; index as windex">
|
||||||
<p class="witness-item">
|
<p class="witness-item">
|
||||||
<ng-container *ngIf="signatures && tx['_sigmap'][witness] as sigInfo">
|
<ng-container *ngIf="tx['_sigmap'][witness] as sigInfo">
|
||||||
<span class="sig sig-key sig-inline sighash-{{sigInfo.sig.sighash}}"
|
<span class="sig sig-key sig-inline sighash-{{sigInfo.sig.sighash}}"
|
||||||
[class.hovered]="selectedSig && selectedSig.txIndex === i && selectedSig.vindex === vindex && selectedSig.sig === sigInfo.sig"
|
[class.hovered]="selectedSig && selectedSig.txIndex === i && selectedSig.vindex === vindex && selectedSig.sig === sigInfo.sig"
|
||||||
(mouseenter)="showSigInfo(i, vindex, sigInfo.sig)"
|
(mouseenter)="showSigInfo(i, vindex, sigInfo.sig)"
|
||||||
(mouseleave)="hideSigInfo()"
|
(mouseleave)="hideSigInfo()"
|
||||||
[title]="sighashLabels[sigInfo.sig.sighash]">
|
[ngbTooltip]="sighashLabels[sigInfo.sig.sighash]">
|
||||||
<fa-icon [icon]="['fas', 'key']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'key']" [fixedWidth]="true"></fa-icon>
|
||||||
</span>
|
</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -190,13 +190,13 @@
|
|||||||
{{ witness }}
|
{{ witness }}
|
||||||
}
|
}
|
||||||
} @else if (witness) {
|
} @else if (witness) {
|
||||||
<ng-container *ngIf="signatures && tx['_sigmap'][witness]?.sig.sighash !== 0 && tx['_sigmap'][witness] as sigInfo; else plainSig">
|
<ng-container *ngIf="tx['_sigmap'][witness]?.sig.sighash !== 0 && tx['_sigmap'][witness] as sigInfo; else plainSig">
|
||||||
<span class="witness">
|
<span class="witness">
|
||||||
{{witness.slice(0, -2)}}<span class="sig sighash-{{sigInfo.sig.sighash}}"
|
{{witness.slice(0, -2)}}<span class="sig sighash-{{sigInfo.sig.sighash}}"
|
||||||
[class.hovered]="selectedSig && selectedSig.txIndex === i && selectedSig.vindex === vindex && selectedSig.sig === sigInfo.sig"
|
[class.hovered]="selectedSig && selectedSig.txIndex === i && selectedSig.vindex === vindex && selectedSig.sig === sigInfo.sig"
|
||||||
(mouseenter)="showSigInfo(i, vindex, sigInfo.sig)"
|
(mouseenter)="showSigInfo(i, vindex, sigInfo.sig)"
|
||||||
(mouseleave)="hideSigInfo()"
|
(mouseleave)="hideSigInfo()"
|
||||||
[title]="sighashLabels[sigInfo.sig.sighash]">{{witness.slice(-2)}}</span>
|
[ngbTooltip]="sighashLabels[sigInfo.sig.sighash]">{{witness.slice(-2)}}</span>
|
||||||
</span>
|
</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #plainSig>
|
<ng-template #plainSig>
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<tr *ngIf="tx.vin.length > getVinLimit(tx)">
|
<tr *ngIf="tx.vin.length > getVinLimit(tx)">
|
||||||
<td [attr.colspan]="signatures ? 4 : 3" class="text-center">
|
<td [attr.colspan]="tx['_showSignatures'] ? 4 : 3" class="text-center">
|
||||||
<button class="btn btn-sm btn-primary mt-2" (click)="showMoreInputs(tx)">
|
<button class="btn btn-sm btn-primary mt-2" (click)="showMoreInputs(tx)">
|
||||||
<span *ngIf="getVinLimit(tx, true) >= tx.vin.length; else showMoreInputsLabel" i18n="show-all">Show all</span>
|
<span *ngIf="getVinLimit(tx, true) >= tx.vin.length; else showMoreInputsLabel" i18n="show-all">Show all</span>
|
||||||
<ng-template #showMoreInputsLabel>
|
<ng-template #showMoreInputsLabel>
|
||||||
@@ -374,7 +374,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="(showDetails$ | async) === true">
|
<tr *ngIf="(showDetails$ | async) === true">
|
||||||
<td [attr.colspan]="signatures ? 4 : 3" class=" details-container" >
|
<td [attr.colspan]="tx['_showSignatures'] ? 4 : 3" class=" details-container" >
|
||||||
<table class="table table-striped table-borderless details-table mb-3">
|
<table class="table table-striped table-borderless details-table mb-3">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
|
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||||
import { StateService } from '@app/services/state.service';
|
import { StateService, SignaturesMode } from '@app/services/state.service';
|
||||||
import { CacheService } from '@app/services/cache.service';
|
import { CacheService } from '@app/services/cache.service';
|
||||||
import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription, of, forkJoin } from 'rxjs';
|
import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription, of, forkJoin } from 'rxjs';
|
||||||
import { Outspend, Transaction, Vin, Vout } from '@interfaces/electrs.interface';
|
import { Outspend, Transaction, Vin, Vout } from '@interfaces/electrs.interface';
|
||||||
@@ -16,6 +16,8 @@ import { Inscription } from '@app/shared/ord/inscription.utils';
|
|||||||
import { Etching, Runestone } from '@app/shared/ord/rune.utils';
|
import { Etching, Runestone } from '@app/shared/ord/rune.utils';
|
||||||
import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressType, AddressTypeInfo, checkedCompareAddressStrings, detectAddressType } from '@app/shared/address-utils';
|
import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressType, AddressTypeInfo, checkedCompareAddressStrings, detectAddressType } from '@app/shared/address-utils';
|
||||||
import { processInputSignatures, Sighash, SigInfo, SighashLabels } from '@app/shared/transaction.utils';
|
import { processInputSignatures, Sighash, SigInfo, SighashLabels } from '@app/shared/transaction.utils';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { SighashFlag } from '../../shared/transaction.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-transactions-list',
|
selector: 'app-transactions-list',
|
||||||
@@ -23,7 +25,7 @@ import { processInputSignatures, Sighash, SigInfo, SighashLabels } from '@app/sh
|
|||||||
styleUrls: ['./transactions-list.component.scss'],
|
styleUrls: ['./transactions-list.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class TransactionsListComponent implements OnInit, OnChanges {
|
export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
network = '';
|
network = '';
|
||||||
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
||||||
showMoreIncrement = 1000;
|
showMoreIncrement = 1000;
|
||||||
@@ -40,13 +42,16 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
@Input() rowLimit = 12;
|
@Input() rowLimit = 12;
|
||||||
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
|
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
|
||||||
@Input() txPreview = false;
|
@Input() txPreview = false;
|
||||||
@Input() signatures = true;
|
@Input() forceSignaturesMode: SignaturesMode = null;
|
||||||
|
|
||||||
@Output() loadMore = new EventEmitter();
|
@Output() loadMore = new EventEmitter();
|
||||||
|
|
||||||
latestBlock$: Observable<BlockExtended>;
|
latestBlock$: Observable<BlockExtended>;
|
||||||
outspendsSubscription: Subscription;
|
outspendsSubscription: Subscription;
|
||||||
currencyChangeSubscription: Subscription;
|
currencyChangeSubscription: Subscription;
|
||||||
|
networkSubscription: Subscription;
|
||||||
|
signaturesSubscription: Subscription;
|
||||||
|
queryParamsSubscription: Subscription;
|
||||||
currency: string;
|
currency: string;
|
||||||
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
|
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
|
||||||
refreshChannels$: ReplaySubject<string[]> = new ReplaySubject();
|
refreshChannels$: ReplaySubject<string[]> = new ReplaySubject();
|
||||||
@@ -64,6 +69,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
sigHighlights: { vin: boolean[], vout: boolean[] } = { vin: [], vout: [] };
|
sigHighlights: { vin: boolean[], vout: boolean[] } = { vin: [], vout: [] };
|
||||||
sighashLabels = SighashLabels;
|
sighashLabels = SighashLabels;
|
||||||
|
|
||||||
|
signaturesPreference: SignaturesMode = null;
|
||||||
|
signaturesOverride: SignaturesMode = null;
|
||||||
|
signaturesMode: SignaturesMode = 'interesting';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
@@ -74,11 +83,29 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
private ref: ChangeDetectorRef,
|
private ref: ChangeDetectorRef,
|
||||||
private priceService: PriceService,
|
private priceService: PriceService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
) { }
|
private route: ActivatedRoute,
|
||||||
|
) {
|
||||||
|
this.signaturesMode = this.forceSignaturesMode || this.stateService.signaturesMode$.value;
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
|
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
|
||||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
this.networkSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||||
|
this.signaturesSubscription = this.stateService.signaturesMode$.subscribe((mode) => {
|
||||||
|
this.signaturesMode = mode;
|
||||||
|
this.updateSignaturesMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
||||||
|
console.log('query params', params);
|
||||||
|
if (params['sigs'] && ['all', 'interesting', 'none'].includes(params['sigs'])) {
|
||||||
|
this.signaturesOverride = params['sigs'] as SignaturesMode;
|
||||||
|
this.updateSignaturesMode();
|
||||||
|
} else {
|
||||||
|
this.signaturesOverride = null;
|
||||||
|
this.updateSignaturesMode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
||||||
this.assetsService.getAssetsMinimalJson$.subscribe((assets) => {
|
this.assetsService.getAssetsMinimalJson$.subscribe((assets) => {
|
||||||
@@ -206,12 +233,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
|
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
|
||||||
|
|
||||||
this.transactions.forEach((tx) => {
|
this.transactions.forEach((tx) => {
|
||||||
tx['@voutLimit'] = true;
|
tx['@voutLimit'] = true;
|
||||||
tx['@vinLimit'] = true;
|
tx['@vinLimit'] = true;
|
||||||
if (tx['addressValue'] !== undefined) {
|
tx['_showSignatures'] = false;
|
||||||
return;
|
tx['_interestingSignatures'] = false;
|
||||||
}
|
|
||||||
|
|
||||||
if (this.addresses?.length) {
|
if (this.addresses?.length) {
|
||||||
const addressIn = tx.vout.map(v => {
|
const addressIn = tx.vout.map(v => {
|
||||||
@@ -293,6 +320,11 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
if (!tx['_interestingSignatures']) {
|
||||||
|
tx['_interestingSignatures'] = tx['_sigs'].some(sigs => sigs.some(sig => this.sigIsInteresting(sig)));
|
||||||
|
}
|
||||||
|
tx['_showSignatures'] = this.shouldShowSignatures(tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.largeInput = tx.largeInput || tx.vin.some(vin => (vin?.prevout?.value > 1000000000));
|
tx.largeInput = tx.largeInput || tx.vin.some(vin => (vin?.prevout?.value > 1000000000));
|
||||||
@@ -541,8 +573,36 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
this.ref.markForCheck();
|
this.ref.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSignaturesMode(): void {
|
||||||
|
this.signaturesMode = this.signaturesOverride || this.forceSignaturesMode || this.signaturesPreference || 'interesting';
|
||||||
|
for (const tx of this.transactions) {
|
||||||
|
tx['_showSignatures'] = this.shouldShowSignatures(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showSig(sigs: SigInfo[]): boolean {
|
||||||
|
return this.signaturesMode === 'all' || (this.signaturesMode === 'interesting' && sigs.some(sig => this.sigIsInteresting(sig)));
|
||||||
|
}
|
||||||
|
|
||||||
|
sigIsInteresting(sig: SigInfo): boolean {
|
||||||
|
return sig.sighash !== SighashFlag.DEFAULT && sig.sighash !== SighashFlag.ALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldShowSignatures(tx): boolean {
|
||||||
|
switch (this.signaturesMode) {
|
||||||
|
case 'all':
|
||||||
|
return true;
|
||||||
|
case 'interesting':
|
||||||
|
return tx['_interestingSignatures'];
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.outspendsSubscription.unsubscribe();
|
this.outspendsSubscription.unsubscribe();
|
||||||
this.currencyChangeSubscription?.unsubscribe();
|
this.currencyChangeSubscription?.unsubscribe();
|
||||||
|
this.networkSubscription.unsubscribe();
|
||||||
|
this.signaturesSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -42,6 +42,8 @@ export interface Customization {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SignaturesMode = 'all' | 'interesting' | 'none' | null;
|
||||||
|
|
||||||
export interface Env {
|
export interface Env {
|
||||||
MAINNET_ENABLED: boolean;
|
MAINNET_ENABLED: boolean;
|
||||||
TESTNET_ENABLED: boolean;
|
TESTNET_ENABLED: boolean;
|
||||||
@@ -150,6 +152,7 @@ export class StateService {
|
|||||||
backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora');
|
backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora');
|
||||||
networkChanged$ = new ReplaySubject<string>(1);
|
networkChanged$ = new ReplaySubject<string>(1);
|
||||||
lightningChanged$ = new ReplaySubject<boolean>(1);
|
lightningChanged$ = new ReplaySubject<boolean>(1);
|
||||||
|
signaturesMode$: BehaviorSubject<SignaturesMode>;
|
||||||
blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]);
|
blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]);
|
||||||
blocks$: Observable<BlockExtended[]>;
|
blocks$: Observable<BlockExtended[]>;
|
||||||
transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
|
transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
|
||||||
@@ -332,6 +335,8 @@ export class StateService {
|
|||||||
this.blocksSubject$.next([]);
|
this.blocksSubject$.next([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.signaturesMode$ = new BehaviorSubject<SignaturesMode>(this.storageService.getValue('signatures-enabled') as SignaturesMode || null);
|
||||||
|
|
||||||
this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
|
this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
|
||||||
|
|
||||||
this.blocks$ = this.blocksSubject$.pipe(filter(blocks => blocks != null && blocks.length > 0));
|
this.blocks$ = this.blocksSubject$.pipe(filter(blocks => blocks != null && blocks.length > 0));
|
||||||
|
Reference in New Issue
Block a user