vojo/src/app/components/members-list/RoomMembersHero.tsx

131 lines
5 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.

// Centred hero block on top of the group-room members sheet —
// counterpart of `UserHero` from the 1:1 peer-profile sheet.
// Renders: large gradient room avatar, room name, subline («N
// members · e2ee») and optional topic clamped to a few lines.
// Consumed by both the mobile horseshoe (`RoomViewMembersPanel`)
// and the desktop right-side pane (`RoomViewMembersSidePanel`)
// via `MembersList`.
import React from 'react';
import { Box, Icon, Icons, Text } from 'folds';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Room } from 'matrix-js-sdk';
import { RoomAvatar, RoomIcon } from '../room-avatar';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { useRoomMemberCount } from '../../hooks/useRoomMemberCount';
import { useStateEvent } from '../../hooks/useStateEvent';
import { StateEvent } from '../../../types/matrix/room';
import { mxcUrlToHttp } from '../../utils/matrix';
import { millify } from '../../plugins/millify';
import { BreakWord, LineClamp3 } from '../../styles/Text.css';
import * as css from './styles.css';
type RoomMembersHeroProps = {
room: Room;
// Optional avatar-tap handler. When provided the avatar surface
// becomes a button (one tab stop, focus ring). The host decides
// what tap does — typically swap the panel body to a full-view of
// the avatar, mirroring the 1:1 profile-horseshoe behaviour.
onAvatarClick?: () => void;
};
export function RoomMembersHero({ room, onAvatarClick }: RoomMembersHeroProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
// Group sheet: use the room's own avatar, NOT the peer fallback —
// the 1:1 peer-fallback path lives in `RoomViewHeaderDm` for the
// chat header, and here we're showing the room itself.
//
// No width/height passed to `mxcUrlToHttp` — same as `UserRoomProfile`
// does for the 1:1 hero. Synapse returns the original upload, the
// browser scales it via CSS without resampling artefacts. Asking for
// a small thumbnail (e.g. 192x192) routes through Synapse's JPEG
// recompression pipeline and ships visibly «pixelated» avatars on
// high-DPR phones (96 CSS px = 192288 native px on 2x/3x screens,
// so the thumb is right at the edge before the recompression hits).
const avatarMxc = useRoomAvatar(room, false);
const avatarUrl = (avatarMxc && mxcUrlToHttp(mx, avatarMxc, useAuthentication)) ?? undefined;
const name = useRoomName(room);
const topic = useRoomTopic(room);
const memberCount = useRoomMemberCount(room);
const encrypted = !!useStateEvent(room, StateEvent.RoomEncryption);
// Drop the folds `<Avatar size="…">` wrapper here on purpose —
// `RoomAvatar` already renders the folds `AvatarImage` / `AvatarFallback`
// primitives with our `.RoomAvatar` class that fills the parent
// (`width: 100% / height: 100%`). Wrapping it in another sized
// `<Avatar>` double-wraps the surface and the inner avatar ends up
// smaller than its container, drifting off-centre. Mirrors the
// pattern `UserHero` uses for the 1:1 profile sheet.
const avatarNode = (
<span className={css.HeroAvatar} aria-hidden={onAvatarClick ? undefined : true}>
<RoomAvatar
roomId={room.roomId}
src={avatarUrl}
alt={name}
renderFallback={() => (
<RoomIcon size="600" joinRule={room.getJoinRule()} roomType={room.getType()} />
)}
/>
</span>
);
return (
<Box direction="Column" alignItems="Center" gap="200" className={css.HeroRoot}>
{onAvatarClick ? (
<button
type="button"
onClick={onAvatarClick}
className={css.HeroAvatarButton}
aria-label={t('Room.expand_avatar', { defaultValue: 'Open avatar' })}
>
{avatarNode}
</button>
) : (
avatarNode
)}
<Text size="H4" align="Center" className={classNames(BreakWord, LineClamp3)} title={name}>
{name}
</Text>
<Box alignItems="Center" gap="200" className={css.HeroSubline}>
<Text as="span" size="T200" className={css.HeroMembersCount}>
{t('Room.members_sheet_title', {
count: memberCount,
formattedCount: millify(memberCount),
})}
</Text>
{encrypted && (
<>
<span className={css.HeroBullet} aria-hidden>
·
</span>
<span className={css.HeroE2ee}>
<Icon size="50" src={Icons.Lock} filled />
<Text as="span" size="T200">
{t('Room.encrypted_short')}
</Text>
</span>
</>
)}
</Box>
{topic && (
<Text
as="p"
size="T200"
align="Center"
className={classNames(BreakWord, LineClamp3, css.HeroTopic)}
>
{topic}
</Text>
)}
</Box>
);
}