import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react"; import { Keyboard } from "react-native"; import { ColorValue, Dimensions, ImageSourcePropType, ImageStyle, InteractionManager, KeyboardAvoidingView, KeyboardAvoidingViewProps, Modal, NativeModules, Platform, Text, TouchableOpacity, TouchableWithoutFeedbackProps, View, ViewStyle, } from "react-native"; import { startStreamingForRunId, stopStreamingForRunId, streamingState$ } from "../shared/services/stream-message.service"; import { CometChatActionSheet, CometChatBottomSheet, CometChatMentionsFormatter, CometChatMessageInput, CometChatMessagePreview, CometChatSuggestionList, CometChatTextFormatter, CometChatUIKit, CometChatUrlsFormatter, SuggestionItem, } from "../shared"; import { Style } from "./styles"; //@ts-ignore import { CheckPropertyExists } from "../shared/helper/functions"; import { CometChat } from "@cometchat/chat-sdk-react-native"; import { ChatConfigurator, CometChatSoundManager } from "../shared"; import { commonVars } from "../shared/base/vars"; import { ConversationOptionConstants, MentionsTargetElement, MentionsVisibility, MessageTypeConstants, ReceiverTypeConstants, ViewAlignment, } from "../shared/constants/UIKitConstants"; import { MessageEvents } from "../shared/events"; import { CometChatUIEventHandler } from "../shared/events/CometChatUIEventHandler/CometChatUIEventHandler"; import { CometChatMessageEvents } from "../shared/events/CometChatMessageEvents"; import { CometChatMessageComposerAction, DeepPartial } from "../shared/helper/types"; import { Icon } from "../shared/icons/Icon"; import { CometChatSendButtonView } from "../shared/views/CometChatSendButtonView/CometChatSendButtonView"; import { getUnixTimestampInMilliseconds, messageStatus, } from "../shared/utils/CometChatMessageHelper"; import { CommonUtils } from "../shared/utils/CommonUtils"; import { isCursorWithinMentionRange, getMentionRangeAtCursor } from "../shared/utils/MentionUtils"; import { permissionUtil } from "../shared/utils/PermissionUtil"; import { CometChatMediaRecorder } from "../shared/views/CometChatMediaRecorder"; import { useTheme } from "../theme"; import { ICONS } from "./resources"; import { CometChatTheme } from "../theme/type"; import { deepMerge } from "../shared/helper/helperFunctions"; import { JSX } from "react"; import { useCometChatTranslation } from "../shared/resources/CometChatLocalizeNew"; type MentionOverlap = { key: string; value: SuggestionItem; start: number; end: number; }; const { FileManager, CommonUtil } = NativeModules; const uiEventListenerShow = "uiEvent_show_" + new Date().getTime(); const uiEventListenerHide = "uiEvent_hide_" + new Date().getTime(); const AttachIconButton = (props: { onPress: TouchableWithoutFeedbackProps["onPress"]; icon: ImageSourcePropType | JSX.Element; iconStyle: ImageStyle; }) => { return ( ); }; const ActionSheetBoard = (props: any) => { const { shouldShow = false, onClose = () => {}, options = [], sheetRef, style } = props; return ( ); }; const RecordAudio = (props: any) => { const { shouldShow = false, onClose = () => {}, options = [], cometChatBottomSheetStyle = {}, sheetRef, onPause = () => {}, onPlay = () => {}, onSend = (recordedFile: String) => {}, onStop = (recordedFile: String) => {}, onStart = () => {}, mediaRecorderStyle, ...otherProps } = props; return ( ); }; type Enumerate = Acc["length"] extends N ? Acc[number] : Enumerate; type IntRange = Exclude, Enumerate>; /** * Properties for the CometChat message composer component. */ export interface CometChatMessageComposerInterface { /** * Message composer identifier. * * @type {string | number} */ id?: string | number; /** * CometChat SDK's user object. * * @type {CometChat.User} */ user?: CometChat.User; /** * CometChat SDK's group object. * * @type {CometChat.Group} */ group?: CometChat.Group; /** * Flag to turn off sound for outgoing messages. * * @type {boolean} */ disableSoundForOutgoingMessages?: boolean; /** * Custom audio sound to be played while sending messages. * * @type {*} */ customSoundForOutgoingMessage?: any; /** * Flag to disable typing events. * * @type {boolean} */ disableTypingEvents?: boolean; /** * Initial text to be displayed in the composer. * * @type {string} */ initialComposertext?: string; /** * Renders a preview section at the top of the composer. * * @param {Object} props - The component properties. * @param {CometChat.User} [props.user] - The user object. * @param {CometChat.Group} [props.group] - The group object. * @returns {JSX.Element} The header view element. */ HeaderView?: ({ user, group }: { user?: CometChat.User; group?: CometChat.Group }) => JSX.Element; /** * Callback triggered when the input text changes. * * @param {string} text - The updated text. */ onTextChange?: (text: string) => void; /** * Returns the attachment options for the composer. * * @param {Object} props - The function properties. * @param {CometChat.User} [props.user] - The user object. * @param {CometChat.Group} [props.group] - The group object. * @param {Map} props.composerId - The composer identifier as a Map. * @returns {CometChatMessageComposerAction[]} An array of composer actions. */ attachmentOptions?: ({ user, group, composerId, }: { user?: CometChat.User; group?: CometChat.Group; composerId: Map; }) => CometChatMessageComposerAction[]; /** * Replaces the default Auxiliary Button. * * @param {Object} props - The function properties. * @param {CometChat.User} [props.user] - The user object. * @param {CometChat.Group} [props.group] - The group object. * @param {string | number} props.composerId - The composer identifier. * @returns {JSX.Element} A custom auxiliary button component. */ AuxiliaryButtonView?: ({ user, group, composerId, }: { user?: CometChat.User; group?: CometChat.Group; composerId: string | number; }) => JSX.Element; /** * Replaces the default Send Button. * * @param {Object} props - The function properties. * @param {CometChat.User} [props.user] - The user object. * @param {CometChat.Group} [props.group] - The group object. * @param {string | number} props.composerId - The composer identifier. * @returns {JSX.Element} A custom send button component. */ SendButtonView?: ({ user, group, composerId, }: { user?: CometChat.User; group?: CometChat.Group; composerId: string | number; }) => JSX.Element; /** * Message id required for threaded messages. * * @type {string | number} */ parentMessageId?: string | number; /** * Custom styles for the message composer component. */ style?: DeepPartial; /** * Flag to hide the voice recording button. * * @type {boolean} */ hideVoiceRecordingButton?: boolean; /** * Callback triggered when the send button is pressed. * * @param {CometChat.BaseMessage} message - The base message object. */ onSendButtonPress?: (message: CometChat.BaseMessage) => void; /** * Callback triggered when an error occurs. * * @param {CometChat.CometChatException} error - The error object. */ onError?: (error: CometChat.CometChatException) => void; /** * Override properties for the KeyboardAvoidingView. */ keyboardAvoidingViewProps?: KeyboardAvoidingViewProps; /** * Collection of text formatter classes to apply custom formatting. * * @type {Array} */ textFormatters?: Array< CometChatMentionsFormatter | CometChatUrlsFormatter | CometChatTextFormatter >; /** * Flag to disable mention functionality. */ disableMentions?: boolean; /** * Flag to disable the special group mention (@all / @channel etc.). * When true, the alias suggestion will not appear. * @default false */ disableMentionAll?: boolean; /** * Custom alias label for the group-wide mention. * Rendered as @Alias in composer and bubbles. * Internally stored as <@all:Alias>. * @default "all" */ mentionAllLabel?: string; /** * Controls image quality when taking pictures from the camera. * A value of 100 means no compression. * * @default 20 * @type {IntRange<1, 100>} */ imageQuality?: IntRange<1, 100>; /** * If true, hides the camera option from the attachment options. */ hideCameraOption?: boolean; /** * If true, hides the image attachment option from the attachment options. */ hideImageAttachmentOption?: boolean; /** * If true, hides the video attachment option from the attachment options. */ hideVideoAttachmentOption?: boolean; /** * If true, hides the audio attachment option from the attachment options. */ hideAudioAttachmentOption?: boolean; /** * If true, hides the file/document attachment option from the attachment options. */ hideFileAttachmentOption?: boolean; /** * If true, hides the polls option from the attachment options. */ hidePollsAttachmentOption?: boolean; /** * If true, hides the collaborative document option (e.g., shared document editing). */ hideCollaborativeDocumentOption?: boolean; /** * If true, hides the collaborative whiteboard option. */ hideCollaborativeWhiteboardOption?: boolean; /** * If true, hides the entire attachment button from the composer. */ hideAttachmentButton?: boolean; /** * If true, hides the stickers button from the composer. */ hideStickersButton?: boolean; /** * If true, hides the send button from the composer. */ hideSendButton?: boolean; /** * If true, hides all auxiliary buttons (such as voice input, GIFs, or other plugin buttons). */ hideAuxiliaryButtons?: boolean; /** * Returns the attachment options for the composer. * * @param {Object} props - The function properties. * @param {CometChat.User} [props.user] - The user object. * @param {CometChat.Group} [props.group] - The group object. * @param {Map} props.composerId - The composer identifier as a Map. * @returns {CometChatMessageComposerAction[]} An array of composer actions. */ addAttachmentOptions?: ({ user, group, composerId, }: { user?: CometChat.User; group?: CometChat.Group; composerId: Map; }) => CometChatMessageComposerAction[]; /** * Determines the alignment of auxiliary buttons (e.g., sticker button). * Can be either "left" or "right". * * @default "left" */ auxiliaryButtonsAlignment?: "left" | "right"; /** * Custom send button view for AI agents (only applies to @agentic users) */ AgentSendButtonView?: React.ComponentType<{ isButtonDisabled: boolean; composerRef: any; }>; } export const CometChatMessageComposer = React.forwardRef( (props: CometChatMessageComposerInterface, ref) => { const editMessageListenerID = "editMessageListener_" + new Date().getTime(); const replyMessageListenerID = "replyMessageListener_" + new Date().getTime(); const UiEventListenerID = "UiEventListener_" + new Date().getTime(); const theme = useTheme(); const {t} = useCometChatTranslation() const { id, user, group, disableSoundForOutgoingMessages = true, customSoundForOutgoingMessage, disableTypingEvents: propDisableTypingEvents, initialComposertext, HeaderView, onTextChange, attachmentOptions, AuxiliaryButtonView, SendButtonView, parentMessageId, style = {}, onSendButtonPress, onError, hideVoiceRecordingButton: propHideVoiceRecordingButton, keyboardAvoidingViewProps, textFormatters, disableMentions: propDisableMentions, disableMentionAll = false, mentionAllLabel = "all", imageQuality = 20, hideCameraOption = false, hideImageAttachmentOption = false, hideVideoAttachmentOption = false, hideAudioAttachmentOption = false, hideFileAttachmentOption = false, hidePollsAttachmentOption = false, hideCollaborativeDocumentOption = false, hideCollaborativeWhiteboardOption = false, hideAttachmentButton: propHideAttachmentButton = false, hideStickersButton: propHideStickersButton = false, hideSendButton = false, hideAuxiliaryButtons = false, addAttachmentOptions, auxiliaryButtonsAlignment = "left", AgentSendButtonView, } = props; // Helper function to check if user is agentic const isAgenticUser = useCallback(() => { return user?.getRole?.() === '@agentic'; }, [user]); // Apply automatic hiding for @agentic users const disableTypingEvents = isAgenticUser() ? true : propDisableTypingEvents; const disableMentions = isAgenticUser() ? true : propDisableMentions; const hideAttachmentButton = isAgenticUser() ? true : propHideAttachmentButton; const hideStickersButton = isAgenticUser() ? true : propHideStickersButton; const hideVoiceRecordingButton = isAgenticUser() ? true : propHideVoiceRecordingButton; const composerIdMap = new Map().set("parentMessageId", parentMessageId); const parentMessageIdRef = useRef(null); const mergedComposerStyle: CometChatTheme["messageComposerStyles"] = useMemo(() => { const mergedStyle = deepMerge(theme.messageComposerStyles, style); if (isAgenticUser()) { return { ...mergedStyle, messageInputStyles: { ...mergedStyle.messageInputStyles, dividerStyle: { display: 'none' } } }; } return mergedStyle; }, [theme, style, isAgenticUser]); const loggedInUser = React.useRef({}); const chatWith = React.useRef(null); const chatWithId = React.useRef(null); const messageInputRef = React.useRef(null); const chatRef = React.useRef(chatWith); const inputValueRef = React.useRef(null); const plainTextInput = React.useRef(initialComposertext || ""); let mentionMap = React.useRef>(new Map()); let trackingCharacters = React.useRef([]); let allFormatters = React.useRef< Map >(new Map()); let activeCharacter = React.useRef(""); let searchStringRef = React.useRef(""); const [selectionPosition, setSelectionPosition] = React.useState({}); const [inputMessage, setInputMessage] = React.useState( initialComposertext || "" ); const [isStreaming, setIsStreaming] = React.useState(false); useEffect(() => { const sub = streamingState$.subscribe((streaming) => { setIsStreaming(streaming); if (!streaming) setShowStopButton(false); }); return () => sub.unsubscribe(); }, []); const [showActionSheet, setShowActionSheet] = React.useState(false); const [showRecordAudio, setShowRecordAudio] = React.useState(false); const [actionSheetItems, setActionSheetItems] = React.useState([]); const [messagePreview, setMessagePreview] = React.useState(); const [replyMessage, setReplyMessage] = React.useState(); // Ref to hold current replyMessage value to avoid stale closures in attachment handlers const replyMessageRef = React.useRef(replyMessage); const [CustomView, setCustomView] = React.useState(null); const [CustomViewHeader, setCustomViewHeader] = React.useState( null ); const [CustomViewFooter, setCustomViewFooter] = React.useState(); const [isVisible, setIsVisible] = React.useState(false); const [kbOffset, setKbOffset] = React.useState(59); const [showMentionList, setShowMentionList] = React.useState(false); const [mentionsSearchData, setMentionsSearchData] = React.useState>([]); const [suggestionListLoader, setSuggestionListLoader] = React.useState(false); const [warningMessage, setWarningMessage] = React.useState(""); const [originalText, setOriginalText] = React.useState(""); const [hasEdited, setHasEdited] = React.useState(false); const [plainText, setPlainText] = React.useState(initialComposertext ?? ""); const bottomSheetRef = React.useRef(null); const [showStopButton, setShowStopButton] = React.useState(false); const [isSendButtonDisabledForDelay, setIsSendButtonDisabledForDelay] = React.useState(false); const sendButtonDelayTimer = React.useRef(null); const closeReplyPreview = useCallback(() => { setReplyMessage(null); }, []); // Reset streaming state when component mounts const defaultAuxiliaryButtonOptions = useMemo(() => { if (hideAuxiliaryButtons) return []; return ChatConfigurator.getDataSource().getAuxiliaryOptions(user, group, composerIdMap, { stickerIcon: mergedComposerStyle.stickerIcon as ImageSourcePropType | JSX.Element, stickerIconStyle: mergedComposerStyle.stickerIconStyle as { active: ImageStyle; inactive: ImageStyle; }, hideStickersButton, replyToMessage: replyMessage?.message, closeReplyPreview, }); }, [mergedComposerStyle, hideStickersButton, hideAuxiliaryButtons, replyMessage, closeReplyPreview]); useEffect(() => { setShowStopButton(false); setIsStreaming(false); // Cleanup function to clear the delay timer on unmount return () => { if (sendButtonDelayTimer.current) { clearTimeout(sendButtonDelayTimer.current); } }; }, []); useLayoutEffect(() => { if (Platform.OS === "ios") { if (Number.isInteger(commonVars.safeAreaInsets.top)) { setKbOffset(commonVars.safeAreaInsets.top ?? 0); return; } CommonUtil.getSafeAreaInsets().then((res: any) => { if (Number.isInteger(res.top)) { commonVars.safeAreaInsets.top = res.top; commonVars.safeAreaInsets.bottom = res.bottom; setKbOffset(res.top); } }); } }, []); const isTyping = useRef | null>(null); /** * Event callback */ React.useImperativeHandle(ref, () => ({ previewMessageForEdit: previewMessage, sendTextMessage, getText: () => messageInputRef.current?.getText?.() ?? "", clear: () => messageInputRef.current?.clear?.(), resetStreaming: () => { setShowStopButton(false); setIsStreaming(false); try { stopStreamingForRunId(); } catch (error) { // console.log(error); // Removed debug log } } })); useLayoutEffect(() => { if (warningMessage) { setCustomViewHeader( {warningMessage} ); return; } setCustomViewHeader(null); }, [warningMessage, theme]); // Keep replyMessageRef in sync with replyMessage state useEffect(() => { replyMessageRef.current = replyMessage; }, [replyMessage]); const previewMessage = ({ message, status }: any) => { if (status === messageStatus.inprogress) { // Don't clear reply preview here - it needs to remain available for sendMediaMessage // The reply preview will be cleared after successful send in sendMediaMessage (line 1127) let textComponents = message?.text; let rawText = message?.text; let users: any = {}; let regexes: Array = []; allFormatters.current.forEach((formatter: CometChatTextFormatter, key) => { formatter.handleComposerPreview(message); if (!regexes.includes(formatter.getRegexPattern())) { regexes.push(formatter.getRegexPattern()); } let suggestionUsers = formatter.getSuggestionItems(); suggestionUsers.forEach((item) => (users[item.underlyingText] = item)); let resp = formatter.getFormattedText(textComponents); if (formatter instanceof CometChatMentionsFormatter) { getMentionLimitView(formatter); } textComponents = resp; }); let edits: any = []; regexes.forEach((regex) => { let match: any; while ((match = regex.exec(rawText)) !== null) { const user = users[match[0]]; if (user) { edits.push({ startIndex: match.index, endIndex: regex.lastIndex, replacement: user.promptText, user, }); } } }); // Sort edits by startIndex to apply them in order edits.sort((a: any, b: any) => a.startIndex - b.startIndex); plainTextInput.current = getPlainString(message?.text, edits); setPlainText(plainTextInput.current); setOriginalText(plainTextInput.current.trim()); setHasEdited(false); const hashMap = new Map(); let offset = 0; // Tracks shift in position due to replacements edits.forEach((edit: any) => { const adjustedStartIndex = edit.startIndex + offset; rawText = rawText.substring(0, adjustedStartIndex) + edit.replacement + rawText.substring(edit.endIndex); offset += edit.replacement.length - (edit.endIndex - edit.startIndex); const rangeKey = `${adjustedStartIndex}_${adjustedStartIndex + edit.replacement.length}`; hashMap.set(rangeKey, edit.user); }); mentionMap.current = hashMap; setMessagePreview({ message: message, mode: ConversationOptionConstants.edit, }); inputValueRef.current = textComponents ?? ""; setInputMessage(textComponents ?? ""); messageInputRef.current.focus(); } }; const previewReplyMessage = (message: any) => { // Clear edit preview if it exists if (messagePreview) { setMessagePreview(null); mentionMap.current = new Map(); plainTextInput.current = ""; setOriginalText(""); } setReplyMessage({ message: message, mode: ConversationOptionConstants.reply, }); try { messageInputRef.current?.focus(); } catch (error) { // console.log(error); // Removed debug log } }; const cameraCallback = async (cameraImage: any) => { if (CheckPropertyExists(cameraImage, "error")) { return; } const { name, uri, type } = cameraImage; let file = { name, type, uri, }; sendMediaMessage(chatWithId.current, file, MessageTypeConstants.image, chatWith.current); }; const fileInputHandler = async (fileType: string) => { if (fileType === MessageTypeConstants.takePhoto) { if (!(await permissionUtil.startResourceBasedTask(["camera"]))) { return; } let quality = imageQuality; if (isNaN(imageQuality) || imageQuality < 1 || imageQuality > 100) { quality = 20; } if (Platform.OS === "android") { FileManager.openCamera(fileType, Math.round(quality), cameraCallback); } else { FileManager.openCamera(fileType, cameraCallback); } } else if (Platform.OS === "ios" && fileType === MessageTypeConstants.video) { NativeModules.VideoPickerModule.pickVideo((file: any) => { if (file.uri) sendMediaMessage( chatWithId.current, file, MessageTypeConstants.video, chatWith.current ); }); } else FileManager.openFileChooser(fileType, async (fileInfo: any) => { if (CheckPropertyExists(fileInfo, "error")) { return; } let { name, uri, type } = fileInfo; let file = { name, type, uri, }; sendMediaMessage(chatWithId.current, file, fileType, chatWith.current); }); }; const playAudio = () => { if (customSoundForOutgoingMessage) { CometChatSoundManager.play( CometChatSoundManager.SoundOutput.outgoingMessage, customSoundForOutgoingMessage ); } else { CometChatSoundManager.play(CometChatSoundManager.SoundOutput.outgoingMessage); } }; const clearInputBox = () => { inputValueRef.current = ""; setPlainText(""); setHasEdited(false); setInputMessage(""); setWarningMessage(""); setReplyMessage(null); }; const clearEditPreview = () => { inputValueRef.current = ""; setPlainText(""); setHasEdited(false); setInputMessage(""); setWarningMessage(""); }; const sendTextMessage = () => { setShowStopButton(true); //ignore sending new message if (messagePreview != null) { editMessage(messagePreview.message); return; } if (Platform.OS === "ios" && messageInputRef.current) { InteractionManager.runAfterInteractions(() => { let textToProcess = plainTextInput.current; if (typeof inputMessage === 'string' && inputMessage.trim().length > 0) { textToProcess = inputMessage; } processAndSendMessage(textToProcess); }); return; // Exit early, processAndSendMessage will handle sending } // For non-iOS platforms, proceed normally processAndSendMessage(plainTextInput.current); }; const processAndSendMessage = (textToProcess: string) => { let finalTextInput = getRegexString(textToProcess); let trimmedTextInput = finalTextInput.trim(); if (trimmedTextInput.trim().length === 0) { return; } const currentReplyMessage = replyMessage; const replyMessageId = currentReplyMessage?.message?.getId?.() ?? null; let textMessage = new CometChat.TextMessage( chatWithId.current, trimmedTextInput, chatWith.current ); textMessage.setSender(loggedInUser.current); textMessage.setReceiver(chatWith.current); textMessage.setText(trimmedTextInput); textMessage.setMuid(String(getUnixTimestampInMilliseconds())); // Handle parent message ID for threaded messages if (parentMessageId) { textMessage.setParentMessageId(parentMessageId as number); } else if (isAgenticUser() && parentMessageIdRef.current) { textMessage.setParentMessageId(Number(parentMessageIdRef.current)); } // Handle reply message - set quoted message for replies if (replyMessageId && currentReplyMessage?.message) { try { // Use only the SDK's built-in quoted message functionality if (typeof textMessage.setQuotedMessage === 'function') { textMessage.setQuotedMessage(currentReplyMessage.message); } if (typeof textMessage.setQuotedMessageId === 'function') { textMessage.setQuotedMessageId(replyMessageId); } } catch (error) { // console.log(error); // Removed debug log } } allFormatters.current.forEach((item) => { textMessage = item.handlePreMessageSend(textMessage); }); setMentionsSearchData([]); plainTextInput.current = ""; // Clear input and reply message immediately for better UX inputValueRef.current = ""; setPlainText(""); setHasEdited(false); setInputMessage(""); setWarningMessage(""); setReplyMessage(null); // Clear the TextInput on iOS to remove auto-corrected text if (Platform.OS === "ios" && messageInputRef.current) { InteractionManager.runAfterInteractions(() => { try { messageInputRef.current?.setNativeProps?.({ text: "" }); messageInputRef.current?.clear?.(); } catch (error) { // Ignore errors when clearing } }); } if (onSendButtonPress) { onSendButtonPress(textMessage); return; } CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: textMessage, status: messageStatus.inprogress, }); if (!disableSoundForOutgoingMessages) playAudio(); // Enable 1-second delay for agentic users if (isAgenticUser()) { setIsSendButtonDisabledForDelay(true); if (sendButtonDelayTimer.current) { clearTimeout(sendButtonDelayTimer.current); } sendButtonDelayTimer.current = setTimeout(() => { setIsSendButtonDisabledForDelay(false); }, 1000); } CometChat.sendMessage(textMessage) .then((message: any) => { if (isAgenticUser() && !parentMessageId && message?.getId) { const messageId = typeof message.getId() === 'string' ? Number(message.getId()) : message.getId(); if (!isNaN(messageId) && !parentMessageIdRef.current) { parentMessageIdRef.current = messageId; } if (messageId && !isNaN(messageId)) { startStreamingForRunId(String(messageId)); } } CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: message, status: messageStatus.success, }); }) .catch((error: any) => { onError && onError(error); textMessage.setMetadata({ error: true }); CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: textMessage, status: messageStatus.error, }); }); }; /** edit message */ const editMessage = (message: any) => { endTyping(null, null); let finalTextInput = getRegexString(plainTextInput.current); let messageText = finalTextInput.trim(); let textMessage = new CometChat.TextMessage( chatWithId.current, messageText, chatWith.current ); textMessage.setId(message.id); parentMessageId && textMessage.setParentMessageId(parentMessageId as number); inputValueRef.current = ""; clearInputBox(); messageInputRef.current.textContent = ""; setMessagePreview(null); if (onSendButtonPress) { onSendButtonPress(textMessage); return; } if (!disableSoundForOutgoingMessages) playAudio(); CometChat.editMessage(textMessage) .then((editedMessage: any) => { inputValueRef.current = ""; setInputMessage(""); CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageEdited, { message: editedMessage, status: messageStatus.success, }); }) .catch((error: any) => { onError && onError(error); }); }; /** send media message */ const sendMediaMessage = ( receiverId?: any, messageInput?: any, messageType?: any, receiverType?: any ) => { setShowActionSheet(false); let mediaMessage = new CometChat.MediaMessage( receiverId, messageInput, messageType, receiverType ); mediaMessage.setSender(loggedInUser.current); mediaMessage.setReceiver(receiverType); mediaMessage.setType(messageType); mediaMessage.setMuid(String(getUnixTimestampInMilliseconds())); mediaMessage.setData({ type: messageType, category: CometChat.CATEGORY_MESSAGE, name: messageInput["name"], file: messageInput, url: messageInput["uri"], sender: loggedInUser.current, }); // Use ref to get current replyMessage value (avoids stale closure) const currentReplyMessage = replyMessageRef.current; const replyMessageId = currentReplyMessage?.message?.getId?.() ?? null; if (replyMessageId) { if (currentReplyMessage?.message) { if (typeof mediaMessage.setQuotedMessage === 'function') { mediaMessage.setQuotedMessage(currentReplyMessage.message); } if (typeof mediaMessage.setQuotedMessageId === 'function') { mediaMessage.setQuotedMessageId(replyMessageId); } // Store full message object directly on the message (not in metadata) (mediaMessage as any).quotedMessage = currentReplyMessage.message; (mediaMessage as any).quotedMessageId = replyMessageId; // Don't store in metadata - it exceeds size limit and SDK handles it } } else if (parentMessageId) { mediaMessage.setParentMessageId(parentMessageId as number); } let localMessage = new CometChat.MediaMessage( receiverId, messageInput, messageType, receiverType ); localMessage.setSender(loggedInUser.current); localMessage.setReceiver(receiverType); localMessage.setType(messageType); localMessage.setMuid(String(getUnixTimestampInMilliseconds())); localMessage.setData({ type: messageType, category: CometChat.CATEGORY_MESSAGE, name: messageInput["name"], file: messageInput, url: messageInput["uri"], sender: loggedInUser.current, }); if (replyMessageId) { if (currentReplyMessage?.message) { if (typeof localMessage.setQuotedMessage === 'function') { localMessage.setQuotedMessage(currentReplyMessage.message); } if (typeof localMessage.setQuotedMessageId === 'function') { localMessage.setQuotedMessageId(replyMessageId); } (localMessage as any).quotedMessage = currentReplyMessage.message; (localMessage as any).quotedMessageId = replyMessageId; const currentMetadata = localMessage.getMetadata() || {}; localMessage.setMetadata({ ...currentMetadata, quotedMessage: currentReplyMessage.message, quotedMessageId: replyMessageId }); } } else if (parentMessageId) { localMessage.setParentMessageId(parentMessageId as number); } localMessage.setData({ type: messageType, category: CometChat.CATEGORY_MESSAGE, name: messageInput["name"], file: messageInput, url: messageInput["uri"], sender: loggedInUser.current, attachments: [messageInput], entities: { sender: { entity: loggedInUser.current, }, }, }); CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: localMessage, status: messageStatus.inprogress, }); if (!disableSoundForOutgoingMessages) playAudio(); CometChat.sendMediaMessage(mediaMessage) .then((message: any) => { CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: message, status: messageStatus.success, }); if (replyMessageId) { CometChatMessageEvents.emit(CometChatMessageEvents.ccReplyToMessage, { message: message, status: messageStatus.success, }); } setReplyMessage(null); setWarningMessage(""); setShowRecordAudio(false); }) .catch((error: any) => { setShowRecordAudio(false); if (error?.code === "ERR_PERMISSION_DENIED") { onError && onError(error); // Set error in data.metaData where the message list receipt logic checks const currentData = localMessage.getData() || {}; localMessage.setData({ ...currentData, metaData: { error: true } }); localMessage.setMetadata({ error: true }); CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: localMessage, status: messageStatus.error, }); setReplyMessage(null); return; } onError && onError(error); // Set error in data.metaData where the message list receipt logic checks const currentData = localMessage.getData() || {}; localMessage.setData({ ...currentData, metaData: { error: true } }); localMessage.setMetadata({ error: true }); CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: localMessage, status: messageStatus.error, }); setReplyMessage(null); }); }; const startTyping = (endTypingTimeout?: any, typingMetadata?: any) => { //if typing is disabled if (disableTypingEvents) { return false; } //if typing is in progress, clear the previous timeout and set new timeout if (isTyping.current) { clearTimeout(isTyping.current); isTyping.current = null; } else { let metadata = typingMetadata || undefined; let typingNotification = new CometChat.TypingIndicator( chatWithId.current, chatWith.current, metadata ); CometChat.startTyping(typingNotification); } let typingInterval = endTypingTimeout || 500; isTyping.current = setTimeout(() => { endTyping(null, typingMetadata); }, typingInterval); return false; }; const endTyping = (event: any, typingMetadata: any) => { if (event) { event.persist(); } if (disableTypingEvents) { return false; } let metadata = typingMetadata || undefined; let typingNotification = new CometChat.TypingIndicator( chatWithId.current, chatWith.current, metadata ); CometChat.endTyping(typingNotification); clearTimeout(isTyping.current!); isTyping.current = null; return false; }; const SecondaryButtonViewElem = useMemo(() => { if (hideAttachmentButton || !actionSheetItems.length) return <>; return ( { Keyboard.dismiss(); setTimeout(() => setShowActionSheet(true), 50); }} icon={mergedComposerStyle.attachmentIcon as JSX.Element | ImageSourcePropType} iconStyle={mergedComposerStyle.attachmentIconStyle as ImageStyle} /> ); }, [mergedComposerStyle, actionSheetItems, hideAttachmentButton]); const RecordAudioButtonView = ({ icon, iconStyle, }: { icon: JSX.Element | ImageSourcePropType; iconStyle?: ImageStyle; }) => { return ( { Keyboard.dismiss(); setTimeout(() => setShowRecordAudio(true), 50); }} > ); }; const voiceRecoringButtonElem = useMemo(() => { const isAgenticUser = user?.getRole?.() === '@agentic'; return (hideVoiceRecordingButton || isAgenticUser) ? undefined : ( ); }, [hideVoiceRecordingButton, mergedComposerStyle, user]); const AuxiliaryButtonViewElem = useCallback(() => { const isAgenticUser = user?.getRole?.() === '@agentic'; if (AuxiliaryButtonView) return ; if (isAgenticUser) { return <>; } if (defaultAuxiliaryButtonOptions) return ( {defaultAuxiliaryButtonOptions} ); return <>; }, [defaultAuxiliaryButtonOptions, user, AuxiliaryButtonView, group, id, theme]); const DefaultAgentSendButtonView = useCallback( ({ isButtonDisabled, composerRef }: { isButtonDisabled: boolean; composerRef: any }) => ( ), [isStreaming, showStopButton] ); const SendButtonViewElem = useCallback(() => { if (hideSendButton) return <>; const isAgenticUserCheck = isAgenticUser(); if (isAgenticUserCheck) { const disabled = isStreaming || plainTextInput.current.trim().length === 0|| (messagePreview && !hasEdited) || isSendButtonDisabledForDelay; const SendButtonComponent = DefaultAgentSendButtonView; // Create a ref-like object that matches what CometChatSendButtonView expects const composerRef = { current: { sendTextMessage: () => { if (!disabled) { sendTextMessage(); } } } }; return ; } if (SendButtonView) return ; const disabled = isStreaming || plainText.trim().length === 0 || (messagePreview && !hasEdited); return ( ); }, [mergedComposerStyle, inputMessage, plainText, messagePreview, hasEdited, isStreaming, DefaultAgentSendButtonView, user, isAgenticUser, sendTextMessage, hideSendButton, SendButtonView, isSendButtonDisabledForDelay]); //fetch logged in user useEffect(() => { CometChat.getLoggedinUser().then((user) => (loggedInUser.current = user)); let _formatter = [...(textFormatters || [])]; if (!disableMentions) { let mentionsFormatter = ChatConfigurator.getDataSource().getMentionsFormatter(); mentionsFormatter.setLoggedInUser(CometChatUIKit.loggedInUser!); mentionsFormatter.setContext("composer"); mentionsFormatter.setMentionsStyle( mergedComposerStyle.mentionsStyle as CometChatTheme["mentionsStyle"] ); mentionsFormatter.setTargetElement(MentionsTargetElement.textinput); if (mentionAllLabel) mentionsFormatter.setMentionAllLabel(mentionAllLabel); mentionsFormatter.setDisableMentionAll(disableMentionAll); if (user) mentionsFormatter.setUser(user); if (group) mentionsFormatter.setGroup(group); _formatter.unshift(mentionsFormatter); } _formatter.forEach((formatter) => { formatter.setComposerId(id!); if (user) formatter.setUser(user); if (group) formatter.setGroup(group); let trackingCharacter = formatter.getTrackingCharacter(); trackingCharacters.current.push(trackingCharacter); let newFormatter = CommonUtils.clone(formatter); allFormatters.current.set(trackingCharacter, newFormatter); }); }, []); useEffect(() => { //update receiver user if (user && user.getUid()) { chatRef.current = { chatWith: ReceiverTypeConstants.user, chatWithId: user.getUid(), }; chatWith.current = ReceiverTypeConstants.user; chatWithId.current = user.getUid(); } else if (group && group.getGuid()) { chatRef.current = { chatWith: ReceiverTypeConstants.group, chatWithId: group.getGuid(), }; chatWith.current = ReceiverTypeConstants.group; chatWithId.current = group.getGuid(); } }, [user, group, chatRef]); const handleOnClick = (CustomView: any) => { let view = CustomView( user, group, { uid: user?.getUid(), guid: group?.getGuid(), parentMessageId: parentMessageId, }, { onClose: () => setIsVisible(false), } ); bottomSheetRef.current?.togglePanel(); setShowActionSheet(false); setTimeout(() => { setCustomView(() => view); setIsVisible(true); }, 200); }; useEffect(() => { const defaultAttachmentOptions = ChatConfigurator.dataSource.getAttachmentOptions( theme, user, group, composerIdMap, { hideCameraOption, hideImageAttachmentOption, hideVideoAttachmentOption, hideAudioAttachmentOption, hideFileAttachmentOption, hidePollsAttachmentOption, hideCollaborativeDocumentOption, hideCollaborativeWhiteboardOption, replyToMessage: replyMessage?.message, closeReplyPreview: () => setReplyMessage(null), } ); setActionSheetItems(() => attachmentOptions && typeof attachmentOptions === "function" ? attachmentOptions({ user, group, composerId: composerIdMap })?.map((item) => { if (typeof item.CustomView === "function") return { ...item, onPress: () => handleOnClick(item.CustomView), }; if (typeof item.onPress == "function") return { ...item, onPress: () => { setShowActionSheet(false); item.onPress?.(user, group); }, }; return { ...item, onPress: () => fileInputHandler(item.id), }; }) : [ ...defaultAttachmentOptions.map((item: any) => { if (typeof item.CustomView === "function") return { ...item, onPress: () => handleOnClick(item.CustomView), }; if (typeof item.onPress === "function") return { ...item, onPress: () => { setShowActionSheet(false); item.onPress?.(user, group); }, }; return { ...item, onPress: () => fileInputHandler(item.id), }; }), ...(addAttachmentOptions && typeof addAttachmentOptions === "function" ? addAttachmentOptions({ user, group, composerId: composerIdMap })?.map((item) => { if (typeof item.CustomView === "function") return { ...item, onPress: () => handleOnClick(item.CustomView), }; if (typeof item.onPress == "function") return { ...item, onPress: () => { setShowActionSheet(false); item.onPress?.(user, group); }, }; return { ...item, onPress: () => fileInputHandler(item.id), }; }) : []), ] ); }, [ user, group, id, parentMessageId, hideCameraOption, hideImageAttachmentOption, hideVideoAttachmentOption, hideAudioAttachmentOption, hideFileAttachmentOption, hidePollsAttachmentOption, hideCollaborativeDocumentOption, hideCollaborativeWhiteboardOption, addAttachmentOptions, replyMessage, ]); useEffect(() => { CometChatUIEventHandler.addMessageListener(editMessageListenerID, { ccMessageEdited: (item: any) => { const incomingParentId = item?.message?.getParentMessageId?.() ?? null; const myParentId = parentMessageId ?? null; if (incomingParentId === myParentId) { previewMessage(item); } }, }); CometChatMessageEvents.addListener( CometChatMessageEvents.ccReplyToMessage, replyMessageListenerID, (data: any) => { if (data.status === messageStatus.inprogress) { previewReplyMessage(data.message); } else if (data.status === messageStatus.success) { } } ); CometChatUIEventHandler.addUIListener(UiEventListenerID, { ccToggleBottomSheet: (item) => { if (item?.bots) { // let newAiOptions = _getAIOptions(item.bots); // setAIOptionItems(newAiOptions); // setShowAIOptions(true); return; } else if (item?.botView) { setCustomView(() => item.child); return; } setIsVisible(false); bottomSheetRef.current?.togglePanel(); }, ccComposeMessage: (text) => { setIsVisible(false); bottomSheetRef.current?.togglePanel(); inputValueRef.current = text?.text; setInputMessage(text?.text); plainTextInput.current = text?.text || ""; setPlainText(text?.text || ""); }, ccSuggestionData(item: { id: string | number; data: Array }) { if (activeCharacter.current && id === item?.id) { const warningView = getMentionLimitView(); if (warningView) { return; } setMentionsSearchData(item?.data); setSuggestionListLoader(false); } }, }); return () => { CometChatUIEventHandler.removeMessageListener(editMessageListenerID); CometChatMessageEvents.removeListener(CometChatMessageEvents.ccReplyToMessage, replyMessageListenerID); CometChatUIEventHandler.removeUIListener(UiEventListenerID); }; }, []); const handlePannel = (item: any) => { if (item.child) { if (item.alignment === ViewAlignment.composerTop) setCustomViewHeader(() => item.child); else if (item.alignment === ViewAlignment.composerBottom) setCustomViewFooter(() => item.child); } else { if (item.alignment === ViewAlignment.composerTop) setCustomViewHeader(null); else if (item.alignment === ViewAlignment.composerBottom) setCustomViewFooter(undefined); } }; useEffect(() => { CometChatUIEventHandler.addUIListener(uiEventListenerShow, { showPanel: (item) => handlePannel(item), }); CometChatUIEventHandler.addUIListener(uiEventListenerHide, { hidePanel: (item) => handlePannel(item), }); return () => { CometChatUIEventHandler.removeUIListener(uiEventListenerShow); CometChatUIEventHandler.removeUIListener(uiEventListenerHide); }; }, []); const _sendRecordedAudio = (recordedFile: String) => { let fileObj = { name: "audio-recording" + recordedFile.split("/audio-recording")[1], type: "audio/mp4", uri: recordedFile, }; // console.log("fileObj", fileObj); // Removed debug log sendMediaMessage(chatWithId.current, fileObj, MessageTypeConstants.audio, chatWith.current); // console.log("Send Audio"); // Removed debug log }; function shouldOpenList( selection: { start: number; end: number; }, searchString: string, tracker: string ) { return ( selection.start === selection.end && !isCursorWithinMentionRange(mentionMap.current, selection.start - searchString.length) && trackingCharacters.current.includes(tracker) && (searchString === "" ? (plainTextInput.current[selection.start - 2]?.length === 1 && plainTextInput.current[selection.start - 2]?.trim()?.length === 0) || plainTextInput.current[selection.start - 2] === undefined : true) && (plainTextInput.current[selection.start - 1]?.length === 1 && plainTextInput.current[selection.start - 1]?.trim()?.length === 0 ? searchString.length > 0 : true) ); } let timeoutId: ReturnType; const openList = (selection: { start: number; end: number }) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { let searchString = extractTextFromCursor(plainTextInput.current, selection.start); let tracker = searchString ? plainTextInput.current[selection.start - (searchString.length + 1)] : plainTextInput.current[selection.start - 1]; if (shouldOpenList(selection, searchString, tracker)) { activeCharacter.current = tracker; searchStringRef.current = searchString; // Show the suggestion list immediately (even while data is loading) to avoid layout jumps setShowMentionList(true); setSuggestionListLoader(true); let formatter = allFormatters.current.get(tracker); if (formatter instanceof CometChatMentionsFormatter) { let shouldShowMentionList = formatter.getVisibleIn() === MentionsVisibility.both || (formatter.getVisibleIn() === MentionsVisibility.usersConversationOnly && user) || (formatter.getVisibleIn() === MentionsVisibility.groupsConversationOnly && group); if (shouldShowMentionList) { formatter?.search(searchString); } } else { formatter?.search(searchString); } } else { activeCharacter.current = ""; searchStringRef.current = ""; setShowMentionList(false); setMentionsSearchData([]); setSuggestionListLoader(false); } }, 100); }; const getRegexString = (str: string) => { // Get an array of the entries in the map using the spread operator const entries = [...mentionMap.current.entries()].reverse(); let uidInput = str; // Iterate over the array in reverse order entries.forEach(([key, value]) => { let [start, end] = key.split("_").map(Number); let pre = uidInput.substring(0, start); let post = uidInput.substring(end); uidInput = pre + value.underlyingText + post; }); return uidInput; }; const getPlainString = ( str: string, edits: Array<{ endIndex: number; replacement: string; startIndex: number; user: SuggestionItem; }> ) => { // Get an array of the entries in the map using the spread operator const entries = [...edits].reverse(); let _plainString = str; // Iterate over the array in reverse order entries.forEach(({ endIndex, replacement, startIndex, user }) => { let pre = _plainString.substring(0, startIndex); let post = _plainString.substring(endIndex); _plainString = pre + replacement + post; }); return _plainString; }; const parseMentionKey = (key: string): { start: number; end: number } | undefined => { const [startStr, endStr] = key.split("_"); const start = Number(startStr); const end = Number(endStr); const isValid = Number.isFinite(start) && Number.isFinite(end); if (typeof __DEV__ !== "undefined" && __DEV__ && !isValid) { throw new Error(`Invalid mention key: "${key}" (expected "start_end")`); } return isValid ? { start, end } : undefined; }; const calcDeletionRange = ( selection: { start: number; end: number }, deletionLength: number ) => { return selection.start === selection.end ? { start: Math.max(0, selection.start - deletionLength), end: selection.start, } : { start: selection.start, end: selection.end }; }; const collectOverlappingMentions = ( range: { start: number; end: number }, mentionMap: Map ): MentionOverlap[] => { const overlaps: MentionOverlap[] = []; mentionMap.forEach((value, key) => { const mentionRange = parseMentionKey(key); if (!mentionRange) return; const { start, end } = mentionRange; // compare against the *deletion* range, not the mention itself if (range.start < end && range.end > start) { overlaps.push({ key, value, start, end }); } }); overlaps.sort((a, b) => a.start - b.start); return overlaps; }; const removeMentionsFromTextAndMap = ( text: string, overlaps: MentionOverlap[], map: Map ) => { let adjustment = 0; let newText = text; overlaps.forEach(({ key, value, start, end }) => { const adjStart = start + adjustment; const adjEnd = end + adjustment; newText = newText.slice(0, adjStart) + newText.slice(adjEnd); map.delete(key); adjustment -= adjEnd - adjStart; // keep formatter in sync if (!ifIdExists(value.id, map)) { const fmt = allFormatters.current.get(value.trackingCharacter!); if (fmt instanceof CometChatMentionsFormatter) { const users = fmt.getSuggestionItems().filter((u) => u.id !== value.id); fmt.setSuggestionItems(users); if (!getMentionLimitView(fmt)) setWarningMessage(""); } } }); return { newText, totalShift: adjustment }; }; const shiftRemainingMentionKeys = ( map: Map, shiftStart: number, delta: number ) => { if (delta === 0) return; const shifted = new Map(); map.forEach((val, key) => { const range = parseMentionKey(key); if (!range) { // key was malformed → skip or handle as you wish return; } const { start, end } = range; if (start > shiftStart) { shifted.set(`${start + delta}_${end + delta}`, val); } else { shifted.set(key, val); } }); map.clear(); shifted.forEach((v, k) => map.set(k, v)); }; const deleteMentionHelper = (oldText: string, newText: string) => { const deletionLen = oldText.length - newText.length; const range = calcDeletionRange(selectionPosition, deletionLen); const deletionMentionMap = new Map(mentionMap.current); const overlaps = collectOverlappingMentions(range, deletionMentionMap); if (overlaps.length === 0) return false; const { newText: finalText, totalShift } = removeMentionsFromTextAndMap( oldText, overlaps, deletionMentionMap ); shiftRemainingMentionKeys(deletionMentionMap, range.start, totalShift); plainTextInput.current = finalText; setPlainText(finalText); onTextChange?.(finalText); mentionMap.current = deletionMentionMap; const firstStart = overlaps[0].start; InteractionManager.runAfterInteractions(() => { messageInputRef.current?.setNativeProps({ selection: { start: firstStart, end: firstStart }, }); }); setFormattedInputMessage(); return true; }; const textChangeHandler = (txt: string) => { if (messagePreview) { setHasEdited(txt.trim() !== originalText.trim()); } const oldText = plainTextInput.current ?? ""; const newText = txt; startTyping(); // Check if this is a deletion if (oldText.length > newText.length) { const handled = deleteMentionHelper(oldText, newText); if (handled) return; } // Existing handling for non-deletion cases const removing = (plainTextInput.current?.length ?? 0) > txt.length; const adding = plainTextInput.current?.length < txt.length; const textDiff = txt.length - (plainTextInput.current?.length ?? 0); const notAtLast = selectionPosition.start + textDiff < txt.length; let decr = 0; plainTextInput.current = newText; setPlainText(newText); const newMentionMap: Map = new Map(mentionMap.current); mentionMap.current.forEach((value, key) => { const range = parseMentionKey(key); if (!range) { // key was malformed → skip or handle as you wish return; } const { start, end } = range; let position = { start, end }; if ( notAtLast && (selectionPosition.start - 1 <= position.start || selectionPosition.start - textDiff <= position.start) ) { if (removing) { decr = selectionPosition.end - selectionPosition.start - textDiff; position.start -= decr; position.end -= decr; } else if (adding) { decr = selectionPosition.end - selectionPosition.start + textDiff; position.start += decr; position.end += decr; } if (removing || adding) { const newKey = `${position.start}_${position.end}`; if (position.start >= 0) newMentionMap.set(newKey, value); newMentionMap.delete(key); } } /* delete mention that was edited/over-typed */ const expected = plainTextInput.current?.substring(position.start, position.end); if (expected !== value.promptText) { newMentionMap.delete(`${position.start}_${position.end}`); if (!ifIdExists(value.id, newMentionMap)) { const targetedFormatter = allFormatters.current.get(value.trackingCharacter!); if (!targetedFormatter) return; const users = [...targetedFormatter.getSuggestionItems()]; const idx = users.findIndex((u) => u.id === value.id); if (idx !== -1) users.splice(idx, 1); if (targetedFormatter instanceof CometChatMentionsFormatter) { targetedFormatter.setSuggestionItems(users); const warn = getMentionLimitView(targetedFormatter); if (!warn) setWarningMessage(""); } } } }); mentionMap.current = newMentionMap; onTextChange?.(plainTextInput.current); setFormattedInputMessage(); }; const onMentionPress = (item: SuggestionItem) => { setShowMentionList(false); setMentionsSearchData([]); let notAtLast = selectionPosition.start < (plainTextInput.current?.length ?? 0); let textDiff = (plainTextInput.current?.length ?? 0) + (item.promptText?.length ?? 0) - searchStringRef.current.length - (plainTextInput.current?.length ?? 0); let incr = 0; let mentionPos = 0; let newMentionMap = new Map(mentionMap.current); let targetedFormatter = allFormatters.current.get(activeCharacter.current); if (!targetedFormatter) return; let existingCCUsers = [...targetedFormatter.getSuggestionItems()]; let userAlreadyExists = existingCCUsers.find( (existingUser: SuggestionItem) => existingUser.id === item.id ); if (!userAlreadyExists) { let cometchatUIUserArray: Array = [...existingCCUsers]; cometchatUIUserArray.push(item); (targetedFormatter as CometChatMentionsFormatter).setSuggestionItems(cometchatUIUserArray); } mentionMap.current.forEach((value, key) => { let position = { start: parseInt(key.split("_")[0]), end: parseInt(key.split("_")[1]) }; if (!(selectionPosition.start <= position.start)) { mentionPos += 1; } if ( position.end === selectionPosition.end || (selectionPosition.start > position.start && selectionPosition.end <= position.end) ) { let newKey = `${position.start}_${position.end}`; newMentionMap.delete(newKey); mentionPos -= 1; } if (notAtLast && selectionPosition.start - 1 <= position.start) { incr = selectionPosition.end - selectionPosition.start + textDiff; let newKey = `${position.start + incr}_${position.end + incr}`; newMentionMap.set(newKey, value); newMentionMap.delete(key); } }); mentionMap.current = newMentionMap; // When updating the input text, just get the latest plain text input and replace the selected text with the new mention const updatedPlainTextInput = `${plainTextInput.current?.substring( 0, selectionPosition.start - (1 + searchStringRef.current.length) )}${item.promptText + " "}${plainTextInput.current?.substring( selectionPosition.end, plainTextInput.current?.length )}`; plainTextInput.current = updatedPlainTextInput; let key = selectionPosition.start - (1 + searchStringRef.current.length) + "_" + (selectionPosition.start - (searchStringRef.current.length + 1) + (item.promptText?.length ?? 0)); let updatedMap = insertMentionAt(mentionMap.current, mentionPos, key, { ...item, trackingCharacter: activeCharacter.current, }); mentionMap.current = updatedMap; // Calculate cursor position after the mention + space // Add 1 to account for the space character added after the mention const newCursorPosition = selectionPosition.start - (searchStringRef.current.length + 1) + (item.promptText?.length ?? 0) + 1; // +1 for the space after mention // On iOS, use InteractionManager to ensure selection is applied after UI updates if (Platform.OS === "ios") { InteractionManager.runAfterInteractions(() => { setSelectionPosition({ start: newCursorPosition, end: newCursorPosition, }); }); } else { setSelectionPosition({ start: newCursorPosition, end: newCursorPosition, }); } setFormattedInputMessage(); }; const setFormattedInputMessage = () => { let textComponents: any = getRegexString(plainTextInput.current); allFormatters.current.forEach((formatter: CometChatTextFormatter, key) => { let resp = formatter.getFormattedText(textComponents); textComponents = resp; }); inputValueRef.current = textComponents; setInputMessage(textComponents); }; function escapeRegExp(string: string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function extractTextFromCursor(inputText: string, cursorPosition: number) { const leftText = inputText.substring(0, cursorPosition); // Escape the mentionPrefixes to safely use them in a regex pattern const escapedPrefixes = trackingCharacters.current.map(escapeRegExp).join("|"); // Build a dynamic regex pattern that matches any of the mention prefixes. // This pattern will match a prefix followed by any combination of word characters // and spaces, including a trailing space. const mentionRegex = new RegExp( `(?:^|\\s)(${escapedPrefixes})([^${escapedPrefixes}\\s][^${escapedPrefixes}]*)$` ); const match = leftText.match(mentionRegex); // If a match is found, return the first capturing group, which is the username return (match && substringUpToNthSpace(match[2], 4)) || ""; } function substringUpToNthSpace(str: string, n: number) { // Split the string by spaces, slice to the (n-1) elements, and then rejoin with spaces return str.split(" ", n).join(" "); } const insertMentionAt = ( mentionMap: Map, insertAt: number, key: string, value: SuggestionItem ): Map => { // Convert the hashmap to an array of [key, value] pairs let mentionsArray = Array.from(mentionMap); // Insert the new mention into the array at the calculated index mentionsArray.splice(insertAt, 0, [key, value]); return new Map(mentionsArray); }; /** * Function to check if the id exists in the mentionMap */ const ifIdExists = (id: string, hashmap: Map) => { let exists = false; hashmap.forEach((value, key) => { if (value.id === id) { exists = true; } }); return exists; }; const onSuggestionListEndReached = () => { let targetedFormatter = allFormatters.current.get(activeCharacter.current); if (!targetedFormatter) return; let fetchingNext = targetedFormatter.fetchNext(); fetchingNext !== null && setSuggestionListLoader(true); }; const getMentionLimitView = (targettedFormatterParam?: CometChatMentionsFormatter) => { let targetedFormatter = allFormatters.current.get(activeCharacter.current) ?? targettedFormatterParam; if (!(targetedFormatter instanceof CometChatMentionsFormatter)) { return false; } let shouldWarn; let limit; if (targetedFormatter?.getLimit && targetedFormatter?.getLimit()) { limit = targetedFormatter?.getLimit(); if ( targetedFormatter.getUniqueUsersList && targetedFormatter.getUniqueUsersList()?.size >= limit ) { shouldWarn = true; } } if (!shouldWarn) { setWarningMessage(""); return false; } setWarningMessage( targetedFormatter?.getErrorString ? targetedFormatter?.getErrorString() : `${t("MENTION_UPTO")} ${limit} ${ limit === 1 ? t("TIME") : t("TIMES") } ${t("AT_A_TIME")}.` ); return true; }; return ( <> {/* {!isVisible && typeof CustomView === "function" && } TODOM */} { setIsVisible(false); }} > {CustomView && CustomView} setShowActionSheet(false)} style={mergedComposerStyle.attachmentOptionsStyles} /> { setShowRecordAudio(false); }} cometChatBottomSheetStyle={{ maxHeight: Dimensions.get("window").height * 0.4, }} onSend={_sendRecordedAudio} mediaRecorderStyle={mergedComposerStyle.mediaRecorderStyle} /> {showMentionList && (plainTextInput.current?.length ?? 0) > 0 && mentionsSearchData.length > 0 && ( )} {HeaderView ? HeaderView({ user, group }) : CustomViewHeader && (typeof CustomViewHeader === "function" ? ( // Invoke CustomViewHeader if it's a functional component ) : ( CustomViewHeader // Render it directly if it's a React node ))} {messagePreview && ( { setMessagePreview(null); clearEditPreview(); mentionMap.current = new Map(); plainTextInput.current = ""; setOriginalText(""); }} style={{ borderRadius: 8, borderWidth: 0, borderLeftWidth: 3, borderLeftColor: theme.color.borderHighlight, margin:theme.spacing.padding.p1, }} /> )} {replyMessage && replyMessage.message && ( { setReplyMessage(null); }} titleStyle={{ color: theme.color.textHighlight }} style={{ borderRadius: 8, borderLeftWidth: 3, borderLeftColor: theme.color.borderHighlight, margin:theme.spacing.padding.p1, }} /> )} { const cursorPos = selection.start; const mentionRange = getMentionRangeAtCursor(mentionMap.current, cursorPos); if (mentionRange) { console.log('Mention boundary positions:', mentionRange); const distanceToStart = cursorPos - mentionRange.start; const distanceToEnd = mentionRange.end - cursorPos; let targetPosition: number; if (distanceToStart <= distanceToEnd) { targetPosition = mentionRange.start; } else { targetPosition = mentionRange.end; } if (targetPosition !== cursorPos) { InteractionManager.runAfterInteractions(() => { setSelectionPosition({ start: targetPosition, end: targetPosition }); }); return; } } setSelectionPosition(selection); openList(selection); }} selection={selectionPosition} onChangeText={textChangeHandler} VoiceRecordingButtonView={voiceRecoringButtonElem} SecondaryButtonView={SecondaryButtonViewElem} AuxiliaryButtonView={AuxiliaryButtonViewElem()} PrimaryButtonView={SendButtonViewElem} auxiliaryButtonAlignment={auxiliaryButtonsAlignment} /> {CustomViewFooter ? ( // If CustomViewFooter is a function component (React.FC) typeof CustomViewFooter === "function" ? ( // Invoke the function component ) : ( CustomViewFooter // Render it directly if it's a React node (JSX or element) ) ) : null} ); } );