Ionosphere.tv
3
fork

Configure Feed

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

fix: debounce scroll speed, add Bluesky links, fix API URL fallback

- Accumulate wheel deltaY with 200px threshold before advancing columns
- Add "bsky ↗" link on every item pointing to the original post
- Fix NEXT_PUBLIC_API_URL fallback from 9401 to 3001
- Always show link row (not just when talk_rkey or external_url exist)

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

+35 -19
+35 -19
apps/ionosphere/src/app/discussion/DiscussionContent.tsx
··· 321 321 }, [startIndex, flowItems, columnHeight]); 322 322 323 323 // Wheel handler 324 + // Debounced wheel scroll — accumulate deltaY, advance when threshold reached 325 + const wheelAccum = useRef(0); 326 + const wheelTimer = useRef<ReturnType<typeof setTimeout>>(); 324 327 useEffect(() => { 325 328 if (numCols <= 1) return; 326 329 const el = containerRef.current; 327 330 if (!el) return; 331 + const THRESHOLD = 200; 328 332 const onWheel = (e: WheelEvent) => { 329 333 e.preventDefault(); 330 - if (e.deltaY > 0) scrollForward(); 331 - else if (e.deltaY < 0) scrollBack(); 334 + wheelAccum.current += e.deltaY; 335 + clearTimeout(wheelTimer.current); 336 + wheelTimer.current = setTimeout(() => { wheelAccum.current = 0; }, 150); 337 + if (wheelAccum.current > THRESHOLD) { 338 + scrollForward(); 339 + wheelAccum.current = 0; 340 + } else if (wheelAccum.current < -THRESHOLD) { 341 + scrollBack(); 342 + wheelAccum.current = 0; 343 + } 332 344 }; 333 345 el.addEventListener("wheel", onWheel, { passive: false }); 334 346 return () => el.removeEventListener("wheel", onWheel); ··· 337 349 const handleSelect = useCallback( 338 350 async (rkey: string) => { 339 351 try { 340 - const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:9401"; 352 + const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; 341 353 const res = await fetch(`${API_BASE}/xrpc/tv.ionosphere.getTalk?rkey=${encodeURIComponent(rkey)}`); 342 354 if (!res.ok) return; 343 355 const { talk } = await res.json(); ··· 419 431 <div className="text-neutral-400 pl-[18px] line-clamp-2 -mt-px"> 420 432 {di.og_title || di.text} 421 433 </div> 422 - {(di.talk_rkey || di.external_url) && ( 423 - <div className="pl-[18px] mt-0.5 flex gap-2 text-[10px]"> 424 - {di.talk_rkey && ( 425 - <button onClick={() => handleSelect(di.talk_rkey!)} 426 - className="text-neutral-500 hover:text-neutral-300 truncate"> 427 - {di.talk_title || "Talk"} &rarr; 428 - </button> 429 - )} 430 - {di.external_url && ( 431 - <a href={di.external_url} target="_blank" rel="noopener" 432 - className={di.content_type === "blog" ? "text-emerald-500" : di.content_type === "video" ? "text-purple-400" : "text-neutral-500"}> 433 - {(() => { try { return new URL(di.external_url).hostname; } catch { return "link"; } })()} &#8599; 434 - </a> 435 - )} 436 - </div> 437 - )} 434 + <div className="pl-[18px] mt-0.5 flex gap-2 text-[10px]"> 435 + {di.talk_rkey && ( 436 + <button onClick={() => handleSelect(di.talk_rkey!)} 437 + className="text-neutral-500 hover:text-neutral-300 truncate"> 438 + {di.talk_title || "Talk"} &rarr; 439 + </button> 440 + )} 441 + {di.external_url && ( 442 + <a href={di.external_url} target="_blank" rel="noopener" 443 + className={di.content_type === "blog" ? "text-emerald-500" : di.content_type === "video" ? "text-purple-400" : "text-neutral-500"}> 444 + {(() => { try { return new URL(di.external_url).hostname; } catch { return "link"; } })()} &#8599; 445 + </a> 446 + )} 447 + {di.author_handle && ( 448 + <a href={`https://bsky.app/profile/${di.author_handle}/post/${di.uri.split("/").pop()}`} 449 + target="_blank" rel="noopener" className="text-neutral-600 hover:text-neutral-400"> 450 + bsky &#8599; 451 + </a> 452 + )} 453 + </div> 438 454 </div> 439 455 ); 440 456 };