import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Box, Button, Text, Spinner, color } from 'folds'; import { Method } from 'matrix-js-sdk'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { SettingTile } from '../../../components/setting-tile'; import { DECORATION_CATEGORIES, decorationUrl } from '../../lotus/avatarDecorations'; import { invalidateDecorationCache } from '../../../hooks/useAvatarDecoration'; const PROFILE_FIELD = 'io.lotus.avatar_decoration'; const CELL_SIZE = 72; function DecorationPreviewCell({ slug, name, selected, onSelect, }: { slug: string; name: string; selected: boolean; onSelect: (slug: string) => void; }) { return ( ); } export function ProfileDecoration() { const mx = useMatrixClient(); const userId = mx.getUserId()!; const [current, setCurrent] = useState(null); const [selected, setSelected] = useState(null); // Distinguish "confirmed no decoration" from "failed to load": a fetch // failure must not be shown as (and saved over) "None". const [loadError, setLoadError] = useState(false); const [loading, setLoading] = useState(true); // True once the user has picked/cleared a decoration — guards against the // mount-time fetch below clobbering a fresh selection if it resolves late // (mirrors ProfileStatus's statusDirtyRef in Profile.tsx). const dirtyRef = useRef(false); const fetchDecoration = useCallback(() => { let cancelled = false; setLoading(true); // Fetch the whole profile, not the `/{field}` sub-resource: an unset field // 404s (a console error for anyone without a decoration). The full profile // returns 200 with all fields incl. custom MSC4133 ones — read it out. mx.http .authedRequest>(Method.Get, `/profile/${encodeURIComponent(userId)}`) .then((res) => { if (cancelled) return; setLoadError(false); setLoading(false); if (dirtyRef.current) return; const val = (res[PROFILE_FIELD] as string | undefined) ?? null; setCurrent(val); setSelected(val); }) .catch(() => { if (cancelled) return; setLoading(false); // Do NOT touch current/selected here — a network failure is not proof // there's no decoration, and defaulting to null risks the user saving // "None" over a real, still-set decoration (see #46). setLoadError(true); }); return () => { cancelled = true; }; }, [mx, userId]); useEffect(() => fetchDecoration(), [fetchDecoration]); const [saveState, save] = useAsyncCallback( useCallback( async (slug: string | null) => { await mx.http.authedRequest( Method.Put, `/profile/${encodeURIComponent(userId)}/${PROFILE_FIELD}`, undefined, { [PROFILE_FIELD]: slug ?? '' }, ); setCurrent(slug); invalidateDecorationCache(userId); }, [mx, userId], ), ); const saving = saveState.status === AsyncStatus.Loading; const hasChanges = selected !== current; const handleSelect = (slug: string) => { dirtyRef.current = true; setSelected((prev) => (prev === slug ? null : slug)); }; const handleClear = () => { dirtyRef.current = true; setSelected(null); }; const handleSave = () => { // Refuse to save while the initial load failed: `current`/`selected` are // not known-good, so saving could silently overwrite a real decoration. if (!hasChanges || saving || loadError) return; save(selected); }; const handleRetry = () => { dirtyRef.current = false; fetchDecoration(); }; return ( Avatar Decoration } description={ Shown on your avatar to all Lotus Chat users. } > {/* Current selection preview */}
{selected && ( Selected decoration preview )}
{loadError ? 'Failed to load' : selected ? (DECORATION_CATEGORIES.flatMap((c) => c.decorations).find( (d) => d.slug === selected, )?.name ?? selected) : 'None'} {selected && !loadError && ( )} {hasChanges && !loadError && ( )}
{loadError && ( Could not load your current decoration. Saving is disabled until this succeeds, so you don’t overwrite it based on a wrong display. )} {saveState.status === AsyncStatus.Error && ( Failed to save. Try again. )} {/* Category grid */}
{DECORATION_CATEGORIES.map((category) => (
{category.label}
{category.decorations.map((d) => ( ))}
))}
); }