refactor(agentworld): extract shared relativeTime helper (#4427) (#4518)

This commit is contained in:
mysma-9403
2026-07-14 19:28:11 +03:00
committed by GitHub
parent 4143d35de9
commit fe1419e62e
7 changed files with 90 additions and 55 deletions
+1 -11
View File
@@ -32,6 +32,7 @@ import type { ToastNotification } from '../../types/intelligence';
import { apiClient } from '../AgentWorldShell';
import { decimalsForAsset, resolveAssetSymbol } from '../assets';
import X402ConfirmDialog, { formatUnits } from '../components/X402ConfirmDialog';
import { relativeTime } from './relativeTime';
// ── State types ───────────────────────────────────────────────────────────────
@@ -42,17 +43,6 @@ type BountiesState =
// ── Helpers ───────────────────────────────────────────────────────────────────
function relativeTime(iso: string): string {
const ms = Date.now() - new Date(iso).getTime();
const mins = Math.floor(ms / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
return `${days}d ago`;
}
/** Group the integer part of a numeric amount with thousands separators. */
function formatAmount(amount: string): string {
if (!Number.isFinite(Number(amount))) return amount;
@@ -29,6 +29,7 @@ import { useT } from '../../../lib/i18n/I18nContext';
import { apiClient } from '../../AgentWorldShell';
import { decimalsForAsset, resolveAssetSymbol } from '../../assets';
import { formatUnits } from '../../components/X402ConfirmDialog';
import { relativeTime } from '../relativeTime';
const debug = debugFactory('agentworld:explore');
@@ -405,16 +406,6 @@ function JobSkeletonList() {
);
}
function relativeTime(isoDate: string): string {
const delta = Date.now() - new Date(isoDate).getTime();
const mins = Math.floor(delta / 60_000);
if (mins < 60) return `${mins}m ago`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
function JobRow({ job }: { job: GqlJobPosting }) {
const skills = job.skills ?? [];
return (
+1 -11
View File
@@ -32,6 +32,7 @@ import {
import { fetchWalletStatus } from '../../services/walletApi';
import { apiClient } from '../AgentWorldShell';
import ConfirmDialog from '../components/ConfirmDialog';
import { relativeTime } from './relativeTime';
const log = debug('agentworld:feed');
@@ -65,17 +66,6 @@ type WalletResolution = { agentId: string | null; configured: WalletConfigured }
// ── Helpers ───────────────────────────────────────────────────────────────────
function relativeTime(iso: string): string {
const ms = Date.now() - new Date(iso).getTime();
const mins = Math.floor(ms / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
return `${days}d ago`;
}
function isWalletLocked(message: string): boolean {
return (
message.includes('wallet is not configured') ||
+1 -12
View File
@@ -29,6 +29,7 @@ import { useT } from '../../lib/i18n/I18nContext';
import { fetchWalletStatus } from '../../services/walletApi';
import { apiClient } from '../AgentWorldShell';
import { explorerTxUrl } from '../hooks/useX402Buy';
import { relativeTime } from './relativeTime';
// ── State types ───────────────────────────────────────────────────────────────
@@ -39,18 +40,6 @@ type JobsState =
// ── Helpers ───────────────────────────────────────────────────────────────────
// TODO: extract shared relativeTime helper once Feed/Ledger/Jobs all use it.
function relativeTime(iso: string): string {
const ms = Date.now() - new Date(iso).getTime();
const mins = Math.floor(ms / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
return `${days}d ago`;
}
/**
* Group the integer part of a numeric amount with thousands separators while
* preserving the original decimals ("1000000" → "1,000,000", "0.50" → "0.50").
+1 -11
View File
@@ -17,6 +17,7 @@ import { apiClient } from '../AgentWorldShell';
import { decimalsForAsset, resolveAssetSymbol } from '../assets';
import { formatUnits, friendlyNetwork } from '../components/X402ConfirmDialog';
import { explorerTxUrl } from '../hooks/useX402Buy';
import { relativeTime } from './relativeTime';
// ── State types ───────────────────────────────────────────────────────────────
@@ -27,17 +28,6 @@ type LedgerState =
// ── Helpers ───────────────────────────────────────────────────────────────────
function relativeTime(iso: string): string {
const ms = Date.now() - new Date(iso).getTime();
const mins = Math.floor(ms / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
return `${days}d ago`;
}
export function abbreviateAddress(addr: string | undefined): string {
if (!addr) return '—';
if (addr.length <= 12) return addr;
@@ -0,0 +1,56 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { relativeTime } from './relativeTime';
describe('relativeTime', () => {
const NOW = 1_700_000_000_000;
const freezeAt = (now: number) => {
vi.useFakeTimers();
vi.setSystemTime(now);
};
const ago = (ms: number) => new Date(NOW - ms).toISOString();
afterEach(() => {
vi.useRealTimers();
});
it('renders sub-minute deltas as "just now"', () => {
freezeAt(NOW);
expect(relativeTime(ago(0))).toBe('just now');
expect(relativeTime(ago(30_000))).toBe('just now');
});
it('renders whole minutes', () => {
freezeAt(NOW);
expect(relativeTime(ago(60_000))).toBe('1m ago');
expect(relativeTime(ago(5 * 60_000))).toBe('5m ago');
expect(relativeTime(ago(59 * 60_000))).toBe('59m ago');
});
it('renders whole hours', () => {
freezeAt(NOW);
expect(relativeTime(ago(60 * 60_000))).toBe('1h ago');
expect(relativeTime(ago(3 * 60 * 60_000))).toBe('3h ago');
expect(relativeTime(ago(23 * 60 * 60_000))).toBe('23h ago');
});
it('renders whole days', () => {
freezeAt(NOW);
expect(relativeTime(ago(24 * 60 * 60_000))).toBe('1d ago');
expect(relativeTime(ago(2 * 24 * 60 * 60_000))).toBe('2d ago');
});
it('treats small negative deltas from clock skew as "just now"', () => {
freezeAt(NOW);
// Timestamp 5s in the future (server clock ahead of client).
expect(relativeTime(new Date(NOW + 5_000).toISOString())).toBe('just now');
});
it('returns "just now" for an unparseable date instead of "NaNd ago"', () => {
freezeAt(NOW);
expect(relativeTime('not-a-date')).toBe('just now');
expect(relativeTime('')).toBe('just now');
});
});
+29
View File
@@ -0,0 +1,29 @@
/**
* Format an ISO-8601 timestamp as a short, human-readable relative time
* (`just now`, `5m ago`, `3h ago`, `2d ago`).
*
* Single source of truth for the Agent World sections. Feed, Ledger, Jobs and
* Bounties each carried a byte-identical copy, and Explore a near-identical one
* (see issue #4427). Duplication meant any future change — adding weeks,
* localisation, or just a unit test — had to be applied in several places, and
* a missed update would silently produce inconsistent timestamps across
* sections.
*
* Sub-minute deltas, including the small negative ones produced by client/server
* clock skew, collapse to `just now`. An unparseable `iso` (whose `getTime()`
* is `NaN`) also collapses to `just now` rather than rendering `NaNd ago`.
*/
export function relativeTime(iso: string): string {
const ms = Date.now() - new Date(iso).getTime();
// `new Date('garbage').getTime()` is NaN; without this guard every `<`
// comparison below is false and it falls through to `NaN d ago`. Treat an
// unparseable timestamp as the safe default.
if (!Number.isFinite(ms)) return 'just now';
const mins = Math.floor(ms / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
return `${days}d ago`;
}