2026-03-07 18:03:32 +11:00
|
|
|
|
import {
|
|
|
|
|
|
ClientWidgetApi,
|
|
|
|
|
|
IWidgetApiAcknowledgeResponseData,
|
|
|
|
|
|
IWidgetApiRequestData,
|
|
|
|
|
|
} from 'matrix-widget-api';
|
2026-09-25 12:09:34 -04:00
|
|
|
|
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
|
2026-03-07 18:03:32 +11:00
|
|
|
|
import { CallControl, CallControlEvent } from './CallControl';
|
|
|
|
|
|
import { CallControlState } from './CallControlState';
|
2026-09-25 12:09:34 -04:00
|
|
|
|
import type { CallEmbed } from './CallEmbed';
|
2026-03-07 18:03:32 +11:00
|
|
|
|
|
|
|
|
|
|
export const useClientWidgetApiEvent = <T>(
|
|
|
|
|
|
api: ClientWidgetApi | undefined,
|
|
|
|
|
|
type: string,
|
2026-05-21 23:30:50 -04:00
|
|
|
|
callback: (event: CustomEvent<T>) => void,
|
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,
|
2026-05-21 23:30:50 -04:00
|
|
|
|
data: T,
|
2026-03-07 18:03:32 +11:00
|
|
|
|
): Promise<IWidgetApiAcknowledgeResponseData> => api.transport.send(action, data),
|
2026-05-21 23:30:50 -04:00
|
|
|
|
[api],
|
2026-03-07 18:03:32 +11:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return sendWidgetAction;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-24 23:16:43 -04:00
|
|
|
|
const DEFAULT_CONTROL_STATE = new CallControlState(false, false, false);
|
|
|
|
|
|
|
|
|
|
|
|
export const useCallControlState = (control: CallControl | undefined): CallControlState => {
|
2026-09-23 21:53:43 -04:00
|
|
|
|
// 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,
|
|
|
|
|
|
}));
|
2026-03-07 18:03:32 +11:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-05-24 23:16:43 -04:00
|
|
|
|
if (!control) {
|
2026-09-23 21:53:43 -04:00
|
|
|
|
setEntry({ control, state: DEFAULT_CONTROL_STATE });
|
2026-05-24 23:16:43 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-23 21:53:43 -04:00
|
|
|
|
setEntry({ control, state: control.getState() });
|
|
|
|
|
|
const handleUpdate = () => setEntry({ control, state: control.getState() });
|
2026-03-07 18:03:32 +11:00
|
|
|
|
control.on(CallControlEvent.StateUpdate, handleUpdate);
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
control.off(CallControlEvent.StateUpdate, handleUpdate);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [control]);
|
|
|
|
|
|
|
2026-09-23 21:53:43 -04:00
|
|
|
|
if (entry.control !== control) return control?.getState() ?? DEFAULT_CONTROL_STATE;
|
|
|
|
|
|
return entry.state;
|
2026-03-07 18:03:32 +11:00
|
|
|
|
};
|
2026-09-25 12:09:34 -04: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);
|
|
|
|
|
|
};
|
2026-09-26 22:04:54 -04:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-09-27 12:48:12 -04:00
|
|
|
|
* [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.
|
2026-09-26 22:04:54 -04:00
|
|
|
|
*/
|
2026-09-27 12:48:12 -04:00
|
|
|
|
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);
|
2026-09-26 22:04:54 -04:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!control) {
|
2026-09-27 12:48:12 -04:00
|
|
|
|
setOpen(false);
|
2026-09-26 22:04:54 -04:00
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
2026-09-27 12:48:12 -04:00
|
|
|
|
const sync = () => setOpen(control.screensharePromptOpen);
|
2026-09-26 22:04:54 -04:00
|
|
|
|
sync();
|
|
|
|
|
|
return control.onFrameScreenshareChange(sync);
|
|
|
|
|
|
}, [control]);
|
2026-09-27 12:48:12 -04:00
|
|
|
|
return open;
|
2026-09-26 22:04:54 -04:00
|
|
|
|
};
|