perf: fast relay connect on mobile and desktop + connect splash + edit-safe instances
Relay connect used to serialize a dead LAN probe (up to 8s per stale address on mobile, 2-4s on desktop) in front of the relay attempt, then paid a second WebSocket connect + E2EE handshake because the probe tunnel was thrown away. - mobile probeConnectionCandidates: race the relay probe against the direct chain with a 1.5s direct headstart; a live LAN still wins, a dead one no longer delays startup - relay probes adopt their tunnel as the runtime tunnel (adoptRelayTunnel) instead of dialing a fresh one — applies to auto-connect, pairing redeem, password login, and the desktop host switcher's relay fallback - relay probe drops the /health round-trip: the E2EE handshake already proves the server identity, /auth/session alone proves liveness and auth - desktop restoreDesktopRelayRuntime: same headstart race; a late direct success hot-switches back (stable runtimeKey); startup probe now passes expectedServerId so a re-leased LAN address never sees the token - launch splash shows 'Connecting to device: <label>' with animated dots under the (still centered) logo, translated in all locales - editing a saved instance no longer rebuilds it from the URL field alone: the id is passed through, relay/https candidates are preserved, and a token-key change migrates the Keychain token instead of orphaning it
This commit is contained in:
@@ -57,10 +57,11 @@ import { SyncProvider, useSession, useSessionMessages } from '@/sync/sync-contex
|
||||
import { SyncAppEffects } from './AppEffects';
|
||||
import { MobileChangesSurface } from './MobileChangesSurface';
|
||||
import { MobileFilesSurface } from './MobileFilesSurface';
|
||||
import { BusyDots } from '@/components/chat/message/parts/BusyDots';
|
||||
import { MobileSessionsSheet } from './MobileSessionsSheet';
|
||||
import { MobileSurfaceShell } from './MobileSurfaceShell';
|
||||
import { DedicatedMobileAppProvider, type MobileAppActions } from './mobileAppContext';
|
||||
import { autoConnectLastInstance, connectionDisplayUrl, isActiveRuntimeConnection, reprobeActiveConnection, useMobileConnection } from './mobileConnections';
|
||||
import { autoConnectLastInstance, connectionDisplayUrl, getAutoConnectTargetLabel, isActiveRuntimeConnection, reprobeActiveConnection, useMobileConnection } from './mobileConnections';
|
||||
import { isRelayModeActive } from '@/lib/relay/runtime-tunnel';
|
||||
import { isQrScanSupported, parseConnectionPayload, scanConnectionQr } from './mobileQrScan';
|
||||
import { reconnectAppForTransportSwitch, resetAppForRuntimeEndpointChange } from './runtimeEndpointReset';
|
||||
@@ -981,10 +982,13 @@ const MobileInstancesSurface: React.FC<{
|
||||
|
||||
const saveInstance = React.useCallback((event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
void saveConnection({ url, label, clientToken }).then((saved) => {
|
||||
// The id is what makes this an EDIT: saveConnection uses it to preserve the
|
||||
// existing relay/https candidates (and the Keychain token they key) instead
|
||||
// of rebuilding the instance from the single URL field.
|
||||
void saveConnection({ id: editingId ?? undefined, url, label, clientToken }).then((saved) => {
|
||||
if (saved) resetForm();
|
||||
});
|
||||
}, [clientToken, label, resetForm, saveConnection, url]);
|
||||
}, [clientToken, editingId, label, resetForm, saveConnection, url]);
|
||||
|
||||
// Scan a pairing QR into the add/edit form fields (does not change edit mode, so
|
||||
// the form-reset effect doesn't wipe the scanned values). The user reviews + saves.
|
||||
@@ -2688,6 +2692,9 @@ export function MobileApp({ apis }: MobileAppProps) {
|
||||
// splash so we don't flash the connect screen; 'done' means we either connected or
|
||||
// exhausted the attempt (then the connect screen shows).
|
||||
const [autoConnectPhase, setAutoConnectPhase] = React.useState<'pending' | 'attempting' | 'done'>('pending');
|
||||
// The instance the splash says we are connecting to. Read once on mount —
|
||||
// auto-connect targets the most-recent saved connection from the same list.
|
||||
const autoConnectLabel = React.useMemo(() => getAutoConnectTargetLabel(), []);
|
||||
// Bumped to force a re-render (and thus a fresh `sdk` prop for SyncProvider)
|
||||
// after a same-device transport swap — reconnects the sync layer in place with
|
||||
// no remount. The value itself is unused; only the re-render matters.
|
||||
@@ -3052,8 +3059,19 @@ export function MobileApp({ apis }: MobileAppProps) {
|
||||
// (no saved instance, unreachable, or needs re-login).
|
||||
if (autoConnectPhase !== 'done') {
|
||||
return (
|
||||
<main className="flex min-h-dvh items-center justify-center bg-background text-foreground">
|
||||
<main className="relative flex min-h-dvh items-center justify-center bg-background text-foreground">
|
||||
<OpenChamberLogo width={120} height={120} isAnimated />
|
||||
{/* Absolutely positioned below the (still perfectly centered) logo so
|
||||
the text never pushes it up. 50% + half the 120px logo + a gap. */}
|
||||
{autoConnectLabel ? (
|
||||
<div className="absolute inset-x-0 top-[calc(50%+84px)] flex flex-col items-center gap-0.5 px-6 text-center">
|
||||
<p className="typography-small text-muted-foreground">{t('mobile.connect.splash.connectingTo')}</p>
|
||||
<p className="typography-small text-foreground">
|
||||
{autoConnectLabel}
|
||||
<BusyDots />
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user