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:
thandClaude Opus 4.6 committed 2026-04-12 20:26:26 +02:00
1 parent f465306c1d
commit 00d14b3015
3 files changed
+30 -5

No files matched your search

+1 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hyprcrd",
"version": "1.3.1",
"version": "1.3.2",
"description": "hyprcrd — self-hosted card manager",
"main": "main.js",
"scripts": {
+28 -3
View File
@@ -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');
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,6 +465,8 @@ async function deleteMessage(id, btn) {
window.deleteMessage = deleteMessage;
async function deleteAllMessages() {
inboxBusy = true;
try {
const res = await api('api/messages/inbox');
const messages = res?.messages || [];
if (!messages.length) return;
@@ -455,6 +479,7 @@ async function deleteAllMessages() {
toast(`${ok} message${ok !== 1 ? 's' : ''} deleted`, 'ok');
loadInbox();
refreshMe();
} finally { inboxBusy = false; }
}
window.deleteAllMessages = deleteAllMessages;