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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user