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:
@@ -1,15 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Box, Text, Icon, Icons, MenuItem, config } from 'folds';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Avatar, Box, Icon, Icons, MenuItem, Text, config } from 'folds';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { sessionsAtom, Session } from '../../state/sessions';
|
||||
import { setLocalStorageItem } from '../../state/utils/atomWithLocalStorage';
|
||||
import { getMxIdServer } from '../../utils/matrix';
|
||||
import { getMxIdServer, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useUserProfile } from '../../hooks/useUserProfile';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { UserAvatar } from '../user-avatar';
|
||||
|
||||
type AccountSwitcherProps = {
|
||||
currentUserId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads and displays an avatar for a session that is not the active client.
|
||||
* Fetches the profile directly via the homeserver REST API using stored credentials.
|
||||
*/
|
||||
function OtherSessionAvatar({ session }: { session: Session }) {
|
||||
const [src, setSrc] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${session.baseUrl}/_matrix/client/v3/profile/${encodeURIComponent(session.userId)}`,
|
||||
{ headers: { Authorization: `Bearer ${session.accessToken}` } }
|
||||
);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (data.avatar_url) {
|
||||
const match = (data.avatar_url as string).match(/^mxc:\/\/([^/]+)\/(.+)$/);
|
||||
if (match) {
|
||||
setSrc(
|
||||
`${session.baseUrl}/_matrix/media/v3/thumbnail/${match[1]}/${match[2]}?width=32&height=32&method=crop`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore — avatar just won't show
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, [session.userId, session.baseUrl, session.accessToken]);
|
||||
|
||||
return (
|
||||
<Avatar size="300" style={{ width: '1.75rem', height: '1.75rem', minWidth: '1.75rem' }}>
|
||||
<UserAvatar
|
||||
userId={session.userId}
|
||||
src={src}
|
||||
alt={session.userId}
|
||||
renderFallback={() => <Icon size="100" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
|
||||
const deleteAllMatrixDatabases = async (): Promise<void> => {
|
||||
console.log('[AccountSwitcher] Deleting all Matrix IndexedDB databases...');
|
||||
|
||||
@@ -79,6 +125,11 @@ const deleteAllMatrixDatabases = async (): Promise<void> => {
|
||||
export function AccountSwitcher({ currentUserId }: AccountSwitcherProps) {
|
||||
const sessions = useAtomValue(sessionsAtom);
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const currentProfile = useUserProfile(currentUserId ?? '');
|
||||
const currentAvatarUrl = currentProfile.avatarUrl
|
||||
? mxcUrlToHttp(mx, currentProfile.avatarUrl, useAuthentication, 32, 32, 'crop') ?? undefined
|
||||
: undefined;
|
||||
|
||||
const handleSwitchAccount = async (session: Session) => {
|
||||
if (session.userId === currentUserId) return;
|
||||
@@ -120,8 +171,8 @@ export function AccountSwitcher({ currentUserId }: AccountSwitcherProps) {
|
||||
const hasMultipleAccounts = sessions.length > 1;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400" style={{ padding: config.space.S200 }}>
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="L400" style={{ paddingInline: config.space.S200, paddingBlock: config.space.S100 }}>
|
||||
{hasMultipleAccounts ? 'Switch Account' : 'Accounts'}
|
||||
</Text>
|
||||
|
||||
@@ -133,11 +184,19 @@ export function AccountSwitcher({ currentUserId }: AccountSwitcherProps) {
|
||||
radii="300"
|
||||
variant="SurfaceVariant"
|
||||
disabled
|
||||
style={{ paddingInline: config.space.S200 }}
|
||||
after={<Icon size="100" src={Icons.Check} filled />}
|
||||
>
|
||||
<Box gap="200" alignItems="Center">
|
||||
<Icon size="100" src={Icons.User} />
|
||||
<Box direction="Column" gap="100">
|
||||
<Avatar size="300" style={{ width: '1.75rem', height: '1.75rem', minWidth: '1.75rem' }}>
|
||||
<UserAvatar
|
||||
userId={currentUserId}
|
||||
src={currentAvatarUrl}
|
||||
alt={currentUserId}
|
||||
renderFallback={() => <Icon size="100" src={Icons.User} filled />}
|
||||
/>
|
||||
</Avatar>
|
||||
<Box direction="Column" style={{ gap: '2px' }}>
|
||||
<Text size="T300" truncate>
|
||||
{currentUserId}
|
||||
</Text>
|
||||
@@ -162,10 +221,11 @@ export function AccountSwitcher({ currentUserId }: AccountSwitcherProps) {
|
||||
radii="300"
|
||||
onClick={() => handleSwitchAccount(session)}
|
||||
variant="Surface"
|
||||
style={{ paddingInline: config.space.S200 }}
|
||||
>
|
||||
<Box gap="200" alignItems="Center">
|
||||
<Icon size="100" src={Icons.User} />
|
||||
<Box direction="Column" gap="100">
|
||||
<OtherSessionAvatar session={session} />
|
||||
<Box direction="Column" style={{ gap: '2px' }}>
|
||||
<Text size="T300" truncate>
|
||||
{session.userId}
|
||||
</Text>
|
||||
|
||||
Reference in New Issue
Block a user