feat: add user color customization and metadata handling

- Implemented `useUserColor` hook to manage user profile color stored in avatar PNG metadata.
- Added `useOtherUserColor` hook to fetch and cache colors from other users' avatars.
- Created utility functions for reading and writing PNG metadata, enabling color embedding and extraction.
- Refactored `SearchResultGroup`, `RoomInput`, `Message`, `RoomPinMenu`, and `Notifications` components to utilize user color for display.
- Introduced `UserColorPicker` component in settings for users to select and save their profile color.
- Enhanced notification and message rendering to prioritize custom user colors over legacy colors.
This commit is contained in:
2026-02-06 05:53:37 +11:00
parent 4ca4af0e8b
commit 516000a25f
11 changed files with 1061 additions and 222 deletions

View File

@@ -12,6 +12,7 @@ import {
Box,
Button,
Chip,
color,
config,
Header,
Icon,
@@ -27,6 +28,7 @@ import {
Text,
toRem,
} from 'folds';
import { HexColorPicker } from 'react-colorful';
import { isKeyHotkey } from 'is-hotkey';
import FocusTrap from 'focus-trap-react';
import { Page, PageContent, PageHeader } from '../../../components/page';
@@ -50,6 +52,8 @@ import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
import { useDateFormatItems } from '../../../hooks/useDateFormat';
import { SequenceCardStyle } from '../styles.css';
import { HexColorPickerPopOut } from '../../../components/HexColorPickerPopOut';
import { useUserColor } from '../../../hooks/useUserColor';
type ThemeSelectorProps = {
themeNames: Record<string, string>;
@@ -427,10 +431,131 @@ function Appearance() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Page Zoom" after={<PageZoomInput />} />
</SequenceCard>
<UserColorPicker />
</Box>
);
}
/**
* Color picker component for user's profile color
* Stored in avatar PNG metadata, visible to other Paarrot users
*/
function UserColorPicker() {
const [userColor, setUserColor, loading] = useUserColor();
const [localColor, setLocalColor] = useState(userColor ?? '#3b82f6');
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string>();
// Update local color when avatar color changes
useEffect(() => {
if (userColor) {
setLocalColor(userColor);
}
}, [userColor]);
const handleColorChange = (newColor: string) => {
setLocalColor(newColor);
setError(undefined);
};
const handleSaveColor = async () => {
setSaving(true);
setError(undefined);
try {
await setUserColor(localColor);
} catch (e) {
setError('Failed to save. Make sure you have an avatar set.');
}
setSaving(false);
};
const handleRemoveColor = async () => {
setSaving(true);
setError(undefined);
try {
await setUserColor(undefined);
setLocalColor('#3b82f6');
} catch (e) {
setError('Failed to remove color.');
}
setSaving(false);
};
return (
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Profile Color"
description="Embedded in your avatar. Other Paarrot users will see this color."
after={
<Box alignItems="Center" gap="200">
{loading ? (
<Text size="T300" style={{ opacity: 0.7 }}>Loading...</Text>
) : (
<HexColorPickerPopOut
picker={
<Box direction="Column" gap="200">
<HexColorPicker color={localColor} onChange={handleColorChange} />
<Box gap="100" alignItems="Center">
<Input
size="300"
variant="Secondary"
style={{ width: toRem(100) }}
value={localColor}
onChange={(e) => {
setLocalColor(e.target.value);
setError(undefined);
}}
/>
<Button
size="300"
variant="Primary"
fill="Solid"
radii="300"
onClick={handleSaveColor}
disabled={saving}
>
<Text size="B300">{saving ? 'Saving...' : 'Save'}</Text>
</Button>
</Box>
{error && (
<Text size="T200" style={{ color: color.Critical.Main }}>{error}</Text>
)}
</Box>
}
onRemove={userColor ? handleRemoveColor : undefined}
>
{(onOpen) => (
<Button
size="300"
variant="Secondary"
fill="Soft"
radii="300"
onClick={onOpen}
disabled={saving}
before={
<Box
style={{
width: toRem(16),
height: toRem(16),
borderRadius: toRem(4),
backgroundColor: userColor ?? localColor,
}}
/>
}
>
<Text size="T300">{userColor ? 'Change' : 'Choose'}</Text>
</Button>
)}
</HexColorPickerPopOut>
)}
</Box>
}
/>
</SequenceCard>
);
}
type DateHintProps = {
hasChanges: boolean;
handleReset: () => void;