diff --git a/config.php b/config.php
index 54de1f5..c6b0ec6 100644
--- a/config.php
+++ b/config.php
@@ -1,7 +1,7 @@
{
- if (!$('view-compose')?.classList.contains('hidden')) loadInbox();
+ if (inboxBusy) return;
+ if (!$('view-compose')?.classList.contains('hidden')) loadInbox({ quiet: true });
}, 15000);
}
function stopInboxPoll() {
if (inboxPollHandle) { clearInterval(inboxPollHandle); inboxPollHandle = null; }
}
+// Suppress poll re-renders while the user is typing in any input/textarea
+document.addEventListener('focusin', e => {
+ if (e.target.matches('input, textarea, select')) inboxBusy = true;
+});
+document.addEventListener('focusout', e => {
+ if (e.target.matches('input, textarea, select')) inboxBusy = false;
+});
+
/** Burger toggle for the mobile nav. The button is hidden on desktop
* via CSS, on mobile it expands #nav-buttons as a dropdown panel
* positioned under the header. */
@@ -371,18 +381,27 @@ async function sendCompose() {
// ── Inbox (counts only — content is encrypted for the hyprcrd client) ──
-async function loadInbox() {
+let lastInboxJson = '';
+
+async function loadInbox({ quiet = false } = {}) {
const list = $('inbox-list');
- list.innerHTML = `
-
-
-
- `;
+ if (!quiet) {
+ list.innerHTML = `
+
+
+
+ `;
+ }
const res = await api('api/messages/inbox');
const messages = res?.messages || [];
+
+ // In quiet mode, skip DOM update if nothing changed
+ const json = JSON.stringify(messages.map(m => m.id + ':' + m.fetched));
+ if (quiet && json === lastInboxJson) return;
+ lastInboxJson = json;
+
$('inbox-count').textContent = messages.length;
$('inbox-delete-all')?.classList.toggle('hidden', messages.length === 0);
- // Mirror count into the header status so the unread dot stays in sync
$('header-status').innerHTML = `${messages.length} in inbox`;
$('header-status').classList.toggle('has-unread', messages.length > 0);
@@ -408,6 +427,7 @@ async function loadInbox() {
}
async function deleteMessage(id, btn) {
+ inboxBusy = true;
// Inline confirmation: replace the trash button with yes/no.
// Event listeners are wired via JS (not inline onclick) to avoid
// passing escaped HTML through attributes.
@@ -425,6 +445,7 @@ async function deleteMessage(id, btn) {
btn.replaceWith(wrap);
wrap.querySelector('.del-yes').addEventListener('click', async () => {
const res = await api('api/messages/' + id, { method: 'DELETE' });
+ inboxBusy = false;
if (res?.ok) {
if (row) { row.style.transition = 'opacity .2s'; row.style.opacity = '0'; setTimeout(() => row.remove(), 220); }
loadInbox();
@@ -435,6 +456,7 @@ async function deleteMessage(id, btn) {
}
});
wrap.querySelector('.del-no').addEventListener('click', () => {
+ inboxBusy = false;
const temp = document.createElement('div');
temp.innerHTML = origHtml;
wrap.replaceWith(temp.firstChild);
@@ -443,18 +465,21 @@ async function deleteMessage(id, btn) {
window.deleteMessage = deleteMessage;
async function deleteAllMessages() {
- const res = await api('api/messages/inbox');
- const messages = res?.messages || [];
- if (!messages.length) return;
- if (!await relayConfirm(`Delete all ${messages.length} message${messages.length !== 1 ? 's' : ''}? This cannot be undone.`, { okLabel: 'Delete all' })) return;
- let ok = 0;
- for (const m of messages) {
- const r = await api('api/messages/' + m.id, { method: 'DELETE' });
- if (r?.ok) ok++;
- }
- toast(`${ok} message${ok !== 1 ? 's' : ''} deleted`, 'ok');
- loadInbox();
- refreshMe();
+ inboxBusy = true;
+ try {
+ const res = await api('api/messages/inbox');
+ const messages = res?.messages || [];
+ if (!messages.length) return;
+ if (!await relayConfirm(`Delete all ${messages.length} message${messages.length !== 1 ? 's' : ''}? This cannot be undone.`, { okLabel: 'Delete all' })) return;
+ let ok = 0;
+ for (const m of messages) {
+ const r = await api('api/messages/' + m.id, { method: 'DELETE' });
+ if (r?.ok) ok++;
+ }
+ toast(`${ok} message${ok !== 1 ? 's' : ''} deleted`, 'ok');
+ loadInbox();
+ refreshMe();
+ } finally { inboxBusy = false; }
}
window.deleteAllMessages = deleteAllMessages;