feat: Implement infinite scrolling for posts and replies on the main feed and user profile pages using cursor-based pagination.
This commit is contained in:
+106
-23
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { ArrowLeftIcon, CalendarIcon } from '@/components/Icons';
|
import { ArrowLeftIcon, CalendarIcon } from '@/components/Icons';
|
||||||
@@ -94,6 +94,10 @@ export default function ProfilePage() {
|
|||||||
const [repliesLoading, setRepliesLoading] = useState(false);
|
const [repliesLoading, setRepliesLoading] = useState(false);
|
||||||
const [followersLoading, setFollowersLoading] = useState(false);
|
const [followersLoading, setFollowersLoading] = useState(false);
|
||||||
const [followingLoading, setFollowingLoading] = useState(false);
|
const [followingLoading, setFollowingLoading] = useState(false);
|
||||||
|
const [postsLoadingMore, setPostsLoadingMore] = useState(false);
|
||||||
|
const [repliesLoadingMore, setRepliesLoadingMore] = useState(false);
|
||||||
|
const [postsCursor, setPostsCursor] = useState<string | null>(null);
|
||||||
|
const [repliesCursor, setRepliesCursor] = useState<string | null>(null);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [profileForm, setProfileForm] = useState({
|
const [profileForm, setProfileForm] = useState({
|
||||||
displayName: '',
|
displayName: '',
|
||||||
@@ -130,13 +134,74 @@ export default function ProfilePage() {
|
|||||||
.catch(() => setLoading(false));
|
.catch(() => setLoading(false));
|
||||||
|
|
||||||
setPostsLoading(true);
|
setPostsLoading(true);
|
||||||
|
setPostsCursor(null);
|
||||||
|
setRepliesCursor(null);
|
||||||
fetch(`/api/users/${handle}/posts`)
|
fetch(`/api/users/${handle}/posts`)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => setPosts(data.posts || []))
|
.then(data => {
|
||||||
|
setPosts(data.posts || []);
|
||||||
|
setPostsCursor(data.nextCursor || null);
|
||||||
|
})
|
||||||
.catch(() => { })
|
.catch(() => { })
|
||||||
.finally(() => setPostsLoading(false));
|
.finally(() => setPostsLoading(false));
|
||||||
}, [handle]);
|
}, [handle]);
|
||||||
|
|
||||||
|
// Infinite scroll ref
|
||||||
|
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Load more posts
|
||||||
|
const loadMorePosts = useCallback(async () => {
|
||||||
|
if (!postsCursor || postsLoadingMore) return;
|
||||||
|
setPostsLoadingMore(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/users/${handle}/posts?cursor=${postsCursor}`);
|
||||||
|
const data = await res.json();
|
||||||
|
setPosts(prev => [...prev, ...(data.posts || [])]);
|
||||||
|
setPostsCursor(data.nextCursor || null);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setPostsLoadingMore(false);
|
||||||
|
}
|
||||||
|
}, [handle, postsCursor, postsLoadingMore]);
|
||||||
|
|
||||||
|
// Load more replies
|
||||||
|
const loadMoreReplies = useCallback(async () => {
|
||||||
|
if (!repliesCursor || repliesLoadingMore || !user) return;
|
||||||
|
setRepliesLoadingMore(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/posts?type=replies&userId=${user.id}&cursor=${repliesCursor}`);
|
||||||
|
const data = await res.json();
|
||||||
|
setRepliesPosts(prev => [...prev, ...(data.posts || [])]);
|
||||||
|
setRepliesCursor(data.nextCursor || null);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setRepliesLoadingMore(false);
|
||||||
|
}
|
||||||
|
}, [user, repliesCursor, repliesLoadingMore]);
|
||||||
|
|
||||||
|
// Infinite scroll observer
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadMoreRef.current) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
if (activeTab === 'posts' && postsCursor && !postsLoadingMore) {
|
||||||
|
loadMorePosts();
|
||||||
|
} else if (activeTab === 'replies' && repliesCursor && !repliesLoadingMore) {
|
||||||
|
loadMoreReplies();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(loadMoreRef.current);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [activeTab, postsCursor, repliesCursor, postsLoadingMore, repliesLoadingMore, loadMorePosts, loadMoreReplies]);
|
||||||
|
|
||||||
const handleLike = async (postId: string, currentLiked: boolean) => {
|
const handleLike = async (postId: string, currentLiked: boolean) => {
|
||||||
const method = currentLiked ? 'DELETE' : 'POST';
|
const method = currentLiked ? 'DELETE' : 'POST';
|
||||||
await fetch(`/api/posts/${postId}/like`, { method });
|
await fetch(`/api/posts/${postId}/like`, { method });
|
||||||
@@ -223,9 +288,13 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
if (activeTab === 'replies' && user) {
|
if (activeTab === 'replies' && user) {
|
||||||
setRepliesLoading(true);
|
setRepliesLoading(true);
|
||||||
|
setRepliesCursor(null);
|
||||||
fetch(`/api/posts?type=replies&userId=${user.id}`)
|
fetch(`/api/posts?type=replies&userId=${user.id}`)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => setRepliesPosts(data.posts || []))
|
.then(data => {
|
||||||
|
setRepliesPosts(data.posts || []);
|
||||||
|
setRepliesCursor(data.nextCursor || null);
|
||||||
|
})
|
||||||
.catch(() => setRepliesPosts([]))
|
.catch(() => setRepliesPosts([]))
|
||||||
.finally(() => setRepliesLoading(false));
|
.finally(() => setRepliesLoading(false));
|
||||||
}
|
}
|
||||||
@@ -765,16 +834,23 @@ export default function ProfilePage() {
|
|||||||
<p>No posts yet</p>
|
<p>No posts yet</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
posts.map((post, index) => (
|
<>
|
||||||
<PostCard
|
{posts.map((post, index) => (
|
||||||
key={`${post.id}-${index}`}
|
<PostCard
|
||||||
post={post}
|
key={`${post.id}-${index}`}
|
||||||
onLike={handleLike}
|
post={post}
|
||||||
onRepost={handleRepost}
|
onLike={handleLike}
|
||||||
onComment={handleComment}
|
onRepost={handleRepost}
|
||||||
onDelete={handleDelete}
|
onComment={handleComment}
|
||||||
/>
|
onDelete={handleDelete}
|
||||||
))
|
/>
|
||||||
|
))}
|
||||||
|
<div ref={loadMoreRef} style={{ padding: '24px', textAlign: 'center' }}>
|
||||||
|
{postsLoadingMore && (
|
||||||
|
<span style={{ color: 'var(--foreground-tertiary)' }}>Loading more...</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -788,16 +864,23 @@ export default function ProfilePage() {
|
|||||||
<p>No replies yet</p>
|
<p>No replies yet</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
repliesPosts.map((post, index) => (
|
<>
|
||||||
<PostCard
|
{repliesPosts.map((post, index) => (
|
||||||
key={`${post.id}-${index}`}
|
<PostCard
|
||||||
post={post}
|
key={`${post.id}-${index}`}
|
||||||
onLike={handleLike}
|
post={post}
|
||||||
onRepost={handleRepost}
|
onLike={handleLike}
|
||||||
onComment={handleComment}
|
onRepost={handleRepost}
|
||||||
onDelete={handleDelete}
|
onComment={handleComment}
|
||||||
/>
|
onDelete={handleDelete}
|
||||||
))
|
/>
|
||||||
|
))}
|
||||||
|
<div ref={loadMoreRef} style={{ padding: '24px', textAlign: 'center' }}>
|
||||||
|
{repliesLoadingMore && (
|
||||||
|
<span style={{ color: 'var(--foreground-tertiary)' }}>Loading more...</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db, posts, users, media, follows, mutes, blocks, likes, remoteFollows, remotePosts, notifications } from '@/db';
|
import { db, posts, users, media, follows, mutes, blocks, likes, remoteFollows, remotePosts, notifications } from '@/db';
|
||||||
import { requireAuth } from '@/lib/auth';
|
import { requireAuth } from '@/lib/auth';
|
||||||
import { eq, desc, and, inArray, isNull, isNotNull, notInArray, or } from 'drizzle-orm';
|
import { eq, desc, and, inArray, isNull, isNotNull, notInArray, or, lt } from 'drizzle-orm';
|
||||||
import type { SQL } from 'drizzle-orm';
|
import type { SQL } from 'drizzle-orm';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -398,8 +398,20 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
if (type === 'local') {
|
if (type === 'local') {
|
||||||
// Local node posts only - no fediverse content
|
// Local node posts only - no fediverse content
|
||||||
|
let whereCondition = baseFilter;
|
||||||
|
|
||||||
|
// Apply cursor-based pagination
|
||||||
|
if (cursor) {
|
||||||
|
const cursorPost = await db.query.posts.findFirst({
|
||||||
|
where: eq(posts.id, cursor),
|
||||||
|
});
|
||||||
|
if (cursorPost) {
|
||||||
|
whereCondition = buildWhere(baseFilter, lt(posts.createdAt, cursorPost.createdAt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
feedPosts = await db.query.posts.findMany({
|
feedPosts = await db.query.posts.findMany({
|
||||||
where: baseFilter,
|
where: whereCondition,
|
||||||
with: {
|
with: {
|
||||||
author: true,
|
author: true,
|
||||||
bot: true,
|
bot: true,
|
||||||
@@ -441,8 +453,20 @@ export async function GET(request: Request) {
|
|||||||
.slice(0, limit) as any;
|
.slice(0, limit) as any;
|
||||||
} else if (type === 'user' && userId) {
|
} else if (type === 'user' && userId) {
|
||||||
// User's posts (excluding replies)
|
// User's posts (excluding replies)
|
||||||
|
let whereCondition = buildWhere(baseFilter, eq(posts.userId, userId));
|
||||||
|
|
||||||
|
// Apply cursor-based pagination
|
||||||
|
if (cursor) {
|
||||||
|
const cursorPost = await db.query.posts.findFirst({
|
||||||
|
where: eq(posts.id, cursor),
|
||||||
|
});
|
||||||
|
if (cursorPost) {
|
||||||
|
whereCondition = buildWhere(baseFilter, eq(posts.userId, userId), lt(posts.createdAt, cursorPost.createdAt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
feedPosts = await db.query.posts.findMany({
|
feedPosts = await db.query.posts.findMany({
|
||||||
where: buildWhere(baseFilter, eq(posts.userId, userId)),
|
where: whereCondition,
|
||||||
with: {
|
with: {
|
||||||
author: true,
|
author: true,
|
||||||
bot: true,
|
bot: true,
|
||||||
@@ -456,8 +480,20 @@ export async function GET(request: Request) {
|
|||||||
});
|
});
|
||||||
} else if (type === 'replies' && userId) {
|
} else if (type === 'replies' && userId) {
|
||||||
// User's replies only
|
// User's replies only
|
||||||
|
let whereCondition = buildWhere(repliesFilter, eq(posts.userId, userId));
|
||||||
|
|
||||||
|
// Apply cursor-based pagination
|
||||||
|
if (cursor) {
|
||||||
|
const cursorPost = await db.query.posts.findFirst({
|
||||||
|
where: eq(posts.id, cursor),
|
||||||
|
});
|
||||||
|
if (cursorPost) {
|
||||||
|
whereCondition = buildWhere(repliesFilter, eq(posts.userId, userId), lt(posts.createdAt, cursorPost.createdAt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
feedPosts = await db.query.posts.findMany({
|
feedPosts = await db.query.posts.findMany({
|
||||||
where: buildWhere(repliesFilter, eq(posts.userId, userId)),
|
where: whereCondition,
|
||||||
with: {
|
with: {
|
||||||
author: true,
|
author: true,
|
||||||
bot: true,
|
bot: true,
|
||||||
@@ -598,9 +634,21 @@ export async function GET(request: Request) {
|
|||||||
// Include own posts + posts from followed users
|
// Include own posts + posts from followed users
|
||||||
const allowedUserIds = [user.id, ...followingIds];
|
const allowedUserIds = [user.id, ...followingIds];
|
||||||
|
|
||||||
|
// Build where condition with cursor support
|
||||||
|
let whereCondition = buildWhere(baseFilter, inArray(posts.userId, allowedUserIds));
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
const cursorPost = await db.query.posts.findFirst({
|
||||||
|
where: eq(posts.id, cursor),
|
||||||
|
});
|
||||||
|
if (cursorPost) {
|
||||||
|
whereCondition = buildWhere(baseFilter, inArray(posts.userId, allowedUserIds), lt(posts.createdAt, cursorPost.createdAt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get local posts from people the user follows + their own posts
|
// Get local posts from people the user follows + their own posts
|
||||||
const localPosts = await db.query.posts.findMany({
|
const localPosts = await db.query.posts.findMany({
|
||||||
where: buildWhere(baseFilter, inArray(posts.userId, allowedUserIds)),
|
where: whereCondition,
|
||||||
with: {
|
with: {
|
||||||
author: true,
|
author: true,
|
||||||
bot: true,
|
bot: true,
|
||||||
@@ -610,7 +658,7 @@ export async function GET(request: Request) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: [desc(posts.createdAt)],
|
orderBy: [desc(posts.createdAt)],
|
||||||
limit: limit * 2, // Get more to account for mixing with remote
|
limit: cursor ? limit : limit * 2, // Get more on first load to account for mixing with remote
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get handles of remote users we follow
|
// Get handles of remote users we follow
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export async function GET(request: Request, context: RouteContext) {
|
|||||||
const cleanHandle = handle.toLowerCase().replace(/^@/, '');
|
const cleanHandle = handle.toLowerCase().replace(/^@/, '');
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50);
|
const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50);
|
||||||
|
const cursor = searchParams.get('cursor');
|
||||||
|
|
||||||
const remote = parseRemoteHandle(handle);
|
const remote = parseRemoteHandle(handle);
|
||||||
|
|
||||||
@@ -374,9 +375,27 @@ export async function GET(request: Request, context: RouteContext) {
|
|||||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's posts
|
// Get user's posts with cursor-based pagination
|
||||||
|
const { lt } = await import('drizzle-orm');
|
||||||
|
|
||||||
|
let whereConditions = and(eq(posts.userId, user.id), eq(posts.isRemoved, false));
|
||||||
|
|
||||||
|
// If cursor provided, get posts older than the cursor
|
||||||
|
if (cursor) {
|
||||||
|
const cursorPost = await db.query.posts.findFirst({
|
||||||
|
where: eq(posts.id, cursor),
|
||||||
|
});
|
||||||
|
if (cursorPost) {
|
||||||
|
whereConditions = and(
|
||||||
|
eq(posts.userId, user.id),
|
||||||
|
eq(posts.isRemoved, false),
|
||||||
|
lt(posts.createdAt, cursorPost.createdAt)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let userPosts: any[] = await db.query.posts.findMany({
|
let userPosts: any[] = await db.query.posts.findMany({
|
||||||
where: and(eq(posts.userId, user.id), eq(posts.isRemoved, false)),
|
where: whereConditions,
|
||||||
with: {
|
with: {
|
||||||
author: true,
|
author: true,
|
||||||
media: true,
|
media: true,
|
||||||
|
|||||||
+68
-19
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useAuth } from '@/lib/contexts/AuthContext';
|
import { useAuth } from '@/lib/contexts/AuthContext';
|
||||||
import { PostCard } from '@/components/PostCard';
|
import { PostCard } from '@/components/PostCard';
|
||||||
@@ -22,6 +22,8 @@ export default function Home() {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [posts, setPosts] = useState<Post[]>([]);
|
const [posts, setPosts] = useState<Post[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
const [nextCursor, setNextCursor] = useState<string | null>(null);
|
||||||
const [replyingTo, setReplyingTo] = useState<Post | null>(null);
|
const [replyingTo, setReplyingTo] = useState<Post | null>(null);
|
||||||
const [feedType, setFeedType] = useState<'latest' | 'curated'>('latest');
|
const [feedType, setFeedType] = useState<'latest' | 'curated'>('latest');
|
||||||
const [isNsfwNode, setIsNsfwNode] = useState(false);
|
const [isNsfwNode, setIsNsfwNode] = useState(false);
|
||||||
@@ -37,6 +39,8 @@ export default function Home() {
|
|||||||
selfBoost: number;
|
selfBoost: number;
|
||||||
};
|
};
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Fetch node info to check if NSFW
|
// Fetch node info to check if NSFW
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -51,26 +55,61 @@ export default function Home() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadFeed = async (type: 'latest' | 'curated') => {
|
const loadFeed = async (type: 'latest' | 'curated', cursor?: string | null) => {
|
||||||
setLoading(true);
|
if (cursor) {
|
||||||
|
setLoadingMore(true);
|
||||||
|
} else {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const endpoint = type === 'curated' ? '/api/posts?type=curated' : '/api/posts?type=home';
|
const endpoint = type === 'curated'
|
||||||
|
? `/api/posts?type=curated${cursor ? `&cursor=${cursor}` : ''}`
|
||||||
|
: `/api/posts?type=home${cursor ? `&cursor=${cursor}` : ''}`;
|
||||||
const res = await fetch(endpoint);
|
const res = await fetch(endpoint);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setPosts(data.posts || []);
|
|
||||||
|
if (cursor) {
|
||||||
|
setPosts(prev => [...prev, ...(data.posts || [])]);
|
||||||
|
} else {
|
||||||
|
setPosts(data.posts || []);
|
||||||
|
}
|
||||||
setFeedMeta(data.meta || null);
|
setFeedMeta(data.meta || null);
|
||||||
|
setNextCursor(data.nextCursor || null);
|
||||||
} catch {
|
} catch {
|
||||||
setPosts([]);
|
if (!cursor) {
|
||||||
|
setPosts([]);
|
||||||
|
}
|
||||||
setFeedMeta(null);
|
setFeedMeta(null);
|
||||||
|
setNextCursor(null);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setLoadingMore(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setPosts([]);
|
||||||
|
setNextCursor(null);
|
||||||
loadFeed(feedType);
|
loadFeed(feedType);
|
||||||
}, [feedType]);
|
}, [feedType]);
|
||||||
|
|
||||||
|
// Infinite scroll observer
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadMoreRef.current || !nextCursor || loadingMore) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0].isIntersecting && nextCursor && !loadingMore) {
|
||||||
|
loadFeed(feedType, nextCursor);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(loadMoreRef.current);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [nextCursor, loadingMore, feedType]);
|
||||||
|
|
||||||
const handlePost = async (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string, isNsfw?: boolean) => {
|
const handlePost = async (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string, isNsfw?: boolean) => {
|
||||||
// Check if we're replying to a swarm post
|
// Check if we're replying to a swarm post
|
||||||
let swarmReplyTo: { postId: string; nodeDomain: string } | undefined;
|
let swarmReplyTo: { postId: string; nodeDomain: string } | undefined;
|
||||||
@@ -94,6 +133,8 @@ export default function Home() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (feedType === 'curated') {
|
if (feedType === 'curated') {
|
||||||
|
setPosts([]);
|
||||||
|
setNextCursor(null);
|
||||||
loadFeed('curated');
|
loadFeed('curated');
|
||||||
} else {
|
} else {
|
||||||
setPosts([{ ...data.post, author: user }, ...posts]);
|
setPosts([{ ...data.post, author: user }, ...posts]);
|
||||||
@@ -199,19 +240,27 @@ export default function Home() {
|
|||||||
<p style={{ fontSize: '13px', marginTop: '8px' }}>Be the first to post something!</p>
|
<p style={{ fontSize: '13px', marginTop: '8px' }}>Be the first to post something!</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
posts.map(post => (
|
<>
|
||||||
<PostCard
|
{posts.map(post => (
|
||||||
key={post.id}
|
<PostCard
|
||||||
post={post}
|
key={post.id}
|
||||||
onLike={handleLike}
|
post={post}
|
||||||
onRepost={handleRepost}
|
onLike={handleLike}
|
||||||
onDelete={handleDelete}
|
onRepost={handleRepost}
|
||||||
onComment={(p) => {
|
onDelete={handleDelete}
|
||||||
setReplyingTo(p);
|
onComment={(p) => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
setReplyingTo(p);
|
||||||
}}
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
/>
|
}}
|
||||||
))
|
/>
|
||||||
|
))}
|
||||||
|
{/* Infinite scroll trigger */}
|
||||||
|
<div ref={loadMoreRef} style={{ padding: '24px', textAlign: 'center' }}>
|
||||||
|
{loadingMore && (
|
||||||
|
<span style={{ color: 'var(--foreground-tertiary)' }}>Loading more...</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user