fix: normalize federated asset URLs in explore and social stores

Resolve remote instance avatars and space icons at the data boundary
so they render correctly instead of 404ing against the home instance.
This commit is contained in:
Jannis Braun
2026-03-08 22:40:02 +01:00
parent b598236f82
commit c3bf635fc9
3 changed files with 24 additions and 7 deletions
+2
View File
@@ -335,12 +335,14 @@ function handleEvent(origin: string, event: ServerEvent): void {
// ─── Social events (all origins — federation) ────────────────────────── // ─── Social events (all origins — federation) ──────────────────────────
case 'friend_request_received': { case 'friend_request_received': {
if (!isHome && event.request.user) normalizeUserAssets(event.request.user, origin);
const { addIncomingRequest } = useSocialStore.getState(); const { addIncomingRequest } = useSocialStore.getState();
addIncomingRequest(event.request, origin); addIncomingRequest(event.request, origin);
break; break;
} }
case 'friend_request_accepted': { case 'friend_request_accepted': {
if (!isHome) normalizeUserAssets(event.friend, origin);
const { addFriendFromAccepted } = useSocialStore.getState(); const { addFriendFromAccepted } = useSocialStore.getState();
addFriendFromAccepted(event.friend, event.requestId, origin); addFriendFromAccepted(event.friend, event.requestId, origin);
break; break;
+4
View File
@@ -1,6 +1,7 @@
import { create } from 'zustand'; import { create } from 'zustand';
import type { ExploreSpace, JoinRequest, SpaceWithChannelsAndMembers } from '@backspace/shared'; import type { ExploreSpace, JoinRequest, SpaceWithChannelsAndMembers } from '@backspace/shared';
import { api } from '../api/client'; import { api } from '../api/client';
import { resolveAssetUrl } from '../utils/assetUrls';
import { useInstanceStore } from './instanceStore'; import { useInstanceStore } from './instanceStore';
import { useSpaceStore } from './spaceStore'; import { useSpaceStore } from './spaceStore';
@@ -89,6 +90,9 @@ export const useExploreStore = create<ExploreState>((set, get) => ({
const key = `${space.id}:${origin}`; const key = `${space.id}:${origin}`;
if (seen.has(key)) continue; if (seen.has(key)) continue;
seen.add(key); seen.add(key);
if (origin && space.icon) {
space.icon = resolveAssetUrl(space.icon, origin) ?? space.icon;
}
allSpaces.push({ ...space, _instanceOrigin: origin, joined: space.joined ?? false }); allSpaces.push({ ...space, _instanceOrigin: origin, joined: space.joined ?? false });
} }
} }
+18 -7
View File
@@ -2,6 +2,7 @@ import { create } from 'zustand';
import type { Friend, FriendRequest, User } from '@backspace/shared'; import type { Friend, FriendRequest, User } from '@backspace/shared';
import { api } from '../api/client'; import { api } from '../api/client';
import { useInstanceStore } from './instanceStore'; import { useInstanceStore } from './instanceStore';
import { normalizeUserAssets } from '../utils/assetUrls';
// ─── Tagged types (origin tracking for federation) ─────────────────────────── // ─── Tagged types (origin tracking for federation) ───────────────────────────
@@ -66,6 +67,7 @@ export const useSocialStore = create<SocialState>((set, get) => ({
const key = `${friend.id}:${origin}`; const key = `${friend.id}:${origin}`;
if (seen.has(key)) continue; if (seen.has(key)) continue;
seen.add(key); seen.add(key);
if (origin) normalizeUserAssets(friend, origin);
allFriends.push({ ...friend, _instanceOrigin: origin }); allFriends.push({ ...friend, _instanceOrigin: origin });
} }
} }
@@ -99,6 +101,7 @@ export const useSocialStore = create<SocialState>((set, get) => ({
const key = `${request.id}:${origin}`; const key = `${request.id}:${origin}`;
if (seen.has(key)) continue; if (seen.has(key)) continue;
seen.add(key); seen.add(key);
if (origin && request.user) normalizeUserAssets(request.user, origin);
allRequests.push({ ...request, _instanceOrigin: origin }); allRequests.push({ ...request, _instanceOrigin: origin });
} }
} }
@@ -217,22 +220,30 @@ export const useSocialStore = create<SocialState>((set, get) => ({
const instances = useInstanceStore.getState().instances; const instances = useInstanceStore.getState().instances;
const connectedInstances = instances.filter(i => i.status === 'connected'); const connectedInstances = instances.filter(i => i.status === 'connected');
const results = await Promise.allSettled([ // Pair each promise with its origin for asset normalization
api.social.search(query), const searches: { promise: Promise<User[]>; origin: string }[] = [
...connectedInstances.map(inst => inst.api.social.search(query)), { promise: api.social.search(query), origin: '' },
]); ...connectedInstances.map(inst => ({
promise: inst.api.social.search(query),
origin: inst.origin,
})),
];
const results = await Promise.allSettled(searches.map(s => s.promise));
const allUsers: User[] = []; const allUsers: User[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
for (const result of results) { results.forEach((result, i) => {
if (result.status !== 'fulfilled') continue; if (result.status !== 'fulfilled') return;
const origin = searches[i]!.origin;
for (const user of result.value) { for (const user of result.value) {
if (seen.has(user.id)) continue; if (seen.has(user.id)) continue;
seen.add(user.id); seen.add(user.id);
if (origin) normalizeUserAssets(user, origin);
allUsers.push(user); allUsers.push(user);
} }
} });
return allUsers; return allUsers;
} catch (err) { } catch (err) {