Mail: swipe a message left to delete, right to archive. Books: page flip by tap and swipe works on touch screens

The page frame is wider than the screen (pages sit side by side inside it), so tap positions
were measured against the wrong width and taps on touch devices never turned the page.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj
This commit is contained in:
Claude committed 2026-10-24 20:15:53 +00:00
1 parent 34a619b235
commit 8acf9418dc
4 files changed
+56 -3

No files matched your search

+45 -1
View File
@@ -243,7 +243,7 @@ async function mailLoadList(reset) {
}
function mailMarkSel(id) { document.querySelectorAll('a.mrow').forEach(a => a.classList.toggle('sel', !!id && decodeURIComponent(a.getAttribute('href').slice(9)) === id)); }
function mailDrawList() {
const rows = $('#mrows'); if (!rows) return;
const rows = $('#mrows'); if (!rows) return; mailSwipeInit(rows);
const sel = (location.hash.match(/^#\/mail\/m\/([^/]+)/) || [])[1];
rows.innerHTML = mail.items.length ? mail.items.map(m => `<a class="row mrow${m.read ? '' : ' unread'}${sel && decodeURIComponent(sel) === m.id ? ' sel' : ''}" href="#/mail/m/${encodeURIComponent(m.id)}">
<span class="grow"><b>${esc(mailFrom(m.from))}</b><span class="msub">${esc(m.subject)}</span><small>${esc(m.preview)}</small></span>
@@ -252,6 +252,50 @@ function mailDrawList() {
if ($('#mmb')) $('#mmb').onclick = () => mailLoadList(false);
}
// ---- Swipe a message in the list: left deletes it, right archives it (touch screens). The row follows your finger and the colour under it shows what will happen;
// let go past a third of the way (or flick it) and it leaves at once and finishes in the background, otherwise it springs back.
function mailSwipeInit(rows) {
if (rows.dataset.swipe) return; rows.dataset.swipe = '1';
let row = null, x0 = 0, y0 = 0, dx = 0, t0 = 0, mode = '', w = 0;
const reset = (anim) => { if (!row) return; const r = row; r.style.transition = anim ? 'transform .18s ease-out' : ''; r.style.transform = ''; setTimeout(() => { r.classList.remove('swl', 'swr'); r.style.transition = ''; r.style.removeProperty('--sww'); }, anim ? 200 : 0); };
rows.addEventListener('touchstart', e => {
const a = e.target.closest && e.target.closest('a.mrow'); if (!a || e.touches.length !== 1) { row = null; return; }
row = a; x0 = e.touches[0].clientX; y0 = e.touches[0].clientY; dx = 0; mode = ''; t0 = Date.now(); w = a.offsetWidth;
}, { passive: true });
rows.addEventListener('touchmove', e => {
if (!row) return; const t = e.touches[0], mx = t.clientX - x0, my = t.clientY - y0;
if (!mode) { if (Math.abs(my) > 12 && Math.abs(my) > Math.abs(mx)) { row = null; return; } if (Math.abs(mx) > 12 && Math.abs(mx) > Math.abs(my) * 1.4) { mode = 'swipe'; row.style.setProperty('--sww', w + 'px'); } else return; }
dx = mx; row.classList.toggle('swl', dx < 0); row.classList.toggle('swr', dx > 0); row.style.transition = ''; row.style.transform = `translateX(${dx}px)`;
row.dataset.sw = dx < 0 ? 'Delete' : 'Archive'; row.classList.toggle('swgo', Math.abs(dx) > w / 3);
}, { passive: true });
const end = () => {
if (!row) return; const r = row; row = null;
if (mode !== 'swipe') return;
const fast = Math.abs(dx) > 60 && Date.now() - t0 < 250; r.dataset.swiped = '1'; setTimeout(() => delete r.dataset.swiped, 400);
if (Math.abs(dx) > w / 3 || fast) {
const id = decodeURIComponent(r.getAttribute('href').slice(9)), kind = dx < 0 ? 'delete' : 'archive';
if (kind === 'delete' && /(^|~)deleteditems$/.test(mail.folder)) { row = r; reset(true); row = null; const it = mail.items.find(x => x.id === id); return confirmSheet('Delete for good?', it ? it.subject : '', 'Delete', () => mailRowAway(id, kind)); }
r.style.transition = 'transform .16s ease-in, opacity .16s'; r.style.transform = `translateX(${dx < 0 ? -w : w}px)`; r.style.opacity = '0.2';
setTimeout(() => mailRowAway(id, kind), 160);
} else { row = r; reset(true); row = null; }
};
rows.addEventListener('touchend', end); rows.addEventListener('touchcancel', () => { if (row) { reset(true); row = null; } });
rows.addEventListener('click', e => { const a = e.target.closest && e.target.closest('a.mrow'); if (a && a.dataset.swiped) { e.preventDefault(); e.stopPropagation(); } }, true); // the finger lifting after a swipe is not a tap
}
function mailRowAway(id, kind) {
const at = mail.items.findIndex(x => x.id === id), had = mail.items[at]; if (!had) return mailDrawList();
const permanent = kind === 'delete' && /(^|~)deleteditems$/.test(mail.folder);
mail.items.splice(at, 1);
const sv = mcGet('l:' + mail.folder); if (sv && sv.items) mcPut('l:' + mail.folder, { items: sv.items.filter(x => x.id !== id), more: sv.more, next: sv.next });
if (mail.msg && mail.msg.id === id) { mail.msg = null; location.hash = '#/mail/f/' + encodeURIComponent(mail.folder); }
mailDrawList();
if (!had.read && mailInInbox() && mailUnread > 0) { mailUnread--; renderNav(); }
toast(kind === 'archive' ? 'Archived' : permanent ? 'Deleted' : 'Moved to Deleted');
const refresh = () => { mailPoll(); jfetch('/api/mail/folders?acct=' + encodeURIComponent(mail.acct)).then(f => { mail.folders = f; mailRefreshFolders(); }).catch(() => {}); };
jfetch('/api/mail/' + (kind === 'archive' ? 'move' : 'delete'), { method: 'POST', body: JSON.stringify(kind === 'archive' ? { id, to: 'archive' } : { id, ...(permanent ? { permanent: true } : {}) }) })
.then(refresh, e => { toast(e.message); if (!had.read && mailInInbox()) { mailUnread++; renderNav(); } mail.items.splice(Math.min(at, mail.items.length), 0, had); mailDrawList(); refresh(); });
}
// Opened messages are also kept on this device (IndexedDB, the newest 80), so one you have read opens at once, even offline. A message's text never
// changes, so only its read/flagged state is taken fresh (from the list). Cleared with the rest of the saved mail when you disconnect or sign out.
const mbDB = () => mbDB.p || (mbDB.p = new Promise(ok => { try { const r = indexedDB.open('hub-mail', 1); r.onupgradeneeded = () => r.result.createObjectStore('b'); r.onsuccess = () => ok(r.result); r.onerror = () => ok(null); } catch { ok(null); } }));