Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 275 additions & 6 deletions src/apps/desktop/src/api/remote_connect_api.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const RELAY_DIAL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1
/// Heartbeats are sent every 30 seconds. Two missed acknowledgements plus
/// scheduling/network slack indicates a half-open socket that should be
/// replaced even when the OS has not surfaced a read error yet.
const RELAY_INBOUND_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(75);
pub const RELAY_INBOUND_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(75);

/// Messages in the relay protocol (both directions).
#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const DeviceStatusControl: React.FC<DeviceStatusControlProps> = ({
overview,
refresh,
accountService,
} = useDeviceInterconnectionOverview(localDeviceLabel);
} = useDeviceInterconnectionOverview(localDeviceLabel, t('remoteConnect.mobileBrowserTitle'));
const [returningLocal, setReturningLocal] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { api } from '@/infrastructure/api/service-api/ApiClient';
import {
remoteConnectAPI,
type DeviceInfo,
type RemoteConnectStatus,
} from '@/infrastructure/api/service-api/RemoteConnectAPI';
import { remoteConnectStatusSource, useRemoteConnectStatus } from '@/infrastructure/remote-connect/remoteConnectStatus';
import { usePeerDeviceModeOptional } from '@/infrastructure/peer-device/peerDeviceContextState';
import { useDispatchJobStore } from '@/features/dispatch/dispatchJobStore';
import {
Expand All @@ -17,14 +17,13 @@ import {

const TOPOLOGY_POLL_MS = 15_000;

export function useDeviceInterconnectionOverview(fallbackLocalDeviceName: string) {
export function useDeviceInterconnectionOverview(fallbackLocalDeviceName: string, fallbackMobileDeviceName?: string) {
const account = useAccountLoginState();
const peerContext = usePeerDeviceModeOptional();
const dispatchJobs = useDispatchJobStore(state => state.jobs);

const [localDevice, setLocalDevice] = useState<DeviceInfo | null>(null);
const [remoteStatus, setRemoteStatus] = useState<RemoteConnectStatus | null>(null);
const [remoteStatusState, setRemoteStatusState] = useState<'loading' | 'ready' | 'unavailable'>('loading');
const { status: remoteStatus, state: remoteStatusState } = useRemoteConnectStatus();
const [accountService, setAccountService] = useState<DeviceOverviewConnectionService | null>(null);
const refreshGenerationRef = useRef(0);

Expand All @@ -38,18 +37,7 @@ export function useDeviceInterconnectionOverview(fallbackLocalDeviceName: string
})
.catch(() => undefined);

const statusPromise = remoteConnectAPI.getStatus()
.then(status => {
if (!isCurrent()) return;
setRemoteStatus(status);
setRemoteStatusState('ready');
})
.catch(() => {
if (!isCurrent()) return;
// A failed probe cannot prove a previous connection still exists.
setRemoteStatus(null);
setRemoteStatusState('unavailable');
});
const statusPromise = remoteConnectStatusSource.refresh().catch(() => undefined);

const relayPromise = account.loggedIn
? remoteConnectAPI.accountGetCredentialHint().then(hint => {
Expand Down Expand Up @@ -82,6 +70,7 @@ export function useDeviceInterconnectionOverview(fallbackLocalDeviceName: string

useEffect(() => {
const unlistenLogin = api.listen('account://login-state', () => {
remoteConnectStatusSource.invalidate();
void refresh();
});
const unlistenPresence = api.listen('account://device-presence', () => {
Expand Down Expand Up @@ -137,6 +126,7 @@ export function useDeviceInterconnectionOverview(fallbackLocalDeviceName: string
const localDeviceName = localDevice?.device_name?.trim() || fallbackLocalDeviceName;
const overview = useMemo(() => projectDeviceInterconnectionOverview({
localDeviceName,
fallbackMobileDeviceName,
peer,
remoteStatus,
remoteStatusState,
Expand All @@ -145,6 +135,7 @@ export function useDeviceInterconnectionOverview(fallbackLocalDeviceName: string
}), [
accountService,
localDeviceName,
fallbackMobileDeviceName,
peer,
projectedDispatchJobs,
remoteStatus,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { RemoteConnectStatus } from '@/infrastructure/api/service-api/RemoteConnectAPI';
import { selectRemoteNetworkConnection } from '@/infrastructure/remote-connect/remoteConnectionState';

export type DeviceOverviewMode = 'local' | 'connected';
export type DeviceOverviewDeviceKind =
Expand Down Expand Up @@ -144,6 +145,7 @@ export interface DeviceOverviewDispatchJob {

export interface DeviceInterconnectionOverviewInput {
localDeviceName: string;
fallbackMobileDeviceName?: string;
peer: { deviceId: string; deviceName: string } | null;
remoteStatus: RemoteConnectStatus | null;
remoteStatusState: 'loading' | 'ready' | 'unavailable';
Expand Down Expand Up @@ -287,16 +289,21 @@ export function projectDeviceInterconnectionOverview(

let connectionService = input.peer ? input.accountService : null;

if (input.remoteStatus?.is_connected) {
const network = selectRemoteNetworkConnection(input.remoteStatus);
if (network.connected && input.remoteStatus) {
addOrMergeDevice(devices, {
id: `mobile:${input.remoteStatus.peer_user_id ?? input.remoteStatus.peer_device_name ?? 'connected'}`,
name: formatDeviceDisplayName(input.remoteStatus.peer_device_name) || 'Mobile device',
name: (network.roomConnected ? formatDeviceDisplayName(input.remoteStatus.peer_device_name) : '')
|| input.fallbackMobileDeviceName || 'Mobile device',
kind: 'mobile',
local: false,
activities: ['controlling'],
backgroundTaskCount: 0,
});
connectionService ??= connectionServiceFromActiveMethod(input.remoteStatus.active_method);
connectionService ??= network.roomConnected
? connectionServiceFromActiveMethod(input.remoteStatus.active_method)
: connectionServiceFromRelayUrl(network.accountRelayUrl)
?? connectionServiceFromActiveMethod(input.remoteStatus.active_method);
}

// A paired bot contributes a controller and nothing else. It does not claim
Expand Down Expand Up @@ -354,6 +361,6 @@ export function projectDeviceInterconnectionOverview(
controllerCount,
backgroundTaskCount,
peerActive: input.peer !== null,
topologyUnavailable: mode === 'connected' && input.remoteStatusState === 'unavailable',
topologyUnavailable: input.remoteStatusState === 'unavailable',
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -404,11 +404,11 @@ describe('Remote Connect safety contracts', () => {
dialogSource.indexOf("activeView !== 'network'"),
);
const connectedRestore = restoreFlow.slice(
restoreFlow.indexOf('applyStatus(s)'),
restoreFlow.indexOf("if (['waiting_for_scan'"),
restoreFlow.indexOf('applyStatus(s, restoreSelection'),
restoreFlow.indexOf("if (!pendingOwnerRef.current"),
);

expect(applyStatus).toContain("remotePairingStateName(nextStatus.pairing_state) === 'connected'");
expect(applyStatus).toContain('selectRemoteNetworkConnection(nextStatus, connectionResultRef.current)');
expect(applyStatus).toContain('setNetworkTab(connectedTab)');
expect(applyStatus).toContain('setBotTab(connectedBot)');
expect(dialogSource).toContain("useState<ActiveView>(initialGroup ?? 'overview')");
Expand Down
Loading
Loading