aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-17 13:29:25 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-17 13:29:25 +0200
commit165847da1ca3c3be627b5968bc77e9a11b3385cb (patch)
tree603329a801d1304fdba833c1a8f4cd8c4ce4e7c6 /packages/meshbay-hub/src/meshbay_hub/static/group-page.js
parenta1d18a2957d167444c6fc6c2e4ec8628509a7009 (diff)
downloadmeshbay-165847da1ca3c3be627b5968bc77e9a11b3385cb.tar.gz
feat(android): ask for the local network when a group opens
Android 17 makes it a permission for an application targeting SDK 37. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-page.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js46
1 files changed, 45 insertions, 1 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 728ffc37..45049a16 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -31,6 +31,10 @@ import { clearPending, nodePkFromLink, pendingFor } from './invite-link.js';
// each try is a connection.
const DEVICE_RETRY_MS = 5000;
+// Whether this application has asked for the local network yet in this run:
+// once, when the first group is opened, and again only when the person says so.
+let lanAsked = false;
+
/** The node's `expires_at` (ISO) in epoch seconds; 0 when it gave none. */
function _untilSeconds(iso) {
const ms = Date.parse(iso || '');
@@ -240,6 +244,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
const [passInput, setPassInput] = useState('');
const [passBusy, setPassBusy] = useState(false);
const [retryKey, setRetryKey] = useState(0);
+ // 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('');
const transportRef = useRef(null);
const gekRef = useRef(null);
// One refresh per group: if a fresh token still says we are not a member, we
@@ -398,6 +405,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
const connect = async () => {
setStatus('discovering');
setError('');
+ setLanBlocked('');
// Belongs to the connection about to be made, not the group just left.
setDeviceReady(false);
gekRef.current = null;
@@ -425,6 +433,16 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// from the node, or creates one there on a first join.
const identity = null;
+ // Android 17 asks before this application reaches the local network,
+ // where the node usually is: when a group is first opened rather than
+ // at start, and a refusal is explained below if it is why the group
+ // does not open (local_network_blocked).
+ if (platform.localNetwork.available && !lanAsked) {
+ lanAsked = true;
+ try { await platform.localNetwork.request(); } catch { /* the connection says the rest */ }
+ if (cancelled) return;
+ }
+
setStatus('connecting');
// Renewed here rather than taken from the prop. This effect no longer
// re-runs when the token rotates (see the dependency list below), so
@@ -706,7 +724,14 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// 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') {
+ 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');
}
@@ -1038,6 +1063,25 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
<button class="admin-btn" style="margin-left:8px;font-size:0.9em"
onClick=${() => setRetryKey(k => k + 1)}>${t('group.retry')}</button>
</div>`}
+ ${lanBlocked === 'prompt' && html`
+ <button class="admin-btn" style="margin-bottom:12px" onClick=${async () => {
+ try { await platform.localNetwork.request(); } catch { /* retried either way */ }
+ setRetryKey(k => k + 1);
+ }}>${t('group.lan_allow')}</button>`}
+ ${lanBlocked === 'denied' && html`
+ <p class="settings-hint">${t('group.lan_settings_hint')}</p>
+ <button class="admin-btn" style="margin-bottom:12px" onClick=${() => {
+ // Back from the settings is back in the foreground: try again then.
+ const back = () => {
+ if (document.visibilityState !== 'visible') return;
+ document.removeEventListener('visibilitychange', back);
+ setRetryKey(k => k + 1);
+ };
+ document.addEventListener('visibilitychange', back);
+ platform.localNetwork.openSettings().catch(() => {
+ document.removeEventListener('visibilitychange', back);
+ });
+ }}>${t('group.lan_open_settings')}</button>`}
${needsDevice && nativeKeys && html`
<div class="invite-form" style="margin-bottom:12px">
<h4>${t('device.add_title')}</h4>