浏览代码

attachments (files/photos/video)

Viktoriia 5 月之前
父节点
当前提交
2dc52412b3

+ 1 - 1
assets/icons/messages/megaphone.svg

@@ -1,6 +1,6 @@
 <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
 <g clip-path="url(#clip0_4475_38582)">
-<path d="M16.875 1.125C16.875 0.671488 16.6008 0.26016 16.1789 0.084379C15.757 -0.0914022 15.2754 0.00703526 14.952 0.326957L13.4191 1.86329C11.7316 3.55079 9.44297 4.5 7.05586 4.5H6.75H5.625H2.25C1.00898 4.5 0 5.50899 0 6.75V10.125C0 11.366 1.00898 12.375 2.25 12.375V16.875C2.25 17.4973 2.75273 18 3.375 18H5.625C6.24727 18 6.75 17.4973 6.75 16.875V12.375H7.05586C9.44297 12.375 11.7316 13.3242 13.4191 15.0117L14.952 16.5445C15.2754 16.868 15.757 16.9629 16.1789 16.7871C16.6008 16.6113 16.875 16.2035 16.875 15.7465V10.5574C17.5289 10.2481 18 9.41485 18 8.43399C18 7.45313 17.5289 6.61993 16.875 6.31055V1.125ZM14.625 3.82149V8.4375V13.0535C12.5578 11.1727 9.86133 10.125 7.05586 10.125H6.75V6.75H7.05586C9.86133 6.75 12.5578 5.70235 14.625 3.82149Z" fill="#EF5B5B"/>
+<path d="M16.875 1.125C16.875 0.671488 16.6008 0.26016 16.1789 0.084379C15.757 -0.0914022 15.2754 0.00703526 14.952 0.326957L13.4191 1.86329C11.7316 3.55079 9.44297 4.5 7.05586 4.5H6.75H5.625H2.25C1.00898 4.5 0 5.50899 0 6.75V10.125C0 11.366 1.00898 12.375 2.25 12.375V16.875C2.25 17.4973 2.75273 18 3.375 18H5.625C6.24727 18 6.75 17.4973 6.75 16.875V12.375H7.05586C9.44297 12.375 11.7316 13.3242 13.4191 15.0117L14.952 16.5445C15.2754 16.868 15.757 16.9629 16.1789 16.7871C16.6008 16.6113 16.875 16.2035 16.875 15.7465V10.5574C17.5289 10.2481 18 9.41485 18 8.43399C18 7.45313 17.5289 6.61993 16.875 6.31055V1.125ZM14.625 3.82149V8.4375V13.0535C12.5578 11.1727 9.86133 10.125 7.05586 10.125H6.75V6.75H7.05586C9.86133 6.75 12.5578 5.70235 14.625 3.82149Z"/>
 </g>
 <defs>
 <clipPath id="clip0_4475_38582">

+ 2 - 1
package.json

@@ -62,9 +62,10 @@
     "react-native-device-detection": "^0.2.1",
     "react-native-document-picker": "^9.3.1",
     "react-native-emoji-selector": "^0.2.0",
+    "react-native-file-viewer": "^2.1.5",
     "react-native-gesture-handler": "~2.16.1",
     "react-native-get-random-values": "^1.11.0",
-    "react-native-gifted-chat": "^2.6.3",
+    "react-native-gifted-chat": "^2.6.5",
     "react-native-google-places-autocomplete": "^2.5.7",
     "react-native-haptic-feedback": "^2.3.2",
     "react-native-image-viewing": "^0.2.2",

+ 314 - 32
src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx

@@ -13,7 +13,8 @@ import {
   ActivityIndicator,
   AppState,
   AppStateStatus,
-  TextInput
+  TextInput,
+  Animated
 } from 'react-native';
 import {
   GiftedChat,
@@ -32,7 +33,7 @@ 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 { ResizeMode, Video, Audio, AVPlaybackStatus } from 'expo-av';
 import ChatMessageBox from '../Components/ChatMessageBox';
 import ReplyMessageBar from '../Components/ReplyMessageBar';
 import { useSharedValue, withTiming } from 'react-native-reanimated';
@@ -62,9 +63,14 @@ import { usePushNotification } from 'src/contexts/PushNotificationContext';
 import ReactionsListModal from '../Components/ReactionsListModal';
 import { dismissChatNotifications } from '../utils';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
+import * as Location from 'expo-location';
+import FileViewer from 'react-native-file-viewer';
+import * as FileSystem from 'expo-file-system';
+import ImageView from 'better-react-native-image-viewing';
 
 import BanIcon from 'assets/icons/messages/ban.svg';
 import AttachmentsModal from '../Components/AttachmentsModal';
+import ChatOptionsBlock from '../Components/ChatOptionsBlock';
 
 const options = {
   enableVibrateFallback: true,
@@ -147,6 +153,10 @@ const ChatScreen = ({ route }: { route: any }) => {
   const [hasMoreMessages, setHasMoreMessages] = useState(true);
   const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
 
+  const [isPlaying, setIsPlaying] = useState(false);
+  const [isBuffering, setIsBuffering] = useState(false);
+  const videoRef = useRef<Video>(null);
+
   const appState = useRef(AppState.currentState);
   const textInputRef = useRef<TextInput>(null);
 
@@ -156,6 +166,232 @@ const ChatScreen = ({ route }: { route: any }) => {
     setModalInfo({ ...modalInfo, visible: false });
   };
 
+  useEffect(() => {
+    Audio.setAudioModeAsync({
+      allowsRecordingIOS: false,
+      staysActiveInBackground: false,
+      playsInSilentModeIOS: true,
+      shouldDuckAndroid: true,
+      playThroughEarpieceAndroid: false
+    });
+  }, []);
+
+  const onSendMedia = useCallback((files: { uri: string; type: 'image' | 'video' }[]) => {
+    const newMsgs = files.map((file) => {
+      const msg: IMessage = {
+        _id: Date.now() + Math.random(),
+        text: '',
+        createdAt: new Date(),
+        user: { _id: +currentUserId, name: 'Me' }
+      };
+
+      if (file.type === 'image') {
+        msg.image = file.uri;
+      } else if (file.type === 'video') {
+        msg.video = file.uri;
+      }
+      return msg;
+    });
+
+    setMessages((prev) => GiftedChat.append(prev, newMsgs));
+  }, []);
+
+  const onSendLocation = useCallback((coords: { latitude: number; longitude: number }) => {
+    const locMsg: IMessage = {
+      _id: Date.now() + Math.random(),
+      text: `Location: lat=${coords.latitude}, lon=${coords.longitude}`,
+      createdAt: new Date(),
+      user: { _id: +currentUserId, name: 'Me' },
+      location: coords
+    };
+    setMessages((prev) => GiftedChat.append(prev, [locMsg]));
+  }, []);
+
+  const onSendFile = useCallback((files: { uri: string; type: string; name?: string }[]) => {
+    const newMsgs = files.map((file) => {
+      const msg: IMessage = {
+        _id: Date.now() + Math.random(),
+        text: '',
+        createdAt: new Date(),
+        user: { _id: +currentUserId, name: 'Me' }
+      };
+
+      if (file.type.includes('image')) {
+        msg.image = file.uri;
+      } else if (file.type.includes('video')) {
+        msg.video = file.uri;
+      } else {
+        msg.attachment = {
+          uri: file.uri,
+          type: file.type,
+          name: file.name || 'Attachment'
+        };
+      }
+
+      return msg;
+    });
+
+    setMessages((prev) => GiftedChat.append(prev, newMsgs));
+  }, []);
+
+  async function openFileInApp(uri: string, fileName: string) {
+    try {
+      // const fileUri = FileSystem.cacheDirectory + encodeURIComponent(fileName);
+
+      // const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri);
+
+      await FileViewer.open(uri, {
+        showOpenWithDialog: true,
+        showAppsSuggestions: true
+      });
+    } catch (err) {
+      console.warn('openFileInApp error:', err);
+      Alert.alert('Cannot open file', 'No application found to open this file.');
+    }
+  }
+
+  async function downloadFileToDevice(uri: string, fileName: string) {
+    try {
+      const downloadFolder = FileSystem.documentDirectory || FileSystem.cacheDirectory;
+      const fileUri = downloadFolder + encodeURIComponent(fileName);
+
+      const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri);
+
+      Alert.alert('File Saved', `File has been saved to:\n${localUri}`);
+    } catch (err) {
+      console.warn('downloadFileToDevice error:', err);
+      Alert.alert('Error', 'Could not download the file.');
+    }
+  }
+
+  const renderMessageFile = (props: BubbleProps<CustomMessage>) => {
+    const { currentMessage } = props;
+    const leftMessage = currentMessage?.user?._id !== +currentUserId;
+    if (!currentMessage?.attachment) return null;
+
+    const { uri, type, name } = currentMessage.attachment;
+    const fileName = name ?? 'Attachment';
+
+    return (
+      <TouchableOpacity
+        style={[
+          styles.fileContainer,
+          { backgroundColor: leftMessage ? 'rgba(15, 63, 79, 0.2)' : 'rgba(244, 244, 244, 0.2)' }
+        ]}
+        onPress={() => openFileInApp(uri, fileName)}
+        onLongPress={() => downloadFileToDevice(uri, fileName)}
+      >
+        <MaterialCommunityIcons
+          name="file"
+          size={32}
+          color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
+        />
+        <Text
+          style={[
+            styles.fileNameText,
+            { color: leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT }
+          ]}
+        >
+          {fileName}
+        </Text>
+      </TouchableOpacity>
+    );
+  };
+
+  const onShareLiveLocation = useCallback(() => {
+    const liveMsg: IMessage = {
+      _id: 'live-loc-' + Date.now(),
+      text: 'Sharing live location...',
+      createdAt: new Date(),
+      user: { _id: +currentUserId, name: 'Me' },
+      system: false
+    };
+    setMessages((prev) => GiftedChat.append(prev, [liveMsg]));
+  }, []);
+
+  const renderMessageVideo = (props: any) => {
+    const { currentMessage } = props;
+    if (!currentMessage?.video) return null;
+
+    return (
+      <View style={{ width: 200, height: 200, backgroundColor: Colors.DARK_BLUE }}>
+        <Video
+          ref={videoRef}
+          source={{ uri: currentMessage.video }}
+          style={{ flex: 1 }}
+          useNativeControls
+          resizeMode={ResizeMode.CONTAIN}
+          // isLooping
+          isMuted={false}
+          volume={1.0}
+          shouldCorrectPitch
+          onPlaybackStatusUpdate={(playbackStatus) => {
+            if (!playbackStatus.isLoaded) {
+              setIsPlaying(false);
+              setIsBuffering(false);
+              return;
+            }
+
+            setIsPlaying(playbackStatus.isPlaying);
+            setIsBuffering(playbackStatus.isBuffering ?? false);
+          }}
+        />
+
+        {isBuffering && (
+          <View
+            style={{
+              position: 'absolute',
+              top: 0,
+              left: 0,
+              right: 0,
+              bottom: 0,
+              alignItems: 'center',
+              justifyContent: 'center'
+            }}
+          >
+            <ActivityIndicator size="large" color="#FFF" />
+          </View>
+        )}
+
+        {isBuffering && (
+          <View
+            style={{
+              position: 'absolute',
+              top: 0,
+              left: 0,
+              right: 0,
+              bottom: 0,
+              alignItems: 'center',
+              justifyContent: 'center'
+            }}
+          >
+            <ActivityIndicator size="large" color="#FFF" />
+          </View>
+        )}
+
+        {!isPlaying && !isBuffering ? (
+          <TouchableOpacity
+            style={{
+              position: 'absolute',
+              top: 0,
+              left: 0,
+              right: 0,
+              bottom: 0,
+              alignItems: 'center',
+              justifyContent: 'center'
+            }}
+            onPress={async () => {
+              await videoRef.current?.presentFullscreenPlayer();
+              await videoRef.current?.playAsync();
+            }}
+          >
+            <MaterialCommunityIcons name="play-circle-outline" size={48} color="#FFF" />
+          </TouchableOpacity>
+        ) : null}
+      </View>
+    );
+  };
+
   useEffect(() => {
     let unsubscribe: any;
 
@@ -691,6 +927,11 @@ const ChatScreen = ({ route }: { route: any }) => {
         handleDeleteMessage(selectedMessage.currentMessage?._id);
         setIsModalVisible(false);
         break;
+      case 'download':
+        console.log('download');
+        downloadFileToDevice(selectedMessage.currentMessage?.image, 'Attachment');
+        setIsModalVisible(false);
+        break;
       default:
         break;
     }
@@ -1018,6 +1259,7 @@ const ChatScreen = ({ route }: { route: any }) => {
     return (
       <TouchableOpacity
         onPress={() => setSelectedMedia(currentMessage.image)}
+        onLongPress={() => handleLongPress(currentMessage, props)}
         style={styles.imageContainer}
       >
         <Image source={{ uri: currentMessage.image }} style={styles.chatImage} resizeMode="cover" />
@@ -1095,6 +1337,44 @@ const ChatScreen = ({ route }: { route: any }) => {
         ? Colors.DARK_BLUE
         : Colors.FILL_LIGHT;
 
+    if (currentMessage.attachment) {
+      return (
+        <View
+          key={`${currentMessage._id}-${isHighlighted ? 'highlighted' : 'normal'}`}
+          ref={(ref) => {
+            if (ref && currentMessage) {
+              messageRefs.current[currentMessage._id] = ref;
+            }
+          }}
+          collapsable={false}
+        >
+          <Bubble
+            {...props}
+            wrapperStyle={{
+              right: {
+                backgroundColor: backgroundColor
+              },
+              left: {
+                backgroundColor: backgroundColor
+              }
+            }}
+            textStyle={{
+              left: {
+                color: Colors.DARK_BLUE
+              },
+              right: {
+                color: Colors.FILL_LIGHT
+              }
+            }}
+            onLongPress={() => handleLongPress(currentMessage, props)}
+            renderTicks={() => null}
+            renderTime={renderTimeContainer}
+            renderCustomView={() => renderMessageFile(props)}
+          />
+        </View>
+      );
+    }
+
     return (
       <View
         key={`${currentMessage._id}-${isHighlighted ? 'highlighted' : 'normal'}`}
@@ -1136,24 +1416,31 @@ const ChatScreen = ({ route }: { route: any }) => {
       payload: {
         name: userName,
         uid: id,
-        setModalInfo
+        setModalInfo,
+        closeOptions: () => {},
+        onSendMedia,
+        onSendLocation,
+        onShareLiveLocation,
+        onSendFile
       } as any
     });
   };
 
   const renderInputToolbar = (props: any) => (
-    <InputToolbar
-      {...props}
-      renderActions={() => (
-        <Actions
-          icon={() => <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />}
-          onPressActionButton={openAttachmentsModal}
-        />
-      )}
-      containerStyle={{
-        backgroundColor: Colors.FILL_LIGHT
-      }}
-    />
+    <View>
+      <InputToolbar
+        {...props}
+        renderActions={() => (
+          <Actions
+            icon={() => <MaterialCommunityIcons name={'plus'} size={28} color={Colors.DARK_BLUE} />}
+            onPressActionButton={openAttachmentsModal}
+          />
+        )}
+        containerStyle={{
+          backgroundColor: Colors.FILL_LIGHT
+        }}
+      />
+    </View>
   );
 
   const renderScrollToBottom = () => {
@@ -1271,7 +1558,11 @@ const ChatScreen = ({ route }: { route: any }) => {
                 {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
               </View>
             )}
-            textInputProps={{ ...styles.composer, selectionColor: Colors.LIGHT_GRAY }}
+            renderMessageVideo={renderMessageVideo}
+            textInputProps={{
+              ...styles.composer,
+              selectionColor: Colors.LIGHT_GRAY
+            }}
             placeholder=""
             renderMessage={(props) => (
               <ChatMessageBox
@@ -1323,22 +1614,13 @@ const ChatScreen = ({ route }: { route: any }) => {
           <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
         )}
 
-        <Modal visible={!!selectedMedia} transparent={true}>
-          <View style={styles.modalContainer}>
-            {selectedMedia && selectedMedia?.includes('.mp4') ? (
-              <Video
-                source={{ uri: selectedMedia }}
-                style={styles.fullScreenMedia}
-                useNativeControls
-              />
-            ) : (
-              <Image source={{ uri: selectedMedia ?? '' }} style={styles.fullScreenMedia} />
-            )}
-            <TouchableOpacity onPress={() => setSelectedMedia(null)} style={styles.closeButton}>
-              <MaterialCommunityIcons name="close" size={30} color="white" />
-            </TouchableOpacity>
-          </View>
-        </Modal>
+        <ImageView
+          images={[{ uri: selectedMedia }]}
+          imageIndex={0}
+          visible={!!selectedMedia}
+          onRequestClose={() => setSelectedMedia(null)}
+          backgroundColor={Colors.DARK_BLUE}
+        />
 
         <ReactModal
           isVisible={isModalVisible}

+ 44 - 0
src/screens/InAppScreens/MessagesScreen/ChatScreen/styles.tsx

@@ -151,5 +151,49 @@ export const styles = StyleSheet.create({
     paddingHorizontal: 6,
     paddingVertical: 4,
     gap: 6
+  },
+  optionsContainer: {
+    width: '100%',
+    backgroundColor: Colors.FILL_LIGHT,
+    // borderTopWidth: 1,
+    // borderTopColor: '#ccc'
+  },
+  optionRow: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    paddingHorizontal: '5%',
+    marginVertical: 20,
+    flexWrap: 'wrap'
+  },
+  optionItem: {
+    width: '30%',
+    paddingVertical: 8,
+    marginBottom: 12,
+    alignItems: 'center'
+  },
+  optionLabel: {
+    marginTop: 6,
+    fontSize: 12,
+    color: Colors.DARK_BLUE,
+    fontWeight: '700'
+  },
+  fileContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    padding: 6,
+    borderRadius: 8,
+    marginVertical: 8,
+    marginHorizontal: 8
+  },
+  fileNameText: {
+    marginLeft: 4,
+    fontSize: 14,
+    fontWeight: '600',
+    maxWidth: 200
+  },
+  fileTypeText: {
+    marginLeft: 6,
+    fontSize: 12,
+    color: Colors.LIGHT_GRAY
   }
 });

+ 1658 - 0
src/screens/InAppScreens/MessagesScreen/ChatScreen/test.tsx

@@ -0,0 +1,1658 @@
+import React, { useState, useCallback, useEffect, useRef } from 'react';
+import {
+  View,
+  TouchableOpacity,
+  Image,
+  Modal,
+  Text,
+  FlatList,
+  Dimensions,
+  Alert,
+  ScrollView,
+  Linking,
+  ActivityIndicator,
+  AppState,
+  AppStateStatus,
+  TextInput,
+  Keyboard,
+  Platform,
+  UIManager,
+  LayoutAnimation,
+  Animated,
+  Easing,
+  InputAccessoryView,
+  KeyboardAvoidingView
+} from 'react-native';
+import {
+  GiftedChat,
+  Bubble,
+  InputToolbar,
+  IMessage,
+  Send,
+  BubbleProps,
+  Composer,
+  TimeProps,
+  MessageProps,
+  Actions
+} 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 { ResizeMode, 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 * as Location from 'expo-location';
+
+import BanIcon from 'assets/icons/messages/ban.svg';
+import AttachmentsModal from '../Components/AttachmentsModal';
+import MegaphoneIcon from 'assets/icons/messages/megaphone.svg';
+import ChatOptionsBlock from '../Components/ChatOptionsBlock';
+
+// if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
+//   UIManager.setLayoutAnimationEnabledExperimental(false);
+// }
+
+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<CustomMessage[] | null>();
+  const navigation = useNavigation();
+  const [prevThenMessageId, setPrevThenMessageId] = useState<number>(-1);
+  const {
+    data: chatData,
+    refetch,
+    isFetching
+  } = usePostGetChatWithQuery(token, id, 50, prevThenMessageId, true);
+  const { mutateAsync: sendMessage } = usePostSendMessageMutation();
+
+  const swipeableRowRef = useRef<Swipeable | null>(null);
+  const messageContainerRef = useRef<FlatList<IMessage> | null>(null);
+  const [selectedMedia, setSelectedMedia] = useState<any>(null);
+
+  const [replyMessage, setReplyMessage] = useState<CustomMessage | null>(null);
+  const [modalInfo, setModalInfo] = useState({
+    visible: false,
+    type: 'confirm',
+    message: '',
+    action: () => {},
+    buttonTitle: '',
+    title: ''
+  });
+
+  const [selectedMessage, setSelectedMessage] = useState<BubbleProps<CustomMessage> | 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<number | null>(null);
+  const { mutateAsync: markMessagesAsRead } = usePostMessagesReadMutation();
+  const { mutateAsync: deleteMessage } = usePostDeleteMessageMutation();
+  const { mutateAsync: reactToMessage } = usePostReactToMessageMutation();
+
+  const [highlightedMessageId, setHighlightedMessageId] = useState<number | null>(null);
+  const [isRerendering, setIsRerendering] = useState<boolean>(false);
+  const [isTyping, setIsTyping] = useState<boolean>(false);
+
+  const messageRefs = useRef<{ [key: string]: any }>({});
+  const flatList = useRef<FlatList | null>(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 [showOptions, setShowOptions] = useState<boolean>(false);
+  const [keyboardHeight, setKeyboardHeight] = useState<number>(0);
+  const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
+  const [lastKeyboardHeight, setLastKeyboardHeight] = useState(300);
+
+  const appState = useRef(AppState.currentState);
+  const textInputRef = useRef<TextInput>(null);
+
+  const socket = useRef<WebSocket | null>(null);
+
+  const closeModal = () => {
+    setModalInfo({ ...modalInfo, visible: false });
+  };
+
+  const translateY = useRef(new Animated.Value(0)).current;
+  const keyboardAnimDuration = useRef(250);
+
+  // useEffect(() => {
+  //   const onKeyboardWillShow = (e: any) => {
+  //     setIsKeyboardOpen(true);
+  //     setLastKeyboardHeight(e.endCoordinates.height - insets.bottom || 300);
+  //     keyboardAnimDuration.current = e.duration;
+  //   };
+
+  //   const onKeyboardWillHide = (e: any) => {
+  //     setIsKeyboardOpen(false);
+  //   };
+
+  //   const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
+  //   const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
+
+  //   const kbShowSub = Keyboard.addListener(showEvent, onKeyboardWillShow);
+  //   const kbHideSub = Keyboard.addListener(hideEvent, onKeyboardWillHide);
+
+  //   return () => {
+  //     kbShowSub.remove();
+  //     kbHideSub.remove();
+  //   };
+  // }, [insets.bottom, showOptions, translateY]);
+
+  // useEffect(() => {
+  //   let toValue = 0;
+  //   if (isKeyboardOpen) {
+  //     toValue = keyboardHeight;
+  //   } else if (showOptions) {
+  //     toValue = keyboardHeight;
+  //   } else {
+  //     toValue = 0;
+  //   }
+
+  //   Animated.timing(translateY, {
+  //     toValue,
+  //     duration: keyboardAnimDuration.current ?? 250,
+  //     easing: Easing.inOut(Easing.ease),
+  //     useNativeDriver: false
+  //   }).start();
+  // }, [isKeyboardOpen, showOptions, keyboardHeight]);
+
+  const handleInputFocus = () => {
+    // if (!showOptions) {
+    //   setImmediate(() => {
+    //     if (Platform.OS === 'ios') {
+    //       LayoutAnimation.configureNext({
+    //         duration: keyboardAnimDuration.current ?? 250,
+    //         update: { type: 'keyboard' }
+    //       });
+    //     }
+    //     setShowOptions(true);
+    //     Animated.timing(translateY, {
+    //       toValue: keyboardHeight,
+    //       duration: keyboardAnimDuration.current ?? 250,
+    //       easing: Easing.inOut(Easing.ease),
+    //       useNativeDriver: false
+    //     }).start();
+    //   });
+    // }
+  };
+  const toggleOptions = useCallback(() => {
+    if (showOptions && !isKeyboardOpen) {
+      textInputRef.current?.focus();
+    } else {
+      if (isKeyboardOpen) {
+        Keyboard.dismiss();
+        setShowOptions(true);
+      } else {
+        if (Platform.OS === 'ios') {
+          LayoutAnimation.configureNext({
+            duration: keyboardAnimDuration.current ?? 250,
+            update: { type: 'keyboard' }
+          });
+        }
+        !showOptions && setShowOptions(true);
+      }
+    }
+  }, [showOptions, isKeyboardOpen]);
+
+  // const handleTouchOutside = () => {
+  //   if (showOptions) {
+  //     if (Platform.OS === 'ios') {
+  //       LayoutAnimation.configureNext({
+  //         duration: keyboardAnimDuration.current ?? 250,
+  //         update: { type: 'keyboard' }
+  //       });
+  //     }
+  //     setShowOptions(false);
+  //   }
+  //   Keyboard.dismiss();
+  // };
+
+  const onSendMedia = useCallback((files: { uri: string; type: 'image' }[]) => {
+    const newMsgs = files.map((file) => {
+      const msg: IMessage = {
+        _id: Date.now() + Math.random(),
+        text: '',
+        createdAt: new Date(),
+        user: { _id: +currentUserId, name: 'Me' }
+      };
+
+      if (file.type === 'image') {
+        msg.image = file.uri;
+      } else if (file.type === 'video') {
+        msg.video = file.uri;
+      }
+      return msg;
+    });
+
+    setMessages((prev) => GiftedChat.append(prev, newMsgs));
+  }, []);
+
+  const onSendLocation = useCallback((coords: { latitude: number; longitude: number }) => {
+    const locMsg: IMessage = {
+      _id: Date.now() + Math.random(),
+      text: `Location: lat=${coords.latitude}, lon=${coords.longitude}`,
+      createdAt: new Date(),
+      user: { _id: +currentUserId, name: 'Me' },
+      location: coords
+    };
+    setMessages((prev) => GiftedChat.append(prev, [locMsg]));
+  }, []);
+
+  const onShareLiveLocation = useCallback(() => {
+    const liveMsg: IMessage = {
+      _id: 'live-loc-' + Date.now(),
+      text: 'Sharing live location...',
+      createdAt: new Date(),
+      user: { _id: +currentUserId, name: 'Me' },
+      system: false
+    };
+    setMessages((prev) => GiftedChat.append(prev, [liveMsg]));
+  }, []);
+
+  const renderMessageVideo = (props: any) => {
+    const { currentMessage } = props;
+    if (!currentMessage?.video) return null;
+
+    return (
+      <View style={{ width: 200, height: 200, backgroundColor: '#000' }}>
+        <Video
+          source={{ uri: currentMessage.video }}
+          style={{ flex: 1 }}
+          useNativeControls
+          resizeMode={ResizeMode.CONTAIN}
+        />
+      </View>
+    );
+  };
+
+  const renderOptionsView = () => {
+    // if (!showOptions) return null;
+    return (
+      <Animated.View
+        style={{
+          transform: [{ translateY }]
+        }}
+      >
+        <ChatOptionsBlock
+          blockHeight={lastKeyboardHeight}
+          closeOptions={() => setShowOptions(false)}
+          onSendMedia={onSendMedia}
+          onSendLocation={onSendLocation}
+          onShareLiveLocation={onShareLiveLocation}
+        />
+      </Animated.View>
+    );
+  };
+
+  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<Set<number>>(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 (
+        <View style={styles.unreadMessagesContainer}>
+          <Text style={styles.unreadMessagesText}>{props.currentMessage.text}</Text>
+        </View>
+      );
+    }
+    return null;
+  };
+
+  const clearReplyMessage = () => setReplyMessage(null);
+
+  const handleLongPress = (message: CustomMessage, props: BubbleProps<CustomMessage>) => {
+    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<CustomMessage>) => {
+    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 (
+      <View
+        style={[
+          styles.bottomContainer,
+          {
+            justifyContent: hasReactions ? 'space-between' : 'flex-end'
+          }
+        ]}
+      >
+        {hasReactions && (
+          <TouchableOpacity
+            style={[
+              styles.bottomCustomContainer,
+              {
+                backgroundColor:
+                  time.position === 'left' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.2)'
+              }
+            ]}
+            onPress={() =>
+              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<string, { count: number }>, { reaction }: { reaction: string }) => {
+                  if (!acc[reaction]) {
+                    acc[reaction] = { count: 0 };
+                  }
+                  acc[reaction].count += 1;
+                  return acc;
+                },
+                {}
+              )
+            ).map(([emoji, { count }]: any) => {
+              return (
+                <View key={emoji}>
+                  <Text style={{}}>
+                    {emoji}
+                    {(count as number) > 1 ? ` ${count}` : ''}
+                  </Text>
+                </View>
+              );
+            })}
+          </TouchableOpacity>
+        )}
+        <View style={styles.timeContainer}>
+          <Text style={styles.timeText}>{formattedTime}</Text>
+          {renderTicks(time.currentMessage)}
+        </View>
+      </View>
+    );
+  };
+
+  const renderSelectedMessage = () =>
+    selectedMessage && (
+      <View
+        style={{
+          maxHeight: '80%',
+          width: messagePosition?.width,
+          position: 'absolute',
+          top: messagePosition?.y,
+          left: messagePosition?.x
+        }}
+      >
+        <ScrollView>
+          <Bubble
+            {...selectedMessage}
+            wrapperStyle={{
+              right: { backgroundColor: Colors.DARK_BLUE },
+              left: { backgroundColor: Colors.FILL_LIGHT }
+            }}
+            textStyle={{
+              right: { color: Colors.WHITE },
+              left: { color: Colors.DARK_BLUE }
+            }}
+            renderTicks={() => null}
+            renderTime={renderTimeContainer}
+          />
+        </ScrollView>
+      </View>
+    );
+
+  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<CustomMessage>) => {
+    if (!props.currentMessage) {
+      return null;
+    }
+    const { currentMessage } = props;
+
+    if (!currentMessage || !currentMessage?.replyMessage) {
+      return null;
+    }
+
+    return (
+      <TouchableOpacity
+        style={[
+          styles.replyMessageContainer,
+          {
+            backgroundColor:
+              currentMessage.user._id === id ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.2)',
+            borderColor: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE
+          }
+        ]}
+        onPress={() => {
+          if (currentMessage?.replyMessage?.id) {
+            scrollToMessage(currentMessage.replyMessage.id);
+          }
+        }}
+      >
+        <View style={styles.replyContent}>
+          <Text
+            style={[
+              styles.replyAuthorName,
+              { color: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE }
+            ]}
+          >
+            {currentMessage.replyMessage.name}
+          </Text>
+
+          <Text
+            numberOfLines={1}
+            style={[
+              styles.replyMessageText,
+              { color: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE }
+            ]}
+          >
+            {currentMessage.replyMessage.text}
+          </Text>
+        </View>
+      </TouchableOpacity>
+    );
+  };
+
+  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 (
+      <TouchableOpacity
+        onPress={() => setSelectedMedia(currentMessage.image)}
+        style={styles.imageContainer}
+      >
+        <Image source={{ uri: currentMessage.image }} style={styles.chatImage} resizeMode="cover" />
+      </TouchableOpacity>
+    );
+  };
+
+  const renderTicks = (message: CustomMessage) => {
+    if (message.user._id === id) return null;
+
+    return message.received ? (
+      <View>
+        <MaterialCommunityIcons name="check-all" size={16} color={Colors.WHITE} />
+      </View>
+    ) : message.sent ? (
+      <View>
+        <MaterialCommunityIcons name="check" size={16} color={Colors.WHITE} />
+      </View>
+    ) : message.pending ? (
+      <View>
+        <MaterialCommunityIcons name="check" size={16} color={Colors.LIGHT_GRAY} />
+      </View>
+    ) : null;
+  };
+
+  const renderBubble = (props: BubbleProps<CustomMessage>) => {
+    const { currentMessage } = props;
+
+    if (currentMessage.deleted) {
+      const text = currentMessage.text.length
+        ? props.currentMessage.text
+        : 'This message was deleted';
+
+      return (
+        <View>
+          <Bubble
+            {...props}
+            renderTime={() => null}
+            currentMessage={{
+              ...props.currentMessage,
+              text: text
+            }}
+            renderMessageText={() => (
+              <View style={{ paddingHorizontal: 12, paddingVertical: 6 }}>
+                <Text style={{ color: Colors.LIGHT_GRAY, fontStyle: 'italic', fontSize: 12 }}>
+                  {text}
+                </Text>
+              </View>
+            )}
+            wrapperStyle={{
+              right: {
+                backgroundColor: Colors.DARK_BLUE
+              },
+              left: {
+                backgroundColor: Colors.FILL_LIGHT
+              }
+            }}
+            textStyle={{
+              left: {
+                color: Colors.DARK_BLUE
+              },
+              right: {
+                color: Colors.WHITE
+              }
+            }}
+          />
+        </View>
+      );
+    }
+
+    const isHighlighted = currentMessage._id === highlightedMessageId;
+    const backgroundColor = isHighlighted
+      ? Colors.ORANGE
+      : currentMessage.user._id === +currentUserId
+        ? Colors.DARK_BLUE
+        : Colors.FILL_LIGHT;
+
+    return (
+      <View
+        key={`${currentMessage._id}-${isHighlighted ? 'highlighted' : 'normal'}`}
+        ref={(ref) => {
+          if (ref && currentMessage) {
+            messageRefs.current[currentMessage._id] = ref;
+          }
+        }}
+        collapsable={false}
+      >
+        <Bubble
+          {...props}
+          wrapperStyle={{
+            right: {
+              backgroundColor: backgroundColor
+            },
+            left: {
+              backgroundColor: backgroundColor
+            }
+          }}
+          textStyle={{
+            left: {
+              color: Colors.DARK_BLUE
+            },
+            right: {
+              color: Colors.FILL_LIGHT
+            }
+          }}
+          onLongPress={() => handleLongPress(currentMessage, props)}
+          renderTicks={() => null}
+          renderTime={renderTimeContainer}
+        />
+      </View>
+    );
+  };
+
+  const openAttachmentsModal = () => {
+    SheetManager.show('chat-attachments', {
+      payload: {
+        name: userName,
+        uid: id,
+        setModalInfo,
+        closeOptions: () => setShowOptions(false),
+        onSendMedia,
+        onSendLocation,
+        onShareLiveLocation
+      } as any
+    });
+  };
+
+  const renderInputToolbar = (props: any) => (
+    <View>
+      <InputToolbar
+        {...props}
+        renderActions={() => (
+          <Actions
+            icon={() => (
+              <MaterialCommunityIcons
+                name={!isKeyboardOpen && showOptions ? 'keyboard-outline' : 'plus'}
+                size={28}
+                color={Colors.DARK_BLUE}
+              />
+            )}
+            onPressActionButton={openAttachmentsModal}
+          />
+        )}
+        containerStyle={{
+          backgroundColor: Colors.FILL_LIGHT
+        }}
+      />
+      {/* <InputAccessoryView nativeID={'inputAccessoryViewID'}>
+    <InputToolbar
+        {...props}
+        renderActions={() => (
+          <Actions
+            icon={() => (
+              <MaterialCommunityIcons
+                name={!isKeyboardOpen && showOptions ? 'keyboard-outline' : 'plus'}
+                size={28}
+                color={Colors.DARK_BLUE}
+              />
+            )}
+            onPressActionButton={toggleOptions}
+          />
+        )}
+        containerStyle={{
+          backgroundColor: Colors.FILL_LIGHT
+        }}
+      />
+    </InputAccessoryView> */}
+
+      {/* {showOptions && renderOptionsView()} */}
+    </View>
+  );
+
+  const renderScrollToBottom = () => {
+    return (
+      <TouchableOpacity
+        style={styles.scrollToBottom}
+        onPress={() => {
+          if (flatList.current) {
+            flatList.current.scrollToIndex({ index: 0, animated: true });
+          }
+        }}
+      >
+        <MaterialCommunityIcons name="chevron-down" size={24} color={Colors.WHITE} />
+      </TouchableOpacity>
+    );
+  };
+
+  const shouldUpdateMessage = (
+    props: MessageProps<IMessage>,
+    nextProps: MessageProps<IMessage>
+  ) => {
+    setIsRerendering(true);
+    const currentId = nextProps.currentMessage._id;
+    return currentId === highlightedMessageId;
+  };
+
+  return (
+    <SafeAreaView
+      edges={['top']}
+      style={{
+        height: '100%'
+      }}
+    >
+      <KeyboardAvoidingView
+        style={{ flex: 1 }}
+        behavior={'height'}
+        keyboardVerticalOffset={Platform.select({ android: 34 })}
+      >
+        <View style={{ paddingHorizontal: '5%' }}>
+          <Header
+            label={userName}
+            textColor={userType !== 'normal' ? Colors.RED : Colors.DARK_BLUE}
+            rightElement={
+              <TouchableOpacity
+                onPress={() =>
+                  navigation.navigate(
+                    ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: id }] as never)
+                  )
+                }
+                disabled={userType !== 'normal'}
+              >
+                {avatar && userType === 'normal' ? (
+                  <Image source={{ uri: API_HOST + avatar }} style={styles.avatar} />
+                ) : userType === 'normal' ? (
+                  <AvatarWithInitials
+                    text={
+                      name
+                        .split(/ (.+)/)
+                        .map((n) => n[0])
+                        .join('') ?? ''
+                    }
+                    flag={API_HOST + 'flag.png'}
+                    size={30}
+                    fontSize={12}
+                  />
+                ) : (
+                  <BanIcon fill={Colors.RED} width={30} height={30} />
+                )}
+              </TouchableOpacity>
+            }
+          />
+        </View>
+
+        <GestureHandlerRootView style={styles.container}>
+          {messages ? (
+            <GiftedChat
+              messages={messages as CustomMessage[]}
+              listViewProps={{
+                ref: flatList,
+                // onTouchStart: handleTouchOutside,
+                // onTouchEnd: handleTouchOutside,
+                showsVerticalScrollIndicator: false,
+                initialNumToRender: 30,
+                onViewableItemsChanged: handleViewableItemsChanged,
+                viewabilityConfig: { itemVisiblePercentThreshold: 50 },
+                onScrollToIndexFailed: (info: any) => {
+                  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) => (
+                <View style={styles.sendBtn}>
+                  {props.text?.trim() && (
+                    <Send
+                      {...props}
+                      containerStyle={{
+                        justifyContent: 'center'
+                      }}
+                    >
+                      <SendIcon fill={Colors.DARK_BLUE} />
+                    </Send>
+                  )}
+                  {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
+                </View>
+              )}
+              renderMessageVideo={renderMessageVideo}
+              textInputProps={{
+                ...styles.composer,
+                selectionColor: Colors.LIGHT_GRAY,
+                onFocus: handleInputFocus
+              }}
+              isKeyboardInternallyHandled={false}
+              placeholder=""
+              renderMessage={(props) => (
+                <ChatMessageBox
+                  {...(props as MessageProps<CustomMessage>)}
+                  updateRowRef={updateRowRef}
+                  setReplyOnSwipeOpen={setReplyMessage}
+                />
+              )}
+              renderChatFooter={() => (
+                <ReplyMessageBar clearReply={clearReplyMessage} message={replyMessage} />
+              )}
+              renderAvatar={null}
+              maxComposerHeight={100}
+              renderComposer={(props) => <Composer {...props} />}
+              keyboardShouldPersistTaps="handled"
+              renderChatEmpty={() => (
+                <View style={styles.emptyChat}>
+                  <Text
+                    style={styles.emptyChatText}
+                  >{`No messages yet.\nFeel free to start the conversation.`}</Text>
+                </View>
+              )}
+              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={() => (
+                <View style={{ paddingVertical: 20 }}>
+                  <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
+                </View>
+              )}
+            />
+          ) : (
+            <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
+          )}
+
+          <Modal visible={!!selectedMedia} transparent={true}>
+            <View style={styles.modalContainer}>
+              {selectedMedia && selectedMedia?.includes('.mp4') ? (
+                <Video
+                  source={{ uri: selectedMedia }}
+                  style={styles.fullScreenMedia}
+                  useNativeControls
+                />
+              ) : (
+                <Image source={{ uri: selectedMedia ?? '' }} style={styles.fullScreenMedia} />
+              )}
+              <TouchableOpacity onPress={() => setSelectedMedia(null)} style={styles.closeButton}>
+                <MaterialCommunityIcons name="close" size={30} color="white" />
+              </TouchableOpacity>
+            </View>
+          </Modal>
+
+          <ReactModal
+            isVisible={isModalVisible}
+            onBackdropPress={handleBackgroundPress}
+            style={styles.reactModalContainer}
+            animationIn="fadeIn"
+            animationOut="fadeOut"
+            useNativeDriver
+            backdropColor="transparent"
+          >
+            <BlurView
+              intensity={80}
+              style={styles.modalBackground}
+              experimentalBlurMethod="dimezisBlurView"
+            >
+              <TouchableOpacity
+                style={styles.modalBackground}
+                activeOpacity={1}
+                onPress={handleBackgroundPress}
+              >
+                <ReactionBar
+                  messagePosition={messagePosition}
+                  selectedMessage={selectedMessage}
+                  reactionEmojis={reactionEmojis}
+                  handleReactionPress={handleReactionPress}
+                  openEmojiSelector={openEmojiSelector}
+                />
+                {renderSelectedMessage()}
+                <OptionsMenu
+                  selectedMessage={selectedMessage}
+                  handleOptionPress={handleOptionPress}
+                  messagePosition={messagePosition}
+                />
+                <EmojiSelectorModal
+                  visible={emojiSelectorVisible}
+                  selectedMessage={selectedMessage}
+                  addReaction={addReaction}
+                  closeEmojiSelector={closeEmojiSelector}
+                />
+              </TouchableOpacity>
+            </BlurView>
+          </ReactModal>
+
+          <WarningModal
+            isVisible={modalInfo.visible}
+            onClose={closeModal}
+            type={modalInfo.type}
+            message={modalInfo.message}
+            buttonTitle={modalInfo.buttonTitle}
+            title={modalInfo.title}
+            action={() => {
+              modalInfo.action();
+              closeModal();
+            }}
+          />
+          <AttachmentsModal />
+          <ReactionsListModal />
+          <Animated.View
+            style={{
+              transform: [{ translateY }]
+            }}
+          >
+            {/* {renderOptionsView()} */}
+          </Animated.View>
+        </GestureHandlerRootView>
+      </KeyboardAvoidingView>
+      {showOptions && renderOptionsView()}
+      <View
+        style={{
+          height: insets.bottom,
+          backgroundColor: Colors.FILL_LIGHT
+        }}
+      />
+    </SafeAreaView>
+  );
+};
+
+export default ChatScreen;

+ 186 - 3
src/screens/InAppScreens/MessagesScreen/Components/AttachmentsModal.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useCallback, useState } from 'react';
 import { StyleSheet, TouchableOpacity, View, Text } from 'react-native';
 import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
 import { getFontSize } from 'src/utils';
@@ -6,8 +6,11 @@ import { Colors } from 'src/theme';
 import { WarningProps } from '../types';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 import { usePostReportConversationMutation } from '@api/chat';
+import * as ImagePicker from 'expo-image-picker';
+import * as DocumentPicker from 'react-native-document-picker';
 
 import MegaphoneIcon from 'assets/icons/messages/megaphone.svg';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
 
 const AttachmentsModal = () => {
   const insets = useSafeAreaInsets();
@@ -40,6 +43,115 @@ const AttachmentsModal = () => {
     }, 300);
   };
 
+  const handleOpenGallery = useCallback(async () => {
+    if (!chatData) return;
+    try {
+      const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
+      if (!perm.granted) {
+        console.warn('Permission for gallery not granted');
+        return;
+      }
+
+      const result = await ImagePicker.launchImageLibraryAsync({
+        mediaTypes: ImagePicker.MediaTypeOptions.All,
+        allowsMultipleSelection: true,
+        quality: 1,
+        selectionLimit: 4
+      });
+
+      if (!result.canceled && result.assets) {
+        const files = result.assets.map((asset) => ({
+          uri: asset.uri,
+          type: asset.type === 'video' ? 'video' : 'image'
+        }));
+        chatData.onSendMedia(files);
+      }
+      SheetManager.hide('chat-attachments');
+    } catch (err) {
+      console.warn('Gallery error: ', err);
+    }
+  }, [chatData?.onSendMedia, chatData?.closeOptions]);
+
+  const handleOpenCamera = useCallback(async () => {
+    if (!chatData) return;
+    try {
+      const perm = await ImagePicker.requestCameraPermissionsAsync();
+      if (!perm.granted) {
+        console.warn('Permission for camera not granted');
+        return;
+      }
+
+      const result = await ImagePicker.launchCameraAsync({
+        mediaTypes: ImagePicker.MediaTypeOptions.Images,
+        quality: 1
+      });
+
+      if (!result.canceled && result.assets) {
+        const files = result.assets.map((asset) => ({
+          uri: asset.uri,
+          type: asset.type === 'video' ? 'video' : 'image'
+        }));
+        chatData.onSendMedia(files);
+      }
+      SheetManager.hide('chat-attachments');
+    } catch (err) {
+      console.warn('Camera error: ', err);
+    }
+  }, [chatData?.onSendMedia, chatData?.closeOptions]);
+
+  const handleShareLocation = useCallback(async () => {
+    if (!chatData) return;
+    try {
+      // TODO:
+      // const { status } = await Location.requestForegroundPermissionsAsync();
+      // if (status !== 'granted') {}
+
+      // const loc = await Location.getCurrentPositionAsync({});
+      // const coords = { latitude: loc.coords.latitude, longitude: loc.coords.longitude };
+
+      const coords = { latitude: 50.4501, longitude: 30.5234 };
+      chatData.onSendLocation(coords);
+      SheetManager.hide('chat-attachments');
+    } catch (err) {
+      console.warn('Location error: ', err);
+    }
+  }, [chatData?.onSendLocation, chatData?.closeOptions]);
+
+  const handleShareLiveLocation = useCallback(() => {
+    if (!chatData) return;
+    chatData.onShareLiveLocation();
+    SheetManager.hide('chat-attachments');
+  }, [chatData?.onShareLiveLocation, chatData?.closeOptions]);
+
+  const handleSendFile = useCallback(async () => {
+    if (!chatData) return;
+
+    try {
+      const res = await DocumentPicker.pick({
+        type: [DocumentPicker.types.allFiles],
+        allowMultiSelection: false
+      });
+
+      const file = {
+        uri: res[0].uri,
+        name: res[0].name,
+        type: res[0].type
+      };
+
+      if (chatData.onSendFile) {
+        chatData.onSendFile([file]);
+      }
+    } catch (err) {
+      if (DocumentPicker.isCancel(err)) {
+        console.log('User canceled document picker');
+      } else {
+        console.warn('DocumentPicker error:', err);
+      }
+    }
+
+    SheetManager.hide('chat-attachments');
+  }, [chatData?.onSendFile, chatData?.closeOptions]);
+
   return (
     <ActionSheet
       id="chat-attachments"
@@ -48,7 +160,7 @@ const AttachmentsModal = () => {
         borderTopLeftRadius: 15,
         borderTopRightRadius: 15
       }}
-      defaultOverlayOpacity={0.5}
+      defaultOverlayOpacity={0.3}
       onBeforeShow={(sheetRef) => {
         const payload = sheetRef || null;
         handleSheetOpen(payload);
@@ -66,7 +178,7 @@ const AttachmentsModal = () => {
         }
       }}
     >
-      <View
+      {/* <View
         style={{
           backgroundColor: 'white',
           paddingHorizontal: 16,
@@ -75,10 +187,59 @@ const AttachmentsModal = () => {
           paddingBottom: 8 + insets.bottom
         }}
       >
+        <TouchableOpacity style={[styles.option]} onPress={handleReport}>
+          <Text style={[styles.optionText]}>Gallery</Text>
+          <MegaphoneIcon fill={Colors.DARK_BLUE} />
+        </TouchableOpacity>
+
+        <TouchableOpacity style={[styles.option]} onPress={handleReport}>
+          <Text style={[styles.optionText]}>Camera</Text>
+          <MegaphoneIcon fill={Colors.DARK_BLUE} />
+        </TouchableOpacity>
+
+        <TouchableOpacity style={[styles.option]} onPress={handleReport}>
+          <Text style={[styles.optionText]}>Location</Text>
+          <MegaphoneIcon fill={Colors.DARK_BLUE} />
+        </TouchableOpacity>
+
         <TouchableOpacity style={[styles.option, styles.dangerOption]} onPress={handleReport}>
           <Text style={[styles.optionText, styles.dangerText]}>Report {chatData?.name}</Text>
           <MegaphoneIcon fill={Colors.RED} />
         </TouchableOpacity>
+      </View> */}
+      <View style={[styles.container, { paddingBottom: 8 + insets.bottom }]}>
+        <View style={styles.optionRow}>
+          <TouchableOpacity style={styles.optionItem} onPress={handleOpenGallery}>
+            <MaterialCommunityIcons name="image" size={36} color={Colors.ORANGE} />
+            <Text style={styles.optionLabel}>Gallery</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity style={styles.optionItem} onPress={handleOpenCamera}>
+            <MaterialCommunityIcons name="camera" size={36} color={Colors.ORANGE} />
+            <Text style={styles.optionLabel}>Camera</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity style={styles.optionItem} onPress={handleShareLocation}>
+            <MaterialCommunityIcons name="map-marker" size={36} color={Colors.ORANGE} />
+            <Text style={styles.optionLabel}>Location</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity style={styles.optionItem} onPress={handleShareLiveLocation}>
+            <MaterialCommunityIcons name="navigation" size={36} color={Colors.ORANGE} />
+            <Text style={styles.optionLabel}>Live</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity style={styles.optionItem} onPress={handleSendFile}>
+            <MaterialCommunityIcons name="file" size={36} color={Colors.ORANGE} />
+            <Text style={styles.optionLabel}>File</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity style={styles.optionItem} onPress={handleReport}>
+            <MegaphoneIcon fill={Colors.RED} width={36} height={36} />
+            <Text style={styles.optionLabel}>Report</Text>
+          </TouchableOpacity>
+          <View style={styles.optionItem}></View>
+        </View>
       </View>
     </ActionSheet>
   );
@@ -102,6 +263,28 @@ const styles = StyleSheet.create({
   },
   dangerText: {
     color: Colors.RED
+  },
+  container: {
+    backgroundColor: Colors.WHITE
+  },
+  optionRow: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    paddingHorizontal: '5%',
+    marginVertical: 20,
+    flexWrap: 'wrap'
+  },
+  optionItem: {
+    width: '30%',
+    paddingVertical: 8,
+    marginBottom: 12,
+    alignItems: 'center'
+  },
+  optionLabel: {
+    marginTop: 6,
+    fontSize: 12,
+    color: Colors.DARK_BLUE,
+    fontWeight: '700'
   }
 });
 

+ 154 - 0
src/screens/InAppScreens/MessagesScreen/Components/ChatOptionsBlock.tsx

@@ -0,0 +1,154 @@
+import React, { useCallback } from 'react';
+import { View, Text, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import * as ImagePicker from 'expo-image-picker';
+import { Colors } from 'src/theme';
+
+const SCREEN_HEIGHT = Dimensions.get('window').height;
+
+interface MediaFile {
+  uri: string;
+  type: 'image';
+}
+
+interface Props {
+  blockHeight: number;
+  closeOptions: () => void;
+  onSendMedia: (media: MediaFile[]) => void;
+  onSendLocation: (coords: { latitude: number; longitude: number }) => void;
+  onShareLiveLocation: () => void;
+}
+
+const ChatOptionsBlock: React.FC<Props> = ({
+  blockHeight,
+  closeOptions,
+  onSendMedia,
+  onSendLocation,
+  onShareLiveLocation
+}) => {
+  const handleOpenGallery = useCallback(async () => {
+    try {
+      const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
+      if (!perm.granted) {
+        console.warn('Permission for gallery not granted');
+        return;
+      }
+
+      const result = await ImagePicker.launchImageLibraryAsync({
+        mediaTypes: ImagePicker.MediaTypeOptions.Images,
+        allowsMultipleSelection: true,
+        quality: 1,
+        selectionLimit: 4
+      });
+
+      if (!result.canceled && result.assets) {
+        const files = result.assets.map((asset) => ({
+          uri: asset.uri,
+          type: 'image'
+        }));
+        onSendMedia(files);
+      }
+      closeOptions();
+    } catch (err) {
+      console.warn('Gallery error: ', err);
+    }
+  }, [onSendMedia, closeOptions]);
+
+  const handleOpenCamera = useCallback(async () => {
+    try {
+      const perm = await ImagePicker.requestCameraPermissionsAsync();
+      if (!perm.granted) {
+        console.warn('Permission for camera not granted');
+        return;
+      }
+
+      const result = await ImagePicker.launchCameraAsync({
+        mediaTypes: ImagePicker.MediaTypeOptions.Images,
+        quality: 1
+      });
+
+      if (!result.canceled && result.assets) {
+        const files = result.assets.map((asset) => ({
+          uri: asset.uri,
+          type: asset.type === 'video' ? 'video' : 'image'
+        }));
+        onSendMedia(files);
+      }
+      closeOptions();
+    } catch (err) {
+      console.warn('Camera error: ', err);
+    }
+  }, [onSendMedia, closeOptions]);
+
+  const handleShareLocation = useCallback(async () => {
+    try {
+      const coords = { latitude: 50.4501, longitude: 30.5234 };
+      onSendLocation(coords);
+      closeOptions();
+    } catch (err) {
+      console.warn('Location error: ', err);
+    }
+  }, [onSendLocation, closeOptions]);
+
+  const handleShareLiveLocation = useCallback(() => {
+    onShareLiveLocation();
+    closeOptions();
+  }, [onShareLiveLocation, closeOptions]);
+
+  return (
+    <View style={[styles.container, { height: blockHeight }]}>
+      <View style={styles.optionRow}>
+        <TouchableOpacity style={styles.optionItem} onPress={handleOpenGallery}>
+          <MaterialCommunityIcons name="image" size={36} color={Colors.ORANGE} />
+          <Text style={styles.optionLabel}>Gallery</Text>
+        </TouchableOpacity>
+
+        <TouchableOpacity style={styles.optionItem} onPress={handleOpenCamera}>
+          <MaterialCommunityIcons name="camera" size={36} color={Colors.ORANGE} />
+          <Text style={styles.optionLabel}>Camera</Text>
+        </TouchableOpacity>
+
+        <TouchableOpacity style={styles.optionItem} onPress={handleShareLocation}>
+          <MaterialCommunityIcons name="map-marker" size={36} color={Colors.ORANGE} />
+          <Text style={styles.optionLabel}>Location</Text>
+        </TouchableOpacity>
+
+        <TouchableOpacity style={styles.optionItem} onPress={handleShareLiveLocation}>
+          <MaterialCommunityIcons name="navigation" size={36} color={Colors.ORANGE} />
+          <Text style={styles.optionLabel}>Live</Text>
+        </TouchableOpacity>
+        {/* <TouchableOpacity style={styles.optionItem}>
+            <MegaphoneIcon fill={Colors.RED} width={36} height={36} />
+            <Text style={styles.optionLabel}>Report</Text>
+          </TouchableOpacity> */}
+      </View>
+    </View>
+  );
+};
+
+export default ChatOptionsBlock;
+
+const styles = StyleSheet.create({
+  container: {
+    backgroundColor: Colors.FILL_LIGHT
+  },
+  optionRow: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    paddingHorizontal: '5%',
+    marginVertical: 20,
+    flexWrap: 'wrap'
+  },
+  optionItem: {
+    width: '30%',
+    paddingVertical: 8,
+    marginBottom: 12,
+    alignItems: 'center'
+  },
+  optionLabel: {
+    marginTop: 6,
+    fontSize: 12,
+    color: Colors.DARK_BLUE,
+    fontWeight: '700'
+  }
+});

+ 9 - 0
src/screens/InAppScreens/MessagesScreen/Components/OptionsMenu.tsx

@@ -42,10 +42,19 @@ const OptionsMenu: React.FC<OptionsMenuProps> = ({
         <Text style={styles.optionText}>Reply</Text>
         <MaterialCommunityIcons name="reply" size={20} color={Colors.DARK_BLUE} />
       </TouchableOpacity>
+
+      {selectedMessage.currentMessage?.image && (
+        <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('download')}>
+          <Text style={styles.optionText}>Download image</Text>
+          <MaterialCommunityIcons name="download" size={20} color={Colors.DARK_BLUE} />
+        </TouchableOpacity>
+      )}
+
       <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('copy')}>
         <Text style={styles.optionText}>Copy</Text>
         <MaterialCommunityIcons name="content-copy" size={20} color={Colors.DARK_BLUE} />
       </TouchableOpacity>
+
       <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('delete')}>
         <Text style={styles.optionText}>Delete</Text>
         <MaterialCommunityIcons name="delete" size={20} color={Colors.DARK_BLUE} />