@replyke/core
Version:
Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.
96 lines • 4.6 kB
JavaScript
import { useCallback, useEffect, useRef } from "react";
import { useReplykeDispatch, useReplykeSelector } from "../../../store/hooks";
import { selectConversationList, selectConversationListHasMore, selectConversationListLoading, setConversationList, setConversationListLoading, setConversationListHasMore, } from "../../../store/slices/chatSlice";
import useAxiosPrivate from "../../../config/useAxiosPrivate";
import useProject from "../../projects/useProject";
import { handleError } from "../../../utils/handleError";
function useConversations({ types, } = {}) {
const dispatch = useReplykeDispatch();
const { projectId } = useProject();
const axios = useAxiosPrivate();
const conversations = useReplykeSelector(selectConversationList);
const loading = useReplykeSelector(selectConversationListLoading);
const hasMore = useReplykeSelector(selectConversationListHasMore);
const typesKey = types ? [...types].sort().join(",") : "";
// Keep fresh refs to avoid stale closures in useCallback
const conversationsRef = useRef(conversations);
conversationsRef.current = conversations;
// Local two-cursor ref — NOT stored in Redux to avoid schema changes
const cursorRef = useRef({ cursor: null, cursorCreatedAt: null });
const fetchPage = useCallback(async (cursors, isRefresh) => {
if (!projectId)
return;
dispatch(setConversationListLoading(true));
try {
const params = { limit: "20" };
if (types && types.length > 0)
params.types = types.join(",");
if (cursors?.cursor)
params.cursor = cursors.cursor;
if (cursors?.cursorCreatedAt)
params.cursorCreatedAt = cursors.cursorCreatedAt;
const response = await axios.get(`/${projectId}/chat/conversations`, {
params,
});
const { conversations: items, hasMore: more } = response.data;
// Derive the next cursor from the last item in the response
if (items.length > 0) {
const last = items[items.length - 1];
cursorRef.current = {
cursor: last.lastMessageAt ? new Date(last.lastMessageAt).toISOString() : null,
cursorCreatedAt: new Date(last.createdAt).toISOString(),
};
}
else if (isRefresh) {
cursorRef.current = { cursor: null, cursorCreatedAt: null };
}
if (isRefresh) {
dispatch(setConversationList(items));
}
else {
// Append to existing list, deduplicating by id
const current = conversationsRef.current;
dispatch(setConversationList(current.concat(items.filter((item) => !current.some((c) => c.id === item.id)))));
}
dispatch(setConversationListHasMore(more));
}
catch (err) {
handleError(err, "Failed to load conversations");
}
finally {
dispatch(setConversationListLoading(false));
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[projectId, typesKey]);
// Initial fetch on mount (or when types/projectId changes)
useEffect(() => {
if (!projectId)
return;
cursorRef.current = { cursor: null, cursorCreatedAt: null };
fetchPage(null, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId, typesKey]);
const loadMore = useCallback(async () => {
if (loading || !hasMore)
return;
await fetchPage(cursorRef.current, false);
}, [loading, hasMore, fetchPage]);
const refresh = useCallback(async () => {
cursorRef.current = { cursor: null, cursorCreatedAt: null };
await fetchPage(null, true);
}, [fetchPage]);
const createGroup = useCallback(async (params) => {
if (!projectId)
throw new Error("No project ID");
const response = await axios.post(`/${projectId}/chat/conversations`, { type: "group", ...params });
const conversation = response.data;
// Prepend to the current list so it appears immediately
const current = conversationsRef.current;
dispatch(setConversationList([conversation, ...current]));
return conversation;
}, [projectId, axios, dispatch]);
return { conversations, loading, hasMore, loadMore, refresh, createGroup };
}
export default useConversations;
//# sourceMappingURL=useConversations.js.map