Ionosphere.tv
3
fork

Configure Feed

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

feat: display photo thumbnails, tune scroll, backfill image URLs

- Add image_url column to mentions, backfilled 192 photo posts with
CDN thumbnail URLs from Bluesky
- Render photo thumbnails inline in discussion items (max-h-24)
- Adjust column height estimation for photo items (150px vs 58px)
- Reduce scroll debounce threshold from 200 to 160

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+15 -6
+2
apps/ionosphere-appview/src/db.ts
··· 241 241 try { db.exec("ALTER TABLE mentions ADD COLUMN external_url TEXT"); } catch {} 242 242 try { db.exec("ALTER TABLE mentions ADD COLUMN og_title TEXT"); } catch {} 243 243 try { db.exec("ALTER TABLE mentions ADD COLUMN talk_rkey TEXT"); } catch {} 244 + try { db.exec("ALTER TABLE mentions ADD COLUMN image_url TEXT"); } catch {} 245 + try { db.exec("ALTER TABLE mentions ADD COLUMN has_images INTEGER DEFAULT 0"); } catch {} 244 246 245 247 db.exec(` 246 248 -- Jetstream cursor for resumable indexing
+4 -4
apps/ionosphere-appview/src/routes.ts
··· 322 322 // Posts: content_type = 'post' or NULL, top-level only, sorted by likes DESC 323 323 const posts = db.prepare( 324 324 `SELECT m.uri, m.author_did, m.text, m.created_at, m.likes, m.reposts, m.replies, 325 - m.content_type, m.external_url, m.og_title, m.talk_rkey, m.mention_type, 325 + m.content_type, m.external_url, m.og_title, m.talk_rkey, m.mention_type, m.image_url, 326 326 COALESCE(p.handle, m.author_handle) as author_handle, 327 327 p.display_name as author_display_name, 328 328 p.avatar_url as author_avatar_url, ··· 337 337 // Blogs: content_type = 'blog', top-level only 338 338 const blogs = db.prepare( 339 339 `SELECT m.uri, m.author_did, m.text, m.created_at, m.likes, m.reposts, m.replies, 340 - m.content_type, m.external_url, m.og_title, m.talk_rkey, m.mention_type, 340 + m.content_type, m.external_url, m.og_title, m.talk_rkey, m.mention_type, m.image_url, 341 341 COALESCE(p.handle, m.author_handle) as author_handle, 342 342 p.display_name as author_display_name, 343 343 p.avatar_url as author_avatar_url, ··· 351 351 // Videos: content_type = 'video', top-level only 352 352 const videos = db.prepare( 353 353 `SELECT m.uri, m.author_did, m.text, m.created_at, m.likes, m.reposts, m.replies, 354 - m.content_type, m.external_url, m.og_title, m.talk_rkey, m.mention_type, 354 + m.content_type, m.external_url, m.og_title, m.talk_rkey, m.mention_type, m.image_url, 355 355 COALESCE(p.handle, m.author_handle) as author_handle, 356 356 p.display_name as author_display_name, 357 357 p.avatar_url as author_avatar_url, ··· 365 365 // Photos: posts with images 366 366 const photos = db.prepare( 367 367 `SELECT m.uri, m.author_did, m.text, m.created_at, m.likes, m.reposts, m.replies, 368 - m.content_type, m.external_url, m.og_title, m.talk_rkey, m.mention_type, 368 + m.content_type, m.external_url, m.og_title, m.talk_rkey, m.mention_type, m.image_url, 369 369 COALESCE(p.handle, m.author_handle) as author_handle, 370 370 p.display_name as author_display_name, 371 371 p.avatar_url as author_avatar_url,
+9 -2
apps/ionosphere/src/app/discussion/DiscussionContent.tsx
··· 49 49 author_display_name: string | null; 50 50 author_avatar_url: string | null; 51 51 talk_title: string | null; 52 + image_url: string | null; 52 53 } 53 54 54 55 interface Stats { ··· 82 83 if (item.type === "stats") return 70; 83 84 if (item.type === "heading") return 28; 84 85 if (item.type === "vodDirectory") return 80; 85 - // item 86 + // item — photos are taller due to thumbnail 87 + if (item.type === "item" && item.item.image_url) return 150; 86 88 return 58; 87 89 } 88 90 ··· 341 343 if (numCols <= 1) return; 342 344 const el = containerRef.current; 343 345 if (!el) return; 344 - const THRESHOLD = 200; 346 + const THRESHOLD = 160; 345 347 const onWheel = (e: WheelEvent) => { 346 348 e.preventDefault(); 347 349 wheelAccum.current += e.deltaY; ··· 442 444 <span className="text-blue-400 text-[11px] truncate">{di.author_handle}</span> 443 445 <span className="text-neutral-600 text-[10px] ml-auto shrink-0">{di.likes || 0}&#9825;</span> 444 446 </div> 447 + {di.image_url && ( 448 + <div className="pl-[18px] mt-1 mb-1"> 449 + <img src={di.image_url} alt="" className="rounded w-full max-h-24 object-cover" loading="lazy" /> 450 + </div> 451 + )} 445 452 <div className="text-neutral-400 pl-[18px] line-clamp-2 -mt-px"> 446 453 {di.og_title || di.text} 447 454 </div>