// 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 `` 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 // `` 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 = ( ( )} /> ); return ( {onAvatarClick ? ( ) : ( avatarNode )} {name} {t('Room.members_sheet_title', { count: memberCount, formattedCount: millify(memberCount), })} {encrypted && ( <> · {t('Room.encrypted_short')} )} {topic && ( {topic} )} ); }