Sifa professional network API (Fastify, AT Protocol, Jetstream) sifa.id/

fix(activity): filter empty categories from activity feed tabs (#152)

* fix(activity): filter empty categories from activity feed tabs

Only include categories in availableCategories when the user has apps
with recentCount > 0. Previously, the scanner created rows for all
registered apps (including those with zero records), causing empty
category tabs like "Photos" to appear with no content.

* fix(activity): pass resolved Bluesky embeds with thumb URLs

The Bluesky AppView API returns resolved embeds (with CDN thumb URLs)
on item.post.embed, but we were only passing item.post.record which
has raw blob refs. Merge the resolved embed into the record so the
frontend card can display image thumbnails and link preview images.

authored by

Guido X Jansen and committed by
GitHub
e77c4b7e 645f0e7c

+18 -7
+18 -7
src/routes/activity.ts
··· 208 208 } 209 209 210 210 /** 211 + * Merge resolved embed (with thumb URLs) from the Bluesky AppView into 212 + * the raw record. The raw record only has blob refs for images; the 213 + * AppView response includes resolved CDN URLs in item.post.embed. 214 + */ 215 + function mergeResolvedEmbed(record: unknown, embed: unknown): unknown { 216 + if (!embed || typeof record !== 'object' || record === null) return record; 217 + return { ...(record as Record<string, unknown>), embed }; 218 + } 219 + 220 + /** 211 221 * Fetch recent items from Bluesky via the public AppView API. 212 222 */ 213 223 async function fetchBlueskyItems(did: string, limit: number): Promise<ActivityItem[]> { ··· 222 232 uri: item.post.uri, 223 233 collection: 'app.bsky.feed.post', 224 234 rkey: extractRkey(item.post.uri), 225 - record: item.post.record, 235 + record: mergeResolvedEmbed(item.post.record, item.post.embed), 226 236 appId: 'bluesky', 227 237 appName: 'Bluesky', 228 238 category: 'Posts', ··· 293 303 uri: item.post.uri, 294 304 collection: 'app.bsky.feed.post', 295 305 rkey: extractRkey(item.post.uri), 296 - record: item.post.record, 306 + record: mergeResolvedEmbed(item.post.record, item.post.embed), 297 307 appId: 'bluesky', 298 308 appName: 'Bluesky', 299 309 category: 'Posts', ··· 446 456 const stats = await getVisibleAppStats(db, did); 447 457 const registry = getAppsRegistry(); 448 458 449 - // Compute available categories from all visible apps the user has 450 - // (a row exists in user_app_stats when the scanner found the collection in the PDS) 459 + // Compute available categories from apps that actually have recent content 451 460 const availableCategories = [ 452 461 ...new Set( 453 462 stats 463 + .filter((s) => s.recentCount > 0) 454 464 .map((s) => registry.find((e) => e.id === s.appId)?.category) 455 465 .filter((c): c is string => c !== undefined), 456 466 ), 457 467 ]; 458 - // Filter apps by category if specified 468 + // Filter apps by category if specified, skipping apps with no recent content 469 + const activeStats = stats.filter((s) => s.recentCount > 0); 459 470 let targetApps: { stat: (typeof stats)[number]; entry: AppRegistryEntry }[]; 460 471 if (categoryParam === 'all') { 461 - targetApps = stats 472 + targetApps = activeStats 462 473 .slice(0, 5) 463 474 .map((stat) => { 464 475 const entry = registry.find((e) => e.id === stat.appId); ··· 466 477 }) 467 478 .filter((x): x is NonNullable<typeof x> => x !== null); 468 479 } else { 469 - targetApps = stats 480 + targetApps = activeStats 470 481 .map((stat) => { 471 482 const entry = registry.find((e) => e.id === stat.appId); 472 483 return entry && entry.category === categoryParam ? { stat, entry } : null;