diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-09 14:01:08 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-09 14:02:19 +0200 |
| commit | f2517faca8d35772fb03272ee7f8056421ce2198 (patch) | |
| tree | e345e2291a99e29acfafc755251cd7925ad96ca9 /packages/meshbay-hub | |
| parent | 4ae61dc4e1dcca6ae24131fcd9acc55124e3c0af (diff) | |
| download | meshbay-f2517faca8d35772fb03272ee7f8056421ce2198.tar.gz | |
fix(hub): an open application looks for new notifications when you come back
The page asked for the notification list at sign-in and at a token renewal,
and at no other time. A notification created after the application started,
such as a chat line the hub wrote 20 ms after the message, stayed unseen until
the next launch.
The hub has no channel to the page and is not polled on a timer, so the list
is asked for again on a gesture: the application returning to the foreground
(an Android phone included) and the home page, where the list is shown. Two
requests less than 30 s apart count as one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 29 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_notification_refresh.py | 48 |
2 files changed, 77 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 2254a11..50bd8ac 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -43,6 +43,9 @@ import { InvitePage, JoinByLink } from './invite-page.js'; // How often to look. Cheap — it reads a timestamp out of the token and almost // always does nothing. const TOKEN_CHECK_MS = 60000; +// The least time between two askings for the notification list on somebody's +// return: a gesture, never a timer, and two in a row are one question. +const NOTIF_REFRESH_MS = 30000; const THEME_KEY = 'mb_theme'; // ── Theme ──────────────────────────────────────────────────────────────────── @@ -980,10 +983,12 @@ function App() { hubFetch('/v1/hub/info').then(setHubInfo).catch(() => {}); }, []); + const notifAskedAtRef = useRef(0); const fetchNotifications = useCallback(() => { if (!user || notifDisabled) { setNotifications([]); setUnreadCount(0); return; } + notifAskedAtRef.current = Date.now(); // `unread_only`: clicking one is what dismisses it (see markRead), so a // read notification is a dismissed notification and must not come back on // the next launch. Without this the two halves disagreed — the click @@ -1046,6 +1051,30 @@ function App() { syncPush(user).catch(() => {}); }, [user]); + // The list used to be asked for at sign-in and then never, so an application + // left open showed nothing created after it started: a chat line the hub + // wrote within 20 ms of the message stayed unseen. The hub has no channel to + // the page, and is not polled on a timer, so the list is asked for again + // when somebody comes back to look: the application returning to the + // foreground (a phone included), and the home page, where it is shown. + const refreshNotifications = useCallback(() => { + if (Date.now() - notifAskedAtRef.current < NOTIF_REFRESH_MS) return; + fetchNotifications(); + }, [fetchNotifications]); + + useEffect(() => { + if (!user) return; + const onVisible = () => { + if (document.visibilityState === 'visible') refreshNotifications(); + }; + document.addEventListener('visibilitychange', onVisible); + return () => document.removeEventListener('visibilitychange', onVisible); + }, [user, refreshNotifications]); + + useEffect(() => { + if (user && route === '/') refreshNotifications(); + }, [user, route, refreshNotifications]); + // The node this application ships, set up, started and linked for whoever // signs in (main.js `ensureNode`): installing it is all it should take. // Keyed on the account, not the session object -- the token rotates, and a diff --git a/packages/meshbay-hub/tests/test_notification_refresh.py b/packages/meshbay-hub/tests/test_notification_refresh.py new file mode 100644 index 0000000..9e41f0c --- /dev/null +++ b/packages/meshbay-hub/tests/test_notification_refresh.py @@ -0,0 +1,48 @@ +""" +An open application asks for the notification list again when somebody comes back. + +Reported live on 2026-10-09: a member posted in a group, the hub wrote the +owner's notification 20 ms later, and the owner, whose application was open, +never saw it. Nothing was broken on the hub or the node. The page asked for the +list at sign-in (and at a token renewal, every few hours) and at no other time, +so a notification created after the application started waited for the next +launch. + +The hub has no channel to the page and is deliberately not polled on a timer, +so the list is asked for again on a gesture: the application returning to the +foreground, or the home page, where the list is shown. Two in a row are one +question. These read the source; there is no browser for the SPA's state here. +""" + +import re +from pathlib import Path + +APP = (Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" + / "app.js").read_text() + + +def _block(start: str) -> str: + i = APP.index(start) + return APP[i:APP.index("\n }, [", i)] + + +def test_coming_back_to_the_application_asks_again(): + listeners = [m.start() for m in re.finditer(r"addEventListener\('visibilitychange'", APP)] + bodies = [APP[APP.rfind("useEffect(", 0, i):i] for i in listeners] + assert any("refreshNotifications()" in b and "'visible'" in b for b in bodies), \ + "no visibilitychange listener asks for the notification list" + + +def test_the_home_page_asks_again(): + assert re.search(r"if \(user && route === '/'\) refreshNotifications\(\);", APP) + + +def test_two_askings_in_a_row_are_one_and_no_timer_asks(): + refresh = _block("const refreshNotifications = useCallback(") + assert "NOTIF_REFRESH_MS" in refresh and "fetchNotifications()" in refresh + # The stamp is taken where the request leaves, so the sign-in fetch counts + # and the home page right after it does not ask a second time. + fetch = _block("const fetchNotifications = useCallback(") + assert "notifAskedAtRef.current = Date.now()" in fetch + for timer in re.finditer(r"setInterval\(([^,]+),", APP): + assert "otif" not in timer.group(1), "the notification list is polled on a timer" |