2026-03-07 18:03:32 +11:00
|
|
|
import { createContext, RefObject, useCallback, useContext, useEffect, useState } from 'react';
|
|
|
|
|
import { MatrixClient, Room } from 'matrix-js-sdk';
|
|
|
|
|
import { useSetAtom } from 'jotai';
|
|
|
|
|
import {
|
|
|
|
|
CallEmbed,
|
|
|
|
|
ElementCallThemeKind,
|
|
|
|
|
ElementWidgetActions,
|
|
|
|
|
useClientWidgetApiEvent,
|
|
|
|
|
} from '../plugins/call';
|
|
|
|
|
import { useMatrixClient } from './useMatrixClient';
|
|
|
|
|
import { ThemeKind, useTheme } from './useTheme';
|
|
|
|
|
import { callEmbedAtom } from '../state/callEmbed';
|
|
|
|
|
import { useResizeObserver } from './useResizeObserver';
|
|
|
|
|
import { CallControlState } from '../plugins/call/CallControlState';
|
2026-03-08 14:22:11 +11:00
|
|
|
import { useCallMembersChange, useCallSession } from './useCall';
|
2026-03-09 14:04:48 +11:00
|
|
|
import { CallPreferences } from '../state/callPreferences';
|
2026-05-14 11:07:10 -04:00
|
|
|
import { useSetting } from '../state/hooks/settings';
|
|
|
|
|
import { settingsAtom } from '../state/settings';
|
2026-03-07 18:03:32 +11:00
|
|
|
|
|
|
|
|
const CallEmbedContext = createContext<CallEmbed | undefined>(undefined);
|
|
|
|
|
|
|
|
|
|
export const CallEmbedContextProvider = CallEmbedContext.Provider;
|
|
|
|
|
|
|
|
|
|
export const useCallEmbed = (): CallEmbed | undefined => {
|
|
|
|
|
const callEmbed = useContext(CallEmbedContext);
|
|
|
|
|
|
|
|
|
|
return callEmbed;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const CallEmbedRefContext = createContext<RefObject<HTMLDivElement> | undefined>(undefined);
|
|
|
|
|
export const CallEmbedRefContextProvider = CallEmbedRefContext.Provider;
|
|
|
|
|
export const useCallEmbedRef = (): RefObject<HTMLDivElement> => {
|
|
|
|
|
const ref = useContext(CallEmbedRefContext);
|
|
|
|
|
if (!ref) {
|
|
|
|
|
throw new Error('CallEmbedRef is not provided!');
|
|
|
|
|
}
|
|
|
|
|
return ref;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const createCallEmbed = (
|
|
|
|
|
mx: MatrixClient,
|
|
|
|
|
room: Room,
|
|
|
|
|
dm: boolean,
|
|
|
|
|
themeKind: ElementCallThemeKind,
|
|
|
|
|
container: HTMLElement,
|
2026-05-14 11:07:10 -04:00
|
|
|
pref?: CallPreferences,
|
2026-05-14 18:54:09 -04:00
|
|
|
noiseSuppression = true,
|
2026-05-21 23:30:50 -04:00
|
|
|
forceAudioOff = false,
|
2026-03-07 18:03:32 +11:00
|
|
|
): CallEmbed => {
|
|
|
|
|
const rtcSession = mx.matrixRTC.getRoomSession(room);
|
2026-05-22 00:19:11 -04:00
|
|
|
const ongoing = rtcSession.memberships.length > 0;
|
2026-03-07 18:03:32 +11:00
|
|
|
|
2026-05-14 19:41:12 +10:00
|
|
|
const intent = CallEmbed.getIntent(dm, ongoing, pref?.video);
|
2026-05-21 23:30:50 -04:00
|
|
|
const initialAudio = forceAudioOff ? false : (pref?.microphone ?? true);
|
2026-05-14 18:54:09 -04:00
|
|
|
const initialVideo = pref?.video ?? false;
|
2026-05-21 20:49:33 -04:00
|
|
|
const widget = CallEmbed.getWidget(
|
|
|
|
|
mx,
|
|
|
|
|
room,
|
|
|
|
|
intent,
|
|
|
|
|
themeKind,
|
|
|
|
|
noiseSuppression,
|
|
|
|
|
initialAudio,
|
2026-05-21 23:30:50 -04:00
|
|
|
initialVideo,
|
2026-05-21 20:49:33 -04:00
|
|
|
);
|
|
|
|
|
const controlState =
|
|
|
|
|
pref && new CallControlState(forceAudioOff ? false : pref.microphone, pref.video, pref.sound);
|
2026-03-09 14:04:48 +11:00
|
|
|
|
2026-03-07 18:03:32 +11:00
|
|
|
const embed = new CallEmbed(mx, room, widget, container, controlState);
|
|
|
|
|
|
|
|
|
|
return embed;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const useCallStart = (dm = false) => {
|
|
|
|
|
const mx = useMatrixClient();
|
|
|
|
|
const theme = useTheme();
|
|
|
|
|
const setCallEmbed = useSetAtom(callEmbedAtom);
|
|
|
|
|
const callEmbedRef = useCallEmbedRef();
|
2026-05-14 11:07:10 -04:00
|
|
|
const [callNoiseSuppression] = useSetting(settingsAtom, 'callNoiseSuppression');
|
2026-05-14 18:54:09 -04:00
|
|
|
const [pttMode] = useSetting(settingsAtom, 'pttMode');
|
2026-03-07 18:03:32 +11:00
|
|
|
|
|
|
|
|
const startCall = useCallback(
|
2026-03-09 14:04:48 +11:00
|
|
|
(room: Room, pref?: CallPreferences) => {
|
2026-03-07 18:03:32 +11:00
|
|
|
const container = callEmbedRef.current;
|
|
|
|
|
if (!container) {
|
|
|
|
|
throw new Error('Failed to start call, No embed container element found!');
|
|
|
|
|
}
|
2026-05-21 20:49:33 -04:00
|
|
|
const callEmbed = createCallEmbed(
|
|
|
|
|
mx,
|
|
|
|
|
room,
|
|
|
|
|
dm,
|
|
|
|
|
theme.kind,
|
|
|
|
|
container,
|
|
|
|
|
pref,
|
|
|
|
|
callNoiseSuppression ?? true,
|
2026-05-21 23:30:50 -04:00
|
|
|
!!pttMode,
|
2026-05-21 20:49:33 -04:00
|
|
|
);
|
2026-03-07 18:03:32 +11:00
|
|
|
|
|
|
|
|
setCallEmbed(callEmbed);
|
|
|
|
|
},
|
2026-05-21 23:30:50 -04:00
|
|
|
[mx, dm, theme, setCallEmbed, callEmbedRef, callNoiseSuppression, pttMode],
|
2026-03-07 18:03:32 +11:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return startCall;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const useCallJoined = (embed?: CallEmbed): boolean => {
|
|
|
|
|
const [joined, setJoined] = useState(embed?.joined ?? false);
|
|
|
|
|
|
|
|
|
|
useClientWidgetApiEvent(
|
|
|
|
|
embed?.call,
|
|
|
|
|
ElementWidgetActions.JoinCall,
|
|
|
|
|
useCallback(() => {
|
|
|
|
|
setJoined(true);
|
2026-05-21 23:30:50 -04:00
|
|
|
}, []),
|
2026-03-07 18:03:32 +11:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!embed) {
|
|
|
|
|
setJoined(false);
|
|
|
|
|
}
|
|
|
|
|
}, [embed]);
|
|
|
|
|
|
|
|
|
|
return joined;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const useCallHangupEvent = (embed: CallEmbed, callback: () => void) => {
|
|
|
|
|
useClientWidgetApiEvent(embed.call, ElementWidgetActions.HangupCall, callback);
|
2026-05-14 19:41:12 +10:00
|
|
|
useClientWidgetApiEvent(embed.call, ElementWidgetActions.Close, callback);
|
2026-03-07 18:03:32 +11:00
|
|
|
};
|
|
|
|
|
|
2026-03-08 14:22:11 +11:00
|
|
|
export const useCallMemberSoundSync = (embed: CallEmbed) => {
|
|
|
|
|
const callSession = useCallSession(embed.room);
|
|
|
|
|
useCallMembersChange(
|
|
|
|
|
callSession,
|
2026-05-21 23:30:50 -04:00
|
|
|
useCallback(() => embed.control.applySound(), [embed]),
|
2026-03-08 14:22:11 +11:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-07 18:03:32 +11:00
|
|
|
export const useCallThemeSync = (embed: CallEmbed) => {
|
|
|
|
|
const theme = useTheme();
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const name: ElementCallThemeKind = theme.kind === ThemeKind.Dark ? 'dark' : 'light';
|
|
|
|
|
|
|
|
|
|
embed.setTheme(name);
|
|
|
|
|
}, [theme.kind, embed]);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-08 14:22:11 +11:00
|
|
|
export const useCallEmbedPlacementSync = (containerViewRef: RefObject<HTMLDivElement>): void => {
|
2026-03-07 18:03:32 +11:00
|
|
|
const callEmbedRef = useCallEmbedRef();
|
|
|
|
|
|
|
|
|
|
const syncCallEmbedPlacement = useCallback(() => {
|
|
|
|
|
const embedEl = callEmbedRef.current;
|
|
|
|
|
const container = containerViewRef.current;
|
|
|
|
|
if (!embedEl || !container) return;
|
|
|
|
|
|
|
|
|
|
embedEl.style.top = `${container.offsetTop}px`;
|
|
|
|
|
embedEl.style.left = `${container.offsetLeft}px`;
|
|
|
|
|
embedEl.style.width = `${container.clientWidth}px`;
|
|
|
|
|
embedEl.style.height = `${container.clientHeight}px`;
|
|
|
|
|
}, [callEmbedRef, containerViewRef]);
|
|
|
|
|
|
|
|
|
|
useResizeObserver(
|
|
|
|
|
syncCallEmbedPlacement,
|
2026-05-21 23:30:50 -04:00
|
|
|
useCallback(() => containerViewRef.current, [containerViewRef]),
|
2026-03-07 18:03:32 +11:00
|
|
|
);
|
|
|
|
|
};
|