2025-05-08 17:57:57 -05:00
|
|
|
import { Room } from 'matrix-js-sdk';
|
2026-02-10 22:55:26 -05:00
|
|
|
import React, {
|
|
|
|
|
useContext,
|
|
|
|
|
useCallback,
|
|
|
|
|
useEffect,
|
|
|
|
|
useRef,
|
|
|
|
|
MouseEventHandler,
|
|
|
|
|
useState,
|
|
|
|
|
ReactNode,
|
|
|
|
|
} from 'react';
|
2026-02-09 01:06:08 -05:00
|
|
|
import { Box, Button, config, Spinner, Text } from 'folds';
|
2025-05-22 19:57:40 -05:00
|
|
|
import { useCallState } from '../../pages/client/call/CallProvider';
|
2026-02-09 00:45:48 -05:00
|
|
|
import { useCallMembers } from '../../hooks/useCallMemberships';
|
|
|
|
|
|
2026-02-10 22:55:26 -05:00
|
|
|
import { CallRefContext } from '../../pages/client/call/PersistentCallContainer';
|
2025-05-10 08:58:03 -05:00
|
|
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
2025-07-31 21:05:15 +02:00
|
|
|
import { useDebounce } from '../../hooks/useDebounce';
|
2026-02-09 00:45:48 -05:00
|
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
|
|
|
import { CallViewUser } from './CallViewUser';
|
|
|
|
|
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
|
|
|
|
import { getMemberDisplayName } from '../../utils/room';
|
|
|
|
|
import { getMxIdLocalPart } from '../../utils/matrix';
|
2026-02-10 22:55:26 -05:00
|
|
|
import * as css from './CallView.css';
|
2025-05-08 17:57:57 -05:00
|
|
|
|
|
|
|
|
type OriginalStyles = {
|
|
|
|
|
position?: string;
|
|
|
|
|
top?: string;
|
|
|
|
|
left?: string;
|
|
|
|
|
width?: string;
|
|
|
|
|
height?: string;
|
|
|
|
|
zIndex?: string;
|
|
|
|
|
display?: string;
|
|
|
|
|
visibility?: string;
|
|
|
|
|
pointerEvents?: string;
|
|
|
|
|
border?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-09 00:45:48 -05:00
|
|
|
export function CallViewUserGrid({ children }: { children: ReactNode }) {
|
|
|
|
|
return (
|
2026-02-10 22:55:26 -05:00
|
|
|
<Box
|
|
|
|
|
className={css.CallViewUserGrid}
|
|
|
|
|
style={{
|
|
|
|
|
maxWidth: React.Children.count(children) === 4 ? '336px' : '503px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-02-09 00:45:48 -05:00
|
|
|
{children}
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-18 17:26:57 -05:00
|
|
|
export function CallView({ room }: { room: Room }) {
|
2026-02-09 22:17:28 -05:00
|
|
|
const callIframeRef = useContext(CallRefContext);
|
2025-05-08 17:57:57 -05:00
|
|
|
const iframeHostRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
const originalIframeStylesRef = useRef<OriginalStyles | null>(null);
|
2026-02-09 00:45:48 -05:00
|
|
|
const mx = useMatrixClient();
|
2026-02-10 22:55:26 -05:00
|
|
|
|
|
|
|
|
const [visibleCallNames, setVisibleCallNames] = useState('');
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
isActiveCallReady,
|
|
|
|
|
activeCallRoomId,
|
|
|
|
|
isChatOpen,
|
|
|
|
|
setActiveCallRoomId,
|
|
|
|
|
hangUp,
|
|
|
|
|
setViewedCallRoomId,
|
2026-02-09 00:45:48 -05:00
|
|
|
} = useCallState();
|
|
|
|
|
|
2026-02-10 22:55:26 -05:00
|
|
|
const isActiveCallRoom = activeCallRoomId === room.roomId;
|
2026-02-09 22:17:28 -05:00
|
|
|
const callIsCurrentAndReady = isActiveCallRoom && isActiveCallReady;
|
2026-02-10 22:55:26 -05:00
|
|
|
const callMembers = useCallMembers(mx, room.roomId);
|
|
|
|
|
|
|
|
|
|
const getName = (userId: string) =>
|
|
|
|
|
getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId);
|
2026-02-09 00:45:48 -05:00
|
|
|
|
2026-02-10 22:55:26 -05:00
|
|
|
const memberDisplayNames = callMembers.map((callMembership) =>
|
|
|
|
|
getName(callMembership.sender ?? '')
|
|
|
|
|
);
|
2026-02-09 00:45:48 -05:00
|
|
|
|
|
|
|
|
const { navigateRoom } = useRoomNavigate();
|
2025-05-10 08:58:03 -05:00
|
|
|
const screenSize = useScreenSizeContext();
|
2026-02-09 00:45:48 -05:00
|
|
|
const isMobile = screenSize === ScreenSize.Mobile;
|
|
|
|
|
|
2026-02-10 22:55:26 -05:00
|
|
|
const activeIframeDisplayRef = callIframeRef;
|
2025-05-08 17:57:57 -05:00
|
|
|
|
|
|
|
|
const applyFixedPositioningToIframe = useCallback(() => {
|
2025-05-09 09:38:43 -05:00
|
|
|
const iframeElement = activeIframeDisplayRef?.current;
|
2025-05-08 17:57:57 -05:00
|
|
|
const hostElement = iframeHostRef?.current;
|
|
|
|
|
|
|
|
|
|
if (iframeElement && hostElement) {
|
|
|
|
|
if (!originalIframeStylesRef.current) {
|
|
|
|
|
const computed = window.getComputedStyle(iframeElement);
|
|
|
|
|
originalIframeStylesRef.current = {
|
|
|
|
|
position: iframeElement.style.position || computed.position,
|
|
|
|
|
top: iframeElement.style.top || computed.top,
|
|
|
|
|
left: iframeElement.style.left || computed.left,
|
|
|
|
|
width: iframeElement.style.width || computed.width,
|
|
|
|
|
height: iframeElement.style.height || computed.height,
|
|
|
|
|
zIndex: iframeElement.style.zIndex || computed.zIndex,
|
|
|
|
|
display: iframeElement.style.display || computed.display,
|
|
|
|
|
visibility: iframeElement.style.visibility || computed.visibility,
|
|
|
|
|
pointerEvents: iframeElement.style.pointerEvents || computed.pointerEvents,
|
|
|
|
|
border: iframeElement.style.border || computed.border,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hostRect = hostElement.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
iframeElement.style.position = 'fixed';
|
|
|
|
|
iframeElement.style.top = `${hostRect.top}px`;
|
|
|
|
|
iframeElement.style.left = `${hostRect.left}px`;
|
|
|
|
|
iframeElement.style.width = `${hostRect.width}px`;
|
|
|
|
|
iframeElement.style.height = `${hostRect.height}px`;
|
|
|
|
|
iframeElement.style.border = 'none';
|
2025-05-09 09:38:43 -05:00
|
|
|
iframeElement.style.zIndex = '1000';
|
2025-05-09 14:17:25 -05:00
|
|
|
iframeElement.style.display = room.isCallRoom() ? 'block' : 'none';
|
2025-05-08 17:57:57 -05:00
|
|
|
iframeElement.style.visibility = 'visible';
|
|
|
|
|
iframeElement.style.pointerEvents = 'auto';
|
|
|
|
|
}
|
2025-05-10 20:41:06 -05:00
|
|
|
}, [activeIframeDisplayRef, room]);
|
2025-05-08 17:57:57 -05:00
|
|
|
|
2025-07-31 21:05:15 +02:00
|
|
|
const debouncedApplyFixedPositioning = useDebounce(applyFixedPositioningToIframe, {
|
|
|
|
|
wait: 50,
|
|
|
|
|
immediate: false,
|
|
|
|
|
});
|
2025-05-08 17:57:57 -05:00
|
|
|
useEffect(() => {
|
2025-05-09 09:38:43 -05:00
|
|
|
const iframeElement = activeIframeDisplayRef?.current;
|
2025-05-08 17:57:57 -05:00
|
|
|
const hostElement = iframeHostRef?.current;
|
|
|
|
|
|
2026-02-09 22:17:28 -05:00
|
|
|
if (room.isCallRoom() || (callIsCurrentAndReady && iframeElement && hostElement)) {
|
2025-05-08 17:57:57 -05:00
|
|
|
applyFixedPositioningToIframe();
|
|
|
|
|
|
|
|
|
|
const resizeObserver = new ResizeObserver(debouncedApplyFixedPositioning);
|
2025-07-31 21:05:15 +02:00
|
|
|
if (hostElement) resizeObserver.observe(hostElement);
|
2025-05-08 17:57:57 -05:00
|
|
|
window.addEventListener('scroll', debouncedApplyFixedPositioning, true);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
resizeObserver.disconnect();
|
|
|
|
|
window.removeEventListener('scroll', debouncedApplyFixedPositioning, true);
|
|
|
|
|
|
|
|
|
|
if (iframeElement && originalIframeStylesRef.current) {
|
|
|
|
|
const originalStyles = originalIframeStylesRef.current;
|
|
|
|
|
(Object.keys(originalStyles) as Array<keyof OriginalStyles>).forEach((key) => {
|
2025-05-09 09:38:43 -05:00
|
|
|
if (key in iframeElement.style) {
|
|
|
|
|
iframeElement.style[key as any] = originalStyles[key] || '';
|
|
|
|
|
}
|
2025-05-08 17:57:57 -05:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
originalIframeStylesRef.current = null;
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-07-31 21:05:15 +02:00
|
|
|
|
|
|
|
|
return undefined;
|
2025-05-22 19:57:40 -05:00
|
|
|
}, [
|
|
|
|
|
activeIframeDisplayRef,
|
|
|
|
|
applyFixedPositioningToIframe,
|
|
|
|
|
debouncedApplyFixedPositioning,
|
2026-02-09 22:17:28 -05:00
|
|
|
callIsCurrentAndReady,
|
2025-05-22 19:57:40 -05:00
|
|
|
room,
|
|
|
|
|
]);
|
2025-05-08 17:57:57 -05:00
|
|
|
|
2026-02-09 00:45:48 -05:00
|
|
|
const handleJoinVCClick: MouseEventHandler<HTMLElement> = (evt) => {
|
2026-02-10 22:55:26 -05:00
|
|
|
if (isMobile) {
|
|
|
|
|
evt.stopPropagation();
|
|
|
|
|
setViewedCallRoomId(room.roomId);
|
|
|
|
|
navigateRoom(room.roomId);
|
|
|
|
|
}
|
|
|
|
|
if (!callIsCurrentAndReady) {
|
|
|
|
|
hangUp();
|
|
|
|
|
setActiveCallRoomId(room.roomId);
|
|
|
|
|
}
|
2026-02-09 00:45:48 -05:00
|
|
|
};
|
|
|
|
|
|
2025-07-31 21:05:15 +02:00
|
|
|
const isCallViewVisible = room.isCallRoom() && (screenSize === ScreenSize.Desktop || !isChatOpen);
|
2025-05-09 09:38:43 -05:00
|
|
|
|
2026-02-09 00:45:48 -05:00
|
|
|
useEffect(() => {
|
2026-02-10 22:55:26 -05:00
|
|
|
if (memberDisplayNames.length <= 2) {
|
|
|
|
|
setVisibleCallNames(memberDisplayNames.join(' and '));
|
2026-02-09 00:45:48 -05:00
|
|
|
} else {
|
|
|
|
|
const visible = memberDisplayNames.slice(0, 2);
|
|
|
|
|
const remaining = memberDisplayNames.length - 2;
|
|
|
|
|
|
2026-02-10 22:55:26 -05:00
|
|
|
setVisibleCallNames(
|
|
|
|
|
`${visible.join(', ')}, and ${remaining} other${remaining > 1 ? 's' : ''}`
|
|
|
|
|
);
|
2026-02-09 00:45:48 -05:00
|
|
|
}
|
2026-02-10 22:55:26 -05:00
|
|
|
}, [memberDisplayNames]);
|
2026-02-09 00:45:48 -05:00
|
|
|
|
2025-05-08 17:57:57 -05:00
|
|
|
return (
|
2025-07-31 21:05:15 +02:00
|
|
|
<Box grow="Yes" direction="Column" style={{ display: isCallViewVisible ? 'flex' : 'none' }}>
|
2025-05-08 17:57:57 -05:00
|
|
|
<div
|
|
|
|
|
ref={iframeHostRef}
|
|
|
|
|
style={{
|
|
|
|
|
width: '100%',
|
|
|
|
|
height: '100%',
|
|
|
|
|
position: 'relative',
|
|
|
|
|
pointerEvents: 'none',
|
2026-02-09 22:17:28 -05:00
|
|
|
display: callIsCurrentAndReady ? 'flex' : 'none',
|
2025-05-08 17:57:57 -05:00
|
|
|
}}
|
|
|
|
|
/>
|
2026-02-10 22:55:26 -05:00
|
|
|
<Box
|
|
|
|
|
grow="Yes"
|
|
|
|
|
justifyContent="Center"
|
|
|
|
|
alignItems="Center"
|
|
|
|
|
direction="Column"
|
|
|
|
|
gap="300"
|
|
|
|
|
style={{
|
|
|
|
|
display: callIsCurrentAndReady ? 'none' : 'flex',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-02-09 00:45:48 -05:00
|
|
|
<CallViewUserGrid>
|
2026-02-10 22:55:26 -05:00
|
|
|
{callMembers
|
|
|
|
|
.map((callMember) => <CallViewUser room={room} callMembership={callMember} />)
|
|
|
|
|
.slice(0, 6)}
|
2026-02-09 00:45:48 -05:00
|
|
|
</CallViewUserGrid>
|
|
|
|
|
|
2026-02-10 22:55:26 -05:00
|
|
|
<Box
|
|
|
|
|
direction="Column"
|
|
|
|
|
alignItems="Center"
|
|
|
|
|
style={{
|
|
|
|
|
paddingBlock: config.space.S200,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Text
|
|
|
|
|
size="H1"
|
|
|
|
|
style={{
|
|
|
|
|
paddingBottom: config.space.S300,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{room.name}
|
|
|
|
|
</Text>
|
|
|
|
|
<Text size="T200">
|
|
|
|
|
{visibleCallNames !== '' ? visibleCallNames : 'No one'}{' '}
|
|
|
|
|
{memberDisplayNames.length > 1 ? 'are' : 'is'} currently in voice
|
|
|
|
|
</Text>
|
2026-02-09 00:45:48 -05:00
|
|
|
</Box>
|
2026-02-10 22:55:26 -05:00
|
|
|
<Button variant="Secondary" disabled={isActiveCallRoom} onClick={handleJoinVCClick}>
|
2026-02-09 00:45:48 -05:00
|
|
|
{isActiveCallRoom ? (
|
2026-02-10 22:55:26 -05:00
|
|
|
<Box justifyContent="Center" alignItems="Center" gap="200">
|
2026-02-09 00:45:48 -05:00
|
|
|
<Spinner />
|
2026-02-10 22:55:26 -05:00
|
|
|
<Text size="B500">{activeCallRoomId === room.roomId ? `Joining` : 'Join Voice'}</Text>
|
2026-02-09 00:45:48 -05:00
|
|
|
</Box>
|
|
|
|
|
) : (
|
|
|
|
|
<Text size="B500">Join Voice</Text>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
2025-05-08 17:57:57 -05:00
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|