From 109ef398ee9fb12a47f81e9a5d49ce902774e173 Mon Sep 17 00:00:00 2001 From: qdust41 Date: Mon, 6 Apr 2026 14:15:36 -0400 Subject: [PATCH] paginating comments and letting tweet authors delete comments on their post --- assets/js/index.tsx | 70 ++++++++++++++++++++++++++++++---------- lib/mixer/posts/tweet.ex | 1 + 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/assets/js/index.tsx b/assets/js/index.tsx index 278c8e4..1c7d500 100644 --- a/assets/js/index.tsx +++ b/assets/js/index.tsx @@ -813,21 +813,49 @@ function TweetDetail({ tweetId }: { tweetId: string }) { }, }); - const { data: comments, isLoading: commentsLoading } = useQuery({ + const commentsSentinelRef = useRef(null); + + const { + data: commentsData, + isLoading: commentsLoading, + fetchNextPage: fetchNextComments, + hasNextPage: hasMoreComments, + isFetchingNextPage: isFetchingMoreComments, + } = useInfiniteQuery({ queryKey: ["comments", tweetId], - queryFn: async () => { + queryFn: async ({ pageParam }: { pageParam: number }) => { const res = await readTweet({ fields: ["id", "content", "likes", "likedByMe", "parentTweetId", "userId", "state", "userEmail", "insertedAt", { media: ["id", "s3Key"] }], filter: { parentTweetId: { eq: tweetId } }, sort: "insertedAt", + page: { limit: COMMENTS_PAGE_SIZE, offset: pageParam }, headers: buildCSRFHeaders(), }); if (!res.success) throw new Error("Failed to load comments"); - const results = Array.isArray(res.data) ? res.data : (res.data as any)?.results ?? []; - return results as Tweet[]; + const pageData = res.data as any; + const comments: Tweet[] = Array.isArray(pageData) ? pageData : (pageData?.results ?? []); + const hasMore: boolean = Array.isArray(pageData) ? false : (pageData?.hasMore ?? false); + return { comments, hasMore, nextOffset: pageParam + COMMENTS_PAGE_SIZE }; }, + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.hasMore ? lastPage.nextOffset : undefined, }); + useEffect(() => { + const el = commentsSentinelRef.current; + if (!el) return; + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMoreComments && !isFetchingMoreComments) { + fetchNextComments(); + } + }, + { threshold: 0.1 }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, [hasMoreComments, isFetchingMoreComments, fetchNextComments]); + const deleteMutation = useMutation({ mutationFn: async () => { const res = await destroyTweet({ identity: tweetId, headers: buildCSRFHeaders() }); @@ -1006,26 +1034,33 @@ function TweetDetail({ tweetId }: { tweetId: string }) { {commentsLoading ? ( - ) : comments && comments.length > 0 ? ( -
- {comments.map((c) => ( - - ))} -
- ) : ( -
-

No replies yet. Be the first!

-
- )} + ) : (() => { + const comments = commentsData?.pages.flatMap((p) => p.comments) ?? []; + return comments.length > 0 ? ( +
+ {comments.map((c) => ( + + ))} +
+ {isFetchingMoreComments && } +
+ ) : ( +
+

No replies yet. Be the first!

+
+ ); + })()}
); } -function CommentCard({ comment }: { comment: Tweet }) { +function CommentCard({ comment, parentTweetOwnerId }: { comment: Tweet; parentTweetOwnerId?: string }) { const { userId: currentUserId } = useContext(AuthCtx); const canLike = !!currentUserId; - const canModify = !!currentUserId && comment.userId === currentUserId; + const canModify = !!currentUserId && ( + comment.userId === currentUserId || parentTweetOwnerId === currentUserId + ); const [confirmDelete, setConfirmDelete] = useState(false); const [error, setError] = useState(null); const qc = useQueryClient(); @@ -1116,6 +1151,7 @@ function CommentCard({ comment }: { comment: Tweet }) { } const FEED_PAGE_SIZE = 10; +const COMMENTS_PAGE_SIZE = 10; function FollowingFeed() { const { userId } = useContext(AuthCtx); diff --git a/lib/mixer/posts/tweet.ex b/lib/mixer/posts/tweet.ex index caed760..3b0f984 100644 --- a/lib/mixer/posts/tweet.ex +++ b/lib/mixer/posts/tweet.ex @@ -140,6 +140,7 @@ defmodule Mixer.Posts.Tweet do policy action(:destroy) do authorize_if relates_to_actor_via(:user) + authorize_if relates_to_actor_via([:parent_tweet, :user]) end policy action(:like) do