140 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|