diff --git a/.env.local.example b/.env.local.example deleted file mode 100644 index df30d56f..00000000 --- a/.env.local.example +++ /dev/null @@ -1,7 +0,0 @@ -NEXT_PUBLIC_THEME=nounish -NEXT_PUBLIC_HIVE_COMMUNITY_TAG=hive-167980 -NEXT_PUBLIC_HIVE_SEARCH_TAG=hive-167980 -NEXT_PUBLIC_HIVE_USER=skatedev -HIVE_POSTING_KEY=posting_private_key_here_ - -# available themes -> bluesky, hacker, forest \ No newline at end of file diff --git a/.gitignore b/.gitignore index fd3dbb57..00bba9bb 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ yarn-error.log* # local env files .env*.local +.env # vercel .vercel diff --git a/components/blog/PostPage.tsx b/components/blog/PostPage.tsx index 5d0db9fe..63aafe2c 100644 --- a/components/blog/PostPage.tsx +++ b/components/blog/PostPage.tsx @@ -1,52 +1,55 @@ // app/page.tsx -'use client'; +"use client"; -import { Box, Container, Flex, Spinner } from '@chakra-ui/react'; -import TweetList from '../homepage/TweetList'; -import TweetComposer from '../homepage/TweetComposer'; -import { useEffect, useState } from 'react'; -import { Comment, Discussion } from '@hiveio/dhive'; // Ensure this import is consistent -import Conversation from '../homepage/Conversation'; -import TweetReplyModal from '../homepage/TweetReplyModal'; -import { getPost } from '@/lib/hive/client-functions'; -import PostDetails from '@/components/blog/PostDetails'; -import { useComments } from '@/hooks/useComments'; +import { Box, Container, Flex, Spinner } from "@chakra-ui/react"; +import TweetList from "../homepage/TweetList"; +import TweetComposer from "../homepage/TweetComposer"; +import { useEffect, useState } from "react"; +import { Comment, Discussion } from "@hiveio/dhive"; +import Conversation from "../homepage/Conversation"; +import TweetReplyModal from "../homepage/TweetReplyModal"; +import { getPost } from "@/lib/hive/client-functions"; +import PostDetails from "@/components/blog/PostDetails"; +import { useComments } from "@/hooks/useComments"; interface PostPageProps { - author: string - permlink: string + author: string; + permlink: string; } export default function PostPage({ author, permlink }: PostPageProps) { - const [isLoading, setIsLoading] = useState(false); const [post, setPost] = useState(null); const [error, setError] = useState(null); const [conversation, setConversation] = useState(); const [reply, setReply] = useState(); const [isOpen, setIsOpen] = useState(false); - const [newComment, setNewComment] = useState(null); // Define the state + const [newComment, setNewComment] = useState(null); const data = useComments(author, permlink, true); const commentsData = { - ...data, - loadNextPage: () => {}, - hasMore: false, + ...data, + loadNextPage: () => {}, + hasMore: false, }; useEffect(() => { async function loadPost() { setIsLoading(true); + setIsOpen(false); + setReply(undefined); + setConversation(undefined); + setNewComment(null); + try { const post = await getPost(author, permlink); setPost(post); } catch (err) { - setError('Failed to load post'); + setError("Failed to load post"); } finally { setIsLoading(false); } } - loadPost(); }, [author, permlink]); @@ -54,44 +57,82 @@ export default function PostPage({ author, permlink }: PostPageProps) { const onClose = () => setIsOpen(false); const handleNewComment = (newComment: Partial | CharacterData) => { - setNewComment(newComment as Comment); // Type assertion + setNewComment(newComment as Comment); }; - if (isLoading || (!post || !author || !permlink)) { + if (isLoading || !post || !author || !permlink) { return ( - + ); } + console.log("RENDER:", { + isOpen, + reply: !!reply, + conversation: !!conversation, + }); + return ( - + {!conversation ? ( <> - {}} /> + {/* Composer fixo: só se modal fechado */} + {!isOpen && ( + + )} + + {/*da forma como está aqui, mostra o frame para responder como Reply e apenas 1 vez. O erro de mostrar 2 vezes + no blog foi corrigido*/} ) : ( - + )} - {isOpen && } + + {/* Modal: só se isOpen e reply existirem */} + {isOpen && reply && ( + + )} ); } diff --git a/components/homepage/TweetList.tsx b/components/homepage/TweetList.tsx index 272c0bbc..74839b60 100644 --- a/components/homepage/TweetList.tsx +++ b/components/homepage/TweetList.tsx @@ -1,35 +1,35 @@ -import React from 'react'; -import InfiniteScroll from 'react-infinite-scroll-component'; -import { Box, Spinner, VStack, Text } from '@chakra-ui/react'; -import Tweet from './Tweet'; -import { ExtendedComment, useComments } from '@/hooks/useComments'; -import { useSnaps } from '@/hooks/useSnaps'; -import TweetComposer from './TweetComposer'; +// components/homepage/TweetList.tsx +import React from "react"; +import InfiniteScroll from "react-infinite-scroll-component"; +import { Box, Spinner, VStack, Text } from "@chakra-ui/react"; +import Tweet from "./Tweet"; +import { ExtendedComment } from "@/hooks/useComments"; +import TweetComposer from "./TweetComposer"; + +interface InfiniteScrollData { + comments: ExtendedComment[]; + loadNextPage: () => void; + isLoading: boolean; + hasMore: boolean; +} interface TweetListProps { - author: string - permlink: string + author: string; + permlink: string; setConversation: (conversation: ExtendedComment) => void; onOpen: () => void; setReply: (reply: ExtendedComment) => void; newComment: ExtendedComment | null; post?: boolean; - data: InfiniteScrollData + data: InfiniteScrollData; + showComposer?: boolean; // NOVA PROP } -interface InfiniteScrollData { - comments: ExtendedComment[]; - loadNextPage: () => void; // Default can be an empty function in usage - isLoading: boolean; - hasMore: boolean; // Default can be `false` in usage -} - -function handleNewComment() { +function handleNewComment() {} -} - -export default function TweetList( -{ +//showComposer = true significa que o composer será exibido no feed (homepage) +//sendo assim esse frame me permite postar um snap +export default function TweetList({ author, permlink, setConversation, @@ -38,51 +38,58 @@ export default function TweetList( newComment, post = false, data, + showComposer = true, // padrão: mostrar }: TweetListProps) { + const { comments, loadNextPage, isLoading, hasMore } = data; - const { comments, loadNextPage, isLoading, hasMore } = data - + // Ordena por data (mais recente primeiro) comments.sort((a: ExtendedComment, b: ExtendedComment) => { return new Date(b.created).getTime() - new Date(a.created).getTime(); }); - // Handle new comment addition - //const updatedComments = newComment ? [newComment, ...comments] : comments; if (isLoading && comments.length === 0) { - // Initial loading state return ( - Loading posts... + Loading Snaps... ); } return ( - - - - )} - scrollableTarget="scrollableDiv" - > - - null} /> - {comments.map((comment: ExtendedComment) => ( - - ))} - - + + + + } + scrollableTarget="scrollableDiv" + > + + {/* CONDICIONAL: só mostra no feed (home) */} + {showComposer && ( + null} + post={false} // "POST" no feed + /> + )} + {comments.map((comment: ExtendedComment) => ( + + ))} + + ); } diff --git a/components/homepage/TweetReplyModal.tsx b/components/homepage/TweetReplyModal.tsx index 7e4bafbc..9a21bac7 100644 --- a/components/homepage/TweetReplyModal.tsx +++ b/components/homepage/TweetReplyModal.tsx @@ -1,58 +1,88 @@ -import { Modal, ModalBody, ModalContent, ModalHeader, ModalOverlay, HStack, Avatar, Link, IconButton, Box, Text } from '@chakra-ui/react'; -import React from 'react'; -import TweetComposer from './TweetComposer'; -import { Comment } from '@hiveio/dhive'; -import { CloseIcon } from '@chakra-ui/icons'; -import markdownRenderer from '@/lib/utils/MarkdownRenderer'; -import { getPostDate } from '@/lib/utils/GetPostDate'; +//TweetReplyModal +import { + Modal, + ModalBody, + ModalContent, + ModalHeader, + ModalOverlay, + HStack, + Avatar, + Link, + IconButton, + Box, + Text, +} from "@chakra-ui/react"; +import React from "react"; +import TweetComposer from "./TweetComposer"; +import { Comment } from "@hiveio/dhive"; +import { CloseIcon } from "@chakra-ui/icons"; +import markdownRenderer from "@/lib/utils/MarkdownRenderer"; +import { getPostDate } from "@/lib/utils/GetPostDate"; interface TweetReplyModalProps { - isOpen: boolean; - onClose: () => void; - comment?: Comment; - onNewReply: (newComment: Partial) => void; + isOpen: boolean; + onClose: () => void; + comment?: Comment; + onNewReply: (newComment: Partial) => void; } -export default function TweetReplyModal({ isOpen, onClose, comment, onNewReply }: TweetReplyModalProps) { +export default function TweetReplyModal({ + isOpen, + onClose, + comment, + onNewReply, +}: TweetReplyModalProps) { + if (!comment) { + return
; + } - if (!comment) { - return
; - } + const commentDate = getPostDate(comment.created); - const commentDate = getPostDate(comment.created) - - return ( - - - - } - onClick={onClose} - position="absolute" - top={2} - right={2} - variant="unstyled" - size="lg" - /> - - - - - - @{comment.author} - - - {commentDate} - - - - - - - - - - - ); + return ( + + + + } + onClick={onClose} + position="absolute" + top={2} + right={2} + variant="unstyled" + size="lg" + /> + + + + + + @{comment.author} + + + {commentDate} + + + + + + + + + + + ); } diff --git a/hooks/useSnaps.ts b/hooks/useSnaps.ts index 40973274..016e4f19 100644 --- a/hooks/useSnaps.ts +++ b/hooks/useSnaps.ts @@ -1,5 +1,6 @@ +//useSnaps.ts import HiveClient from '@/lib/hive/hiveclient'; -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { ExtendedComment } from './useComments'; interface lastContainerInfo { @@ -20,7 +21,7 @@ export const useSnaps = () => { // Filter comments by the target tag - function filterCommentsByTag(comments: ExtendedComment[], targetTag: string): ExtendedComment[] { + const filterCommentsByTag = useCallback((comments: ExtendedComment[], targetTag: string): ExtendedComment[] => { return comments.filter((commentItem) => { try { if (!commentItem.json_metadata) { @@ -34,20 +35,24 @@ export const useSnaps = () => { return false; // Exclude comments with invalid JSON } }); - } + }, []); + + // Cache for storing filtered comments + const commentsCache = useRef>(new Map()); + // Fetch comments with a minimum size - async function getMoreSnaps(): Promise { + const getMoreSnaps = useCallback(async (): Promise => { const tag = process.env.NEXT_PUBLIC_HIVE_COMMUNITY_TAG || '' const author = "peak.snaps"; - const limit = 3; + const limit = 10; // Increased batch size const allFilteredComments: ExtendedComment[] = []; - let hasMoreData = true; // To track if there are more containers to fetch + let hasMoreData = true; let permlink = lastContainerRef.current?.permlink || ""; let date = lastContainerRef.current?.date || new Date().toISOString(); while (allFilteredComments.length < pageMinSize && hasMoreData) { - + // Get discussions in larger batches const result = await HiveClient.database.call('get_discussions_by_author_before_date', [ author, permlink, @@ -60,21 +65,52 @@ export const useSnaps = () => { break; } - for (const resultItem of result) { + // Prepare parallel requests for content replies + const replyPromises = result.map(async (resultItem: any) => { + // Check cache first + const cacheKey = `${author}-${resultItem.permlink}`; + if (commentsCache.current.has(cacheKey)) { + return { + comments: commentsCache.current.get(cacheKey)!, + permlink: resultItem.permlink, + date: resultItem.created + }; + } + + // If not in cache, fetch from blockchain const comments = (await HiveClient.database.call("get_content_replies", [ author, resultItem.permlink, ])) as ExtendedComment[]; const filteredComments = filterCommentsByTag(comments, tag); - allFilteredComments.push(...filteredComments); - - // Add permlink to the fetched set - fetchedPermlinksRef.current.add(resultItem.permlink); + + // Store in cache + commentsCache.current.set(cacheKey, filteredComments); + + return { + comments: filteredComments, + permlink: resultItem.permlink, + date: resultItem.created + }; + }); + + // Execute all requests in parallel + const replies = await Promise.all(replyPromises); + + // Process results + for (const reply of replies) { + if (!fetchedPermlinksRef.current.has(reply.permlink)) { + allFilteredComments.push(...reply.comments); + fetchedPermlinksRef.current.add(reply.permlink); + permlink = reply.permlink; + date = reply.date; + } + } - // Update the last container info for the next fetch - permlink = resultItem.permlink; - date = resultItem.created; + // Break early if we have enough comments + if (allFilteredComments.length >= pageMinSize * 2) { + break; } } @@ -82,7 +118,7 @@ export const useSnaps = () => { lastContainerRef.current = { permlink, date }; return allFilteredComments; - } + }, [filterCommentsByTag]); // Fetch posts when `currentPage` changes useEffect(() => { @@ -109,7 +145,7 @@ export const useSnaps = () => { }; fetchPosts(); - }, [currentPage]); + }, [currentPage, getMoreSnaps]); // Load the next page const loadNextPage = () => {