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/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, }; }, }, diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 768e2e3..c737253 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -23,119 +23,107 @@ 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); - 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 + 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) - client-side filtering for now + 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 () => { - if (loadingMore || !hasMore) return; + // Use refs to check current state values to avoid stale closures + const currentLoadingMore = loadingMoreRef.current; + const currentHasMore = hasMoreRef.current; + + if (currentLoadingMore || !currentHasMore) { + return; + } 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, - }); + // 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, + }); - // Filter out replies - const newMainPosts = morePosts.filter((post) => post.parent_post === null); + // 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); - } - } 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, + if (newMainPosts.length > 0) { + setPosts((prev) => { + const newPosts = [...prev, ...newMainPosts]; + return newPosts; }); - - // 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); - } + // 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); + // Recursively load more if we only got replies + setTimeout(() => { + void loadMorePosts(); + }, 100); + } else { + setHasMore(false); } - } catch (err) { - console.error("Error loading more posts:", err); + } catch { + // Error handling without logging } finally { setLoadingMore(false); } - }, [auth.isAuthenticated, 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; + + if (isIntersecting && currentHasMore && !currentLoadingMore) { void loadMorePosts(); } }, @@ -149,7 +137,7 @@ export default function Home() { return () => { observer.disconnect(); }; - }, [loadMorePosts, hasMore, loadingMore]); + }, [loadMorePosts, posts.length]); // Remove hasMore and loadingMore from dependencies // Load initial feed when auth changes useEffect(() => { @@ -187,7 +175,7 @@ export default function Home() { }, [location.state]); return ( -
+
{/* Message de notification */} {showMessage && ( 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 (
-
+