Skip to content

Commit 14b2e34

Browse files
authored
fix: recover profile and avatar loading (#566)
### Description Fixes profile and avatar loading regressions introduced around the MSC4440 bio compatibility work. This PR addresses two related problems: - Own profile data could disappear when `useUserProfile` encountered `gay.fomx.biography` in an unexpected shape or when the field was missing entirely. - Avatar components could get stuck in fallback mode after a transient image load failure, causing the account avatar and other avatars to disappear until a full reload. The fix does three things: - parses `gay.fomx.biography` defensively with optional access instead of assuming `m.text[0].body` always exists - retries profile fetches when the cache was previously poisoned into an `_fetched`-only state after normalization failed - resets avatar error state when the image `src` changes so transient media/auth failures can recover on later renders This was verified against the broken Account settings / account switcher behavior where avatar, banner, bio, status, and pronouns could appear missing. Fixup of #559 #### Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings ### AI disclosure: - [x] Partially AI assisted (GitHub Copilot) `useUserProfile.ts`: Hardened MSC4440 bio parsing so missing or malformed `gay.fomx.biography` data does not throw during normalization, and allowed refetching when the profile cache only contains `_fetched: true` from a prior failed normalization. `UserAvatar.tsx` and `RoomAvatar.tsx`: Reset local image error state when `src` changes so a temporary image load failure does not permanently lock the component into fallback rendering.
2 parents 5433f29 + 724b45f commit 14b2e34

3 files changed

Lines changed: 16 additions & 4 deletions

File tree

src/app/components/room-avatar/RoomAvatar.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { JoinRule } from '$types/matrix-sdk';
22
import { AvatarFallback, Icon, Icons, color } from 'folds';
3-
import { ComponentProps, ReactNode, forwardRef, useState } from 'react';
3+
import { ComponentProps, ReactNode, forwardRef, useEffect, useState } from 'react';
44
import { getRoomIconSrc } from '$utils/room';
55
import colorMXID from '$utils/colorMXID';
66
import * as css from './RoomAvatar.css';
@@ -17,6 +17,10 @@ type RoomAvatarProps = {
1717
export function RoomAvatar({ roomId, src, alt, renderFallback, uniformIcons }: RoomAvatarProps) {
1818
const [error, setError] = useState(false);
1919

20+
useEffect(() => {
21+
setError(false);
22+
}, [src]);
23+
2024
if (!src || error) {
2125
return (
2226
<AvatarFallback

src/app/components/user-avatar/UserAvatar.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AvatarFallback, AvatarImage, color } from 'folds';
2-
import { ReactEventHandler, ReactNode, useState } from 'react';
2+
import { ReactEventHandler, ReactNode, useEffect, useState } from 'react';
33
import classNames from 'classnames';
44
import colorMXID from '$utils/colorMXID';
55
import * as css from './UserAvatar.css';
@@ -14,6 +14,10 @@ type UserAvatarProps = {
1414
export function UserAvatar({ className, userId, src, alt, renderFallback }: UserAvatarProps) {
1515
const [error, setError] = useState(false);
1616

17+
useEffect(() => {
18+
setError(false);
19+
}, [src]);
20+
1721
const handleLoad: ReactEventHandler<HTMLImageElement> = (evt) => {
1822
evt.currentTarget.setAttribute('data-image-loaded', 'true');
1923
};

src/app/hooks/useUserProfile.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type UserProfile = {
3232
};
3333

3434
const normalizeInfo = (info: any): UserProfile => {
35+
const msc4440Bio = info['gay.fomx.biography'] as MSC4440Bio | undefined;
3536
const knownKeys = new Set([
3637
'avatar_url',
3738
'displayname',
@@ -61,7 +62,7 @@ const normalizeInfo = (info: any): UserProfile => {
6162
pronouns: info['io.fsky.nyx.pronouns'],
6263
timezone: info['us.cloke.msc4175.tz'] || info['m.tz'],
6364
bio:
64-
(info['gay.fomx.biography'] satisfies MSC4440Bio)['m.text'][0].body ||
65+
msc4440Bio?.['m.text']?.[0]?.body ||
6566
info['moe.sable.app.bio'] ||
6667
info['chat.commet.profile_bio'],
6768
status: info['chat.commet.profile_status'],
@@ -103,7 +104,10 @@ export const useUserProfile = (
103104
const cached = useAtomValue(userSelector);
104105
const setGlobalProfiles = useSetAtom(profilesCacheAtom);
105106

106-
const needsFetch = !!userId && userId !== 'undefined' && !cached?._fetched;
107+
const hasOnlyFetchedMarker =
108+
cached?._fetched === true && Object.keys(cached ?? {}).every((key) => key === '_fetched');
109+
const needsFetch =
110+
!!userId && userId !== 'undefined' && (!cached?._fetched || hasOnlyFetchedMarker);
107111

108112
useEffect(() => {
109113
if (!needsFetch) return undefined;

0 commit comments

Comments
 (0)