@cometchat/chat-uikit-react-native
Version:
Ready-to-use Chat UI Components for React Native
1,047 lines (1,045 loc) • 50.9 kB
JavaScript
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")}