160 lines
5.3 KiB
TypeScript
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>
|
|
);
|
|
}
|