131 lines
5 KiB
TypeScript
131 lines
5 KiB
TypeScript
// 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 = 192–288 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>
|
||
);
|
||
}
|