Merge pull request #5004 from mempool/natsoni/toggle-block-fees
Add block display mode toggle
This commit is contained in:
commit
a66920c51e
@ -12,6 +12,7 @@
|
|||||||
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
|
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
|
||||||
[class.offscreen]="!static && count && i >= count"
|
[class.offscreen]="!static && count && i >= count"
|
||||||
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
|
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
|
||||||
|
[style]="blockTransformation"
|
||||||
[class.blink-bg]="isSpecial(block.height)">
|
[class.blink-bg]="isSpecial(block.height)">
|
||||||
<a draggable="false" [routerLink]="[getHref(i, block) | relativeUrl]" [state]="{ data: { block: block } }"
|
<a draggable="false" [routerLink]="[getHref(i, block) | relativeUrl]" [state]="{ data: { block: block } }"
|
||||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||||
@ -40,7 +41,7 @@
|
|||||||
<app-fee-rate unitClass=""></app-fee-rate>
|
<app-fee-rate unitClass=""></app-fee-rate>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo$ | async; else noMiningInfo"
|
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="blockDisplayMode === 'fees'; else noMiningInfo"
|
||||||
class="block-size">
|
class="block-size">
|
||||||
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription, delay, filter, tap } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { specialBlocks } from '../../app.constants';
|
import { specialBlocks } from '../../app.constants';
|
||||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||||
@ -45,7 +45,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
markBlockSubscription: Subscription;
|
markBlockSubscription: Subscription;
|
||||||
txConfirmedSubscription: Subscription;
|
txConfirmedSubscription: Subscription;
|
||||||
loadingBlocks$: Observable<boolean>;
|
loadingBlocks$: Observable<boolean>;
|
||||||
showMiningInfo$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
showMiningInfoSubscription: Subscription;
|
||||||
|
blockDisplayModeSubscription: Subscription;
|
||||||
|
blockDisplayMode: 'size' | 'fees';
|
||||||
|
blockTransformation = {};
|
||||||
blockStyles = [];
|
blockStyles = [];
|
||||||
emptyBlockStyles = [];
|
emptyBlockStyles = [];
|
||||||
interval: any;
|
interval: any;
|
||||||
@ -78,22 +81,38 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
enabledMiningInfoIfNeeded(url) {
|
|
||||||
const urlParts = url.split('/');
|
|
||||||
const onDashboard = ['','testnet','signet','mining','acceleration'].includes(urlParts[urlParts.length - 1]);
|
|
||||||
if (onDashboard) { // Only update showMiningInfo if we are on the main, mining or acceleration dashboards
|
|
||||||
this.stateService.showMiningInfo$.next(url.includes('/mining') || url.includes('/acceleration'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
||||||
|
|
||||||
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
this.blockDisplayMode = this.stateService.blockDisplayMode$.value as 'size' | 'fees';
|
||||||
this.enabledMiningInfoIfNeeded(this.location.path());
|
this.blockDisplayModeSubscription = this.stateService.blockDisplayMode$
|
||||||
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
.pipe(
|
||||||
this.showMiningInfo$ = this.stateService.showMiningInfo$;
|
filter((mode: 'size' | 'fees') => mode !== this.blockDisplayMode),
|
||||||
}
|
tap(() => {
|
||||||
|
this.blockTransformation = this.timeLtr ? {
|
||||||
|
transform: 'scaleX(-1) rotateX(90deg)',
|
||||||
|
transition: 'transform 0.375s'
|
||||||
|
} : {
|
||||||
|
transform: 'rotateX(90deg)',
|
||||||
|
transition: 'transform 0.375s'
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
delay(375),
|
||||||
|
tap((mode) => {
|
||||||
|
this.blockDisplayMode = mode;
|
||||||
|
this.blockTransformation = this.timeLtr ? {
|
||||||
|
transform: 'scaleX(-1)',
|
||||||
|
transition: 'transform 0.375s'
|
||||||
|
} : {
|
||||||
|
transition: 'transform 0.375s'
|
||||||
|
};
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
|
delay(375),
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.blockTransformation = {};
|
||||||
|
});
|
||||||
|
|
||||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||||
this.timeLtr = !!ltr;
|
this.timeLtr = !!ltr;
|
||||||
@ -204,6 +223,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.networkSubscription.unsubscribe();
|
this.networkSubscription.unsubscribe();
|
||||||
this.tabHiddenSubscription.unsubscribe();
|
this.tabHiddenSubscription.unsubscribe();
|
||||||
this.markBlockSubscription.unsubscribe();
|
this.markBlockSubscription.unsubscribe();
|
||||||
|
this.blockDisplayModeSubscription.unsubscribe();
|
||||||
this.timeLtrSubscription.unsubscribe();
|
this.timeLtrSubscription.unsubscribe();
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div id="divider" [hidden]="pageIndex > 0">
|
<div id="divider" [hidden]="pageIndex > 0">
|
||||||
|
<button class="block-display-toggle" (click)="toggleBlockDisplayMode()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button>
|
||||||
<button class="time-toggle" (click)="toggleTimeDirection()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button>
|
<button class="time-toggle" (click)="toggleTimeDirection()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@ -67,9 +67,24 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-display-toggle {
|
||||||
|
color: white;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 15.8em;
|
||||||
|
left: 1px;
|
||||||
|
transform: translateX(-50%) rotate(90deg);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.blockchain-wrapper.ltr-transition .blocks-wrapper,
|
.blockchain-wrapper.ltr-transition .blocks-wrapper,
|
||||||
.blockchain-wrapper.ltr-transition .position-container,
|
.blockchain-wrapper.ltr-transition .position-container,
|
||||||
.blockchain-wrapper.ltr-transition .time-toggle {
|
.blockchain-wrapper.ltr-transition .time-toggle,
|
||||||
|
.blockchain-wrapper.ltr-transition .block-display-toggle {
|
||||||
transition: transform 1s;
|
transition: transform 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +96,10 @@
|
|||||||
.time-toggle {
|
.time-toggle {
|
||||||
transform: translateX(-50%) scaleX(-1);
|
transform: translateX(-50%) scaleX(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-display-toggle {
|
||||||
|
transform: translateX(-50%) scaleX(-1) rotate(90deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(.ltr-layout) {
|
:host-context(.ltr-layout) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { firstValueFrom, Subscription } from 'rxjs';
|
import { firstValueFrom, Subscription } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { StorageService } from '../../services/storage.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-blockchain',
|
selector: 'app-blockchain',
|
||||||
@ -26,15 +27,18 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
connectionStateSubscription: Subscription;
|
connectionStateSubscription: Subscription;
|
||||||
loadingTip: boolean = true;
|
loadingTip: boolean = true;
|
||||||
connected: boolean = true;
|
connected: boolean = true;
|
||||||
|
blockDisplayMode: 'size' | 'fees';
|
||||||
|
|
||||||
dividerOffset: number | null = null;
|
dividerOffset: number | null = null;
|
||||||
mempoolOffset: number | null = null;
|
mempoolOffset: number | null = null;
|
||||||
positionStyle = {
|
positionStyle = {
|
||||||
transform: "translateX(1280px)",
|
transform: "translateX(1280px)",
|
||||||
};
|
};
|
||||||
|
blockDisplayToggleStyle = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
|
public StorageService: StorageService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -51,6 +55,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
firstValueFrom(this.stateService.chainTip$).then(() => {
|
firstValueFrom(this.stateService.chainTip$).then(() => {
|
||||||
this.loadingTip = false;
|
this.loadingTip = false;
|
||||||
});
|
});
|
||||||
|
this.blockDisplayMode = this.StorageService.getValue('block-display-mode-preference') as 'size' | 'fees' || 'size';
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -84,6 +89,13 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleBlockDisplayMode(): void {
|
||||||
|
if (this.blockDisplayMode === 'size') this.blockDisplayMode = 'fees';
|
||||||
|
else this.blockDisplayMode = 'size';
|
||||||
|
this.StorageService.setValue('block-display-mode-preference', this.blockDisplayMode);
|
||||||
|
this.stateService.blockDisplayMode$.next(this.blockDisplayMode);
|
||||||
|
}
|
||||||
|
|
||||||
onMempoolWidthChange(width): void {
|
onMempoolWidthChange(width): void {
|
||||||
if (this.flipping) {
|
if (this.flipping) {
|
||||||
return;
|
return;
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
class="spotlight-bottom"
|
class="spotlight-bottom"
|
||||||
[style.right]="mempoolBlockStyles[i].right"
|
[style.right]="mempoolBlockStyles[i].right"
|
||||||
></div>
|
></div>
|
||||||
<div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" [class.hide-block]="count && i >= count" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
|
<div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" [class.hide-block]="count && i >= count" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink" [style]="blockTransformation">
|
||||||
<a draggable="false" [routerLink]="[getHref(i) | relativeUrl]"
|
<a draggable="false" [routerLink]="[getHref(i) | relativeUrl]"
|
||||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||||
<div class="block-body">
|
<div class="block-body">
|
||||||
@ -20,7 +20,7 @@
|
|||||||
-
|
-
|
||||||
<app-fee-rate [fee]="projectedBlock.feeRange[projectedBlock.feeRange.length - 1]" rounding="1.0-0" unitClass=""></app-fee-rate>
|
<app-fee-rate [fee]="projectedBlock.feeRange[projectedBlock.feeRange.length - 1]" rounding="1.0-0" unitClass=""></app-fee-rate>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="showMiningInfo$ | async; else noMiningInfo" class="block-size">
|
<div *ngIf="blockDisplayMode === 'fees'; else noMiningInfo" class="block-size">
|
||||||
<app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
<app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #noMiningInfo>
|
<ng-template #noMiningInfo>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
|
||||||
import { Subscription, Observable, of, combineLatest, BehaviorSubject } from 'rxjs';
|
import { Subscription, Observable, of, combineLatest } from 'rxjs';
|
||||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { map, switchMap, tap } from 'rxjs/operators';
|
import { delay, filter, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { feeLevels } from '../../app.constants';
|
import { feeLevels } from '../../app.constants';
|
||||||
import { specialBlocks } from '../../app.constants';
|
import { specialBlocks } from '../../app.constants';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
@ -43,7 +43,10 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
mempoolBlocks$: Observable<MempoolBlock[]>;
|
mempoolBlocks$: Observable<MempoolBlock[]>;
|
||||||
difficultyAdjustments$: Observable<DifficultyAdjustment>;
|
difficultyAdjustments$: Observable<DifficultyAdjustment>;
|
||||||
loadingBlocks$: Observable<boolean>;
|
loadingBlocks$: Observable<boolean>;
|
||||||
showMiningInfo$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
showMiningInfoSubscription: Subscription;
|
||||||
|
blockDisplayModeSubscription: Subscription;
|
||||||
|
blockDisplayMode: 'size' | 'fees';
|
||||||
|
blockTransformation = {};
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
|
|
||||||
mempoolBlocksFull: MempoolBlock[] = [];
|
mempoolBlocksFull: MempoolBlock[] = [];
|
||||||
@ -99,9 +102,29 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.mempoolWidth = width;
|
this.mempoolWidth = width;
|
||||||
this.widthChange.emit(this.mempoolWidth);
|
this.widthChange.emit(this.mempoolWidth);
|
||||||
|
|
||||||
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
this.blockDisplayMode = this.stateService.blockDisplayMode$.value as 'size' | 'fees';
|
||||||
this.showMiningInfo$ = this.stateService.showMiningInfo$;
|
this.blockDisplayModeSubscription = this.stateService.blockDisplayMode$
|
||||||
}
|
.pipe(
|
||||||
|
filter((mode: 'size' | 'fees') => mode !== this.blockDisplayMode),
|
||||||
|
tap(() => {
|
||||||
|
this.blockTransformation = {
|
||||||
|
transform: 'rotateX(90deg)',
|
||||||
|
transition: 'transform 0.375s'
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
delay(375),
|
||||||
|
tap((mode) => {
|
||||||
|
this.blockDisplayMode = mode;
|
||||||
|
this.blockTransformation = {
|
||||||
|
transition: 'transform 0.375s'
|
||||||
|
};
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
|
delay(375),
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.blockTransformation = {};
|
||||||
|
});
|
||||||
|
|
||||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||||
this.timeLtr = !this.forceRtl && !!ltr;
|
this.timeLtr = !this.forceRtl && !!ltr;
|
||||||
@ -262,6 +285,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.markBlocksSubscription.unsubscribe();
|
this.markBlocksSubscription.unsubscribe();
|
||||||
this.blockSubscription.unsubscribe();
|
this.blockSubscription.unsubscribe();
|
||||||
this.networkSubscription.unsubscribe();
|
this.networkSubscription.unsubscribe();
|
||||||
|
this.blockDisplayModeSubscription.unsubscribe();
|
||||||
this.timeLtrSubscription.unsubscribe();
|
this.timeLtrSubscription.unsubscribe();
|
||||||
this.chainTipSubscription.unsubscribe();
|
this.chainTipSubscription.unsubscribe();
|
||||||
this.keySubscription.unsubscribe();
|
this.keySubscription.unsubscribe();
|
||||||
|
@ -151,7 +151,7 @@ export class StateService {
|
|||||||
hideAudit: BehaviorSubject<boolean>;
|
hideAudit: BehaviorSubject<boolean>;
|
||||||
fiatCurrency$: BehaviorSubject<string>;
|
fiatCurrency$: BehaviorSubject<string>;
|
||||||
rateUnits$: BehaviorSubject<string>;
|
rateUnits$: BehaviorSubject<string>;
|
||||||
showMiningInfo$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
blockDisplayMode$: BehaviorSubject<string> = new BehaviorSubject<string>('size');
|
||||||
|
|
||||||
searchFocus$: Subject<boolean> = new Subject<boolean>();
|
searchFocus$: Subject<boolean> = new Subject<boolean>();
|
||||||
menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
@ -259,6 +259,9 @@ export class StateService {
|
|||||||
const rateUnitPreference = this.storageService.getValue('rate-unit-preference');
|
const rateUnitPreference = this.storageService.getValue('rate-unit-preference');
|
||||||
this.rateUnits$ = new BehaviorSubject<string>(rateUnitPreference || 'vb');
|
this.rateUnits$ = new BehaviorSubject<string>(rateUnitPreference || 'vb');
|
||||||
|
|
||||||
|
const blockDisplayModePreference = this.storageService.getValue('block-display-mode-preference');
|
||||||
|
this.blockDisplayMode$ = new BehaviorSubject<string>(blockDisplayModePreference || 'size');
|
||||||
|
|
||||||
this.backend$.subscribe(backend => {
|
this.backend$.subscribe(backend => {
|
||||||
this.backend = backend;
|
this.backend = backend;
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user