diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 50bbd88b9..d1129a602 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -21,6 +21,7 @@ import { StorageService } from './services/storage.service'; import { HttpCacheInterceptor } from './services/http-cache.interceptor'; import { LanguageService } from './services/language.service'; import { ThemeService } from './services/theme.service'; +import { TimeService } from './services/time.service'; import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe'; import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe'; import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; @@ -42,6 +43,7 @@ const providers = [ EnterpriseService, LanguageService, ThemeService, + TimeService, ShortenStringPipe, FiatShortenerPipe, FiatCurrencyPipe, diff --git a/frontend/src/app/components/time/time.component.ts b/frontend/src/app/components/time/time.component.ts index f0c73c80b..6360bca4a 100644 --- a/frontend/src/app/components/time/time.component.ts +++ b/frontend/src/app/components/time/time.component.ts @@ -1,29 +1,6 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core'; import { StateService } from '../../services/state.service'; -import { dates } from '../../shared/i18n/dates'; -import { DatePipe } from '@angular/common'; - -const datePipe = new DatePipe(navigator.language || 'en-US'); - -const intervals = { - year: 31536000, - month: 2592000, - week: 604800, - day: 86400, - hour: 3600, - minute: 60, - second: 1 -}; - -const precisionThresholds = { - year: 100, - month: 18, - week: 12, - day: 31, - hour: 48, - minute: 90, - second: 90 -}; +import { TimeService } from '../../services/time.service'; @Component({ selector: 'app-time', @@ -52,6 +29,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { constructor( private ref: ChangeDetectorRef, private stateService: StateService, + private timeService: TimeService, ) {} ngOnInit() { @@ -79,7 +57,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { } calculateTime(): void { - const { text, tooltip } = TimeComponent.calculate( + const { text, tooltip } = this.timeService.calculate( this.time, this.kind, this.relative, @@ -95,238 +73,4 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { this.text = text; this.tooltip = tooltip; } - - static calculate( - time: number, - kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within', - relative: boolean = false, - precision: number = 0, - minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second', - showTooltip: boolean = false, - units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], - dateString?: string, - lowercaseStart: boolean = false, - numUnits: number = 1, - fractionDigits: number = 0, - ): { text: string, tooltip: string } { - if (time == null) { - return { text: '', tooltip: '' }; - } - - let seconds: number; - let tooltip: string = ''; - switch (kind) { - case 'since': - seconds = Math.floor((+new Date() - +new Date(dateString || time * 1000)) / 1000); - tooltip = datePipe.transform(new Date(dateString || time * 1000), 'yyyy-MM-dd HH:mm'); - break; - case 'until': - case 'within': - seconds = (+new Date(time) - +new Date()) / 1000; - tooltip = datePipe.transform(new Date(time), 'yyyy-MM-dd HH:mm'); - break; - default: - seconds = Math.floor(time); - tooltip = ''; - } - - if (!showTooltip || relative) { - tooltip = ''; - } - - if (seconds < 1 && kind === 'span') { - return { tooltip, text: $localize`:@@date-base.immediately:Immediately` }; - } else if (seconds < 60) { - if (relative || kind === 'since') { - if (lowercaseStart) { - return { tooltip, text: $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1) }; - } - return { tooltip, text: $localize`:@@date-base.just-now:Just now` }; - } else if (kind === 'until' || kind === 'within') { - seconds = 60; - } - } - - let counter: number; - const result = []; - let usedUnits = 0; - for (const [index, unit] of units.entries()) { - let precisionUnit = units[Math.min(units.length - 1, index + precision)]; - counter = Math.floor(seconds / intervals[unit]); - const precisionCounter = Math.round(seconds / intervals[precisionUnit]); - if (precisionCounter > precisionThresholds[precisionUnit]) { - precisionUnit = unit; - } - if (units.indexOf(precisionUnit) === units.indexOf(minUnit)) { - counter = Math.max(1, counter); - } - if (counter > 0) { - let rounded; - const roundFactor = Math.pow(10,fractionDigits || 0); - if ((kind === 'until' || kind === 'within') && usedUnits < numUnits) { - rounded = Math.floor((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; - } else { - rounded = Math.round((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; - } - if ((kind !== 'until' && kind !== 'within')|| numUnits === 1) { - return { tooltip, text: TimeComponent.formatTime(kind, precisionUnit, rounded) }; - } else { - if (!usedUnits) { - result.push(TimeComponent.formatTime(kind, precisionUnit, rounded)); - } else { - result.push(TimeComponent.formatTime('', precisionUnit, rounded)); - } - seconds -= (rounded * intervals[precisionUnit]); - usedUnits++; - if (usedUnits >= numUnits) { - return { tooltip, text: result.join(', ') }; - } - } - } - } - return { tooltip, text: result.join(', ') }; - } - - static formatTime(kind, unit, number): string { - const dateStrings = dates(number); - switch (kind) { - case 'since': - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break; - case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break; - case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break; - case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break; - case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break; - case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break; - case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break; - case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break; - case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break; - case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break; - case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break; - case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break; - case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break; - } - } - break; - case 'until': - if (number === 1) { - switch (unit) { // singular (In ~1 day) - case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break; - case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break; - case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break; - case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break; - case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break; - case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`; - case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`; - } - } else { - switch (unit) { // plural (In ~2 days) - case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break; - case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break; - case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break; - case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break; - case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break; - case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break; - case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break; - } - } - break; - case 'within': - if (number === 1) { - switch (unit) { // singular (In ~1 day) - case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYear}:DATE:`; break; - case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonth}:DATE:`; break; - case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeek}:DATE:`; break; - case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDay}:DATE:`; break; - case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHour}:DATE:`; break; - case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinute}:DATE:`; - case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSecond}:DATE:`; - } - } else { - switch (unit) { // plural (In ~2 days) - case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYears}:DATE:`; break; - case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonths}:DATE:`; break; - case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeeks}:DATE:`; break; - case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDays}:DATE:`; break; - case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHours}:DATE:`; break; - case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinutes}:DATE:`; break; - case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSeconds}:DATE:`; break; - } - } - break; - case 'span': - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break; - case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break; - case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break; - case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break; - case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break; - case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break; - case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break; - case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break; - case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break; - case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break; - case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break; - case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break; - case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break; - } - } - break; - case 'before': - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return $localize`:@@time-before:${dateStrings.i18nYear}:DATE: before`; break; - case 'month': return $localize`:@@time-before:${dateStrings.i18nMonth}:DATE: before`; break; - case 'week': return $localize`:@@time-before:${dateStrings.i18nWeek}:DATE: before`; break; - case 'day': return $localize`:@@time-before:${dateStrings.i18nDay}:DATE: before`; break; - case 'hour': return $localize`:@@time-before:${dateStrings.i18nHour}:DATE: before`; break; - case 'minute': return $localize`:@@time-before:${dateStrings.i18nMinute}:DATE: before`; break; - case 'second': return $localize`:@@time-before:${dateStrings.i18nSecond}:DATE: before`; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return $localize`:@@time-before:${dateStrings.i18nYears}:DATE: before`; break; - case 'month': return $localize`:@@time-before:${dateStrings.i18nMonths}:DATE: before`; break; - case 'week': return $localize`:@@time-before:${dateStrings.i18nWeeks}:DATE: before`; break; - case 'day': return $localize`:@@time-before:${dateStrings.i18nDays}:DATE: before`; break; - case 'hour': return $localize`:@@time-before:${dateStrings.i18nHours}:DATE: before`; break; - case 'minute': return $localize`:@@time-before:${dateStrings.i18nMinutes}:DATE: before`; break; - case 'second': return $localize`:@@time-before:${dateStrings.i18nSeconds}:DATE: before`; break; - } - } - break; - default: - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return dateStrings.i18nYear; break; - case 'month': return dateStrings.i18nMonth; break; - case 'week': return dateStrings.i18nWeek; break; - case 'day': return dateStrings.i18nDay; break; - case 'hour': return dateStrings.i18nHour; break; - case 'minute': return dateStrings.i18nMinute; break; - case 'second': return dateStrings.i18nSecond; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return dateStrings.i18nYears; break; - case 'month': return dateStrings.i18nMonths; break; - case 'week': return dateStrings.i18nWeeks; break; - case 'day': return dateStrings.i18nDays; break; - case 'hour': return dateStrings.i18nHours; break; - case 'minute': return dateStrings.i18nMinutes; break; - case 'second': return dateStrings.i18nSeconds; break; - } - } - } - } } diff --git a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts index 91dc70240..310ff0356 100644 --- a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts +++ b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { EChartsOption } from '../../graphs/echarts'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { Subscription } from 'rxjs'; import { Utxo } from '../../interfaces/electrs.interface'; import { StateService } from '../../services/state.service'; import { Router } from '@angular/router'; @@ -8,6 +8,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi import { renderSats } from '../../shared/common.utils'; import { colorToHex, hexToColor, mix } from '../block-overview-graph/utils'; import { TimeComponent } from '../time/time.component'; +import { TimeService } from '../../services/time.service'; const newColorHex = '1bd8f4'; const oldColorHex = '9339f4'; @@ -55,6 +56,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { private zone: NgZone, private router: Router, private relativeUrlPipe: RelativeUrlPipe, + private timeService: TimeService, ) { // re-render the chart every 10 seconds, to keep the age colors up to date this.updateInterval = setInterval(() => { @@ -276,7 +278,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy {
${valueStr}
- ${utxo.status.confirmed ? 'Confirmed ' + TimeComponent.calculate(utxo.status.block_time, 'since', true, 1, 'minute').text : 'Pending'} + ${utxo.status.confirmed ? 'Confirmed ' + this.timeService.calculate(utxo.status.block_time, 'since', true, 1, 'minute').text : 'Pending'} `; }, }