-
- @{post.authorName}
-
- • {formattedDate}
+ @{post.authorName}• {formattedDate}
{post.content}
@@ -77,10 +73,11 @@ function PostReactions({ post, addReaction, clearReaction }: PostReactionsProps)
return (
onClick()}
+ onClick={onClick}
/>
)
})}
diff --git a/src/app/feed/pages/AuthorPage.tsx b/src/app/feed/pages/AuthorPage.tsx
deleted file mode 100644
index 1cec207..0000000
--- a/src/app/feed/pages/AuthorPage.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { useCallback } from 'react'
-import FeedView from '../components/FeedView.tsx'
-import { PostsService } from '../posts/postsService.ts'
-import { useParams } from 'react-router-dom'
-import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx'
-import NavBar from '../../../components/NavBar.tsx'
-import { useFeedViewModel } from '../components/FeedView.ts'
-import NavButton from '../../../components/buttons/NavButton.tsx'
-import AuthNavButtons from '../../auth/components/AuthNavButtons.tsx'
-
-interface AuthorPageParams {
- postsService: PostsService
-}
-
-export default function AuthorPage({ postsService }: AuthorPageParams) {
- const { username } = useParams()
-
- const fetchPosts = useCallback(
- async (cursor: string | null, amount: number | null) => {
- return postsService.loadByAuthor(username!, cursor, amount)
- },
- [postsService, username],
- )
-
- const { pages, loadNextPage } = useFeedViewModel(fetchPosts)
-
- const addReaction = async (postId: string, emoji: string) => {
- await postsService.addReaction(postId, emoji)
- }
-
- const clearReaction = async (postId: string, emoji: string) => {
- await postsService.removeReaction(postId, emoji)
- }
-
- return (
-
- home
-
-
- }
- >
-
-
- )
-}
diff --git a/src/app/feed/pages/HomePage.tsx b/src/app/feed/pages/HomePage.tsx
index f4f6827..e0b73b8 100644
--- a/src/app/feed/pages/HomePage.tsx
+++ b/src/app/feed/pages/HomePage.tsx
@@ -1,20 +1,23 @@
-import { useCallback, useState } from 'react'
+import { useCallback, useRef, useState } from 'react'
import FeedView from '../components/FeedView.tsx'
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 { useFeedViewModel } from '../components/FeedView.ts'
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'
interface HomePageProps {
postsService: PostsService
mediaService: MediaService
}
+const PageSize = 20
+
export default function HomePage({ postsService, mediaService }: HomePageProps) {
const user = useUser()
useSaveSignupCodeToLocalStorage()
@@ -27,7 +30,31 @@ export default function HomePage({ postsService, mediaService }: HomePageProps)
[postsService],
)
- const { pages, setPages, loadNextPage } = useFeedViewModel(fetchPosts)
+ 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 (
@@ -51,24 +78,52 @@ export default function HomePage({ postsService, mediaService }: HomePageProps)
}),
)
const post = await postsService.createNew(user.id, content, media, isPublic)
- setPages((pages) => [[post], ...pages])
+ setPosts((pages) => [post, ...pages])
} catch (error) {
console.error('Failed to create post:', error)
} finally {
setIsSubmitting(false)
}
},
- [mediaService, postsService, setPages, user],
+ [mediaService, postsService, setPosts, user],
)
const isLoggedIn = user != null
- const addReaction = async (postId: string, emoji: string) => {
+ 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 clearReaction = async (postId: string, emoji: string) => {
+ 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
+ }
+ }),
+ )
}
return (
@@ -82,10 +137,10 @@ export default function HomePage({ postsService, mediaService }: HomePageProps)
{isLoggedIn && }
diff --git a/src/app/feed/posts/posts.ts b/src/app/feed/posts/posts.ts
index b25dba3..bcd1b0b 100644
--- a/src/app/feed/posts/posts.ts
+++ b/src/app/feed/posts/posts.ts
@@ -1,5 +1,6 @@
import { Temporal } from '@js-temporal/polyfill'
import { components } from '../../api/schema.ts'
+import { immerable } from 'immer'
export interface EmojiReaction {
emoji: string
@@ -8,6 +9,8 @@ export interface EmojiReaction {
}
export class Post {
+ [immerable] = true
+
public readonly postId: string
public readonly content: string
public readonly media: PostMedia[]
@@ -44,9 +47,9 @@ export class Post {
dto.reactions.map((r) => ({
emoji: r.emoji,
count: r.count,
- didReact: r.didReact
+ didReact: r.didReact,
})),
- dto.possibleReactions
+ dto.possibleReactions,
)
}
}
diff --git a/src/app/feed/posts/postsService.ts b/src/app/feed/posts/postsService.ts
index d00c99b..e6c2c88 100644
--- a/src/app/feed/posts/postsService.ts
+++ b/src/app/feed/posts/postsService.ts
@@ -60,31 +60,23 @@ export class PostsService {
}
async addReaction(postId: string, emoji: string): Promise {
- const response = await this.client.POST('/posts/{postId}/reactions', {
+ await this.client.POST('/posts/{postId}/reactions', {
params: {
- path: { postId }
+ path: { postId },
},
body: { emoji },
credentials: 'include',
})
-
- if (!response.data) {
- throw new Error('Failed to add reaction')
- }
}
async removeReaction(postId: string, emoji: string): Promise {
- const response = await this.client.DELETE('/posts/{postId}/reactions', {
+ await this.client.DELETE('/posts/{postId}/reactions', {
params: {
- path: { postId }
+ path: { postId },
},
body: { emoji },
credentials: 'include',
})
-
- if (!response.data) {
- throw new Error('Failed to remove reaction')
- }
}
}
diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts
new file mode 100644
index 0000000..9f74991
--- /dev/null
+++ b/src/utils/debounce.ts
@@ -0,0 +1,17 @@
+import { useCallback, useRef } from 'react'
+
+export function useDebounce(
+ fn: (...args: Args) => Promise,
+ delay: number,
+) {
+ const timeout = useRef | null>(null)
+
+ return useCallback(
+ (...args: Args) => {
+ if (timeout.current) clearTimeout(timeout.current)
+
+ setTimeout(() => fn(...args), delay)
+ },
+ [delay, fn],
+ )
+}