summaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-15 02:48:25 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-15 02:48:25 +0200
commit99f95dadee1a3c892bb0bc87e3564f71d07c88e0 (patch)
tree47691af252accd7097c0d7f58a9c2d44c10fb6ae /packages
parentacb0d666540dacb092f596dada25822d1d0466f0 (diff)
downloadmeshbay-99f95dadee1a3c892bb0bc87e3564f71d07c88e0.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')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js27
-rw-r--r--packages/meshbay-hub/tests/harness/group_tab_probe.py28
-rw-r--r--packages/meshbay-hub/tests/test_group_tab_fallback.py19
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"]