From b0d60d0a5b0856c798a3ad0c686fe3b2e9bd7e5b Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:11:53 -0400 Subject: [PATCH] Fix chat sliding when the iPhone keyboard opens Size the shell to the visible area while a field is focused, tuck the bottom bar away, and refit the chat as the keyboard comes and goes. Co-Authored-By: Claude Sonnet 5.5 --- public/chat-view.js | 5 +++-- public/shell.js | 19 +++++++++++++++++++ public/style.css | 8 ++++++++ public/sw.js | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/public/chat-view.js b/public/chat-view.js index d94172d..fa0e5a6 100644 --- a/public/chat-view.js +++ b/public/chat-view.js @@ -345,10 +345,11 @@ async function chAnnounceCard(el) { function chFit() { const m = chMounted() && ch.root.querySelector('.sgm'); if (!m || m.offsetParent === null) return; if (window.innerWidth >= 1024) { m.style.height = ''; return; } - let bottom = window.visualViewport ? window.visualViewport.height : window.innerHeight; + let bottom = window.visualViewport ? window.visualViewport.offsetTop + window.visualViewport.height : window.innerHeight; // (page coordinates: the visible area can be panned down while the keyboard is up) const top0 = m.getBoundingClientRect().top; // (look up from the screen's edge for the first spot that is page content, not the bottom bar or mini player) for (let y = Math.floor(bottom) - 2; y > top0 + 130; y -= 4) { const hit = document.elementsFromPoint(innerWidth / 2, y).find(e => !e.closest('.sgm')); const covers = hit && !hit.closest('#view'); if (!covers) { bottom = y + 4; break; } } - const top = m.getBoundingClientRect().top; m.style.height = Math.max(260, Math.floor(bottom - top - 10)) + 'px'; + const top = m.getBoundingClientRect().top; m.style.height = Math.max(document.body.classList.contains('kb') ? 150 : 260, Math.floor(bottom - top - 10)) + 'px'; } +window.addEventListener('kbfit', () => { chFit(); if (!chMounted() || !chq('.sgm')) return; const v = document.getElementById('view'); if (v) v.scrollTop = 0; const l = chq('#chmsgs'); if (l && document.activeElement && document.activeElement.id === 'chtext') l.scrollTop = l.scrollHeight; }); // (the keyboard came or went: refit, and keep the newest message in sight) window.addEventListener('resize', chFit); if (window.visualViewport) window.visualViewport.addEventListener('resize', chFit); setInterval(chFit, 700); diff --git a/public/shell.js b/public/shell.js index 83f80b1..5f55a26 100644 --- a/public/shell.js +++ b/public/shell.js @@ -68,3 +68,22 @@ else if (!e.shiftKey && (document.activeElement === last || !s.contains(document.activeElement))) { e.preventDefault(); first.focus(); } }); })(); + +// ---- iPhone keyboard: iOS shrinks only the visual viewport when the keyboard opens and pans the whole page to reveal the field, so the app used to slide up with it. +// Pin the shell to what is really visible while typing and tuck the bottom bar away; chat and mail size themselves again on 'kbfit'. +(function () { + const vv = window.visualViewport; if (!vv || !matchMedia('(pointer: coarse)').matches) return; + const root = document.documentElement; let raf = 0; + const typing = () => { const a = document.activeElement; return !!a && (a.tagName === 'TEXTAREA' || a.isContentEditable || (a.tagName === 'INPUT' && !/^(checkbox|radio|button|submit|reset|range|file|color|image)$/.test(a.type))); }; + function apply() { + raf = 0; + const kb = typing() && window.innerHeight - vv.height > 120 && window.innerWidth < 768; + document.body.classList.toggle('kb', kb); + if (kb) { root.style.setProperty('--vvh', vv.height + 'px'); root.style.setProperty('--vvt', vv.offsetTop + 'px'); if (window.scrollY) window.scrollTo(0, 0); } + else { root.style.removeProperty('--vvh'); root.style.removeProperty('--vvt'); } + window.dispatchEvent(new Event('kbfit')); + } + const soon = () => { if (!raf) raf = requestAnimationFrame(apply); }; + vv.addEventListener('resize', soon); vv.addEventListener('scroll', soon); + document.addEventListener('focusin', soon); document.addEventListener('focusout', () => setTimeout(soon, 80)); +})(); diff --git a/public/style.css b/public/style.css index cdc5421..27506c5 100644 --- a/public/style.css +++ b/public/style.css @@ -264,6 +264,14 @@ body { -webkit-touch-callout: none; } button, a, .row { -webkit-user-select: none; user-select: none; } input, textarea, [contenteditable], .sheet p, .card p, #view .sub { -webkit-user-select: text; user-select: text; } +/* iPhone keyboard (shell.js adds body.kb while a field is focused and the keyboard is up): the shell is exactly the visible area, with no bottom bar in the way */ +@media (max-width: 767px) { + body.kb { overflow: hidden; } + body.kb #shell { height: var(--vvh, 100dvh); transform: translateY(var(--vvt, 0px)); } + body.kb #nav, body.kb #player, body.kb #upd, body.kb #toast { display: none; } + body.kb .sgm { min-height: 0; } +} + /* "A new version is available" notice */ #upd { position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(var(--nav-h) + var(--sab) + 70px); z-index: 30; display: flex; align-items: center; gap: 12px; background: var(--text); color: var(--bg); padding: 8px 8px 8px 16px; border-radius: 14px; box-shadow: 0 6px 24px rgba(0,0,0,.35); max-width: 92vw; } diff --git a/public/sw.js b/public/sw.js index 1e476fc..8d5ae10 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ // App-shell cache only; Seafile/API traffic always goes to the network. -const CACHE = 'hub-v88'; +const CACHE = 'hub-v89'; const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];