From 8125df6d8149368ed8a84c9db6c5e37bfccb5a15 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 17 Oct 2026 18:37:35 +0200 Subject: fix(spa): a reconnect refused the local network says so Each failed attempt reaches the group page, which shows the same message and buttons as at opening while the reconnect goes on. Try now gives up an attempt still waiting on ICE and starts the next at once. Co-Authored-By: Claude Opus 5.5 --- .../src/meshbay_hub/static/group-page.js | 68 +++++++++++++++++----- 1 file changed, 52 insertions(+), 16 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-page.js') 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 45049a16..304a28fb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -247,6 +247,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // In the Android application, when the local network was refused: whether // Android will still ask ("prompt") or only its settings can change it. const [lanBlocked, setLanBlocked] = useState(''); + // Set while the message is a reconnect refused the local network: the + // reconnect that gets through takes it away. + const lanLostRef = useRef(false); const transportRef = useRef(null); const gekRef = useRef(null); // One refresh per group: if a fresh token still says we are not a member, we @@ -394,6 +397,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // Dropped by the teardown below, so a transport handed on to a running // download (`releaseWhenIdle`) stops driving a page that is gone. let offReconnect = null; + let offRefused = null; let deviceRetry = null; // The cache is written here and read only by the search page. It used to @@ -402,10 +406,27 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // cache landing afterwards. Files shows what the node says, or says it // cannot reach the node. + // This device dropped the node's local addresses (transport.js + // localNetworkRefused): a setting to change here, not a node to wait for. + // Android 17 gates it per application, Chrome from 155. In the application, + // whether Android will still ask or only its settings can change it. + const explainLanRefusal = async () => { + if (!platform.localNetwork.available) { + return t(/Android/i.test(navigator.userAgent) + ? 'group.lan_blocked_android' : 'group.lan_blocked_browser'); + } + try { + const state = await platform.localNetwork.state(); + if (!cancelled) setLanBlocked(state === 'granted' ? '' : state); + } catch { /* the message stands alone */ } + return t('group.lan_blocked_app'); + }; + const connect = async () => { setStatus('discovering'); setError(''); setLanBlocked(''); + lanLostRef.current = false; // Belongs to the connection about to be made, not the group just left. setDeviceReady(false); gekRef.current = null; @@ -632,8 +653,24 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // full fetch rather than a delta: this session was never told what it // missed, and a delta is computed against a snapshot only the node // has. + // Connected on mobile data, then on the node's Wi-Fi without the + // permission: every attempt is refused and the reconnect goes on, so a + // permission granted or mobile data again reaches the group by itself. + // Said while it lasts, over the view the group had. + offRefused = transport.addReconnectFailureListener(async (e) => { + if (cancelled || e.reason !== 'local_network_blocked') return; + const message = await explainLanRefusal(); + if (cancelled) return; + lanLostRef.current = true; + setError(message); + }); offReconnect = transport.addReconnectListener((reack) => { if (cancelled) return; + if (lanLostRef.current) { + lanLostRef.current = false; + setError(''); + setLanBlocked(''); + } (async () => { try { applyAck(reack); @@ -721,19 +758,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, if (err.reason === 'no_keys') setNeedsPass(true); // An identity sealed before the pepper: only the operator can clear it. if (err.reason === 'bundle_format_retired') err.message = t('group.bundle_format_retired'); - // This device dropped the node's local addresses (transport.js - // localNetworkRefused): a setting to change here, not a node to wait - // for. Android 17 gates it per application, Chrome from 155. - if (err.reason === 'local_network_blocked' && platform.localNetwork.available) { - err.message = t('group.lan_blocked_app'); - try { - const state = await platform.localNetwork.state(); - if (cancelled) return; - setLanBlocked(state === 'granted' ? '' : state); - } catch { /* the message stands alone */ } - } else if (err.reason === 'local_network_blocked') { - err.message = t(/Android/i.test(navigator.userAgent) - ? 'group.lan_blocked_android' : 'group.lan_blocked_browser'); + if (err.reason === 'local_network_blocked') { + err.message = await explainLanRefusal(); + if (cancelled) return; } // A key this node has never pinned, for an account it knows. The way in // is a device already trusted here, not an operator — which is the @@ -787,6 +814,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, return () => { cancelled = true; if (offReconnect) { offReconnect(); offReconnect = null; } + if (offRefused) { offRefused(); offRefused = null; } clearTimeout(deviceRetry); // Nothing will update this group's dock row once the page lets go of it. reportIndexPush(groupId, null); @@ -814,6 +842,14 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // it; nothing in between should disturb a working connection. }, [groupId, Boolean(token), retryKey]); + // A connection still reconnecting is asked to try now rather than torn down: + // what it carries (a download, a film) goes on once it gets through. + const retry = useCallback(() => { + const transport = transportRef.current; + if (transport && transport.reconnecting) transport.retryNow(); + else setRetryKey((k) => k + 1); + }, []); + // A group that could not be reached is tried again when the device joins // another network: from the Wi-Fi to mobile data, or back, is what decides // whether the node can be reached, and in the Android application there is @@ -1061,12 +1097,12 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,

${t('group.browser_access_off')}

`} ${error && html`
${error}${' '} + onClick=${retry}>${t('group.retry')}
`} ${lanBlocked === 'prompt' && html` `} ${lanBlocked === 'denied' && html`

${t('group.lan_settings_hint')}

@@ -1075,7 +1111,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, const back = () => { if (document.visibilityState !== 'visible') return; document.removeEventListener('visibilitychange', back); - setRetryKey(k => k + 1); + retry(); }; document.addEventListener('visibilitychange', back); platform.localNetwork.openSettings().catch(() => { -- cgit v1.2.3