Merge pull request #4836 from mempool/mononaut/first-seen-tooltip
Display first seen time on block visualisation tooltips
This commit is contained in:
		
						commit
						c51159d275
					
				@ -552,6 +552,7 @@ export class Common {
 | 
			
		||||
      value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
 | 
			
		||||
      acc: tx.acceleration || undefined,
 | 
			
		||||
      rate: tx.effectiveFeePerVsize,
 | 
			
		||||
      time: tx.firstSeen || undefined,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -598,7 +598,8 @@ class MempoolBlocks {
 | 
			
		||||
        tx.value,
 | 
			
		||||
        Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
 | 
			
		||||
        tx.flags,
 | 
			
		||||
        1
 | 
			
		||||
        tx.time || 0,
 | 
			
		||||
        1,
 | 
			
		||||
      ];
 | 
			
		||||
    } else {
 | 
			
		||||
      return [
 | 
			
		||||
@ -608,6 +609,7 @@ class MempoolBlocks {
 | 
			
		||||
        tx.value,
 | 
			
		||||
        Math.round((tx.rate || (tx.fee / tx.vsize)) * 100) / 100,
 | 
			
		||||
        tx.flags,
 | 
			
		||||
        tx.time || 0,
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -200,6 +200,7 @@ export interface TransactionStripped {
 | 
			
		||||
  value: number;
 | 
			
		||||
  acc?: boolean;
 | 
			
		||||
  rate?: number; // effective fee rate
 | 
			
		||||
  time?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionClassified extends TransactionStripped {
 | 
			
		||||
@ -207,7 +208,7 @@ export interface TransactionClassified extends TransactionStripped {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// [txid, fee, vsize, value, rate, flags, acceleration?]
 | 
			
		||||
export type TransactionCompressed = [string, number, number, number, number, number, 1?];
 | 
			
		||||
export type TransactionCompressed = [string, number, number, number, number, number, number, 1?];
 | 
			
		||||
// [txid, rate, flags, acceleration?]
 | 
			
		||||
export type MempoolDeltaChange = [string, number, number, (1|0)];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@
 | 
			
		||||
      [blockConversion]="blockConversion"
 | 
			
		||||
      [filterFlags]="activeFilterFlags"
 | 
			
		||||
      [filterMode]="filterMode"
 | 
			
		||||
      [relativeTime]="relativeTime"
 | 
			
		||||
    ></app-block-overview-tooltip>
 | 
			
		||||
    <app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
 | 
			
		||||
    <div *ngIf="!webGlEnabled" class="placeholder">
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
 | 
			
		||||
  @Input() excludeFilters: string[] = [];
 | 
			
		||||
  @Input() filterFlags: bigint | null = null;
 | 
			
		||||
  @Input() filterMode: FilterMode = 'and';
 | 
			
		||||
  @Input() relativeTime: number | null;
 | 
			
		||||
  @Input() blockConversion: Price;
 | 
			
		||||
  @Input() overrideColors: ((tx: TxView) => Color) | null = null;
 | 
			
		||||
  @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
  rate?: number;
 | 
			
		||||
  flags: number;
 | 
			
		||||
  bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
 | 
			
		||||
  time?: number;
 | 
			
		||||
  status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
 | 
			
		||||
  context?: 'projected' | 'actual';
 | 
			
		||||
  scene?: BlockScene;
 | 
			
		||||
@ -53,6 +54,7 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
    this.scene = scene;
 | 
			
		||||
    this.context = tx.context;
 | 
			
		||||
    this.txid = tx.txid;
 | 
			
		||||
    this.time = tx.time || 0;
 | 
			
		||||
    this.fee = tx.fee;
 | 
			
		||||
    this.vsize = tx.vsize;
 | 
			
		||||
    this.value = tx.value;
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,26 @@
 | 
			
		||||
          <a [routerLink]="['/tx/' | relativeUrl, txid]">{{ txid | shortenString : 16}}</a>
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr *ngIf="time">
 | 
			
		||||
        <ng-container [ngSwitch]="timeMode">
 | 
			
		||||
          <ng-container *ngSwitchCase="'mempool'">
 | 
			
		||||
            <td class="label" i18n="transaction.first-seen|Transaction first seen">First seen</td>
 | 
			
		||||
            <td class="value"><i><app-time kind="since" [time]="time" [fastRender]="true"></app-time></i></td>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
          <ng-container *ngSwitchCase="'missed'">
 | 
			
		||||
            <td class="label" i18n="transaction.first-seen|Transaction first seen">First seen</td>
 | 
			
		||||
            <td class="value"><i><app-time kind="before" [time]="relativeTime - time"></app-time></i></td>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
          <ng-container *ngSwitchCase="'after'">
 | 
			
		||||
            <td class="label" i18n="transaction.first-seen|Transaction first seen">First seen</td>
 | 
			
		||||
            <td class="value"><i><app-time kind="span" [time]="time - relativeTime"></app-time></i></td>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
          <ng-container *ngSwitchCase="'mined'">
 | 
			
		||||
            <td class="label" i18n="transaction.confirmed-after|Transaction confirmed after">Confirmed</td>
 | 
			
		||||
            <td class="value"><i><app-time kind="span" [time]="relativeTime - time"></app-time></i></td>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td class="label" i18n="dashboard.latest-transactions.amount">Amount</td>
 | 
			
		||||
        <td class="value"><app-amount [blockConversion]="blockConversion" [satoshis]="value" [noFiat]="true"></app-amount></td>
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { Position } from '../../components/block-overview-graph/sprite-types.js'
 | 
			
		||||
import { Price } from '../../services/price.service';
 | 
			
		||||
import { TransactionStripped } from '../../interfaces/node-api.interface.js';
 | 
			
		||||
import { Filter, FilterMode, TransactionFlags, toFilters } from '../../shared/filters.utils';
 | 
			
		||||
import { Block } from '../../interfaces/electrs.interface.js';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block-overview-tooltip',
 | 
			
		||||
@ -11,6 +12,7 @@ import { Filter, FilterMode, TransactionFlags, toFilters } from '../../shared/fi
 | 
			
		||||
})
 | 
			
		||||
export class BlockOverviewTooltipComponent implements OnChanges {
 | 
			
		||||
  @Input() tx: TransactionStripped | void;
 | 
			
		||||
  @Input() relativeTime?: number;
 | 
			
		||||
  @Input() cursorPosition: Position;
 | 
			
		||||
  @Input() clickable: boolean;
 | 
			
		||||
  @Input() auditEnabled: boolean = false;
 | 
			
		||||
@ -19,6 +21,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
 | 
			
		||||
  @Input() filterMode: FilterMode = 'and';
 | 
			
		||||
 | 
			
		||||
  txid = '';
 | 
			
		||||
  time: number = 0;
 | 
			
		||||
  fee = 0;
 | 
			
		||||
  value = 0;
 | 
			
		||||
  vsize = 1;
 | 
			
		||||
@ -26,6 +29,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
 | 
			
		||||
  effectiveRate;
 | 
			
		||||
  acceleration;
 | 
			
		||||
  hasEffectiveRate: boolean = false;
 | 
			
		||||
  timeMode: 'mempool' | 'mined' | 'missed' | 'after' = 'mempool';
 | 
			
		||||
  filters: Filter[] = [];
 | 
			
		||||
  activeFilters: { [key: string]: boolean } = {};
 | 
			
		||||
 | 
			
		||||
@ -56,6 +60,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
 | 
			
		||||
 | 
			
		||||
    if (this.tx && (changes.tx || changes.filterFlags || changes.filterMode)) {
 | 
			
		||||
      this.txid = this.tx.txid || '';
 | 
			
		||||
      this.time = this.tx.time || 0;
 | 
			
		||||
      this.fee = this.tx.fee || 0;
 | 
			
		||||
      this.value = this.tx.value || 0;
 | 
			
		||||
      this.vsize = this.tx.vsize || 1;
 | 
			
		||||
@ -72,6 +77,22 @@ export class BlockOverviewTooltipComponent implements OnChanges {
 | 
			
		||||
          this.activeFilters[filter.key] = true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!this.relativeTime) {
 | 
			
		||||
        this.timeMode = 'mempool';
 | 
			
		||||
      } else {
 | 
			
		||||
        if (this.tx?.context === 'actual' || this.tx?.status === 'found') {
 | 
			
		||||
          this.timeMode = 'mined';
 | 
			
		||||
        } else {
 | 
			
		||||
          const time = this.relativeTime || Date.now();
 | 
			
		||||
          if (this.time <= time) {
 | 
			
		||||
            this.timeMode = 'missed';
 | 
			
		||||
          } else {
 | 
			
		||||
            this.timeMode = 'after';
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.cd.markForCheck();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@
 | 
			
		||||
      [orientation]="'top'"
 | 
			
		||||
      [flip]="false"
 | 
			
		||||
      [disableSpinner]="true"
 | 
			
		||||
      [relativeTime]="block?.timestamp"
 | 
			
		||||
      (txClickEvent)="onTxClick($event)"
 | 
			
		||||
    ></app-block-overview-graph>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -117,6 +117,7 @@
 | 
			
		||||
            [blockConversion]="blockConversion"
 | 
			
		||||
            [showFilters]="true"
 | 
			
		||||
            [excludeFilters]="['replacement']"
 | 
			
		||||
            [relativeTime]="block?.timestamp"
 | 
			
		||||
            (txClickEvent)="onTxClick($event)"
 | 
			
		||||
          ></app-block-overview-graph>
 | 
			
		||||
          <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
 | 
			
		||||
@ -232,7 +233,7 @@
 | 
			
		||||
          <app-block-overview-graph #blockGraphProjected [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86"
 | 
			
		||||
            [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
 | 
			
		||||
            (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"
 | 
			
		||||
            [showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
 | 
			
		||||
            [showFilters]="true" [excludeFilters]="['replacement']" [relativeTime]="block?.timestamp"></app-block-overview-graph>
 | 
			
		||||
          <ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
 | 
			
		||||
        </div>
 | 
			
		||||
        <ng-container *ngIf="network !== 'liquid'">
 | 
			
		||||
@ -247,7 +248,7 @@
 | 
			
		||||
          <app-block-overview-graph #blockGraphActual [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86"
 | 
			
		||||
            [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined"  [auditHighlighting]="showAudit"
 | 
			
		||||
            (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"
 | 
			
		||||
            [showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
 | 
			
		||||
            [showFilters]="true" [excludeFilters]="['replacement']" [relativeTime]="block?.timestamp"></app-block-overview-graph>
 | 
			
		||||
          <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
 | 
			
		||||
        </div>
 | 
			
		||||
        <ng-container *ngIf="network !== 'liquid'">
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@
 | 
			
		||||
          [animationDuration]="animationDuration"
 | 
			
		||||
          [animationOffset]="animationOffset"
 | 
			
		||||
          [disableSpinner]="true"
 | 
			
		||||
          [relativeTime]="blockInfo[i]?.timestamp"
 | 
			
		||||
          (txClickEvent)="onTxClick($event)"
 | 
			
		||||
        ></app-block-overview-graph>
 | 
			
		||||
        <div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  @Input() time: number;
 | 
			
		||||
  @Input() dateString: number;
 | 
			
		||||
  @Input() kind: 'plain' | 'since' | 'until' | 'span' = 'plain';
 | 
			
		||||
  @Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' = 'plain';
 | 
			
		||||
  @Input() fastRender = false;
 | 
			
		||||
  @Input() fixedRender = false;
 | 
			
		||||
  @Input() relative = false;
 | 
			
		||||
@ -86,7 +86,9 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        seconds = Math.floor(this.time);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (seconds < 60) {
 | 
			
		||||
    if (seconds < 1 && this.kind === 'span') {
 | 
			
		||||
      return $localize`:@@date-base.immediately:Immediately`;
 | 
			
		||||
    } else if (seconds < 60) {
 | 
			
		||||
      if (this.relative || this.kind === 'since') {
 | 
			
		||||
        return $localize`:@@date-base.just-now:Just now`;
 | 
			
		||||
      } else if (this.kind === 'until') {
 | 
			
		||||
@ -206,6 +208,29 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case 'before':
 | 
			
		||||
      if (number === 1) {
 | 
			
		||||
        switch (unit) { // singular (1 day)
 | 
			
		||||
          case 'year': return $localize`:@@time-span:${dateStrings.i18nYear}:DATE: before`; break;
 | 
			
		||||
          case 'month': return $localize`:@@time-span:${dateStrings.i18nMonth}:DATE: before`; break;
 | 
			
		||||
          case 'week': return $localize`:@@time-span:${dateStrings.i18nWeek}:DATE: before`; break;
 | 
			
		||||
          case 'day': return $localize`:@@time-span:${dateStrings.i18nDay}:DATE: before`; break;
 | 
			
		||||
          case 'hour': return $localize`:@@time-span:${dateStrings.i18nHour}:DATE: before`; break;
 | 
			
		||||
          case 'minute': return $localize`:@@time-span:${dateStrings.i18nMinute}:DATE: before`; break;
 | 
			
		||||
          case 'second': return $localize`:@@time-span:${dateStrings.i18nSecond}:DATE: before`; break;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        switch (unit) { // plural (2 days)
 | 
			
		||||
          case 'year': return $localize`:@@time-span:${dateStrings.i18nYears}:DATE: before`; break;
 | 
			
		||||
          case 'month': return $localize`:@@time-span:${dateStrings.i18nMonths}:DATE: before`; break;
 | 
			
		||||
          case 'week': return $localize`:@@time-span:${dateStrings.i18nWeeks}:DATE: before`; break;
 | 
			
		||||
          case 'day': return $localize`:@@time-span:${dateStrings.i18nDays}:DATE: before`; break;
 | 
			
		||||
          case 'hour': return $localize`:@@time-span:${dateStrings.i18nHours}:DATE: before`; break;
 | 
			
		||||
          case 'minute': return $localize`:@@time-span:${dateStrings.i18nMinutes}:DATE: before`; break;
 | 
			
		||||
          case 'second': return $localize`:@@time-span:${dateStrings.i18nSeconds}:DATE: before`; break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
      default:
 | 
			
		||||
        if (number === 1) {
 | 
			
		||||
          switch (unit) { // singular (1 day)
 | 
			
		||||
 | 
			
		||||
@ -230,6 +230,7 @@ export interface TransactionStripped {
 | 
			
		||||
  rate?: number; // effective fee rate
 | 
			
		||||
  acc?: boolean;
 | 
			
		||||
  flags?: number | null;
 | 
			
		||||
  time?: number;
 | 
			
		||||
  status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
 | 
			
		||||
  context?: 'projected' | 'actual';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -101,12 +101,13 @@ export interface TransactionStripped {
 | 
			
		||||
  acc?: boolean; // is accelerated?
 | 
			
		||||
  rate?: number; // effective fee rate
 | 
			
		||||
  flags?: number;
 | 
			
		||||
  time?: number;
 | 
			
		||||
  status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
 | 
			
		||||
  context?: 'projected' | 'actual';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// [txid, fee, vsize, value, rate, flags, acceleration?]
 | 
			
		||||
export type TransactionCompressed = [string, number, number, number, number, number, 1?];
 | 
			
		||||
export type TransactionCompressed = [string, number, number, number, number, number, number, 1?];
 | 
			
		||||
// [txid, rate, flags, acceleration?]
 | 
			
		||||
export type MempoolDeltaChange = [string, number, number, (1|0)];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -164,7 +164,8 @@ export function uncompressTx(tx: TransactionCompressed): TransactionStripped {
 | 
			
		||||
    value: tx[3],
 | 
			
		||||
    rate: tx[4],
 | 
			
		||||
    flags: tx[5],
 | 
			
		||||
    acc: !!tx[6],
 | 
			
		||||
    time: tx[6],
 | 
			
		||||
    acc: !!tx[7],
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user