Files
RustBox/src/web/js/20-preferences-auth.js
T
Mateusz Gruszczyński db0d230d3b
RustBox CI / rustbox (push) Canceled after 0s
v0.2.10
2026-11-02 09:37:35 +01:00

855 lines
53 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function resolveTheme(theme) {
if (theme !== "system") return theme;
return matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
}
const preferenceMap = {
theme: "theme", interfaceSkin: "interface_skin", presetScaleUnlocked: "preset_scale_unlocked", density: "density", preview: "preview_pane", scale: "ui_scale", language: "language",
colorPreset: "color_preset", accentColor: "accent_color", backgroundColor: "background_color", panelColor: "panel_color", textColor: "text_color", fontFamily: "font_family", fontSize: "font_size",
rowHeight: "row_height", folderPaneWidth: "folder_pane_width", messagePaneWidth: "message_pane_width",
borderRadius: "border_radius", contrast: "contrast", showAvatars: "show_avatars",
showAttachmentIcons: "show_attachment_icons", showFlags: "show_flags", reduceMotion: "reduce_motion", composeFormat: "compose_format",
composeAutosaveMs: "compose_autosave_ms", defaultPriority: "default_priority", requestMdnDefault: "request_mdn_default", attachmentReminder: "attachment_reminder", composeFontFamily: "compose_font_family", replyPosition: "reply_position", quoteStyle: "quote_style", forwardMode: "forward_mode", spellcheckEnabled: "spellcheck_enabled",
dateFormat: "date_format", timeFormat: "time_format", iconSize: "icon_size", toolbarDensity: "toolbar_density", sidebarItemHeight: "sidebar_item_height",
readerMaxWidth: "reader_max_width", composerWidth: "composer_width", selectionColor: "selection_color", unreadFontWeight: "unread_font_weight", unreadBadgeStyle: "unread_badge_style",
compactHeaders: "compact_headers", showBorders: "show_borders", scrollbarDensity: "scrollbar_density", monochrome: "monochrome", folderBackground: "folder_background", messageBackground: "message_background", readerBackground: "reader_background",
};
const preferenceTimers = new Map();
let presetPromotionTimer = null;
let presetPromotionInFlight = false;
let presetPromotionNeedsAnotherSave = false;
let presetPromotionActive = false;
const interfaceStyleProfiles = {
"roundcube-classic": {
label: "Roundcube Classic",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 12, rowHeight: 44, folderPaneWidth: 224, messagePaneWidth: 410, borderRadius: 0, contrast: "normal", iconSize: 16, toolbarDensity: "compact", sidebarItemHeight: 24, readerMaxWidth: 0, composerWidth: 900, selectionColor: "#cc3333", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: true, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
"roundcube-larry": {
label: "Roundcube Larry",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 48, folderPaneWidth: 230, messagePaneWidth: 420, borderRadius: 3, contrast: "normal", iconSize: 17, toolbarDensity: "normal", sidebarItemHeight: 28, readerMaxWidth: 0, composerWidth: 920, selectionColor: "#c7e3ef", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: true, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
"roundcube-elastic": {
label: "Roundcube Elastic",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 14, rowHeight: 52, folderPaneWidth: 238, messagePaneWidth: 440, borderRadius: 4, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 35, readerMaxWidth: 0, composerWidth: 940, selectionColor: "#ebf8ff", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
clean: {
label: "Clean",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 58, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 8, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#dce9ff", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
gmail: {
label: "Gmail inspired",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 248, messagePaneWidth: 440, borderRadius: 10, contrast: "soft", iconSize: 19, toolbarDensity: "normal", sidebarItemHeight: 34, readerMaxWidth: 980, composerWidth: 960, selectionColor: "#d3e3fd", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
proton: {
label: "Proton inspired",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 242, messagePaneWidth: 430, borderRadius: 9, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 33, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#e7e1ff", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
zoho: {
label: "Zoho inspired",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 52, folderPaneWidth: 236, messagePaneWidth: 420, borderRadius: 6, contrast: "normal", iconSize: 18, toolbarDensity: "compact", sidebarItemHeight: 30, readerMaxWidth: 0, composerWidth: 920, selectionColor: "#e1ebff", unreadFontWeight: 710, unreadBadgeStyle: "dot", compactHeaders: true, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
outlook: {
label: "Outlook inspired",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 50, folderPaneWidth: 232, messagePaneWidth: 420, borderRadius: 4, contrast: "normal", iconSize: 18, toolbarDensity: "compact", sidebarItemHeight: 30, readerMaxWidth: 0, composerWidth: 940, selectionColor: "#deecf9", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: true, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
minimal: {
label: "Minimal",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 14, rowHeight: 60, folderPaneWidth: 230, messagePaneWidth: 430, borderRadius: 12, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 34, readerMaxWidth: 860, composerWidth: 900, selectionColor: "#ececec", unreadFontWeight: 680, unreadBadgeStyle: "outline", compactHeaders: false, showBorders: false, scrollbarDensity: "hidden", monochrome: true },
},
graphite: {
label: "Graphite",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 50, folderPaneWidth: 220, messagePaneWidth: 400, borderRadius: 2, contrast: "normal", iconSize: 17, toolbarDensity: "compact", sidebarItemHeight: 27, readerMaxWidth: 0, composerWidth: 880, selectionColor: "#32373c", unreadFontWeight: 720, unreadBadgeStyle: "solid", compactHeaders: true, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
fastmail: {
label: "Fastmail inspired",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 240, messagePaneWidth: 430, borderRadius: 7, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#d9e8ff", unreadFontWeight: 710, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
"apple-mail": {
label: "Apple Mail inspired",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 54, folderPaneWidth: 238, messagePaneWidth: 430, borderRadius: 9, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 920, composerWidth: 920, selectionColor: "#dcebff", unreadFontWeight: 700, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
yahoo: {
label: "Yahoo Mail inspired",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 244, messagePaneWidth: 435, borderRadius: 10, contrast: "normal", iconSize: 19, toolbarDensity: "normal", sidebarItemHeight: 33, readerMaxWidth: 960, composerWidth: 950, selectionColor: "#ede3ff", unreadFontWeight: 720, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
hey: {
label: "HEY inspired",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "humanist", fontSize: 14, rowHeight: 62, folderPaneWidth: 250, messagePaneWidth: 450, borderRadius: 12, contrast: "normal", iconSize: 19, toolbarDensity: "spacious", sidebarItemHeight: 36, readerMaxWidth: 900, composerWidth: 930, selectionColor: "#fff0a8", unreadFontWeight: 760, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
nord: {
label: "Nord",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 52, folderPaneWidth: 232, messagePaneWidth: 420, borderRadius: 6, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 30, readerMaxWidth: 940, composerWidth: 920, selectionColor: "#d8dee9", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
"high-contrast": {
label: "High contrast",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 14, rowHeight: 60, folderPaneWidth: 240, messagePaneWidth: 440, borderRadius: 2, contrast: "high", iconSize: 20, toolbarDensity: "normal", sidebarItemHeight: 34, readerMaxWidth: 980, composerWidth: 960, selectionColor: "#ffe500", unreadFontWeight: 800, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: true, scrollbarDensity: "normal", monochrome: false },
},
github: {
label: "GitHub inspired",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 52, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 6, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 30, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#ddf4ff", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
gitlab: {
label: "GitLab inspired",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 52, folderPaneWidth: 238, messagePaneWidth: 430, borderRadius: 7, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 30, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#eee7ff", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
discord: {
label: "Discord inspired",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 246, messagePaneWidth: 440, borderRadius: 8, contrast: "normal", iconSize: 19, toolbarDensity: "normal", sidebarItemHeight: 34, readerMaxWidth: 980, composerWidth: 950, selectionColor: "#e0e3ff", unreadFontWeight: 720, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
slack: {
label: "Slack inspired",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 56, folderPaneWidth: 244, messagePaneWidth: 435, borderRadius: 8, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 33, readerMaxWidth: 960, composerWidth: 950, selectionColor: "#f3e6f5", unreadFontWeight: 720, unreadBadgeStyle: "dot", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
reddit: {
label: "Reddit inspired",
preferences: { density: "compact", preview: "right", scale: "normal", fontFamily: "system", fontSize: 13, rowHeight: 52, folderPaneWidth: 236, messagePaneWidth: 430, borderRadius: 9, contrast: "normal", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 31, readerMaxWidth: 980, composerWidth: 940, selectionColor: "#fff0e8", unreadFontWeight: 720, unreadBadgeStyle: "solid", compactHeaders: false, showBorders: true, scrollbarDensity: "thin", monochrome: false },
},
notion: {
label: "Notion inspired",
preferences: { density: "comfortable", preview: "right", scale: "normal", fontFamily: "system", fontSize: 14, rowHeight: 58, folderPaneWidth: 238, messagePaneWidth: 430, borderRadius: 5, contrast: "soft", iconSize: 18, toolbarDensity: "normal", sidebarItemHeight: 32, readerMaxWidth: 900, composerWidth: 920, selectionColor: "#efefef", unreadFontWeight: 700, unreadBadgeStyle: "outline", compactHeaders: false, showBorders: false, scrollbarDensity: "thin", monochrome: false },
},
};
function resolvedInterfaceSkin(prefs = state.prefs) {
return prefs.interfaceSkin === "default"
? (root.dataset.defaultInterfaceSkin || "rustbox")
: (prefs.interfaceSkin || root.dataset.defaultInterfaceSkin || "rustbox");
}
function activeInterfaceStyle(prefs = state.prefs) {
return interfaceStyleProfiles[resolvedInterfaceSkin(prefs)] || null;
}
const localizedTextNodes = new Map();
const localizedAttributes = new Map();
function translateScopedUiText(locale) {
for (const [node, original] of localizedTextNodes.entries()) {
if (node.isConnected) node.nodeValue = original;
else localizedTextNodes.delete(node);
}
for (const [node, originals] of localizedAttributes.entries()) {
if (!node.isConnected) { localizedAttributes.delete(node); continue; }
for (const [attribute, original] of Object.entries(originals)) node.setAttribute(attribute, original);
}
if (locale !== "pl") return;
const map = state.translations._text || {};
if (!map || typeof map !== "object") return;
$$('[data-i18n-scope]').forEach((scope) => {
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
const parent = node.parentElement;
if (!parent || parent.closest("script,style,pre,code,[data-i18n-skip]")) continue;
if (!localizedTextNodes.has(node)) localizedTextNodes.set(node, node.nodeValue);
const original = localizedTextNodes.get(node) || "";
const normalized = original.trim().replace(/\s+/g, " ");
const translated = map[normalized];
if (!translated) continue;
const leading = original.match(/^\s*/)?.[0] || "";
const trailing = original.match(/\s*$/)?.[0] || "";
node.nodeValue = `${leading}${translated}${trailing}`;
}
const nodes = [scope, ...scope.querySelectorAll("[title],[aria-label],[placeholder],[data-placeholder]")];
for (const node of nodes) {
const originals = localizedAttributes.get(node) || {};
for (const attribute of ["title", "aria-label", "placeholder", "data-placeholder"]) {
if (!node.hasAttribute(attribute)) continue;
if (!(attribute in originals)) originals[attribute] = node.getAttribute(attribute);
const translated = map[originals[attribute]];
if (translated) node.setAttribute(attribute, translated);
}
localizedAttributes.set(node, originals);
}
});
}
async function loadTranslations(language) {
const locale = ["en", "pl"].includes(language) ? language : "en";
try {
const response = await fetch(`${BASE}/assets/i18n/${locale}.json`, { credentials: "same-origin" });
state.translations = response.ok ? await response.json() : {};
} catch (_) { state.translations = {}; }
root.lang = locale;
$$('[data-i18n]').forEach((node) => { const key = node.dataset.i18n; if (state.translations[key]) node.textContent = state.translations[key]; });
$$('[data-i18n-placeholder]').forEach((node) => { const key = node.dataset.i18nPlaceholder; if (state.translations[key]) node.placeholder = state.translations[key]; });
$$('[data-i18n-title]').forEach((node) => { const key = node.dataset.i18nTitle; if (state.translations[key]) node.title = state.translations[key]; });
$$('[data-i18n-aria-label]').forEach((node) => { const key = node.dataset.i18nAriaLabel; if (state.translations[key]) node.setAttribute('aria-label', state.translations[key]); });
translateScopedUiText(locale);
}
function tr(key, fallback = key) {
return state.translations[key] || fallback;
}
function trText(text) {
return state.translations?._text?.[text] || text;
}
function applyPreferences() {
const p = state.prefs;
const fixedStyle = activeInterfaceStyle(p);
const visual = fixedStyle
? { ...p, ...fixedStyle.preferences }
: p;
root.dataset.theme = resolveTheme(p.theme);
root.dataset.interfaceSkin = resolvedInterfaceSkin(p);
root.dataset.density = visual.density;
root.dataset.preview = visual.preview;
root.dataset.scale = visual.scale;
root.dataset.colorPreset = p.colorPreset;
root.dataset.fontFamily = visual.fontFamily;
root.dataset.contrast = visual.contrast;
root.dataset.hideAvatars = String(!p.showAvatars);
root.dataset.hideAttachments = String(!p.showAttachmentIcons);
root.dataset.hideFlags = String(!p.showFlags);
root.dataset.reduceMotion = String(p.reduceMotion);
root.dataset.toolbarDensity = visual.toolbarDensity;
root.dataset.unreadBadge = visual.unreadBadgeStyle;
root.dataset.compactHeaders = String(visual.compactHeaders);
root.dataset.showBorders = String(visual.showBorders);
root.dataset.scrollbarDensity = visual.scrollbarDensity;
root.dataset.monochrome = String(visual.monochrome);
root.style.setProperty("--user-accent", p.accentColor);
root.style.setProperty("--user-bg", p.backgroundColor);
root.style.setProperty("--user-panel", p.panelColor);
root.style.setProperty("--user-text", p.textColor);
const scaleFactor = ({ small: 0.90, normal: 1, large: 1.12 })[visual.scale] || 1;
const densityFactor = visual.density === "comfortable" ? 1.13 : 0.93;
root.style.setProperty("--ui-scale-factor", String(scaleFactor));
root.style.setProperty("--density-factor", String(densityFactor));
root.style.setProperty("--ui-size", `${Math.round(Number(visual.fontSize || 13) * scaleFactor * 100) / 100}px`);
root.style.setProperty("--row-height", `${Math.max(42, Math.round(Number(visual.rowHeight || 58) * scaleFactor))}px`);
root.style.setProperty("--folder-pane-width", `${Math.max(170, Math.round(Number(visual.folderPaneWidth || 228) * scaleFactor))}px`);
root.style.setProperty("--message-pane-width", `${Math.max(300, Math.round(Number(visual.messagePaneWidth || 420) * scaleFactor))}px`);
root.style.setProperty("--control-height", `${Math.max(26, Math.round(30 * scaleFactor * densityFactor))}px`);
root.style.setProperty("--appbar-height", `${Math.max(38, Math.round(44 * scaleFactor * densityFactor))}px`);
root.style.setProperty("--dialog-header-height", `${Math.max(38, Math.round(43 * scaleFactor * densityFactor))}px`);
root.style.setProperty("--icon-size", `${Math.max(14, Math.round(Number(visual.iconSize || 18) * scaleFactor))}px`);
const toolbarFactor = ({ compact: 0.88, normal: 1, spacious: 1.14 })[visual.toolbarDensity] || 1;
root.style.setProperty("--toolbar-density-factor", String(toolbarFactor));
root.style.setProperty("--sidebar-item-height", `${Math.max(24, Math.round(Number(visual.sidebarItemHeight || 29) * scaleFactor))}px`);
root.style.setProperty("--reader-max-width", Number(visual.readerMaxWidth) > 0 ? `${Number(visual.readerMaxWidth)}px` : "none");
root.style.setProperty("--composer-width", `${Math.max(640, Number(visual.composerWidth || 900))}px`);
root.style.setProperty("--compose-font-family", composeFontCss(p.composeFontFamily));
const lockedRoundcubeSkin = Boolean(fixedStyle);
if (lockedRoundcubeSkin) {
["--selected", "--folder-bg", "--message-bg", "--reader-bg"].forEach((name) => root.style.removeProperty(name));
} else {
root.style.setProperty("--selected", visual.selectionColor || "#28352e");
root.style.setProperty("--folder-bg", p.folderBackground || "var(--sidebar)");
root.style.setProperty("--message-bg", p.messageBackground || "var(--panel)");
root.style.setProperty("--reader-bg", p.readerBackground || "var(--panel)");
}
root.style.setProperty("--unread-font-weight", String(Number(visual.unreadFontWeight || 730)));
root.style.setProperty("--button-pad-x", `${Math.max(8, Math.round(10 * scaleFactor))}px`);
root.style.setProperty("--button-pad-y", `${Math.max(3, Math.round(5 * scaleFactor * densityFactor))}px`);
root.style.setProperty("--field-pad-x", `${Math.max(7, Math.round(8 * scaleFactor))}px`);
root.style.setProperty("--field-pad-y", `${Math.max(3, Math.round(5 * scaleFactor * densityFactor))}px`);
root.style.setProperty("--radius-control", `${visual.borderRadius}px`);
root.style.setProperty("--radius-panel", `${Math.min(visual.borderRadius + 1, 13)}px`);
const presetCss = fixedStyle ? getComputedStyle(root) : null;
const presetColor = (name, fallback) => presetCss ? presetCssHex(presetCss, name, fallback) : fallback;
const displayedAccent = presetColor("--accent", p.accentColor);
const displayedBackground = presetColor("--bg", p.backgroundColor);
const displayedPanel = presetColor("--panel", p.panelColor);
const displayedText = presetColor("--text", p.textColor);
const displayedFolderBackground = presetColor("--folder-bg", p.folderBackground || "#101310");
const displayedMessageBackground = presetColor("--message-bg", p.messageBackground || "#181c19");
const displayedReaderBackground = presetColor("--reader-bg", p.readerBackground || "#181c19");
const values = {
"#pref-theme": p.theme, "#pref-interface-skin": p.interfaceSkin, "#pref-density": visual.density, "#pref-preview": visual.preview, "#pref-scale": visual.scale,
"#pref-language": p.language, "#pref-color-preset": fixedStyle ? "custom" : p.colorPreset, "#pref-accent": displayedAccent,
"#pref-background": displayedBackground, "#pref-panel": displayedPanel, "#pref-text-color": displayedText,
"#pref-font-family": visual.fontFamily, "#pref-font-size": visual.fontSize, "#pref-row-height": visual.rowHeight,
"#pref-folder-width": visual.folderPaneWidth, "#pref-message-width": visual.messagePaneWidth, "#pref-radius": visual.borderRadius,
"#pref-contrast": visual.contrast, "#pref-date-format": p.dateFormat, "#pref-time-format": p.timeFormat,
"#compose-pref-format": p.composeFormat, "#compose-pref-autosave": String(p.composeAutosaveMs), "#compose-pref-priority": p.defaultPriority, "#compose-pref-font": p.composeFontFamily, "#compose-pref-reply-position": p.replyPosition, "#compose-pref-quote-style": p.quoteStyle, "#compose-pref-forward-mode": p.forwardMode,
"#pref-icon-size": visual.iconSize, "#pref-toolbar-density": visual.toolbarDensity, "#pref-sidebar-height": visual.sidebarItemHeight, "#pref-reader-width": String(visual.readerMaxWidth), "#pref-composer-width": visual.composerWidth,
"#pref-selection-color": visual.selectionColor, "#pref-unread-weight": visual.unreadFontWeight, "#pref-unread-badge": visual.unreadBadgeStyle, "#pref-scrollbar-density": visual.scrollbarDensity,
};
Object.entries(values).forEach(([selector, value]) => { const node = $(selector); if (node) node.value = value; });
const checks = { "#pref-preset-scale-unlocked": p.presetScaleUnlocked, "#pref-avatars": p.showAvatars, "#pref-attachment-icons": p.showAttachmentIcons, "#pref-flags": p.showFlags, "#pref-reduce-motion": p.reduceMotion, "#compose-pref-mdn": p.requestMdnDefault, "#compose-pref-attachment-reminder": p.attachmentReminder, "#compose-pref-spellcheck": p.spellcheckEnabled, "#pref-compact-headers": visual.compactHeaders, "#pref-show-borders": visual.showBorders, "#pref-monochrome": visual.monochrome, "#pref-folder-bg-enabled": Boolean(p.folderBackground), "#pref-message-bg-enabled": Boolean(p.messageBackground), "#pref-reader-bg-enabled": Boolean(p.readerBackground) };
Object.entries(checks).forEach(([selector, value]) => { const node = $(selector); if (node) node.checked = Boolean(value); });
const outputs = { "#pref-accent-value": displayedAccent, "#pref-background-value": displayedBackground, "#pref-panel-value": displayedPanel, "#pref-text-color-value": displayedText, "#pref-font-size-value": `${visual.fontSize} px`, "#pref-row-height-value": `${visual.rowHeight} px`, "#pref-folder-width-value": `${visual.folderPaneWidth} px`, "#pref-message-width-value": `${visual.messagePaneWidth} px`, "#pref-radius-value": `${visual.borderRadius} px`, "#pref-icon-size-value": `${visual.iconSize} px`, "#pref-sidebar-height-value": `${visual.sidebarItemHeight} px`, "#pref-composer-width-value": `${visual.composerWidth} px`, "#pref-selection-color-value": visual.selectionColor, "#pref-unread-weight-value": String(visual.unreadFontWeight) };
Object.entries(outputs).forEach(([selector, value]) => { const node = $(selector); if (node) node.textContent = value; });
[["#pref-folder-bg", displayedFolderBackground], ["#pref-message-bg", displayedMessageBackground], ["#pref-reader-bg", displayedReaderBackground]].forEach(([selector, value]) => { const node = $(selector); if (node) node.value = value; });
const customizationLocked = Boolean(fixedStyle);
[
"#pref-color-preset", "#pref-accent", "#pref-background", "#pref-panel", "#pref-text-color",
"#pref-density", "#pref-preview", "#pref-font-family", "#pref-font-size",
"#pref-row-height", "#pref-folder-width", "#pref-message-width", "#pref-radius", "#pref-contrast",
"#pref-icon-size", "#pref-toolbar-density", "#pref-sidebar-height", "#pref-reader-width", "#pref-composer-width",
"#pref-selection-color", "#pref-unread-weight", "#pref-unread-badge", "#pref-scrollbar-density",
"#pref-compact-headers", "#pref-show-borders", "#pref-monochrome",
"#pref-folder-bg-enabled", "#pref-folder-bg", "#pref-message-bg-enabled", "#pref-message-bg", "#pref-reader-bg-enabled", "#pref-reader-bg",
].forEach((selector) => { const node = $(selector); if (node) node.disabled = customizationLocked && !p.presetScaleUnlocked; });
const scaleControl = $("#pref-scale");
if (scaleControl) scaleControl.disabled = customizationLocked && !p.presetScaleUnlocked;
const scaleUnlock = $("#pref-preset-scale-unlocked");
if (scaleUnlock) scaleUnlock.disabled = !fixedStyle;
const scaleUnlockRow = $("#pref-preset-scale-unlocked-row");
if (scaleUnlockRow) scaleUnlockRow.classList.toggle("hidden", !fixedStyle);
const skinNote = $("#pref-interface-skin-note");
if (skinNote) {
const styleLabel = state.prefs.language === "pl" ? (state.translations._text?.[fixedStyle?.label] || fixedStyle?.label) : fixedStyle?.label;
skinNote.textContent = fixedStyle
? `${styleLabel} — ${tr("fixed_interface_style_note", "predefined interface style. Enable preset editing below to create a customizable copy on the first change.")}`
: tr("custom_interface_style_note", "RustBox is fully customizable: colors, spacing, panes, typography and advanced interface controls are available below.");
}
}
function normalizeServerPreferences(data) {
if (!data) return;
state.prefs = {
...state.prefs,
theme: data.theme || root.dataset.defaultTheme || "system", interfaceSkin: data.interface_skin || "default", presetScaleUnlocked: Boolean(data.preset_scale_unlocked), density: data.density || "compact", preview: data.preview_pane || "right", composeFormat: data.compose_format || "html",
composeAutosaveMs: Number(data.compose_autosave_ms || 3000), defaultPriority: data.default_priority || "normal", requestMdnDefault: Boolean(data.request_mdn_default), attachmentReminder: data.attachment_reminder !== false, composeFontFamily: data.compose_font_family || "system", replyPosition: data.reply_position || "above", quoteStyle: data.quote_style || "standard", forwardMode: data.forward_mode || "inline", spellcheckEnabled: data.spellcheck_enabled !== false,
scale: data.ui_scale || "normal", language: data.language || "en", colorPreset: data.color_preset || "sage",
accentColor: data.accent_color || "#6f9c86", backgroundColor: data.background_color || "#121513", panelColor: data.panel_color || "#181c19", textColor: data.text_color || "#edf0ec", fontFamily: data.font_family || "system", fontSize: data.font_size || 13,
rowHeight: data.row_height || 58, folderPaneWidth: data.folder_pane_width || 228, messagePaneWidth: data.message_pane_width || 420,
borderRadius: data.border_radius ?? 4, contrast: data.contrast || "normal", showAvatars: data.show_avatars !== false,
showAttachmentIcons: data.show_attachment_icons !== false, showFlags: data.show_flags !== false, reduceMotion: Boolean(data.reduce_motion), dateFormat: data.date_format || "locale", timeFormat: data.time_format || "locale",
iconSize: Number(data.icon_size || 18), toolbarDensity: data.toolbar_density || "normal", sidebarItemHeight: Number(data.sidebar_item_height || 29), readerMaxWidth: Number(data.reader_max_width || 0), composerWidth: Number(data.composer_width || 900), selectionColor: data.selection_color || "#28352e", unreadFontWeight: Number(data.unread_font_weight || 730), unreadBadgeStyle: data.unread_badge_style || "dot", compactHeaders: Boolean(data.compact_headers), showBorders: data.show_borders !== false, scrollbarDensity: data.scrollbar_density || "thin", monochrome: Boolean(data.monochrome), folderBackground: data.folder_background || "", messageBackground: data.message_background || "", readerBackground: data.reader_background || "",
};
}
async function loadPreferences() {
try { normalizeServerPreferences(await request("/settings/display", { method: "GET" })); }
catch (error) { console.warn("Could not load display preferences", error); }
applyPreferences();
await loadTranslations(state.prefs.language);
}
async function loadMailPreferences() {
try {
const prefs = await request("/settings/mailbox", { method: "GET" });
state.mailPrefs = { ...state.mailPrefs, ...prefs };
state.perPage = Number(state.mailPrefs.messages_per_page || 300);
const values = {
"#mail-pref-per-page": String(state.perPage),
"#mail-pref-read-delay": String(state.mailPrefs.mark_read_delay_ms || 0),
"#mail-pref-remote-images": state.mailPrefs.remote_images || "ask",
"#mail-pref-preferred-body": state.mailPrefs.preferred_body || "auto",
"#mail-pref-thread-mode": state.mailPrefs.thread_mode || "threaded",
"#mail-pref-sort": state.mailPrefs.default_sort || "date_desc",
};
Object.entries(values).forEach(([selector, value]) => { const node = $(selector); if (node) node.value = value; });
const messageSort = $("#message-sort");
if (messageSort) messageSort.value = state.mailPrefs.default_sort || "date_desc";
const collapse = $("#mail-pref-collapse-quotes");
if (collapse) collapse.checked = state.mailPrefs.collapse_quotes !== false;
const snippets = $("#mail-pref-show-snippets");
if (snippets) snippets.checked = state.mailPrefs.show_snippets !== false;
const flaggedTop = $("#mail-pref-flagged-top");
if (flaggedTop) flaggedTop.checked = Boolean(state.mailPrefs.flagged_on_top);
const syncKeywords = $("#mail-pref-sync-keywords");
if (syncKeywords) syncKeywords.checked = Boolean(state.mailPrefs.sync_imap_keywords);
} catch (error) { console.warn("Could not load mailbox preferences", error); }
}
async function saveMailPreferences() {
const payload = {
messages_per_page: Number($("#mail-pref-per-page").value),
mark_read_delay_ms: Number($("#mail-pref-read-delay").value),
remote_images: $("#mail-pref-remote-images").value,
preferred_body: $("#mail-pref-preferred-body").value,
thread_mode: $("#mail-pref-thread-mode").value,
collapse_quotes: $("#mail-pref-collapse-quotes").checked,
show_snippets: $("#mail-pref-show-snippets").checked,
default_sort: $("#mail-pref-sort").value,
flagged_on_top: $("#mail-pref-flagged-top").checked,
sync_imap_keywords: $("#mail-pref-sync-keywords").checked,
};
try {
state.mailPrefs = await request("/settings/mailbox", { method: "PUT", body: JSON.stringify(payload) });
state.perPage = Number(state.mailPrefs.messages_per_page || 300);
toast("Mailbox settings saved");
if (state.currentDetail) renderMessageDetail(state.currentDetail);
await loadMessages(true);
} catch (error) { toast(error.message, "error"); }
}
async function reconcileImapLabels() {
const button = $("#mail-pref-reconcile-labels");
const status = $("#mail-pref-reconcile-status");
if (!$("#mail-pref-sync-keywords")?.checked) {
toast("Enable IMAP keyword synchronization first", "warning");
return;
}
if (button) button.disabled = true;
if (status) status.textContent = "Reconciling…";
try {
const result = await request("/tags/reconcile", { method: "POST", body: "{}" });
const added = Number(result.labels_added || 0);
const removed = Number(result.labels_removed || 0);
const folders = Number(result.folders_checked || 0);
const skipped = Number(result.skipped_folders || 0);
if (status) status.textContent = `${folders} folder(s) checked · +${added} / −${removed}${skipped ? ` · ${skipped} skipped` : ""}`;
toast(`Labels reconciled: +${added}, −${removed}`, "success");
await loadTags();
await loadMessages(true);
} catch (error) {
if (status) status.textContent = error.message;
toast(error.message, "error");
} finally {
if (button) button.disabled = false;
}
}
const presetEditablePreferenceKeys = new Set([
"colorPreset", "accentColor", "backgroundColor", "panelColor", "textColor",
"density", "preview", "scale", "fontFamily", "fontSize", "rowHeight",
"folderPaneWidth", "messagePaneWidth", "borderRadius", "contrast",
"iconSize", "toolbarDensity", "sidebarItemHeight", "readerMaxWidth", "composerWidth",
"selectionColor", "unreadFontWeight", "unreadBadgeStyle", "scrollbarDensity",
"compactHeaders", "showBorders", "monochrome",
"folderBackground", "messageBackground", "readerBackground",
]);
function presetCssHex(styles, name, fallback) {
const value = styles.getPropertyValue(name).trim();
if (/^#[0-9a-f]{6}$/i.test(value)) return value;
const shortHex = value.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/i);
if (shortHex) return `#${shortHex.slice(1).map((part) => part + part).join("")}`;
return fallback;
}
function customAppearancePayload(prefs) {
const keys = [
"interfaceSkin", "presetScaleUnlocked", "colorPreset", "accentColor", "backgroundColor", "panelColor", "textColor",
"density", "preview", "scale", "fontFamily", "fontSize", "rowHeight", "folderPaneWidth", "messagePaneWidth",
"borderRadius", "contrast", "iconSize", "toolbarDensity", "sidebarItemHeight", "readerMaxWidth", "composerWidth",
"selectionColor", "unreadFontWeight", "unreadBadgeStyle", "scrollbarDensity", "compactHeaders", "showBorders", "monochrome",
"folderBackground", "messageBackground", "readerBackground",
];
return Object.fromEntries(keys.map((key) => [preferenceMap[key], prefs[key]]).filter(([field]) => field));
}
function schedulePromotedAppearanceSave() {
if (presetPromotionTimer) clearTimeout(presetPromotionTimer);
if (presetPromotionInFlight) {
presetPromotionNeedsAnotherSave = true;
return;
}
presetPromotionTimer = setTimeout(async () => {
presetPromotionTimer = null;
presetPromotionInFlight = true;
presetPromotionNeedsAnotherSave = false;
try {
await request("/settings/display", { method: "PUT", body: JSON.stringify(customAppearancePayload(state.prefs)) });
} catch (error) {
toast(error.message, "error");
} finally {
presetPromotionInFlight = false;
if (presetPromotionNeedsAnotherSave) schedulePromotedAppearanceSave();
else presetPromotionActive = false;
}
}, 180);
}
function promotePresetToCustom(key, value) {
const fixedStyle = activeInterfaceStyle(state.prefs);
if (!fixedStyle || !state.prefs.presetScaleUnlocked || !presetEditablePreferenceKeys.has(key)) return false;
const styles = getComputedStyle(root);
state.prefs = {
...state.prefs,
...fixedStyle.preferences,
interfaceSkin: "rustbox",
presetScaleUnlocked: false,
colorPreset: "custom",
accentColor: presetCssHex(styles, "--accent", state.prefs.accentColor),
backgroundColor: presetCssHex(styles, "--bg", state.prefs.backgroundColor),
panelColor: presetCssHex(styles, "--panel", state.prefs.panelColor),
textColor: presetCssHex(styles, "--text", state.prefs.textColor),
selectionColor: presetCssHex(styles, "--selected", fixedStyle.preferences.selectionColor || state.prefs.selectionColor),
folderBackground: presetCssHex(styles, "--folder-bg", state.prefs.folderBackground || "#101310"),
messageBackground: presetCssHex(styles, "--message-bg", state.prefs.messageBackground || "#181c19"),
readerBackground: presetCssHex(styles, "--reader-bg", state.prefs.readerBackground || "#181c19"),
[key]: value,
};
presetPromotionActive = true;
applyPreferences();
schedulePromotedAppearanceSave();
return true;
}
function savePreference(key, value) {
if (promotePresetToCustom(key, value)) return;
state.prefs[key] = value;
if (presetPromotionActive && presetEditablePreferenceKeys.has(key)) {
if (["accentColor", "backgroundColor", "panelColor", "textColor"].includes(key)) state.prefs.colorPreset = "custom";
applyPreferences();
schedulePromotedAppearanceSave();
return;
}
if (["accentColor", "backgroundColor", "panelColor", "textColor"].includes(key)) state.prefs.colorPreset = "custom";
applyPreferences();
if (key === "language") loadTranslations(value);
const previousTimer = preferenceTimers.get(key);
if (previousTimer) clearTimeout(previousTimer);
const timer = setTimeout(async () => {
preferenceTimers.delete(key);
const field = preferenceMap[key];
if (!field) return;
const payload = { [field]: state.prefs[key] };
if (["accentColor", "backgroundColor", "panelColor", "textColor"].includes(key)) payload.color_preset = "custom";
try { await request("/settings/display", { method: "PUT", body: JSON.stringify(payload) }); }
catch (error) { toast(error.message, "error"); }
}, 180);
preferenceTimers.set(key, timer);
}
async function resetAppearance() {
const payload = { theme: root.dataset.defaultTheme || "system", interface_skin: "default", preset_scale_unlocked: false, density: "compact", preview_pane: "right", ui_scale: "normal", color_preset: "sage", accent_color: "#6f9c86", background_color: "#121513", panel_color: "#181c19", text_color: "#edf0ec", font_family: "system", font_size: 13, row_height: 58, folder_pane_width: 228, message_pane_width: 420, border_radius: 4, contrast: "normal", show_avatars: true, show_attachment_icons: true, show_flags: true, reduce_motion: false, date_format: "locale", time_format: "locale", icon_size: 18, toolbar_density: "normal", sidebar_item_height: 29, reader_max_width: 0, composer_width: 900, selection_color: "#28352e", unread_font_weight: 730, unread_badge_style: "dot", compact_headers: false, show_borders: true, scrollbar_density: "thin", monochrome: false, folder_background: "", message_background: "", reader_background: "" };
try { normalizeServerPreferences(await request("/settings/display", { method: "PUT", body: JSON.stringify(payload) })); applyPreferences(); toast("Appearance reset"); }
catch (error) { toast(error.message, "error"); }
}
function formatDate(raw, long = false) {
const date = new Date(raw);
if (Number.isNaN(date.getTime())) return raw || "";
const timeOptions = state.prefs.timeFormat === "24h" ? { hour: "2-digit", minute: "2-digit", hour12: false } : state.prefs.timeFormat === "12h" ? { hour: "numeric", minute: "2-digit", hour12: true } : { hour: "2-digit", minute: "2-digit" };
if (long) {
if (state.prefs.dateFormat === "iso") return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${date.toLocaleTimeString([], timeOptions)}`;
const locale = state.prefs.dateFormat === "eu" ? "en-GB" : state.prefs.dateFormat === "us" ? "en-US" : undefined;
return `${date.toLocaleDateString(locale)} ${date.toLocaleTimeString([], timeOptions)}`;
}
const now = new Date();
if (date.toDateString() === now.toDateString()) return date.toLocaleTimeString([], timeOptions);
if (state.prefs.dateFormat === "iso") return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
if (state.prefs.dateFormat === "eu") return date.toLocaleDateString("en-GB");
if (state.prefs.dateFormat === "us") return date.toLocaleDateString("en-US");
if (date.getFullYear() === now.getFullYear()) return date.toLocaleDateString([], { month: "short", day: "numeric" });
return date.toLocaleDateString([], { year: "numeric", month: "short", day: "numeric" });
}
function formatBytes(value) {
const bytes = Number(value || 0);
if (!Number.isFinite(bytes) || bytes < 1) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
return `${(bytes / (1024 ** index)).toFixed(index ? 1 : 0)} ${units[index]}`;
}
function initial(value) {
return (value || "?").trim().charAt(0).toUpperCase() || "?";
}
function showLogin() {
root.dataset.theme = resolveTheme(root.dataset.defaultTheme || "system");
root.dataset.interfaceSkin = root.dataset.defaultInterfaceSkin || "rustbox";
root.dataset.colorPreset = "sage";
$("#boot").classList.add("hidden");
$("#app-view").classList.add("hidden");
$("#login-view").classList.remove("hidden");
}
function showApp() {
$("#boot").classList.add("hidden");
$("#login-view").classList.add("hidden");
$("#app-view").classList.remove("hidden");
}
function finishAuthenticatedStartup({ showLoginToast = false } = {}) {
state.startupToastsMuted = false;
armRealtimeNotifications();
if (showLoginToast) toast(tr("login_success", "Successfully logged in"), "success");
}
function prepareAuthenticatedStartup() {
state.startupToastsMuted = true;
resetRealtimeNotificationGate();
}
function settingsTabAvailable(name) {
if (name === "openpgp") return state.authPolicy.pgpEnabled !== false;
if (name === "sieve") return state.authPolicy.sieveEnabled !== false;
if (name === "diagnostics") return state.authPolicy.diagnosticsEnabled !== false;
if (name === "about") return state.authPolicy.aboutEnabled !== false;
return true;
}
function applyFeatureAvailability() {
for (const [name, enabled] of [
["sieve", state.authPolicy.sieveEnabled !== false],
["diagnostics", state.authPolicy.diagnosticsEnabled !== false],
["about", state.authPolicy.aboutEnabled !== false],
]) {
$(`.settings-tab[data-tab="${name}"]`)?.classList.toggle("hidden", !enabled);
$(`.settings-panel[data-panel="${name}"]`)?.toggleAttribute("data-feature-disabled", !enabled);
}
$("#diagnostic-sieve-card")?.classList.toggle("hidden", state.authPolicy.sieveEnabled === false);
$("#quick-filter-btn")?.classList.toggle("hidden", state.authPolicy.sieveEnabled === false);
}
async function loadAuthPolicy() {
try {
const policy = await request("/auth/policy", { method: "GET" });
state.authPolicy = { ...state.authPolicy, ...policy };
} catch (_) {
state.authPolicy = { ...state.authPolicy, rememberMeEnabled: true };
}
const enabled = state.authPolicy.rememberMeEnabled !== false;
["#login-remember-row", "#add-account-remember-row"].forEach((selector) => $(selector)?.classList.toggle("hidden", !enabled));
["#login-remember", "#add-account-remember"].forEach((selector) => {
const input = $(selector);
if (!input) return;
input.disabled = !enabled;
if (!enabled) input.checked = false;
});
["#login-remember-label", "#add-account-remember-label"].forEach((selector) => {
const label = $(selector);
if (label) label.textContent = "Keep me signed in";
});
}
async function loadSessionPolicy() {
try {
const policy = await request("/auth/session-policy", { method: "GET" });
state.authPolicy = { ...state.authPolicy, ...policy };
} catch (_) {
state.authPolicy = {
...state.authPolicy,
pgpEnabled: false,
sieveEnabled: false,
diagnosticsEnabled: false,
aboutEnabled: false,
};
}
setOpenPgpAvailability(state.authPolicy.pgpEnabled !== false);
applyFeatureAvailability();
}
async function loadAccounts() {
try {
const data = await request("/auth/accounts", { method: "GET" });
state.accounts = data.accounts || [];
if (!state.accounts.length) {
state.accountId = null;
localStorage.removeItem("rustbox.activeAccount");
state.folders = [];
updateUnreadAppBadge();
showLogin();
return false;
}
if (!state.accountId || !state.accounts.some((account) => account.id === state.accountId)) {
state.accountId = state.accounts[0].id;
localStorage.setItem("rustbox.activeAccount", state.accountId);
}
const account = state.accounts.find((item) => item.id === state.accountId) || state.accounts[0];
$("#account-email").textContent = account.email;
showApp();
return true;
} catch (error) {
console.error(error);
showLogin();
return false;
}
}
function renderAccountSettings() {
const list = $("#account-settings-list");
if (!list) return;
list.replaceChildren();
if (!state.accounts.length) {
list.append(el("div", "empty-list", "No active accounts."));
return;
}
for (const account of state.accounts) {
const row = el("div", `account-settings-row ${account.id === state.accountId ? "active" : ""}`);
const email = el("span", "account-email", account.email || "Account");
const use = el("button", "button quiet", account.id === state.accountId ? "Current" : "Switch");
use.type = "button";
use.disabled = account.id === state.accountId;
use.addEventListener("click", () => switchAccount(account.id));
const remove = el("button", "button quiet danger", "Remove");
remove.type = "button";
remove.addEventListener("click", () => removeBrowserAccount(account.id));
row.append(email, use, remove);
list.append(row);
}
}
async function loadActiveSessions() {
const list = $("#account-session-list");
if (!list) return;
list.replaceChildren(el("div", "empty-list", "Loading sessions…"));
try {
const data = await request("/auth/sessions", { method: "GET" });
const sessions = data.sessions || [];
list.replaceChildren();
if (!sessions.length) {
list.append(el("div", "empty-list", "No active sessions."));
return;
}
for (const session of sessions) {
const row = el("div", `account-settings-row ${session.current ? "active" : ""}`);
const details = el("div", "account-session-details");
const expiry = Number(session.expiresAt || 0);
const expiryText = expiry ? ` · expires ${new Date(expiry * 1000).toLocaleString()}` : "";
const rememberedText = session.remembered ? " · remembered" : "";
details.append(
el("strong", "account-email", session.current ? `${session.email} · current` : session.email),
el("span", "muted", `Created ${new Date(session.createdAt * 1000).toLocaleString()} · active ${new Date(session.lastActive * 1000).toLocaleString()}${expiryText}${rememberedText}`),
);
const client = el("span", "muted", session.clientName || "Unknown device");
if (session.userAgent) client.title = session.userAgent;
details.append(client);
const action = el("button", "button quiet danger", session.current ? "Current" : "Revoke");
action.type = "button";
action.disabled = Boolean(session.current);
action.addEventListener("click", () => revokeActiveSession(session.id));
row.append(details, action);
list.append(row);
}
} catch (error) {
list.replaceChildren(el("div", "empty-list", error.message));
}
}
async function revokeActiveSession(id) {
if (!await appConfirm("Revoke this RustBox session?", { title: "Revoke session", confirmLabel: "Revoke", danger: true })) return;
try {
await request(`/auth/sessions/${encodeURIComponent(id)}`, { method: "DELETE" });
toast("Session revoked");
await loadActiveSessions();
} catch (error) { toast(error.message, "error"); }
}
async function revokeOtherSessions() {
if (!await appConfirm("Revoke every other active RustBox session for this account?", { title: "Revoke other sessions", confirmLabel: "Revoke sessions", danger: true })) return;
try {
const result = await request("/auth/sessions/revoke-others", { method: "POST", body: "{}" });
toast(`Revoked ${result.count || 0} session(s)`);
await loadActiveSessions();
} catch (error) { toast(error.message, "error"); }
}
async function reloadActiveAccount() {
setFolderDrawerOpen(false);
state.folder = "INBOX";
state.selected = null;
state.currentDetail = null;
state.selectedMessages.clear();
state.selectionMode = false;
closeReader();
await loadSessionPolicy();
await loadPreferences();
await loadMailPreferences();
if (state.authPolicy.pgpEnabled !== false) await loadOpenPgp();
else setOpenPgpAvailability(false);
await loadIdentities();
await loadFolders(true);
await loadSpecialFolders();
await loadQuota();
await loadTags();
await loadNotificationPreferences();
await loadTrustedSenders();
startCalendarReminderWatcher();
renderAccountSettings();
}
async function switchAccount(accountId) {
if (!state.accounts.some((account) => account.id === accountId) || accountId === state.accountId) return;
state.accountId = accountId;
localStorage.setItem("rustbox.activeAccount", accountId);
const account = state.accounts.find((item) => item.id === accountId);
if (account) $("#account-email").textContent = account.email;
try { await reloadActiveAccount(); } catch (error) { toast(error.message, "error"); }
}
async function removeBrowserAccount(accountId) {
const account = state.accounts.find((item) => item.id === accountId);
if (!account || !await appConfirm(`Remove ${account.email} from this browser session?`, { title: "Remove account", confirmLabel: "Remove", danger: true })) return;
try {
await request(`/auth/accounts/${encodeURIComponent(accountId)}`, { method: "DELETE" });
const removingCurrent = accountId === state.accountId;
await loadAccounts();
if (!state.accounts.length) return;
if (removingCurrent) await reloadActiveAccount();
else renderAccountSettings();
} catch (error) { toast(error.message, "error"); }
}
async function addBrowserAccount(event) {
event.preventDefault();
const errorBox = $("#add-account-error");
const submit = $("#add-account-form button[type=submit]");
errorBox.classList.add("hidden");
setBusy(submit, true);
try {
const data = await request("/auth/login", { method: "POST", body: JSON.stringify({ email: $("#add-account-email").value.trim(), password: $("#add-account-password").value, remember: $("#add-account-remember").checked }) });
state.accountId = data.account.id;
localStorage.setItem("rustbox.activeAccount", state.accountId);
$("#add-account-dialog").close();
$("#add-account-form").reset();
await loadAccounts();
await reloadActiveAccount();
disconnectRealtime();
connectRealtime();
toast("Account added");
} catch (error) {
errorBox.textContent = error.message;
errorBox.classList.remove("hidden");
} finally { setBusy(submit, false); }
}
async function login(event) {
event.preventDefault();
const submit = $("#login-form button[type=submit]");
const errorBox = $("#login-error");
errorBox.classList.add("hidden");
setBusy(submit, true);
prepareAuthenticatedStartup();
try {
const data = await request("/auth/login", {
method: "POST",
body: JSON.stringify({
email: $("#login-email").value.trim(),
password: $("#login-password").value,
remember: $("#login-remember").checked,
}),
});
state.accountId = data.account.id;
localStorage.setItem("rustbox.activeAccount", state.accountId);
await loadAccounts();
await loadSessionPolicy();
await loadPreferences();
await loadMailPreferences();
if (state.authPolicy.pgpEnabled !== false) await loadOpenPgp();
else setOpenPgpAvailability(false);
await loadIdentities();
await loadFolders(true);
await loadSpecialFolders();
await loadQuota();
await loadTags();
await loadNotificationPreferences();
await loadTrustedSenders();
startCalendarReminderWatcher();
connectRealtime();
finishAuthenticatedStartup({ showLoginToast: true });
} catch (error) {
errorBox.textContent = error.message;
errorBox.classList.remove("hidden");
} finally {
setBusy(submit, false);
}
}
async function logout() {
disconnectRealtime();
prepareAuthenticatedStartup();
if (state.calendarReminderTimer) { clearInterval(state.calendarReminderTimer); state.calendarReminderTimer = null; }
try { await request("/auth/logout", { method: "POST", body: "{}" }); } catch (_) { }
state.accountId = null;
state.accounts = [];
localStorage.removeItem("rustbox.activeAccount");
showLogin();
}