Files
beavergram/frontend/src/lib/format/presence.ts
T

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))
);
}