softchatjs-react-native
Version:
React native UI SDK for softchatjs-core. Create a free account at: https://www.softchatjs.com
1 lines • 1.05 MB
Source Map (JSON)
{"version":3,"sources":["../../../src/components/Chat/index.tsx","../../../src/components/Chat/ChatItem/index.tsx","../../../src/assets/icons.tsx","../../../src/constants/Colors.ts","../../../src/helpers/haptics.ts","../../../src/components/Chat/ChatItem/Layouts/Stacked.tsx","../../../src/components/Chat/MessageAvatar.tsx","../../../src/contexts/ChatProvider.tsx","../../../src/contexts/ModalProvider.tsx","../../../src/theme/colors.ts","../../../src/theme/index.ts","../../../src/contexts/MessageStateContext.tsx","../../../src/constants/defaultUser.ts","../../../src/components/Chat/ChatItem/Sticker.tsx","../../../src/utils/index.ts","../../../src/components/Chat/ChatItem/Reactions.tsx","../../../src/components/Chat/ChatItem/Quoted.tsx","../../../src/components/Chat/ChatItem/Preview.tsx","../../../src/components/Chat/ChatItem/Media/index.tsx","../../../src/components/Modals/ImagePreview.tsx","../../../src/components/Chat/ChatInput.tsx","../../../src/components/Chat/ChatItem/Media/VoiceMessage.tsx","../../../src/components/Chat/ChatItem/Media/Video.tsx","../../../src/components/Modals/VideoViewer.tsx","../../../src/components/Chat/ChatItem/Layouts/Default.tsx","../../../src/components/Chat/ChatHeader.tsx","../../../src/components/Conversations/ConversationAvatar.tsx","../../../src/components/Chat/SelectedMessage.tsx","../../../src/components/Chat/EmojiSheet/index.tsx","../../../src/components/Search.tsx","../../../src/assets/emoji.ts","../../../src/components/Chat/MediaOptions/index.tsx","../../../src/components/Chat/MessageOptions/index.tsx","../../../src/components/Modals/EmojiList.tsx"],"sourcesContent":["import React, {\r\n MutableRefObject,\r\n RefObject,\r\n createRef,\r\n useCallback,\r\n useEffect,\r\n useRef,\r\n useState,\r\n} from \"react\";\r\nimport {\r\n View,\r\n StyleSheet,\r\n TextInput,\r\n Text,\r\n KeyboardAvoidingView,\r\n Platform,\r\n Dimensions,\r\n RefreshControl,\r\n} from \"react-native\";\r\nimport {\r\n ChatBubbleRenderProps,\r\n ChatHeaderRenderProps,\r\n ChatInputRenderProps,\r\n Children,\r\n AttachmentTypes,\r\n Prettify,\r\n BottomSheetRef,\r\n} from \"../../types\";\r\nimport { ChatItem } from \"./ChatItem\";\r\nimport { GestureHandlerRootView } from \"react-native-gesture-handler\";\r\nimport ChatInput, { METERING_MIN_POWER } from \"./ChatInput\";\r\nimport ChatHeader from \"./ChatHeader\";\r\nimport SelectedMessage from \"./SelectedMessage\";\r\nimport EmojiSheet from \"./EmojiSheet\";\r\nimport MediaOptions from \"./MediaOptions\";\r\nimport { restructureMessages } from \"../../utils\";\r\nimport MessageOptions from \"./MessageOptions\";\r\nimport Haptics from \"../../helpers/haptics\";\r\nimport { FlashList } from \"@shopify/flash-list\";\r\nimport { useConfig } from \"../../contexts/ChatProvider\";\r\nimport {\r\n ChatEventGenerics,\r\n ConnectionEvent,\r\n Events,\r\n generateId,\r\n Message,\r\n MediaType,\r\n ConversationListItem,\r\n} from \"softchatjs-core\";\r\nimport { format, isThisWeek } from \"date-fns\";\r\nimport { useMessageState } from \"../../contexts/MessageStateContext\";\r\nimport { LockIcon, MessagePlus } from \"../../assets/icons\";\r\nimport { Audio } from \"expo-av\";\r\nimport { interpolate } from \"react-native-reanimated\";\r\nimport { useModalProvider } from \"../../contexts/ModalProvider\";\r\nimport EmojiListModal from '../../components/Modals/EmojiList'\r\n\r\ntype ChatProps = {\r\n /**\r\n * Active conversation is the current conversation the user is actively engaged in\r\n */\r\n activeConversation: ConversationListItem;\r\n /**\r\n * Render a custom chat item\r\n */\r\n renderChatBubble?: (props: Prettify<ChatBubbleRenderProps>) => void;\r\n /**\r\n * Render a customer chat input\r\n */\r\n renderChatInput?: (props: Prettify<ChatInputRenderProps>) => void;\r\n /**\r\n * Render a custom chat header\r\n */\r\n renderChatHeader?: (props: Prettify<ChatHeaderRenderProps>) => void;\r\n /**\r\n * Render a custom empty state when a user has no messages\r\n */\r\n placeholder?: Children;\r\n /**\r\n * Value passed to adjust how the keyboard adjusts the input field when it's open\r\n */\r\n keyboardOffset?: number;\r\n};\r\n\r\nexport type SendMessage = {\r\n message: string;\r\n};\r\n\r\nexport type SelectedMessage = {\r\n message: Message | null;\r\n ref: MutableRefObject<View | undefined> | null;\r\n itemIndex: number;\r\n isMessageOwner: boolean;\r\n};\r\n\r\ntype GroupedMessages = Array<string | Message>;\r\n\r\nconst KeyboardAvoiding = (props: { keyboardOffset: number, children: Children }) => {\r\n if(Platform.OS === \"android\"){\r\n return <>{props.children}</>\r\n }\r\n return (\r\n <KeyboardAvoidingView\r\n style={{ flex: 1, height: '100%', width: '100%' }}\r\n behavior={Platform.OS === \"ios\" ? \"padding\" : \"height\"}\r\n keyboardVerticalOffset={props.keyboardOffset}\r\n >\r\n {props.children}\r\n </KeyboardAvoidingView>\r\n )\r\n}\r\n\r\nexport default function Chat(props: ChatProps) {\r\n const { client, theme, fontFamily, fontScale } = useConfig();\r\n const {\r\n activeConversation,\r\n renderChatBubble,\r\n renderChatInput,\r\n renderChatHeader,\r\n placeholder,\r\n keyboardOffset = Platform.OS === \"ios\" ? 10 : 0\r\n } = props;\r\n let layout: \"stacked\" | undefined\r\n const chatUserId = client.chatUserId;\r\n const conversationId = activeConversation.conversation.conversationId;\r\n const participantList = activeConversation.conversation.participantList;\r\n const participants = activeConversation.conversation.participants;\r\n const [permissionResponse, requestPermission] = Audio.usePermissions();\r\n const { displayModal } = useModalProvider();\r\n const scrollRef = useRef<FlashList<Message | string> | null>(null);\r\n const inputRef = useRef<TextInput>(null);\r\n const emojiListRef = useRef<BottomSheetRef>(null);\r\n const mediaOptionsRef = useRef<\r\n BottomSheetRef & { pickAttachment: () => void }\r\n >(null);\r\n const messageOptionsRef = useRef<BottomSheetRef>(null);\r\n const [isTyping, showTyping] = useState(false);\r\n const [loadingMessages, setLoadingMessages] = useState(false);\r\n const {\r\n globalTextMessage,\r\n setGlobalTextMessage,\r\n pendingMessages,\r\n pauseVoiceMessage,\r\n addNewPendingMessages,\r\n } = useMessageState();\r\n\r\n const [messages, setMessages] = useState<Array<string | Message>>(activeConversation.conversation?.messages? restructureMessages([\r\n ...activeConversation.conversation.messages.reverse(),\r\n ]) : []);\r\n const [isEditing, setIsEditing] = useState(false);\r\n const [refMap, setRefMap] = useState<{\r\n [key: string]: { ref: RefObject<View> | null; index: number };\r\n }>({});\r\n const [viewable, setViewable] = useState<GroupedMessages>([]);\r\n const [isScrolling, setIsScrolling] = useState(false);\r\n const [currentPage, setCurrentPage] = useState(2);\r\n const [loadingOlderMessages, setLoadingOlderMessages] = useState(false);\r\n const [recipientId, setRecipientId] = useState(\"\");\r\n const [connectionStatus, setConnectionStatus] = useState<ConnectionEvent>({\r\n isConnected: false,\r\n fetchingConversations: false,\r\n connecting: false,\r\n });\r\n const width = Dimensions.get(\"window\").width;\r\n const emojiSize = 40;\r\n\r\n const [selectedMessage, setSelectedMessage] = useState<SelectedMessage>({\r\n message: null,\r\n ref: null,\r\n itemIndex: 0,\r\n isMessageOwner: false,\r\n });\r\n // quoted message and ref\r\n const [activeQuote, setActiveQuote] = useState<\r\n Omit<SelectedMessage, \"isMessageOwner\">\r\n >({\r\n message: null,\r\n ref: null,\r\n itemIndex: 0,\r\n });\r\n const [audioTime, setAudioTime] = useState(0);\r\n const [audioWaves, setAudioWaves] = useState<{\r\n [key: number]: { metering: number; height: number };\r\n }>({});\r\n const [recording, setRecording] = useState<Audio.Recording>();\r\n // const onViewRef = useRef((viewableItems: any) => {\r\n // let Check = [];\r\n // for (var i = 0; i < viewableItems.viewableItems.length; i++) {\r\n // Check.push(viewableItems.viewableItems[i].item);\r\n // }\r\n // setViewable(Check);\r\n // });\r\n\r\n const viewConfigRef = useRef({ viewAreaCoveragePercentThreshold: 80 });\r\n\r\n // const conversationId = useMemo(() => {\r\n // try {\r\n // if(!participantId && !conversationId){\r\n // throw new Error('ConversationId and Participant cannot be null')\r\n // }\r\n // if(participantId && conversationId){\r\n // throw new Error('One of ConversationId or Participant can be passed but not both.')\r\n // }\r\n // return participantId? generateConversationId(chatUser.uid, participantId, client.projectId) : conversationId;\r\n // } catch (error) {\r\n // throw new Error(error.message)\r\n // }\r\n\r\n // },[participantId, chatUser, client, conversationId]);\r\n\r\n useEffect(() => {\r\n setRefMap((prevMap) => {\r\n const newMap = { ...prevMap };\r\n\r\n messages.forEach((message, index) => {\r\n const messageKey =\r\n typeof message === \"string\" ? message : message.messageId;\r\n\r\n if (!newMap[messageKey]) {\r\n newMap[messageKey] = { ref: createRef<View>(), index };\r\n } else {\r\n newMap[messageKey] = { ...newMap[messageKey], index };\r\n }\r\n });\r\n\r\n return newMap;\r\n });\r\n }, [messages]);\r\n\r\n const clearSelectedMessage = () =>\r\n setActiveQuote({ message: null, ref: null, itemIndex: 0 });\r\n\r\n async function getMessages() {\r\n try {\r\n setLoadingMessages(true);\r\n const messages = (await client\r\n ?.messageClient(conversationId)\r\n .getMessages()) as Array<Message>;\r\n if (messages.length > 0) {\r\n var restructuredMessages: GroupedMessages = restructureMessages(\r\n messages.reverse()\r\n );\r\n setMessages(restructuredMessages);\r\n }\r\n } catch (error) {\r\n console.log(error);\r\n } finally {\r\n setLoadingMessages(false);\r\n }\r\n }\r\n\r\n async function getBroadcastListMessages() {\r\n try {\r\n setLoadingMessages(true);\r\n const messages = (await client\r\n ?.messageClient(conversationId)\r\n .getBroadcastListMessages()) as Array<Message>;\r\n if (messages.length > 0) {\r\n var restructuredMessages: GroupedMessages = restructureMessages(\r\n messages.reverse()\r\n );\r\n setMessages(restructuredMessages);\r\n }\r\n } catch (error) {\r\n console.log(error);\r\n } finally {\r\n setLoadingMessages(false);\r\n }\r\n }\r\n\r\n async function getOlderMessages() {\r\n try {\r\n if (client && messages.length >= 25) {\r\n setLoadingOlderMessages(true);\r\n const olderMessages = (await client\r\n .messageClient(conversationId)\r\n .getMessages(currentPage)) as Array<Message>;\r\n setMessages((prev) => {\r\n return restructureMessages([...prev, ...olderMessages.reverse()]);\r\n });\r\n if (olderMessages.length > 0) {\r\n setCurrentPage((prev) => prev + 1);\r\n }\r\n }\r\n } catch (error) {\r\n console.log(error);\r\n } finally {\r\n setLoadingOlderMessages(false);\r\n }\r\n }\r\n\r\n // const getConversation = async () => {\r\n // try {\r\n // if (client) {\r\n // const conversation = (await client\r\n // ?.messageClient(conversationId)\r\n // .getConversation(conversationId)) as Conversation;\r\n // console.log(conversation, \"---the conversation\");\r\n // if (conversation) {\r\n // setConversationMeta(conversation);\r\n // }\r\n // }\r\n // } catch (error) {}\r\n // };\r\n\r\n useEffect(() => {\r\n if (activeConversation) {\r\n const recipients = participants.filter((id) => id !== client?.chatUserId);\r\n if (recipients && recipients.length > 0) {\r\n setRecipientId(recipients[0]);\r\n }\r\n if (\r\n activeConversation.conversation.conversationType === \"broadcast-chat\"\r\n ) {\r\n getBroadcastListMessages();\r\n } else {\r\n getMessages();\r\n }\r\n }\r\n }, [activeConversation]);\r\n\r\n const handleNewMessages = (event: any) => {\r\n try {\r\n console.log(event, \":::event\");\r\n if (event.message.conversationId === conversationId) {\r\n setMessages((prev) => {\r\n return restructureMessages([event.message, ...prev]);\r\n });\r\n }\r\n } catch (error) {\r\n console.log(error);\r\n }\r\n };\r\n\r\n // const handleNewBroadcastMessages = (event: any) => {\r\n // try {\r\n // console.log(event, ':::event');\r\n // setMessages((prev) => {\r\n // return restructureMessages([event.message, ...prev]);\r\n // });\r\n // }\r\n // } catch (error) {\r\n // console.log(error);\r\n // }\r\n // };\r\n\r\n const handleEditedMessage = (event: any) => {\r\n setMessages((prev) => {\r\n const newMessages = [...prev];\r\n return newMessages.map((message) => {\r\n if (\r\n typeof message !== \"string\" &&\r\n message.messageId === event.message.messageId\r\n ) {\r\n return { ...message, ...event.message };\r\n }\r\n return message;\r\n });\r\n });\r\n };\r\n\r\n const handleTypingStarted = (event: any) => {\r\n if (event.conversationId === conversationId) {\r\n showTyping(true);\r\n }\r\n };\r\n\r\n const handleStoppedStarted = (event: any) => {\r\n if (event.conversationId === conversationId) {\r\n showTyping(false);\r\n }\r\n };\r\n\r\n const handleDeletedMessage = (event: any) => {\r\n setMessages((prev) => {\r\n var prevMessage = prev.filter((message) => {\r\n if (typeof message !== \"string\") {\r\n return message.messageId !== event.message.messageId;\r\n }\r\n });\r\n return restructureMessages(prevMessage);\r\n });\r\n };\r\n\r\n const handleConnectionChanged = (\r\n event: ChatEventGenerics<ConnectionEvent>\r\n ) => {\r\n setConnectionStatus(event);\r\n };\r\n\r\n useEffect(() => {\r\n if (client && conversationId) {\r\n client.messageClient(conversationId).setActiveConversation();\r\n client.subscribe(Events.NEW_MESSAGE, handleNewMessages);\r\n client.subscribe(Events.EDITED_MESSAGE, handleEditedMessage);\r\n client.subscribe(Events.HAS_STARTED_TYPING, handleTypingStarted);\r\n client.subscribe(Events.HAS_STOPPED_TYPING, handleStoppedStarted);\r\n client.subscribe(Events.DELETED_MESSAGE, handleDeletedMessage);\r\n client.subscribe(Events.CONNECTION_CHANGED, handleConnectionChanged);\r\n }\r\n return () => {\r\n if (client) {\r\n if (conversationId) {\r\n client.messageClient(conversationId).unSetActiveConversation();\r\n }\r\n // client.unsubscribe(\"new_broadcast_message\", handleNewBroadcastMessages);\r\n client.unsubscribe(Events.NEW_MESSAGE, handleNewMessages);\r\n client.unsubscribe(Events.EDITED_MESSAGE, handleEditedMessage);\r\n client.unsubscribe(Events.HAS_STARTED_TYPING, handleTypingStarted);\r\n client.unsubscribe(Events.HAS_STOPPED_TYPING, handleStoppedStarted);\r\n client.unsubscribe(Events.DELETED_MESSAGE, handleDeletedMessage);\r\n client.unsubscribe(Events.CONNECTION_CHANGED, handleConnectionChanged);\r\n }\r\n };\r\n }, [client, conversationId]);\r\n\r\n const openEmojis = () => {\r\n // emojiListRef?.current?.open();\r\n inputRef?.current?.blur();\r\n displayModal({\r\n children: <EmojiListModal \r\n message={selectedMessage.message}\r\n recipientId={recipientId}\r\n theme={theme}\r\n type=\"message\"\r\n onSelect={(value) => setGlobalTextMessage(p => p+value)}\r\n />\r\n })\r\n };\r\n\r\n const sendMessage = async () => {\r\n try {\r\n if (!globalTextMessage) return null;\r\n if (conversationId) {\r\n if (client) {\r\n client.messageClient(conversationId).sendMessage({\r\n conversationId: conversationId,\r\n to: recipientId,\r\n message: globalTextMessage,\r\n reactions: [],\r\n attachedMedia: [],\r\n attachmentType: AttachmentTypes.NONE,\r\n quotedMessage: activeQuote.message,\r\n });\r\n }\r\n setGlobalTextMessage(\"\");\r\n setIsEditing(false);\r\n clearSelectedMessage();\r\n if (activeQuote.message) {\r\n clearSelectedMessage();\r\n }\r\n }\r\n } catch (err) {\r\n console.log(err);\r\n }\r\n };\r\n\r\n const sendEditedMessage = async (externalInputRef?: RefObject<TextInput>) => {\r\n try {\r\n if (client && selectedMessage.message) {\r\n client\r\n .messageClient(selectedMessage.message.conversationId)\r\n .editMessage({\r\n to: selectedMessage.message.to,\r\n conversationId: selectedMessage.message.conversationId,\r\n messageId: selectedMessage.message.messageId,\r\n textMessage: globalTextMessage,\r\n shouldEdit: true,\r\n });\r\n setGlobalTextMessage(\"\");\r\n clearSelectedMessage();\r\n setIsEditing(false);\r\n if (externalInputRef && externalInputRef.current) {\r\n externalInputRef.current?.blur();\r\n } else {\r\n inputRef.current?.blur();\r\n }\r\n }\r\n } catch (err) {\r\n console.log(err);\r\n }\r\n };\r\n\r\n const broadcastMessage = async (externalInputRef?: RefObject<TextInput>) => {\r\n try {\r\n if (!globalTextMessage) return null;\r\n if (client && conversationId) {\r\n client.messageClient(conversationId).broadcastMessage({\r\n broadcastListId: conversationId,\r\n participantsIds: activeConversation.conversation.participants,\r\n newMessage: {\r\n conversationId: conversationId,\r\n to: recipientId,\r\n message: globalTextMessage,\r\n reactions: [],\r\n attachedMedia: [],\r\n attachmentType: AttachmentTypes.NONE,\r\n quotedMessage: activeQuote.message,\r\n },\r\n });\r\n }\r\n setGlobalTextMessage(\"\");\r\n setIsEditing(false);\r\n clearSelectedMessage();\r\n if (activeQuote.message) {\r\n clearSelectedMessage();\r\n }\r\n console.log(activeConversation.conversation.conversationType);\r\n } catch (err) {\r\n console.log(err);\r\n }\r\n };\r\n\r\n const sendVoiceMessage = async () => {\r\n try {\r\n setRecording(undefined);\r\n await recording?.stopAndUnloadAsync();\r\n await Audio.setAudioModeAsync({\r\n allowsRecordingIOS: false,\r\n });\r\n const uri = recording?.getURI();\r\n if (client) {\r\n // remove any audio being played\r\n pauseVoiceMessage();\r\n addNewPendingMessages({\r\n from: client.chatUserId,\r\n messageId: generateId(),\r\n conversationId: conversationId,\r\n to: recipientId,\r\n message: \"\",\r\n reactions: [],\r\n attachedMedia: [\r\n {\r\n type: MediaType.AUDIO,\r\n ext: \".mp3\",\r\n mediaId: generateId(),\r\n mediaUrl: uri as string,\r\n mimeType: \"audio/mp3\",\r\n meta: {\r\n audioDurationSec: audioTime,\r\n },\r\n },\r\n ],\r\n attachmentType: AttachmentTypes.MEDIA,\r\n quotedMessage: null,\r\n createdAt: new Date(),\r\n });\r\n setAudioWaves({});\r\n setAudioTime(0);\r\n }\r\n } catch (err) {\r\n setAudioWaves({});\r\n setAudioTime(0);\r\n setRecording(undefined);\r\n console.error(err);\r\n }\r\n };\r\n\r\n const send = (externalInputRef: RefObject<TextInput>) => {\r\n if (activeConversation.conversation.conversationType === \"broadcast-chat\") {\r\n return broadcastMessage();\r\n }\r\n if (isEditing) {\r\n return sendEditedMessage(externalInputRef);\r\n }\r\n sendMessage();\r\n };\r\n\r\n const onChatItemLongPress = (\r\n selectedMessage: Message,\r\n ref: MutableRefObject<View | undefined>,\r\n isMessageOwner: boolean\r\n ) => {\r\n const messageIndex = messages.indexOf(selectedMessage);\r\n setSelectedMessage({\r\n message: selectedMessage,\r\n ref: ref,\r\n itemIndex: messageIndex,\r\n isMessageOwner,\r\n });\r\n messageOptionsRef?.current?.open();\r\n Haptics.medium();\r\n };\r\n\r\n const onScrollToMessage = (messageId: string) => {\r\n try {\r\n const itemRef = refMap[messageId].ref?.current;\r\n scrollRef.current?.scrollToIndex({\r\n animated: true,\r\n index: refMap[messageId].index,\r\n viewPosition: 0.5,\r\n });\r\n if (itemRef) {\r\n itemRef.setNativeProps({\r\n style: { backgroundColor: theme?.background.secondary },\r\n });\r\n setTimeout(() => {\r\n itemRef.setNativeProps({\r\n style: { backgroundColor: \"transparent\" },\r\n });\r\n }, 1000);\r\n }\r\n } catch (error) {\r\n scrollRef.current?.scrollToEnd({\r\n animated: true,\r\n });\r\n }\r\n };\r\n\r\n function formatViewableDate(date: Date | string): string {\r\n if (isThisWeek(date, { weekStartsOn: 1 })) {\r\n // weekStartsOn: 1 makes the week start on Monday\r\n return format(date, \"EEEE\"); // 'EEEE' returns the full weekday name (e.g., 'Monday')\r\n } else {\r\n return format(date, \"yyyy-MM-dd\"); // returns full date (e.g., '2024-09-13')\r\n }\r\n }\r\n\r\n const threaded = (item: string | Message, index: number) => {\r\n var nextMessage = messages[index - 1];\r\n if (typeof item === \"string\") {\r\n return false;\r\n }\r\n if (typeof nextMessage === \"string\" || !nextMessage) {\r\n return false;\r\n }\r\n return item.messageOwner.uid === nextMessage.messageOwner.uid;\r\n };\r\n\r\n useEffect(() => {\r\n let debounceTimer: NodeJS.Timeout | undefined;\r\n let idleTimer: NodeJS.Timeout | undefined;\r\n if (conversationId) {\r\n if (globalTextMessage.length > 0) {\r\n clearTimeout(debounceTimer);\r\n // set a new debounce timer to send a typing notification after 350ms\r\n debounceTimer = setTimeout(() => {\r\n if (client) {\r\n client\r\n .messageClient(conversationId)\r\n .sendTypingNotification(recipientId);\r\n debounceTimer = undefined; // clear debounce timer reference after sending the typing notification\r\n }\r\n }, 300);\r\n // clear the previous idle timer (stopped typing)\r\n clearTimeout(idleTimer);\r\n // set a new idle timer to send a stopped typing notification after 1300ms of inactivity\r\n idleTimer = setTimeout(() => {\r\n if (client) {\r\n client\r\n .messageClient(conversationId)\r\n .sendStoppedTypingNotification(recipientId);\r\n }\r\n }, 1300);\r\n }\r\n }\r\n return () => clearTimeout(debounceTimer);\r\n }, [client, globalTextMessage, conversationId]);\r\n\r\n useEffect(() => {\r\n if (client && conversationId) {\r\n const msClient = client.messageClient(conversationId);\r\n msClient.readMessages(conversationId, {\r\n uid: client.chatUserId,\r\n messageIds: activeConversation.unread,\r\n });\r\n }\r\n }, [client, conversationId, activeConversation]);\r\n\r\n const onStartedScrolling = () => {\r\n let scrollStateRef: NodeJS.Timeout | undefined = undefined;\r\n setIsScrolling(true);\r\n clearTimeout(scrollStateRef);\r\n scrollStateRef = setTimeout(() => {\r\n setIsScrolling(false);\r\n }, 3000);\r\n };\r\n\r\n const onRecordingStatusUpdate = (data: Audio.RecordingStatus) => {\r\n var durationSecond = data.durationMillis / 1000;\r\n var metering = data.metering ?? -160;\r\n if (durationSecond >= 300) {\r\n }\r\n var interp = interpolate(metering, [METERING_MIN_POWER, 0], [1, 100]);\r\n setAudioWaves((prev) => {\r\n return { ...prev, [durationSecond]: { metering, height: interp } };\r\n });\r\n setAudioTime(durationSecond);\r\n };\r\n\r\n async function startRecording() {\r\n try {\r\n if (permissionResponse?.status !== \"granted\") {\r\n await requestPermission();\r\n }\r\n // pause any audio being played\r\n pauseVoiceMessage();\r\n await Audio.setAudioModeAsync({\r\n allowsRecordingIOS: true,\r\n playsInSilentModeIOS: true,\r\n });\r\n\r\n const { recording } = await Audio.Recording.createAsync(\r\n Audio.RecordingOptionsPresets.LOW_QUALITY,\r\n onRecordingStatusUpdate\r\n );\r\n setRecording(recording);\r\n } catch (err) {\r\n console.error(\"Failed to start recording\", err);\r\n }\r\n }\r\n\r\n async function deleteRecording() {\r\n setRecording(undefined);\r\n setAudioWaves({});\r\n // setIsRecordingPaused(false)\r\n await recording?.stopAndUnloadAsync();\r\n }\r\n\r\n const renderMessageOptions = useCallback(() => {\r\n return (\r\n <MessageOptions\r\n ref={messageOptionsRef}\r\n recipientId={recipientId}\r\n message={selectedMessage.message}\r\n isMessageOwner={selectedMessage.isMessageOwner}\r\n onReply={() => {\r\n messageOptionsRef?.current?.close();\r\n setTimeout(() => {\r\n setActiveQuote(selectedMessage);\r\n inputRef.current?.focus();\r\n }, 500);\r\n }}\r\n onStartEditing={() => {\r\n messageOptionsRef?.current?.close();\r\n setIsEditing(true);\r\n\r\n setTimeout(() => {\r\n inputRef?.current?.focus();\r\n setGlobalTextMessage(selectedMessage.message?.message || \"\");\r\n }, 500);\r\n }}\r\n theme={theme}\r\n openEmojiList={() => {\r\n displayModal({\r\n children: <EmojiListModal \r\n message={selectedMessage.message}\r\n recipientId={recipientId}\r\n theme={theme}\r\n />\r\n })\r\n }}\r\n />\r\n );\r\n }, [\r\n selectedMessage,\r\n recipientId,\r\n messageOptionsRef,\r\n inputRef,\r\n messageOptionsRef,\r\n setIsEditing,\r\n theme,\r\n ]);\r\n\r\n const messageListHeader = useCallback(() => {\r\n return (\r\n <View style={{ width: \"100%\" }}>\r\n {pendingMessages\r\n .filter((message) => message.conversationId === conversationId)\r\n .map((message, index) => (\r\n <ChatItem\r\n key={index}\r\n ref={null}\r\n onScrollToIndex={(messageId) => {}}\r\n layout={layout}\r\n onLongPress={({ message, chatItemRef, isMessageOwner }) => {}}\r\n inputRef={inputRef}\r\n position={chatUserId === message.from ? \"right\" : \"left\"}\r\n message={message as Message}\r\n onSelectedMessage={({ message, chatItemRef }) => {}}\r\n conversation={activeConversation.conversation}\r\n chatUserId={chatUserId}\r\n recipientId={recipientId}\r\n renderChatBubble={renderChatBubble}\r\n isPending={true}\r\n />\r\n ))}\r\n </View>\r\n );\r\n }, [loadingMessages, pendingMessages, theme]);\r\n\r\n const renderChatItem = useCallback(\r\n ({ item, index }: { item: string | Message; index: number }) => {\r\n if (typeof item === \"string\") {\r\n if (layout !== \"stacked\") {\r\n return (\r\n <View\r\n style={{\r\n alignSelf: \"center\",\r\n padding: 5,\r\n marginTop: 5,\r\n backgroundColor: theme?.background.secondary,\r\n borderRadius: 10,\r\n marginBottom: 20,\r\n }}\r\n >\r\n <Text\r\n style={{\r\n textAlign: \"center\",\r\n paddingHorizontal: 5,\r\n color: theme?.text.secondary,\r\n fontSize: 11,\r\n fontFamily: fontFamily || undefined,\r\n }}\r\n >\r\n {item}\r\n </Text>\r\n </View>\r\n );\r\n }\r\n return (\r\n <View\r\n style={{\r\n flexDirection: \"row\",\r\n alignItems: \"center\",\r\n paddingVertical: 20,\r\n backgroundColor: theme?.background.primary,\r\n }}\r\n >\r\n <View\r\n style={{\r\n height: 1,\r\n width: \"100%\",\r\n flex: 1,\r\n backgroundColor: theme?.divider,\r\n }}\r\n />\r\n <Text\r\n style={{\r\n textAlign: \"center\",\r\n paddingHorizontal: 15,\r\n color: theme?.text.secondary,\r\n fontSize: 11,\r\n fontFamily: fontFamily || undefined,\r\n }}\r\n >\r\n {item}\r\n </Text>\r\n <View\r\n style={{\r\n height: 1,\r\n width: \"100%\",\r\n flex: 1,\r\n backgroundColor: theme?.divider,\r\n }}\r\n />\r\n </View>\r\n );\r\n }\r\n\r\n return (\r\n <ChatItem\r\n ref={refMap[item.messageId]?.ref}\r\n onScrollToIndex={(messageId) => {\r\n onScrollToMessage(messageId);\r\n }}\r\n layout={layout}\r\n onLongPress={({ message, chatItemRef, isMessageOwner }) => {\r\n activeConversation.conversation.conversationType !== \"admin-chat\"\r\n ? onChatItemLongPress(message, chatItemRef, isMessageOwner)\r\n : null\r\n }\r\n \r\n }\r\n inputRef={inputRef}\r\n position={chatUserId === item.from ? \"right\" : \"left\"}\r\n message={item}\r\n onSelectedMessage={({ message, chatItemRef }) => {\r\n setActiveQuote({ message, ref: chatItemRef, itemIndex: index });\r\n }}\r\n threaded={threaded(item, index)}\r\n conversation={activeConversation.conversation}\r\n chatUserId={chatUserId}\r\n recipientId={recipientId}\r\n renderChatBubble={renderChatBubble}\r\n />\r\n );\r\n },\r\n [activeConversation, renderChatBubble, refMap, theme, layout]\r\n );\r\n\r\n const renderPlaceholder = useCallback(() => {\r\n if (placeholder) return placeholder;\r\n return (\r\n <View\r\n style={{\r\n flex: 1,\r\n height: Dimensions.get(\"window\").height,\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n }}\r\n >\r\n <MessagePlus size={100} color={theme?.icon} />\r\n <Text\r\n style={{\r\n color: theme?.text.disabled,\r\n marginTop: 20 * fontScale,\r\n fontFamily: fontFamily || undefined,\r\n }}\r\n >\r\n Start by sending a message.\r\n </Text>\r\n </View>\r\n );\r\n }, [placeholder, fontScale]);\r\n\r\n const chatInputProps: ChatInputRenderProps = {\r\n // sendMessage: (externalInputRef: RefObject<TextInput>) =>\r\n // isEditing ? sendEditedMessage(externalInputRef) : sendMessage(),\r\n sendMessage: (externalInputRef: RefObject<TextInput>) =>\r\n send(externalInputRef),\r\n value: globalTextMessage,\r\n onValueChange: setGlobalTextMessage,\r\n openMediaOptions: (externalInputRef: RefObject<TextInput>) => {\r\n mediaOptionsRef?.current?.open();\r\n externalInputRef?.current?.blur();\r\n },\r\n openEmojis,\r\n onStopEditing: () => {\r\n setIsEditing(false);\r\n clearSelectedMessage();\r\n },\r\n isRecording: recording !== undefined,\r\n audioDuration: audioTime,\r\n onDeleteRecording: deleteRecording,\r\n onStartRecording: startRecording,\r\n meteringProgress: audioWaves,\r\n isEditing,\r\n sendVoiceMessage: () => sendVoiceMessage(),\r\n isLoading: connectionStatus.connecting || loadingMessages,\r\n };\r\n\r\n // const [ modal, showModal ] = useState(false)\r\n\r\n // return (\r\n // <GestureHandlerRootView\r\n // style={{\r\n // ...styles.main,\r\n // backgroundColor: theme?.background.primary,\r\n // }}\r\n // >\r\n // <View>\r\n // <Modal visible={modal} style={{ flex: 1 }}>\r\n // <View style={{ flex: 1, height: '100%', width: '100%', backgroundColor: 'red' }}>\r\n // <Text>sdfsdf</Text>\r\n // </View>\r\n // </Modal> \r\n // <TouchableOpacity onPress={() => showModal(true)} style={{ padding: 10, backgroundColor: 'red' }}>\r\n // <Text>cliek me</Text>\r\n // </TouchableOpacity>\r\n // <EmojiSheet\r\n // ref={emojiListRef}\r\n // openKeyboard={() => inputRef?.current?.focus()}\r\n // sendSticker={sendMessage}\r\n // recipientId={recipientId}\r\n // />\r\n // <MediaOptions\r\n // conversationId={conversationId}\r\n // clearActiveQuote={clearSelectedMessage}\r\n // activeQuote={activeQuote?.message}\r\n // ref={mediaOptionsRef}\r\n // chatUserId={client?.chatUserId as string}\r\n // recipientId={recipientId}\r\n // />\r\n // <>{renderMessageOptions()}</>\r\n // </View>\r\n // </GestureHandlerRootView>\r\n\r\n \r\n // )\r\n\r\n return (\r\n <GestureHandlerRootView\r\n style={{\r\n ...styles.main,\r\n backgroundColor: theme?.background.primary,\r\n }}\r\n >\r\n <ChatHeader\r\n conversation={activeConversation.conversation}\r\n chatUserId={chatUserId}\r\n renderChatHeader={renderChatHeader}\r\n isTyping={isTyping}\r\n />\r\n {/* <Button title=\"oepner\" \r\n onPress={() => {\r\n messageOptionsRef?.current?.open();\r\n }}\r\n /> */}\r\n <KeyboardAvoiding keyboardOffset={keyboardOffset}>\r\n {messages.length === 0 ? (\r\n renderPlaceholder()\r\n ) : (\r\n <View\r\n style={{\r\n flex: 1,\r\n height: \"100%\",\r\n }}\r\n >\r\n <FlashList\r\n ref={scrollRef}\r\n inverted\r\n // onScroll={() => (isScrolling ? null : onStartedScrolling())}\r\n data={messages}\r\n keyExtractor={(_, index) => index.toString()}\r\n renderItem={renderChatItem}\r\n refreshControl={\r\n <RefreshControl refreshing={false} onRefresh={getMessages} />\r\n }\r\n showsVerticalScrollIndicator={false}\r\n ListHeaderComponent={messageListHeader}\r\n ListFooterComponent={() => (\r\n <View\r\n style={{\r\n display: loadingOlderMessages ? \"flex\" : \"none\",\r\n paddingVertical: 10,\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n }}\r\n >\r\n <Text\r\n style={{\r\n color: theme?.text.disabled,\r\n fontFamily: fontFamily || undefined,\r\n }}\r\n >\r\n Loading older messages...\r\n </Text>\r\n </View>\r\n )}\r\n contentContainerStyle={{\r\n paddingTop: 0,\r\n }}\r\n estimatedItemSize={200}\r\n onEndReached={() => {\r\n getOlderMessages();\r\n }}\r\n />\r\n </View>\r\n )}\r\n\r\n <View>\r\n {activeQuote.message && (\r\n <SelectedMessage\r\n scrollRef={scrollRef}\r\n message={activeQuote.message}\r\n messageRef={activeQuote.ref}\r\n itemIndex={activeQuote.itemIndex}\r\n onClear={clearSelectedMessage}\r\n />\r\n )}\r\n {activeConversation.conversation.conversationType === \"admin-chat\" ? (\r\n <View\r\n style={{\r\n height: 50,\r\n width: \"100%\",\r\n borderTopWidth: 1,\r\n borderTopColor: theme?.divider,\r\n alignItems: \"center\",\r\n flexDirection: \"row\",\r\n justifyContent: \"center\",\r\n }}\r\n >\r\n <LockIcon size={15} color={theme?.icon} />\r\n <Text\r\n style={{\r\n marginStart: 5,\r\n fontFamily: fontFamily || undefined,\r\n fontSize: 14 * fontScale,\r\n color: theme?.text.disabled,\r\n }}\r\n >\r\n Only the Admin can send messages.\r\n </Text>\r\n </View>\r\n ) : (\r\n <View>\r\n <>\r\n {renderChatInput ? (\r\n renderChatInput(chatInputProps)\r\n ) : (\r\n <ChatInput\r\n openEmojis={openEmojis}\r\n inputRef={inputRef}\r\n mediaOptionsRef={mediaOptionsRef}\r\n sendMessage={() => {\r\n if (isEditing) {\r\n return sendEditedMessage();\r\n } else if (\r\n activeConversation.conversation.conversationType ===\r\n \"broadcast-chat\"\r\n ) {\r\n return broadcastMessage();\r\n }\r\n sendMessage();\r\n }}\r\n isLoading={connectionStatus.connecting || loadingMessages}\r\n conversationId={conversationId || \"\"}\r\n chatUserId={chatUserId}\r\n recipientId={recipientId}\r\n // selectedMessage={activeQuote}\r\n value={globalTextMessage}\r\n audioWaves={audioWaves}\r\n audioTime={audioTime}\r\n setValue={setGlobalTextMessage}\r\n onStopEditing={() => {\r\n setIsEditing(false);\r\n clearSelectedMessage();\r\n }}\r\n isEditing={isEditing}\r\n sendVoiceMessage={() => sendVoiceMessage()}\r\n onStartRecording={() => startRecording()}\r\n onDeleteRecording={() => deleteRecording()}\r\n isRecording={recording !== undefined}\r\n />\r\n )}\r\n </>\r\n </View>\r\n )}\r\n </View>\r\n </KeyboardAvoiding>\r\n <EmojiSheet\r\n ref={emojiListRef}\r\n message={selectedMessage.message}\r\n recipientId={recipientId}\r\n theme={theme}\r\n />\r\n <MediaOptions\r\n conversationId={conversationId}\r\n clearActiveQuote={clearSelectedMessage}\r\n activeQuote={activeQuote?.message}\r\n ref={mediaOptionsRef}\r\n chatUserId={client?.chatUserId as string}\r\n recipientId={recipientId}\r\n />\r\n <>{renderMessageOptions()}</>\r\n {/* <Modal visible style={{ flex: 1 }}>\r\n <View style={{ flex: 1, height: '100%', width: '100%', backgroundColor: 'red' }}>\r\n <Text>sdfsdf</Text>\r\n </View>\r\n </Modal> */}\r\n </GestureHandlerRootView>\r\n );\r\n}\r\n\r\nconst styles = StyleSheet.create({\r\n main: {\r\n flex: 1,\r\n height: \"100%\",\r\n width: \"100%\",\r\n paddingBottom: Platform.OS === \"android\" ? 0 : 20,\r\n },\r\n});\r\n","import React, {\r\n forwardRef,\r\n useCallback,\r\n useEffect,\r\n useState,\r\n} from \"react\";\r\nimport {\r\n StyleSheet,\r\n View,\r\n TextInput,\r\n Platform,\r\n Text,\r\n TouchableWithoutFeedback,\r\n Dimensions,\r\n TouchableOpacity,\r\n} from \"react-native\";\r\nimport Animated, {\r\n useAnimatedStyle,\r\n useSharedValue,\r\n withSpring,\r\n interpolate,\r\n Extrapolation,\r\n useAnimatedReaction,\r\n runOnJS,\r\n} from \"react-native-reanimated\";\r\nimport { Gesture, GestureDetector } from \"react-native-gesture-handler\";\r\nimport {\r\n AttachmentTypes,\r\n Conversation,\r\n MediaType,\r\n MessageStates,\r\n Message,\r\n generateId\r\n} from \"softchatjs-core\";\r\nimport {\r\n ClockIcon,\r\n DoubleCheck,\r\n ErrorIcon,\r\n ReplyIcon,\r\n SingleCheck,\r\n} from \"../../../assets/icons\";\r\nimport { Colors } from \"../../../constants/Colors\";\r\nimport Haptics from \"../../../helpers/haptics\";\r\nimport Stacked from \"./Layouts/Stacked\";\r\nimport Default from \"./Layouts/Default\";\r\nimport { useConfig } from \"../../../contexts/ChatProvider\";\r\nimport { useMessageState } from \"../../../contexts/MessageStateContext\";\r\nimport { ChatBubbleRenderProps } from \"../../../types\";\r\n\r\ntype OnSelectedMessage = {\r\n message: Message;\r\n chatItemRef: React.MutableRefObject<View | undefined>;\r\n};\r\n\r\ntype ChatItemProps = {\r\n inputRef: React.RefObject<TextInput>;\r\n position: \"left\" | \"right\";\r\n onSelectedMessage: ({ message, chatItemRef }: OnSelectedMessage) => void;\r\n message: Message;\r\n conversation: Conversation | null;\r\n onLongPress: (data: OnSelectedMessage & { isMessageOwner: boolean }) => void;\r\n chatUserId: string;\r\n recipientId: string;\r\n renderChatBubble?: (props: ChatBubbleRenderProps) => void;\r\n layout?: \"stacked\";\r\n onScrollToIndex: (messageId: string) => void;\r\n isPending?: boolean,\r\n threaded?: boolean\r\n};\r\n\r\nexport const ChatItem = forwardRef((props: ChatItemProps, ref: any) => {\r\n const {\r\n layout,\r\n inputRef,\r\n position,\r\n message,\r\n onSelectedMessage,\r\n conversation,\r\n onLongPress,\r\n chatUserId,\r\n recipientId,\r\n renderChatBubble,\r\n onScrollToIndex,\r\n isPending,\r\n threaded\r\n } = props;\r\n const { client } = useConfig();\r\n const pressed = useSharedValue(false);\r\n const offset = useSharedValue(0);\r\n const [finished, setFinished] = React.useState(false);\r\n const [threshHoldReached, setThreshHoldReached] = React.useState(false);\r\n const [messageState, setMessageState] = useState<MessageStates>(\r\n MessageStates.LOADING\r\n );\r\n const { removePendingMessage, updatePendingMessage } = useMessageState();\r\n\r\n const touchStart = useSharedValue({ x: 0, y: 0, time: 0 });\r\n const touchStartX = useSharedValue(0);\r\n const isDragging = useSharedValue(false);\r\n const maxThreshHold = -90;\r\n const TOUCH_SLOP = Platform.OS === \"android\" ? 40 : 10;\r\n const DISTANCE_TO_ACTIVATE_PAN = 70\r\n const deviceWidth = Dimensions.get(\"window\").width;\r\n\r\n const pan = Gesture.Pan()\r\n .enabled(false)\r\n // .enabled(conversation.conversationType !== \"admin-chat\")\r\n .minDistance(DISTANCE_TO_ACTIVATE_PAN)\r\n .onTouchesDown((e, state) => {\r\n 'worklet';\r\n touchStart.value = {\r\n x: e.changedTouches[0].x,\r\n y: e.changedTouches[0].y,\r\n time: Date.now(),\r\n };\r\n })\r\n .onTouchesMove((e, state) => {\r\n 'worklet';\r\n if (messageState < MessageStates.SENT) {\r\n return;\r\n }\r\n touchStartX.value = e.changedTouches[0].x;\r\n if (e.changedTouches[0].x + TOUCH_SLOP < touchStart.value.x) {\r\n state.activate();\r\n }\r\n })\r\n .onTouchesUp((e, state) => {\r\n 'worklet';\r\n touchStartX.value = 0;\r\n state.fail();\r\n })\r\n .onUpdate((e) => {\r\n 'worklet';\r\n if (Math.abs(e.translationX) < deviceWidth * (30 / 100)) {\r\n offset.value = e.translationX;\r\n }\r\n })\r\n .onFinalize(() => {\r\n 'worklet';\r\n isDragging.value = false;\r\n offset.value = withSpring(0, {\r\n damping: 100,\r\n });\r\n pressed.value = false;\r\n });\r\n\r\n const animatedStyles = useAnimatedStyle(() => ({\r\n transform: [{ translateX: offset.value < -1 ? offset.value : 0 }],\r\n }));\r\n\r\n const shareStyle = useAnimatedStyle(() => ({\r\n opacity: interpolate(\r\n offset.value,\r\n [-30, -100],\r\n [0, 1],\r\n Extrapolation.CLAMP\r\n ),\r\n transform: [\r\n {\r\n translateX: interpolate(\r\n offset.value,\r\n [-1, -100],\r\n [-1, -50],\r\n Extrapolation.CLAMP\r\n ),\r\n },\r\n {\r\n scale: interpolate(\r\n offset.value,\r\n [-30, -50],\r\n [0, 1],\r\n Extrapolation.CLAMP\r\n ),\r\n },\r\n ] as any,\r\n }));\r\n\r\n useAnimatedReaction(\r\n () => {\r\n return {\r\n offsetValue: offset.value,\r\n pressedValue: pressed.value,\r\n touchValue: touchStart.value,\r\n };\r\n },\r\n (result, previous) => {\r\n var value = Math.ceil(result.offsetValue);\r\n var value2 = result.pressedValue;\r\n if (value < maxThreshHold + 10) {\r\n runOnJS(setThreshHoldReached)(true);\r\n runOnJS(setFinished)(!value2);\r\n } else {\r\n runOnJS(setFinished)(false);\r\n runOnJS(setThreshHoldReached)(false);\r\n }\r\n }\r\n );\r\n\r\n const uploadAttachment = async () => {\r\n try {\r\n if(client && conversation){\r\n var media = message.attachedMedia[0];\r\n const res = await client.messageClient(conversation?.conversationId).uploadFile(\r\n media.mediaUrl,\r\n {\r\n filename: `${generateId()}${media.ext}`,\r\n mimeType: media.mimeType as string,\r\n ext: media.ext\r\n }\r\n )\r\n if(res.success){\r\n removePendingMessage(message.messageId);\r\n if(conversation.conversationType !== \"broadcast-chat\"){\r\n client.messageClient(conversation?.conversationId).sendMessage({\r\n ...message,\r\n attachedMedia: [\r\n {\r\n ...media,\r\n uploading: false,\r\n mediaUrl: res.link,\r\n }\r\n ],\r\n });\r\n }else{\r\n client.messageClient(conversation?.conversationId).broadcastMessage({\r\n broadcastListId: conversation?.conversationId,\r\n participantsIds: conversation.participants,\r\n newMessage: {\r\n ...message,\r\n attachedMedia: [\r\n {\r\n ...media,\r\n uploading: false,\r\n mediaUrl: res.link,\r\n }\r\n ],\r\n }\r\n });\r\n }\r\n \r\n }else{\r\n throw new Error('upload failed');\r\n }\r\n }else{\r\n throw new Error(\"Client not initialized\")\r\n }\r\n } catch (error) {\r\n updatePendingMessage(message.messageId, { ...message, messageState: MessageStates.FAILED });\r\n }\r\n }\r\n\r\n const retryUpload = () => {\r\n updatePendingMessage(message.messageId, { ...message, messageState: MessageStates.LOADING });\r\n }\r\n\r\n useEffect(() => {\r\n if(isPending && message.messageState !== MessageStates.FAILED){\r\n uploadAttachment();\r\n }\r\n },[ isPending, message ])\r\n\r\n useEffect(() => {\r\n if (finished) {\r\n onSelectedMessage({ message, chatItemRef: ref });\r\n inputRef.current?.focus();\r\n Haptics.medium();\r\n }\r\n }, [finished]);\r\n\r\n\r\n const renderStateIcon = useCallback((color: string) => {\r\n switch (message.messageState) {\r\n case MessageStates.FAILED:\r\n return <ErrorIcon size={18} color={color} />;\r\n case MessageStates.LOADING:\r\n return <ClockIcon size={12} color={color} />;\r\n case MessageStates.SENT:\r\n return <SingleCheck color={color} />;\r\n case MessageStates.READ:\r\n return <DoubleCheck color={color} />;\r\n default:\r\n return <ClockIcon size={12} color={color} />;\r\n }\r\n }, [message]);\r\n\r\n const getLayout = () => {\r\n if (layout === \"stacked\") {\r\n return <Stacked \r\n message={message} \r\n animatedStyles={animatedStyles}\r\n renderStateIcon={renderStateIcon}\r\n chatUserId={chatUserId}\r\n recipientId={recipientId}\r\n myMessage={position === 'right'}\r\n onScrollToIndex={(messageId) => onScrollToIndex(messageId)}\r\n isPending={isPending}\r\n retryUpload={retryUpload}\r\n />;\r\n }\r\n return <Default \r\n message={message