diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-15 02:48:25 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-15 02:48:25 +0200 |
| commit | 99f95dadee1a3c892bb0bc87e3564f71d07c88e0 (patch) | |
| tree | 47691af252accd7097c0d7f58a9c2d44c10fb6ae /packages | |
| parent | acb0d666540dacb092f596dada25822d1d0466f0 (diff) | |
| download | meshbay-fefad9b2abdfbc978aeed759e0a91a4737e9167d.tar.gz | |
fix(ui): a group opened directly lands on the preferred app once preferences load0.14
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XuNrwLf5EFWCMHzfoEvnpm
Diffstat (limited to 'packages')
3 files changed, 67 insertions, 7 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index 988bcb1..51e39dc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -55,7 +55,21 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, if (o && o.groupId === groupId) { session.openGroupTab = null; return o.tab; } return null; }, [groupId]); - const [tab, setTab] = useState(() => consumeTabOverride() || defaultTab); + // Whether the tab on screen is still the one the preference picked. The + // preferences come from the hub after sign-in, so a group opened directly — a + // reload, a link — mounts on the built-in 'chat' before they are known, and + // has to move when they land. Not once the reader has picked a tab, and not + // when the wizard asked for one. + const onDefaultTabRef = useRef(true); + const [tab, setTab] = useState(() => { + const override = consumeTabOverride(); + onDefaultTabRef.current = !override; + return override || defaultTab; + }); + const chooseTab = useCallback((key) => { + onDefaultTabRef.current = false; + setTab(key); + }, []); // GroupPage is not remounted when switching groups (see the refreshedRef note // below), so react to a real groupId change here — but not to the initial // mount, where useState already picked the right tab. @@ -63,8 +77,13 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, useEffect(() => { if (tabGroupRef.current === groupId) return; tabGroupRef.current = groupId; - setTab(consumeTabOverride() || defaultTab); + const override = consumeTabOverride(); + onDefaultTabRef.current = !override; + setTab(override || defaultTab); }, [groupId]); + useEffect(() => { + if (onDefaultTabRef.current) setTab(defaultTab); + }, [defaultTab]); const [groupMuted, setGroupMuted] = useState(() => !!(group && group.muted)); const _lastTouch = useRef(0); @@ -848,11 +867,11 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, <div class="group-tabs" ref=${tabBand}> ${apps.map(a => html` <button key=${a.key} class="group-tab ${tab === a.key ? 'active' : ''}" - onClick=${() => setTab(a.key)} title=${t(a.labelKey)}> + onClick=${() => chooseTab(a.key)} title=${t(a.labelKey)}> <${Icon} name=${a.icon} cls="tab-icon" /></button> `)} <button class="group-tab ${tab === 'settings' ? 'active' : ''}" - onClick=${() => setTab('settings')} title=${t('group.tab_settings')}> + onClick=${() => chooseTab('settings')} title=${t('group.tab_settings')}> <${Icon} name="gear" cls="tab-icon" /></button> </div> diff --git a/packages/meshbay-hub/tests/harness/group_tab_probe.py b/packages/meshbay-hub/tests/harness/group_tab_probe.py index da1e8d1..473bcdc 100644 --- a/packages/meshbay-hub/tests/harness/group_tab_probe.py +++ b/packages/meshbay-hub/tests/harness/group_tab_probe.py @@ -40,8 +40,17 @@ CASES = [ ("preferred app absent", "music", ["chat", "files"]), ("preferred app present", "video", ["chat", "files", "video"]), ("preference names nothing real", "nonsense", ["files", "music"]), + # The preferences are fetched by app.js after sign-in, so a group opened + # directly (a reload, a link) mounts before they arrive. + ("preference arrives after the group opened", "video", ["chat", "files", "video"]), + ("tab chosen before the preference arrives", "video", ["chat", "files", "video"]), ] +# How each case delivers the preference: at mount, or after mount — and, for +# the second, after the reader has already clicked Files. +LATE = {"preference arrives after the group opened"} +CLICKED_FIRST = {"tab chosen before the preference arrives"} + FRAME = r"""<!doctype html><html><head><meta charset=utf-8> <link rel="stylesheet" href="/style.css"></head><body> <div id="root"></div> @@ -69,9 +78,19 @@ import { initLocale } from '/i18n.js'; import { GroupPage } from '/group-page.js'; await initLocale(); -render(html`<${GroupPage} groupId="g1" token="t" username="me" userId="u1" +const draw = (prefs) => render(html`<${GroupPage} groupId="g1" token="t" username="me" userId="u1" group=${{ id: 'g1', name: 'a group', owner_username: 'me', is_admin: true }} - userPrefs=${{ default_tab: %(pref)s }} />`, document.getElementById('root')); + userPrefs=${prefs} />`, document.getElementById('root')); +const prefs = { default_tab: %(pref)s }; +if (%(late)s || %(clicked)s) { + draw({}); + if (%(clicked)s) { + setTimeout(() => document.querySelectorAll('.group-tabs .group-tab')[1].click(), 400); + } + setTimeout(() => draw(prefs), 800); +} else { + draw(prefs); +} setTimeout(() => { const tabs = [...document.querySelectorAll('.group-tabs .group-tab')]; @@ -137,7 +156,10 @@ class H(http.server.BaseHTTPRequestHandler): _, pref, enabled = CASES[index] self._send((FRAME % {"index": index, "pref": json.dumps(pref), - "enabled": json.dumps(enabled)}).encode(), + "enabled": json.dumps(enabled), + "late": json.dumps(CASES[index][0] in LATE), + "clicked": json.dumps(CASES[index][0] in CLICKED_FIRST), + }).encode(), "text/html; charset=utf-8") elif path == "/v1/groups/g1/nodes": self._send(b'{"nodes": [{"node_id": "n1"}]}', "application/json") diff --git a/packages/meshbay-hub/tests/test_group_tab_fallback.py b/packages/meshbay-hub/tests/test_group_tab_fallback.py index 6310b9c..634c5eb 100644 --- a/packages/meshbay-hub/tests/test_group_tab_fallback.py +++ b/packages/meshbay-hub/tests/test_group_tab_fallback.py @@ -67,6 +67,25 @@ def test_a_preference_that_names_nothing_falls_back(cases): assert cases["preference names nothing real"]["active"] == 0 +def test_a_preference_that_arrives_after_the_group_opened_applies(cases): + """The reported bug: Videos chosen in Settings, and a group opened directly + (a reload, a link) still landed on Chat. + + app.js fetches the preferences after sign-in; GroupPage chose its tab at + mount and never looked again. + """ + case = cases["preference arrives after the group opened"] + assert case["active"] == 2, ( + f"the preference arrived and was ignored: landed on tab {case['active']}") + + +def test_a_late_preference_does_not_undo_a_tab_the_reader_chose(cases): + """Arriving late must not yank someone off a tab they just clicked.""" + case = cases["tab chosen before the preference arrives"] + assert case["active"] == 1, ( + f"the reader clicked Files and was moved to tab {case['active']}") + + def test_a_preference_the_group_offers_is_kept(cases): """The fallback must not become 'always the first app'.""" case = cases["preferred app present"] |