UNPKG

@replyke/core

Version:

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

160 lines 8.19 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { createContext, useCallback, useContext, useEffect, useRef, } from "react"; import { useChatContext } from "./chat-context"; import { useReplykeDispatch, useReplykeSelector } from "../store/hooks"; import { selectNewestMessageId } from "../store/slices/chatSlice"; import useConversationData from "../hooks/chat/useConversationData"; import useMarkConversationAsRead from "../hooks/chat/useMarkConversationAsRead"; import useAxiosPrivate from "../config/useAxiosPrivate"; import useProject from "../hooks/projects/useProject"; import { upsertMessage } from "../store/slices/chatSlice"; import { handleError } from "../utils/handleError"; export const ConversationContext = createContext({}); export function useConversationContext() { return useContext(ConversationContext); } export const ConversationProvider = ({ conversationId, onDeleted, children, }) => { const dispatch = useReplykeDispatch(); const { projectId } = useProject(); const axios = useAxiosPrivate(); const { socket, registerActiveConversation, unregisterActiveConversation } = useChatContext(); // Read the newest message id from Redux for reconnect catch-up const newestMessageId = useReplykeSelector(selectNewestMessageId(conversationId)); const newestMessageIdRef = useRef(newestMessageId); useEffect(() => { newestMessageIdRef.current = newestMessageId; }, [newestMessageId]); const mark = useMarkConversationAsRead({ conversationId }); const catchUpMessages = useCallback(async (afterTimestamp) => { if (!projectId || !conversationId) return; try { const response = await axios.get(`/${projectId}/v7/chat/conversations/${conversationId}/messages`, { params: { after: afterTimestamp, limit: 100, sort: "asc" } }); const { messages } = response.data; messages.forEach((msg) => dispatch(upsertMessage(msg))); } catch (err) { handleError(err, "Failed to fetch missed messages"); } }, [projectId, conversationId, axios, dispatch]); // Keep a ref to the messages state so socket handlers can find latest messages const messagesRef = useRef([]); const reduxMessages = useReplykeSelector((state) => state.replyke.chat.messages[conversationId]?.items ?? []); useEffect(() => { messagesRef.current = reduxMessages; }, [reduxMessages]); // ── Conversation data (messages, send, members, etc.) ───────────────────── // Called before the socket effects so upsertMember / removeMemberLocally are // available as stable callbacks in the member event handlers below. const data = useConversationData({ conversationId }); // ── Room join / leave ────────────────────────────────────────────────────── useEffect(() => { if (!socket || !conversationId) return; const room = conversationId; // Register this conversation as active (suppresses unread increments in ChatProvider) registerActiveConversation(room); // Join the Socket.io room socket.emit("join:conversation", { conversationId }); // Mark as read on mount (catches up any unread before the view was opened) const newestId = newestMessageIdRef.current; if (newestId) { mark({ messageId: newestId }); } return () => { socket.emit("leave:conversation", { conversationId }); unregisterActiveConversation(room); }; }, [ socket, conversationId, registerActiveConversation, unregisterActiveConversation, mark, ]); // ── Reconnect handler ────────────────────────────────────────────────────── // On reconnects (not the initial connect), re-join the room and catch up on // messages that arrived during the disconnection window. useEffect(() => { if (!socket || !conversationId) return; // Track whether the initial connect has been observed so we can distinguish // it from subsequent reconnects (socket.on("connect") fires on both). const hasConnectedOnceRef = { current: socket.connected }; const handleConnect = async () => { if (!hasConnectedOnceRef.current) { // Initial connect — the mount effect already joined the room. hasConnectedOnceRef.current = true; return; } // True reconnect: re-join the room and catch up on missed messages. socket.emit("join:conversation", { conversationId }); const newest = newestMessageIdRef.current; if (newest) { const newestMsg = messagesRef.current.find((m) => m.id === newest); if (newestMsg) { await catchUpMessages(new Date(newestMsg.createdAt).toISOString()); } } }; socket.on("connect", handleConnect); return () => { socket.off("connect", handleConnect); }; }, [socket, conversationId, catchUpMessages]); // ── conversation:deleted ─────────────────────────────────────────────────── useEffect(() => { if (!socket || !conversationId) return; const handleDeleted = ({ conversationId: deletedId, }) => { if (deletedId !== conversationId) return; socket.emit("leave:conversation", { conversationId }); unregisterActiveConversation(conversationId); onDeleted?.(); }; socket.on("conversation:deleted", handleDeleted); return () => { socket.off("conversation:deleted", handleDeleted); }; }, [socket, conversationId, unregisterActiveConversation, onDeleted]); // ── Member join / leave ──────────────────────────────────────────────────── // Update local members state when the server broadcasts member changes. useEffect(() => { if (!socket || !conversationId) return; const handleMemberJoined = (payload) => { if (payload.conversationId !== conversationId) return; data.upsertMember(payload.member); }; const handleMemberLeft = (payload) => { if (payload.conversationId !== conversationId) return; data.removeMemberLocally({ userId: payload.userId }); }; socket.on("member:joined", handleMemberJoined); socket.on("member:left", handleMemberLeft); return () => { socket.off("member:joined", handleMemberJoined); socket.off("member:left", handleMemberLeft); }; }, [socket, conversationId, data.upsertMember, data.removeMemberLocally]); // ── Mark read on new messages ────────────────────────────────────────────── // While this conversation is mounted, each incoming message is immediately marked read. useEffect(() => { if (!socket || !conversationId) return; const handleMessage = (message) => { if (message.conversationId !== conversationId) return; mark({ messageId: message.id }); }; socket.on("message:created", handleMessage); return () => { socket.off("message:created", handleMessage); }; }, [socket, conversationId, mark]); return (_jsx(ConversationContext.Provider, { value: { ...data, conversationId }, children: children })); }; //# sourceMappingURL=conversation-context.js.map