import { useCallback, useRef, useState } from 'react' import { PostsService } from '../posts/postsService.ts' import { useUser } from '../../user/user.ts' import { MediaService } from '../../media/mediaService.ts' import NewPostWidget from '../../../components/NewPostWidget.tsx' import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx' import NavBar from '../../../components/NavBar.tsx' import AuthNavButtons from '../../auth/components/AuthNavButtons.tsx' import { useSaveSignupCodeToLocalStorage } from '../../../hooks/useSaveSignupCodeToLocalStorage.ts' import { Post } from '../posts/posts.ts' import { produce, WritableDraft } from 'immer' import PostItem from '../components/PostItem.tsx' import { useIntersectionLoad } from '../../../hooks/useIntersectionLoad.ts' interface HomePageProps { postsService: PostsService mediaService: MediaService } const PageSize = 20 export default function HomePage({ postsService, mediaService }: HomePageProps) { const user = useUser() useSaveSignupCodeToLocalStorage() const [isSubmitting, setIsSubmitting] = useState(false) const fetchPosts = useCallback( async (cursor: string | null, amount: number | null) => { return postsService.loadPublicFeed(cursor, amount) }, [postsService], ) const [posts, setPosts] = useState([]) const [hasMore, setHasMore] = useState(true) const [error, setError] = useState(null) const cursor = useRef(null) const loading = useRef(false) const loadNextPage = useCallback(async () => { if (loading.current || !hasMore || error) return loading.current = true try { const delay = new Promise((resolve) => setTimeout(resolve, 500)) const pagePromise = fetchPosts(cursor.current, PageSize) const [page] = await Promise.all([pagePromise, delay]) setHasMore(page.length >= PageSize) cursor.current = page.at(-1)?.postId ?? null setPosts((prev) => [...prev, ...page]) } catch (e: unknown) { const err = e as Error setError(err.message) } finally { loading.current = false } }, [fetchPosts, hasMore, error]) const onCreatePost = useCallback( async ( content: string, files: { file: File; width: number; height: number }[], isPublic: boolean, ) => { setIsSubmitting(true) if (user == null) throw new Error('Not logged in') try { const media = await Promise.all( files.map(async ({ file, width, height }) => { const { mediaId, url } = await mediaService.uploadImage(file) return { mediaId, url, width, height, } }), ) const post = await postsService.createNew(user.id, content, media, isPublic) setPosts((pages) => [post, ...pages]) } catch (error) { console.error('Failed to create post:', error) } finally { setIsSubmitting(false) } }, [mediaService, postsService, setPosts, user], ) const isLoggedIn = user != null const onAddReaction = async (postId: string, emoji: string) => { await postsService.addReaction(postId, emoji) setPosts((prev) => produce(prev, (draft: WritableDraft) => { const post = draft.find((p) => p.postId === postId) if (!post) return const theReaction = post.reactions.find((r) => r.emoji === emoji) if (theReaction) { theReaction.count++ theReaction.didReact = true } else { post.reactions.push({ emoji, count: 1, didReact: true }) } }), ) } const onClearReaction = async (postId: string, emoji: string) => { await postsService.removeReaction(postId, emoji) setPosts((prev) => produce(prev, (draft: WritableDraft) => { const post = draft.find((p) => p.postId === postId) if (!post) return const theReaction = post.reactions.find((r) => r.emoji === emoji) if (theReaction) { theReaction.count = Math.max(theReaction.count - 1, 0) theReaction.didReact = false } }), ) } const sentinelRef = useRef(null) useIntersectionLoad(loadNextPage, sentinelRef) return ( } >
{isLoggedIn && }
{posts.map((post) => ( onAddReaction(post.postId, emoji)} clearReaction={(emoji) => onClearReaction(post.postId, emoji)} /> ))}
) }