UNPKG

@cometchat/chat-uikit-react-native

Version:

Ready-to-use Chat UI Components for React Native

1,047 lines (1,045 loc) 50.9 kB
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react"; import { Dimensions, Image, KeyboardAvoidingView, Modal, NativeModules, Platform, Text, TouchableOpacity, View, } from "react-native"; import { CometChatActionSheet, CometChatBottomSheet, CometChatMentionsFormatter, CometChatMessageInput, CometChatMessagePreview, CometChatSuggestionList, CometChatUIKit, localize, } 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 { Icon } from "../shared/icons/Icon"; import { getUnixTimestampInMilliseconds, messageStatus, } from "../shared/utils/CometChatMessageHelper"; import { CommonUtils } from "../shared/utils/CommonUtils"; import { permissionUtil } from "../shared/utils/PermissionUtil"; import { CometChatMediaRecorder } from "../shared/views/CometChatMediaRecorder"; import { useTheme } from "../theme"; import { ICONS } from "./resources"; import { deepMerge } from "../shared/helper/helperFunctions"; const { FileManager, CommonUtil } = NativeModules; const uiEventListenerShow = "uiEvent_show_" + new Date().getTime(); const uiEventListenerHide = "uiEvent_hide_" + new Date().getTime(); const MessagePreviewTray = (props) => { const { shouldShow = false, text = "", onClose = () => { } } = props; return shouldShow ? (<CometChatMessagePreview messagePreviewTitle={localize("EDIT_MESSAGE")} messagePreviewSubtitle={text} closeIconURL={ICONS.CLOSE} onCloseClick={onClose}/>) : null; }; const ImageButton = (props) => { const { image, onClick, buttonStyle, imageStyle, disable } = props; return (<TouchableOpacity activeOpacity={disable ? 1 : undefined} onPress={disable ? () => { } : onClick} style={buttonStyle}> <Image source={image} style={[{ height: 24, width: 24 }, imageStyle]}/> </TouchableOpacity>); }; const AttachIconButton = (props) => { return (<TouchableOpacity onPress={props.onPress}> <Icon name='add-circle' icon={props.icon} color={props.iconStyle.tintColor} height={props.iconStyle.height} width={props.iconStyle.width} imageStyle={props.iconStyle}/> </TouchableOpacity>); }; const ActionSheetBoard = (props) => { const { shouldShow = false, onClose = () => { }, options = [], sheetRef, style } = props; return (<CometChatBottomSheet style={{ maxHeight: Dimensions.get("window").height * 0.49 }} ref={sheetRef} onClose={onClose} isOpen={shouldShow}> <CometChatActionSheet actions={options} style={style}/> </CometChatBottomSheet>); }; const RecordAudio = (props) => { const { shouldShow = false, onClose = () => { }, options = [], cometChatBottomSheetStyle = {}, sheetRef, onPause = () => { }, onPlay = () => { }, onSend = (recordedFile) => { }, onStop = (recordedFile) => { }, onStart = () => { }, mediaRecorderStyle, ...otherProps } = props; return (<CometChatBottomSheet ref={sheetRef} onClose={onClose} style={cometChatBottomSheetStyle} isOpen={shouldShow}> <CometChatMediaRecorder onClose={onClose} onPause={onPause} onPlay={onPlay} onSend={onSend} onStop={onStop} onStart={onStart} style={mediaRecorderStyle}/> </CometChatBottomSheet>); }; export const CometChatMessageComposer = React.forwardRef((props, ref) => { const editMessageListenerID = "editMessageListener_" + new Date().getTime(); const UiEventListenerID = "UiEventListener_" + new Date().getTime(); const theme = useTheme(); const { id, user, group, disableSoundForOutgoingMessages = true, customSoundForOutgoingMessage, disableTypingEvents, initialComposertext, HeaderView, onTextChange, attachmentOptions, AuxiliaryButtonView, SendButtonView, parentMessageId, style = {}, onSendButtonPress, onError, hideVoiceRecordingButton, keyboardAvoidingViewProps, textFormatters, disableMentions, imageQuality = 20, hideCameraOption = false, hideImageAttachmentOption = false, hideVideoAttachmentOption = false, hideAudioAttachmentOption = false, hideFileAttachmentOption = false, hidePollsAttachmentOption = false, hideCollaborativeDocumentOption = false, hideCollaborativeWhiteboardOption = false, hideAttachmentButton = false, hideStickersButton = false, hideSendButton = false, hideAuxiliaryButtons = false, addAttachmentOptions, auxiliaryButtonsAlignment = "left", } = props; const composerIdMap = new Map().set("parentMessageId", parentMessageId); const mergedComposerStyle = useMemo(() => { return deepMerge(theme.messageComposerStyles, style); }, [theme, style]); const defaultAuxiliaryButtonOptions = useMemo(() => { if (hideAuxiliaryButtons) return []; return ChatConfigurator.getDataSource().getAuxiliaryOptions(user, group, composerIdMap, { stickerIcon: mergedComposerStyle.stickerIcon, stickerIconStyle: mergedComposerStyle.stickerIconStyle, hideStickersButton, }); }, [mergedComposerStyle, hideStickersButton, hideAuxiliaryButtons]); 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(new Map()); let activeCharacter = React.useRef(""); let searchStringRef = React.useRef(""); const [selectionPosition, setSelectionPosition] = React.useState({}); const [inputMessage, setInputMessage] = React.useState(initialComposertext || ""); const [showActionSheet, setShowActionSheet] = React.useState(false); const [showRecordAudio, setShowRecordAudio] = React.useState(false); const [actionSheetItems, setActionSheetItems] = React.useState([]); const [messagePreview, setMessagePreview] = React.useState(); 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 bottomSheetRef = React.useRef(null); useLayoutEffect(() => { if (Platform.OS === "ios") { if (Number.isInteger(commonVars.safeAreaInsets.top)) { setKbOffset(commonVars.safeAreaInsets.top ?? 0); return; } CommonUtil.getSafeAreaInsets().then((res) => { if (Number.isInteger(res.top)) { commonVars.safeAreaInsets.top = res.top; commonVars.safeAreaInsets.bottom = res.bottom; setKbOffset(res.top); } }); } }, []); const isTyping = useRef(null); /** * Event callback */ React.useImperativeHandle(ref, () => ({ previewMessageForEdit: previewMessage, })); useLayoutEffect(() => { if (warningMessage) { setCustomViewHeader(<View style={{ flexDirection: "row", alignItems: "center", padding: theme.spacing.padding.p2, borderRadius: mergedComposerStyle?.containerStyle?.borderRadius, backgroundColor: mergedComposerStyle?.containerStyle?.backgroundColor, borderColor: mergedComposerStyle?.containerStyle?.borderColor, borderWidth: mergedComposerStyle?.containerStyle?.borderWidth, marginBottom: 2, }}> <Icon name='info-fill' color={theme.color.error}></Icon> <Text style={{ marginLeft: 5, color: theme.color.error, ...theme.typography.caption1.regular, }}> {warningMessage} </Text> </View>); return; } setCustomViewHeader(null); }, [warningMessage, theme]); const previewMessage = ({ message, status }) => { if (status === messageStatus.inprogress) { let textComponents = message?.text; let rawText = message?.text; let users = {}; let regexes = []; allFormatters.current.forEach((formatter, 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 = []; regexes.forEach((regex) => { let match; 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, b) => a.startIndex - b.startIndex); plainTextInput.current = getPlainString(message?.text, edits); const hashMap = new Map(); let offset = 0; // Tracks shift in position due to replacements edits.forEach((edit) => { 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, text: textComponents }, mode: ConversationOptionConstants.edit, }); inputValueRef.current = textComponents ?? ""; setInputMessage(textComponents ?? ""); messageInputRef.current.focus(); } }; const cameraCallback = async (cameraImage) => { 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) => { 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) => { if (file.uri) sendMediaMessage(chatWithId.current, file, MessageTypeConstants.video, chatWith.current); }); } else FileManager.openFileChooser(fileType, async (fileInfo) => { 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 = ""; setInputMessage(""); setWarningMessage(""); }; const sendTextMessage = () => { //ignore sending new message if (messagePreview != null) { editMessage(messagePreview.message); return; } let finalTextInput = getRegexString(plainTextInput.current); // Trim the trailing spaces and store in a variable let trimmedTextInput = finalTextInput.trim(); // Create the text message with the trimmed text let textMessage = new CometChat.TextMessage(chatWithId.current, trimmedTextInput, chatWith.current); textMessage.setSender(loggedInUser.current); textMessage.setReceiver(chatWith.current); // Use the trimmed text here as well textMessage.setText(trimmedTextInput); textMessage.setMuid(String(getUnixTimestampInMilliseconds())); parentMessageId && textMessage.setParentMessageId(parentMessageId); allFormatters.current.forEach((item) => { textMessage = item.handlePreMessageSend(textMessage); }); setMentionsSearchData([]); plainTextInput.current = ""; if (trimmedTextInput.trim().length === 0) { return; } clearInputBox(); if (onSendButtonPress) { onSendButtonPress(textMessage); return; } CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: textMessage, status: messageStatus.inprogress, }); if (!disableSoundForOutgoingMessages) playAudio(); CometChat.sendMessage(textMessage) .then((message) => { CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: message, status: messageStatus.success, }); }) .catch((error) => { onError && onError(error); textMessage.setMetadata({ error: true }); CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: textMessage, status: messageStatus.error, }); clearInputBox(); }); }; /** edit message */ const editMessage = (message) => { 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); inputValueRef.current = ""; clearInputBox(); messageInputRef.current.textContent = ""; setMessagePreview(null); if (onSendButtonPress) { onSendButtonPress(textMessage); return; } if (!disableSoundForOutgoingMessages) playAudio(); CometChat.editMessage(textMessage) .then((editedMessage) => { inputValueRef.current = ""; setInputMessage(""); CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageEdited, { message: editedMessage, status: messageStatus.success, }); }) .catch((error) => { onError && onError(error); }); }; /** send media message */ const sendMediaMessage = (receiverId, messageInput, messageType, receiverType) => { 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, }); parentMessageId && mediaMessage.setParentMessageId(parentMessageId); 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, }); parentMessageId && localMessage.setParentMessageId(parentMessageId); localMessage.setData({ type: messageType, category: CometChat.CATEGORY_MESSAGE, name: messageInput["name"], file: messageInput, url: messageInput["uri"], sender: loggedInUser.current, attachments: [messageInput], }); CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: localMessage, status: messageStatus.inprogress, }); if (!disableSoundForOutgoingMessages) playAudio(); CometChat.sendMediaMessage(mediaMessage) .then((message) => { CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: message, status: messageStatus.success, }); setShowRecordAudio(false); }) .catch((error) => { setShowRecordAudio(false); onError && onError(error); localMessage.setMetadata({ error: true }); CometChatUIEventHandler.emitMessageEvent(MessageEvents.ccMessageSent, { message: localMessage, status: messageStatus.error, }); console.log("media message sent error", error); }); }; const startTyping = (endTypingTimeout, typingMetadata) => { //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, typingMetadata) => { 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 (<AttachIconButton onPress={() => setShowActionSheet(true)} icon={mergedComposerStyle.attachmentIcon} iconStyle={mergedComposerStyle.attachmentIconStyle}/>); }, [mergedComposerStyle]); const RecordAudioButtonView = ({ icon, iconStyle, }) => { return (<TouchableOpacity onPress={() => setShowRecordAudio(true)}> <Icon name='mic' height={24} width={24} icon={icon} color={iconStyle?.tintColor ?? theme.color.iconSecondary} imageStyle={iconStyle}/> </TouchableOpacity>); }; const voiceRecoringButtonElem = useMemo(() => { return hideVoiceRecordingButton ? undefined : (<RecordAudioButtonView icon={mergedComposerStyle.voiceRecordingIcon} iconStyle={mergedComposerStyle.voiceRecordingIconStyle}/>); }, [hideVoiceRecordingButton, mergedComposerStyle]); const AuxiliaryButtonViewElem = useCallback(() => { if (AuxiliaryButtonView) return <AuxiliaryButtonView user={user} group={group} composerId={id}/>; else if (defaultAuxiliaryButtonOptions) return (<View style={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.spacing.s2, }}> {defaultAuxiliaryButtonOptions} </View>); return <></>; }, [defaultAuxiliaryButtonOptions]); const SendButtonViewElem = useCallback(() => { if (hideSendButton) return <></>; if (SendButtonView) return <SendButtonView user={user} group={group} composerId={id}/>; const disabled = typeof inputMessage === "string" && inputMessage.length === 0; return (<TouchableOpacity onPress={sendTextMessage} style={[ { borderRadius: theme.spacing.radius.max, padding: 4, backgroundColor: disabled ? theme.color.background4 : theme.color.fabButtonBackground, }, mergedComposerStyle.sendIconContainerStyle, ]} disabled={disabled}> <Icon name='send-fill' icon={mergedComposerStyle.sendIcon} color={(mergedComposerStyle.sendIconStyle?.tintColor ?? theme.color.fabButtonIcon)} imageStyle={mergedComposerStyle.sendIconStyle} height={24} width={24}/> </TouchableOpacity>); }, [mergedComposerStyle, inputMessage]); //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.setMentionsStyle(mergedComposerStyle.mentionsStyle); mentionsFormatter.setTargetElement(MentionsTargetElement.textinput); 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) => { 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, }); 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) => { 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, ]); useEffect(() => { CometChatUIEventHandler.addMessageListener(editMessageListenerID, { ccMessageEdited: (item) => previewMessage(item), }); 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); }, ccSuggestionData(item) { if (activeCharacter.current && id === item?.id) { const warningView = getMentionLimitView(); if (warningView) { return; } setMentionsSearchData(item?.data); setSuggestionListLoader(false); } }, }); return () => { CometChatUIEventHandler.removeMessageListener(editMessageListenerID); CometChatUIEventHandler.removeUIListener(UiEventListenerID); }; }, []); const handlePannel = (item) => { 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) => { let fileObj = { name: "audio-recording" + recordedFile.split("/audio-recording")[1], type: "audio/mp4", uri: recordedFile, }; console.log("fileObj", fileObj); sendMediaMessage(chatWithId.current, fileObj, MessageTypeConstants.audio, chatWith.current); console.log("Send Audio"); }; function isCursorWithinMentionRange(mentionRanges, cursorPosition) { for (let [range, mention] of mentionRanges) { const [start, end] = range.split("_").map(Number); if (cursorPosition >= start && cursorPosition <= end) { return true; // Cursor is within the range of a mention } } return false; // No mention found at the cursor position } function shouldOpenList(selection, searchString, tracker) { 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; const openList = (selection) => { 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; setShowMentionList(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([]); } }, 100); }; const getRegexString = (str) => { // 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, edits) => { // 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 textChangeHandler = (txt) => { let removing = plainTextInput.current?.length ?? 0 > txt.length; let adding = plainTextInput.current?.length < txt.length; let textDiff = txt.length - (plainTextInput.current?.length ?? 0); let notAtLast = selectionPosition.start + textDiff < txt.length; plainTextInput.current = txt; onTextChange && onTextChange(txt); startTyping(); let decr = 0; let newMentionMap = new Map(mentionMap.current); mentionMap.current.forEach((value, key) => { let position = { start: parseInt(key.split("_")[0]), end: parseInt(key.split("_")[1]) }; //Runs when cursor before the mention and before the last position if (notAtLast && (selectionPosition.start - 1 <= position.start || selectionPosition.start - textDiff <= position.start)) { if (removing) { decr = selectionPosition.end - selectionPosition.start - textDiff; position = { start: position.start - decr, end: position.end - decr }; } else if (adding) { decr = selectionPosition.end - selectionPosition.start + textDiff; position = { start: position.start + decr, end: position.end + decr }; } if (removing || adding) { let newKey = `${position.start}_${position.end}`; position.start >= 0 && newMentionMap.set(newKey, value); newMentionMap.delete(key); } } // Code to delete mention from hashmap 👇 let expctedMentionPos = plainTextInput.current?.substring(position.start, position.end); if (expctedMentionPos !== `${value.promptText}`) { let newKey = `${position.start}_${position.end}`; newMentionMap.delete(newKey); if (!ifIdExists(value.id, newMentionMap)) { let targetedFormatter = allFormatters.current.get(value.trackingCharacter); if (!targetedFormatter) return; let existingCCUsers = [...targetedFormatter.getSuggestionItems()]; let userPosition = existingCCUsers.findIndex((item) => item.id === value.id); if (userPosition !== -1) { existingCCUsers.splice(userPosition, 1); targetedFormatter.setSuggestionItems(existingCCUsers); } if (targetedFormatter instanceof CometChatMentionsFormatter) { let showWarning = getMentionLimitView(targetedFormatter); if (!showWarning) { setWarningMessage(""); } } } } }); mentionMap.current = newMentionMap; setFormattedInputMessage(); }; const onMentionPress = (item) => { 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) => existingUser.id === item.id); if (!userAlreadyExists) { let cometchatUIUserArray = [...existingCCUsers]; cometchatUIUserArray.push(item); targetedFormatter.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; } // Code to delete mention from hashmap 👇 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; setSelectionPosition({ start: selectionPosition.start - (searchStringRef.current.length + 1) + (item.promptText?.length ?? 0), end: selectionPosition.start - (searchStringRef.current.length + 1) + (item.promptText?.length ?? 0), }); setFormattedInputMessage(); }; const setFormattedInputMessage = () => { let textComponents = getRegexString(plainTextInput.current); allFormatters.current.forEach((formatter, key) => { let resp = formatter.getFormattedText(textComponents); textComponents = resp; }); inputValueRef.current = textComponents; setInputMessage(textComponents); }; function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function extractTextFromCursor(inputText, cursorPosition) { 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, n) { // Split the string by spaces, slice to the (n-1) elements, and then rejoin with spaces return str.split(" ", n).join(" "); } const insertMentionAt = (mentionMap, insertAt, key, value) => { // 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, hashmap) => { 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) => { 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() : `${localize("MENTION_UPTO")} ${limit} ${limit === 1 ? localize("TIME") : localize("TIMES")} ${localize("AT_A_TIME")}.`); return true; }; return (<> {/* {!isVisible && typeof CustomView === "function" && <CustomView />} TODOM */} <Modal transparent={true} // animationType='slide' visible={isVisible} onRequestClose={() => { setIsVisible(false); }}> {CustomView && CustomView} </Modal> <KeyboardAvoidingView key={id} behavior={Platform.OS === "ios" ? "padding" : "height"} keyboardVerticalOffset={Platform.select({ ios: kbOffset })} {...keyboardAvoidingViewProps}> <View style={[ Style.container, { paddingTop: CustomViewHeader ? theme.spacing.padding.p0 : theme.spacing.padding.p2, paddingHorizontal: theme.spacing.padding.p2, }, { backgroundColor: theme.messageListStyles.containerStyle.backgroundColor, }, mergedComposerStyle.containerStyle, ]}> <ActionSheetBoard sheetRef={bottomSheetRef} options={actionSheetItems} shouldShow={showActionSheet} onClose={() => setShowActionSheet(false)} style={mergedComposerStyle.attachmentOptionsStyles}/> <RecordAudio sheetRef={bottomSheetRef} options={actionSheetItems} shouldShow={showRecordAudio} onClose={() => { setShowRecordAudio(false); }} cometChatBottomSheetStyle={{ maxHeight: Dimensions.get("window").height * 0.4, }} onSend={_sendRecordedAudio} mediaRecorderStyle={mergedComposerStyle.mediaRecorderStyle}/> {mentionsSearchData.length > 0 && (plainTextInput.current?.length ?? 0) > 0 && (<View style={[ theme.mentionsListStyle.containerStyle, messagePreview ? { maxHeight: Dimensions.get("window").height * 0.2 } : {}, ]}> <CometChatSuggestionList data={mentionsSearchData} listStyle={theme.mentionsListStyle} onPress={onMentionPress} onEndReached={onSuggestionListEndReached} loading={suggestionListLoader}/> </View>)} <View style={[ { flexDirection: "column" }, mentionsSearchData.length ? { maxHeight: Dimensions.get("window").height * 0.2 } : { maxHeight: Dimensions.get("window").height * 0.3 }, ]}> {HeaderView ? HeaderView({ user, group }) : CustomViewHeader && (typeof CustomViewHeader === "function" ? (<CustomViewHeader /> // Invoke CustomViewHeader if it's a functional component ) : (CustomViewHeader // Render it directly if it's a React node ))} <MessagePreviewTray onClose={() => setMessagePreview(null)} text={messagePreview?.message?.text} shouldShow={messagePreview != null}/> </View> <CometChatMessageInput messageInputRef={messageInputRef} text={inputMessage} placeHolderText={localize("ENTER_YOUR_MESSAGE_HERE")}