UNPKG

@replyke/core

Version:

Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.

139 lines 6.69 kB
import { useCallback, useEffect, useRef } from "react"; import { useReplykeDispatch, useReplykeSelector } from "../../../store/hooks"; import { selectMessages, selectMessagesLoading, selectMessagesHasMore, selectThreadReplies, selectThreadLoading, selectThreadHasMore, upsertMessage, setMessagesLoading, setMessagesHasMore, setThreadReplies, setThreadLoading, } from "../../../store/slices/chatSlice"; import useAxiosPrivate from "../../../config/useAxiosPrivate"; import useProject from "../../projects/useProject"; import { handleError } from "../../../utils/handleError"; function useChatMessages({ conversationId, parentId, limit = 50, includeFiles, }) { const dispatch = useReplykeDispatch(); const { projectId } = useProject(); const axios = useAxiosPrivate(); const isThread = Boolean(parentId); // Read from the correct Redux bucket const mainMessages = useReplykeSelector(selectMessages(conversationId)); const mainLoading = useReplykeSelector(selectMessagesLoading(conversationId)); const mainHasMore = useReplykeSelector(selectMessagesHasMore(conversationId)); const threadMessages = useReplykeSelector(selectThreadReplies(parentId ?? "")); const threadLoading = useReplykeSelector(selectThreadLoading(parentId ?? "")); const threadHasMore = useReplykeSelector(selectThreadHasMore(parentId ?? "")); const messages = isThread ? threadMessages : mainMessages; const loading = isThread ? threadLoading : mainLoading; const hasMore = isThread ? threadHasMore : mainHasMore; // Keep fresh refs to message arrays so loadOlder can read cursors without // closing over stale state const mainMessagesRef = useRef(mainMessages); mainMessagesRef.current = mainMessages; const threadMessagesRef = useRef(threadMessages); threadMessagesRef.current = threadMessages; // Fetch a page of messages. // `before` is an ISO 8601 timestamp — the server queries messages created // before this point in time (not a UUID cursor). const fetchPage = useCallback(async (before) => { if (!projectId || !conversationId) return; const params = { limit, sort: isThread ? "asc" : "desc", }; if (parentId) params.parentId = parentId; if (before) params.before = before; if (includeFiles) params.include = "files"; try { const response = await axios.get(`/${projectId}/chat/conversations/${conversationId}/messages`, { params }); const { messages: items, hasMore: more } = response.data; if (isThread) { // Thread replies come back ASC — dispatch as-is dispatch(setThreadReplies({ parentMessageId: parentId, messages: items, hasMore: more, })); } else { // Main stream comes back DESC (newest first for cursor efficiency). // Redux stores messages ASC — reverse before dispatching. const ascending = [...items].reverse(); ascending.forEach((msg) => dispatch(upsertMessage(msg))); dispatch(setMessagesHasMore({ conversationId, hasMore: more })); } } catch (err) { handleError(err, "Failed to load messages"); } }, [projectId, conversationId, parentId, isThread, limit, includeFiles, axios, dispatch]); // Initial fetch on mount useEffect(() => { if (!projectId || !conversationId) return; const initialFetch = async () => { if (isThread) { dispatch(setThreadLoading({ parentMessageId: parentId, loading: true })); } else { dispatch(setMessagesLoading({ conversationId, loading: true })); } await fetchPage(null); if (isThread) { dispatch(setThreadLoading({ parentMessageId: parentId, loading: false })); } else { dispatch(setMessagesLoading({ conversationId, loading: false })); } }; initialFetch(); // Only re-run when the conversation/thread identity changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectId, conversationId, parentId]); // Load more messages: // - Main stream: fetch older messages using `before` cursor (oldest loaded createdAt) // - Thread: fetch newer replies using `after` cursor (newest loaded createdAt), append const loadOlder = useCallback(async () => { if (loading || !hasMore) return; if (isThread) { const currentItems = threadMessagesRef.current; const newest = currentItems[currentItems.length - 1]; if (!newest || !parentId || !projectId || !conversationId) return; const after = new Date(newest.createdAt).toISOString(); dispatch(setThreadLoading({ parentMessageId: parentId, loading: true })); try { const response = await axios.get(`/${projectId}/chat/conversations/${conversationId}/messages`, { params: { parentId, after, limit, sort: "asc", ...(includeFiles ? { include: "files" } : {}), }, }); const { messages: newItems, hasMore: more } = response.data; dispatch(setThreadReplies({ parentMessageId: parentId, messages: [...currentItems, ...newItems], hasMore: more, })); } catch (err) { handleError(err, "Failed to load more thread replies"); } finally { dispatch(setThreadLoading({ parentMessageId: parentId, loading: false })); } return; } const oldest = mainMessagesRef.current[0]; if (!oldest) return; const before = new Date(oldest.createdAt).toISOString(); dispatch(setMessagesLoading({ conversationId, loading: true })); await fetchPage(before); dispatch(setMessagesLoading({ conversationId, loading: false })); }, [loading, hasMore, isThread, parentId, projectId, conversationId, limit, includeFiles, axios, dispatch, fetchPage]); return { messages, loading, hasMore, loadOlder }; } export default useChatMessages; //# sourceMappingURL=useChatMessages.js.map