2024-08-13 10:26:23 +03:00
|
|
|
import { i18n } from '@lingui/core';
|
|
|
|
import { t, Trans } from '@lingui/macro';
|
2023-01-05 05:50:27 +03:00
|
|
|
import dayjs from 'dayjs';
|
2024-03-30 12:21:31 +03:00
|
|
|
import { useEffect, useMemo, useReducer } from 'preact/hooks';
|
2023-01-05 05:50:27 +03:00
|
|
|
|
2024-08-13 10:26:23 +03:00
|
|
|
import localeMatch from '../utils/locale-match';
|
|
|
|
import mem from '../utils/mem';
|
2023-01-05 05:50:27 +03:00
|
|
|
|
2024-08-13 10:26:23 +03:00
|
|
|
const resolvedLocale = new Intl.DateTimeFormat().resolvedOptions().locale;
|
|
|
|
const DTF = mem((locale, opts = {}) => {
|
|
|
|
const lang = localeMatch([locale], [resolvedLocale]);
|
|
|
|
try {
|
|
|
|
return new Intl.DateTimeFormat(lang, opts);
|
|
|
|
} catch (e) {}
|
|
|
|
try {
|
|
|
|
return new Intl.DateTimeFormat(locale, opts);
|
|
|
|
} catch (e) {}
|
|
|
|
return new Intl.DateTimeFormat(undefined, opts);
|
|
|
|
});
|
|
|
|
const RTF = mem((locale) => new Intl.RelativeTimeFormat(locale || undefined));
|
|
|
|
|
|
|
|
const minute = 60;
|
|
|
|
const hour = 60 * minute;
|
|
|
|
const day = 24 * hour;
|
|
|
|
|
|
|
|
const rtfFromNow = (date) => {
|
|
|
|
// date = Date object
|
|
|
|
const rtf = RTF(i18n.locale);
|
|
|
|
const seconds = (date.getTime() - Date.now()) / 1000;
|
|
|
|
const absSeconds = Math.abs(seconds);
|
|
|
|
if (absSeconds < minute) {
|
|
|
|
return rtf.format(seconds, 'second');
|
|
|
|
} else if (absSeconds < hour) {
|
|
|
|
return rtf.format(Math.floor(seconds / minute), 'minute');
|
|
|
|
} else if (absSeconds < day) {
|
|
|
|
return rtf.format(Math.floor(seconds / hour), 'hour');
|
|
|
|
} else {
|
|
|
|
return rtf.format(Math.floor(seconds / day), 'day');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const twitterFromNow = (date) => {
|
|
|
|
// date = Date object
|
|
|
|
const seconds = (Date.now() - date.getTime()) / 1000;
|
|
|
|
if (seconds < minute) {
|
|
|
|
return t({
|
|
|
|
comment: 'Relative time in seconds, as short as possible',
|
|
|
|
message: `${seconds < 1 ? 1 : Math.floor(seconds)}s`,
|
|
|
|
});
|
|
|
|
} else if (seconds < hour) {
|
|
|
|
return t({
|
|
|
|
comment: 'Relative time in minutes, as short as possible',
|
|
|
|
message: `${Math.floor(seconds / minute)}m`,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return t({
|
|
|
|
comment: 'Relative time in hours, as short as possible',
|
|
|
|
message: `${Math.floor(seconds / hour)}h`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
2023-01-05 05:50:27 +03:00
|
|
|
|
|
|
|
export default function RelativeTime({ datetime, format }) {
|
|
|
|
if (!datetime) return null;
|
2024-03-30 12:21:31 +03:00
|
|
|
const [renderCount, rerender] = useReducer((x) => x + 1, 0);
|
2024-01-25 16:28:41 +03:00
|
|
|
const date = useMemo(() => dayjs(datetime), [datetime]);
|
2024-03-30 12:21:31 +03:00
|
|
|
const [dateStr, dt, title] = useMemo(() => {
|
2024-04-18 18:11:18 +03:00
|
|
|
if (!date.isValid()) return ['' + datetime, '', ''];
|
2024-03-30 12:21:31 +03:00
|
|
|
let str;
|
2024-01-25 16:28:41 +03:00
|
|
|
if (format === 'micro') {
|
|
|
|
// If date <= 1 day ago or day is within this year
|
|
|
|
const now = dayjs();
|
|
|
|
const dayDiff = now.diff(date, 'day');
|
2024-08-13 10:26:23 +03:00
|
|
|
if (dayDiff <= 1) {
|
|
|
|
str = twitterFromNow(date.toDate());
|
2024-01-25 16:28:41 +03:00
|
|
|
} else {
|
2024-08-13 10:26:23 +03:00
|
|
|
const currentYear = now.year();
|
|
|
|
const dateYear = date.year();
|
|
|
|
if (dateYear === currentYear) {
|
|
|
|
str = DTF(i18n.locale, {
|
|
|
|
year: undefined,
|
|
|
|
month: 'short',
|
|
|
|
day: 'numeric',
|
|
|
|
}).format(date.toDate());
|
|
|
|
} else {
|
|
|
|
str = DTF(i18n.locale, {
|
|
|
|
dateStyle: 'short',
|
|
|
|
}).format(date.toDate());
|
|
|
|
}
|
2024-01-25 16:28:41 +03:00
|
|
|
}
|
2023-12-24 17:49:23 +03:00
|
|
|
}
|
2024-08-13 10:26:23 +03:00
|
|
|
if (!str) str = rtfFromNow(date.toDate());
|
|
|
|
return [str, date.toISOString(), date.toDate().toLocaleString()];
|
2024-03-30 12:21:31 +03:00
|
|
|
}, [date, format, renderCount]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-04-18 18:11:18 +03:00
|
|
|
if (!date.isValid()) return;
|
2024-03-30 12:21:31 +03:00
|
|
|
let timeout;
|
|
|
|
let raf;
|
|
|
|
function rafRerender() {
|
|
|
|
raf = requestAnimationFrame(() => {
|
|
|
|
rerender();
|
|
|
|
scheduleRerender();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
function scheduleRerender() {
|
|
|
|
// If less than 1 minute, rerender every 10s
|
|
|
|
// If less than 1 hour rerender every 1m
|
|
|
|
// Else, don't need to rerender
|
|
|
|
if (date.diff(dayjs(), 'minute', true) < 1) {
|
|
|
|
timeout = setTimeout(rafRerender, 10_000);
|
|
|
|
} else if (date.diff(dayjs(), 'hour', true) < 1) {
|
|
|
|
timeout = setTimeout(rafRerender, 60_000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
scheduleRerender();
|
|
|
|
return () => {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
cancelAnimationFrame(raf);
|
|
|
|
};
|
|
|
|
}, []);
|
2023-01-05 05:50:27 +03:00
|
|
|
|
|
|
|
return (
|
2024-01-25 16:28:41 +03:00
|
|
|
<time datetime={dt} title={title}>
|
2023-01-05 05:50:27 +03:00
|
|
|
{dateStr}
|
|
|
|
</time>
|
|
|
|
);
|
|
|
|
}
|