Files
cinny/src/app/utils/formatTimestamp.ts
T

118 lines
4.1 KiB
TypeScript
Raw Normal View History

import dayjs from 'dayjs';
/**
* [Gitea #139] One place that turns a timestamp into words, honouring the
* user's 12/24 h clock and date-format preferences everywhere.
*
* Rules for the `auto` styles, relative to `now`:
* today → "21:14"
* yesterday → "Yesterday 21:14"
* tomorrow → "Tomorrow 21:14" (scheduled sends, reminders)
* last 6 days → "Mon 21:14"
* older / further → "5 Sep 2026 21:14" (date per `dateFormatString`)
*/
export type TimestampPrefs = {
hour24Clock: boolean;
dateFormatString: string;
};
export type TimestampStyle =
/** Time for today, day word + time for nearby days, date + time otherwise. */
| 'auto'
/** Like `auto` but never the time: "Today", "Yesterday", "Mon", "5 Sep 2026". */
| 'autoDate'
/** Always the clock time. */
| 'time'
/** Always the date per `dateFormatString`. */
| 'date'
/** Always date + time. */
| 'dateTime';
const DEFAULT_DATE_FORMAT = 'D MMM YYYY';
const dateFormat = (prefs: TimestampPrefs) => prefs.dateFormatString || DEFAULT_DATE_FORMAT;
export const formatTime = (ts: number, prefs: Pick<TimestampPrefs, 'hour24Clock'>): string =>
dayjs(ts).format(prefs.hour24Clock ? 'HH:mm' : 'hh:mm A');
export const formatDate = (ts: number, prefs: TimestampPrefs): string =>
dayjs(ts).format(dateFormat(prefs));
/** Whole days between the start of `ts`'s day and the start of `now`'s day (negative = future). */
export const dayDistance = (ts: number, now: number): number =>
dayjs(now).startOf('day').diff(dayjs(ts).startOf('day'), 'day');
/** "Today" / "Yesterday" / "Tomorrow" / "Mon" for nearby days, else undefined. */
export const dayWord = (ts: number, now: number): string | undefined => {
const d = dayDistance(ts, now);
if (d === 0) return 'Today';
if (d === 1) return 'Yesterday';
if (d === -1) return 'Tomorrow';
if (d > 1 && d < 7) return dayjs(ts).format('ddd');
return undefined;
};
export function formatTimestamp(
ts: number,
prefs: TimestampPrefs,
style: TimestampStyle = 'auto',
now: number = Date.now(),
): string {
const time = formatTime(ts, prefs);
if (style === 'time') return time;
const date = formatDate(ts, prefs);
if (style === 'date') return date;
if (style === 'dateTime') return `${date} ${time}`;
const word = dayWord(ts, now);
if (style === 'autoDate') return word ?? date;
if (word === 'Today') return time;
return `${word ?? date} ${time}`;
}
/** Short relative age for dense lists: "now", "5m", "3h", "Yesterday", "Mon", "5 Sep". */
export function formatShortAge(
ts: number,
prefs: TimestampPrefs,
now: number = Date.now(),
): string {
const diffMin = Math.floor((now - ts) / 60_000);
if (diffMin < 60) return diffMin < 1 ? 'now' : `${diffMin}m`;
const diffHours = Math.floor(diffMin / 60);
if (diffHours < 24 && dayDistance(ts, now) === 0) return `${diffHours}h`;
const word = dayWord(ts, now);
if (word && word !== 'Today') return word;
// Same year: drop the year from the user's format for brevity.
const sameYear = dayjs(ts).year() === dayjs(now).year();
const fmt = dateFormat(prefs);
return dayjs(ts).format(sameYear ? fmt.replace(/[-/.\s]*Y{2,4}[-/.\s]*/, ' ').trim() : fmt);
}
/** Conversational age for list rows: "just now", "5m ago", "3h ago", "yesterday", "3d ago", then the date. */
export function formatRelativeAge(
ts: number,
prefs: TimestampPrefs,
now: number = Date.now(),
): string {
const diffMin = Math.floor((now - ts) / 60_000);
if (diffMin < 1) return 'just now';
if (diffMin < 60) return `${diffMin}m ago`;
const diffHours = Math.floor(diffMin / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const days = dayDistance(ts, now);
if (days === 1) return 'yesterday';
if (days < 7) return `${days}d ago`;
return formatDate(ts, prefs);
}
/** Timeline day divider: "Today", "Yesterday", "Monday" (last 6 days), else the date. */
export function formatDayDivider(
ts: number,
prefs: TimestampPrefs,
now: number = Date.now(),
): string {
const d = dayDistance(ts, now);
if (d > 1 && d < 7) return dayjs(ts).format('dddd');
return formatTimestamp(ts, prefs, 'autoDate', now);
}