Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol. app.exosphere.site
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: performance and ux improvements

Hugo 63af6493 ac0705f9

+52 -30
+3 -1
packages/app/src/server.ts
··· 153 153 154 154 if (sphere) { 155 155 const prefetches = ssrPrefetch(c.req.path, sphereHandleFromUrl); 156 + const cookieHeader = c.req.header("Cookie"); 157 + const prefetchInit = cookieHeader ? { headers: { Cookie: cookieHeader } } : undefined; 156 158 for (const prefetch of prefetches) { 157 159 try { 158 - const res = await app.request(prefetch.apiUrl); 160 + const res = await app.request(prefetch.apiUrl, prefetchInit); 159 161 if (res.ok) pageData[prefetch.key] = await res.json(); 160 162 } catch { 161 163 /* prefetch failed — client will fetch */
+10 -2
packages/app/src/ssr-prefetch.ts
··· 15 15 key: "feature-requests", 16 16 apiUrl: `${apiBase}/feature-requests?status=requested,approved,in-progress`, 17 17 }, 18 + { key: "feature-request-votes", apiUrl: `${apiBase}/feature-requests/votes` }, 18 19 ]; 19 20 if (modulePath === "/infuse/done") 20 - return [{ key: "feature-requests-done", apiUrl: `${apiBase}/feature-requests?status=done` }]; 21 + return [ 22 + { key: "feature-requests-done", apiUrl: `${apiBase}/feature-requests?status=done` }, 23 + { key: "feature-request-votes", apiUrl: `${apiBase}/feature-requests/votes` }, 24 + ]; 21 25 if (modulePath === "/infuse/not-planned") 22 26 return [ 23 27 { 24 28 key: "feature-requests-not-planned", 25 29 apiUrl: `${apiBase}/feature-requests?status=not-planned`, 26 30 }, 31 + { key: "feature-request-votes", apiUrl: `${apiBase}/feature-requests/votes` }, 27 32 ]; 28 33 const frMatch = modulePath.match(/^\/infuse\/(\d+)$/); 29 34 if (frMatch) 30 - return [{ key: "feature-request", apiUrl: `${apiBase}/feature-requests/${frMatch[1]}` }]; 35 + return [ 36 + { key: "feature-request", apiUrl: `${apiBase}/feature-requests/${frMatch[1]}` }, 37 + { key: "feature-request-votes", apiUrl: `${apiBase}/feature-requests/votes` }, 38 + ]; 31 39 return []; 32 40 }
+1 -1
packages/client/src/hooks.ts
··· 9 9 refetch: () => void; 10 10 } 11 11 12 - const LOADING_DELAY = 400; 12 + const LOADING_DELAY = 300; 13 13 const queryCache = new Map<string, unknown>(); 14 14 15 15 export function useQuery<T>(
+11 -2
packages/feature-requests/src/ui/pages/feature-request.tsx
··· 476 476 const { params } = useRoute(); 477 477 const { route } = useLocation(); 478 478 const number = parseInt(params.number, 10); 479 - const votedIds = useSignal<Set<string>>(new Set()); 480 479 const voteCountAdjust = useSignal(0); 481 480 const statusVersion = useSignal(0); 482 481 const prefetched = ssrPageData.peek()?.["feature-request"] as 483 482 | Awaited<ReturnType<typeof getFeatureRequest>> 483 + | undefined; 484 + const prefetchedVotes = ssrPageData.peek()?.["feature-request-votes"] as 485 + | Awaited<ReturnType<typeof getMyVotes>> 484 486 | undefined; 485 487 useEffect(() => { 486 488 const pd = ssrPageData.peek(); 487 - if (pd && "feature-request" in pd) delete pd["feature-request"]; 489 + if (pd) { 490 + if ("feature-request" in pd) delete pd["feature-request"]; 491 + if ("feature-request-votes" in pd) delete pd["feature-request-votes"]; 492 + } 488 493 }, []); 489 494 const { data, pending, loading, error, refetch } = useQuery( 490 495 () => getFeatureRequest(number), ··· 496 501 const currentDid = isAuthenticated ? auth.value.did : null; 497 502 const currentHandle = isAuthenticated ? auth.value.handle : null; 498 503 504 + const votedIds = useSignal<Set<string>>( 505 + new Set(prefetchedVotes?.votes), 506 + ); 499 507 const votesQuery = useQuery( 500 508 () => (isAuthenticated ? getMyVotes() : Promise.resolve(null)), 501 509 [isAuthenticated], 510 + prefetchedVotes ? { initialData: prefetchedVotes, cacheKey: "feature-request-votes" } : { cacheKey: "feature-request-votes" }, 502 511 ); 503 512 useEffect(() => { 504 513 if (votesQuery.data) {
+27 -24
packages/feature-requests/src/ui/pages/feature-requests.tsx
··· 145 145 export function FeatureRequestsListPage() { 146 146 const { activeTab, statuses } = useActiveTab(); 147 147 const showForm = useSignal(false); 148 - const votedIds = useSignal<Set<string>>(new Set()); 149 148 const { sortBy, sortOrder } = useSortParams(); 150 149 const prefetchKey = 151 150 activeTab === "requests" ? "feature-requests" : `feature-requests-${statuses.join(",")}`; 152 151 const prefetched = ssrPageData.peek()?.[prefetchKey] as 153 152 | Awaited<ReturnType<typeof getFeatureRequests>> 154 153 | undefined; 155 - // Consume SSR key after first render so client-side re-navigation fetches fresh data 154 + const prefetchedVotes = ssrPageData.peek()?.["feature-request-votes"] as 155 + | Awaited<ReturnType<typeof getMyVotes>> 156 + | undefined; 157 + // Consume SSR keys after first render so client-side re-navigation fetches fresh data 156 158 useEffect(() => { 157 159 const pd = ssrPageData.peek(); 158 - if (pd && prefetchKey in pd) delete pd[prefetchKey]; 160 + if (pd) { 161 + if (prefetchKey in pd) delete pd[prefetchKey]; 162 + if ("feature-request-votes" in pd) delete pd["feature-request-votes"]; 163 + } 159 164 }, []); 160 165 const { data, pending, loading, error, refetch } = useQuery( 161 166 () => getFeatureRequests(statuses, sortBy.value, sortOrder.value), ··· 166 171 const isAuthenticated = auth.value.authenticated; 167 172 const currentDid = isAuthenticated ? auth.value.did : null; 168 173 169 - // Fetch user's votes when authenticated 174 + const votedIds = useSignal<Set<string>>( 175 + new Set(prefetchedVotes?.votes), 176 + ); 177 + // Fetch user's votes when authenticated (skips if SSR-prefetched) 170 178 const votesQuery = useQuery( 171 179 () => (isAuthenticated ? getMyVotes() : Promise.resolve(null)), 172 180 [isAuthenticated], 181 + prefetchedVotes ? { initialData: prefetchedVotes, cacheKey: "feature-request-votes" } : { cacheKey: "feature-request-votes" }, 173 182 ); 174 183 useEffect(() => { 175 184 if (votesQuery.data) { ··· 184 193 if (role === "owner" || role === "admin") return true; 185 194 return sphereData.sphere.ownerDid === auth.value.did; 186 195 })(); 196 + 197 + const tabs = [ 198 + { tab: "requests", label: "Requests", path: "/infuse" }, 199 + { tab: "done", label: "Done", path: "/infuse/done" }, 200 + { tab: "not-planned", label: "Not planned", path: "/infuse/not-planned" }, 201 + ] as const; 187 202 188 203 const onCreated = () => { 189 204 showForm.value = false; ··· 265 280 </div> 266 281 267 282 <nav class={ui.tabNav}> 268 - {activeTab === "requests" ? ( 269 - <span class={ui.tabNavActive}>Requests</span> 270 - ) : ( 271 - <a href={spherePath("/infuse")} class={ui.tabNavLink}> 272 - Requests 273 - </a> 274 - )} 275 - {activeTab === "done" ? ( 276 - <span class={ui.tabNavActive}>Done</span> 277 - ) : ( 278 - <a href={spherePath("/infuse/done")} class={ui.tabNavLink}> 279 - Done 280 - </a> 281 - )} 282 - {activeTab === "not-planned" ? ( 283 - <span class={ui.tabNavActive}>Not planned</span> 284 - ) : ( 285 - <a href={spherePath("/infuse/not-planned")} class={ui.tabNavLink}> 286 - Not planned 287 - </a> 283 + {tabs.map(({ tab, label, path }) => 284 + activeTab === tab ? ( 285 + <span class={ui.tabNavActive}>{label}</span> 286 + ) : ( 287 + <a href={spherePath(path)} class={ui.tabNavLink}> 288 + {label} 289 + </a> 290 + ), 288 291 )} 289 292 </nav> 290 293