Files
cinny/src/app/hooks/useCallEmbed.ts
T
root 8ebb1a8d8c chore: merge v4.12.1 — security, calling, editor, media fixes
Key v4.12.1 changes merged:
- Security: sanitize-html updated to v2.17.4
- Calling: video calls in DMs/rooms, user avatars during calls, right-click to start
- Calling: IncomingCallListener with ring sound and answer/reject UI
- Editor: list crash fixes (Firefox + empty headings), codeblock filename support
- Media: URL preview hover state, keyboard nav, click-to-open, OGG audio support
- Date: ISO 8601 (YYYY-MM-DD) date format option
- Misc: stable mutual rooms endpoint, Android notification crash fix

Lotus customisations preserved:
- PiP drag/resize, DM call ring notification, PTT, GIF picker, noise suppression
- Poll voting, message forwarding, image captions, location sharing
- Lotus Terminal design theme
2026-05-15 13:43:04 -04:00

152 lines
5.0 KiB
TypeScript

import { createContext, RefObject, useCallback, useContext, useEffect, useState } from 'react';
import { MatrixRTCSession } from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSession';
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 =
MatrixRTCSession.sessionMembershipsForRoom(room, rtcSession.sessionDescription).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);
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;
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,
useCallback(() => containerViewRef.current, [containerViewRef])
);
};