116 lines
3.5 KiB
TypeScript
116 lines
3.5 KiB
TypeScript
import type { PresenceSample } from "#lib/api/types.js";
|
|
import { formatDay, formatTime } from "#lib/format/datetime.js";
|
|
import { locale, t } from "#lib/i18n/locale.svelte.js";
|
|
import type { MessageKey } from "#lib/i18n/messages.js";
|
|
|
|
const SECONDS_IN_MINUTE = 60;
|
|
const MINUTES_IN_HOUR = 60;
|
|
const SECONDS_IN_HOUR = 3600;
|
|
const MS_IN_MINUTE = 60_000;
|
|
const SECONDS_IN_DAY = 86_400;
|
|
const MS_IN_DAY = 86_400_000;
|
|
const DAYS_IN_WEEK = 7;
|
|
const MONDAY = Date.UTC(2024, 0, 1);
|
|
|
|
const FUZZY: Record<string, MessageKey> = {
|
|
last_month: "presence.lastMonth",
|
|
last_week: "presence.lastWeek",
|
|
long_time_ago: "presence.longAgo",
|
|
recently: "presence.recently",
|
|
};
|
|
|
|
export function isOnline(
|
|
sample: PresenceSample | null,
|
|
now = Date.now()
|
|
): boolean {
|
|
if (sample?.status !== "online" || sample.next_offline_date === null) {
|
|
return false;
|
|
}
|
|
return new Date(sample.next_offline_date).getTime() > now;
|
|
}
|
|
|
|
function lastSeen(iso: string, now: number): string {
|
|
const date = new Date(iso);
|
|
const minutes = Math.floor((now - date.getTime()) / MS_IN_MINUTE);
|
|
if (minutes < 1) {
|
|
return t("presence.justNow");
|
|
}
|
|
if (minutes < MINUTES_IN_HOUR) {
|
|
return t("presence.ago", { count: minutes });
|
|
}
|
|
const time = formatTime(iso);
|
|
const today = new Date(now);
|
|
if (date.toDateString() === today.toDateString()) {
|
|
return t("presence.todayAt", { time });
|
|
}
|
|
if (date.toDateString() === new Date(now - MS_IN_DAY).toDateString()) {
|
|
return t("presence.yesterdayAt", { time });
|
|
}
|
|
return t("presence.on", { day: formatDay(iso), time });
|
|
}
|
|
|
|
export function formatPresence(
|
|
sample: PresenceSample,
|
|
now = Date.now()
|
|
): string {
|
|
if (sample.status === "online") {
|
|
return isOnline(sample, now)
|
|
? t("presence.online")
|
|
: lastSeen(sample.next_offline_date ?? sample.ts, now);
|
|
}
|
|
const fuzzy = FUZZY[sample.status];
|
|
if (fuzzy) {
|
|
return t(fuzzy);
|
|
}
|
|
return sample.last_online_date
|
|
? lastSeen(sample.last_online_date, now)
|
|
: t("presence.offline");
|
|
}
|
|
|
|
export function formatSpan(seconds: number): string {
|
|
if (seconds < SECONDS_IN_MINUTE) {
|
|
return t("presence.seconds", { seconds: Math.round(seconds) });
|
|
}
|
|
const minutes = Math.round(seconds / SECONDS_IN_MINUTE);
|
|
if (minutes < MINUTES_IN_HOUR) {
|
|
return t("presence.minutes", { minutes });
|
|
}
|
|
const hours = Math.floor(minutes / MINUTES_IN_HOUR);
|
|
const rest = minutes % MINUTES_IN_HOUR;
|
|
return rest === 0
|
|
? t("presence.hours", { hours })
|
|
: t("presence.hoursMinutes", { hours, minutes: rest });
|
|
}
|
|
|
|
export function formatClock(secondsOfDay: number): string {
|
|
const hours = Math.floor(secondsOfDay / SECONDS_IN_HOUR);
|
|
const minutes = Math.floor(
|
|
(secondsOfDay % SECONDS_IN_HOUR) / SECONDS_IN_MINUTE
|
|
);
|
|
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
|
|
}
|
|
|
|
export function formatLongSpan(seconds: number): string {
|
|
if (seconds < 2 * SECONDS_IN_DAY) {
|
|
return formatSpan(seconds);
|
|
}
|
|
const days = Math.floor(seconds / SECONDS_IN_DAY);
|
|
const hours = Math.round((seconds % SECONDS_IN_DAY) / SECONDS_IN_HOUR);
|
|
const text = t("presence.days", { count: days });
|
|
return hours === 0 ? text : `${text} ${t("presence.hours", { hours })}`;
|
|
}
|
|
|
|
function capitalize(text: string): string {
|
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
}
|
|
|
|
export function weekdayLabels(): string[] {
|
|
const format = new Intl.DateTimeFormat(locale.tag, {
|
|
timeZone: "UTC",
|
|
weekday: "short",
|
|
});
|
|
return Array.from({ length: DAYS_IN_WEEK }, (_, day) =>
|
|
capitalize(format.format(MONDAY + day * MS_IN_DAY))
|
|
);
|
|
}
|