94 lines
3.9 KiB
TypeScript
94 lines
3.9 KiB
TypeScript
// Drive a self-managed Telecom call session alongside the active DM call
|
|
// (docs/plans/telecom_migration.md, Phase B). Mirror of
|
|
// useAndroidCallForegroundSync: keyed to the widget's JoinCall signal (via
|
|
// useCallJoined), not the mere presence of callEmbedAtom.
|
|
//
|
|
// What this gives us once TELECOM_ENABLED is flipped on:
|
|
// - the Vojo call is registered with the OS as a real call → cellular-call
|
|
// interop (an incoming GSM call holds/arbitrates against it), audio-focus
|
|
// arbitration, and a system "ongoing call" presence;
|
|
// - a remote/system Telecom disconnect (the user takes a GSM call, or a
|
|
// Bluetooth/Android-Auto "end call") tears the Vojo call down too;
|
|
// - Telecom owns the audio route, so its active endpoint is mirrored into the
|
|
// speaker-toggle atom (earpiece / speaker / Bluetooth all reflected).
|
|
//
|
|
// §4 (validated on Samsung One UI: the call gets system call-audio focus and
|
|
// the route endpoint flow emits cleanly): Telecom owns routing. The speaker
|
|
// toggle now drives Telecom endpoints (see useCallSpeaker) and AudioRoutePlugin
|
|
// is kept off the audio session while a Telecom call is live.
|
|
//
|
|
// Android-only, and additionally gated on TELECOM_ENABLED + API>=26 inside
|
|
// telecomCall — on web / iOS / old Android every call here is a no-op.
|
|
|
|
import { useEffect } from 'react';
|
|
import { useAtomValue, useSetAtom, useStore } from 'jotai';
|
|
import { callEmbedAtom, callSpeakerAtom } from '../state/callEmbed';
|
|
import { telecomCall, type TelecomRoute } from '../plugins/call/telecomCall';
|
|
import { useCallJoined } from './useCallEmbed';
|
|
|
|
export const useTelecomConnectionSync = (): void => {
|
|
const callEmbed = useAtomValue(callEmbedAtom);
|
|
const joined = useCallJoined(callEmbed);
|
|
const store = useStore();
|
|
const setSpeaker = useSetAtom(callSpeakerAtom);
|
|
|
|
useEffect(() => {
|
|
if (!telecomCall.enabled()) return undefined;
|
|
if (!callEmbed || !joined) return undefined;
|
|
|
|
let cancelled = false;
|
|
const removers: Array<() => void> = [];
|
|
const track = (p: ReturnType<typeof telecomCall.addListener>) => {
|
|
p.then((handle) => {
|
|
if (cancelled) handle.remove();
|
|
else removers.push(() => handle.remove());
|
|
}).catch(() => {
|
|
/* listener registration best-effort */
|
|
});
|
|
};
|
|
|
|
telecomCall
|
|
.startCall({
|
|
roomId: callEmbed.roomId,
|
|
// For a DM the room name resolves to the peer's display name; good
|
|
// enough for the system call label. Refined per-direction later.
|
|
displayName: callEmbed.room.name || 'Vojo',
|
|
video: !callEmbed.voiceOnly,
|
|
incoming: false,
|
|
})
|
|
.catch((err: unknown) => {
|
|
// eslint-disable-next-line no-console
|
|
console.warn('[telecom] startCall failed', err);
|
|
});
|
|
|
|
// A Telecom-side disconnect we did NOT initiate (interrupting cellular
|
|
// call, BT/Auto "end", system) ends the Vojo call too. hangup() lets
|
|
// Element Call tear LiveKit down; the resulting Hangup event clears the
|
|
// atom, which unmounts this effect and fires endCall() below. Idempotent:
|
|
// our own hangup re-entering here is a no-op via the current-embed guard.
|
|
track(
|
|
telecomCall.addListener('telecomDisconnect', () => {
|
|
if (store.get(callEmbedAtom) !== callEmbed) return;
|
|
callEmbed.hangup().catch(() => {
|
|
/* widget already gone */
|
|
});
|
|
})
|
|
);
|
|
|
|
// Telecom owns the route; reflect the active endpoint in the speaker-toggle
|
|
// atom so the UI tracks reality (speaker vs earpiece/Bluetooth/wired).
|
|
track(
|
|
telecomCall.addListener('telecomEndpoint', (data: { route: TelecomRoute }) => {
|
|
setSpeaker(data.route === 'SPEAKER');
|
|
})
|
|
);
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
removers.forEach((remove) => remove());
|
|
telecomCall.endCall().catch(() => {
|
|
/* best-effort teardown */
|
|
});
|
|
};
|
|
}, [callEmbed, joined, store, setSpeaker]);
|
|
};
|