UNPKG

@replyke/core

Version:

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

168 lines 8.94 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConversationProvider = exports.ConversationContext = void 0; exports.useConversationContext = useConversationContext; const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("react"); const chat_context_1 = require("./chat-context"); const hooks_1 = require("../store/hooks"); const chatSlice_1 = require("../store/slices/chatSlice"); const useConversationData_1 = __importDefault(require("../hooks/chat/useConversationData")); const useMarkConversationAsRead_1 = __importDefault(require("../hooks/chat/useMarkConversationAsRead")); const useAxiosPrivate_1 = __importDefault(require("../config/useAxiosPrivate")); const useProject_1 = __importDefault(require("../hooks/projects/useProject")); const chatSlice_2 = require("../store/slices/chatSlice"); const handleError_1 = require("../utils/handleError"); exports.ConversationContext = (0, react_1.createContext)({}); function useConversationContext() { return (0, react_1.useContext)(exports.ConversationContext); } const ConversationProvider = ({ conversationId, onDeleted, children, }) => { const dispatch = (0, hooks_1.useReplykeDispatch)(); const { projectId } = (0, useProject_1.default)(); const axios = (0, useAxiosPrivate_1.default)(); const { socket, registerActiveConversation, unregisterActiveConversation } = (0, chat_context_1.useChatContext)(); // Read the newest message id from Redux for reconnect catch-up const newestMessageId = (0, hooks_1.useReplykeSelector)((0, chatSlice_1.selectNewestMessageId)(conversationId)); const newestMessageIdRef = (0, react_1.useRef)(newestMessageId); (0, react_1.useEffect)(() => { newestMessageIdRef.current = newestMessageId; }, [newestMessageId]); const mark = (0, useMarkConversationAsRead_1.default)({ conversationId }); const catchUpMessages = (0, react_1.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((0, chatSlice_2.upsertMessage)(msg))); } catch (err) { (0, handleError_1.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 = (0, react_1.useRef)([]); const reduxMessages = (0, hooks_1.useReplykeSelector)((state) => state.replyke.chat.messages[conversationId]?.items ?? []); (0, react_1.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 = (0, useConversationData_1.default)({ conversationId }); // ── Room join / leave ────────────────────────────────────────────────────── (0, react_1.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. (0, react_1.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 ─────────────────────────────────────────────────── (0, react_1.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. (0, react_1.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. (0, react_1.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 ((0, jsx_runtime_1.jsx)(exports.ConversationContext.Provider, { value: { ...data, conversationId }, children: children })); }; exports.ConversationProvider = ConversationProvider; //# sourceMappingURL=conversation-context.js.map