@cometchat/chat-uikit-react
Version:
Ready-to-use Chat UI Components for React
1,391 lines (1,383 loc) • 837 kB
JavaScript
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