'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; interface Post { uri: string; cid: string; author: { did: string; handle: string; displayName?: string; avatar?: string; }; record: { text: string; createdAt: string; reply?: unknown; }; replyCount?: number; repostCount?: number; likeCount?: number; indexedAt: string; isRepost?: boolean; isThread?: boolean; } interface FeedCursor { handle: string; cursor?: string; } export default function BlogPosts() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [cursors, setCursors] = useState([]); const observerRef = useRef(null); const loadMoreRef = useRef(null); const seenUris = useRef>(new Set()); const handles = [ 'stream1.atmosphereconf.org', 'stream2.atmosphereconf.org', 'stream3.atmosphereconf.org', 'iame.li', 'stream.place' ]; const fetchMorePosts = async () => { if (loadingMore) return; setLoadingMore(true); try { const newPosts: Post[] = []; const newCursors: FeedCursor[] = []; for (const cursorData of cursors.length > 0 ? cursors : handles.map(h => ({ handle: h, cursor: undefined }))) { const cursorParam = cursorData.cursor ? `&cursor=${cursorData.cursor}` : ''; const response = await fetch(`https://api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${cursorData.handle}&limit=20${cursorParam}`); if (!response.ok) { newCursors.push(cursorData); continue; } const data = await response.json(); for (const item of data.feed) { if (seenUris.current.has(item.post.uri)) { continue; } if (item.reply && item.reply.parent?.author?.did !== item.post.author.did) { continue; } seenUris.current.add(item.post.uri); const post: Post = { uri: item.post.uri, cid: item.post.cid, author: item.post.author, record: item.post.record, replyCount: item.post.replyCount, repostCount: item.post.repostCount, likeCount: item.post.likeCount, indexedAt: item.post.indexedAt, isRepost: item.reason?.$type === 'app.bsky.feed.defs#reasonRepost', isThread: item.reply && item.reply.parent?.author?.did === item.post.author.did, }; newPosts.push(post); } // Save cursor for next fetch if (data.cursor) { newCursors.push({ handle: cursorData.handle, cursor: data.cursor }); } } newPosts.sort((a, b) => new Date(b.indexedAt).getTime() - new Date(a.indexedAt).getTime()); setPosts(prev => [...prev, ...newPosts]); setCursors(newCursors); } catch (error) { console.error('Failed to fetch posts:', error); } finally { setLoadingMore(false); setLoading(false); } }; useEffect(() => { fetchMorePosts(); }, []); // Infinite scroll observer const handleObserver = useCallback((entries: IntersectionObserverEntry[]) => { const target = entries[0]; if (target.isIntersecting && !loadingMore && cursors.length > 0) { fetchMorePosts(); } }, [loadingMore, cursors]); useEffect(() => { const option = { root: null, rootMargin: '200px', threshold: 0 }; observerRef.current = new IntersectionObserver(handleObserver, option); if (loadMoreRef.current) { observerRef.current.observe(loadMoreRef.current); } return () => { if (observerRef.current) { observerRef.current.disconnect(); } }; }, [handleObserver]); // Convert Bluesky URI to web URL const getPostUrl = (uri: string) => { // uri format: at://did:plc:xxx/app.bsky.feed.post/xxxxx const parts = uri.replace('at://', '').split('/'); const did = parts[0]; const postId = parts[2]; return `https://bsky.app/profile/${did}/post/${postId}`; }; if (loading) { return (
Loading iBlogs...
); } return (