2026-06-16 05:32:19 +02:00
|
|
|
// retoor <retoor@molodetz.nl>
|
|
|
|
|
|
|
|
|
|
const REFRESH_MS = 60000;
|
|
|
|
|
|
|
|
|
|
export class LocalTime {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.localize(document);
|
|
|
|
|
this.observer = new MutationObserver((mutations) => {
|
|
|
|
|
for (const mutation of mutations) {
|
|
|
|
|
for (const node of mutation.addedNodes) {
|
|
|
|
|
if (node.nodeType === 1) this.localize(node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (document.body) {
|
|
|
|
|
this.observer.observe(document.body, { childList: true, subtree: true });
|
|
|
|
|
}
|
|
|
|
|
window.setInterval(() => this.refreshRelative(), REFRESH_MS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
format(iso, mode) {
|
|
|
|
|
const date = new Date(iso);
|
|
|
|
|
if (isNaN(date.getTime())) return null;
|
|
|
|
|
if (mode === "ago") return this.relative(date);
|
|
|
|
|
if (mode === "date") return date.toLocaleDateString();
|
|
|
|
|
if (mode === "time") {
|
|
|
|
|
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
|
|
|
}
|
|
|
|
|
return date.toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
relative(date) {
|
|
|
|
|
const seconds = (Date.now() - date.getTime()) / 1000;
|
|
|
|
|
if (seconds < 0) return "just now";
|
|
|
|
|
if (seconds < 60) return "just now";
|
|
|
|
|
const minutes = Math.floor(seconds / 60);
|
|
|
|
|
if (minutes < 60) return minutes + "m ago";
|
|
|
|
|
const hours = Math.floor(minutes / 60);
|
|
|
|
|
if (hours < 24) return hours + "h ago";
|
|
|
|
|
const days = Math.floor(hours / 24);
|
|
|
|
|
if (days <= 30) return days + "d ago";
|
|
|
|
|
return date.toLocaleDateString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
apply(el) {
|
|
|
|
|
const iso = el.getAttribute("datetime");
|
|
|
|
|
if (!iso) return;
|
|
|
|
|
const mode = el.dataset.dtMode || "datetime";
|
|
|
|
|
const text = this.format(iso, mode);
|
|
|
|
|
if (text === null) return;
|
|
|
|
|
el.textContent = text;
|
|
|
|
|
const full = new Date(iso);
|
|
|
|
|
if (!isNaN(full.getTime())) {
|
|
|
|
|
el.title = full.toLocaleString([], { dateStyle: "full", timeStyle: "long" });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
localize(root) {
|
|
|
|
|
if (root.matches && root.matches("[data-dt]")) this.apply(root);
|
|
|
|
|
if (!root.querySelectorAll) return;
|
|
|
|
|
root.querySelectorAll("[data-dt]").forEach((el) => this.apply(el));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refreshRelative() {
|
feat: add author-username search to feed/gists/projects listings and partial-index migration
Extend `database.text_search_clause` with an `author_field` parameter that resolves username matches to user UIDs, enabling author-username search across the three public listings (`/feed`, `/gists`, `/projects`). Update the corresponding API docs and README route descriptions to reflect the new search scope. Add six partial indexes (`idx_comments_target_live`, `idx_votes_target_live`, `idx_reactions_target_live`, `idx_gists_live_created`, `idx_projects_live_created`, `idx_attachments_user_created_live`) to optimize filtered queries on non-deleted rows. Introduce a hot-settings cache (`_hot_settings`) with a 2-second TTL for `maintenance_mode`, `rate_limit_per_minute`, and `rate_limit_window_seconds`, plus a periodic rate-limit store sweep (`_sweep_rate_limit_store`) to evict stale IP entries every 60 seconds. Add `GZipMiddleware` to the FastAPI app for response compression.
2026-06-20 00:24:51 +02:00
|
|
|
if (document.hidden) return;
|
|
|
|
|
const relative = document.querySelectorAll('[data-dt][data-dt-mode="ago"]');
|
|
|
|
|
if (!relative.length) return;
|
|
|
|
|
relative.forEach((el) => this.apply(el));
|
2026-06-16 05:32:19 +02:00
|
|
|
}
|
|
|
|
|
}
|