paginating comments and letting tweet authors delete comments on their post
This commit is contained in:
@@ -813,21 +813,49 @@ function TweetDetail({ tweetId }: { tweetId: string }) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: comments, isLoading: commentsLoading } = useQuery({
|
const commentsSentinelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: commentsData,
|
||||||
|
isLoading: commentsLoading,
|
||||||
|
fetchNextPage: fetchNextComments,
|
||||||
|
hasNextPage: hasMoreComments,
|
||||||
|
isFetchingNextPage: isFetchingMoreComments,
|
||||||
|
} = useInfiniteQuery({
|
||||||
queryKey: ["comments", tweetId],
|
queryKey: ["comments", tweetId],
|
||||||
queryFn: async () => {
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
const res = await readTweet({
|
const res = await readTweet({
|
||||||
fields: ["id", "content", "likes", "likedByMe", "parentTweetId", "userId", "state", "userEmail", "insertedAt", { media: ["id", "s3Key"] }],
|
fields: ["id", "content", "likes", "likedByMe", "parentTweetId", "userId", "state", "userEmail", "insertedAt", { media: ["id", "s3Key"] }],
|
||||||
filter: { parentTweetId: { eq: tweetId } },
|
filter: { parentTweetId: { eq: tweetId } },
|
||||||
sort: "insertedAt",
|
sort: "insertedAt",
|
||||||
|
page: { limit: COMMENTS_PAGE_SIZE, offset: pageParam },
|
||||||
headers: buildCSRFHeaders(),
|
headers: buildCSRFHeaders(),
|
||||||
});
|
});
|
||||||
if (!res.success) throw new Error("Failed to load comments");
|
if (!res.success) throw new Error("Failed to load comments");
|
||||||
const results = Array.isArray(res.data) ? res.data : (res.data as any)?.results ?? [];
|
const pageData = res.data as any;
|
||||||
return results as Tweet[];
|
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({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const res = await destroyTweet({ identity: tweetId, headers: buildCSRFHeaders() });
|
const res = await destroyTweet({ identity: tweetId, headers: buildCSRFHeaders() });
|
||||||
@@ -1006,26 +1034,33 @@ function TweetDetail({ tweetId }: { tweetId: string }) {
|
|||||||
|
|
||||||
{commentsLoading ? (
|
{commentsLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : comments && comments.length > 0 ? (
|
) : (() => {
|
||||||
|
const comments = commentsData?.pages.flatMap((p) => p.comments) ?? [];
|
||||||
|
return comments.length > 0 ? (
|
||||||
<div className="mx-comments-list">
|
<div className="mx-comments-list">
|
||||||
{comments.map((c) => (
|
{comments.map((c) => (
|
||||||
<CommentCard key={c.id} comment={c} />
|
<CommentCard key={c.id} comment={c} parentTweetOwnerId={tweet?.userId} />
|
||||||
))}
|
))}
|
||||||
|
<div ref={commentsSentinelRef} style={{ height: "1px" }} />
|
||||||
|
{isFetchingMoreComments && <Spinner />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mx-empty mx-empty--sm">
|
<div className="mx-empty mx-empty--sm">
|
||||||
<p className="mx-empty-sub">No replies yet. Be the first!</p>
|
<p className="mx-empty-sub">No replies yet. Be the first!</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommentCard({ comment }: { comment: Tweet }) {
|
function CommentCard({ comment, parentTweetOwnerId }: { comment: Tweet; parentTweetOwnerId?: string }) {
|
||||||
const { userId: currentUserId } = useContext(AuthCtx);
|
const { userId: currentUserId } = useContext(AuthCtx);
|
||||||
const canLike = !!currentUserId;
|
const canLike = !!currentUserId;
|
||||||
const canModify = !!currentUserId && comment.userId === currentUserId;
|
const canModify = !!currentUserId && (
|
||||||
|
comment.userId === currentUserId || parentTweetOwnerId === currentUserId
|
||||||
|
);
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
@@ -1116,6 +1151,7 @@ function CommentCard({ comment }: { comment: Tweet }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FEED_PAGE_SIZE = 10;
|
const FEED_PAGE_SIZE = 10;
|
||||||
|
const COMMENTS_PAGE_SIZE = 10;
|
||||||
|
|
||||||
function FollowingFeed() {
|
function FollowingFeed() {
|
||||||
const { userId } = useContext(AuthCtx);
|
const { userId } = useContext(AuthCtx);
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ defmodule Mixer.Posts.Tweet do
|
|||||||
|
|
||||||
policy action(:destroy) do
|
policy action(:destroy) do
|
||||||
authorize_if relates_to_actor_via(:user)
|
authorize_if relates_to_actor_via(:user)
|
||||||
|
authorize_if relates_to_actor_via([:parent_tweet, :user])
|
||||||
end
|
end
|
||||||
|
|
||||||
policy action(:like) do
|
policy action(:like) do
|
||||||
|
|||||||
Reference in New Issue
Block a user