37e68e906b
Auto-fixed by prettier --write. Patch scripts used in the previous session wrote code without running the formatter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
180 lines
5.3 KiB
TypeScript
180 lines
5.3 KiB
TypeScript
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';
|
|
import { useCallMembersChange, useCallSession } from './useCall';
|
|
import { CallPreferences } from '../state/callPreferences';
|
|
import { useSetting } from '../state/hooks/settings';
|
|
import { settingsAtom } from '../state/settings';
|
|
|
|
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,
|
|
pref?: CallPreferences,
|
|
noiseSuppression = true,
|
|
forceAudioOff = false,
|
|
): CallEmbed => {
|
|
const rtcSession = mx.matrixRTC.getRoomSession(room);
|
|
const ongoing = rtcSession.memberships.length > 0;
|
|
|
|
const intent = CallEmbed.getIntent(dm, ongoing, pref?.video);
|
|
const initialAudio = forceAudioOff ? false : (pref?.microphone ?? true);
|
|
const initialVideo = pref?.video ?? false;
|
|
const widget = CallEmbed.getWidget(
|
|
mx,
|
|
room,
|
|
intent,
|
|
themeKind,
|
|
noiseSuppression,
|
|
initialAudio,
|
|
initialVideo,
|
|
);
|
|
const controlState =
|
|
pref && new CallControlState(forceAudioOff ? false : pref.microphone, pref.video, pref.sound);
|
|
|
|
const embed = new CallEmbed(mx, room, widget, container, controlState, themeKind);
|
|
|
|
return embed;
|
|
};
|
|
|
|
export const useCallStart = (dm = false) => {
|
|
const mx = useMatrixClient();
|
|
const theme = useTheme();
|
|
const setCallEmbed = useSetAtom(callEmbedAtom);
|
|
const callEmbedRef = useCallEmbedRef();
|
|
const [callNoiseSuppression] = useSetting(settingsAtom, 'callNoiseSuppression');
|
|
const [pttMode] = useSetting(settingsAtom, 'pttMode');
|
|
|
|
const startCall = useCallback(
|
|
(room: Room, pref?: CallPreferences) => {
|
|
const container = callEmbedRef.current;
|
|
if (!container) {
|
|
throw new Error('Failed to start call, No embed container element found!');
|
|
}
|
|
const callEmbed = createCallEmbed(
|
|
mx,
|
|
room,
|
|
dm,
|
|
theme.kind,
|
|
container,
|
|
pref,
|
|
callNoiseSuppression ?? true,
|
|
!!pttMode,
|
|
);
|
|
|
|
setCallEmbed(callEmbed);
|
|
},
|
|
[mx, dm, theme, setCallEmbed, callEmbedRef, callNoiseSuppression, pttMode],
|
|
);
|
|
|
|
return startCall;
|
|
};
|
|
|
|
export const useCallJoined = (embed?: CallEmbed): boolean => {
|
|
const [joined, setJoined] = useState(embed?.joined ?? false);
|
|
|
|
useClientWidgetApiEvent(
|
|
embed?.call,
|
|
ElementWidgetActions.JoinCall,
|
|
useCallback(() => {
|
|
setJoined(true);
|
|
}, []),
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!embed) {
|
|
setJoined(false);
|
|
}
|
|
}, [embed]);
|
|
|
|
return joined;
|
|
};
|
|
|
|
export const useCallHangupEvent = (embed: CallEmbed, callback: () => void) => {
|
|
useClientWidgetApiEvent(embed.call, ElementWidgetActions.HangupCall, callback);
|
|
useClientWidgetApiEvent(embed.call, ElementWidgetActions.Close, callback);
|
|
};
|
|
|
|
export const useCallMemberSoundSync = (embed: CallEmbed) => {
|
|
const callSession = useCallSession(embed.room);
|
|
useCallMembersChange(
|
|
callSession,
|
|
useCallback(() => embed.control.applySound(), [embed]),
|
|
);
|
|
};
|
|
|
|
export const useCallThemeSync = (embed: CallEmbed) => {
|
|
const theme = useTheme();
|
|
|
|
useEffect(() => {
|
|
const name: ElementCallThemeKind = theme.kind === ThemeKind.Dark ? 'dark' : 'light';
|
|
|
|
embed.setTheme(name);
|
|
}, [theme.kind, embed]);
|
|
};
|
|
|
|
export const useCallEmbedPlacementSync = (containerViewRef: RefObject<HTMLDivElement>): void => {
|
|
const callEmbedRef = useCallEmbedRef();
|
|
|
|
const syncCallEmbedPlacement = useCallback(() => {
|
|
const embedEl = callEmbedRef.current;
|
|
const container = containerViewRef.current;
|
|
if (!embedEl || !container) return;
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
console.log('[CallEmbed] syncCallEmbedPlacement:', {
|
|
top: rect.top,
|
|
left: rect.left,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
});
|
|
embedEl.style.top = `${rect.top}px`;
|
|
embedEl.style.left = `${rect.left}px`;
|
|
embedEl.style.width = `${rect.width}px`;
|
|
embedEl.style.height = `${rect.height}px`;
|
|
}, [callEmbedRef, containerViewRef]);
|
|
|
|
// Sync once on mount so the embed is positioned immediately (deps are stable refs)
|
|
useEffect(() => {
|
|
syncCallEmbedPlacement();
|
|
}, [syncCallEmbedPlacement]);
|
|
|
|
useResizeObserver(
|
|
syncCallEmbedPlacement,
|
|
useCallback(() => containerViewRef.current, [containerViewRef]),
|
|
);
|
|
};
|