perf(mobile): paginate threads to 40 (was 500) — kills the heavy-DOM jank at the source
User's insight: the jank/jumps come from rendering ALL ~500 messages on open. Load only the latest 40 (server + client PAGE), and the existing loadOlder() pages in older history on scroll-up with a scroll anchor (no jump). Whole-thread SEARCH is a separate endpoint, unaffected. Also #1: keyboard-show only pins to newest when already near the bottom, so replying to an OLD message no longer yanks to latest. Build marker -> 2026-07-18-batch120. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1134,7 +1134,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="/icons.js?v=6"></script>
|
<script src="/icons.js?v=6"></script>
|
||||||
<script>window.__BUILD='2026-07-18-batch119';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
<script>window.__BUILD='2026-07-18-batch120';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||||
// Emoji are rendered with the OS's own (colour) emoji font — instant, zero network.
|
// Emoji are rendered with the OS's own (colour) emoji font — instant, zero network.
|
||||||
//
|
//
|
||||||
// We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from
|
// We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from
|
||||||
@@ -1155,7 +1155,7 @@ function twemojify(_el){ /* native emoji: nothing to do */ }</script>
|
|||||||
// gone (inputs are 16px), that height is clean, so we lift the composer by exactly it. keyboardWillShow
|
// gone (inputs are 16px), that height is clean, so we lift the composer by exactly it. keyboardWillShow
|
||||||
// fires at the START of the animation, so the CSS bottom-transition slides the composer up WITH the
|
// fires at the START of the animation, so the CSS bottom-transition slides the composer up WITH the
|
||||||
// keyboard (smooth). raw>innerHeight guards the rare device-pixel unit.
|
// keyboard (smooth). raw>innerHeight guards the rare device-pixel unit.
|
||||||
function setKb(px){ px=Math.max(0,Math.round(px)); r.style.setProperty('--kb',px+'px'); document.body.classList.toggle('kb-open',px>4); var b=document.getElementById('msgs'); if(b) b.scrollTop=b.scrollHeight; }
|
function setKb(px){ px=Math.max(0,Math.round(px)); r.style.setProperty('--kb',px+'px'); document.body.classList.toggle('kb-open',px>4); var b=document.getElementById('msgs'); if(b && (b.scrollHeight-b.scrollTop-b.clientHeight)<400) b.scrollTop=b.scrollHeight; } /* #1: only keep pinned to newest when ALREADY near the bottom — replying to an OLD message must not yank to the latest */
|
||||||
window.__bzResetKb=function(){ setKb(0); }; // called on composer blur as a safety net (#1: keyboard closed but lift stayed, leaving empty space after reply-cancel)
|
window.__bzResetKb=function(){ setKb(0); }; // called on composer blur as a safety net (#1: keyboard closed but lift stayed, leaving empty space after reply-cancel)
|
||||||
var KB=C.Plugins&&C.Plugins.Keyboard;
|
var KB=C.Plugins&&C.Plugins.Keyboard;
|
||||||
if(KB && KB.addListener){
|
if(KB && KB.addListener){
|
||||||
@@ -1500,7 +1500,7 @@ function enablePullRefresh(el, onRefresh){
|
|||||||
async function reloadThread(){ if(!selected) return; const kind=selected.kind, id=selected.id; const url=kind==='group'?('/api/messages/thread?group='+encodeURIComponent(id)):('/api/messages/thread?with='+encodeURIComponent(id)); try{ const msgs=await fetch(url).then(r=>r.json()); if(selected&&selected.kind===kind&&selected.id===id&&Array.isArray(msgs)){ THREAD=msgs; THREAD_CACHE.set(kind+':'+id, THREAD.slice()); renderThread(); } }catch(_){} }
|
async function reloadThread(){ if(!selected) return; const kind=selected.kind, id=selected.id; const url=kind==='group'?('/api/messages/thread?group='+encodeURIComponent(id)):('/api/messages/thread?with='+encodeURIComponent(id)); try{ const msgs=await fetch(url).then(r=>r.json()); if(selected&&selected.kind===kind&&selected.id===id&&Array.isArray(msgs)){ THREAD=msgs; THREAD_CACHE.set(kind+':'+id, THREAD.slice()); renderThread(); } }catch(_){} }
|
||||||
// ---- Older-history pagination: the thread loads the newest ~500; scrolling to the top fetches the
|
// ---- Older-history pagination: the thread loads the newest ~500; scrolling to the top fetches the
|
||||||
// previous page (messages older than the oldest one loaded) and prepends them, preserving scroll. ----
|
// previous page (messages older than the oldest one loaded) and prepends them, preserving scroll. ----
|
||||||
const PAGE=500;
|
const PAGE=40; // load the latest 40 on open (fast render, no 500-node jank); loadOlder() pages in older history on scroll-up. MUST match the server thread limit.
|
||||||
let _hasMoreOlder=false, _loadingOlder=false, _olderCd=0;
|
let _hasMoreOlder=false, _loadingOlder=false, _olderCd=0;
|
||||||
function threadUrl(kind,id,before){ return (kind==='group'?'/api/messages/thread?group='+encodeURIComponent(id):'/api/messages/thread?with='+encodeURIComponent(id))+(before?('&before='+before):''); }
|
function threadUrl(kind,id,before){ return (kind==='group'?'/api/messages/thread?group='+encodeURIComponent(id):'/api/messages/thread?with='+encodeURIComponent(id))+(before?('&before='+before):''); }
|
||||||
async function loadOlder(){
|
async function loadOlder(){
|
||||||
|
|||||||
+2
-2
@@ -818,7 +818,7 @@ route('GET', '/api/messages/thread', async (req, res) => {
|
|||||||
const group = q.get('group');
|
const group = q.get('group');
|
||||||
if (group) {
|
if (group) {
|
||||||
if (!R.conversations.isMember(group, u.id)) return json(res, 403, { error: 'not a member of this group' });
|
if (!R.conversations.isMember(group, u.id)) return json(res, 403, { error: 'not a member of this group' });
|
||||||
const rows = R.messages.threadByConversation(group, 500, before);
|
const rows = R.messages.threadByConversation(group, 40, before); // page size (latest 40 / older via ?before) — matches client PAGE for smooth open + lazy load
|
||||||
if (!peek && !before) {
|
if (!peek && !before) {
|
||||||
R.conversations.markRead(group, u.id);
|
R.conversations.markRead(group, u.id);
|
||||||
const evt = { type: 'group-read', group, by: u.id, byName: names[u.id] || u.email, at: now() };
|
const evt = { type: 'group-read', group, by: u.id, byName: names[u.id] || u.email, at: now() };
|
||||||
@@ -837,7 +837,7 @@ route('GET', '/api/messages/thread', async (req, res) => {
|
|||||||
const other = R.users.resolve(q.get('with')); // follow a merge redirect so a stale peer id still loads the thread
|
const other = R.users.resolve(q.get('with')); // follow a merge redirect so a stale peer id still loads the thread
|
||||||
if (!other) return json(res, 400, { error: 'with or group required' });
|
if (!other) return json(res, 400, { error: 'with or group required' });
|
||||||
if (!R.users.inTenant(other, u.team_id)) return json(res, 404, { error: 'no such contact' });
|
if (!R.users.inTenant(other, u.team_id)) return json(res, 404, { error: 'no such contact' });
|
||||||
const rows = R.messages.thread(u.team_id, u.id, other, 500, before);
|
const rows = R.messages.thread(u.team_id, u.id, other, 40, before); // page size (latest 40 / older via ?before) — matches client PAGE for smooth open + lazy load
|
||||||
if (!peek && !before) { R.messages.markRead(u.team_id, u.id, other); try { CHAT.pushToUser(other, { type: 'chat-read', by: u.id }); } catch (_) {} try { CHAT.pushToUser(u.id, { type: 'notif-clear', kind: 'dm', id: other }); } catch (_) {} } // #13
|
if (!peek && !before) { R.messages.markRead(u.team_id, u.id, other); try { CHAT.pushToUser(other, { type: 'chat-read', by: u.id }); } catch (_) {} try { CHAT.pushToUser(u.id, { type: 'notif-clear', kind: 'dm', id: other }); } catch (_) {} } // #13
|
||||||
const rxBy = groupReactions(R.reactions.forPair(u.team_id, u.id, other), u.id, names);
|
const rxBy = groupReactions(R.reactions.forPair(u.team_id, u.id, other), u.id, names);
|
||||||
return json(res, 200, rows.map((m) => { const d = buildMsgDTO(m, names, u.id); d.reactions = dtoReactions(rxBy, m.id); return d; }));
|
return json(res, 200, rows.map((m) => { const d = buildMsgDTO(m, names, u.id); d.reactions = dtoReactions(rxBy, m.id); return d; }));
|
||||||
|
|||||||
Reference in New Issue
Block a user