eleven / ocho Public

web: delete the dead feed-cache cluster + the #feed div — the retired feed's last residue (#919)

feedCache entries were never created (newFeedCache had zero callers), so
stashFeed always no-opped at its guard; the five alias maps (msgEls,
msgData, …) had zero writers; pinFeedToBottom only ran in the pre-mount
didAttach fallback against the empty, .app-mode-hidden #feed. The div and
its #feed ID rules (incl. the .native-embed insets — chat.js injects its
own for .chatapp-feed) go with them. The .feed CLASS rules stay: chat.js's
live feed wears .feed deliberately to inherit them.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell d596c4235d6527e21c8e8a5e172a351c8ee662c8 parent 4461233
3 files changed, +3 −147
  • web/pages/index.html +0 −1
  • web/static/shell.js +3 −114
  • web/static/style.css +0 −32
diff --git a/web/pages/index.html b/web/pages/index.html
index b552964..0e811b2 100644
--- a/web/pages/index.html
+++ b/web/pages/index.html
@@ -226,7 +226,6 @@
<div class="app-split-divider" id="app-split-divider" role="separator" aria-orientation="vertical" aria-label="Resize chat and interface panes" aria-valuemin="20" aria-valuemax="80" tabindex="0"></div>
<div class="app-pane interface-pane" id="interface-pane"></div>
</div>
- <div class="feed" id="feed"></div>
<div class="drop-overlay hidden" id="drop-overlay">Drop a photo or file to send</div>
<!-- Zero-height anchor at the top of the composer stack: the staged-
attachment chips paint upward from here, floating over the feed
diff --git a/web/static/shell.js b/web/static/shell.js
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();
diff --git a/web/static/style.css b/web/static/style.css
index 85022f0..f101894 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -467,22 +467,6 @@ input:focus, textarea:focus { outline: 2px solid var(--accent); border-color: tr
.menu-item.danger { color: var(--danger); }
.menu-item.disabled,
.menu-item.disabled:hover { color: var(--muted); cursor: default; background: transparent; }
-
-#feed {
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- overflow-x: hidden; /* clip messages mid swipe-to-reply; no horizontal scrollbar */
- padding: 18px;
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-/* push a few messages to the bottom (near the composer); scrolls normally when full */
-#feed::before {
- content: "";
- margin-top: auto;
-}
.msg {
display: flex;
flex-direction: column;
@@ -2629,22 +2613,6 @@ html.native .thread-head {
.native-embed .composer-edit {
display: none;
}
-/* The native shell can run this page full-bleed with floating (glass) bars over
- it. It injects the bar+keyboard heights as --native-top/bottom-inset; pad the
- feed with them so messages scroll UNDER the bars but rest clear of them. The
- vars default to 0, so a shell that doesn't inject them gets the plain layout. */
-.native-embed #feed {
- padding-top: calc(18px + var(--native-top-inset, 0px));
- /* Bottom clearance is a real in-flow spacer (::after), NOT padding: WebKit drops
- a scroll container's trailing padding at the scroll end, so padding-bottom here
- let the last message slip under the floating composer once a chat scrolled. */
- padding-bottom: 0;
-}
-.native-embed #feed::after {
- content: "";
- flex: none;
- height: calc(18px + var(--native-bottom-inset, 0px));
-}
/* Keep the attachment preview visible: it's how a staged photo shows up just above
the native composer (drag-drop or the native + button stage into it). The composer
below is hidden, so clear the native bar's height (plus the old 10px breathing room). */