← Public packages
@kody/mailroom
Offline-first PWA for your Kody mailbox: sent and received threads, copy-prompt for agents, no send.
src/ui/client-script.ts
709 lines · 26.3 KB · TypeScriptimport { parseDeepLink } from '../deep-link.ts'
import {
buildMessagePrompt,
buildThreadPrompt,
textFromMessage,
} from '../prompt-text.ts'
import { filterThreads, messageWhen, snippetFrom, threadKey } from '../threads.ts'
import {
INBOX_POLL_INTERVAL_MS,
INBOX_REFRESH_MIN_INTERVAL_MS,
inboxHasNewMail,
shouldRefreshInbox,
} from './inbox-refresh.ts'
import {
UPDATE_CHECK_MIN_INTERVAL_MS,
describeAvailableUpdate,
shouldRunUpdateCheck,
} from './update-check.ts'
export const LOCAL_THREADS_KEY = 'mailroom-threads'
export const clientScript = `
(function () {
const base = document.documentElement.dataset.base || '/';
const dbName = 'mailroom';
const storeName = 'cache';
const LOCAL_THREADS_KEY = ${JSON.stringify(LOCAL_THREADS_KEY)};
const UPDATE_CHECK_MIN_INTERVAL_MS = ${JSON.stringify(UPDATE_CHECK_MIN_INTERVAL_MS)};
const INBOX_REFRESH_MIN_INTERVAL_MS = ${JSON.stringify(INBOX_REFRESH_MIN_INTERVAL_MS)};
const INBOX_POLL_INTERVAL_MS = ${JSON.stringify(INBOX_POLL_INTERVAL_MS)};
const shouldRunUpdateCheck = ${shouldRunUpdateCheck.toString()};
const shouldRefreshInbox = ${shouldRefreshInbox.toString()};
const inboxHasNewMail = ${inboxHasNewMail.toString()};
const describeAvailableUpdate = ${describeAvailableUpdate.toString()};
const parseDeepLink = ${parseDeepLink.toString()};
const filterThreads = ${filterThreads.toString()};
const messageWhen = ${messageWhen.toString()};
const threadKey = ${threadKey.toString()};
const snippetFrom = ${snippetFrom.toString()};
const textFromMessage = ${textFromMessage.toString()};
const buildMessagePrompt = ${buildMessagePrompt.toString()};
const buildThreadPrompt = ${buildThreadPrompt.toString()};
let deferredInstall = null;
const state = {
direction: 'inbound',
threads: [],
selectedId: null,
query: '',
aboutOpen: false,
total: 0,
latestAt: null,
inboxes: [],
};
const updateCheck = { inFlight: false, lastStartedAt: null };
const inboxCheck = { inFlight: false, lastStartedAt: null };
let inboxPollTimer = null;
const els = {
statusText: document.querySelector('[data-status-text]'),
actions: document.querySelector('[data-actions]'),
mailbox: document.querySelector('[data-mailbox]'),
list: document.querySelector('[data-list]'),
search: document.querySelector('[data-search]'),
sheet: document.querySelector('[data-sheet]'),
about: document.querySelector('[data-about]'),
aboutSheet: document.querySelector('[data-about-sheet]'),
toast: document.querySelector('[data-toast]'),
toastTitle: document.querySelector('[data-toast-title]'),
toastText: document.querySelector('[data-toast-text]'),
toastAction: document.querySelector('[data-toast-action]'),
toastDismiss: document.querySelector('[data-toast-dismiss]'),
installWrap: document.querySelector('[data-install-wrap]'),
installOpen: document.querySelector('[data-install-open]'),
install: document.querySelector('[data-install]'),
installPopover: document.querySelector('[data-install-popover]'),
installSteps: document.querySelector('[data-install-steps]'),
installHint: document.querySelector('[data-install-hint]'),
installHintDismiss: document.querySelector('[data-install-hint-dismiss]'),
};
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
deferredInstall = event;
syncInstallUi();
});
window.addEventListener('appinstalled', () => {
deferredInstall = null;
syncInstallUi();
});
if (shouldCanonicalizeClientPath(location.pathname)) {
location.replace(location.pathname + '/' + location.search + location.hash);
return;
}
function shouldCanonicalizeClientPath(pathname) {
return Boolean(pathname) && !pathname.endsWith('/');
}
function serviceWorkerScope(basePath, origin) {
const root = new URL(basePath || '/', origin);
if (!root.pathname.endsWith('/')) root.pathname += '/';
return root.href;
}
function api(path) {
const urlRoot = new URL(base || '/', location.origin);
if (!urlRoot.pathname.endsWith('/')) urlRoot.pathname += '/';
return new URL(String(path).replace(/^\\//, ''), urlRoot).toString();
}
function withTimeout(promise, ms, label) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(label || 'timeout')), ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); },
);
});
}
function openDb() {
return withTimeout(new Promise((resolve, reject) => {
const req = indexedDB.open(dbName, 1);
req.onupgradeneeded = () => req.result.createObjectStore(storeName);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
}), 1500, 'cache open timeout');
}
async function cacheGet(key) {
try {
const db = await openDb();
return await withTimeout(new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readonly');
const req = tx.objectStore(storeName).get(key);
req.onsuccess = () => resolve(req.result || null);
req.onerror = () => reject(req.error);
}), 1500, 'cache read timeout');
} catch {
return null;
}
}
async function cacheSet(key, value) {
try {
if (key.startsWith('threads:')) {
localStorage.setItem(LOCAL_THREADS_KEY + ':' + key.slice(8), JSON.stringify(value));
}
} catch {
try { localStorage.removeItem(LOCAL_THREADS_KEY + ':' + key.slice(8)); } catch {}
}
try {
const db = await openDb();
await withTimeout(new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite');
tx.objectStore(storeName).put(value, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
}), 1500, 'cache write timeout');
} catch {}
}
function readLocalThreads(direction) {
try {
const raw = localStorage.getItem(LOCAL_THREADS_KEY + ':' + direction);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || !Array.isArray(parsed.threads)) return null;
return parsed;
} catch {
return null;
}
}
function setStatus(text) {
if (els.statusText) els.statusText.textContent = text;
}
function setUpdating(updating) {
els.actions?.classList.toggle('is-updating', updating);
els.list?.setAttribute('aria-busy', updating ? 'true' : 'false');
}
function countLabel(total, direction) {
const noun = total === 1 ? 'thread' : 'threads';
const box = direction === 'outbound' ? 'sent' : 'received';
return total + ' ' + box + ' ' + noun;
}
function syncScrollLock() {
const open = Boolean(
(els.sheet && !els.sheet.hidden) || (els.aboutSheet && !els.aboutSheet.hidden),
);
document.documentElement.classList.toggle('is-modal-open', open);
}
function closeAbout() {
state.aboutOpen = false;
if (els.aboutSheet) els.aboutSheet.hidden = true;
syncScrollLock();
}
function closeSheet() {
if (els.sheet) els.sheet.hidden = true;
state.selectedId = null;
renderList();
syncScrollLock();
}
function escapeHtml(value) {
return String(value ?? '')
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"');
}
function escapeAttr(value) {
return String(value ?? '').replaceAll('&', '&').replaceAll('"', '"');
}
function formatWhen(iso) {
if (!iso) return '';
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return iso;
return date.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
}
function visibleThreads() {
return filterThreads(state.threads, state.query);
}
function renderMailbox() {
els.mailbox?.querySelectorAll('[data-direction]').forEach((button) => {
button.setAttribute('aria-current', button.getAttribute('data-direction') === state.direction ? 'true' : 'false');
});
}
function renderList() {
const threads = visibleThreads();
if (!els.list) return;
if (threads.length === 0) {
els.list.innerHTML = '<p class="empty">No ' + (state.direction === 'outbound' ? 'sent' : 'received') + ' threads on this device yet.</p>';
return;
}
els.list.innerHTML = threads.map((thread) => {
const people = (thread.participants || []).slice(0, 3).join(', ');
return \`<button class="card" type="button" data-id="\${escapeHtml(thread.id)}" aria-current="\${thread.id === state.selectedId ? 'true' : 'false'}">
<div class="card-meta"><span>\${escapeHtml(formatWhen(thread.latestAt))}</span><span>\${escapeHtml(String(thread.messageCount || 1))} msg</span></div>
<h2>\${escapeHtml(thread.subject || '(no subject)')}</h2>
<p>\${escapeHtml(people)}\${thread.snippet ? ' · ' + escapeHtml(thread.snippet) : ''}</p>
</button>\`;
}).join('');
}
async function copyText(text, button) {
try {
await navigator.clipboard.writeText(text);
} catch {
const area = document.createElement('textarea');
area.value = text;
document.body.appendChild(area);
area.select();
document.execCommand('copy');
area.remove();
}
if (button) {
const prior = button.textContent;
button.textContent = 'Copied';
setTimeout(() => { button.textContent = prior; }, 1200);
}
}
function renderSheet(thread) {
if (!els.sheet) return;
els.sheet.hidden = false;
const messages = thread.messages || [];
const blocks = messages.map((message) => {
const body = textFromMessage(message) || '';
const html = message.html_body
? \`<div class="preview"><iframe title="Email preview" sandbox srcdoc="\${escapeAttr(message.html_body)}"></iframe></div>\`
: '';
return \`<article class="message" data-message-id="\${escapeHtml(message.id)}">
<h3>\${escapeHtml(message.direction === 'outbound' ? 'Sent' : 'Received')} · \${escapeHtml(formatWhen(messageWhen(message)))}</h3>
<p class="meta">\${escapeHtml(message.from_address || '')} → \${escapeHtml((message.to_addresses || []).join(', '))}</p>
<pre class="message-body">\${escapeHtml(body)}</pre>
\${html}
<button class="copy" type="button" data-copy-message="\${escapeHtml(message.id)}">Copy prompt</button>
</article>\`;
}).join('');
els.sheet.innerHTML = \`
<div class="sheet-card">
<div class="sheet-toolbar">
<button class="ghost" type="button" data-close>Close</button>
<button class="copy" type="button" data-copy-thread>Copy thread prompt</button>
</div>
<h2>\${escapeHtml(thread.subject || '(no subject)')}</h2>
<p class="meta">\${escapeHtml(String(messages.length))} messages · read only</p>
\${blocks || '<p class="empty">No messages in this thread.</p>'}
</div>\`;
syncScrollLock();
}
async function openThread(id) {
if (!id) return;
state.selectedId = id;
renderList();
const cached = await cacheGet('thread:' + id);
if (cached?.messages) renderSheet(cached);
try {
const response = await fetch(api('api/threads/' + encodeURIComponent(id)), { cache: 'no-store' });
if (!response.ok) throw new Error('thread ' + response.status);
const thread = await response.json();
await cacheSet('thread:' + id, thread);
renderSheet(thread);
} catch {
if (!cached) renderSheet({ subject: 'Unavailable offline', messages: [] });
}
}
async function promptFor(kind, id) {
const cached = kind === 'thread' ? await cacheGet('thread:' + id) : null;
if (kind === 'thread' && cached?.messages) {
return buildThreadPrompt({ id, subject: cached.subject, messages: cached.messages });
}
if (kind === 'message' && cached?.messages) {
const message = cached.messages.find((item) => item.id === id);
if (message) return buildMessagePrompt(message);
}
const params = kind === 'thread' ? ('thread=' + encodeURIComponent(id)) : ('message=' + encodeURIComponent(id));
const response = await fetch(api('api/prompt?' + params), { cache: 'no-store' });
if (!response.ok) throw new Error('prompt ' + response.status);
const payload = await response.json();
return payload.prompt || '';
}
async function loadVersion() {
try {
const response = await fetch(api('api/version'), { cache: 'no-store' });
if (!response.ok) throw new Error('version ' + response.status);
return await response.json();
} catch {
const sha = document.documentElement.dataset.runningSha || '';
return { sha, shortSha: sha.slice(0, 7), message: '', committedAt: '', publishedAt: '' };
}
}
function inboxLine() {
const first = (state.inboxes || [])[0];
const address = first?.addresses?.[0]?.address;
return address ? 'Mail for ' + address : 'Your Kody mailbox';
}
function renderAbout(version, checking) {
if (!els.aboutSheet) return;
const runningSha = document.documentElement.dataset.runningSha || version.sha || '';
const latestSha = version.sha || runningSha;
state.aboutOpen = true;
els.aboutSheet.hidden = false;
els.aboutSheet.innerHTML = \`
<div class="sheet-card">
<button class="ghost" type="button" data-close>Close</button>
<h2>About Mailroom</h2>
<p class="meta">Read-only client for this account's Kody mailbox. Fork the package so the hosted app is yours. Agents send email; this app does not.</p>
<dl class="about-dl">
<div><dt>Mailbox</dt><dd>\${escapeHtml(inboxLine())}</dd></div>
<div><dt>Running version</dt><dd>\${escapeHtml(runningSha ? runningSha.slice(0, 7) : 'Not recorded yet')}</dd></div>
<div><dt>Latest published</dt><dd>\${escapeHtml(latestSha ? latestSha.slice(0, 7) : 'Not recorded yet')}</dd></div>
<div><dt>Commit message</dt><dd>\${escapeHtml(version.message || 'Unknown')}</dd></div>
<div><dt>Committed</dt><dd>\${escapeHtml(version.committedAt ? formatWhen(version.committedAt) : 'Unknown')}</dd></div>
<div><dt>Published</dt><dd>\${escapeHtml(version.publishedAt ? formatWhen(version.publishedAt) : 'Unknown')}</dd></div>
<div><dt>Cache</dt><dd>IndexedDB holds the last threads and messages from the server.</dd></div>
</dl>
<div class="about-actions">
<button class="install" type="button" data-check-updates \${checking ? 'disabled' : ''}>\${checking ? 'Checking…' : 'Check for updates'}</button>
</div>
</div>\`;
els.aboutSheet.querySelector('[data-close]')?.addEventListener('click', closeAbout);
els.aboutSheet.querySelector('[data-check-updates]')?.addEventListener('click', () => checkForUpdates(true));
syncScrollLock();
}
function showToast(title, text, actionLabel, action) {
if (!els.toast) return;
els.toast.hidden = false;
if (els.toastTitle) els.toastTitle.textContent = title;
if (els.toastText) els.toastText.textContent = text;
if (els.toastAction) {
els.toastAction.textContent = actionLabel;
els.toastAction.onclick = action;
}
}
function applyUpdate() {
const worker = state.waitingWorker;
if (worker) worker.postMessage('SKIP_WAITING');
else location.reload();
}
async function checkForUpdates(fromAbout) {
if (!fromAbout && !shouldRunUpdateCheck({
visible: document.visibilityState === 'visible',
inFlight: updateCheck.inFlight,
lastStartedAt: updateCheck.lastStartedAt,
now: Date.now(),
minIntervalMs: UPDATE_CHECK_MIN_INTERVAL_MS,
})) return;
updateCheck.inFlight = true;
updateCheck.lastStartedAt = Date.now();
try {
if (fromAbout) renderAbout(await loadVersion(), true);
const registration = await ensureServiceWorker();
const [version] = await Promise.all([
loadVersion(),
registration?.update?.() || Promise.resolve(),
]);
const waiting = state.waitingWorker || registration?.waiting || null;
if (waiting) state.waitingWorker = waiting;
if (fromAbout) renderAbout(version, false);
const available = describeAvailableUpdate({
runningSha: document.documentElement.dataset.runningSha || '',
latestSha: version.sha || '',
hasWaitingWorker: Boolean(waiting),
});
if (available === 'worker') {
showToast('Update available', 'A newer Mailroom app is ready. Reload to download it.', 'Reload', applyUpdate);
return;
}
if (available === 'published') {
showToast(
'Update available',
'Latest published commit is ' + (version.shortSha || version.sha.slice(0, 7)) + '. Reload to download it.',
'Reload',
() => location.reload(),
);
}
} finally {
updateCheck.inFlight = false;
}
}
function onAppFocused() {
checkForUpdates(false);
refreshInbox({ silent: true });
}
function startInboxPoll() {
if (inboxPollTimer) return;
inboxPollTimer = setInterval(() => {
if (document.visibilityState !== 'visible') return;
pollInbox();
}, INBOX_POLL_INTERVAL_MS);
}
async function pollInbox() {
if (!shouldRefreshInbox({
visible: document.visibilityState === 'visible',
inFlight: inboxCheck.inFlight,
lastStartedAt: inboxCheck.lastStartedAt,
now: Date.now(),
minIntervalMs: INBOX_REFRESH_MIN_INTERVAL_MS,
})) return;
inboxCheck.inFlight = true;
inboxCheck.lastStartedAt = Date.now();
try {
const response = await fetch(api('api/status'), { cache: 'no-store' });
if (!response.ok) return;
const status = await response.json();
const side = status[state.direction] || {};
if (inboxHasNewMail({
previousTotal: state.total,
nextTotal: Number(side.total ?? 0),
previousLatestSentAt: state.latestAt,
nextLatestSentAt: side.latestAt || null,
})) {
await refresh({ silent: true, announce: true });
}
} catch {
} finally {
inboxCheck.inFlight = false;
}
}
async function refreshInbox(options) {
if (!shouldRefreshInbox({
visible: document.visibilityState === 'visible',
inFlight: inboxCheck.inFlight,
lastStartedAt: inboxCheck.lastStartedAt,
now: Date.now(),
minIntervalMs: options?.force ? 0 : INBOX_REFRESH_MIN_INTERVAL_MS,
}) && !options?.force) return;
await refresh(options);
}
function openDeepLinked() {
const found = parseDeepLink({ search: location.search, hash: location.hash });
if (found.threadId) openThread(found.threadId);
else if (found.messageId) openThread(found.messageId);
}
function isStandaloneDisplay() {
return window.matchMedia('(display-mode: standalone)').matches || navigator.standalone === true;
}
function isIosBrowser() {
if (/iphone|ipad|ipod/i.test(navigator.userAgent)) return true;
return navigator.platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1;
}
function installStepsHtml() {
if (isIosBrowser()) {
return '<ol><li>Tap <strong>Share</strong></li><li>Tap <strong>Add to Home Screen</strong></li><li>Tap <strong>Add</strong></li></ol>';
}
return '<p>Use the install icon in the address bar, or the browser menu, then choose <strong>Install Mailroom</strong>.</p>';
}
function syncInstallUi() {
const ios = isIosBrowser();
const standalone = isStandaloneDisplay();
if (els.installWrap) els.installWrap.hidden = standalone;
if (els.install) els.install.hidden = standalone || !deferredInstall;
if (els.installSteps) els.installSteps.innerHTML = installStepsHtml();
if (els.installHint) {
let dismissed = false;
try { dismissed = localStorage.getItem('mailroom-install-hint-dismissed') !== null; } catch {}
els.installHint.hidden = standalone || !ios || dismissed;
}
}
async function ensureServiceWorker() {
if (!('serviceWorker' in navigator)) return null;
if (state.registration) return state.registration;
try {
state.registration = await navigator.serviceWorker.register(api('sw.js'), {
scope: serviceWorkerScope(base || '/', location.origin),
updateViaCache: 'none',
});
const track = (worker) => {
if (!worker) return;
worker.addEventListener('statechange', () => {
if (worker.state === 'installed' && navigator.serviceWorker.controller) {
state.waitingWorker = worker;
showToast('Update available', 'A newer Mailroom app is ready.', 'Reload', applyUpdate);
}
});
};
track(state.registration.installing);
state.registration.addEventListener('updatefound', () => track(state.registration.installing));
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (state.waitingWorker) location.reload();
});
return state.registration;
} catch {
return null;
}
}
async function promptInstall() {
const promptEvent = deferredInstall;
if (promptEvent?.prompt) {
deferredInstall = null;
try {
await promptEvent.prompt();
await promptEvent.userChoice;
syncInstallUi();
return true;
} catch {
syncInstallUi();
return false;
}
}
if (typeof navigator.install === 'function') {
try {
await navigator.install();
syncInstallUi();
return true;
} catch {
syncInstallUi();
return false;
}
}
return false;
}
async function refresh(options) {
const silent = Boolean(options?.silent);
const announce = Boolean(options?.announce);
const previousTotal = state.total;
const previousLatest = state.latestAt;
const cacheKey = 'threads:' + state.direction;
const cached = (await cacheGet(cacheKey)) || readLocalThreads(state.direction);
if (cached?.threads) {
state.threads = cached.threads;
state.total = cached.total ?? cached.threads.length;
state.latestAt = cached.threads[0]?.latestAt || null;
state.inboxes = cached.inboxes || state.inboxes;
renderMailbox();
renderList();
setStatus(countLabel(state.total, state.direction));
}
if (!silent) setUpdating(true);
inboxCheck.inFlight = true;
inboxCheck.lastStartedAt = Date.now();
try {
const response = await fetch(api('api/threads?direction=' + encodeURIComponent(state.direction)), { cache: 'no-store' });
if (!response.ok) throw new Error('list ' + response.status);
const payload = await response.json();
state.threads = payload.threads || [];
state.total = payload.total ?? state.threads.length;
state.latestAt = state.threads[0]?.latestAt || null;
state.inboxes = payload.inboxes || state.inboxes;
await cacheSet(cacheKey, payload);
renderMailbox();
renderList();
setStatus(countLabel(state.total, state.direction));
if (announce && inboxHasNewMail({
previousTotal,
nextTotal: state.total,
previousLatestSentAt: previousLatest,
nextLatestSentAt: state.latestAt,
})) {
const newest = state.threads[0];
showToast(
state.direction === 'outbound' ? 'New sent thread' : 'New received thread',
newest?.subject || 'New mail is on the server.',
newest ? 'Open' : 'Dismiss',
newest ? () => openThread(newest.id) : () => { if (els.toast) els.toast.hidden = true; },
);
}
} catch {
if (state.threads.length) setStatus(countLabel(state.threads.length, state.direction) + ' · offline');
else setStatus('Offline · no cached mail yet');
} finally {
if (!silent) setUpdating(false);
inboxCheck.inFlight = false;
}
}
els.mailbox?.addEventListener('click', (event) => {
const button = event.target.closest('[data-direction]');
if (!button) return;
const next = button.getAttribute('data-direction');
if (next !== 'inbound' && next !== 'outbound') return;
if (next === state.direction) return;
state.direction = next;
state.selectedId = null;
renderMailbox();
refresh({ force: true });
});
els.list?.addEventListener('click', (event) => {
const button = event.target.closest('[data-id]');
if (button) openThread(button.getAttribute('data-id'));
});
els.search?.addEventListener('input', () => {
state.query = els.search.value;
renderList();
});
els.sheet?.addEventListener('click', async (event) => {
if (event.target === els.sheet || event.target.closest('[data-close]')) {
closeSheet();
return;
}
const threadBtn = event.target.closest('[data-copy-thread]');
if (threadBtn && state.selectedId) {
try { await copyText(await promptFor('thread', state.selectedId), threadBtn); } catch {}
return;
}
const msgBtn = event.target.closest('[data-copy-message]');
if (msgBtn) {
try { await copyText(await promptFor('message', msgBtn.getAttribute('data-copy-message')), msgBtn); } catch {}
}
});
els.aboutSheet?.addEventListener('click', (event) => {
if (event.target === els.aboutSheet || event.target.closest('[data-close]')) closeAbout();
});
els.about?.addEventListener('click', async () => {
renderAbout(await loadVersion(), false);
});
els.toastDismiss?.addEventListener('click', () => { if (els.toast) els.toast.hidden = true; });
els.installOpen?.addEventListener('click', (event) => {
if (!els.installPopover) return;
if (typeof els.installPopover.showPopover === 'function') return;
event.preventDefault();
els.installPopover.classList.toggle('is-open');
});
els.install?.addEventListener('click', promptInstall);
els.installHintDismiss?.addEventListener('click', () => {
try { localStorage.setItem('mailroom-install-hint-dismissed', String(Date.now())); } catch {}
syncInstallUi();
});
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') return;
if (els.aboutSheet && !els.aboutSheet.hidden) closeAbout();
else if (els.sheet && !els.sheet.hidden) closeSheet();
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') onAppFocused();
});
window.addEventListener('pageshow', onAppFocused);
window.addEventListener('focus', onAppFocused);
window.addEventListener('hashchange', openDeepLinked);
syncInstallUi();
ensureServiceWorker().then(() => onAppFocused());
startInboxPoll();
refresh({ force: true }).then(openDeepLinked);
})();
`