feat: Enhance user color and banner management

- Updated `useUserColor` hook to support fetching and embedding user color from avatar metadata with authentication.
- Introduced `useUserBanner` hook for managing user banners stored in avatar metadata, including fetching and embedding functionality.
- Added `fetchAndExtractMetadata` utility to retrieve both color and banner from images.
- Implemented `CustomStatusDialog` component for users to set and clear custom status messages.
- Modified `SettingsTab` to include custom status functionality and improved user experience.
- Enhanced image metadata utilities to support banner extraction and embedding in PNG format.
- Updated sidebar settings to handle user profile changes more gracefully.
This commit is contained in:
2026-03-12 10:58:38 +11:00
parent 2c2560f5a2
commit fc30d81f8f
12 changed files with 1804 additions and 419 deletions

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import {
Avatar,
Box,
color,
Icon,
Icons,
Modal,
@@ -9,38 +10,51 @@ import {
OverlayBackdrop,
OverlayCenter,
Text,
toRem,
} from 'folds';
import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import * as css from './styles.css';
import { UserAvatar } from '../user-avatar';
import colorMXID from '../../../util/colorMXID';
import { getMxIdLocalPart } from '../../utils/matrix';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { BreakWord, LineClamp3 } from '../../styles/Text.css';
import { UserPresence } from '../../hooks/useUserPresence';
import { AvatarPresence, PresenceBadge } from '../presence';
import { ImageViewer } from '../image-viewer';
import { stopPropagation } from '../../utils/keyboard';
import { useOtherUserColor } from '../../hooks/useUserColor';
import { useOtherUserBanner } from '../../hooks/useUserBanner';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
type UserHeroProps = {
userId: string;
avatarUrl?: string;
avatarMxc?: string;
presence?: UserPresence;
};
export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) {
export function UserHero({ userId, avatarUrl, avatarMxc, presence }: UserHeroProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [viewAvatar, setViewAvatar] = useState<string>();
const bannerUrl = useOtherUserBanner(userId, avatarMxc); // Now returns blob URL directly
return (
<Box direction="Column" className={css.UserHero}>
<div
className={css.UserHeroCoverContainer}
style={{
backgroundColor: colorMXID(userId),
filter: avatarUrl ? undefined : 'brightness(50%)',
backgroundColor: bannerUrl ? undefined : colorMXID(userId),
filter: bannerUrl || avatarUrl ? undefined : 'brightness(50%)',
}}
>
{avatarUrl && (
<img className={css.UserHeroCover} src={avatarUrl} alt={userId} draggable="false" />
{bannerUrl ? (
<img className={css.UserHeroBanner} src={bannerUrl} alt={userId} draggable="false" />
) : (
avatarUrl && (
<img className={css.UserHeroCover} src={avatarUrl} alt={userId} draggable="false" />
)
)}
</div>
<div className={css.UserHeroAvatarContainer}>
@@ -65,6 +79,23 @@ export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) {
/>
</Avatar>
</AvatarPresence>
{presence?.status && (
<Box
className={css.UserStatusBubble}
style={{
backgroundColor: color.Surface.Container,
border: `${toRem(1)} solid ${color.Surface.ContainerLine}`,
padding: `${toRem(6)} ${toRem(10)}`,
borderRadius: toRem(16),
maxWidth: toRem(250),
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
}}
>
<Text size="T300" className={BreakWord}>
{presence.status}
</Text>
</Box>
)}
{viewAvatar && (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
@@ -76,7 +107,7 @@ export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) {
escapeDeactivates: stopPropagation,
}}
>
<Modal size="500" onContextMenu={(evt: any) => evt.stopPropagation()}>
<Modal size="500" onContextMenu={(evt: React.MouseEvent) => evt.stopPropagation()}>
<ImageViewer
src={viewAvatar}
alt={userId}
@@ -95,12 +126,14 @@ export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) {
type UserHeroNameProps = {
displayName?: string;
userId: string;
avatarMxc?: string;
};
export function UserHeroName({ displayName, userId }: UserHeroNameProps) {
export function UserHeroName({ displayName, userId, avatarMxc }: UserHeroNameProps) {
const username = getMxIdLocalPart(userId);
const userColor = useOtherUserColor(userId, avatarMxc);
return (
<Box grow="Yes" direction="Column" gap="0">
<Box grow="Yes" direction="Column" gap="100">
<Box alignItems="Baseline" gap="200" wrap="Wrap">
<Text
size="H4"
@@ -111,8 +144,13 @@ export function UserHeroName({ displayName, userId }: UserHeroNameProps) {
</Text>
</Box>
<Box alignItems="Center" gap="100" wrap="Wrap">
<Text size="T200" className={classNames(BreakWord, LineClamp3)} title={username}>
@{username}
<Text
size="T200"
className={BreakWord}
title={userId}
style={{ color: userColor || colorMXID(userId) }}
>
{userId}
</Text>
</Box>
</Box>