Speed up email composer typing

This commit is contained in:
pewdiepie-archdaemon
2026-06-30 11:49:20 +00:00
parent b5ea5a1607
commit 4f387e089a
3 changed files with 34 additions and 9 deletions
+3 -3
View File
@@ -219,8 +219,8 @@
}, { once: true }); }, { once: true });
})(); })();
</script> </script>
<link rel="stylesheet" href="/static/style.css?v=20260630emailquote"> <link rel="stylesheet" href="/static/style.css?v=20260630emailtype">
<link rel="modulepreload" href="/static/app.js?v=20260630emailquote"> <link rel="modulepreload" href="/static/app.js?v=20260630emailtype">
<link rel="modulepreload" href="/static/js/chat.js"> <link rel="modulepreload" href="/static/js/chat.js">
<link rel="modulepreload" href="/static/js/ui.js"> <link rel="modulepreload" href="/static/js/ui.js">
<link rel="modulepreload" href="/static/js/sessions.js"> <link rel="modulepreload" href="/static/js/sessions.js">
@@ -2512,7 +2512,7 @@
<script type="module" src="/static/js/settings.js"></script> <script type="module" src="/static/js/settings.js"></script>
<script type="module" src="/static/js/admin.js"></script> <script type="module" src="/static/js/admin.js"></script>
<script type="module" src="/static/js/assistant.js"></script> <script type="module" src="/static/js/assistant.js"></script>
<script type="module" src="/static/app.js?v=20260630emailquote"></script> <!-- app.js must be LAST --> <script type="module" src="/static/app.js?v=20260630emailtype"></script> <!-- app.js must be LAST -->
<script type="module" src="/static/js/init.js"></script> <script type="module" src="/static/js/init.js"></script>
<script type="module" src="/static/js/a11y.js"></script> <script type="module" src="/static/js/a11y.js"></script>
<script nonce="{{CSP_NONCE}}">if('serviceWorker' in navigator){navigator.serviceWorker.register('/static/sw.js').catch(()=>{});}</script> <script nonce="{{CSP_NONCE}}">if('serviceWorker' in navigator){navigator.serviceWorker.register('/static/sw.js').catch(()=>{});}</script>
+30 -5
View File
@@ -36,6 +36,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
let _emailStreamRenderedBody = ''; let _emailStreamRenderedBody = '';
let _emailStreamTargetBody = ''; let _emailStreamTargetBody = '';
let _emailLocalDraftDebounce = null; let _emailLocalDraftDebounce = null;
let _emailRichbodySaveDebounce = null;
const _EMAIL_LOCAL_DRAFT_PREFIX = 'odysseus.email.replyDraft.v1:'; const _EMAIL_LOCAL_DRAFT_PREFIX = 'odysseus.email.replyDraft.v1:';
// Diff mode state // Diff mode state
@@ -2353,7 +2354,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
function _persistEmailLocalDraftSoon() { function _persistEmailLocalDraftSoon() {
clearTimeout(_emailLocalDraftDebounce); clearTimeout(_emailLocalDraftDebounce);
_emailLocalDraftDebounce = setTimeout(_persistEmailLocalDraftNow, 250); _emailLocalDraftDebounce = setTimeout(_persistEmailLocalDraftNow, 800);
} }
function _clearEmailLocalDraft(sourceUid, sourceFolder, inReplyTo) { function _clearEmailLocalDraft(sourceUid, sourceFolder, inReplyTo) {
@@ -2393,20 +2394,41 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
const ta = document.getElementById('doc-editor-textarea'); const ta = document.getElementById('doc-editor-textarea');
if (!ta) return; if (!ta) return;
ta.value = rich.innerText; ta.value = rich.innerText;
ta.dispatchEvent(new Event('input', { bubbles: true })); const doc = activeDocId && docs.get(activeDocId);
if (doc && doc.language === 'email') {
const fields = _parseEmailHeader(doc.content || '');
doc.content = _buildEmailContent(
document.getElementById('doc-email-to')?.value || fields.to || '',
document.getElementById('doc-email-subject')?.value || fields.subject || '',
document.getElementById('doc-email-in-reply-to')?.value || fields.inReplyTo || '',
document.getElementById('doc-email-references')?.value || fields.references || '',
rich.innerHTML,
document.getElementById('doc-email-source-uid')?.value || fields.sourceUid || '',
document.getElementById('doc-email-source-folder')?.value || fields.sourceFolder || '',
document.getElementById('doc-email-cc')?.value || fields.cc || '',
document.getElementById('doc-email-bcc')?.value || fields.bcc || '',
);
}
}
function _scheduleEmailRichbodySave() {
_persistEmailLocalDraftSoon();
clearTimeout(_emailRichbodySaveDebounce);
_emailRichbodySaveDebounce = setTimeout(() => { saveDocument({ silent: true }); }, 2500);
} }
function _wireEmailRichbody(rich) { function _wireEmailRichbody(rich) {
if (rich._wired) { _syncEmailRichbody(rich); return; } if (rich._wired) { _syncEmailRichbody(rich); return; }
rich._wired = true; rich._wired = true;
rich.addEventListener('input', () => { rich.addEventListener('input', () => {
_syncEmailRichbody(rich); _syncEmailRichbody(rich);
_persistEmailLocalDraftSoon(); _scheduleEmailRichbodySave();
}); });
// Highlight toolbar buttons (B / I / S, headings, lists) when the caret // Highlight toolbar buttons (B / I / S, headings, lists) when the caret
// sits inside formatted text. queryCommandState reflects the live // sits inside formatted text. queryCommandState reflects the live
// selection — we just translate that into .is-active classes the CSS // selection — we just translate that into .is-active classes the CSS
// already understands. // already understands.
const syncActive = () => { let syncActiveFrame = 0;
const syncActiveNow = () => {
syncActiveFrame = 0;
if (!rich.isConnected || rich.style.display === 'none') return; if (!rich.isConnected || rich.style.display === 'none') return;
// Only sync when focus is inside the rich body — otherwise selection // Only sync when focus is inside the rich body — otherwise selection
// outside it (e.g. clicking the toolbar itself) gives misleading state. // outside it (e.g. clicking the toolbar itself) gives misleading state.
@@ -2430,10 +2452,13 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
if (lBtn) lBtn.classList.toggle('is-active', !!inList); if (lBtn) lBtn.classList.toggle('is-active', !!inList);
} catch (_) {} } catch (_) {}
}; };
const syncActive = () => {
if (syncActiveFrame) return;
syncActiveFrame = requestAnimationFrame(syncActiveNow);
};
rich.addEventListener('keyup', syncActive); rich.addEventListener('keyup', syncActive);
rich.addEventListener('mouseup', syncActive); rich.addEventListener('mouseup', syncActive);
rich.addEventListener('focus', syncActive); rich.addEventListener('focus', syncActive);
rich.addEventListener('input', syncActive);
// selectionchange fires on the document; filter to selections inside rich. // selectionchange fires on the document; filter to selections inside rich.
document.addEventListener('selectionchange', () => { document.addEventListener('selectionchange', () => {
const sel = window.getSelection(); const sel = window.getSelection();
+1 -1
View File
@@ -7,7 +7,7 @@
// - Other static assets (images/fonts/libs): cache-first with bg refresh. // - Other static assets (images/fonts/libs): cache-first with bg refresh.
// - API / non-GET: never cached. // - API / non-GET: never cached.
// Bump CACHE_NAME whenever the precache list or SW logic changes. // Bump CACHE_NAME whenever the precache list or SW logic changes.
const CACHE_NAME = 'odysseus-v331'; const CACHE_NAME = 'odysseus-v332';
// Core shell precached on install so repeat opens are instant without any // Core shell precached on install so repeat opens are instant without any
// network wait. Keep this list in sync with the <script type="module"> tags // network wait. Keep this list in sync with the <script type="module"> tags