| index 4b4540d..19018fc 100644 |
| --- a/web/static/shell.js |
| +++ b/web/static/shell.js |
| @@ -1417,17 +1417,28 @@ async function bootEmbed() { |
| // rules) — classed before the first render so the feed never flashes. |
| if (embed.mode === "command") document.body.classList.add("command-host"); |
| try { |
| - // 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(); |
| setMyUserId(embed.myUserId || ""); |
| setCurrentUser(myUserId, session.name || "New member"); |
| - await loadRoster(); // token-only; gives sender labels + profile photos |
| + // The instance key first (a local unwrap, no network): applyRoster can |
| + // then decrypt labels in its first pass, and member apps below never hit |
| + // their no-key no-op. |
| if (embed.instanceKey) { |
| try { |
| setInstanceKey(await importKey(embed.instanceKey), embed.instanceKey); |
| } catch {} |
| } |
| + // The boot round-trips are independent of each other, so they run |
| + // together rather than as a waterfall — this pane is behind the native |
| + // loading veil, and every serialized fetch here holds the veil longer. |
| + // The native shell can land straight in a custom app's thread, so |
| + // registration must still be complete before the render below. |
| + const customAppsLoad = loadCustomApps(); |
| + const rosterLoad = loadRoster(); // token-only; gives sender labels + profile photos |
| + // Thread objects for kind/members/name_blob, but only the active thread |
| + // has a key — the others can't be unwrapped here without the private key. |
| + const threadsLoad = fetch(`/api/threads?token=${encodeURIComponent(session.token)}`) |
| + .then((r) => r.json()) |
| + .catch(() => ({})); |
| // Per-viewer names (#577): the embed never holds userPriv, so it can't |
| // open the nicknames blob — the native shell (which can) hands the |
| // RESOLVED uid → name map over the bridge instead. Fold it in as |
| @@ -1442,20 +1453,18 @@ async function bootEmbed() { |
| // The seed is baked at webview creation and parked webviews outlive a |
| // rename — the native shell pushes fresh maps here after each fold. |
| window.__lchatSetNames = (map) => applyEmbedNames(map); |
| - // The native shell can land straight in a member app's thread too — the |
| - // sandboxed proxies must be registered before the render below. |
| - await loadMemberAppsNow(); |
| const tid = embed.threadId; |
| threadKeyB64[tid] = embed.threadKey; |
| threadKeys[tid] = await importKey(embed.threadKey); |
| - // Fetch thread objects for kind/members/name_blob, but only the active thread |
| - // 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(); |
| - setThreads(r.threads || []); |
| - } catch { |
| - setThreads([]); |
| - } |
| + // Member apps read roster labels for their bylines, so they start once |
| + // the roster lands — still overlapping the custom-app phase, so the two |
| + // load timeouts run in the same window instead of back to back. The |
| + // native shell can land straight in a member app's thread too — the |
| + // sandboxed proxies must be registered before the render below. |
| + await rosterLoad; |
| + await Promise.all([customAppsLoad, loadMemberAppsNow()]); |
| + const r = await threadsLoad; |
| + setThreads(r.threads || []); |
| if (!threads.find((t) => t.id === tid)) threads.push({ id: tid, kind: "dm", members: [] }); |
| threadName[tid] = await displayName(threads.find((t) => t.id === tid)); |
| // 🤖 The native shell can ask for a specific view — its companion split |