feat(profile): inline-expand hero avatar in desktop side pane instead of swapping the whole card to full-view
This commit is contained in:
parent
9c204c1af6
commit
626a7c2d1d
7 changed files with 74 additions and 82 deletions
|
|
@ -426,6 +426,7 @@
|
|||
"Room": {
|
||||
"drag_to_close": "Drag up to close",
|
||||
"collapse_avatar": "Collapse avatar",
|
||||
"expand_avatar": "Open avatar",
|
||||
"new_messages": "New Messages",
|
||||
"jump_to_unread": "Jump to Unread",
|
||||
"mark_as_read": "Mark as Read",
|
||||
|
|
|
|||
|
|
@ -428,6 +428,7 @@
|
|||
"Room": {
|
||||
"drag_to_close": "Потянуть вверх чтобы закрыть",
|
||||
"collapse_avatar": "Свернуть аватар",
|
||||
"expand_avatar": "Развернуть аватар",
|
||||
"new_messages": "Новые сообщения",
|
||||
"jump_to_unread": "К непрочитанным",
|
||||
"mark_as_read": "Отметить прочитанным",
|
||||
|
|
|
|||
|
|
@ -75,6 +75,12 @@ type UserHeroProps = {
|
|||
presence?: UserPresence;
|
||||
encrypted?: boolean;
|
||||
onAvatarClick?: () => void;
|
||||
// Desktop side-pane only. When true the avatar stretches to fill
|
||||
// the card width as a square; the rest of the hero (name / presence
|
||||
// / e2ee chip) stays in flow below. The online dot is suppressed in
|
||||
// this mode — it makes no sense as a tiny corner glyph on a
|
||||
// ~340px avatar tile.
|
||||
avatarExpanded?: boolean;
|
||||
};
|
||||
|
||||
export function UserHero({
|
||||
|
|
@ -84,6 +90,7 @@ export function UserHero({
|
|||
presence,
|
||||
encrypted,
|
||||
onAvatarClick,
|
||||
avatarExpanded,
|
||||
}: UserHeroProps) {
|
||||
const { t } = useTranslation();
|
||||
const username = getMxIdLocalPart(userId);
|
||||
|
|
@ -124,7 +131,10 @@ export function UserHero({
|
|||
const fallbackBg = `linear-gradient(135deg, ${fallbackColor}, ${shadeHex(fallbackColor, -22)})`;
|
||||
|
||||
const avatarNode = (
|
||||
<span className={css.HeroAvatar} aria-hidden={onAvatarClick ? undefined : true}>
|
||||
<span
|
||||
className={classNames(css.HeroAvatar, avatarExpanded && css.HeroAvatarExpanded)}
|
||||
aria-hidden={onAvatarClick ? undefined : true}
|
||||
>
|
||||
<UserAvatar
|
||||
userId={userId}
|
||||
src={avatarUrl}
|
||||
|
|
@ -135,7 +145,7 @@ export function UserHero({
|
|||
</span>
|
||||
)}
|
||||
/>
|
||||
{online && <span className={css.HeroOnlineDot} aria-hidden />}
|
||||
{online && !avatarExpanded && <span className={css.HeroOnlineDot} aria-hidden />}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
|
@ -145,8 +155,14 @@ export function UserHero({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onAvatarClick}
|
||||
className={css.HeroAvatarButton}
|
||||
aria-label={t('Room.expand_avatar', { defaultValue: 'Open avatar' })}
|
||||
className={classNames(
|
||||
css.HeroAvatarButton,
|
||||
avatarExpanded && css.HeroAvatarButtonExpanded
|
||||
)}
|
||||
aria-label={t(
|
||||
avatarExpanded ? 'Room.collapse_avatar' : 'Room.expand_avatar',
|
||||
{ defaultValue: avatarExpanded ? 'Close avatar' : 'Open avatar' }
|
||||
)}
|
||||
>
|
||||
{avatarNode}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -45,9 +45,19 @@ import * as css from './styles.css';
|
|||
type UserRoomProfileProps = {
|
||||
userId: string;
|
||||
onAvatarClick?: () => void;
|
||||
// Desktop side-pane only — forwarded straight to `UserHero` so the
|
||||
// hero avatar can render in its inline-expanded mode while the
|
||||
// rest of the card stays in flow. Mobile leaves this `undefined`
|
||||
// and keeps using the full-rail avatar swap inside
|
||||
// `RoomViewProfilePanel`.
|
||||
avatarExpanded?: boolean;
|
||||
};
|
||||
|
||||
export function UserRoomProfile({ userId, onAvatarClick }: UserRoomProfileProps) {
|
||||
export function UserRoomProfile({
|
||||
userId,
|
||||
onAvatarClick,
|
||||
avatarExpanded,
|
||||
}: UserRoomProfileProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -137,6 +147,7 @@ export function UserRoomProfile({ userId, onAvatarClick }: UserRoomProfileProps)
|
|||
presence={presence}
|
||||
encrypted={encrypted}
|
||||
onAvatarClick={onAvatarClick}
|
||||
avatarExpanded={avatarExpanded}
|
||||
/>
|
||||
|
||||
<div className={css.InfoSection}>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,29 @@ export const HeroAvatarButton = style({
|
|||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
// Desktop side-pane expanded avatar. Clicking the small hero avatar
|
||||
// in the right-side profile pane flips this on: the wrapper
|
||||
// stretches to the card width, squares up via `aspect-ratio` so the
|
||||
// server-side 1:1 avatar shows full-bleed without cropping, and the
|
||||
// rest of the card stays in flow below. Replaces the legacy full-pane
|
||||
// avatar swap which hid the name / presence / info rows entirely.
|
||||
// `border-radius: !important` is mandatory: the global override in
|
||||
// `components/user-avatar/UserAvatar.css.ts` uses `!important` to
|
||||
// force every avatar parent to 50%, so without matching specificity
|
||||
// the expanded square would render as a huge circle.
|
||||
export const HeroAvatarExpanded = style({
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
aspectRatio: '1 / 1',
|
||||
borderRadius: `${toRem(16)} !important`,
|
||||
boxShadow: 'none',
|
||||
});
|
||||
|
||||
export const HeroAvatarButtonExpanded = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const HeroIdentity = style({
|
||||
width: '100%',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,32 +39,3 @@ export const scrollWrap = style({
|
|||
},
|
||||
});
|
||||
|
||||
export const avatarFullView = style({
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
});
|
||||
|
||||
export const avatarFullImage = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
});
|
||||
|
||||
export const avatarFullFallback = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: toRem(96),
|
||||
fontWeight: 600,
|
||||
color: color.Surface.Container,
|
||||
textTransform: 'uppercase',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,13 +14,8 @@ import { useTranslation } from 'react-i18next';
|
|||
import { userRoomProfileAtom } from '../../state/userRoomProfile';
|
||||
import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { getMemberAvatarMxc } from '../../utils/room';
|
||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { UserRoomProfile } from '../../components/user-profile/UserRoomProfile';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { PageHeader } from '../../components/page';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import * as css from './RoomViewProfileSidePanel.css';
|
||||
|
|
@ -30,11 +25,16 @@ export function RoomViewProfileSidePanel() {
|
|||
const profileState = useAtomValue(userRoomProfileAtom);
|
||||
const close = useCloseUserRoomProfile();
|
||||
const room = useRoom();
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
|
||||
const open = !!profileState;
|
||||
const [avatarMode, setAvatarMode] = useState(false);
|
||||
// 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
|
||||
|
|
@ -44,20 +44,13 @@ export function RoomViewProfileSidePanel() {
|
|||
|
||||
// Reset avatar-zoom mode whenever the rendered user changes.
|
||||
useEffect(() => {
|
||||
setAvatarMode(false);
|
||||
setAvatarExpanded(false);
|
||||
}, [profileState?.userId]);
|
||||
|
||||
const profileStateRef = useRef(profileState);
|
||||
profileStateRef.current = profileState;
|
||||
|
||||
const renderUserId = profileState?.userId;
|
||||
const renderUserAvatarMxc = renderUserId
|
||||
? getMemberAvatarMxc(room, renderUserId)
|
||||
: undefined;
|
||||
const renderUserAvatarUrl =
|
||||
(renderUserAvatarMxc &&
|
||||
mxcUrlToHttp(mx, renderUserAvatarMxc, useAuthentication, 720, 720, 'scale')) ??
|
||||
undefined;
|
||||
|
||||
if (!open || !renderUserId) return null;
|
||||
|
||||
|
|
@ -96,39 +89,15 @@ export function RoomViewProfileSidePanel() {
|
|||
</Box>
|
||||
</PageHeader>
|
||||
|
||||
{avatarMode ? (
|
||||
<button
|
||||
type="button"
|
||||
className={css.avatarFullView}
|
||||
onClick={() => setAvatarMode(false)}
|
||||
aria-label={t('Room.collapse_avatar')}
|
||||
>
|
||||
{renderUserAvatarUrl ? (
|
||||
<img
|
||||
className={css.avatarFullImage}
|
||||
src={renderUserAvatarUrl}
|
||||
alt={renderUserId}
|
||||
draggable={false}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={css.avatarFullFallback}
|
||||
style={{ backgroundColor: colorMXID(renderUserId) }}
|
||||
>
|
||||
{getMxIdLocalPart(renderUserId)?.[0] ?? '?'}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<div className={css.scrollWrap}>
|
||||
<div style={{ padding: config.space.S400 }}>
|
||||
<UserRoomProfile
|
||||
userId={renderUserId}
|
||||
onAvatarClick={() => setAvatarMode(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className={css.scrollWrap}>
|
||||
<div style={{ padding: config.space.S400 }}>
|
||||
<UserRoomProfile
|
||||
userId={renderUserId}
|
||||
onAvatarClick={() => setAvatarExpanded((prev) => !prev)}
|
||||
avatarExpanded={avatarExpanded}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue