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

140 lines
5.1 KiB
TypeScript

import React from 'react';
import { Avatar, Box, Icon, Icons, Text } from 'folds';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import * as css from './styles.css';
import { CallControl } from './CallControl';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { CallEmbed } from '../../plugins/call/CallEmbed';
import { useCallJoined } from '../../hooks/useCallEmbed';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useRoomAvatar, useRoomName } from '../../hooks/useRoomMeta';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { UserAvatar } from '../../components/user-avatar';
import { RoomAvatar } from '../../components/room-avatar';
import { getMemberDisplayName } from '../../utils/room';
import {
getMxIdLocalPart,
guessDmRoomUserId,
mxcUrlToHttp,
} from '../../utils/matrix';
type CallStatusProps = {
callEmbed: CallEmbed;
};
export function CallStatus({ callEmbed }: CallStatusProps) {
const { t } = useTranslation();
const { room } = callEmbed;
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const callJoined = useCallJoined(callEmbed);
const callSession = useCallSession(room);
const callMembers = useCallMembers(room, callSession);
const screenSize = useScreenSizeContext();
const compact = screenSize === ScreenSize.Mobile;
const { navigateRoom } = useRoomNavigate();
// Member-count gate mirrors the rest of the post-P3c app: 1:1 chrome
// surfaces the peer (their avatar + display name); group chrome
// surfaces the room itself. Snapshot — call duration is short relative
// to membership churn, and `useCallMembers` already reactively covers
// the «who's actually live» count below, so peer chrome at most lags
// one render behind a third joiner before re-painting.
const isOneOnOne = room.getInvitedAndJoinedMemberCount() === 2;
const myUserId = mx.getSafeUserId();
const peerCandidate = isOneOnOne ? guessDmRoomUserId(room, myUserId) : undefined;
const peerUserId = peerCandidate && peerCandidate !== myUserId ? peerCandidate : undefined;
const peerName = peerUserId
? getMemberDisplayName(room, peerUserId) ?? getMxIdLocalPart(peerUserId) ?? peerUserId
: undefined;
const roomName = useRoomName(room);
const avatarMxc = useRoomAvatar(room, isOneOnOne);
const avatarUrl = avatarMxc
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined;
// Defensive fallback chain — empty roomName + unresolved peer would
// otherwise collapse the H4 to "" and leave the pill anonymous.
const displayName =
(isOneOnOne && peerName) ||
roomName ||
peerUserId ||
room.roomId;
// Sub-text is the live state: «Соединение…» until the widget reports
// joined, then either «В звонке» (1:1) or «N в звонке» (group). The
// pulsing dot is bound to `callJoined` so the user gets instant
// visual confirmation that LiveKit is up.
const memberCount = callMembers.length;
let subText: string;
if (!callJoined) {
subText = t('Call.connecting');
} else if (isOneOnOne || memberCount <= 1) {
subText = t('Call.in_call');
} else {
subText = t('Call.in_call_count', { count: memberCount });
}
return (
<Box
className={classNames(css.CallStatus, ContainerColor({ variant: 'Background' }))}
shrink="No"
direction="Row"
alignItems="Center"
gap="400"
>
<Box
as="button"
type="button"
grow="Yes"
alignItems="Center"
gap="400"
className={css.CallIdentityButton}
onClick={() => navigateRoom(room.roomId)}
aria-label={t('Call.open_call_room')}
>
<Avatar className={css.RingAvatar}>
{isOneOnOne ? (
<UserAvatar
userId={peerUserId ?? ''}
src={avatarUrl}
alt={displayName}
renderFallback={() => <Icon size="400" src={Icons.User} filled />}
/>
) : (
<RoomAvatar
roomId={room.roomId}
src={avatarUrl}
alt={displayName}
renderFallback={() => <Icon size="400" src={Icons.Hash} filled />}
/>
)}
</Avatar>
<Box grow="Yes" direction="Column" gap="100" alignItems="Start">
<Text size="H4" truncate>
{displayName}
</Text>
<Box alignItems="Center" gap="200">
<span
className={classNames(css.LiveDot, callJoined && css.LiveDotPulsing)}
aria-hidden
/>
<Text size="T200" priority="300" truncate>
{subText}
</Text>
</Box>
</Box>
</Box>
<Box shrink="No" alignItems="Center" gap="200">
<CallControl callJoined={callJoined} compact={compact} callEmbed={callEmbed} />
</Box>
</Box>
);
}