import React, { useState, useCallback, useEffect, useRef } from 'react'; import { View, TouchableOpacity, Image, Modal, Text, FlatList, Dimensions, Alert, ScrollView, Linking, ActivityIndicator, AppState, AppStateStatus, TextInput } from 'react-native'; import { GiftedChat, Bubble, InputToolbar, IMessage, Send, BubbleProps, Composer, TimeProps, MessageProps } from 'react-native-gifted-chat'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler'; import { AvatarWithInitials, Header, WarningModal } from 'src/components'; import { Colors } from 'src/theme'; import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { Video } from 'expo-av'; import ChatMessageBox from '../Components/ChatMessageBox'; import ReplyMessageBar from '../Components/ReplyMessageBar'; import { useSharedValue, withTiming } from 'react-native-reanimated'; import { BlurView } from 'expo-blur'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import Clipboard from '@react-native-clipboard/clipboard'; import { trigger } from 'react-native-haptic-feedback'; import ReactModal from 'react-native-modal'; import { storage, StoreType } from 'src/storage'; import { usePostDeleteMessageMutation, usePostGetChatWithQuery, usePostMessagesReadMutation, usePostReactToMessageMutation, usePostSendMessageMutation } from '@api/chat'; import { CustomMessage, Message, Reaction } from '../types'; import { API_HOST, WEBSOCKET_URL } from 'src/constants'; import ReactionBar from '../Components/ReactionBar'; import OptionsMenu from '../Components/OptionsMenu'; import EmojiSelectorModal from '../Components/EmojiSelectorModal'; import { styles } from './styles'; import SendIcon from 'assets/icons/messages/send.svg'; import { SheetManager } from 'react-native-actions-sheet'; import { NAVIGATION_PAGES } from 'src/types'; import { usePushNotification } from 'src/contexts/PushNotificationContext'; import ReactionsListModal from '../Components/ReactionsListModal'; import { dismissChatNotifications } from '../utils'; import { useMessagesStore } from 'src/stores/unreadMessagesStore'; import BanIcon from 'assets/icons/messages/ban.svg'; const options = { enableVibrateFallback: true, ignoreAndroidSystemSettings: false }; const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭']; const ChatScreen = ({ route }: { route: any }) => { const token = storage.get('token', StoreType.STRING) as string; const { id, name, avatar, userType }: { id: number; name: string; avatar: string | null; userType: 'normal' | 'not_exist' | 'blocked'; } = route.params; const userName = userType === 'blocked' ? 'Account is blocked' : userType === 'not_exist' ? 'Account does not exist' : name; const currentUserId = storage.get('uid', StoreType.STRING) as number; const insets = useSafeAreaInsets(); const [messages, setMessages] = useState(); const navigation = useNavigation(); const [prevThenMessageId, setPrevThenMessageId] = useState(-1); const { data: chatData, refetch, isFetching } = usePostGetChatWithQuery(token, id, 50, prevThenMessageId, true); const { mutateAsync: sendMessage } = usePostSendMessageMutation(); const swipeableRowRef = useRef(null); const messageContainerRef = useRef | null>(null); const [selectedMedia, setSelectedMedia] = useState(null); const [replyMessage, setReplyMessage] = useState(null); const [modalInfo, setModalInfo] = useState({ visible: false, type: 'confirm', message: '', action: () => {} }); const [selectedMessage, setSelectedMessage] = useState | null>(null); const [emojiSelectorVisible, setEmojiSelectorVisible] = useState(false); const [messagePosition, setMessagePosition] = useState<{ x: number; y: number; width: number; height: number; isMine: boolean; } | null>(null); const [isModalVisible, setIsModalVisible] = useState(false); const [unreadMessageIndex, setUnreadMessageIndex] = useState(null); const { mutateAsync: markMessagesAsRead } = usePostMessagesReadMutation(); const { mutateAsync: deleteMessage } = usePostDeleteMessageMutation(); const { mutateAsync: reactToMessage } = usePostReactToMessageMutation(); const [highlightedMessageId, setHighlightedMessageId] = useState(null); const [isRerendering, setIsRerendering] = useState(false); const [isTyping, setIsTyping] = useState(false); const messageRefs = useRef<{ [key: string]: any }>({}); const flatList = useRef(null); const scrollY = useSharedValue(0); const { isSubscribed } = usePushNotification(); const [isLoadingEarlier, setIsLoadingEarlier] = useState(false); const [hasMoreMessages, setHasMoreMessages] = useState(true); const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount); const appState = useRef(AppState.currentState); const textInputRef = useRef(null); const socket = useRef(null); const closeModal = () => { setModalInfo({ ...modalInfo, visible: false }); }; useEffect(() => { let unsubscribe: any; const setupNotificationHandler = async () => { unsubscribe = await dismissChatNotifications(id, isSubscribed, setModalInfo, navigation); }; setupNotificationHandler(); return () => { if (unsubscribe) unsubscribe(); updateUnreadMessagesCount(); }; }, [id]); useEffect(() => { socket.current = new WebSocket(WEBSOCKET_URL); socket.current.onopen = () => { socket.current?.send(JSON.stringify({ token })); }; socket.current.onmessage = (event) => { const data = JSON.parse(event.data); handleWebSocketMessage(data); }; socket.current.onclose = () => { console.log('WebSocket connection closed chat screen'); }; return () => { if (socket.current) { socket.current.close(); socket.current = null; } }; }, [token]); useEffect(() => { const handleAppStateChange = async (nextAppState: AppStateStatus) => { if (appState.current.match(/inactive|background/) && nextAppState === 'active') { if (!socket.current || socket.current.readyState === WebSocket.CLOSED) { socket.current = new WebSocket(WEBSOCKET_URL); socket.current.onopen = () => { socket.current?.send(JSON.stringify({ token })); }; socket.current.onmessage = (event) => { const data = JSON.parse(event.data); handleWebSocketMessage(data); }; } await dismissChatNotifications(id, isSubscribed, setModalInfo, navigation); } }; const subscription = AppState.addEventListener('change', handleAppStateChange); return () => { subscription.remove(); if (socket.current) { socket.current.close(); socket.current = null; } }; }, [token]); const handleWebSocketMessage = (data: any) => { switch (data.action) { case 'new_message': if (data.conversation_with === id && data.message) { const newMessage = mapApiMessageToGiftedMessage(data.message); setMessages((previousMessages) => { const messageExists = previousMessages && previousMessages.some((msg) => msg._id === newMessage._id); if (!messageExists) { return GiftedChat.append(previousMessages ?? [], [newMessage]); } return previousMessages; }); } break; case 'new_reaction': if (data.conversation_with === id && data.reaction) { updateMessageWithReaction(data.reaction); } break; case 'unreact': if (data.conversation_with === id && data.unreacted_message_id) { removeReactionFromMessage(data.unreacted_message_id); } break; case 'delete_message': if (data.conversation_with === id && data.deleted_message_id) { removeDeletedMessage(data.deleted_message_id); } break; case 'is_typing': if (data.conversation_with === id) { setIsTyping(true); } break; case 'stopped_typing': if (data.conversation_with === id) { setIsTyping(false); } break; case 'messages_read': if (data.conversation_with === id && data.read_messages_ids) { setMessages( (prevMessages) => prevMessages?.map((msg) => { if (data.read_messages_ids.includes(msg._id)) { return { ...msg, received: true }; } return msg; }) ?? [] ); } break; default: break; } }; const updateMessageWithReaction = (reactionData: any) => { setMessages( (prevMessages) => prevMessages?.map((msg) => { if (msg._id === reactionData.message_id) { const updatedReactions = [ ...(Array.isArray(msg.reactions) ? msg.reactions?.filter((r: any) => r.uid !== reactionData.uid) : []), reactionData ]; return { ...msg, reactions: updatedReactions }; } return msg; }) ?? [] ); }; const removeReactionFromMessage = (messageId: number) => { setMessages( (prevMessages) => prevMessages?.map((msg) => { if (msg._id === messageId) { const updatedReactions = Array.isArray(msg.reactions) ? msg.reactions?.filter((r: any) => r.uid !== id) : []; return { ...msg, reactions: updatedReactions }; } return msg; }) ?? [] ); }; const removeDeletedMessage = (messageId: number) => { setMessages( (prevMessages) => prevMessages?.map((msg) => { if (msg._id === messageId) { return { ...msg, deleted: true, text: 'This message was deleted', pending: false, sent: false, received: false }; } return msg; }) ?? [] ); }; useEffect(() => { const pingInterval = setInterval(() => { if (socket.current && socket.current.readyState === WebSocket.OPEN) { socket.current.send(JSON.stringify({ action: 'ping', conversation_with: id })); } else { socket.current = new WebSocket(WEBSOCKET_URL); socket.current.onopen = () => { socket.current?.send(JSON.stringify({ token })); }; socket.current.onmessage = (event) => { const data = JSON.parse(event.data); handleWebSocketMessage(data); }; return () => { if (socket.current) { socket.current.close(); socket.current = null; } }; } }, 50000); return () => clearInterval(pingInterval); }, []); const sendWebSocketMessage = ( action: string, message: CustomMessage | null = null, reaction: string | null = null, readMessagesIds: number[] | null = null ) => { if (socket.current && socket.current.readyState === WebSocket.OPEN) { const data: any = { action, conversation_with: id }; if (action === 'new_message' && message) { data.message = { id: message._id, text: message.text, sender: +currentUserId, sent_datetime: new Date().toISOString().replace('T', ' ').substring(0, 19), reply_to_id: message.replyMessage?.id ?? -1, reply_to: message.replyMessage ?? null, reactions: message.reactions ?? '{}', status: 2, attachement: -1 }; } if (action === 'new_reaction' && message && reaction) { data.reaction = { message_id: message._id, reaction, uid: +currentUserId, datetime: new Date().toISOString() }; } if (action === 'unreact' && message) { data.message_id = message._id; } if (action === 'delete_message' && message) { data.message_id = message._id; } if (action === 'messages_read' && readMessagesIds) { data.messages_ids = readMessagesIds; } socket.current.send(JSON.stringify(data)); } }; const handleTyping = (isTyping: boolean) => { if (isTyping) { sendWebSocketMessage('is_typing'); } else { sendWebSocketMessage('stopped_typing'); } }; const mapApiMessageToGiftedMessage = (message: Message): CustomMessage => { return { _id: message.id, text: message.text, createdAt: new Date(message.sent_datetime + 'Z'), user: { _id: message.sender, name: message.sender === id ? userName : 'Me' }, replyMessage: message.reply_to_id !== -1 ? { text: message.reply_to.text, id: message.reply_to.id, name: message.reply_to.sender === id ? userName : 'Me' } : null, reactions: JSON.parse(message.reactions || '{}'), attachment: message.attachement !== -1 ? message.attachement : null, pending: message.status === 1, sent: message.status === 2, received: message.status === 3, deleted: message.status === 4 }; }; useFocusEffect( useCallback(() => { refetch(); }, []) ); useFocusEffect( useCallback(() => { if (chatData?.messages) { const mappedMessages = chatData.messages.map(mapApiMessageToGiftedMessage); if (unreadMessageIndex === null && !isFetching) { const firstUnreadIndex = mappedMessages.findLastIndex( (msg) => !msg.received && !msg?.deleted && msg.user._id === id ); if (firstUnreadIndex !== -1) { setUnreadMessageIndex(firstUnreadIndex); const unreadMarker: any = { _id: 'unreadMarker', text: 'Unread messages', system: true }; mappedMessages.splice(firstUnreadIndex + 1, 0, unreadMarker); setTimeout(() => { if (flatList.current) { flatList.current.scrollToIndex({ index: firstUnreadIndex, animated: true, viewPosition: 0.5 }); } }, 500); } else { setUnreadMessageIndex(0); } } setMessages((previousMessages) => { const newMessages = mappedMessages.filter( (newMsg) => !previousMessages?.some((oldMsg) => oldMsg._id === newMsg._id) ); return prevThenMessageId !== -1 && previousMessages ? GiftedChat.prepend(previousMessages, newMessages) : mappedMessages; }); if (mappedMessages.length < 50) { setHasMoreMessages(false); } if (mappedMessages.length === 0 && !modalInfo.visible) { setTimeout(() => { textInputRef.current?.focus(); }, 500); } setIsLoadingEarlier(false); } }, [chatData]) ); useEffect(() => { if (messages?.length === 0 && !modalInfo.visible) { setTimeout(() => { textInputRef.current?.focus(); }, 500); } }, [modalInfo]); const loadEarlierMessages = async () => { if (!hasMoreMessages || isLoadingEarlier || !messages) return; setIsLoadingEarlier(true); const previousMessageId = messages[messages.length - 1]._id; setPrevThenMessageId(previousMessageId); }; const sentToServer = useRef>(new Set()); const handleViewableItemsChanged = ({ viewableItems }: { viewableItems: any[] }) => { const newViewableUnreadMessages = viewableItems .filter( (item) => !item.item.received && !item.item.deleted && !item.item.system && item.item.user._id === id && !sentToServer.current.has(item.item._id) ) .map((item) => item.item._id); if (newViewableUnreadMessages.length > 0) { markMessagesAsRead( { token, from_user: id, messages_id: newViewableUnreadMessages }, { onSuccess: () => { newViewableUnreadMessages.forEach((id) => sentToServer.current.add(id)); sendWebSocketMessage('messages_read', null, null, newViewableUnreadMessages); } } ); } }; const renderSystemMessage = (props: any) => { if (props.currentMessage._id === 'unreadMarker') { return ( {props.currentMessage.text} ); } return null; }; const clearReplyMessage = () => setReplyMessage(null); const handleLongPress = (message: CustomMessage, props: BubbleProps) => { const messageRef = messageRefs.current[message._id]; setSelectedMessage(props); trigger('impactMedium', options); const isMine = message.user._id === +currentUserId; if (messageRef) { messageRef.measureInWindow((x: number, y: number, width: number, height: number) => { const screenHeight = Dimensions.get('window').height; const spaceAbove = y - insets.top; const spaceBelow = screenHeight - (y + height) - insets.bottom * 2; let finalY = y; scrollY.value = 0; if (isNaN(y) || isNaN(height)) { console.error("Invalid measurement values for 'y' or 'height'", { y, height }); return; } if (spaceBelow < 160) { const extraShift = 160 - spaceBelow; finalY -= extraShift; } if (spaceAbove < 50) { const extraShift = 50 - spaceAbove; finalY += extraShift; } if (spaceBelow < 160 || spaceAbove < 50) { const targetY = screenHeight / 2 - height / 2; scrollY.value = withTiming(finalY - finalY); } if (height > Dimensions.get('window').height - 200) { finalY = 100; } finalY = isNaN(finalY) ? 0 : finalY; setMessagePosition({ x, y: finalY, width, height, isMine }); setIsModalVisible(true); }); } }; const openEmojiSelector = () => { SheetManager.show('emoji-selector'); trigger('impactLight', options); }; const closeEmojiSelector = () => { SheetManager.hide('emoji-selector'); }; const handleReactionPress = (emoji: string, messageId: number) => { addReaction(messageId, emoji); }; const handleDeleteMessage = (messageId: number) => { deleteMessage( { token, message_id: messageId, conversation_with_user: id }, { onSuccess: () => { setMessages( (prevMessages) => prevMessages?.map((msg) => { if (msg._id === messageId) { return { ...msg, deleted: true, text: 'This message was deleted', pending: false, sent: false, received: false }; } return msg; }) ?? [] ); const messageToDelete = messages?.find((msg) => msg._id === messageId); if (messageToDelete) { sendWebSocketMessage('delete_message', messageToDelete, null, null); } } } ); }; const handleOptionPress = (option: string) => { if (!selectedMessage) return; switch (option) { case 'reply': setReplyMessage(selectedMessage.currentMessage); setIsModalVisible(false); break; case 'copy': Clipboard.setString(selectedMessage?.currentMessage?.text ?? ''); setIsModalVisible(false); Alert.alert('Copied'); break; case 'delete': handleDeleteMessage(selectedMessage.currentMessage?._id); setIsModalVisible(false); break; default: break; } closeEmojiSelector(); }; const openReactionList = ( reactions: { uid: number; name: string; reaction: string }[], messageId: number ) => { SheetManager.show('reactions-list-modal', { payload: { users: reactions, currentUserId: +currentUserId, token, messageId, conversation_with_user: id, setMessages, sendWebSocketMessage } as any }); }; const renderTimeContainer = (time: TimeProps) => { const createdAt = new Date(time.currentMessage.createdAt); const formattedTime = createdAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }); const hasReactions = time.currentMessage.reactions && Array.isArray(time.currentMessage.reactions) && time.currentMessage.reactions.length > 0; return ( {hasReactions && ( Array.isArray(time.currentMessage.reactions) && openReactionList( time.currentMessage.reactions.map((reaction) => ({ ...reaction, name: reaction.uid === id ? userName : 'Me' })), time.currentMessage._id ) } > {Object.entries( (Array.isArray(time.currentMessage.reactions) ? time.currentMessage.reactions : [] ).reduce( (acc: Record, { reaction }: { reaction: string }) => { if (!acc[reaction]) { acc[reaction] = { count: 0 }; } acc[reaction].count += 1; return acc; }, {} ) ).map(([emoji, { count }]: any) => { return ( {emoji} {(count as number) > 1 ? ` ${count}` : ''} ); })} )} {formattedTime} {renderTicks(time.currentMessage)} ); }; const renderSelectedMessage = () => selectedMessage && ( null} renderTime={renderTimeContainer} /> ); const handleBackgroundPress = () => { setIsModalVisible(false); setSelectedMessage(null); closeEmojiSelector(); }; useFocusEffect( useCallback(() => { navigation?.getParent()?.setOptions({ tabBarStyle: { display: 'none' } }); }, [navigation]) ); const onSend = useCallback( (newMessages: CustomMessage[] = []) => { if (replyMessage) { newMessages[0].replyMessage = { text: replyMessage.text, id: replyMessage._id, name: replyMessage.user._id === id ? userName : 'Me' }; } const message = { ...newMessages[0], pending: true }; setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message])); sendMessage( { token, to_uid: id, text: message.text, reply_to_id: replyMessage ? (replyMessage._id as number) : -1 }, { onSuccess: (res) => { const newMessage = { _id: res.message_id, text: message.text, replyMessage: { ...message.replyMessage, sender: replyMessage?.user?._id } }; setMessages((previousMessages) => (previousMessages ?? []).map((msg) => msg._id === message._id ? { ...msg, _id: res.message_id } : msg ) ); sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage); }, onError: (err) => console.log('err', err) } ); clearReplyMessage(); }, [replyMessage] ); const addReaction = (messageId: number, reaction: string) => { if (!messages) return; const updatedMessages = messages.map((msg: any) => { if (msg._id === messageId) { const updatedReactions: Reaction[] = [ ...(Array.isArray(msg.reactions) ? msg.reactions?.filter((r: Reaction) => r.uid !== +currentUserId) : []), { datetime: new Date().toISOString(), reaction: reaction, uid: +currentUserId } ]; return { ...msg, reactions: updatedReactions }; } return msg; }); setMessages(updatedMessages); reactToMessage( { token, message_id: messageId, reaction: reaction, conversation_with_user: id }, { onSuccess: () => { const message = messages.find((msg) => msg._id === messageId); if (message) { sendWebSocketMessage('new_reaction', message, reaction); } }, onError: (err) => console.log('err', err) } ); setIsModalVisible(false); }; const updateRowRef = useCallback( (ref: any) => { if ( ref && replyMessage && ref.props.children.props.currentMessage?._id === replyMessage._id ) { swipeableRowRef.current = ref; } }, [replyMessage] ); const renderReplyMessageView = (props: BubbleProps) => { if (!props.currentMessage) { return null; } const { currentMessage } = props; if (!currentMessage || !currentMessage?.replyMessage) { return null; } return ( { if (currentMessage?.replyMessage?.id) { scrollToMessage(currentMessage.replyMessage.id); } }} > {currentMessage.replyMessage.name} {currentMessage.replyMessage.text} ); }; const scrollToMessage = (messageId: number) => { if (!messages) return; const messageIndex = messages.findIndex((message) => message._id === messageId); if (messageIndex !== -1 && flatList.current) { flatList.current.scrollToIndex({ index: messageIndex, animated: true, viewPosition: 0.5 }); setHighlightedMessageId(messageId); } }; useEffect(() => { if (highlightedMessageId && isRerendering) { setTimeout(() => { setHighlightedMessageId(null); setIsRerendering(false); }, 1500); } }, [highlightedMessageId, isRerendering]); useEffect(() => { if (replyMessage && swipeableRowRef.current) { swipeableRowRef.current.close(); swipeableRowRef.current = null; } }, [replyMessage]); const renderMessageImage = (props: any) => { const { currentMessage } = props; return ( setSelectedMedia(currentMessage.image)} style={styles.imageContainer} > ); }; const renderTicks = (message: CustomMessage) => { if (message.user._id === id) return null; return message.received ? ( ) : message.sent ? ( ) : message.pending ? ( ) : null; }; const renderBubble = (props: BubbleProps) => { const { currentMessage } = props; if (currentMessage.deleted) { const text = currentMessage.text.length ? props.currentMessage.text : 'This message was deleted'; return ( null} currentMessage={{ ...props.currentMessage, text: text }} renderMessageText={() => ( {text} )} wrapperStyle={{ right: { backgroundColor: Colors.DARK_BLUE }, left: { backgroundColor: Colors.FILL_LIGHT } }} textStyle={{ left: { color: Colors.DARK_BLUE }, right: { color: Colors.WHITE } }} /> ); } const isHighlighted = currentMessage._id === highlightedMessageId; const backgroundColor = isHighlighted ? Colors.ORANGE : currentMessage.user._id === +currentUserId ? Colors.DARK_BLUE : Colors.FILL_LIGHT; return ( { if (ref && currentMessage) { messageRefs.current[currentMessage._id] = ref; } }} collapsable={false} > handleLongPress(currentMessage, props)} renderTicks={() => null} renderTime={renderTimeContainer} /> ); }; const renderInputToolbar = (props: any) => ( // } // // onPressActionButton={openActionSheet} // /> null } containerStyle={{ backgroundColor: Colors.FILL_LIGHT }} /> ); const renderScrollToBottom = () => { return ( { if (flatList.current) { flatList.current.scrollToIndex({ index: 0, animated: true }); } }} > ); }; const shouldUpdateMessage = ( props: MessageProps, nextProps: MessageProps ) => { setIsRerendering(true); const currentId = nextProps.currentMessage._id; return currentId === highlightedMessageId; }; return (
navigation.navigate( ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: id }] as never) ) } disabled={userType !== 'normal'} > {avatar && userType === 'normal' ? ( ) : userType === 'normal' ? ( n[0]) .join('') ?? '' } flag={API_HOST + 'flag.png'} size={30} fontSize={12} /> ) : ( )} } /> {messages ? ( { const wait = new Promise((resolve) => setTimeout(resolve, 300)); wait.then(() => { flatList.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0.5 }); }); } }} renderSystemMessage={renderSystemMessage} onSend={(newMessages: CustomMessage[]) => onSend(newMessages)} user={{ _id: +currentUserId, name: 'Me' }} renderBubble={renderBubble} renderMessageImage={renderMessageImage} renderInputToolbar={renderInputToolbar} renderCustomView={renderReplyMessageView} isCustomViewBottom={false} messageContainerRef={messageContainerRef} minComposerHeight={34} onInputTextChanged={(text) => handleTyping(text.length > 0)} textInputRef={textInputRef} isTyping={isTyping} renderSend={(props) => ( {props.text?.trim() && ( )} {!props.text?.trim() && } )} textInputProps={{ ...styles.composer, selectionColor: Colors.LIGHT_GRAY }} placeholder="" renderMessage={(props) => ( )} updateRowRef={updateRowRef} setReplyOnSwipeOpen={setReplyMessage} /> )} renderChatFooter={() => ( )} renderAvatar={null} maxComposerHeight={100} renderComposer={(props) => } keyboardShouldPersistTaps="handled" renderChatEmpty={() => ( {`No messages yet.\nFeel free to start the conversation.`} )} shouldUpdateMessage={shouldUpdateMessage} scrollToBottom={true} scrollToBottomComponent={renderScrollToBottom} scrollToBottomStyle={{ backgroundColor: 'transparent' }} parsePatterns={(linkStyle) => [ { type: 'url', style: { color: Colors.ORANGE, textDecorationLine: 'underline' }, onPress: (url: string) => Linking.openURL(url), onLongPress: (url: string) => { Clipboard.setString(url ?? ''); Alert.alert('Link copied'); } } ]} infiniteScroll={true} loadEarlier={hasMoreMessages} isLoadingEarlier={isLoadingEarlier} onLoadEarlier={loadEarlierMessages} renderLoadEarlier={() => ( )} /> ) : ( )} {selectedMedia && selectedMedia?.includes('.mp4') ? ( {renderSelectedMessage()} { modalInfo.action(); closeModal(); }} /> ); }; export default ChatScreen;