relay: suppress poll re-renders during user actions
Inbox poll continues fetching but skips DOM updates when user is typing, deleting, or confirming. Quiet mode diffs message IDs to avoid unnecessary re-renders. Focus/blur tracking on all inputs auto-guards. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f465306c1d
commit
00d14b3015
3 files changed
+47
-22
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
define('APP_NAME', 'hyprcrd');
|
||||
define('APP_VERSION', '1.3.1');
|
||||
define('APP_VERSION', '1.3.2');
|
||||
|
||||
// Database path — writable directory, outside webroot
|
||||
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hyprcrd",
|
||||
"version": "1.3.1",
|
||||
"version": "1.3.2",
|
||||
"description": "hyprcrd — self-hosted card manager",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
+45
-20
@@ -113,16 +113,26 @@ function showView(name) {
|
||||
// the moment the user navigates away (account/admin/login) so we don't
|
||||
// hammer the server in the background.
|
||||
let inboxPollHandle = null;
|
||||
let inboxBusy = false;
|
||||
function startInboxPoll() {
|
||||
stopInboxPoll();
|
||||
inboxPollHandle = setInterval(() => {
|
||||
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 = `
|
||||
<div class="skeleton-row"><div class="skel tall"></div></div>
|
||||
<div class="skeleton-row"><div class="skel short"></div></div>
|
||||
<div class="skeleton-row"><div class="skel"></div></div>
|
||||
`;
|
||||
if (!quiet) {
|
||||
list.innerHTML = `
|
||||
<div class="skeleton-row"><div class="skel tall"></div></div>
|
||||
<div class="skeleton-row"><div class="skel short"></div></div>
|
||||
<div class="skeleton-row"><div class="skel"></div></div>
|
||||
`;
|
||||
}
|
||||
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 = `<i class="ph ph-tray"></i><span>${messages.length} in inbox</span>`;
|
||||
$('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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user