diff options
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.js | 68 |
1 files changed, 59 insertions, 9 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 3e3b483..422d90b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -26,6 +26,17 @@ const GroupSettingsPanel = lazy(() => import('./group-settings.js'), 'GroupSetti import { reportIndexPush } from './index-dock.js'; import { clearPending, nodePkFromLink, pendingFor } from './invite-link.js'; +// While a linking code is shown, how long after a refused try the group is +// tried again: a person typing the code on another device takes longer, and +// each try is a connection. +const DEVICE_RETRY_MS = 5000; + +/** The node's `expires_at` (ISO) in epoch seconds; 0 when it gave none. */ +function _untilSeconds(iso) { + const ms = Date.parse(iso || ''); + return Number.isFinite(ms) ? ms / 1000 : 0; +} + /** * The group shell: everything a group's "applications" (Chat, Files, and * whatever registers in apps.js next) share — the WebRTC connection, the file @@ -218,6 +229,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, }, []); const browserAccessOff = !nativeKeys && userPrefs && userPrefs.browser_access === 'off'; const [deviceCode, setDeviceCode] = useState(''); + // While a code is shown: until when the node keeps its request (epoch + // seconds). The group is tried again after each refusal until then. + const deviceWaitRef = useRef(0); const [codeInput, setCodeInput] = useState(''); // This browser has never derived the passphrase-bundle key (fresh browser, // cleared storage, or a device-key sign-in). Ask for the passphrase here @@ -236,9 +250,15 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // set to `true` while looking at one group would otherwise silently disable // the retry for every group opened afterward in the same session, forever. const refreshedRef = useRef(false); + // Set by "Get a linking code" for the one connection that files the request. + const fileDeviceRef = useRef(false); useEffect(() => { refreshedRef.current = false; + fileDeviceRef.current = false; setNeedsPass(false); + deviceWaitRef.current = 0; + setNeedsDevice(false); + setDeviceCode(''); }, [groupId]); const submitJoinCode = useCallback((e) => { @@ -367,6 +387,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 deviceRetry = null; // The cache is written here and read only by the search page. It used to // seed this list too, which put a stale index on screen and then raced the @@ -453,6 +474,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, transport.onDeviceIdentity = (ok) => { if (!cancelled) setDeviceReady(ok); }; + // "Get a linking code" retries with this set: the request is filed + // on the connection the node refused, which is closed right after. + transport.fileDeviceRequest = fileDeviceRef.current; try { ack = await transport.connect( n.node_id, live, groupId, null, identity, session.bundleKey, @@ -615,8 +639,12 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, })(); }); - // We are in: an invitation to this group has served its purpose. + // We are in: an invitation to this group has served its purpose, and a + // linking code shown before has been approved. if (onJoined) onJoined(groupId); + deviceWaitRef.current = 0; + setNeedsDevice(false); + setDeviceCode(''); const indexMsg = await transport.fetchIndex(); if (cancelled) return; @@ -680,7 +708,30 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // whole point of device linking: a second browser or a native client // must not cost anyone a support request. if (err.reason === 'unknown_device') setNeedsDevice(true); - setError(err.message); + fileDeviceRef.current = false; + if (err.deviceCode) { + setDeviceCode(err.deviceCode); + deviceWaitRef.current = _untilSeconds(err.deviceExpiresAt) + || Date.now() / 1000 + 3600; + } + // A code is shown: the group opens by itself once a linked device + // approves it, as the command-line client goes on by itself. Tried + // again only once this try is over, so a slow connection is never cut + // short by the next; past the request's life the code is dropped and + // the button comes back for a new one. + if (deviceWaitRef.current && err.reason === 'unknown_device') { + if (Date.now() / 1000 < deviceWaitRef.current) { + deviceRetry = setTimeout(() => setRetryKey(k => k + 1), DEVICE_RETRY_MS); + } else { + deviceWaitRef.current = 0; + setDeviceCode(''); + } + } + // In the application the linking form says all there is to say, and + // while a code is shown this is the refusal of each retry, not news. + const quiet = err.reason === 'unknown_device' && await platform.nativeKeys(); + if (cancelled) return; + if (!quiet) setError(err.message); setStatus('error'); if (transportRef.current) { try { transportRef.current.close(); } catch { /* already gone */ } @@ -704,6 +755,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, return () => { cancelled = true; if (offReconnect) { offReconnect(); offReconnect = null; } + clearTimeout(deviceRetry); // Nothing will update this group's dock row once the page lets go of it. reportIndexPush(groupId, null); if (transportRef.current) { @@ -967,17 +1019,15 @@ 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>`} - ${needsDevice && html` + ${needsDevice && nativeKeys && html` <div class="invite-form" style="margin-bottom:12px"> <h4>${t('device.add_title')}</h4> <p class="settings-hint">${t('device.add_hint')}</p> ${!deviceCode && html` - <button class="admin-btn" onClick=${async () => { - try { - const transport = transportRef.current; - const out = await transport.requestDeviceAdd(userId); - setDeviceCode(out.code); - } catch (err) { setError(err.message); } + <button class="admin-btn" onClick=${() => { + // The refused connection is closed: a new one files the request. + fileDeviceRef.current = true; + setRetryKey(k => k + 1); }}>${t('device.add_btn')}</button> `} ${deviceCode && html` |