| index 27193fe..f7af543 100644 |
| --- a/web/static/shell.js |
| +++ b/web/static/shell.js |
| @@ -136,6 +136,7 @@ import { setHost as setChatAppHost, setConnected as setChatAppConnected, appendN |
| import { SearchIndex, snippetFor } from "/static/search.js"; // message search over the local store (#586) |
| import { loadMemberApps, setMemberSeeder, setSurfaceRefresh, setMemberAdmin, recentLocalDeploy, pushTheme } from "/static/sandbox.js"; // member-built apps, run sandboxed (docs/member-apps.md) |
| import { THEME_META, THEME_ORDER, currentTheme, currentAppearance, setTheme, setAppearance, currentTextSize, setTextSize, onThemeApplied } from "/static/theme.js"; // applies the saved theme/text-size on import |
| +import { session, roster, myUserId, threads, threadKeys, activeId, ws, setSession, setRoster, setMyUserId, setThreads, setActiveId, setWs } from "/static/shellstate.js"; // the shared shell state (docs/shell-modules.md) |
| |
| import { openLightbox } from "/static/lightbox.js"; |
| import { parseInviteFragment, buildInviteFragment } from "/static/invite.js"; |
| @@ -775,7 +776,7 @@ const app = document.getElementById("app"); |
| // never handed to the webview: native unwraps the thread key and passes only it, |
| // so a single thread's page can't reach any other thread's content. |
| const embed = window.__lchatEmbed || null; |
| -let session = embed ? { token: embed.token, name: embed.name } : loadSession(); |
| +setSession(embed ? { token: embed.token, name: embed.name } : loadSession()); |
| |
| // Post a message back to the native shell's "lchat" handler (no-op in a browser). |
| function nativePost(msg) { |
| @@ -827,10 +828,6 @@ window.__lchatMenuPick = (token, id) => { |
| }; |
| |
| // App state. |
| -let roster = []; // [{user_id, label, public_key}] |
| -let myUserId = ""; |
| -let threads = []; // server Thread objects (id, kind, name_blob, members, wrapped) |
| -const threadKeys = {}; // thread id -> CryptoKey |
| const threadKeyB64 = {}; // thread id -> raw key (base64url), to wrap for new members |
| const threadName = {}; // thread id -> display name |
| const threadApp = {}; // thread id -> mini-app name, for a kind=="self" app-thread |
| @@ -863,9 +860,7 @@ let chatFilter = ""; // lowercased substring filtering the thread list (search b |
| // The funnel menu beside search: online-only plus category checkboxes. All |
| // categories on + online off = the unfiltered default (funnel unhighlighted). |
| const chatFilters = { online: false, people: true, bots: true, apps: true }; |
| -let activeId = null; |
| let pendingJoinThread = null; // the DM a fresh invite created — the welcome conversation offers it at the end |
| -let ws = null; |
| let everConnected = false; // has the socket ever opened? (distinguishes reconnect from first connect) |
| let wsConnected = false; // is the socket open right now? (gates the send button while reconnecting) |
| let pingTimer = null; // keepalive ping interval while the socket is open |
| @@ -885,7 +880,7 @@ const CUSTOM_APPS_TIMEOUT_MS = 4000; |
| // Otherwise route straight away — the extra tick only happens on a real return. |
| if (!embed && !session && location.hash.includes("lchat_restore=")) { |
| checkRestoreReturn().then((restored) => { |
| - if (restored) session = loadSession(); |
| + if (restored) setSession(loadSession()); |
| route(); |
| }); |
| } else { |
| @@ -1185,7 +1180,7 @@ async function bootEmbed() { |
| // The native shell can land straight in a custom app's thread, so the |
| // embed path needs registration complete before its render too. |
| await loadCustomApps(); |
| - myUserId = embed.myUserId || ""; |
| + setMyUserId(embed.myUserId || ""); |
| setCurrentUser(myUserId, session.name || "New member"); |
| await loadRoster(); // token-only; gives sender labels + profile photos |
| if (embed.instanceKey) { |
| @@ -1218,9 +1213,9 @@ async function bootEmbed() { |
| // has a key — the others can't be unwrapped here without the private key. |
| try { |
| const r = await (await fetch(`/api/threads?token=${encodeURIComponent(session.token)}`)).json(); |
| - threads = r.threads || []; |
| + setThreads(r.threads || []); |
| } catch { |
| - threads = []; |
| + setThreads([]); |
| } |
| if (!threads.find((t) => t.id === tid)) threads.push({ id: tid, kind: "dm", members: [] }); |
| threadName[tid] = await displayName(threads.find((t) => t.id === tid)); |
| @@ -1445,12 +1440,12 @@ async function loadRoster() { |
| |
| // The apply half of loadRoster — also run over the cached response on boot. |
| async function applyRoster(r) { |
| - roster = r.roster || []; |
| + setRoster(r.roster || []); |
| // In embed mode the shell tells us our user id up front (it can't be matched by |
| // public key — the webview never holds one). Otherwise find ourselves by pubkey. |
| if (!myUserId) { |
| const me = roster.find((e) => e.public_key === session.userPub); |
| - myUserId = me ? me.user_id : ""; |
| + setMyUserId(me ? me.user_id : ""); |
| } |
| // The "New member" fallback (here and on every outbound payload): setup and |
| // welcome claimers are signed in nameless until Eleven's first question — if |
| @@ -2516,7 +2511,7 @@ async function loadThreads() { |
| // also run over the cached response on boot, so hydration and the network |
| // path are one code path. |
| async function applyThreads(r) { |
| - threads = r.threads || []; |
| + setThreads(r.threads || []); |
| for (const t of threads) { |
| if (!threadKeys[t.id] && (t.wrapped || t.open_key)) { |
| // Two key sources: the per-member wrapped copy, then — for open groups — |
| @@ -3883,7 +3878,7 @@ async function openThread(id) { |
| if (activeId) stashAppPane(activeId); // park the outgoing app-thread's full-pane root |
| |
| const myOpen = ++openSeq; |
| - activeId = id; |
| + setActiveId(id); |
| localStorage.setItem(ACTIVE_KEY, id); // remember where we are, to restore on refresh |
| unread[id] = false; |
| markThreadRead(id); // …and tell the server, so your other devices clear too |
| @@ -7331,7 +7326,7 @@ async function resyncActive() { |
| |
| function connectWS() { |
| const proto = location.protocol === "https:" ? "wss://" : "ws://"; |
| - ws = new WebSocket(`${proto}${location.host}/ws?token=${encodeURIComponent(session.token)}`); |
| + setWs(new WebSocket(`${proto}${location.host}/ws?token=${encodeURIComponent(session.token)}`)); |
| ws.onopen = () => { |
| // (Re)connected: hide the pill and (re)start the keepalive. |
| if (reconnectStatusTimer) { |
| @@ -8606,7 +8601,7 @@ async function closeChat() { |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ token: session.token, hidden: true }), |
| }).catch(() => {}); |
| - activeId = null; |
| + setActiveId(null); |
| localStorage.removeItem(ACTIVE_KEY); // don't reopen a closed chat on refresh |
| renderThreadList(); |
| document.getElementById("thread-view").classList.add("hidden"); |
| @@ -8635,7 +8630,7 @@ async function endConversation() { |
| } |
| dmClosed[id] = true; |
| hidden[id] = true; |
| - activeId = null; |
| + setActiveId(null); |
| localStorage.removeItem(ACTIVE_KEY); |
| renderThreadList(); |
| document.getElementById("thread-view").classList.add("hidden"); |
| @@ -8875,12 +8870,12 @@ async function leaveGroup() { |
| } catch { |
| return alert("Couldn't leave the group. Try again."); |
| } |
| - threads = threads.filter((x) => x.id !== id); |
| + setThreads(threads.filter((x) => x.id !== id)); |
| delete threadKeys[id]; |
| storeClearThread(id).catch(() => {}); // a left thread keeps no local rows (#457) |
| searchIdx.dropThread(id); // …and stops matching in search (#586) |
| if (activeId === id) { |
| - activeId = null; |
| + setActiveId(null); |
| localStorage.removeItem(ACTIVE_KEY); // the thread we were in is gone |
| document.getElementById("thread-view").classList.add("hidden"); |
| document.getElementById("thread-empty").classList.remove("hidden"); |
| @@ -9974,7 +9969,7 @@ async function cancelInvitedChat(threadId) { |
| delete inviteLinks[threadId]; |
| delete threadKeys[threadId]; |
| delete threadKeyB64[threadId]; |
| - activeId = null; |
| + setActiveId(null); |
| await loadThreads(); |
| renderThreadList(); |
| document.getElementById("thread-view").classList.add("hidden"); |