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: improve status history view

Hugo fb6395e9 c96d8700

+76 -20
+76 -20
packages/feature-requests/src/ui/pages/feature-request.tsx
··· 240 240 ); 241 241 } 242 242 243 - function StatusHistory({ requestId, version }: { requestId: string; version: number }) { 244 - const { data, pending, loading } = useQuery( 245 - () => getStatusHistory(requestId), 246 - [requestId, version], 247 - ); 243 + function StatusHistory({ 244 + requestId, 245 + version, 246 + createdAt, 247 + authorDid, 248 + authorHandle, 249 + }: { 250 + requestId: string; 251 + version: number; 252 + createdAt: string; 253 + authorDid: string; 254 + authorHandle: string | null; 255 + }) { 256 + const statuses = useSignal< 257 + Array<{ id: string; authorDid: string; authorHandle: string | null; status: string; createdAt: string }> | null 258 + >(null); 259 + const loading = useSignal(false); 260 + const expandedRef = useRef(false); 248 261 249 - if (pending) return loading ? <p class={ui.muted}>Loading history...</p> : null; 250 - if (!data || data.statuses.length === 0) return null; 262 + const fetchStatuses = async () => { 263 + loading.value = true; 264 + try { 265 + const res = await getStatusHistory(requestId); 266 + statuses.value = res.statuses; 267 + } catch { 268 + statuses.value = []; 269 + } finally { 270 + loading.value = false; 271 + } 272 + }; 273 + 274 + const handleToggle = (expanded: boolean) => { 275 + expandedRef.current = expanded; 276 + if (expanded && statuses.value === null && !loading.value) { 277 + fetchStatuses(); 278 + } 279 + }; 280 + 281 + // Refetch or invalidate when a status change occurs 282 + const prevVersion = useRef(version); 283 + useEffect(() => { 284 + if (version === prevVersion.current) return; 285 + prevVersion.current = version; 286 + if (expandedRef.current) { 287 + fetchStatuses(); 288 + } else { 289 + statuses.value = null; 290 + } 291 + }, [version]); 292 + 293 + // Virtual initial "requested" status — derived from creation, not a real record 294 + const initialEntry = { id: "initial", authorDid, authorHandle, status: "requested", createdAt }; 295 + // API returns newest first; append the virtual entry at the end 296 + const allStatuses = statuses.value !== null ? [...statuses.value, initialEntry] : [initialEntry]; 251 297 252 298 return ( 253 - <CollapsibleSection title="Status history"> 254 - <div class={ui.stackSm}> 255 - {data.statuses.map((entry) => ( 256 - <div key={entry.id} class={ui.metaRow}> 257 - <span class={ui.muted}>{entry.authorHandle ?? entry.authorDid}</span> 258 - <span> 259 - set status to <strong>{statusLabel(entry.status)}</strong> 260 - </span> 261 - <span class={ui.muted}>{formatDate(entry.createdAt, fullDateOpts)}</span> 262 - </div> 263 - ))} 264 - </div> 299 + <CollapsibleSection title="Status history" onToggle={handleToggle}> 300 + {loading.value ? ( 301 + <p class={ui.muted}>Loading...</p> 302 + ) : ( 303 + <div class={ui.stackSm}> 304 + {allStatuses.map((entry) => ( 305 + <div key={entry.id} class={ui.metaRow}> 306 + <span class={ui.muted}>{entry.authorHandle ?? entry.authorDid}</span> 307 + <span> 308 + set status to <strong>{statusLabel(entry.status)}</strong> 309 + </span> 310 + <span class={ui.muted}>{formatDate(entry.createdAt, fullDateOpts)}</span> 311 + </div> 312 + ))} 313 + </div> 314 + )} 265 315 </CollapsibleSection> 266 316 ); 267 317 } ··· 637 687 currentHandle={currentHandle} 638 688 /> 639 689 640 - <StatusHistory requestId={fr.id} version={statusVersion.value} /> 690 + <StatusHistory 691 + requestId={fr.id} 692 + version={statusVersion.value} 693 + createdAt={fr.createdAt} 694 + authorDid={fr.authorDid} 695 + authorHandle={fr.authorHandle ?? null} 696 + /> 641 697 642 698 {((currentDid === fr.authorDid || isAdminOrOwner.value) && fr.status === "requested") || 643 699 data.duplicateCount > 0 ? (