| index 874040a..fdfa15f 100644 |
| --- a/web/static/shell.js |
| +++ b/web/static/shell.js |
| @@ -756,16 +756,6 @@ const TIME_SEPARATOR_GAP_MS = 15 * 60 * 1000; |
| // The UI icon set (web/static/icons.js — Lucide idiom, docs/icons.md). |
| import { ICON_REPLY, ICON_X, ICON_EDIT, ICON_REACT, ICON_TRASH, ICON_PLAY, ICON_PIN, ICON_MUTE, ICON_REPORT, ICON_COPY, ICON_CHEVRON_LEFT, ICON_MORE_VERTICAL, ICON_BRAIN, ICON_WRENCH } from "/static/icons.js"; |
| |
| -// These five always ALIAS the open thread's feed-cache entry (see feedCache / |
| -// bindThreadState). All the render/update code operates on "the active thread" |
| -// through them; switching threads just re-points them at another cache entry. |
| -let msgEls = {}; // message id -> its .msg element (for live reaction/edit updates) |
| -let msgData = {}; // message id -> { mine, createdAt, data, replyMeta } (edit/reply) |
| -// Reply threading, keyed by the parent's message id: how many replies it has |
| -// (drives the "N Replies" tag) and the tag element to repaint as they arrive. |
| -let replyCounts = {}; |
| -let replyCountEls = {}; |
| -let myReactions = new Set(); // "<msgId>|<emoji>" the current user has reacted with |
| let editing = null; // message id being edited, or null |
| let replyingTo = null; // { id, name, snippet } being replied to, or null |
| let typeahead = null; // active composer autocomplete: { kind, start, items, index } or null |
| @@ -1356,9 +1346,7 @@ function installNativeBridge() { |
| // pinged about is the newest one, so jump to it even if the parked feed |
| // had been scrolled up. |
| didAttach(force) { |
| - if (chatAppNative) return chatAppNative.didAttach(force); |
| - const feed = document.getElementById("feed"); |
| - if (feed && (force || feedAtBottom)) pinFeedToBottom(feed, openSeq); |
| + if (chatAppNative) chatAppNative.didAttach(force); |
| }, |
| }; |
| } |
| @@ -3601,59 +3589,7 @@ function refreshAvatars() { |
| |
| let openSeq = 0; // bumped each openThread so a slow history load bails if you switch away |
| |
| -// Whether the user's last real scroll left the feed at (or within 80px of) the |
| -// bottom. Content growth fires no scroll event, so after messages arrive while |
| -// rendering is suspended (parked native webview, hidden PWA tab) this still says |
| -// "was at the bottom" — where an instantaneous gap check would misread the grown |
| -// gap as "scrolled up into history". Maintained by the feed scroll listener |
| -// (renderApp); every deliberate pin sets it true. Feeds open pinned, so it |
| -// starts true. |
| -let feedAtBottom = true; |
| - |
| -// Per-thread rendered-feed cache. Switching chats used to clear #feed and |
| -// re-fetch every time, which flashed empty for the network round-trip and reset |
| -// the scroll. Instead we keep each opened thread's message nodes alive: when you |
| -// leave a thread its nodes are parked in a DocumentFragment (images stay decoded, |
| -// listeners intact) and its scroll offset saved; re-opening re-attaches them |
| -// instantly, then catches up in the background. Each entry also owns the state |
| -// maps that index its nodes; the module globals alias the open entry's maps. |
| -const feedCache = {}; // threadId -> cache entry (see newFeedCache) |
| - |
| -function newFeedCache() { |
| - return { |
| - nodes: null, // parked message nodes while this thread isn't the open one |
| - scrollTop: null, // saved scroll offset (null → pin to bottom on first show) |
| - loaded: false, // has the initial history load completed at least once? |
| - msgEls: {}, |
| - msgData: {}, |
| - replyCounts: {}, |
| - replyCountEls: {}, |
| - myReactions: new Set(), |
| - }; |
| -} |
| - |
| -// Re-point the active-thread globals at a cache entry's state. |
| -function bindThreadState(c) { |
| - msgEls = c.msgEls; |
| - msgData = c.msgData; |
| - replyCounts = c.replyCounts; |
| - replyCountEls = c.replyCountEls; |
| - myReactions = c.myReactions; |
| -} |
| - |
| -// Detach the open thread's nodes into its cache entry (kept alive in a fragment) |
| -// and remember where it was scrolled, so another thread can take the #feed slot. |
| -function stashFeed(id) { |
| - const c = feedCache[id]; |
| - const feed = document.getElementById("feed"); |
| - if (!c || !feed) return; |
| - c.scrollTop = feed.scrollTop; |
| - const frag = document.createDocumentFragment(); |
| - while (feed.firstChild) frag.appendChild(feed.firstChild); |
| - c.nodes = frag; |
| -} |
| - |
| -// Per-thread full-pane app cache — the app-thread analogue of feedCache. A chat-app |
| +// Per-thread full-pane app cache. A chat-app |
| // or personal-app thread renders full-pane into #app-pane; opening one used to wipe |
| // the pane and re-fetch + re-fold its whole history before anything painted, so |
| // every open flashed blank for the network round-trip (the "rebuilt/reduced every |
| @@ -3938,10 +3874,7 @@ async function openThread(id) { |
| if (!isCoarse) document.getElementById("text").focus(); |
| return; |
| } |
| - if (activeId) { |
| - stashFeed(activeId); // park the outgoing thread's feed before the swap |
| - stashAppPane(activeId); // and its full-pane app root, if it was an app-thread |
| - } |
| + if (activeId) stashAppPane(activeId); // park the outgoing app-thread's full-pane root |
| |
| const myOpen = ++openSeq; |
| activeId = id; |
| @@ -3997,34 +3930,6 @@ async function openThread(id) { |
| setAppThreadMode(false); |
| } |
| |
| -// Pin the feed to the bottom, and keep it pinned for a short settle window as |
| -// images / link previews finish loading and grow the feed (otherwise opening a |
| -// chat lands a message or two short of the latest). Yields immediately if the |
| -// user scrolls up, or if they switch threads. |
| -function pinFeedToBottom(feed, gen) { |
| - if (!feed) return; |
| - feedAtBottom = true; // a deliberate pin IS "at the bottom", even if no scroll event fires |
| - feed.scrollTop = feed.scrollHeight; |
| - let active = true; |
| - const onScroll = () => { |
| - // A scroll event with the viewport well off the bottom means the user scrolled |
| - // up (content growth changes scrollHeight, which doesn't fire 'scroll'). |
| - if (feed.scrollHeight - feed.scrollTop - feed.clientHeight > 60) active = false; |
| - }; |
| - feed.addEventListener("scroll", onScroll, { passive: true }); |
| - const stopAt = Date.now() + 1500; |
| - const tick = () => { |
| - if (!active || gen !== openSeq || !document.body.contains(feed)) { |
| - feed.removeEventListener("scroll", onScroll); |
| - return; |
| - } |
| - feed.scrollTop = feed.scrollHeight; |
| - if (Date.now() < stopAt) requestAnimationFrame(tick); |
| - else feed.removeEventListener("scroll", onScroll); |
| - }; |
| - requestAnimationFrame(tick); |
| -} |
| - |
| // A brief status pill (reuses the #toast element). |
| let toastTimer; |
| function toast(msg) { |
| @@ -7282,19 +7187,6 @@ function renderActiveSurfaces() { |
| } else renderSurfaces(activeId, document.getElementById("app-surface")); |
| } |
| |
| -// The id of your most recent message that's still editable (yours, has text, and |
| -// within the edit window), or null. Drives ↑-to-edit. Message ids are ascending. |
| -function lastEditableMine() { |
| - let best = null; |
| - for (const [id, d] of Object.entries(msgData)) { |
| - if (d.mine && d.data && d.data.text && Date.now() - d.createdAt * 1000 < EDIT_WINDOW_MS) { |
| - const n = Number(id); |
| - if (best === null || n > best) best = n; |
| - } |
| - } |
| - return best; |
| -} |
| - |
| function cancelEdit() { |
| editing = null; |
| const banner = document.getElementById("composer-edit"); |
| @@ -9362,7 +9254,6 @@ async function closeChat() { |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ token: session.token, hidden: true }), |
| }).catch(() => {}); |
| - stashFeed(id); // keep its rendered feed cached for a quick reopen |
| activeId = null; |
| localStorage.removeItem(ACTIVE_KEY); // don't reopen a closed chat on refresh |
| renderThreadList(); |
| @@ -9392,7 +9283,6 @@ async function endConversation() { |
| } |
| dmClosed[id] = true; |
| hidden[id] = true; |
| - stashFeed(id); |
| activeId = null; |
| localStorage.removeItem(ACTIVE_KEY); |
| renderThreadList(); |
| @@ -10732,7 +10622,6 @@ async function cancelInvitedChat(threadId) { |
| delete inviteLinks[threadId]; |
| delete threadKeys[threadId]; |
| delete threadKeyB64[threadId]; |
| - delete feedCache[threadId]; |
| activeId = null; |
| await loadThreads(); |
| renderThreadList(); |