73 lines
2.6 KiB
TypeScript
73 lines
2.6 KiB
TypeScript
import React from 'react';
|
|
import { useSyncExternalStore } from 'react';
|
|
import { subscribe, getSnapshot } from './TimeTicker';
|
|
|
|
interface TimeAgoProps {
|
|
time?: string | null | Date;
|
|
format?: 'short' | 'long';
|
|
showAgo?: boolean;
|
|
}
|
|
|
|
// Returns a compact label for a time difference (UTC millisecond inputs).
|
|
function formatRelative(nowMs: number, timeMs: number, fmt: 'short' | 'long' = 'short', showAgo = true): string {
|
|
const diffSec = Math.max(0, Math.floor((nowMs - timeMs) / 1000));
|
|
|
|
const units = [
|
|
{ threshold: 60, divisor: 1, short: 's', long: 'second' },
|
|
{ threshold: 60 * 60, divisor: 60, short: 'm', long: 'minute' },
|
|
{ threshold: 60 * 60 * 24, divisor: 60 * 60, short: 'h', long: 'hour' },
|
|
{ threshold: 60 * 60 * 24 * 7, divisor: 60 * 60 * 24, short: 'd', long: 'day' },
|
|
{ threshold: 60 * 60 * 24 * 30, divisor: 60 * 60 * 24 * 7, short: 'w', long: 'week' },
|
|
// months: fallback bucket
|
|
{ threshold: Infinity, divisor: 60 * 60 * 24 * 30, short: 'mo', long: 'month' },
|
|
];
|
|
|
|
const plural = (n: number, singular: string) => (n === 1 ? `${n} ${singular}` : `${n} ${singular}s`);
|
|
|
|
// find first matching unit
|
|
for (const u of units) {
|
|
if (diffSec < u.threshold) {
|
|
if (fmt === 'short') {
|
|
if (u.short === 'mo') {
|
|
// for months, compute from days to ensure at least 1 month
|
|
const months = Math.max(1, Math.floor(diffSec / u.divisor));
|
|
return `${months}${u.short}${showAgo ? ' ago' : ''}`;
|
|
}
|
|
const value = Math.floor(diffSec / u.divisor);
|
|
return `${value}${u.short}${showAgo ? ' ago' : ''}`;
|
|
}
|
|
|
|
// long format
|
|
if (diffSec < 1) return 'just now';
|
|
if (u.long === 'month') {
|
|
const months = Math.max(1, Math.floor(diffSec / u.divisor));
|
|
return `${plural(months, u.long)}${showAgo ? ' ago' : ''}`;
|
|
}
|
|
const value = Math.floor(diffSec / u.divisor);
|
|
return `${plural(value, u.long)}${showAgo ? ' ago' : ''}`;
|
|
}
|
|
}
|
|
|
|
return 'just now';
|
|
}
|
|
|
|
// TimeAgo subscribes to a single shared ticker (via useSyncExternalStore) so
|
|
// many instances update efficiently without per-component timers.
|
|
const TimeAgo: React.FC<TimeAgoProps> = ({ time, format = 'short', showAgo = true }) => {
|
|
if (!time) return <>-</>;
|
|
|
|
const now = useSyncExternalStore(subscribe, getSnapshot);
|
|
|
|
const timeMs = typeof time === 'string'
|
|
? Date.parse(time)
|
|
: (time instanceof Date ? time.getTime() : NaN);
|
|
|
|
if (!timeMs || Number.isNaN(timeMs)) return <>-</>;
|
|
|
|
const label = formatRelative(now, timeMs, format, showAgo);
|
|
|
|
return <span title={new Date(timeMs).toLocaleString()}>{label}</span>;
|
|
};
|
|
|
|
export default TimeAgo;
|