v0.2.6
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-01 15:46:10 +01:00
1 parent 47c3cd7f65
commit cfc74d9db6
33 files changed
+850 -244

No files matched your search

+76 -7
View File
@@ -160,6 +160,58 @@ function playNotificationSound() {
} catch (_) { }
}
function setIndexingIndicator(active, pending = 0) {
state.indexingActive = Boolean(active);
const indicator = $("#indexing-indicator");
if (!indicator) return;
indicator.classList.toggle("hidden", !state.indexingActive);
const count = Math.max(0, Number(pending) || 0);
const label = count > 0 ? `Indexing messages (${count} pending)` : "Indexing messages";
indicator.title = label;
indicator.setAttribute("aria-label", label);
}
function syncRealtimeWatchedFolder(force = false) {
const socket = state.ws;
if (!socket || socket.readyState !== WebSocket.OPEN || !state.accountId || !state.folder) return;
const key = `${state.accountId}|${state.folder}`;
if (!force && state.realtimeWatchedFolder === key) return;
try {
socket.send(JSON.stringify({ type: "watch_folder", accountId: state.accountId, folder: state.folder }));
state.realtimeWatchedFolder = key;
} catch (_) { }
}
function newMailNotificationKey(data, accountId) {
const uid = Number(data?.latest_uid);
if (Number.isFinite(uid) && uid > 0) return `${accountId || ""}|${data?.folder || "INBOX"}|${uid}`;
return `${accountId || ""}|${data?.folder || "INBOX"}|${data?.latest_sender || ""}|${data?.latest_subject || ""}|${data?.count || 1}`;
}
function isDuplicateNewMailNotification(data, accountId) {
const now = Date.now();
for (const [key, seenAt] of state.realtimeNotifiedMessages) {
if (now - seenAt > 10 * 60 * 1000) state.realtimeNotifiedMessages.delete(key);
}
const key = newMailNotificationKey(data, accountId);
if (state.realtimeNotifiedMessages.has(key)) return true;
state.realtimeNotifiedMessages.set(key, now);
return false;
}
function toastNewMail(data, accountId) {
const count = Math.max(1, Number(data?.count) || 1);
const folder = data?.folder || "INBOX";
const account = state.accounts.find((item) => item.id === accountId);
const accountPrefix = accountId && accountId !== state.accountId && account?.email ? `${account.email} · ` : "";
const sender = data?.latest_sender ? `${data.latest_sender}: ` : "";
const subject = data?.latest_subject || (count === 1 ? "New message" : `${count} new messages`);
const message = count === 1
? `${accountPrefix}${folder} · ${sender}${subject}`
: `${accountPrefix}${folder} · ${count} new messages${data?.latest_subject ? ` · ${data.latest_subject}` : ""}`;
toast(message, "info", 8000, { label: "Open", onClick: () => openRealtimeNotificationTarget(accountId, data) });
}
async function openRealtimeNotificationTarget(accountId, data) {
window.focus();
const folder = data?.folder || "INBOX";
@@ -185,7 +237,8 @@ function notifyNewMail(data, accountId) {
const account = state.accounts.find((item) => item.id === accountId);
const title = data.latest_sender || account?.email || "New mail";
const body = data.latest_subject || `${data.count || 1} new message(s) in ${data.folder}`;
const notification = new Notification(title, { body, tag: `rustbox-${accountId}-${data.folder}`, renotify: true });
const notificationTag = `rustbox-${accountId}-${data.folder}-${Number.isFinite(uid) && uid > 0 ? uid : "new"}`;
const notification = new Notification(title, { body, tag: notificationTag, renotify: false });
notification.onclick = () => { notification.close(); void openRealtimeNotificationTarget(accountId, data); };
}
if (prefs.sound) playNotificationSound();
@@ -270,13 +323,14 @@ function scheduleRealtimeRefresh(event, accountId) {
clearTimeout(state.realtimeRefreshTimer);
state.realtimeRefreshTimer = setTimeout(async () => {
const events = state.realtimeRefreshEvents.splice(0);
await refreshFolderListOnly();
const refreshCurrent = events.some((item) => ["NewMessages", "FlagsChanged"].includes(item.type) && item.data?.folder === state.folder);
const refreshCurrent = events.some((item) => item.type === "FolderUpdated" || (["NewMessages", "FlagsChanged"].includes(item.type) && item.data?.folder === state.folder));
const tasks = [refreshFolderListOnly()];
if (refreshCurrent) {
if (state.searchContext) await runSearch(new URLSearchParams(state.searchContext.params), state.searchContext.label, { restoreScrollTop: state.searchContext.scrollTop });
else await loadMessages(true);
if (state.searchContext) tasks.push(runSearch(new URLSearchParams(state.searchContext.params), state.searchContext.label, { restoreScrollTop: state.searchContext.scrollTop }));
else tasks.push(loadMessages(true));
}
}, 180);
await Promise.all(tasks);
}, 80);
}
function startRealtimeFallbackPolling() {
@@ -325,7 +379,9 @@ function connectRealtime() {
state.wsConnectedOnce = true;
state.wsRetryAttempt = 0;
state.wsLastHeartbeatAt = Date.now();
state.realtimeWatchedFolder = null;
startRealtimeWatchdog(socket);
syncRealtimeWatchedFolder(true);
if (reconnect) void resyncRealtimeState();
};
socket.onmessage = (message) => {
@@ -339,13 +395,24 @@ function connectRealtime() {
}
if (isDuplicateRealtimeEvent(payload)) return;
const event = payload.event || {};
if (event.type === "NewMessages") notifyNewMail(event.data || {}, payload.accountId);
if (event.type === "NewMessages") {
const data = event.data || {};
if (!isDuplicateNewMailNotification(data, payload.accountId)) {
toastNewMail(data, payload.accountId);
notifyNewMail(data, payload.accountId);
}
}
if (event.type === "IndexingStatus" && payload.accountId === state.accountId) {
setIndexingIndicator(Boolean(event.data?.active), event.data?.pending);
}
if (["NewMessages", "FlagsChanged", "FolderUpdated"].includes(event.type)) scheduleRealtimeRefresh(event, payload.accountId);
} catch (error) { console.warn("Invalid realtime event", error); }
};
socket.onclose = () => {
if (state.ws !== socket) return;
state.ws = null;
state.realtimeWatchedFolder = null;
setIndexingIndicator(false);
clearInterval(state.wsWatchdogTimer);
state.wsWatchdogTimer = null;
scheduleRealtimeReconnect();
@@ -366,6 +433,8 @@ function disconnectRealtime() {
state.realtimeRefreshEvents = [];
state.wsRetryAttempt = 0;
state.wsLastHeartbeatAt = 0;
state.realtimeWatchedFolder = null;
setIndexingIndicator(false);
const socket = state.ws;
state.ws = null;
if (socket && socket.readyState < WebSocket.CLOSING) socket.close(1000, "Client disconnect");