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

@@ -1,8 +1,8 @@
import React, { MouseEventHandler, useState } from 'react';
import { Box, config, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text } from 'folds';
import { Box, config, Icon, Icons, Line, Menu, MenuItem, PopOut, RectCords, Text } from 'folds';
import FocusTrap from 'focus-trap-react';
import { useNavigate } from 'react-router-dom';
import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../components/sidebar';
import { SidebarItem, SidebarAvatar } from '../../../components/sidebar';
import { UserAvatar } from '../../../components/user-avatar';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
@@ -13,21 +13,23 @@ import { useUserProfile } from '../../../hooks/useUserProfile';
import { Modal500 } from '../../../components/Modal500';
import { AccountSwitcher } from '../../../components/account-switcher';
import { stopPropagation } from '../../../utils/keyboard';
import { getLoginPath, withSearchParam } from '../../pathUtils';
import { getLoginPath } from '../../pathUtils';
import { logoutClient } from '../../../../client/initMatrix';
import { CustomStatusDialog } from '../../../components/custom-status';
export function SettingsTab() {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const userId = mx.getUserId()!;
const profile = useUserProfile(userId);
const userId = mx.getUserId();
const profile = useUserProfile(userId ?? '');
const navigate = useNavigate();
const [settings, setSettings] = useState(false);
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [customStatusOpen, setCustomStatusOpen] = useState(false);
const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
const avatarUrl = profile.avatarUrl
const displayName = profile.displayName ?? getMxIdLocalPart(userId ?? '') ?? userId ?? '';
const avatarUrl = profile.avatarUrl && userId
? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined;
@@ -42,22 +44,17 @@ export function SettingsTab() {
return (
<SidebarItem active={settings}>
<SidebarItemTooltip tooltip="User Settings">
{(triggerRef) => (
<SidebarAvatar
as="button"
ref={triggerRef}
onClick={openSettings}
onContextMenu={handleContextMenu}
>
<UserAvatar
userId={userId}
src={avatarUrl}
renderFallback={() => <Text size="H4">{nameInitials(displayName)}</Text>}
/>
</SidebarAvatar>
)}
</SidebarItemTooltip>
<SidebarAvatar
as="button"
onClick={openSettings}
onContextMenu={handleContextMenu}
>
<UserAvatar
userId={userId ?? ''}
src={avatarUrl}
renderFallback={() => <Text size="H4">{nameInitials(displayName)}</Text>}
/>
</SidebarAvatar>
<PopOut
anchor={menuAnchor}
@@ -78,18 +75,14 @@ export function SettingsTab() {
>
<Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<AccountSwitcher currentUserId={userId} />
<AccountSwitcher currentUserId={userId ?? ''} />
</Box>
<Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem
onClick={() => {
setMenuAnchor(undefined);
const loginPath = getLoginPath() + '?add-account=true';
console.log('[SettingsTab] Navigating to add account:', loginPath);
localStorage.setItem('debug-add-account-nav', JSON.stringify({
timestamp: new Date().toISOString(),
loginPath,
currentUrl: window.location.href
}));
navigate(loginPath);
navigate(`${getLoginPath()}?add-account=true`);
}}
size="300"
radii="300"
@@ -116,11 +109,34 @@ export function SettingsTab() {
</Text>
</MenuItem>
</Box>
<Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem
onClick={() => {
setMenuAnchor(undefined);
setCustomStatusOpen(true);
}}
size="300"
radii="300"
>
<Box gap="200" alignItems="Center">
<Icon size="100" src={Icons.Message} />
<Text as="span" size="T300" truncate>
Set Custom Status
</Text>
</Box>
</MenuItem>
</Box>
</Menu>
</FocusTrap>
}
/>
<CustomStatusDialog
open={customStatusOpen}
onClose={() => setCustomStatusOpen(false)}
/>
{settings && (
<Modal500 requestClose={closeSettings}>
<Settings requestClose={closeSettings} />