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

@@ -0,0 +1,180 @@
import React, { useState, useCallback, ChangeEventHandler } from 'react';
import {
Box,
Button,
config,
Dialog,
Input,
Text,
Overlay,
OverlayBackdrop,
OverlayCenter,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useUserPresence } from '../../hooks/useUserPresence';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
/**
* Props for the CustomStatusDialog component
*/
export interface CustomStatusDialogProps {
/** Whether the dialog is currently open */
open: boolean;
/** Callback function invoked when the dialog should close */
onClose: () => void;
}
/**
* CustomStatusDialog Component
*
* Provides a dialog interface for users to set or clear their custom status message.
* The status is stored in Matrix presence and will be visible to other users.
*
* @param props - Component props
* @returns The custom status dialog component
*/
export function CustomStatusDialog({ open, onClose }: CustomStatusDialogProps) {
const mx = useMatrixClient();
const userId = mx.getUserId();
const presence = useUserPresence(userId ?? '');
const [statusText, setStatusText] = useState(presence?.status || '');
/**
* Handles input changes for the status text field
* @param evt - The change event from the input
*/
const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
setStatusText(evt.currentTarget.value);
};
/**
* Async callback to save the status to Matrix presence
*/
const [saveState, saveStatus] = useAsyncCallback(
useCallback(async () => {
await mx.setPresence({
presence: presence?.presence || 'online',
status_msg: statusText,
});
onClose();
}, [mx, presence?.presence, statusText, onClose])
);
/**
* Async callback to clear the status from Matrix presence
*/
const [clearState, clearStatus] = useAsyncCallback(
useCallback(async () => {
await mx.setPresence({
presence: presence?.presence || 'online',
status_msg: '',
});
setStatusText('');
onClose();
}, [mx, presence?.presence, onClose])
);
/**
* Handles form submission to save the custom status
* @param evt - The form submit event
*/
const handleSubmit = (evt: React.FormEvent) => {
evt.preventDefault();
saveStatus();
};
/**
* Handles the clear status button click
*/
const handleClear = () => {
clearStatus();
};
if (!open) return null;
return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: onClose,
clickOutsideDeactivates: true,
}}
>
<Dialog>
<Box
as="form"
onSubmit={handleSubmit}
direction="Column"
gap="400"
style={{ padding: config.space.S400 }}
>
<Box direction="Column" gap="100">
<Text size="H4">Set Custom Status</Text>
<Text size="T300" priority="400">
Your custom status will be visible to other users
</Text>
</Box>
<Box direction="Column" gap="200">
<Input
value={statusText}
onChange={handleChange}
placeholder="What's on your mind?"
maxLength={100}
autoFocus
disabled={saveState.status === AsyncStatus.Loading || clearState.status === AsyncStatus.Loading}
/>
{statusText.length > 0 && (
<Text size="T200" priority="300">
{statusText.length}/100 characters
</Text>
)}
</Box>
{(saveState.status === AsyncStatus.Error || clearState.status === AsyncStatus.Error) && (
<Text size="T300" priority="400" style={{ color: 'var(--color-critical-main)' }}>
Failed to update status. Please try again.
</Text>
)}
<Box direction="Row" gap="200" justifyContent="End">
<Button
type="button"
variant="Secondary"
onClick={onClose}
disabled={saveState.status === AsyncStatus.Loading || clearState.status === AsyncStatus.Loading}
>
<Text size="B400">Cancel</Text>
</Button>
{presence?.status && (
<Button
type="button"
variant="Critical"
fill="None"
onClick={handleClear}
disabled={saveState.status === AsyncStatus.Loading || clearState.status === AsyncStatus.Loading}
>
<Text size="B400">Clear Status</Text>
</Button>
)}
<Button
type="submit"
variant="Primary"
disabled={saveState.status === AsyncStatus.Loading || clearState.status === AsyncStatus.Loading}
>
<Text size="B400">
{saveState.status === AsyncStatus.Loading ? 'Saving...' : 'Save'}
</Text>
</Button>
</Box>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}

View File

@@ -0,0 +1 @@
export { CustomStatusDialog, type CustomStatusDialogProps } from './CustomStatusDialog';