UNPKG

@cometchat/chat-uikit-react

Version:

Ready-to-use Chat UI Components for React

1,391 lines (1,383 loc) 837 kB
import './index.css'; import { CometChatPopover } from './chunk-GI7Y3RZV.js'; import { CometChatEmojiKeyboardEmptyState, CometChatEmojiKeyboardCategorySection, CometChatEmojiKeyboardEmojiGrid, CometChatEmojiKeyboardSearchBar, CometChatEmojiKeyboardCategoryTabs, CometChatEmojiKeyboardRoot, CometChatSearchBar } from './chunk-3IC2CRN3.js'; import { CometChatDate, CometChatContextMenu, useGlobalConfig, isMessagePendingModeration, isPermissionDeniedError, isMessageModerated, getReceiptStatus, CometChatMessageBubble, CometChatPluginRegistryContext } from './chunk-3WCEOEGP.js'; import { CometChatButton } from './chunk-KOCEARYG.js'; import { CometChatAvatar } from './chunk-LDXTHTWH.js'; import { POLLS_CONSTANTS, STICKERS_CONSTANTS } from './chunk-TNRMDFEG.js'; import { CometChatUIKit, CometChatUIKitCalls, useLoggedInUser } from './chunk-UKFBODV7.js'; import { formatDateWithConfig } from './chunk-PJV2HJD5.js'; import { usePublishEvent, useCometChatEventsContext, CometChatMarkdownFormatter, escapeUserHtml } from './chunk-MI3XOQGY.js'; import { CometChatUIKitConstants } from './chunk-3DZVPW3I.js'; import { CometChatTextFormatter } from './chunk-OZUCO4ZL.js'; import { useTheme } from './chunk-SXG4FP55.js'; import { useCometChatFrameContext } from './chunk-UTFM6ET6.js'; import { useLocale } from './chunk-UVWJYQJ3.js'; import React44, { forwardRef, useState, useRef, useImperativeHandle, useCallback, useEffect, createContext, lazy, useId, useMemo, useSyncExternalStore, Suspense, useContext, useReducer, useLayoutEffect } from 'react'; import { CometChat, CometChatException } from '@cometchat/chat-sdk-javascript'; import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import DOMPurify from 'dompurify'; import { createPortal } from 'react-dom'; // src/utils/CometChatLogger.ts var LogLevel = /* @__PURE__ */ ((LogLevel2) => { LogLevel2[LogLevel2["none"] = 0] = "none"; LogLevel2[LogLevel2["error"] = 1] = "error"; LogLevel2[LogLevel2["warn"] = 2] = "warn"; LogLevel2[LogLevel2["info"] = 3] = "info"; LogLevel2[LogLevel2["debug"] = 4] = "debug"; return LogLevel2; })(LogLevel || {}); var currentLevel = 1 /* error */; function setLogLevel(level) { currentLevel = level; } function getLogLevel() { return currentLevel; } function error(tag, message, ...args) { if (currentLevel >= 1 /* error */) { console.error(`[CometChat:${tag}]`, message, ...args); } } function warn(tag, message, ...args) { if (currentLevel >= 2 /* warn */) { console.warn(`[CometChat:${tag}]`, message, ...args); } } function info(tag, message, ...args) { if (currentLevel >= 3 /* info */) { console.info(`[CometChat:${tag}]`, message, ...args); } } function debug(tag, message, ...args) { if (currentLevel >= 4 /* debug */) { console.debug(`[CometChat:${tag}]`, message, ...args); } } var CometChatLogger = { setLogLevel, getLogLevel, error, warn, info, debug }; // src/formatters/CometChatUrlFormatter.ts var CometChatUrlFormatter = class extends CometChatTextFormatter { constructor() { super(...arguments); this.id = "url-formatter"; this.priority = 100; this.urls = []; } getRegex() { return /(https?:\/\/[^\s<]+)|(www\.[^\s<]+)/gi; } format(text) { if (!text) { this.originalText = ""; this.formattedText = ""; this.urls = []; this.metadata = { urls: [] }; return ""; } this.originalText = text; this.urls = []; const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; const placeholders = []; let protectedText = text.replace(markdownLinkRegex, (match) => { const idx = placeholders.length; placeholders.push(match); return `__COMETCHAT_LINK_${String(idx)}__`; }); const existingLinkRegex = /<a\s[^>]*href="[^"]*"[^>]*>[^<]*<\/a>/gi; protectedText = protectedText.replace(existingLinkRegex, (match) => { const idx = placeholders.length; placeholders.push(match); return `__COMETCHAT_LINK_${String(idx)}__`; }); this.formattedText = protectedText.replace(this.getRegex(), (match) => { const cleaned = match.replace(/[).,;:!?]+$/, ""); const trailing = match.slice(cleaned.length); this.urls.push(cleaned); const href = cleaned.startsWith("www.") ? `https://${cleaned}` : cleaned; return `<a href="${href}" target="_blank" rel="noopener noreferrer" class="cometchat-link">${cleaned}</a>${trailing}`; }); this.formattedText = this.formattedText.replace( /__COMETCHAT_LINK_(\d+)__/g, (_, idx) => placeholders[parseInt(idx, 10)] ?? "" ); this.metadata = { urls: this.urls }; return this.formattedText; } /** Get detected URLs from the last format() call. */ getUrls() { return [...this.urls]; } reset() { super.reset(); this.urls = []; } }; // src/components/CometChatAIAssistantChat/CometChatAIStreamingService.ts var streamStateMap = /* @__PURE__ */ new Map(); var streamListeners = /* @__PURE__ */ new Map(); var DEFAULT_STREAM_STATE = Object.freeze({ text: "", isComplete: false, activeToolCall: null, toolExecutionText: "", isThinking: false, hasContent: false, hasError: false, hasStarted: false, currentRunId: "", streamCards: [] }); function getStreamState(chatId) { return streamStateMap.get(chatId) ?? DEFAULT_STREAM_STATE; } function setStreamState(chatId, updater) { const prev = getStreamState(chatId); const next = updater(prev); streamStateMap.set(chatId, next); notifyListeners(chatId); } function notifyListeners(chatId) { streamListeners.get(chatId)?.forEach((listener) => { try { listener(); } catch { } }); } function subscribeToStreamState(chatId, listener) { if (!streamListeners.has(chatId)) { streamListeners.set(chatId, /* @__PURE__ */ new Set()); } streamListeners.get(chatId).add(listener); return () => { streamListeners.get(chatId)?.delete(listener); if (streamListeners.get(chatId)?.size === 0) { streamListeners.delete(chatId); } }; } var messageQueue = []; var isProcessing = false; var streamSpeedMs = 30; var aiTools = null; var toolCallArgsMap = /* @__PURE__ */ new Map(); var toolCallNameMap = /* @__PURE__ */ new Map(); function setStreamSpeed(ms) { streamSpeedMs = ms; } function getStreamSpeed() { return streamSpeedMs; } function setAIAssistantTools(tools) { aiTools = tools; } function getAIAssistantTools() { return aiTools; } function handleWebsocketMessage(event, chatId) { messageQueue.push({ event, chatId }); if (!isProcessing) { void processQueue(); } } async function processQueue() { if (isProcessing) return; isProcessing = true; while (messageQueue.length > 0) { const item = messageQueue.shift(); if (!item) break; const { event, chatId } = item; const type = event.getType(); let delayMs = 0; if (type === CometChatUIKitConstants.streamMessageTypes.text_message_content) { delayMs = streamSpeedMs; } else if (type === CometChatUIKitConstants.streamMessageTypes.tool_call_args) { delayMs = 0; } if (delayMs > 0) { await new Promise((resolve) => setTimeout(resolve, delayMs)); } processEvent(event, chatId); } isProcessing = false; } function processEvent(event, chatId) { const type = event.getType(); const runId = event.getMessageId(); const { streamMessageTypes } = CometChatUIKitConstants; switch (type) { case streamMessageTypes.run_started: { const eventRunId = runId || String(Date.now()); setStreamState(chatId, () => ({ text: "", isComplete: false, activeToolCall: null, toolExecutionText: "", isThinking: true, hasContent: false, hasError: false, hasStarted: true, currentRunId: eventRunId, streamCards: [] })); break; } case streamMessageTypes.text_message_start: { setStreamState(chatId, (prev) => ({ ...prev, isThinking: false })); break; } case streamMessageTypes.text_message_content: { const contentEvent = event; const delta = typeof contentEvent.getDelta === "function" ? contentEvent.getDelta() : ""; setStreamState(chatId, (prev) => ({ ...prev, text: prev.text + delta, hasContent: true, isThinking: false })); break; } case streamMessageTypes.text_message_end: { break; } case streamMessageTypes.tool_call_start: { const toolEvent = event; const toolName = typeof toolEvent.getToolCallName === "function" ? toolEvent.getToolCallName() : ""; toolCallNameMap.set(runId, toolName); toolCallArgsMap.set(runId, ""); const eventData = event.getData?.(); const executionText = eventData?.executionText ?? ""; setStreamState(chatId, (prev) => ({ ...prev, activeToolCall: toolName, toolExecutionText: executionText, isThinking: false })); break; } case streamMessageTypes.tool_call_args: { const argEvent = event; const delta = typeof argEvent.getDelta === "function" ? argEvent.getDelta() : ""; const prev = toolCallArgsMap.get(runId) ?? ""; toolCallArgsMap.set(runId, prev + delta); break; } case streamMessageTypes.tool_call_end: { const toolName = toolCallNameMap.get(runId); const rawArgs = toolCallArgsMap.get(runId) ?? ""; if (toolName && aiTools) { const handler = aiTools.getAction(toolName); if (handler) { try { handler(JSON.parse(rawArgs)); } catch { } } } setStreamState(chatId, (prev) => ({ ...prev, activeToolCall: null, toolExecutionText: "" })); toolCallNameMap.delete(runId); toolCallArgsMap.delete(runId); break; } case streamMessageTypes.card_start: { const cardEvent = event; const cardId = typeof cardEvent.getCardId === "function" ? cardEvent.getCardId() : ""; const executionText = typeof cardEvent.getExecutionText === "function" ? cardEvent.getExecutionText() : ""; setStreamState(chatId, (prev) => { if (prev.streamCards.some((c) => c.cardId === cardId)) return prev; return { ...prev, isThinking: false, streamCards: [...prev.streamCards, { cardId, executionText, card: null }] }; }); break; } case streamMessageTypes.card: { const cardEvent = event; const cardId = typeof cardEvent.getCardId === "function" ? cardEvent.getCardId() : ""; const card = typeof cardEvent.getCard === "function" ? cardEvent.getCard() : null; setStreamState(chatId, (prev) => { const exists = prev.streamCards.some((c) => c.cardId === cardId); const streamCards = exists ? prev.streamCards.map((c) => c.cardId === cardId ? { ...c, card } : c) : [...prev.streamCards, { cardId, executionText: "", card }]; return { ...prev, isThinking: false, streamCards }; }); break; } case streamMessageTypes.card_end: { break; } case streamMessageTypes.run_finished: { setStreamState(chatId, (prev) => ({ ...prev, isComplete: true, isThinking: false, activeToolCall: null, toolExecutionText: "" })); break; } } } function startStreamingMessage(chatId, runId) { streamStateMap.set(chatId, { text: "", isComplete: false, activeToolCall: null, toolExecutionText: "", isThinking: true, hasContent: false, hasError: false, hasStarted: true, currentRunId: runId ? String(runId) : String(Date.now()), streamCards: [] }); notifyListeners(chatId); } function stopStreamingMessage(chatId) { streamStateMap.delete(chatId); toolCallArgsMap.clear(); toolCallNameMap.clear(); messageQueue = messageQueue.filter((item) => item.chatId !== chatId); notifyListeners(chatId); } function setStreamError(chatId) { setStreamState(chatId, (prev) => ({ ...prev, hasError: true, isThinking: false, activeToolCall: null })); } function isStreaming(chatId) { const state = streamStateMap.get(chatId); return state !== void 0 && state.hasStarted && !state.isComplete; } var SHIMMER_COUNT = 8; function isDifferentDay(a, b) { if (!a || !b) return false; const dateA = new Date(a * 1e3); const dateB = new Date(b * 1e3); return dateA.getDate() !== dateB.getDate() || dateA.getMonth() !== dateB.getMonth() || dateA.getFullYear() !== dateB.getFullYear(); } var CometChatAIAssistantChatHistory = forwardRef( ({ user, group, hideNewChat = false, loadLastAgentConversation = false, onMessageClick, onNewChatClick, onClose, onEmpty, onError, emptyStateView, errorStateView, className }, ref) => { const { getLocalizedString, calendarObject, timezone, dateLocaleLanguage } = useLocale(); const [messages, setMessages] = useState([]); const [state, setState] = useState("loading"); const [focusedIndex, setFocusedIndex] = useState(-1); const requestRef = useRef(null); const isFetchingRef = useRef(false); const messageCountRef = useRef(0); const listRef = useRef(null); const itemRefs = useRef([]); const hasFetchedRef = useRef(false); const onMessageClickRef = useRef(onMessageClick); onMessageClickRef.current = onMessageClick; const onEmptyRef = useRef(onEmpty); onEmptyRef.current = onEmpty; const onErrorRef = useRef(onError); onErrorRef.current = onError; useImperativeHandle( ref, () => ({ addMessage: (message) => { setMessages((prev) => { const updated = [message, ...prev]; messageCountRef.current = updated.length; return updated; }); setState("loaded"); } }), [] ); const buildRequest = useCallback(() => { const builder = new CometChat.MessagesRequestBuilder().hideReplies(true).setLimit(30).setType(CometChatUIKitConstants.MessageTypes.text).setCategory(CometChatUIKitConstants.MessageCategory.message).hideDeletedMessages(true); if (user) builder.setUID(user.getUid()); else if (group) builder.setGUID(group.getGuid()); return builder.build(); }, [user, group]); const fetchMessages = useCallback(async (shouldLoadLast = false) => { if (isFetchingRef.current || !requestRef.current) return; isFetchingRef.current = true; try { const fetched = await requestRef.current.fetchPrevious(); const textMessages = []; for (const m of fetched) { if (m instanceof CometChat.TextMessage) { textMessages.push(m); } } if (textMessages.length > 0) { const reversed = [...textMessages].reverse(); setMessages((prev) => { const updated = [...prev, ...reversed]; messageCountRef.current = updated.length; return updated; }); setState("loaded"); if (shouldLoadLast && reversed.length > 0) { const first = reversed[0]; if (first) onMessageClickRef.current?.(first); } } else { if (messageCountRef.current === 0) { setState("empty"); onEmptyRef.current?.(); } else { setState("loaded"); } } } catch (err) { if (messageCountRef.current === 0) { setState("error"); } onErrorRef.current?.(err); } finally { isFetchingRef.current = false; } }, []); useEffect(() => { hasFetchedRef.current = false; }, [user, group]); useEffect(() => { if (hasFetchedRef.current) return; hasFetchedRef.current = true; requestRef.current = buildRequest(); messageCountRef.current = 0; setMessages([]); setState("loading"); void fetchMessages(loadLastAgentConversation); }, [user, group, buildRequest, fetchMessages, loadLastAgentConversation]); const handleScroll = useCallback( (e) => { const el = e.currentTarget; const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50; if (isAtBottom && state === "loaded" && !isFetchingRef.current) { void fetchMessages(); } }, [state, fetchMessages] ); const handleDeleteMessage = useCallback( (message, e) => { e.stopPropagation(); CometChat.deleteMessage(String(message.getId())).then(() => { setMessages((prev) => prev.filter((m) => m.getId() !== message.getId())); messageCountRef.current -= 1; if (messageCountRef.current === 0) { setState("empty"); onEmpty?.(); } onNewChatClick?.(message); }).catch((err) => onError?.(err)); }, [onEmpty, onNewChatClick, onError] ); const handleKeyDown = useCallback( (e, index) => { switch (e.key) { case "ArrowDown": { e.preventDefault(); const next = Math.min(index + 1, messages.length - 1); setFocusedIndex(next); itemRefs.current[next]?.focus(); break; } case "ArrowUp": { e.preventDefault(); const prev = Math.max(index - 1, 0); setFocusedIndex(prev); itemRefs.current[prev]?.focus(); break; } case "Enter": case " ": { e.preventDefault(); const msg = messages[index]; if (msg) onMessageClick?.(msg); break; } case "Escape": { e.preventDefault(); onClose?.(); break; } } }, [messages, onMessageClick, onClose] ); const rootClasses = ["cometchat-ai-assistant-chat-history", className].filter(Boolean).join(" "); return /* @__PURE__ */ jsxs("div", { className: rootClasses, children: [ /* @__PURE__ */ jsxs("div", { className: "cometchat-ai-assistant-chat-history__header", children: [ /* @__PURE__ */ jsx("span", { className: "cometchat-ai-assistant-chat-history__title", children: getLocalizedString("ai_assistant_chat_history_title") }), /* @__PURE__ */ jsx( "button", { type: "button", className: "cometchat-ai-assistant-chat-history__close-btn", "aria-label": getLocalizedString("ai_assistant_chat_history_close"), onClick: onClose, children: /* @__PURE__ */ jsx( "span", { className: "cometchat-ai-assistant-chat-history__close-icon", "aria-hidden": "true" } ) } ) ] }), !hideNewChat && /* @__PURE__ */ jsxs( "button", { type: "button", className: "cometchat-ai-assistant-chat-history__new-chat-row", "aria-label": getLocalizedString("ai_assistant_chat_new_chat"), onClick: () => onNewChatClick?.(), children: [ /* @__PURE__ */ jsx( "span", { className: "cometchat-ai-assistant-chat-history__new-chat-icon", "aria-hidden": "true" } ), /* @__PURE__ */ jsx("span", { className: "cometchat-ai-assistant-chat-history__new-chat-label", children: getLocalizedString("ai_assistant_chat_new_chat") }) ] } ), state === "loading" && messages.length === 0 && /* @__PURE__ */ jsx( "ul", { className: "cometchat-ai-assistant-chat-history__shimmer-list", "aria-busy": "true", "aria-label": getLocalizedString("ai_assistant_chat_history_loading"), children: Array.from({ length: SHIMMER_COUNT }).map((_, i) => /* @__PURE__ */ jsxs( "li", { className: "cometchat-ai-assistant-chat-history__shimmer-item", "aria-hidden": "true", children: [ /* @__PURE__ */ jsx( "div", { className: [ "cometchat-ai-assistant-chat-history__shimmer-line", "cometchat-ai-assistant-chat-history__shimmer-line--title" ].join(" ") } ), /* @__PURE__ */ jsx( "div", { className: [ "cometchat-ai-assistant-chat-history__shimmer-line", "cometchat-ai-assistant-chat-history__shimmer-line--subtitle" ].join(" ") } ) ] }, i )) } ), state === "error" && /* @__PURE__ */ jsx("div", { className: "cometchat-ai-assistant-chat-history__error-state", role: "alert", children: errorStateView ?? /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("p", { className: "cometchat-ai-assistant-chat-history__error-title", children: "Something went wrong" }), /* @__PURE__ */ jsx("p", { className: "cometchat-ai-assistant-chat-history__error-subtitle", children: "Unable to load chat history. Please try again." }) ] }) }), state === "empty" && /* @__PURE__ */ jsx("div", { className: "cometchat-ai-assistant-chat-history__empty-state", children: emptyStateView ?? /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("p", { className: "cometchat-ai-assistant-chat-history__empty-title", children: "No conversations yet" }), /* @__PURE__ */ jsx("p", { className: "cometchat-ai-assistant-chat-history__empty-subtitle", children: "Start a new chat to begin" }) ] }) }), (state === "loaded" || state === "loading" && messages.length > 0) && /* @__PURE__ */ jsx( "ul", { ref: listRef, className: "cometchat-ai-assistant-chat-history__list", role: "listbox", "aria-label": getLocalizedString("ai_assistant_chat_history_title"), onScroll: handleScroll, children: messages.map((message, index) => { const prevMessage = messages[index - 1]; const showSeparator = isDifferentDay(prevMessage?.getSentAt(), message.getSentAt()); return /* @__PURE__ */ jsxs(React44.Fragment, { children: [ showSeparator && /* @__PURE__ */ jsx( "li", { className: "cometchat-ai-assistant-chat-history__date-separator", role: "presentation", "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", { className: "cometchat-ai-assistant-chat-history__date-separator-text", children: formatDateWithConfig( message.getSentAt(), { today: getLocalizedString("date_today"), yesterday: getLocalizedString("date_yesterday"), lastWeek: void 0, otherDays: "DD MMM, YYYY", ...calendarObject }, { timezone, locale: dateLocaleLanguage } ) }) } ), /* @__PURE__ */ jsxs( "li", { ref: (el) => { itemRefs.current[index] = el; }, className: "cometchat-ai-assistant-chat-history__item", role: "option", tabIndex: focusedIndex === index || focusedIndex === -1 && index === 0 ? 0 : -1, "aria-selected": false, onClick: () => onMessageClick?.(message), onKeyDown: (e) => { handleKeyDown(e, index); }, children: [ /* @__PURE__ */ jsx("span", { className: "cometchat-ai-assistant-chat-history__item-text", children: message.getText() }), /* @__PURE__ */ jsx( "button", { type: "button", className: "cometchat-ai-assistant-chat-history__delete-btn", "aria-label": getLocalizedString("conversation_delete_icon_hover"), onClick: (e) => { handleDeleteMessage(message, e); }, children: /* @__PURE__ */ jsx( "span", { className: "cometchat-ai-assistant-chat-history__delete-icon", "aria-hidden": "true" } ) } ) ] } ) ] }, message.getId()); }) } ) ] }); } ); CometChatAIAssistantChatHistory.displayName = "CometChatAIAssistantChatHistory"; // src/components/CometChatMessageList/CometChatMessageList.types.ts var CometChatMessageListAlignment = /* @__PURE__ */ ((CometChatMessageListAlignment2) => { CometChatMessageListAlignment2[CometChatMessageListAlignment2["left"] = 0] = "left"; CometChatMessageListAlignment2[CometChatMessageListAlignment2["standard"] = 1] = "standard"; return CometChatMessageListAlignment2; })(CometChatMessageListAlignment || {}); var initialMessageListState = { messages: [], fetchState: "idle", hasMore: true, hasMoreNewer: false, hasReachedLatest: true, isFetchingMore: false, error: null, scrollToMessageId: null, scrollToMessageHighlight: false, isAtBottom: true, newMessageCount: 0, lastReadMessageId: null, unreadCount: 0, isConversationRead: false, markedUnreadByUser: false, showUnreadBanner: false }; function usePluginRegistry() { const registry = useContext(CometChatPluginRegistryContext); if (!registry) { throw new Error( "usePluginRegistry: no CometChatPluginRegistryContext found. Wrap your component tree in a CometChatProvider or provide the context directly." ); } return registry; } // src/hooks/useDefaultMessageTypes.ts function useDefaultMessageTypes() { return usePluginRegistry().getAllMessageTypes(); } function useDefaultMessageCategories() { return usePluginRegistry().getAllMessageCategories(); } function useCometChatEvents(handler, deps) { const { subscribe } = useCometChatEventsContext(); const handlerRef = useRef(handler); useEffect(() => { handlerRef.current = handler; }); useEffect(() => { const unsubscribe = subscribe((event) => { handlerRef.current(event); }); return unsubscribe; }, [subscribe, ...deps]); } function isMessageForConversation(message, user, group, parentMessageId, loggedInUserId, options) { const senderId = message.getSender().getUid(); const excludeSender = options?.excludeSender ?? true; const isAgentChat = options?.isAgentChat ?? false; if (excludeSender && senderId === loggedInUserId) { return false; } const receiverId = message.getReceiverId(); const receiverType = message.getReceiverType(); const msgParentId = message.getParentMessageId() || 0; if (parentMessageId) { return msgParentId === parentMessageId; } if (msgParentId && !isAgentChat) { return false; } if (user) { return receiverType === "user" && (receiverId === user.getUid() || senderId === user.getUid()); } if (group) { return receiverType === "group" && receiverId === group.getGuid(); } return false; } function isThreadReplyForConversation(message, user, group) { const msgParentId = message.getParentMessageId() || 0; if (!msgParentId) return false; const receiverId = message.getReceiverId(); const senderId = message.getSender().getUid(); if (user) { return receiverId === user.getUid() || senderId === user.getUid(); } if (group) { return receiverId === group.getGuid(); } return false; } function isReactionForConversation(event, user, group, parentMessageId) { const receiverId = event.getReceiverId(); const receiverType = event.getReceiverType(); const reactedById = event.getReaction().getReactedBy().getUid(); const eventParentId = event.getParentMessageId() || 0; if (parentMessageId) { return eventParentId === parentMessageId; } if (eventParentId) { return false; } if (user) { return receiverType === "user" && (receiverId === user.getUid() || reactedById === user.getUid()); } if (group) { return receiverType === "group" && receiverId === group.getGuid(); } return false; } function deduplicateById(messages) { const lastIndex = /* @__PURE__ */ new Map(); for (let i = 0; i < messages.length; i++) { const msg = messages[i]; if (!msg) continue; const id = msg.getId(); if (!id) continue; lastIndex.set(id, i); } return messages.filter((_msg, i) => { const msg = messages[i]; if (!msg) return false; const id = msg.getId(); if (!id) return true; return lastIndex.get(id) === i; }); } function cloneMessage(message) { return Object.assign(Object.create(Object.getPrototypeOf(message)), message); } function updateReceiptsOnMessages(messages, receiptType, messageId, timestamp, loggedInUserId) { const needsUpdate = messages.some((m) => { const id = m.getId(); const senderUid = m.getSender().getUid(); if (id <= messageId && senderUid === loggedInUserId) { if (receiptType === "delivered" && !m.getDeliveredAt()) return true; if (receiptType === "read" && !m.getReadAt()) return true; } return false; }); if (!needsUpdate) return messages; return messages.map((m) => { const id = m.getId(); const senderUid = m.getSender().getUid(); if (id <= messageId && senderUid === loggedInUserId) { const shouldSetDelivered = receiptType === "delivered" && !m.getDeliveredAt(); const shouldSetRead = receiptType === "read" && !m.getReadAt(); if (!shouldSetDelivered && !shouldSetRead) return m; const next = cloneMessage(m); if (shouldSetDelivered) next.setDeliveredAt(timestamp); else if (shouldSetRead) next.setReadAt(timestamp); return next; } return m; }); } function updateQuotedMessageReferences(messages, editedMessage) { const editedId = editedMessage.getId(); const needsUpdate = messages.some( (m) => "getQuotedMessageId" in m && typeof m.getQuotedMessageId === "function" && m.getQuotedMessageId() === editedId && "setQuotedMessage" in m && typeof m.setQuotedMessage === "function" ); if (!needsUpdate) return messages; return messages.map((m) => { if (!("getQuotedMessageId" in m) || typeof m.getQuotedMessageId !== "function" || m.getQuotedMessageId() !== editedId || !("setQuotedMessage" in m) || typeof m.setQuotedMessage !== "function") { return m; } const next = cloneMessage(m); next.setQuotedMessage(editedMessage); return next; }); } function shouldMarkConversationRead(messages, lastReadId, unreadCount, parentMessageId) { if (parentMessageId) return false; if (unreadCount === 0) return true; if (!lastReadId) return true; if (messages.some((m) => Number(m.getId()) === lastReadId)) return true; if (messages.length > 0 && messages[0] && lastReadId < Number(messages[0].getId())) return true; return false; } function isDifferentDay2(ts1, ts2) { const d1 = new Date(ts1 * 1e3); const d2 = new Date(ts2 * 1e3); return d1.getFullYear() !== d2.getFullYear() || d1.getMonth() !== d2.getMonth() || d1.getDate() !== d2.getDate(); } // src/components/CometChatMessageList/CometChatMessageList.reducer.ts function isTerminalFailure(message) { if (message instanceof CometChat.TextMessage || message instanceof CometChat.MediaMessage) { if (message.getModerationStatus() === CometChatUIKitConstants.moderationStatus.disapproved) { return true; } } const direct = message.error; if (direct) return true; const getMeta = message.getMetadata; if (typeof getMeta !== "function") return false; const meta = getMeta.call(message); return Boolean(meta?.error); } function shouldReplace(existing, incoming) { if (isTerminalFailure(existing)) { return isTerminalFailure(incoming); } if ((existing instanceof CometChat.TextMessage || existing instanceof CometChat.MediaMessage) && (incoming instanceof CometChat.TextMessage || incoming instanceof CometChat.MediaMessage)) { const existingStatus = existing.getModerationStatus(); const incomingStatus = incoming.getModerationStatus(); const moderation = CometChatUIKitConstants.moderationStatus; const existingResolved = existingStatus === moderation.approved || existingStatus === moderation.disapproved; if (existingResolved && incomingStatus === moderation.pending) { return false; } } return true; } function messageAlreadyPresent(messages, message) { const id = message.getId(); const muid = message.getMuid(); return messages.some((m) => { if (id && String(m.getId()) === String(id)) return true; if (muid && m.getMuid() === muid) return true; return false; }); } function messageListReducer(state, action) { switch (action.type) { // --- Fetch previous (older messages, prepended) --- case "FETCH_PREVIOUS_START": { const fetchState = state.messages.length === 0 ? "loading" : state.fetchState; return { ...state, isFetchingMore: state.messages.length > 0, fetchState, error: null }; } case "FETCH_PREVIOUS_SUCCESS": { const merged = [...action.messages, ...state.messages]; const deduped = deduplicateById(merged); return { ...state, messages: deduped, fetchState: deduped.length === 0 && !state.hasMoreNewer ? "empty" : "loaded", hasMore: action.hasMore, isFetchingMore: false }; } case "FETCH_PREVIOUS_ERROR": { const fetchState = state.messages.length === 0 ? "error" : state.fetchState; return { ...state, fetchState, isFetchingMore: false, error: action.error }; } // --- Fetch next (newer messages, appended) --- case "FETCH_NEXT_START": return { ...state, isFetchingMore: true, error: null }; case "FETCH_NEXT_SUCCESS": { const merged = [...state.messages, ...action.messages]; const deduped = deduplicateById(merged); return { ...state, messages: deduped, fetchState: "loaded", hasMoreNewer: action.hasMoreNewer, isFetchingMore: false }; } case "FETCH_NEXT_ERROR": return { ...state, isFetchingMore: false, error: action.error }; // --- Fetch around a messageId (bidirectional — for goToMessage flow) --- case "FETCH_AROUND_SUCCESS": { const deduped = deduplicateById(action.messages); return { ...state, messages: deduped, fetchState: deduped.length === 0 ? "empty" : "loaded", hasMore: action.hasMore, hasMoreNewer: action.hasMoreNewer, hasReachedLatest: !action.hasMoreNewer, isFetchingMore: false, scrollToMessageId: action.targetMessageId, scrollToMessageHighlight: action.highlight ?? false }; } // --- Send lifecycle --- case "MESSAGE_SEND_START": { const existsByMuid = state.messages.some((m) => m.getMuid() === action.muid); if (!existsByMuid && messageAlreadyPresent(state.messages, action.message)) { return state; } const messages = existsByMuid ? state.messages.map((m) => m.getMuid() === action.muid ? action.message : m) : [...state.messages, action.message]; const fetchState = state.fetchState === "empty" && messages.length > 0 ? "loaded" : state.fetchState; return { ...state, messages, fetchState }; } case "MESSAGE_SEND_SUCCESS": { const messages = state.messages.map((m) => { if (m.getMuid() !== action.muid) return m; if (!shouldReplace(m, action.confirmedMessage)) return m; const getQuoted = m.getQuotedMessage; const setQuoted = action.confirmedMessage.setQuotedMessage; if (typeof getQuoted === "function" && typeof setQuoted === "function") { const quoted = getQuoted.call(m); if (quoted) setQuoted.call(action.confirmedMessage, quoted); } return action.confirmedMessage; }); return { ...state, messages }; } case "MESSAGE_SEND_ERROR": { const found = state.messages.some((m) => m.getMuid() === action.muid); if (found) { const messages = state.messages.map( (m) => m.getMuid() === action.muid ? action.message : m ); return { ...state, messages }; } if (messageAlreadyPresent(state.messages, action.message)) return state; return { ...state, messages: [...state.messages, action.message] }; } // --- Real-time updates --- case "REMOVE_STREAMING_BUBBLE": { const runStartedType = CometChat.AI_ASSISTANT_EVENTS.RUN_STARTED; const lastRunStartedIdx = state.messages.reduce( (lastIdx, m, i) => m.getType() === runStartedType ? i : lastIdx, -1 ); if (lastRunStartedIdx === -1) return state; const filtered = state.messages.filter((_, i) => i !== lastRunStartedIdx); return { ...state, messages: filtered }; } case "ADD_STREAMING_BUBBLE": { if (messageAlreadyPresent(state.messages, action.message)) return state; const messages = [...state.messages, action.message]; const fetchState = state.fetchState === "empty" ? "loaded" : state.fetchState; return { ...state, messages, fetchState }; } case "PROCESS_PENDING_MESSAGES": { const { pendingMessagesMap } = action; if (Object.keys(pendingMessagesMap).length === 0) return state; const runStartedType = CometChat.AI_ASSISTANT_EVENTS.RUN_STARTED; let messagesCopy = [...state.messages]; let changed = false; for (const runId of Object.keys(pendingMessagesMap)) { const pendingMessages = pendingMessagesMap[runId]; if (!pendingMessages || pendingMessages.length === 0) continue; const runStartedIndex = messagesCopy.findIndex((m) => { if (m.getType() !== runStartedType) return false; const data = m.getData?.(); return data && String(data.runId) === runId; }); if (runStartedIndex !== -1) { messagesCopy.splice(runStartedIndex, 1, ...pendingMessages); changed = true; } else { messagesCopy = [...messagesCopy, ...pendingMessages]; changed = true; } } if (!changed) return state; return { ...state, messages: messagesCopy }; } case "MESSAGE_RECEIVED": { const incomingId = action.message.getId(); if (!action.isLocalGroupAction) { const existingIndex = state.messages.findIndex( (m) => String(m.getId()) === String(incomingId) ); if (existingIndex !== -1) { const messages2 = [...state.messages]; messages2[existingIndex] = action.message; return { ...state, messages: messages2 }; } if (messageAlreadyPresent(state.messages, action.message)) { return state; } } if (!state.hasReachedLatest) { return { ...state, newMessageCount: action.fromLoggedInUser ? state.newMessageCount : state.newMessageCount + 1 }; } const messages = [...state.messages, action.message]; const shouldResetCount = !state.markedUnreadByUser && (state.isAtBottom || action.fromLoggedInUser); const isNewFromOther = !action.fromLoggedInUser; return { ...state, messages, fetchState: "loaded", newMessageCount: shouldResetCount ? 0 : isNewFromOther ? state.newMessageCount + 1 : state.newMessageCount, unreadCount: state.markedUnreadByUser && isNewFromOther ? state.unreadCount + 1 : state.unreadCount }; } case "MESSAGE_EDITED": { let messages = state.messages.map((m) => { if (String(m.getId()) !== String(action.message.getId())) return m; return shouldReplace(m, action.message) ? action.message : m; }); messages = updateQuotedMessageReferences(messages, action.message); return { ...state, messages }; } case "MESSAGE_DELETED": { const messages = state.messages.map( (m) => String(m.getId()) === String(action.message.getId()) ? action.message : m ); return { ...state, messages }; } // --- Moderation --- case "MESSAGE_MODERATED": { const msgId = action.message.getId(); const msgMuid = action.message.getMuid() || ""; const messages = state.messages.map((m) => { if (m.getId() === msgId || msgMuid && m.getMuid() === msgMuid) { return action.message; } return m; }); return { ...state, messages }; } // --- Receipts --- case "RECEIPT_UPDATE": { const messages = updateReceiptsOnMessages( state.messages, action.receiptType, action.messageId, action.timestamp, action.loggedInUserId ); if (messages === state.messages) return state; return { ...state, messages }; } // --- Reactions --- case "REACTION_UPDATE": { const messages = state.messages.map((m) => { if (String(m.getId()) === String(action.messageId)) { const next = cloneMessage(m); next.setReactions(action.reactions); return next; } return m; }); return { ...state, messages }; } // --- Reply count --- case "UPDATE_REPLY_COUNT": { const idx = state.messages.findIndex((m) => m.getId() === action.parentMessageId); if (idx === -1) return state; const target = state.messages[idx]; if (!target) return state; const next = cloneMessage(target); const currentCount = next.getReplyCount() || 0; next.setReplyCount(currentCount + 1); const messages = [...state.messages]; messages[idx] = next; return { ...state, messages }; } // --- State flags --- case "SET_HAS_REACHED_LATEST": return { ...state, hasReachedLatest: action.hasReachedLatest }; case "UPDATE_GROUP_REFERENCE": return state; case "SET_AT_BOTTOM": return { ...state, isAtBottom: action.isAtBottom, newMessageCount: action.isAtBottom && !state.markedUnreadByUser ? 0 : state.newMessageCount }; case "SET_SCROLL_TO_MESSAGE": return { ...state, scrollToMessageId: action.messageId, scrollToMessageHighlight: action.highlight ?? false }; case "CLEAR_NEW_MESSAGE_COUNT": return { ...state, newMessageCount: state.markedUnreadByUser ? state.newMessageCount : 0 }; // --- Unread tracking --- case "SET_LAST_READ_MESSAGE_ID": return { ...state, lastReadMessageId: action.messageId }; case "SET_UNREAD_COUNT": return { ...state, unreadCount: action.count }; case "SET_CONVERSATION_READ": return { ...state, isConversationRead: true, unreadCount: 0, newMessageCount: 0 }; case "SET_MARKED_UNREAD_BY_USER": return { ...state, markedUnreadByUser: action.value }; case "SET_SHOW_UNREAD_BANNER": return { ...state, showUnreadBanner: action.value }; // --- Reset --- case "RESET": return initialMessageListState; default: return state; } } var CometChatMessageListManager = class { constructor(options) { this.nextRequest = null; this.builderConfig = options; this.receiverType = options.user ? CometChat.RECEIVER_TYPE.USER : CometChat.RECEIVER_TYPE.GROUP; this.receiverId = options.user ? options.user.getUid() : options.group ? options.group.getGuid() : ""; this.previousRequest = this.buildRequest(); } /** Build a MessagesRequest from the stored config. */ buildRequest(messageId) { const opts = this.builderConfig; if (opts.builder) { const builder2 = opts.builder; if (messageId !== void 0) { builder2.setMessageId(messageId); } return builder2.build(); } const builder = new CometChat.MessagesRequestBuilder().setLimit(opts.limit ?? 30).hideDeletedMessages(true); if (opts.user) { builder.setUID(opts.user.getUid()); } else if (opts.group) { builder.setGUID(opts.group.getGuid()); } if (opts.parentMessageId) { builder.setParentMessageId(opts.parentMessageId); builder.hideReplies(false); builder.withParent(true); } else { builder.hideReplies(true); } if (opts.messageTypes) { builder.setTypes(opts.messageTypes); } if (opts.messageCategories) { builder.setCategories(opts.messageCategories); } if (messageId !== void 0) { builder.setMessageId(messageId); } return builder.build(); } // --- Fetch --- /** Fetch older messages (reverse pagination). */ async fetchPrevious() { const result = await this.previousRequest.fetchPrevious(); return result; } /** Fetch newer messages (forward pagination). */ async fetchNext() { if (!this.nextRequest) { return []; } return this.nextRequest.fetchNext(); } /** * Initialize the next request for forward pagination from a given messageId. * Call this after fetching around a messageId to enable fetchNext(). */ initNextRequest(messageId) { this.nextRequest = this.buildRequest(messageId); } /** * Fetch messages around a specific messageId (bidirectional). * SDK's setMessageId is exclusive — fetchPrevious returns messages BEFORE the target, * fetchNext returns messages AFTER the target. * We fetch the target message separately and combine all three sets. * * Returns `{ messages, hasMoreNewer }` so callers can determine if there are * newer messages beyond the fetched window. */ async fetchAroundMessageId(messageId) { const request = this.buildRequest(messageId); this.previousRequest = request; const previousMessages = await request.fetchPrevious(); let targetMessage = null; try { const details = await CometChat.getMessageDetails(String(messageId)); if (details) { targetMessage = details; } } catch { } this.nextRequest = this.buildRequest(messageId); const nextMessages = await this.nextRequest.fetchNext(); const allMessages = [...previousMessages]; if (targetMessage) { allMessages.push(targetMessage); } allMessages.push(...nextMessages); const limit = this.builderConfig.limit ?? 30; const hasMoreNewer = nextMessages.length >= limit; return { messages: allMessages, hasMoreNewer }; } // --- Send --- async sendTextMessage(text, parentMessageId, richTextHtml) { const msg = new CometChat.TextMessage(this.receiverId, text, this.receiverType); if (parentMessageId) { msg.setParentMessageId(parentMessageId); } if (richTextHtml && richTextHtml.trim().length > 0) { msg.setMetadata({ richText: { html: richTextHtml, hasFormatting: true } }); } return CometChat.sendMessage(msg); } async sendMediaMessage(file, type, parentMessageId) { const msg = new CometChat.MediaMessage(this.receiverId, file, type, this.receiverType); if (parentMessageId) { msg.setParentMessageId(parentMessageId); } return CometChat.sendMediaMessage(msg); } // --- Edit / Delete --- async editMessage(message) { return CometChat.editMessage(message); } async deleteMessage(messageId) { return CometChat.deleteMessage(String(messageId)); } // --- Read receipts --- async markAsRead(message) { await CometChat.markAsRead(message); } async markConversationAsRead() { await CometChat.markConversationAsRead(this.receiverId, this.receiverType); } async markAsDelivered(message) { await CometChat.markAsDelivered(message); } /** Mark a message as unread. Returns the updated conversation. */ async markMessageAsUnread(message) { return CometChat.markMessageAsUnread(message); } // --- Conversation --- async getConversation() { return CometChat.getConversation(this.receiverId, this.receiverType); } // --- Accessors --- getReceiverId() { return this.receiverId; } getReceiverType() { return this.receiverType; } }; // src/components/CometChatMessageList/messageListHelpers.ts function noop() { } function extractGroupFromEvent(event) { switch (event.type) { case "group/member-joined": return event.joinedGroup; case "group/member-left": return event.leftGroup; case "group/member-kicked": return event.kickedFrom; case "group/member-banned": return event.bannedFrom; case "group/member-unbanned": return event.unbannedFrom; case "group/member-added": return event.addedTo; case "group/member-scope-changed": return event.changedGroup; default: return void 0; } } // src/components/CometChatMessageList/useMessageListInit.ts function useMessageListInit(options, refs, dispatch) { const { user, group, loggedInUser, messagesRequestBuilder, parentMessageId, startFromUnreadMessages, goToMessageId, messageTypes, messageCategories, onError, isAgentChat, loadLastAgentConversation, onActiveChatChanged } = options; const publish = usePublishEvent(); useEffect(() => { if (!u