diff options
Diffstat (limited to 'src/features/messages.js')
| -rw-r--r-- | src/features/messages.js | 40 |
1 files changed, 28 insertions, 12 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 88e032a..ffdf0e3 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -4039,10 +4039,20 @@ export function messagesFeature(ctx) { // fetch answers for a thread fetched ahead before the answer came function adoptRepliesInHand(c) { const have = new Set(c.replies.map((e) => e.id)); - let added = 0; - for (const e of repliesInHand(c.rootId)) if (!have.has(e.id) && e.id !== c.rootId) { have.add(e.id); c.replies.push(e); noteForSpam(e); added++; } - if (added) c.replies.sort((a, b) => a.created_at - b.created_at); - return added; + let added = []; + for (const e of repliesInHand(c.rootId)) if (!have.has(e.id) && e.id !== c.rootId) { have.add(e.id); c.replies.push(e); noteForSpam(e); added.push(e); } + if (added.length) { c.replies.sort((a, b) => a.created_at - b.created_at); warmThread(added); } + return added.length; + } + // A thread's rows painted in their final shape: the faces, pictures, + // previews and quotes of what is in hand are fetched ahead (noteReady, as + // the feed does per row), so an opened thread doesn't shift as they land. + const threadWarmed = new WeakSet(); + const THREAD_ROW_MS = 1500; // a reply arriving from a relay waits this long at most for its face + function warmThread(evs, deadline = Date.now() + READY_MS) { + const fresh = evs.filter((e) => e && !threadWarmed.has(e)); + for (const e of fresh) threadWarmed.add(e); + return Promise.all(fresh.map((e) => noteReady(e, deadline).catch(() => {}))); } const reacts = new Map(); // note id -> Map([emoji, url] -> { emoji, url, who: Set<pubkey> }) // Resolve from this reaction's own tag: packs can reuse the same shortcode @@ -10289,6 +10299,7 @@ export function messagesFeature(ctx) { adoptRepliesInHand(c); if (seed.id !== rootId && !c.replies.some((e) => e.id === seed.id)) c.replies.push(seed); c.replies.sort((a, b) => a.created_at - b.created_at); + warmThread([c.root, ...c.replies]); threadCache.set(requestedRootId, c); threadCache.set(rootId, c); if (ui.noteThread?.focusId === seed.id) ui.noteThread.rootId = rootId; @@ -10311,11 +10322,16 @@ export function messagesFeature(ctx) { let paintT = null; const paint = () => { if (paintT || !onScreen()) return; paintT = setTimeout(() => { paintT = null; if (onScreen()) renderThreadStable(); }, 150); }; const got = new Set(c.replies.map((e) => e.id)); + const streamed = new Map(); const take = (e) => { if (!e || e.id === c.rootId || got.has(e.id)) return; - got.add(e.id); noteForSpam(e); - c.replies = [...c.replies, e].sort((a, b) => a.created_at - b.created_at); - paint(); + got.add(e.id); noteForSpam(e); streamed.set(e.id, e); + // painted whole: its face and pictures first (briefly), then the row + warmThread([e], Date.now() + THREAD_ROW_MS).then(() => { + if (c.replies.some((x) => x.id === e.id)) return; + c.replies = [...c.replies, e].sort((a, b) => a.created_at - b.created_at); + paint(); + }); }; await Promise.all([ c.root ? Promise.resolve() : queryOn(relays, { ids: [rootId] }, 4000).then((roots) => { // a root can be a poll @@ -10326,7 +10342,7 @@ export function messagesFeature(ctx) { queryStreamOn(relays, { kinds: [COMMENT], '#E': [rootId], limit: 120 }, take, 4500).catch(() => {}), ]); clearTimeout(paintT); paintT = null; - let all = [...c.replies]; + let all = [...c.replies, ...streamed.values()]; // A client that tags only the note it answered leaves the root // unnamed: what we resolved as the root is itself a reply. Climb — // fetch ITS root and re-root the thread there — a few hops at most. @@ -10690,10 +10706,10 @@ export function messagesFeature(ctx) { } }; walk(c.rootId, 0); - // the spinner only while there is nothing to read yet: replies in hand - // (or arriving) are the conversation, and the rest slot in as they come - if (c.status === 'loading' && !c.replies.length) kids.push(h('div', { class: 'row', style: 'justify-content:center;padding:12px' }, h('span', { class: 'spinner sm' }))); - else if (c.status !== 'loading' && !c.replies.length) kids.push(h('div', { class: 'small faint', style: 'text-align:center;padding:10px 0' }, t('threadNoReplies'))); + // No spinner: the post reads at once and its replies come in whole as + // they arrive (Adam: rather have them pop in than wait to learn there + // are none); "no replies" only once the relays have spoken. + if (c.status !== 'loading' && !c.replies.length) kids.push(h('div', { class: 'small faint', style: 'text-align:center;padding:10px 0' }, t('threadNoReplies'))); if (!boxPlaced) kids.push(replyBox(false)); // A tapped reply deep in a long thread has to be ON SCREEN when the // thread opens — highlighting it somewhere below the fold reads as the |
