- io.lotus.request_state: when the fork's lotus handlers (re)register (an EC-side remount that doesn't unmount us) we re-send deafen, quality and the focus pin, and the decoration pusher re-pushes its roster — decorations and the pin no longer vanish for the rest of the call (element-call#17). - focus_participant carries the per-device media id from call_state (speaking device preferred) so a multi-device user pins the right device (element-call#30). - injectAudio returns the fork's reply; when it refuses with reason:"muted" the soundboard shows "Unmute your microphone…" instead of playing the clip locally as if it went out (element-call#13). All backwards compatible with the 0.25.0-lotus.1 bundle (unknown action is acked; missing reply fields default to "played"). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useRef, type ReactElement } from 'react';
|
|
import { type CallEmbed } from '../../plugins/call';
|
|
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
|
import { useAvatarDecoration } from '../../hooks/useAvatarDecoration';
|
|
import { decorationUrl } from './avatarDecorations';
|
|
|
|
/**
|
|
* [lotus #6] Pushes each call participant's avatar-decoration image URL to the
|
|
* forked Element Call (`io.lotus.decorations`), which renders it on the in-call
|
|
* video-tile avatars. Mounted only while joined, so the EC-side handler exists.
|
|
*
|
|
* The decoration roster is per-user slugs resolved via `useAvatarDecoration`;
|
|
* we render one invisible probe per member to reuse that hook + its cache, then
|
|
* debounce-send the aggregated `{ userId: url }` map whenever it changes.
|
|
*/
|
|
function DecorationProbe({
|
|
userId,
|
|
onResolve,
|
|
}: {
|
|
userId: string;
|
|
onResolve: (userId: string, url: string | null) => void;
|
|
}): null {
|
|
const slug = useAvatarDecoration(userId);
|
|
useEffect(() => {
|
|
onResolve(userId, slug ? decorationUrl(slug) : null);
|
|
}, [userId, slug, onResolve]);
|
|
return null;
|
|
}
|
|
|
|
export function LotusDecorationPusher({ callEmbed }: { callEmbed: CallEmbed }): ReactElement {
|
|
const session = useCallSession(callEmbed.room);
|
|
const members = useCallMembers(session);
|
|
const map = useRef<Map<string, string>>(new Map());
|
|
const pushTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
|
|
const userIds = useMemo(
|
|
() => Array.from(new Set(members.map((m) => m.userId).filter((u): u is string => !!u))),
|
|
[members],
|
|
);
|
|
|
|
const push = useCallback(() => {
|
|
const decorations: Record<string, string> = {};
|
|
map.current.forEach((url, userId) => {
|
|
decorations[userId] = url;
|
|
});
|
|
callEmbed.call.transport.send('io.lotus.decorations', { decorations }).catch(() => undefined);
|
|
}, [callEmbed]);
|
|
|
|
const schedulePush = useCallback(() => {
|
|
if (pushTimer.current) clearTimeout(pushTimer.current);
|
|
pushTimer.current = setTimeout(push, 300);
|
|
}, [push]);
|
|
|
|
// [Gitea #17] The fork asks for a re-push when its decoration handler
|
|
// (re)registers; our map hasn't changed, so the change-driven push above
|
|
// would never fire on its own.
|
|
useEffect(() => callEmbed.onForkStateRequest(schedulePush), [callEmbed, schedulePush]);
|
|
|
|
const onResolve = useCallback(
|
|
(userId: string, url: string | null) => {
|
|
const prev = map.current.get(userId);
|
|
if (url) {
|
|
if (prev !== url) {
|
|
map.current.set(userId, url);
|
|
schedulePush();
|
|
}
|
|
} else if (prev !== undefined) {
|
|
map.current.delete(userId);
|
|
schedulePush();
|
|
}
|
|
},
|
|
[schedulePush],
|
|
);
|
|
|
|
// Drop decorations for participants who left the call.
|
|
useEffect(() => {
|
|
const present = new Set(userIds);
|
|
let changed = false;
|
|
map.current.forEach((_url, userId) => {
|
|
if (!present.has(userId)) {
|
|
map.current.delete(userId);
|
|
changed = true;
|
|
}
|
|
});
|
|
if (changed) schedulePush();
|
|
}, [userIds, schedulePush]);
|
|
|
|
useEffect(
|
|
() => () => {
|
|
if (pushTimer.current) clearTimeout(pushTimer.current);
|
|
},
|
|
[],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{userIds.map((userId) => (
|
|
<DecorationProbe key={userId} userId={userId} onResolve={onResolve} />
|
|
))}
|
|
</>
|
|
);
|
|
}
|