From 49b2a6cbfad573a927281f31d944495bf369558f Mon Sep 17 00:00:00 2001 From: Hue Date: Tue, 17 Jun 2025 15:26:07 +0200 Subject: [PATCH 1/6] postgres indexing/auth schema selection --- .../migrations/20250617132119_optimise_db.sql | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 supabase/migrations/20250617132119_optimise_db.sql diff --git a/supabase/migrations/20250617132119_optimise_db.sql b/supabase/migrations/20250617132119_optimise_db.sql new file mode 100644 index 0000000..883f58a --- /dev/null +++ b/supabase/migrations/20250617132119_optimise_db.sql @@ -0,0 +1,74 @@ +drop policy "request co-authoring" on "public"."pendingAuthors"; + +drop policy "allow deletes for post authors" on "public"."postsCategories"; + +drop policy "allow insert for authors" on "public"."postsCategories"; + +CREATE INDEX idx_authors_post ON public.authors USING btree (post); + +CREATE INDEX idx_authors_profile ON public.authors USING btree (profile); + +CREATE INDEX idx_features_featuree ON public.features USING btree (featuree); + +CREATE INDEX idx_features_featurer ON public.features USING btree (featurer); + +CREATE INDEX idx_follows_followee ON public.follows USING btree (followee); + +CREATE INDEX idx_follows_follower ON public.follows USING btree (follower); + +CREATE INDEX idx_likes_post ON public.likes USING btree (post); + +CREATE INDEX idx_likes_profile ON public.likes USING btree (profile); + +CREATE INDEX "idx_pendingAuthors_from_profile" ON public."pendingAuthors" USING btree (from_profile); + +CREATE INDEX "idx_pendingAuthors_post" ON public."pendingAuthors" USING btree (post); + +CREATE INDEX "idx_pendingAuthors_to_profile" ON public."pendingAuthors" USING btree (to_profile); + +CREATE INDEX "idx_postsCategories_category" ON public."postsCategories" USING btree (category); + +CREATE INDEX "idx_postsCategories_post" ON public."postsCategories" USING btree (post); + +CREATE INDEX idx_posts_parent_post ON public.posts USING btree (parent_post); + +CREATE INDEX idx_posts_rt_of ON public.posts USING btree (rt_of); + +CREATE INDEX "idx_profilesCategories_category" ON public."profilesCategories" USING btree (category); + +CREATE INDEX "idx_profilesCategories_profile" ON public."profilesCategories" USING btree (profile); + +CREATE INDEX idx_profiles_id ON public.profiles USING btree (id); + +create policy "request co-authoring" +on "public"."pendingAuthors" +as permissive +for insert +to public +with check (((( SELECT auth.uid() AS uid) = from_profile) AND (EXISTS ( SELECT 1 + FROM authors + WHERE (authors.profile = ( SELECT auth.uid() AS uid)))))); + + +create policy "allow deletes for post authors" +on "public"."postsCategories" +as permissive +for delete +to public +using ((EXISTS ( SELECT 1 + FROM authors + WHERE ((authors.post = "postsCategories".post) AND (authors.profile = ( SELECT auth.uid() AS uid)))))); + + +create policy "allow insert for authors" +on "public"."postsCategories" +as permissive +for insert +to public +with check ((EXISTS ( SELECT 1 + FROM authors + WHERE ((authors.post = "postsCategories".post) AND (authors.profile = ( SELECT auth.uid() AS uid)))))); + + + + From 471bd8be792658dc374a538bb7f8c6e04d9753db Mon Sep 17 00:00:00 2001 From: Hue Date: Wed, 18 Jun 2025 11:36:11 +0200 Subject: [PATCH 2/6] added likes and rt count to standard post info --- src/contexts/supabase/supabase.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/contexts/supabase/supabase.ts b/src/contexts/supabase/supabase.ts index f854c85..bba5850 100644 --- a/src/contexts/supabase/supabase.ts +++ b/src/contexts/supabase/supabase.ts @@ -17,6 +17,8 @@ interface stdPostInfo { post: Tables<"posts">; categories: Tables<"categories">[]; profiles: Tables<"profiles">[]; + likesCount: number; + rtCount: number; } interface PostSearchQuery { @@ -864,7 +866,9 @@ const queries = { ), authors:authors( profiles:profiles(*) - ) + ), + likes:likes(count), + rt_of:posts!rt_of(count) `, ) .eq("id", id) @@ -880,6 +884,8 @@ const queries = { post: req.data as Tables<"posts">, categories, profiles, + likesCount: req.data.likes[0]?.count ?? 0, + rtCount: req.data.rt_of?.count ?? 0, }; }, }, From 9ae04e2e74b5c6cd2625b9348e08a0e82b5dba3e Mon Sep 17 00:00:00 2001 From: AFCMS Date: Wed, 18 Jun 2025 13:54:25 +0200 Subject: [PATCH 3/6] Fix ViewPost bottom margin --- src/pages/ViewPost/ViewPost.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ViewPost/ViewPost.tsx b/src/pages/ViewPost/ViewPost.tsx index 4ad03bc..19acec0 100644 --- a/src/pages/ViewPost/ViewPost.tsx +++ b/src/pages/ViewPost/ViewPost.tsx @@ -70,7 +70,7 @@ export default function ViewPost() { return (
-
+
Date: Wed, 18 Jun 2025 13:57:54 +0200 Subject: [PATCH 4/6] STUFF --- src/Components/AuthorAsk/AuthorAsk.tsx | 14 +- src/Components/PostAdd/PostAdd.tsx | 9 +- src/Components/PostViewer/PostViewer.tsx | 166 ++++++------------ .../QuoteTweetModal/QuoteTweetModal.tsx | 6 +- src/pages/Home/Home.tsx | 116 +++++------- 5 files changed, 111 insertions(+), 200 deletions(-) diff --git a/src/Components/AuthorAsk/AuthorAsk.tsx b/src/Components/AuthorAsk/AuthorAsk.tsx index 706b295..ec7f717 100644 --- a/src/Components/AuthorAsk/AuthorAsk.tsx +++ b/src/Components/AuthorAsk/AuthorAsk.tsx @@ -130,10 +130,10 @@ export default function AuthorAsk({ post }: AuthorAskProps) { setLoadingParentAuthor(true); try { - // Get authors of the parent post - const parentAuthors = await queries.authors.ofPost(post.parent_post); - if (parentAuthors.length > 0) { - setParentPostAuthor(parentAuthors[0]); // Take the first author + // Get authors of the parent post using standardPostInfo + const parentPostInfo = await queries.views.standardPostInfo(post.parent_post); + if (parentPostInfo.profiles.length > 0) { + setParentPostAuthor(parentPostInfo.profiles[0]); // Take the first author } else { setParentPostAuthor(null); } @@ -152,11 +152,11 @@ export default function AuthorAsk({ post }: AuthorAskProps) { useEffect(() => { async function fetchAuthors() { try { - const postAuthors = await queries.authors.ofPost(post.id); - setAuthors(postAuthors); + const postInfo = await queries.views.standardPostInfo(post.id); + setAuthors(postInfo.profiles); // Check if the user is the last author - if (auth.user && postAuthors.length === 1 && postAuthors[0]?.id === auth.user.id) { + if (auth.user && postInfo.profiles.length === 1 && postInfo.profiles[0]?.id === auth.user.id) { setIsLastAuthor(true); } else { setIsLastAuthor(false); diff --git a/src/Components/PostAdd/PostAdd.tsx b/src/Components/PostAdd/PostAdd.tsx index 0cd08ad..93697c8 100644 --- a/src/Components/PostAdd/PostAdd.tsx +++ b/src/Components/PostAdd/PostAdd.tsx @@ -75,14 +75,13 @@ export default function PostAdd({ try { setIsLoadingPost(true); - // Charger le post - const post = await queries.posts.get(editPostId); - setBody(post.body ?? ""); + // Load post info including authors + const postInfo = await queries.views.standardPostInfo(editPostId); + setBody(postInfo.post.body ?? ""); // Check that the user is the author of the post - const authors = await queries.authors.ofPost(editPostId); const currentUserId = auth.user.id; - const isAuthor = authors.some((author) => author.id === currentUserId); + const isAuthor = postInfo.profiles.some((author) => author.id === currentUserId); if (!isAuthor) { setError("You are not allowed to edit this post."); diff --git a/src/Components/PostViewer/PostViewer.tsx b/src/Components/PostViewer/PostViewer.tsx index bbf13e1..bd0f6c3 100644 --- a/src/Components/PostViewer/PostViewer.tsx +++ b/src/Components/PostViewer/PostViewer.tsx @@ -70,58 +70,65 @@ export default function PostViewer(props: PostViewerProps) { const mainAuthor = authors.length > 0 ? authors[0] : null; const isAuthor = auth.user && authors.some((author) => author.id === auth.user?.id); const isSimpleRetweet = queries.posts.isSimpleRetweet(props.post); - // Fetch post authors + + // Fetch post info (authors, categories, likes, retweets) in one request useEffect(() => { - async function fetchAuthors() { + async function fetchPostInfo() { try { - // Si c'est un retweet simple, on récupère les auteurs du post original + // Si c'est un retweet simple, on récupère les infos du post original if (queries.posts.isSimpleRetweet(props.post)) { const originalPost = await queries.posts.getOriginalPost(props.post); if (originalPost) { - const originalAuthors = await queries.authors.ofPost(originalPost.id); - setAuthors(originalAuthors); + // Set the original post state for use in other effects + setOriginalPost(originalPost); + + const originalPostInfo = await queries.views.standardPostInfo(originalPost.id); + setAuthors(originalPostInfo.profiles); + setCategories(originalPostInfo.categories); + setLikeCount(originalPostInfo.likesCount); + setRetweetCount(originalPostInfo.rtCount); + + // Récupérer l'auteur du retweet + const retweetAuthors = await queries.authors.ofPost(props.post.id); + setRetweetedBy(retweetAuthors[0] || null); // Pour un retweet simple, l'utilisateur n'est jamais le dernier auteur du post original setIsLastAuthor(false); return; + } else { + setOriginalPost(null); + setRetweetedBy(null); } + } else { + setOriginalPost(null); + setRetweetedBy(null); } // Pour les posts normaux ou les quote retweets - const postAuthors = await queries.authors.ofPost(props.post.id); - setAuthors(postAuthors); + const postInfo = await queries.views.standardPostInfo(props.post.id); + setAuthors(postInfo.profiles); + setCategories(postInfo.categories); + setLikeCount(postInfo.likesCount); + setRetweetCount(postInfo.rtCount); // Check if the user is the last author - if (auth.user && postAuthors.length === 1 && postAuthors[0]?.id === auth.user.id) { + if (auth.user && postInfo.profiles.length === 1 && postInfo.profiles[0]?.id === auth.user.id) { setIsLastAuthor(true); } else { setIsLastAuthor(false); } } catch { setAuthors([]); + setCategories([]); + setLikeCount(0); + setRetweetCount(0); setIsLastAuthor(false); + setOriginalPost(null); + setRetweetedBy(null); } } - void fetchAuthors(); + void fetchPostInfo(); }, [props.post.id, props.post, auth.user]); - // Fetch categories - useEffect(() => { - async function fetchCategories() { - try { - // Si c'est un retweet simple, récupérer les catégories du post original - let postIdToUse = props.post.id; - if (queries.posts.isSimpleRetweet(props.post) && originalPost) { - postIdToUse = originalPost.id; - } - - const postCategories = await queries.postsCategories.get(postIdToUse); - setCategories(postCategories); - } catch { - setCategories([]); - } - } - void fetchCategories(); - }, [props.post.id, props.post, originalPost]); // Fetch child posts with likes sorting useEffect(() => { @@ -129,14 +136,14 @@ export default function PostViewer(props: PostViewerProps) { try { const childPosts = await queries.posts.getChildren(props.post.id); - // For each child, get the number of likes + // For each child, get the number of likes using standardPostInfo const childrenWithLikes = await Promise.all( childPosts.map(async (child) => { try { - const likes = await queries.like.byWho(child.id); + const childInfo = await queries.views.standardPostInfo(child.id); return { ...child, - likeCount: likes.length, + likeCount: childInfo.likesCount, }; } catch { return { @@ -223,52 +230,11 @@ export default function PostViewer(props: PostViewerProps) { void fetchMediaUrls(); }, [props.post.id, props.post, originalPost]); - // Fetch likes + // Check if user liked the post and if user has retweeted useEffect(() => { - async function fetchLikes() { - try { - // Si c'est un retweet simple, on récupère les likes du post original - let targetPostId = props.post.id; - if (queries.posts.isSimpleRetweet(props.post) && originalPost) { - targetPostId = originalPost.id; - } - - const likedByUsers = await queries.like.byWho(targetPostId); - setLikeCount(likedByUsers.length); - - if (auth.user) { - const userLikesPost = await queries.like.doesUserLikePost(auth.user.id, targetPostId); - setIsLiked(userLikesPost); - } else { - setIsLiked(false); - } - } catch { - setLikeCount(0); - setIsLiked(false); - } - } - void fetchLikes(); - }, [props.post.id, props.post, auth.user, originalPost]); // Récupération des retweets - useEffect(() => { - async function fetchRetweets() { - try { - // Pour les retweets simples, compter les retweets du post original - let targetPostId = props.post.id; - if (queries.posts.isSimpleRetweet(props.post) && originalPost) { - targetPostId = originalPost.id; - } - - const retweets = await queries.posts.getRetweetsOf(targetPostId); - setRetweetCount(retweets.length); - } catch { - setRetweetCount(0); - } - } - void fetchRetweets(); - }, [props.post.id, props.post, originalPost]); // Vérifier si l'utilisateur a déjà retweeté ce post - useEffect(() => { - async function checkUserRetweet() { + async function checkUserActions() { if (!auth.user) { + setIsLiked(false); setHasRetweeted(false); return; } @@ -280,39 +246,23 @@ export default function PostViewer(props: PostViewerProps) { targetPostId = originalPost.id; } - const hasUserRetweeted = await queries.posts.hasUserRetweeted(targetPostId, auth.user.id); + const [userLikesPost, hasUserRetweeted] = await Promise.all([ + queries.like.doesUserLikePost(auth.user.id, targetPostId), + queries.posts.hasUserRetweeted(targetPostId, auth.user.id), + ]); + + setIsLiked(userLikesPost); setHasRetweeted(hasUserRetweeted); - } catch { + } catch (error) { + console.error("[ERROR] Error checking user actions:", error); + setIsLiked(false); setHasRetweeted(false); } } - void checkUserRetweet(); + void checkUserActions(); }, [props.post.id, props.post, auth.user, originalPost]); - // Récupérer le post original si c'est un retweet simple - useEffect(() => { - async function fetchOriginalPost() { - if (queries.posts.isSimpleRetweet(props.post)) { - try { - const original = await queries.posts.getOriginalPost(props.post); - setOriginalPost(original); - - // Récupérer l'auteur du retweet - const retweetAuthors = await queries.authors.ofPost(props.post.id); - setRetweetedBy(retweetAuthors[0] || null); - } catch { - setOriginalPost(null); - setRetweetedBy(null); - } - } else { - setOriginalPost(null); - setRetweetedBy(null); - } - } - - void fetchOriginalPost(); - }, [props.post]); // Récupérer le post cité si c'est un quote tweet useEffect(() => { async function fetchQuotedPost() { @@ -322,12 +272,10 @@ export default function PostViewer(props: PostViewerProps) { setQuotedPost(quoted); if (quoted) { - const quotedAuthors = await queries.authors.ofPost(quoted.id); - setQuotedPostAuthors(quotedAuthors); - - // Récupérer les catégories du post cité - const quotedCategories = await queries.postsCategories.get(quoted.id); - setQuotedPostCategories(quotedCategories); + // Use standardPostInfo for quoted post data + const quotedPostInfo = await queries.views.standardPostInfo(quoted.id); + setQuotedPostAuthors(quotedPostInfo.profiles); + setQuotedPostCategories(quotedPostInfo.categories); // Récupérer les médias du post cité try { @@ -436,7 +384,7 @@ export default function PostViewer(props: PostViewerProps) { await queries.like.remove(targetPostId); // Check after removal - const afterRemove = await queries.like.doesUserLikePost(auth.user.id, props.post.id); + const afterRemove = await queries.like.doesUserLikePost(auth.user.id, targetPostId); if (!afterRemove) { setLikeCount((prev) => prev - 1); @@ -502,8 +450,8 @@ export default function PostViewer(props: PostViewerProps) { setTimeout(() => { void (async () => { try { - const postAuthors = await queries.authors.ofPost(props.post.id); - setAuthors(postAuthors); + const postInfo = await queries.views.standardPostInfo(props.post.id); + setAuthors(postInfo.profiles); // Refresh the page to ensure everything is up to date setTimeout(() => { window.location.reload(); diff --git a/src/Components/QuoteTweetModal/QuoteTweetModal.tsx b/src/Components/QuoteTweetModal/QuoteTweetModal.tsx index 01a49fe..9a3d9b0 100644 --- a/src/Components/QuoteTweetModal/QuoteTweetModal.tsx +++ b/src/Components/QuoteTweetModal/QuoteTweetModal.tsx @@ -24,12 +24,12 @@ export default function QuoteTweetModal({ post, originalPost, modalId, onSuccess setLoading(true); try { - const [authorsResult, { data: mediaFiles }] = await Promise.all([ - queries.authors.ofPost(postToQuote.id), + const [postInfoResult, { data: mediaFiles }] = await Promise.all([ + queries.views.standardPostInfo(postToQuote.id), supabase.storage.from("post-media").list(postToQuote.id, { limit: 1 }), ]); - setAuthor(authorsResult[0] || null); + setAuthor(postInfoResult.profiles[0] || null); if (mediaFiles?.length) { const url = supabase.storage.from("post-media").getPublicUrl(`${postToQuote.id}/${mediaFiles[0].name}`) diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 768e2e3..23e4c5a 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -31,48 +31,29 @@ export default function Home() { setLoading(true); setError(null); - if (!auth.isAuthenticated) { - // For non-authenticated users, show a general feed - const feedPosts = await queries.feed.posts.get({ - sort_by: "created_at", - sort_order: "desc", - paging_limit: PAGE_SIZE, - paging_offset: 0, - }); - - // Filter out replies - const mainPosts = feedPosts.filter((post) => post.parent_post === null); - - setPosts(mainPosts); - currentOffset.current = PAGE_SIZE; - setHasMore(mainPosts.length === PAGE_SIZE); - } else { - // Generate personalized feed parameters for authenticated users - // const feedParams = await queries.feed.posts.generateParams(); // Temporarily disabled - - // Get initial posts, excluding replies (parent_post = null) - const feedPosts = await queries.feed.posts.get({ - // ...feedParams, // Temporarily disabled - using general feed for now - sort_by: "created_at", - sort_order: "desc", - paging_limit: PAGE_SIZE, - paging_offset: 0, - }); + // Get initial posts, excluding replies by filtering at the database level + // We'll make the query more specific rather than filtering client-side + const feedPosts = await queries.feed.posts.get({ + sort_by: "created_at", + sort_order: "desc", + paging_limit: PAGE_SIZE, + paging_offset: 0, + }); - // Filter out replies - const mainPosts = feedPosts.filter((post) => post.parent_post === null); + // Filter out replies (parent_post !== null) - temporarily keep this filter + // until we can modify the database function to exclude replies directly + const mainPosts = feedPosts.filter((post) => post.parent_post === null); - setPosts(mainPosts); - currentOffset.current = PAGE_SIZE; - setHasMore(mainPosts.length === PAGE_SIZE); - } + setPosts(mainPosts); + currentOffset.current = feedPosts.length; // Use actual count of fetched posts + setHasMore(feedPosts.length === PAGE_SIZE); // Check if we got full page from DB } catch (err) { console.error("Error loading feed:", err); setError(err instanceof Error ? err.message : "Failed to load feed"); } finally { setLoading(false); } - }, [auth.isAuthenticated]); + }, []); // Load more posts for infinite scroll const loadMorePosts = useCallback(async () => { @@ -81,55 +62,38 @@ export default function Home() { try { setLoadingMore(true); - if (!auth.isAuthenticated) { - // For non-authenticated users, show a general feed - const morePosts = await queries.feed.posts.get({ - sort_by: "created_at", - sort_order: "desc", - paging_limit: PAGE_SIZE, - paging_offset: currentOffset.current, - }); - - // Filter out replies - const newMainPosts = morePosts.filter((post) => post.parent_post === null); + // Get more posts with the current offset + const morePosts = await queries.feed.posts.get({ + sort_by: "created_at", + sort_order: "desc", + paging_limit: PAGE_SIZE, + paging_offset: currentOffset.current, + }); - if (newMainPosts.length > 0) { - setPosts((prev) => [...prev, ...newMainPosts]); - currentOffset.current += PAGE_SIZE; - setHasMore(newMainPosts.length === PAGE_SIZE); - } else { - setHasMore(false); - } + // Filter out replies + const newMainPosts = morePosts.filter((post) => post.parent_post === null); + + if (newMainPosts.length > 0) { + setPosts((prev) => [...prev, ...newMainPosts]); + // Only increment offset by the number of total posts fetched, not filtered posts + currentOffset.current += morePosts.length; + // Continue if we got a full page from the database + setHasMore(morePosts.length === PAGE_SIZE); + } else if (morePosts.length > 0 && morePosts.length === PAGE_SIZE) { + // If we got posts but none were main posts (all were replies), + // and we got a full page, try to get more posts + currentOffset.current += morePosts.length; + setHasMore(true); + // The intersection observer will trigger another load } else { - // Generate personalized feed parameters for authenticated users - // const feedParams = await queries.feed.posts.generateParams(); // Temporarily disabled - - // Get more posts - const morePosts = await queries.feed.posts.get({ - // ...feedParams, // Temporarily disabled - using general feed for now - sort_by: "created_at", - sort_order: "desc", - paging_limit: PAGE_SIZE, - paging_offset: currentOffset.current, - }); - - // Filter out replies - const newMainPosts = morePosts.filter((post) => post.parent_post === null); - - if (newMainPosts.length > 0) { - setPosts((prev) => [...prev, ...newMainPosts]); - currentOffset.current += PAGE_SIZE; - setHasMore(newMainPosts.length === PAGE_SIZE); - } else { - setHasMore(false); - } + setHasMore(false); } } catch (err) { console.error("Error loading more posts:", err); } finally { setLoadingMore(false); } - }, [auth.isAuthenticated, loadingMore, hasMore]); + }, [loadingMore, hasMore]); // Intersection Observer for infinite scroll useEffect(() => { @@ -187,7 +151,7 @@ export default function Home() { }, [location.state]); return ( -
+
{/* Message de notification */} {showMessage && ( From 9eb9d4bbe69874a7cd4247b46b04ce946e63049c Mon Sep 17 00:00:00 2001 From: AFCMS Date: Wed, 18 Jun 2025 14:24:56 +0200 Subject: [PATCH 5/6] Fix infinite scroll --- src/pages/Home/Home.tsx | 79 +++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 23e4c5a..4b4fed2 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -23,16 +23,26 @@ export default function Home() { const location = useLocation(); const observerRef = useRef(null); const currentOffset = useRef(0); + const loadingMoreRef = useRef(false); + const hasMoreRef = useRef(true); const PAGE_SIZE = 10; + // Keep refs in sync with state + useEffect(() => { + loadingMoreRef.current = loadingMore; + }, [loadingMore]); + + useEffect(() => { + hasMoreRef.current = hasMore; + }, [hasMore]); + // Load initial feed const loadInitialFeed = useCallback(async () => { try { setLoading(true); setError(null); - // Get initial posts, excluding replies by filtering at the database level - // We'll make the query more specific rather than filtering client-side + // Get initial posts const feedPosts = await queries.feed.posts.get({ sort_by: "created_at", sort_order: "desc", @@ -40,8 +50,7 @@ export default function Home() { paging_offset: 0, }); - // Filter out replies (parent_post !== null) - temporarily keep this filter - // until we can modify the database function to exclude replies directly + // Filter out replies (parent_post !== null) - client-side filtering for now const mainPosts = feedPosts.filter((post) => post.parent_post === null); setPosts(mainPosts); @@ -57,10 +66,24 @@ export default function Home() { // Load more posts for infinite scroll const loadMorePosts = useCallback(async () => { - if (loadingMore || !hasMore) return; + // Use refs to check current state values to avoid stale closures + const currentLoadingMore = loadingMoreRef.current; + const currentHasMore = hasMoreRef.current; + + console.log('[INFINITE SCROLL] loadMorePosts called:', { + loadingMore: currentLoadingMore, + hasMore: currentHasMore, + currentOffset: currentOffset.current + }); + + if (currentLoadingMore || !currentHasMore) { + console.log('[INFINITE SCROLL] Skipping load - loadingMore:', currentLoadingMore, 'hasMore:', currentHasMore); + return; + } try { setLoadingMore(true); + console.log('[INFINITE SCROLL] Fetching posts with offset:', currentOffset.current); // Get more posts with the current offset const morePosts = await queries.feed.posts.get({ @@ -70,36 +93,69 @@ export default function Home() { paging_offset: currentOffset.current, }); + console.log('[INFINITE SCROLL] Received posts:', { + totalPosts: morePosts.length, + postIds: morePosts.map(p => p.id) + }); + // Filter out replies const newMainPosts = morePosts.filter((post) => post.parent_post === null); + + console.log('[INFINITE SCROLL] After filtering:', { + mainPosts: newMainPosts.length, + mainPostIds: newMainPosts.map(p => p.id) + }); if (newMainPosts.length > 0) { - setPosts((prev) => [...prev, ...newMainPosts]); + setPosts((prev) => { + const newPosts = [...prev, ...newMainPosts]; + console.log('[INFINITE SCROLL] Updated posts count:', newPosts.length); + return newPosts; + }); // Only increment offset by the number of total posts fetched, not filtered posts currentOffset.current += morePosts.length; // Continue if we got a full page from the database setHasMore(morePosts.length === PAGE_SIZE); + console.log('[INFINITE SCROLL] New offset:', currentOffset.current, 'hasMore:', morePosts.length === PAGE_SIZE); } else if (morePosts.length > 0 && morePosts.length === PAGE_SIZE) { // If we got posts but none were main posts (all were replies), // and we got a full page, try to get more posts currentOffset.current += morePosts.length; setHasMore(true); - // The intersection observer will trigger another load + console.log('[INFINITE SCROLL] Page full of replies, continuing with offset:', currentOffset.current); + // Recursively load more if we only got replies + setTimeout(() => { + void loadMorePosts(); + }, 100); } else { setHasMore(false); + console.log('[INFINITE SCROLL] End of feed reached'); } } catch (err) { console.error("Error loading more posts:", err); } finally { setLoadingMore(false); } - }, [loadingMore, hasMore]); + }, []); // Empty dependency array to prevent recreation // Intersection Observer for infinite scroll useEffect(() => { const observer = new IntersectionObserver( (entries) => { - if (entries[0]?.isIntersecting && hasMore && !loadingMore) { + const isIntersecting = entries[0]?.isIntersecting; + const currentHasMore = hasMoreRef.current; + const currentLoadingMore = loadingMoreRef.current; + + console.log('[INFINITE SCROLL] Observer triggered:', { + isIntersecting, + hasMore: currentHasMore, + loadingMore: currentLoadingMore, + currentOffset: currentOffset.current, + postsCount: posts.length + }); + + if (isIntersecting && currentHasMore && !currentLoadingMore) { + console.log('[INFINITE SCROLL] Loading more posts...'); void loadMorePosts(); } }, @@ -107,13 +163,16 @@ export default function Home() { ); if (observerRef.current) { + console.log('[INFINITE SCROLL] Observing element:', observerRef.current); observer.observe(observerRef.current); + } else { + console.log('[INFINITE SCROLL] No observer target found'); } return () => { observer.disconnect(); }; - }, [loadMorePosts, hasMore, loadingMore]); + }, [loadMorePosts, posts.length]); // Remove hasMore and loadingMore from dependencies // Load initial feed when auth changes useEffect(() => { From 34a2332f5691276185849c15b20bd3b9e11a8b6c Mon Sep 17 00:00:00 2001 From: AFCMS Date: Wed, 18 Jun 2025 14:34:14 +0200 Subject: [PATCH 6/6] Cleanup logs --- src/pages/Home/Home.tsx | 39 ++------------------------------------- 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 4b4fed2..c737253 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -57,7 +57,6 @@ export default function Home() { currentOffset.current = feedPosts.length; // Use actual count of fetched posts setHasMore(feedPosts.length === PAGE_SIZE); // Check if we got full page from DB } catch (err) { - console.error("Error loading feed:", err); setError(err instanceof Error ? err.message : "Failed to load feed"); } finally { setLoading(false); @@ -70,20 +69,12 @@ export default function Home() { const currentLoadingMore = loadingMoreRef.current; const currentHasMore = hasMoreRef.current; - console.log('[INFINITE SCROLL] loadMorePosts called:', { - loadingMore: currentLoadingMore, - hasMore: currentHasMore, - currentOffset: currentOffset.current - }); - if (currentLoadingMore || !currentHasMore) { - console.log('[INFINITE SCROLL] Skipping load - loadingMore:', currentLoadingMore, 'hasMore:', currentHasMore); return; } try { setLoadingMore(true); - console.log('[INFINITE SCROLL] Fetching posts with offset:', currentOffset.current); // Get more posts with the current offset const morePosts = await queries.feed.posts.get({ @@ -93,46 +84,32 @@ export default function Home() { paging_offset: currentOffset.current, }); - console.log('[INFINITE SCROLL] Received posts:', { - totalPosts: morePosts.length, - postIds: morePosts.map(p => p.id) - }); - // Filter out replies const newMainPosts = morePosts.filter((post) => post.parent_post === null); - - console.log('[INFINITE SCROLL] After filtering:', { - mainPosts: newMainPosts.length, - mainPostIds: newMainPosts.map(p => p.id) - }); if (newMainPosts.length > 0) { setPosts((prev) => { const newPosts = [...prev, ...newMainPosts]; - console.log('[INFINITE SCROLL] Updated posts count:', newPosts.length); return newPosts; }); // Only increment offset by the number of total posts fetched, not filtered posts currentOffset.current += morePosts.length; // Continue if we got a full page from the database setHasMore(morePosts.length === PAGE_SIZE); - console.log('[INFINITE SCROLL] New offset:', currentOffset.current, 'hasMore:', morePosts.length === PAGE_SIZE); } else if (morePosts.length > 0 && morePosts.length === PAGE_SIZE) { // If we got posts but none were main posts (all were replies), // and we got a full page, try to get more posts currentOffset.current += morePosts.length; setHasMore(true); - console.log('[INFINITE SCROLL] Page full of replies, continuing with offset:', currentOffset.current); // Recursively load more if we only got replies setTimeout(() => { void loadMorePosts(); }, 100); } else { setHasMore(false); - console.log('[INFINITE SCROLL] End of feed reached'); } - } catch (err) { - console.error("Error loading more posts:", err); + } catch { + // Error handling without logging } finally { setLoadingMore(false); } @@ -146,16 +123,7 @@ export default function Home() { const currentHasMore = hasMoreRef.current; const currentLoadingMore = loadingMoreRef.current; - console.log('[INFINITE SCROLL] Observer triggered:', { - isIntersecting, - hasMore: currentHasMore, - loadingMore: currentLoadingMore, - currentOffset: currentOffset.current, - postsCount: posts.length - }); - if (isIntersecting && currentHasMore && !currentLoadingMore) { - console.log('[INFINITE SCROLL] Loading more posts...'); void loadMorePosts(); } }, @@ -163,10 +131,7 @@ export default function Home() { ); if (observerRef.current) { - console.log('[INFINITE SCROLL] Observing element:', observerRef.current); observer.observe(observerRef.current); - } else { - console.log('[INFINITE SCROLL] No observer target found'); } return () => {