Files
cinny/src/app/hooks/useCallEmbed.ts
T
jared 37e68e906b fix: prettier formatting in CallEmbedProvider and useCallEmbed
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>
2026-05-23 00:51:39 -04:00

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]),
);
};