import { Box, config, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text } from 'folds'; import { CallMembership } from 'matrix-js-sdk/lib/matrixrtc/CallMembership'; import React, { useEffect, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { Room } from 'matrix-js-sdk'; import { UserAvatar } from '../../components/user-avatar'; import { getMemberAvatarMxc, getMemberName } from '../../utils/room'; import { mxcUrlToHttp } from '../../utils/matrix'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { StackedAvatar } from '../../components/stacked-avatar'; import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile'; import { stopPropagation } from '../../utils/keyboard'; import { CallEmbed } from '../../plugins/call/CallEmbed'; import { CallControlEvent } from '../../plugins/call/CallControl'; import * as css from './styles.css'; // [Gitea #56] Subscribes to CallControl's focus pin so the menu can render a // "Focus camera" / "Unfocus camera" toggle instead of a one-way pin. function useFocusedUserId(callEmbed?: CallEmbed): string | null { const control = callEmbed?.control; const [focusedUserId, setFocusedUserId] = useState(control?.focusedUserId ?? null); useEffect(() => { if (!control) { setFocusedUserId(null); return undefined; } setFocusedUserId(control.focusedUserId); const handleUpdate = () => setFocusedUserId(control.focusedUserId); control.on(CallControlEvent.StateUpdate, handleUpdate); return () => { control.off(CallControlEvent.StateUpdate, handleUpdate); }; }, [control]); return focusedUserId; } type ParticipantMenuProps = { anchor: RectCords; name: string; userId: string; room: Room; callEmbed?: CallEmbed; onClose: () => void; profileCords: DOMRect; }; function ParticipantMenu({ anchor, name, userId, room, callEmbed, onClose, profileCords, }: ParticipantMenuProps) { const openUserProfile = useOpenUserRoomProfile(); const focusedUserId = useFocusedUserId(callEmbed); const isFocused = focusedUserId === userId; const handleViewProfile = () => { onClose(); openUserProfile(room.roomId, undefined, userId, profileCords, 'Top'); }; // [Gitea #56] Toggle: focusing the already-focused participant clears the // pin and returns EC to speaker-follows, instead of leaving no way back. const handleFocusCamera = () => { onClose(); if (isFocused) { callEmbed?.control.clearFocusParticipant(); } else { // [EC#30] Pass the fork's per-device media id when we have one (from // io.lotus.call_state) so a multi-device user pins the active device. const parts = callEmbed?.getLotusParticipants() ?? []; const mine = parts.filter((p) => p.userId === userId); const pick = mine.find((p) => p.speaking) ?? mine.find((p) => p.audioEnabled) ?? mine[0]; callEmbed?.control.focusCameraParticipant(userId, pick?.id ?? null); } }; return ( {name} {callEmbed && ( } onClick={handleFocusCamera} > {isFocused ? 'Unfocus camera' : 'Focus camera'} )} } onClick={handleViewProfile} > View profile } > {/* PopOut requires a JSX child even if we anchor externally */} ); } type MemberGlanceProps = { room: Room; members: CallMembership[]; speakers: Set; callEmbed?: CallEmbed; max?: number; }; export function MemberGlance({ room, members, speakers, callEmbed, max = 6 }: MemberGlanceProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const [menuState, setMenuState] = useState<{ anchor: RectCords; profileCords: DOMRect; userId: string; name: string; } | null>(null); const visibleMembers = members.slice(0, max); const remainingCount = max && members.length > max ? members.length - max : 0; return ( <> {visibleMembers.map((callMember) => { const { userId } = callMember; if (!userId) return null; const name = getMemberName(room, userId); const avatarMxc = getMemberAvatarMxc(room, userId); const avatarUrl = avatarMxc ? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined) : undefined; return ( { const rect = evt.currentTarget.getBoundingClientRect(); setMenuState({ anchor: rect, profileCords: rect, userId, name, }); }} > } /> ); })} {remainingCount > 0 && ( +{remainingCount} )} {menuState && ( setMenuState(null)} /> )} ); }