Files
backspace/packages/web/src/stores/exploreStore.ts
T
Jannis Braun 6e44a4ef2f fix: DM avatar color and reactions in federation + explore/server discovery
- Fix DM welcome header avatar using home identity for correct gradient color
- Register DM channel IDs in channelOriginMap so federated DM operations
  (reactions, messages, typing) route to the correct instance
- Pass origin when creating DM channels from friends list and WS events
- Add server discovery/explore page with public server listings
- Add server visibility and description fields
2026-03-04 18:02:43 +01:00

145 lines
4.6 KiB
TypeScript

import { create } from 'zustand';
import type { ExploreServer, JoinRequest, ServerWithChannelsAndMembers } from '@backspace/shared';
import { api } from '../api/client';
import { useInstanceStore } from './instanceStore';
import { useServerStore } from './serverStore';
// ─── Types ──────────────────────────────────────────────────────────────────
export interface TaggedExploreServer extends ExploreServer {
_instanceOrigin: string; // '' = home instance
}
interface ExploreState {
servers: TaggedExploreServer[];
myRequests: JoinRequest[];
searchQuery: string;
isLoading: boolean;
discoveryEnabled: boolean;
error: string | null;
fetchServers: (query?: string) => Promise<void>;
fetchMyRequests: () => Promise<void>;
publicJoin: (server: TaggedExploreServer) => Promise<ServerWithChannelsAndMembers>;
requestJoin: (server: TaggedExploreServer, message?: string) => Promise<JoinRequest>;
setSearchQuery: (q: string) => void;
reset: () => void;
}
// ─── Helpers ────────────────────────────────────────────────────────────────
function getApiForOrigin(origin: string) {
if (!origin) return api;
const instance = useInstanceStore.getState().instances.find(i => i.origin === origin);
return instance?.api ?? api;
}
// ─── Store ──────────────────────────────────────────────────────────────────
export const useExploreStore = create<ExploreState>((set, get) => ({
servers: [],
myRequests: [],
searchQuery: '',
isLoading: false,
discoveryEnabled: true,
error: null,
fetchServers: async (query?: string) => {
set({ isLoading: true, error: null });
try {
const instances = useInstanceStore.getState().instances;
const connectedInstances = instances.filter(i => i.status === 'connected');
// Fetch from home + all connected remote instances in parallel
const results = await Promise.allSettled([
api.explore.list(query).then(res => ({ ...res, origin: '' })),
...connectedInstances.map(inst =>
inst.api.explore.list(query).then(res => ({ ...res, origin: inst.origin }))
),
]);
const allServers: TaggedExploreServer[] = [];
const seen = new Set<string>(); // dedup by serverId+origin
let homeDiscoveryEnabled = true;
for (const result of results) {
if (result.status !== 'fulfilled') continue;
const { servers, discoveryEnabled, origin } = result.value;
// Track home instance discovery state
if (!origin) {
homeDiscoveryEnabled = discoveryEnabled;
}
for (const server of servers) {
const key = `${server.id}:${origin}`;
if (seen.has(key)) continue;
seen.add(key);
allServers.push({ ...server, _instanceOrigin: origin });
}
}
set({
servers: allServers,
discoveryEnabled: homeDiscoveryEnabled,
isLoading: false,
});
} catch (err) {
set({
isLoading: false,
error: err instanceof Error ? err.message : 'Failed to fetch servers',
});
}
},
fetchMyRequests: async () => {
try {
const { requests } = await api.explore.myJoinRequests('pending');
set({ myRequests: requests });
} catch {
// Non-critical — silently fail
}
},
publicJoin: async (server: TaggedExploreServer) => {
const client = getApiForOrigin(server._instanceOrigin);
const fullServer = await client.explore.publicJoin(server.id);
// Add to server store
useServerStore.getState().addServerFromReady(server._instanceOrigin, fullServer);
// Remove from explore list
set((state) => ({
servers: state.servers.filter(s =>
!(s.id === server.id && s._instanceOrigin === server._instanceOrigin)
),
}));
return fullServer;
},
requestJoin: async (server: TaggedExploreServer, message?: string) => {
const client = getApiForOrigin(server._instanceOrigin);
const request = await client.explore.requestJoin(server.id, message);
set((state) => ({
myRequests: [...state.myRequests, request],
}));
return request;
},
setSearchQuery: (q: string) => set({ searchQuery: q }),
reset: () => set({
servers: [],
myRequests: [],
searchQuery: '',
isLoading: false,
discoveryEnabled: true,
error: null,
}),
}));