call: consume self-built Element Call fork + activate Lotus features
Switch to @lotusguild/element-call-embedded@0.20.1-lotus.1 (our self-built fork) and turn on the source-level features it adds: - #1 denoise CUTOVER: in-source ML denoise (lotusDenoiseSource=1) replaces the build-time getUserMedia shim — removed the shim injection from vite.config.js (denoise/ assets still shipped; the processor loads them). Survives reconnects (fixes A7). - #2 call-state: CallEmbed consumes io.lotus.call_state; useCallSpeakers / useRemoteAllMuted prefer it over scraping EC's DOM (DOM fallback kept; empty payloads ignored). - #4 focus: CallControl.focusCameraParticipant sends io.lotus.focus_participant (works during screenshare), replacing the DOM tile-click hack. - #5 theming: lotusTransparent=1 (native transparent background). - #6 decorations: LotusDecorationPusher sends each member's decoration URL via io.lotus.decorations -> rendered on in-call tiles. #3 soundboard / #7 quality ship dormant (EC-ready; no host UI sends them yet). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
||||
import { LotusDecorationPusher } from '../lotus/LotusDecorationPusher';
|
||||
import { useStateEvent } from '../../hooks/useStateEvent';
|
||||
import { VoiceLimitContent } from '../common-settings/general/RoomVoiceLimit';
|
||||
import { CallMemberRenderer } from './CallMemberCard';
|
||||
@@ -199,6 +200,8 @@ function CallJoined({ joined, containerRef }: CallJoinedProps) {
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Box grow="Yes" ref={containerRef} />
|
||||
{callEmbed && joined && <CallControls callEmbed={callEmbed} />}
|
||||
{/* [lotus #6] push avatar decorations to EC's in-call tiles (post-join) */}
|
||||
{callEmbed && joined && <LotusDecorationPusher callEmbed={callEmbed} />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
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;
|
||||
});
|
||||
void 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]);
|
||||
|
||||
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} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user