feat: add call management features with useCall and useRoomCallMembers hooks

- Implemented useCall hook for managing call state and actions.
- Created useRoomCallMembers hook to track active call members in a room.
- Added useRtcConfig for fetching RTC configurations and LiveKit JWT.
- Developed Audio settings component for managing audio devices and settings.
- Introduced device selection and screen sharing options in the Audio settings.
- Persisted audio settings in localStorage for user preferences.
This commit is contained in:
2026-01-23 19:35:25 +11:00
parent c88cb4bca9
commit 94f8466d1c
19 changed files with 3270 additions and 14 deletions

View File

@@ -69,6 +69,8 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { InviteUserPrompt } from '../../components/invite-user-prompt';
import { useRoomCall, useRoomCallMembers } from '../call';
import { CallState, CallType } from '../call/types';
type RoomMenuProps = {
room: Room;
@@ -254,6 +256,170 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
);
});
type CallIndicatorProps = {
roomId: string;
};
/**
* Shows when others are in a call in this room
*/
function CallIndicator({ roomId }: CallIndicatorProps) {
const { othersInCall, isCallActive } = useRoomCallMembers(roomId);
if (!isCallActive || othersInCall.length === 0) {
return null;
}
const userCount = othersInCall.length;
const tooltipText = userCount === 1
? `${othersInCall[0].userId.split(':')[0].slice(1)} is in a call`
: `${userCount} people are in a call`;
return (
<TooltipProvider
position="Bottom"
offset={4}
tooltip={
<Tooltip>
<Text>{tooltipText}</Text>
</Tooltip>
}
>
{(triggerRef) => (
<Box
ref={triggerRef}
alignItems="Center"
gap="100"
style={{
padding: `${toRem(4)} ${toRem(8)}`,
borderRadius: toRem(12),
backgroundColor: 'var(--mx-positive-container)',
cursor: 'default',
}}
>
<Icon
size="100"
src={Icons.Phone}
style={{ color: 'var(--mx-positive)' }}
/>
<Text size="T200" style={{ color: 'var(--mx-positive)' }}>
{userCount} in call
</Text>
</Box>
)}
</TooltipProvider>
);
}
type RoomCallButtonsProps = {
roomId: string;
};
/**
* Component for voice/video call buttons in the room header
*/
function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
const {
isInCall,
callState,
activeCall,
startVoiceCall,
startVideoCall,
endCall,
callSupported,
callSupportLoading,
} = useRoomCall(roomId);
// Debug logging
React.useEffect(() => {
// eslint-disable-next-line no-console
console.log('RoomCallButtons render:', { callSupportLoading, callSupported, roomId });
}, [callSupportLoading, callSupported, roomId]);
if (callSupportLoading || !callSupported) {
return null;
}
const isConnecting = callState === CallState.Connecting;
const isConnected = callState === CallState.Connected;
const isActive = isConnecting || isConnected;
const handleVoiceCall = async () => {
if (isInCall) {
await endCall();
} else {
try {
await startVoiceCall();
} catch (error) {
console.error('Failed to start voice call:', error);
}
}
};
const handleVideoCall = async () => {
if (isInCall) {
await endCall();
} else {
try {
await startVideoCall();
} catch (error) {
console.error('Failed to start video call:', error);
}
}
};
return (
<>
<TooltipProvider
position="Bottom"
offset={4}
tooltip={
<Tooltip>
<Text>{isInCall ? 'End Call' : 'Voice Call'}</Text>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton
ref={triggerRef}
onClick={handleVoiceCall}
variant={isActive && activeCall?.callType === CallType.Voice ? 'Critical' : 'Background'}
>
{isConnecting ? (
<Spinner size="100" variant="Secondary" />
) : (
<Icon size="400" src={Icons.Phone} filled={isActive && activeCall?.callType === CallType.Voice} />
)}
</IconButton>
)}
</TooltipProvider>
<TooltipProvider
position="Bottom"
offset={4}
tooltip={
<Tooltip>
<Text>{isInCall ? 'End Call' : 'Video Call'}</Text>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton
ref={triggerRef}
onClick={handleVideoCall}
variant={isActive && activeCall?.callType === CallType.Video ? 'Critical' : 'Background'}
>
{isConnecting ? (
<Spinner size="100" variant="Secondary" />
) : (
<Icon size="400" src={Icons.VideoCamera} filled={isActive && activeCall?.callType === CallType.Video} />
)}
</IconButton>
)}
</TooltipProvider>
</>
);
}
export function RoomViewHeader() {
const navigate = useNavigate();
const mx = useMatrixClient();
@@ -369,7 +535,9 @@ export function RoomViewHeader() {
)}
</Box>
</Box>
<Box shrink="No">
<Box shrink="No" alignItems="Center" gap="100">
<CallIndicator roomId={room.roomId} />
<RoomCallButtons roomId={room.roomId} />
{!ecryptedRoom && (
<TooltipProvider
position="Bottom"