mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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') ||
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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`;
|
||||
}
|
||||
Reference in New Issue
Block a user