Merge pull request #3258 from mempool/mononaut/difficulty-widget-redesign

Redesign difficulty adjustment dashboard widget
This commit is contained in:
softsimon 2023-03-11 18:11:34 +09:00 committed by GitHub
commit 368f858ff2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 956 additions and 185 deletions

View File

@ -23,9 +23,11 @@ describe('Mempool Difficulty Adjustment', () => {
remainingBlocks: 1834,
remainingTime: 977591692,
previousRetarget: 0.6280047707459726,
previousTime: 1660820820,
nextRetargetHeight: 751968,
timeAvg: 533038,
timeOffset: 0,
expectedBlocks: 161.68833333333333,
},
],
[ // Vector 2 (testnet)
@ -43,11 +45,13 @@ describe('Mempool Difficulty Adjustment', () => {
estimatedRetargetDate: 1661895424692,
remainingBlocks: 1834,
remainingTime: 977591692,
previousTime: 1660820820,
previousRetarget: 0.6280047707459726,
nextRetargetHeight: 751968,
timeAvg: 533038,
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
expectedBlocks: 161.68833333333333,
},
],
] as [[number, number, number, number, string, number], DifficultyAdjustment][];

View File

@ -9,9 +9,11 @@ export interface DifficultyAdjustment {
remainingBlocks: number; // Block count
remainingTime: number; // Duration of time in ms
previousRetarget: number; // Percent: -75 to 300
previousTime: number; // Unix time in ms
nextRetargetHeight: number; // Block Height
timeAvg: number; // Duration of time in ms
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
expectedBlocks: number; // Block count
}
export function calcDifficultyAdjustment(
@ -32,12 +34,12 @@ export function calcDifficultyAdjustment(
const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100;
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET;
let difficultyChange = 0;
let timeAvgSecs = BLOCK_SECONDS_TARGET;
let timeAvgSecs = diffSeconds / blocksInEpoch;
// Only calculate the estimate once we have 7.2% of blocks in current epoch
if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
timeAvgSecs = diffSeconds / blocksInEpoch;
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
// Max increase is x4 (+300%)
if (difficultyChange > 300) {
@ -74,9 +76,11 @@ export function calcDifficultyAdjustment(
remainingBlocks,
remainingTime,
previousRetarget,
previousTime: DATime,
nextRetargetHeight,
timeAvg,
timeOffset,
expectedBlocks,
};
}

View File

@ -0,0 +1,87 @@
<div *ngIf="showTitle" class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<div class="card-wrapper">
<div class="card">
<div class="card-body more-padding">
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
<div class="item">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.remainingBlocks === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.remainingBlocks }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div>
<div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownDifficulty >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownPreviousDifficulty >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
</div>
</div>
<div class="item" *ngIf="showProgress">
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
<div class="card-text">{{ epochData.progress | number: '1.2-2' }} <span class="symbol">%</span></div>
<div class="progress small-bar">
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}">&nbsp;</div>
</div>
</div>
<div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div>
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time></div>
</div>
</div>
</div>
</div>
</div>
<ng-template #loadingDifficulty>
<div class="difficulty-skeleton loading-container">
<div class="item">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>

View File

@ -0,0 +1,154 @@
.difficulty-adjustment-container {
display: flex;
flex-direction: row;
justify-content: space-around;
height: 76px;
.shared-block {
color: #ffffff66;
font-size: 12px;
}
.item {
padding: 0 5px;
width: 100%;
&:nth-child(1) {
display: none;
@media (min-width: 485px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: table-cell;
}
}
}
.card-text {
font-size: 22px;
margin-top: -9px;
position: relative;
}
}
.difficulty-skeleton {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
&:first-child{
display: none;
@media (min-width: 485px) {
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
&:last-child {
margin-bottom: 0;
}
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
margin: 14px auto 0;
max-width: 80px;
}
&:last-child {
margin: 10px auto 0;
max-width: 120px;
}
}
}
}
.card {
background-color: #1d1f31;
height: 100%;
}
.card-title {
color: #4a68b9;
font-size: 1rem;
}
.progress {
display: inline-flex;
width: 100%;
background-color: #2d3348;
height: 1.1rem;
max-width: 180px;
}
.skeleton-loader {
max-width: 100%;
}
.more-padding {
padding: 18px;
}
.small-bar {
height: 8px;
top: -4px;
max-width: 120px;
}
.loading-container {
min-height: 76px;
}
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.card-wrapper {
.card {
height: auto !important;
}
.card-body {
display: flex;
flex: inherit;
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 24px 20px;
}
}
.retarget-sign {
margin-right: -3px;
font-size: 14px;
top: -2px;
position: relative;
}
.previous-retarget-sign {
margin-right: -2px;
font-size: 10px;
}
.symbol {
font-size: 13px;
}

View File

@ -0,0 +1,86 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { combineLatest, Observable, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { StateService } from '../../services/state.service';
interface EpochProgress {
base: string;
change: number;
progress: number;
remainingBlocks: number;
newDifficultyHeight: number;
colorAdjustments: string;
colorPreviousAdjustments: string;
estimatedRetargetDate: number;
previousRetarget: number;
blocksUntilHalving: number;
timeUntilHalving: number;
}
@Component({
selector: 'app-difficulty-mining',
templateUrl: './difficulty-mining.component.html',
styleUrls: ['./difficulty-mining.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DifficultyMiningComponent implements OnInit {
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
@Input() showProgress = true;
@Input() showHalving = false;
@Input() showTitle = true;
constructor(
public stateService: StateService,
) { }
ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.difficultyEpoch$ = combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)),
this.stateService.difficultyAdjustment$,
])
.pipe(
map(([block, da]) => {
let colorAdjustments = '#ffffff66';
if (da.difficultyChange > 0) {
colorAdjustments = '#3bcc49';
}
if (da.difficultyChange < 0) {
colorAdjustments = '#dc3545';
}
let colorPreviousAdjustments = '#dc3545';
if (da.previousRetarget) {
if (da.previousRetarget >= 0) {
colorPreviousAdjustments = '#3bcc49';
}
if (da.previousRetarget === 0) {
colorPreviousAdjustments = '#ffffff66';
}
} else {
colorPreviousAdjustments = '#ffffff66';
}
const blocksUntilHalving = 210000 - (block.height % 210000);
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
const data = {
base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange,
progress: da.progressPercent,
remainingBlocks: da.remainingBlocks - 1,
colorAdjustments,
colorPreviousAdjustments,
newDifficultyHeight: da.nextRetargetHeight,
estimatedRetargetDate: da.estimatedRetargetDate,
previousRetarget: da.previousRetarget,
blocksUntilHalving,
timeUntilHalving,
};
return data;
})
);
}
}

View File

@ -0,0 +1,41 @@
<div
#tooltip
*ngIf="status"
class="difficulty-tooltip"
[style.visibility]="status ? 'visible' : 'hidden'"
[style.left]="tooltipPosition.x + 'px'"
[style.top]="tooltipPosition.y + 'px'"
>
<ng-container [ngSwitch]="status">
<ng-container *ngSwitchCase="'mined'">
<ng-container *ngIf="isAhead">
<ng-container *ngTemplateOutlet="expected === 1 ? blocksSingular : blocksPlural; context: {$implicit: expected }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.expected-blocks">{{ i }} blocks expected</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.expected-block">{{ i }} block expected</ng-template>
</ng-container>
<ng-container *ngIf="!isAhead">
<ng-container *ngTemplateOutlet="mined === 1 ? blocksSingular : blocksPlural; context: {$implicit: mined }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.mined-blocks">{{ i }} blocks mined</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.mined-block">{{ i }} block mined</ng-template>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'remaining'">
<ng-container *ngTemplateOutlet="remaining === 1 ? blocksSingular : blocksPlural; context: {$implicit: remaining }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.remaining-blocks">{{ i }} blocks remaining</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.remaining-block">{{ i }} block remaining</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'ahead'">
<ng-container *ngTemplateOutlet="ahead === 1 ? blocksSingular : blocksPlural; context: {$implicit: ahead }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-ahead">{{ i }} blocks ahead</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.block-ahead">{{ i }} block ahead</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'behind'">
<ng-container *ngTemplateOutlet="behind === 1 ? blocksSingular : blocksPlural; context: {$implicit: behind }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-behind">{{ i }} blocks behind</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.block-behind">{{ i }} block behind</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'next'">
<span i18n="@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c">next block</span>
</ng-container>
</ng-container>
</div>

View File

@ -0,0 +1,18 @@
.difficulty-tooltip {
position: fixed;
background: rgba(#11131f, 0.95);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: #b1b1b1;
padding: 10px 15px;
text-align: left;
pointer-events: none;
max-width: 300px;
min-width: 200px;
text-align: center;
p {
margin: 0;
white-space: nowrap;
}
}

View File

@ -0,0 +1,66 @@
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
interface EpochProgress {
base: string;
change: number;
progress: number;
minedBlocks: number;
remainingBlocks: number;
expectedBlocks: number;
newDifficultyHeight: number;
colorAdjustments: string;
colorPreviousAdjustments: string;
estimatedRetargetDate: number;
previousRetarget: number;
blocksUntilHalving: number;
timeUntilHalving: number;
}
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
@Component({
selector: 'app-difficulty-tooltip',
templateUrl: './difficulty-tooltip.component.html',
styleUrls: ['./difficulty-tooltip.component.scss'],
})
export class DifficultyTooltipComponent implements OnChanges {
@Input() status: string | void;
@Input() progress: EpochProgress | void = null;
@Input() cursorPosition: { x: number, y: number };
mined: number;
ahead: number;
behind: number;
expected: number;
remaining: number;
isAhead: boolean;
isBehind: boolean;
tooltipPosition = { x: 0, y: 0 };
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
constructor() {}
ngOnChanges(changes): void {
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
let x = changes.cursorPosition.currentValue.x;
let y = changes.cursorPosition.currentValue.y - 50;
if (this.tooltipElement) {
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
x -= elementBounds.width / 2;
x = Math.min(Math.max(x, 20), (window.innerWidth - 20 - elementBounds.width));
}
this.tooltipPosition = { x, y };
}
if ((changes.progress || changes.status) && this.progress && this.status) {
this.remaining = this.progress.remainingBlocks;
this.expected = this.progress.expectedBlocks;
this.mined = this.progress.minedBlocks;
this.ahead = Math.max(0, this.mined - this.expected);
this.behind = Math.max(0, this.expected - this.mined);
this.isAhead = this.ahead > 0;
this.isBehind = this.behind > 0;
}
}
}

View File

@ -3,81 +3,100 @@
<div class="card">
<div class="card-body more-padding">
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
<div class="item">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.remainingBlocks === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.remainingBlocks }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div>
<div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
<div class="epoch-progress">
<svg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 234 9" shape-rendering="crispEdges" preserveAspectRatio="none">
<defs>
<linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#105fb0" />
<stop offset="100%" stop-color="#9339f4" />
</linearGradient>
<linearGradient id="diff-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#2486eb" />
<stop offset="100%" stop-color="#ae6af7" />
</linearGradient>
</defs>
<rect
*ngFor="let rect of shapes"
[attr.x]="rect.x" [attr.y]="rect.y"
[attr.width]="rect.w" [attr.height]="rect.h"
class="rect {{rect.status}}"
[class.hover]="hoverSection && rect.status === hoverSection.status"
(pointerover)="onHover($event, rect);"
(pointerout)="onBlur($event);"
>
<animate
*ngIf="rect.status === 'next'"
attributeType="XML"
attributeName="fill"
[attr.values]="'#fff;' + (rect.expected ? '#D81B60' : '#2d3348') + ';#fff'"
dur="2s"
repeatCount="indefinite"/>
</rect>
</svg>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownDifficulty >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
<div class="difficulty-stats">
<div class="item">
<div class="card-text">
~<app-time [time]="epochData.timeAvg / 1000" [forceFloorOnTimeIntervals]="['minute']" [fractionDigits]="1"></app-time>
</div>
<div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div>
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
<div class="item">
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownPreviousDifficulty >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
<ng-template #arrowDownDifficulty >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<ng-template #recentlyAdjusted>
<div class="card-text">&#8212;</div>
</ng-template>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #arrowDownPreviousDifficulty >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
</div>
</div>
</div>
<div class="item" *ngIf="showProgress">
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
<div class="card-text">{{ epochData.progress | number: '1.2-2' }} <span class="symbol">%</span></div>
<div class="progress small-bar">
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}">&nbsp;</div>
<div class="item">
<div class="card-text"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
<div class="symbol">
{{ epochData.retargetDateString }}
</div>
</div>
</div>
<div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
<div class="card-text">
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div>
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time></div>
</div>
</div>
</div>
</div>
</div>
<ng-template #loadingDifficulty>
<div class="epoch-progress">
<div class="skeleton-loader"></div>
</div>
<div class="difficulty-skeleton loading-container">
<div class="item">
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
@ -85,3 +104,10 @@
</div>
</div>
</ng-template>
<app-difficulty-tooltip
*ngIf="hoverSection && (isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData"
[cursorPosition]="tooltipPosition"
[status]="hoverSection.status"
[progress]="epochData"
></app-difficulty-tooltip>

View File

@ -1,8 +1,14 @@
.difficulty-adjustment-container {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.difficulty-stats {
display: flex;
flex-direction: row;
justify-content: space-around;
height: 76px;
height: 50.5px;
.shared-block {
color: #ffffff66;
font-size: 12px;
@ -24,8 +30,8 @@
}
}
.card-text {
font-size: 22px;
margin-top: -9px;
font-size: 20px;
margin: auto;
position: relative;
}
}
@ -33,7 +39,9 @@
.difficulty-skeleton {
display: flex;
justify-content: space-between;
flex-direction: row;
justify-content: space-around;
height: 50.5px;
@media (min-width: 376px) {
flex-direction: row;
}
@ -65,7 +73,7 @@
width: 100%;
display: block;
&:first-child {
margin: 14px auto 0;
margin: 10px auto 4px;
max-width: 80px;
}
&:last-child {
@ -109,7 +117,7 @@
}
.loading-container {
min-height: 76px;
min-height: 50.5px;
}
.main-title {
@ -133,7 +141,7 @@
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 24px 20px;
padding: 20px;
}
}
@ -151,4 +159,50 @@
.symbol {
font-size: 13px;
}
.epoch-progress {
width: 100%;
height: 22px;
margin-bottom: 12px;
}
.epoch-blocks {
display: block;
width: 100%;
background: #2d3348;
.rect {
fill: #2d3348;
&.behind {
fill: #D81B60;
}
&.mined {
fill: url(#diff-gradient);
}
&.ahead {
fill: #1a9436;
}
&.hover {
fill: #535e84;
&.behind {
fill: #e94d86;
}
&.mined {
fill: url(#diff-hover-gradient);
}
&.ahead {
fill: #29d951;
}
}
}
}
.blocks-ahead {
color: #3bcc49;
}
.blocks-behind {
color: #D81B60;
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, Observable, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { StateService } from '../..//services/state.service';
@ -7,16 +7,33 @@ interface EpochProgress {
base: string;
change: number;
progress: number;
minedBlocks: number;
remainingBlocks: number;
expectedBlocks: number;
newDifficultyHeight: number;
colorAdjustments: string;
colorPreviousAdjustments: string;
estimatedRetargetDate: number;
retargetDateString: string;
previousRetarget: number;
blocksUntilHalving: number;
timeUntilHalving: number;
timeAvg: number;
}
type BlockStatus = 'mined' | 'behind' | 'ahead' | 'next' | 'remaining';
interface DiffShape {
x: number;
y: number;
w: number;
h: number;
status: BlockStatus;
expected: boolean;
}
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
@Component({
selector: 'app-difficulty',
templateUrl: './difficulty.component.html',
@ -24,15 +41,27 @@ interface EpochProgress {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DifficultyComponent implements OnInit {
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
@Input() showProgress = true;
@Input() showHalving = false;
@Input() showTitle = true;
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
epochStart: number;
currentHeight: number;
currentIndex: number;
expectedHeight: number;
expectedIndex: number;
difference: number;
shapes: DiffShape[];
tooltipPosition = { x: 0, y: 0 };
hoverSection: DiffShape | void;
constructor(
public stateService: StateService,
@Inject(LOCALE_ID) private locale: string,
) { }
ngOnInit(): void {
@ -65,22 +94,110 @@ export class DifficultyComponent implements OnInit {
const blocksUntilHalving = 210000 - (block.height % 210000);
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH;
const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks);
if (newEpochStart !== this.epochStart || newExpectedHeight !== this.expectedHeight || this.currentHeight !== this.stateService.latestBlockHeight) {
this.epochStart = newEpochStart;
this.expectedHeight = newExpectedHeight;
this.currentHeight = this.stateService.latestBlockHeight;
this.currentIndex = this.currentHeight - this.epochStart;
this.expectedIndex = Math.min(this.expectedHeight - this.epochStart, 2106) - 1;
this.difference = this.currentIndex - this.expectedIndex;
this.shapes = [];
this.shapes = this.shapes.concat(this.blocksToShapes(
0, Math.min(this.currentIndex, this.expectedIndex), 'mined', true
));
this.shapes = this.shapes.concat(this.blocksToShapes(
this.currentIndex + 1, this.expectedIndex, 'behind', true
));
this.shapes = this.shapes.concat(this.blocksToShapes(
this.expectedIndex + 1, this.currentIndex, 'ahead', false
));
if (this.currentIndex < 2105) {
this.shapes = this.shapes.concat(this.blocksToShapes(
this.currentIndex + 1, this.currentIndex + 1, 'next', (this.expectedIndex > this.currentIndex)
));
}
this.shapes = this.shapes.concat(this.blocksToShapes(
Math.max(this.currentIndex + 2, this.expectedIndex + 1), 2105, 'remaining', false
));
}
let retargetDateString;
if (da.remainingBlocks > 1870) {
retargetDateString = (new Date(da.estimatedRetargetDate)).toLocaleDateString(this.locale, { month: 'long', day: 'numeric' });
} else {
retargetDateString = (new Date(da.estimatedRetargetDate)).toLocaleTimeString(this.locale, { month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
}
const data = {
base: `${da.progressPercent.toFixed(2)}%`,
change: da.difficultyChange,
progress: da.progressPercent,
remainingBlocks: da.remainingBlocks,
minedBlocks: this.currentIndex + 1,
remainingBlocks: da.remainingBlocks - 1,
expectedBlocks: Math.floor(da.expectedBlocks),
colorAdjustments,
colorPreviousAdjustments,
newDifficultyHeight: da.nextRetargetHeight,
estimatedRetargetDate: da.estimatedRetargetDate,
retargetDateString,
previousRetarget: da.previousRetarget,
blocksUntilHalving,
timeUntilHalving,
timeAvg: da.timeAvg,
};
return data;
})
);
}
blocksToShapes(start: number, end: number, status: BlockStatus, expected: boolean = false): DiffShape[] {
const startY = start % 9;
const startX = Math.floor(start / 9);
const endY = (end % 9);
const endX = Math.floor(end / 9);
if (startX > endX) {
return [];
}
if (startX === endX) {
return [{
x: startX, y: startY, w: 1, h: 1 + endY - startY, status, expected
}];
}
const shapes = [];
shapes.push({
x: startX, y: startY, w: 1, h: 9 - startY, status, expected
});
shapes.push({
x: endX, y: 0, w: 1, h: endY + 1, status, expected
});
if (startX < endX - 1) {
shapes.push({
x: startX + 1, y: 0, w: endX - startX - 1, h: 9, status, expected
});
}
return shapes;
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
}
onHover(event, rect): void {
this.hoverSection = rect;
}
onBlur(event): void {
this.hoverSection = null;
}
}

View File

@ -22,7 +22,7 @@
<!-- difficulty adjustment -->
<div class="col">
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<app-difficulty [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
<app-difficulty-mining [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty-mining>
</div>
<!-- pool distribution -->

View File

@ -19,6 +19,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
@Input() fixedRender = false;
@Input() relative = false;
@Input() forceFloorOnTimeIntervals: string[];
@Input() fractionDigits: number = 0;
constructor(
private ref: ChangeDetectorRef,
@ -88,7 +89,12 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
} else {
counter = Math.round(seconds / this.intervals[i]);
}
const dateStrings = dates(counter);
let rounded = counter;
if (this.fractionDigits) {
const roundFactor = Math.pow(10,this.fractionDigits);
rounded = Math.round((seconds / this.intervals[i]) * roundFactor) / roundFactor;
}
const dateStrings = dates(rounded);
if (counter > 0) {
switch (this.kind) {
case 'since':

View File

@ -33,9 +33,11 @@ export interface DifficultyAdjustment {
remainingBlocks: number;
remainingTime: number;
previousRetarget: number;
previousTime: number;
nextRetargetHeight: number;
timeAvg: number;
timeOffset: number;
expectedBlocks: number;
}
export interface AddressInformation {

View File

@ -58,6 +58,8 @@ import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.c
import { StatusViewComponent } from '../components/status-view/status-view.component';
import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
import { DifficultyMiningComponent } from '../components/difficulty-mining/difficulty-mining.component';
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component';
import { TxBowtieGraphTooltipComponent } from '../components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component';
@ -133,6 +135,8 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
StatusViewComponent,
FeesBoxComponent,
DifficultyComponent,
DifficultyMiningComponent,
DifficultyTooltipComponent,
TxBowtieGraphComponent,
TxBowtieGraphTooltipComponent,
TermsOfServiceComponent,
@ -234,6 +238,8 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
StatusViewComponent,
FeesBoxComponent,
DifficultyComponent,
DifficultyMiningComponent,
DifficultyTooltipComponent,
TxBowtieGraphComponent,
TxBowtieGraphTooltipComponent,
TermsOfServiceComponent,

View File

@ -2452,6 +2452,10 @@
<context context-type="sourcefile">src/app/components/block/block.component.html</context>
<context context-type="linenumber">8,9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty-tooltip.component.html</context>
<context context-type="linenumber">38</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.ts</context>
<context context-type="linenumber">75</context>
@ -2870,6 +2874,10 @@
</trans-unit>
<trans-unit id="63da83692b85cf17e0606153029a83fd4038d6dd" datatype="html">
<source>Difficulty Adjustment</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">1,5</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="linenumber">1,5</context>
@ -2883,11 +2891,11 @@
<trans-unit id="0912816d94f2f05a7eee8f7622670e0c6bbbce16" datatype="html">
<source>Remaining</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">7,9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">66,69</context>
</context-group>
<note priority="1" from="description">difficulty-box.remaining</note>
@ -2896,11 +2904,11 @@
<source><x id="INTERPOLATION" equiv-text="&lt;span class=&quot;shared-block&quot;&gt;blocks&lt;/span&gt;&lt;/ng-template&gt;
&lt;ng-template"/> <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;shared-block&quot;&gt;"/>blocks<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">10,11</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">53,54</context>
</context-group>
<context-group purpose="location">
@ -2921,11 +2929,11 @@
<source><x id="INTERPOLATION" equiv-text="&lt;span class=&quot;shared-block&quot;&gt;block&lt;/span&gt;&lt;/ng-template&gt;
&lt;/div&gt;"/> <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;shared-block&quot;&gt;"/>block<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">11,12</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">54,55</context>
</context-group>
<context-group purpose="location">
@ -2937,11 +2945,11 @@
<trans-unit id="5e65ce907fc0e1a1a09d4130eb8c59d00a9457ef" datatype="html">
<source>Estimate</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">16,17</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">73,76</context>
</context-group>
<note priority="1" from="description">difficulty-box.estimate</note>
@ -2949,19 +2957,23 @@
<trans-unit id="680d5c75b7fd8d37961083608b9fcdc4167b4c43" datatype="html">
<source>Previous</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">31,33</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="linenumber">59,61</context>
</context-group>
<note priority="1" from="description">difficulty-box.previous</note>
</trans-unit>
<trans-unit id="5db469cd0357e5f578b85a996f7e99c9e4148ff5" datatype="html">
<source>Current Period</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">43,44</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">80,83</context>
</context-group>
<note priority="1" from="description">difficulty-box.current-period</note>
@ -2969,11 +2981,99 @@
<trans-unit id="df71fa93f0503396ea2bb3ba5161323330314d6c" datatype="html">
<source>Next Halving</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">50,52</context>
</context-group>
<note priority="1" from="description">difficulty-box.next-halving</note>
</trans-unit>
<trans-unit id="0c65c3ee0ce537e507e0b053b479012e5803d2cf" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> blocks expected</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty-tooltip.component.html</context>
<context context-type="linenumber">13</context>
</context-group>
<note priority="1" from="description">difficulty-box.expected-blocks</note>
</trans-unit>
<trans-unit id="ec9f27d00a7778cd1cfe1806105d2ca3314fa506" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> block expected</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty-tooltip.component.html</context>
<context context-type="linenumber">14</context>
</context-group>
<note priority="1" from="description">difficulty-box.expected-block</note>
</trans-unit>
<trans-unit id="b89cb92adf0a831d4a263ecdba02139abbda02ae" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> blocks mined</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty-tooltip.component.html</context>
<context context-type="linenumber">18</context>
</context-group>
<note priority="1" from="description">difficulty-box.mined-blocks</note>
</trans-unit>
<trans-unit id="4f7e823fd45c6def13a3f15f678888c7fe254fa5" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> block mined</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty-tooltip.component.html</context>
<context context-type="linenumber">19</context>
</context-group>
<note priority="1" from="description">difficulty-box.mined-block</note>
</trans-unit>
<trans-unit id="229dfb17b342aa8b9a1db27557069445ea1a7051" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> blocks remaining</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty-tooltip.component.html</context>
<context context-type="linenumber">24</context>
</context-group>
<note priority="1" from="description">difficulty-box.remaining-blocks</note>
</trans-unit>
<trans-unit id="13ff0d092caf85cd23815f0235e316dc3a6d1bbe" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> block remaining</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty-tooltip.component.html</context>
<context context-type="linenumber">25</context>
</context-group>
<note priority="1" from="description">difficulty-box.remaining-block</note>
</trans-unit>
<trans-unit id="4f78348af343fb64016891d67b53bdab473f9dbf" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> blocks ahead</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty-tooltip.component.html</context>
<context context-type="linenumber">29</context>
</context-group>
<note priority="1" from="description">difficulty-box.blocks-ahead</note>
</trans-unit>
<trans-unit id="15c5f3475966bf3be381378b046a65849f0f6bb6" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> block ahead</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty-tooltip.component.html</context>
<context context-type="linenumber">30</context>
</context-group>
<note priority="1" from="description">difficulty-box.block-ahead</note>
</trans-unit>
<trans-unit id="697b8cb1caaf1729809bc5c065d4dd873810550a" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> blocks behind</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty-tooltip.component.html</context>
<context context-type="linenumber">34</context>
</context-group>
<note priority="1" from="description">difficulty-box.blocks-behind</note>
</trans-unit>
<trans-unit id="32137887e3f5a25b3a016eb03357f4e363fccb0b" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> block behind</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty-tooltip.component.html</context>
<context context-type="linenumber">35</context>
</context-group>
<note priority="1" from="description">difficulty-box.block-behind</note>
</trans-unit>
<trans-unit id="5e78899c9b98f29856ce3c7c265e1344bc7a5a18" datatype="html">
<source>Average block time</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty/difficulty.component.html</context>
<context context-type="linenumber">42,45</context>
</context-group>
<note priority="1" from="description">difficulty-box.average-block-time</note>
</trans-unit>
<trans-unit id="6ff9e8b67bc2cda7569dc0996d4c2fd858c5d4e6" datatype="html">
<source>Either 2x the minimum, or the Low Priority rate (whichever is lower)</source>
<context-group purpose="location">
@ -4054,39 +4154,27 @@
<source>Just now</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">78</context>
<context context-type="linenumber">79</context>
</context-group>
</trans-unit>
<trans-unit id="time-since" datatype="html">
<source><x id="DATE" equiv-text="dateStrings.i18nYear"/> ago</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">97</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">98</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">99</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">100</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">101</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">102</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">103</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">105</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">106</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">107</context>
@ -4099,53 +4187,53 @@
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">109</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">110</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">111</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">112</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">113</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">114</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">115</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">116</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">117</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">118</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">119</context>
</context-group>
</trans-unit>
<trans-unit id="time-until" datatype="html">
<source>In ~<x id="DATE" equiv-text="dateStrings.i18nYear"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">120</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">121</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">122</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">123</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">124</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">125</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">126</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">127</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">128</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">129</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">130</context>
@ -4158,53 +4246,53 @@
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">132</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">133</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">134</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">135</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">136</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">137</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">138</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">139</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">140</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">141</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">142</context>
</context-group>
</trans-unit>
<trans-unit id="time-span" datatype="html">
<source>After <x id="DATE" equiv-text="dateStrings.i18nYear"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">143</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">144</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">145</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">146</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">147</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">148</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">149</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">150</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">151</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">152</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">153</context>
@ -4217,22 +4305,34 @@
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">155</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">156</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">157</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">158</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">159</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">160</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">161</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">162</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">163</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">164</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">165</context>
</context-group>
</trans-unit>
<trans-unit id="0094b97dd052620710f173e7aedf6807a1eba1f5" datatype="html">
<source>This transaction has been replaced by:</source>