Files
cinny/src/app/plugins/call/hooks.ts
T

121 lines
4.1 KiB
TypeScript
Raw Normal View History

+4
2026-03-07 18:03:32 +11:00
import {
ClientWidgetApi,
IWidgetApiAcknowledgeResponseData,
IWidgetApiRequestData,
} from 'matrix-widget-api';
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
+4
2026-03-07 18:03:32 +11:00
import { CallControl, CallControlEvent } from './CallControl';
import { CallControlState } from './CallControlState';
import type { CallEmbed } from './CallEmbed';
+4
2026-03-07 18:03:32 +11:00
export const useClientWidgetApiEvent = <T>(
api: ClientWidgetApi | undefined,
type: string,
callback: (event: CustomEvent<T>) => void,
+4
2026-03-07 18:03:32 +11:00
) => {
useEffect(() => {
api?.on(`action:${type}`, callback);
return () => {
api?.off(`action:${type}`, callback);
};
}, [api, type, callback]);
};
export const useSendClientWidgetApiAction = (api: ClientWidgetApi) => {
const sendWidgetAction = useCallback(
async <T extends IWidgetApiRequestData = IWidgetApiRequestData>(
action: string,
data: T,
+4
2026-03-07 18:03:32 +11:00
): Promise<IWidgetApiAcknowledgeResponseData> => api.transport.send(action, data),
[api],
+4
2026-03-07 18:03:32 +11:00
);
return sendWidgetAction;
};
const DEFAULT_CONTROL_STATE = new CallControlState(false, false, false);
export const useCallControlState = (control: CallControl | undefined): CallControlState => {
// Remember which control the stored state came from. When a call starts the
// control changes, and the effect below only catches up after a render; until
// then the old (default: all off) state would be returned for the NEW call —
// one render of "muted + deafened" that the thumbbar, SMTC and tray (#4)
// pushed to the OS as a flicker. Read the new control directly instead.
const [entry, setEntry] = useState(() => ({
control,
state: control?.getState() ?? DEFAULT_CONTROL_STATE,
}));
+4
2026-03-07 18:03:32 +11:00
useEffect(() => {
if (!control) {
setEntry({ control, state: DEFAULT_CONTROL_STATE });
return;
}
setEntry({ control, state: control.getState() });
const handleUpdate = () => setEntry({ control, state: control.getState() });
+4
2026-03-07 18:03:32 +11:00
control.on(CallControlEvent.StateUpdate, handleUpdate);
return () => {
control.off(CallControlEvent.StateUpdate, handleUpdate);
};
}, [control]);
if (entry.control !== control) return control?.getState() ?? DEFAULT_CONTROL_STATE;
return entry.state;
+4
2026-03-07 18:03:32 +11:00
};
const noSubscribe = () => () => undefined;
/** [Gitea #146] The local mic level (0–3 bars) reported by the call, or 0. */
export const useCallMicLevel = (callEmbed: CallEmbed | undefined): number => {
const subscribe = useCallback(
(cb: () => void) => (callEmbed ? callEmbed.onMicLevel(cb) : noSubscribe()),
[callEmbed],
);
return useSyncExternalStore(subscribe, () => callEmbed?.getMicLevel() ?? 0);
};
/**
* [Gitea #43] How the call bar's screenshare button behaves:
* - `hidden`: EC's own button is inside the frame (fork lotus.20/21), so ours hides;
* - `prompt`: starting asks the fork for its in-frame "Share your screen?"
* (no Capability Delegation in this engine);
* - `direct`: our own confirm, then a (delegated) set_screenshare.
*/
export type ScreenshareMode = 'hidden' | 'prompt' | 'direct';
const screenshareMode = (control: CallControl): ScreenshareMode => {
if (control.frameScreenshare) return 'hidden';
return control.screenshareNeedsPrompt ? 'prompt' : 'direct';
};
export const useScreenshareMode = (control: CallControl | undefined): ScreenshareMode => {
const [mode, setMode] = useState<ScreenshareMode>(() =>
control ? screenshareMode(control) : 'direct',
);
useEffect(() => {
if (!control) {
setMode('direct');
return undefined;
}
const sync = () => setMode(screenshareMode(control));
sync();
return control.onFrameScreenshareChange(sync);
}, [control]);
return mode;
};
/** [Gitea #43] The fork's in-frame "Share your screen?" prompt is showing. */
export const useScreensharePromptOpen = (control: CallControl | undefined): boolean => {
const [open, setOpen] = useState(() => control?.screensharePromptOpen ?? false);
useEffect(() => {
if (!control) {
setOpen(false);
return undefined;
}
const sync = () => setOpen(control.screensharePromptOpen);
sync();
return control.onFrameScreenshareChange(sync);
}, [control]);
return open;
};