vojo/src/app/features/room/RoomViewProfileSidePanel.tsx
2026-06-13 12:50:55 +03:00

127 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Desktop / tablet right-side profile pane. Renders the same
// `UserRoomProfile` content the mobile top horseshoe shows, but as a
// flex sibling next to the chat column instead of a slide-down rail.
//
// Mounted in `Room.tsx` only when the screen size is not Mobile —
// the mobile branch keeps using the top horseshoe via
// `RoomViewProfilePanel`.
import React, { useEffect, useRef, useState } from 'react';
import { useAtomValue } from 'jotai';
import { Box, Icon, IconButton, Icons, Text, config } from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { userRoomProfileAtom } from '../../state/userRoomProfile';
import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { useOpenRoomMembersSheet } from '../../state/hooks/roomMembersSheet';
import { useIsOneOnOne, useRoom } from '../../hooks/useRoom';
import { UserRoomProfile } from '../../components/user-profile/UserRoomProfile';
import { stopPropagation } from '../../utils/keyboard';
import { PageHeader } from '../../components/page';
import { ContainerColor } from '../../styles/ContainerColor.css';
import * as css from './RoomViewProfileSidePanel.css';
export function RoomViewProfileSidePanel() {
const { t } = useTranslation();
const profileState = useAtomValue(userRoomProfileAtom);
const close = useCloseUserRoomProfile();
const room = useRoom();
const isOneOnOne = useIsOneOnOne();
const openMembersSheet = useOpenRoomMembersSheet();
const open = !!profileState;
// Inline avatar-expanded mode: the hero avatar stretches to fill
// the card width as a square, pushing the rest of the card down in
// the scroll container. Replaces the legacy full-pane image swap
// which hid the name / presence / info rows. Mobile keeps the
// full-rail swap inside `RoomViewProfilePanel` — the rail there is
// short enough that pushing content down would just create a
// scroll well.
const [avatarExpanded, setAvatarExpanded] = useState(false);
// Close profile when the room changes — atom is global state and
// would otherwise carry the previous room's userId into this room
// where chrome would render against the wrong member / power
// levels.
useEffect(() => () => close(), [room.roomId, close]);
// Reset avatar-zoom mode whenever the rendered user changes.
useEffect(() => {
setAvatarExpanded(false);
}, [profileState?.userId]);
const profileStateRef = useRef(profileState);
profileStateRef.current = profileState;
const renderUserId = profileState?.userId;
if (!open || !renderUserId) return null;
return (
<FocusTrap
focusTrapOptions={{
initialFocus: false,
// Outside clicks pass through to the chat (`allowOutsideClick`)
// but don't close the pane — clicking around in the chat to
// scroll, react, or compose shouldn't dismiss the profile.
// Explicit close is via the header's `×` or `Esc` (the latter
// still routes through focus-trap → `onDeactivate`).
clickOutsideDeactivates: false,
allowOutsideClick: () => true,
escapeDeactivates: stopPropagation,
onDeactivate: () => {
if (profileStateRef.current) close();
},
checkCanFocusTrap: () => Promise.resolve(),
}}
active={open}
>
<div className={css.panel}>
{/* Use the same `PageHeader` (folds `Header size="600"`) the
chat header uses, so the bottom rule lines up with the
adjacent `RoomViewHeaderDm` row to the pixel. */}
<PageHeader className={`${ContainerColor({ variant: 'Surface' })} ${css.header}`}>
{/* Group rooms: leading back arrow returns to the members sheet
(the room card) the profile replaced in this pane — the open
hook clears the profile atom, so the panes swap in place
(redesign item 15). 1:1 has no members sheet → no arrow.
Call rooms neither: Room.tsx mounts the members side pane only
when `!callView`, so opening the sheet atom from inside a call
would close the profile into nothing AND leak a stale atom
that auto-opens the pane in the next group room. */}
{!isOneOnOne && !room.isCallRoom() && (
<Box shrink="No" alignItems="Center">
<IconButton
fill="None"
onClick={() => openMembersSheet(room.roomId)}
aria-label={t('User.back')}
>
<Icon size="400" src={Icons.ArrowLeft} />
</IconButton>
</Box>
)}
<Box grow="Yes" alignItems="Center">
<Text size="H4" truncate>
{t('User.profile_title')}
</Text>
</Box>
<Box shrink="No" alignItems="Center">
<IconButton fill="None" onClick={close} aria-label={t('Room.close')}>
<Icon size="400" src={Icons.Cross} />
</IconButton>
</Box>
</PageHeader>
<div className={css.scrollWrap}>
<div style={{ padding: config.space.S400 }}>
<UserRoomProfile
userId={renderUserId}
onAvatarClick={() => setAvatarExpanded((prev) => !prev)}
avatarExpanded={avatarExpanded}
/>
</div>
</div>
</div>
</FocusTrap>
);
}