vojo/src/app/features/call-status/IncomingCallStrip.tsx

160 lines
5.3 KiB
TypeScript

import React from 'react';
import { Avatar, Box, Icon, IconButton, Icons, Text, Tooltip, TooltipProvider } from 'folds';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Room } from 'matrix-js-sdk';
import { useSetAtom } from 'jotai';
import { useNavigate } from 'react-router-dom';
import * as css from './styles.css';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { UserAvatar } from '../../components/user-avatar';
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
import { getCanonicalAliasOrRoomId, getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { useSwitchOrStartDmCall } from '../../hooks/useSwitchOrStartDmCall';
import { getDirectRoomPath } from '../../pages/pathUtils';
import { IncomingCall, incomingCallsAtom } from '../../state/incomingCalls';
import { getIncomingCallKey } from '../../utils/rtcNotification';
import { CallPhoneDownIcon, CallPhoneIcon } from './callIcons';
const DECLINE_RETRY_DELAY_MS = 500;
type IncomingCallStripProps = {
call: IncomingCall;
room: Room;
};
export function IncomingCallStrip({ call, room }: IncomingCallStripProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const navigate = useNavigate();
const useAuthentication = useMediaAuthentication();
const switchOrStartDmCall = useSwitchOrStartDmCall();
const setIncoming = useSetAtom(incomingCallsAtom);
const senderId = call.notifEvent.getSender();
const displayName =
(senderId && getMemberDisplayName(room, senderId)) ||
(senderId && getMxIdLocalPart(senderId)) ||
senderId ||
t('Call.unknown_caller');
const avatarMxc = senderId ? getMemberAvatarMxc(room, senderId) : undefined;
const avatarUrl = avatarMxc
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined;
const callKey = getIncomingCallKey(call.callId, call.roomId);
const handleAnswer = () => {
// Mirror the native CallStyle Answer path (usePushNotifications
// pushNotificationActionPerformed handler): in-app strip Answer is the
// same semantic action — switch to the call room — so it should not
// grow the back stack either.
navigate(getDirectRoomPath(getCanonicalAliasOrRoomId(mx, call.roomId)), { replace: true });
switchOrStartDmCall(call.roomId)
.then(() => {
const evId = call.notifEvent.getId();
if (evId) {
setIncoming({ type: 'REMOVE_BY_NOTIF_ID', notifEventId: evId });
return;
}
setIncoming({ type: 'REMOVE', key: callKey });
})
.catch((err: unknown) => {
// eslint-disable-next-line no-console
console.warn('[call] strip answer switch/start failed', err);
});
};
const handleDecline = async () => {
setIncoming({ type: 'REMOVE', key: callKey });
const evId = call.notifEvent.getId();
if (!evId) return;
try {
await mx.sendRtcDecline(call.roomId, evId);
} catch (err) {
await new Promise((resolve) => {
setTimeout(resolve, DECLINE_RETRY_DELAY_MS);
});
try {
await mx.sendRtcDecline(call.roomId, evId);
} catch (retryErr) {
// eslint-disable-next-line no-console
console.warn('[call] sendRtcDecline failed after retry', retryErr);
}
}
};
return (
<Box
className={classNames(css.RingRow, ContainerColor({ variant: 'Background' }))}
shrink="No"
direction="Row"
alignItems="Center"
gap="500"
>
<Avatar className={css.RingAvatar}>
<UserAvatar
userId={senderId ?? ''}
src={avatarUrl}
alt={displayName}
renderFallback={() => <Icon size="400" src={Icons.User} filled />}
/>
</Avatar>
<Box grow="Yes" direction="Column" gap="100">
<Text size="H4" truncate>
{displayName}
</Text>
<Text size="T200" priority="300">
{t('Call.incoming')}
</Text>
</Box>
<Box shrink="No" alignItems="Center" gap="300">
<TooltipProvider
position="Top"
tooltip={
<Tooltip>
<Text size="T200">{t('Call.decline')}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant="Critical"
fill="Soft"
radii="Pill"
size="500"
onClick={handleDecline}
aria-label={t('Call.decline')}
>
<Icon size="300" src={CallPhoneDownIcon} />
</IconButton>
)}
</TooltipProvider>
<TooltipProvider
position="Top"
tooltip={
<Tooltip>
<Text size="T200">{t('Call.answer')}</Text>
</Tooltip>
}
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant="Success"
fill="Soft"
radii="Pill"
size="500"
onClick={handleAnswer}
aria-label={t('Call.answer')}
>
<Icon size="300" src={CallPhoneIcon} />
</IconButton>
)}
</TooltipProvider>
</Box>
</Box>
);
}