feat(call): add sound effects and incoming call notification UI
- Implemented sound management for call events including mute, unmute, deafen, undeafen, call joined, call depart, and dialing sounds. - Created a new CallSounds class to handle sound playback and state management. - Added IncomingCallNotification component to display incoming call alerts with caller information and action buttons. - Styled the incoming call notification using vanilla-extract CSS for animations and layout. - Integrated sound playback for incoming calls with a 15-second threshold for dialing sound.
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
RectCords,
|
||||
Badge,
|
||||
Spinner,
|
||||
color,
|
||||
} from 'folds';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { JoinRule, Room } from 'matrix-js-sdk';
|
||||
@@ -71,6 +72,8 @@ import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||
import { useRoomCall, useRoomCallMembers } from '../call';
|
||||
import { CallState, CallType } from '../call/types';
|
||||
import { UserAvatar } from '../../components/user-avatar';
|
||||
import { getMemberDisplayName, getMemberAvatarMxc } from '../../utils/room';
|
||||
|
||||
type RoomMenuProps = {
|
||||
room: Room;
|
||||
@@ -261,53 +264,106 @@ type CallIndicatorProps = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows when others are in a call in this room
|
||||
* Shows all users currently in a call in this room (including self)
|
||||
*/
|
||||
function CallIndicator({ roomId }: CallIndicatorProps) {
|
||||
const { othersInCall, isCallActive } = useRoomCallMembers(roomId);
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const { callMembers, isCallActive } = useRoomCallMembers(roomId);
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
if (!isCallActive || othersInCall.length === 0) {
|
||||
if (!isCallActive || callMembers.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`;
|
||||
const getAvatarUrl = (userId: string): string | undefined => {
|
||||
if (!room) return undefined;
|
||||
const mxcUrl = getMemberAvatarMxc(room, userId);
|
||||
if (!mxcUrl) return undefined;
|
||||
return mxcUrlToHttp(mx, mxcUrl, useAuthentication, 24, 24, 'crop') ?? undefined;
|
||||
};
|
||||
|
||||
const getDisplayName = (userId: string): string => {
|
||||
if (!room) return userId.split(':')[0].slice(1);
|
||||
return getMemberDisplayName(room, userId) ?? userId.split(':')[0].slice(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>{tooltipText}</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
<Box
|
||||
alignItems="Center"
|
||||
gap="100"
|
||||
style={{
|
||||
padding: `${toRem(4)} ${toRem(8)}`,
|
||||
borderRadius: toRem(12),
|
||||
backgroundColor: 'var(--mx-positive-container)',
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
{(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)' }}
|
||||
/>
|
||||
<Box alignItems="Center" gap="100">
|
||||
{callMembers.slice(0, 5).map((member) => {
|
||||
const displayName = getDisplayName(member.userId);
|
||||
const avatarUrl = getAvatarUrl(member.userId);
|
||||
return (
|
||||
<TooltipProvider
|
||||
key={member.userId}
|
||||
position="Bottom"
|
||||
offset={8}
|
||||
tooltip={
|
||||
<Box
|
||||
direction="Column"
|
||||
gap="50"
|
||||
style={{
|
||||
padding: `${toRem(8)} ${toRem(12)}`,
|
||||
backgroundColor: color.SurfaceVariant.Container,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
borderRadius: toRem(8),
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Speech bubble arrow */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: toRem(-6),
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: `${toRem(6)} solid transparent`,
|
||||
borderRight: `${toRem(6)} solid transparent`,
|
||||
borderBottom: `${toRem(6)} solid ${color.SurfaceVariant.Container}`,
|
||||
}}
|
||||
/>
|
||||
<Text size="T300" style={{ fontWeight: 600, color: color.SurfaceVariant.OnContainer }}>{displayName}</Text>
|
||||
<Text size="T200" style={{ opacity: 0.7, color: color.SurfaceVariant.OnContainer }}>{member.userId}</Text>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<Avatar ref={triggerRef} size="200">
|
||||
<UserAvatar
|
||||
userId={member.userId}
|
||||
src={avatarUrl}
|
||||
alt={displayName}
|
||||
renderFallback={() => (
|
||||
<Text size="T200">
|
||||
{displayName.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
);
|
||||
})}
|
||||
{callMembers.length > 5 && (
|
||||
<Text size="T200" style={{ color: 'var(--mx-positive)' }}>
|
||||
{userCount} in call
|
||||
+{callMembers.length - 5}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user