Browse Source

chat offline

Viktoriia 3 months ago
parent
commit
cb77cac13f

+ 3 - 1
src/modules/api/chat/chat-api.ts

@@ -80,7 +80,7 @@ interface Attachement {
   attachment_link?: string;
   attachment_link?: string;
 }
 }
 
 
-interface Message {
+export interface Message {
   id: number;
   id: number;
   sender: number;
   sender: number;
   recipient: number;
   recipient: number;
@@ -94,6 +94,7 @@ interface Message {
   edits: string;
   edits: string;
   attachement: -1 | Attachement;
   attachement: -1 | Attachement;
   encrypted: 0 | 1;
   encrypted: 0 | 1;
+  isSending?: boolean;
 }
 }
 
 
 interface GroupMessage {
 interface GroupMessage {
@@ -112,6 +113,7 @@ interface GroupMessage {
   encrypted: 0 | 1;
   encrypted: 0 | 1;
   sender_avatar: string | null;
   sender_avatar: string | null;
   sender_name: string;
   sender_name: string;
+  isSending?: boolean;
 }
 }
 
 
 export interface PostGetChatWithReturn extends ResponseType {
 export interface PostGetChatWithReturn extends ResponseType {

+ 9 - 1
src/modules/api/chat/queries/use-post-get-conversation-list.tsx

@@ -4,14 +4,22 @@ import { chatQueryKeys } from '../chat-query-keys';
 import { chatApi, type PostGetChatsListReturn } from '../chat-api';
 import { chatApi, type PostGetChatsListReturn } from '../chat-api';
 
 
 import type { BaseAxiosError } from '../../../../types';
 import type { BaseAxiosError } from '../../../../types';
+import { storage, StoreType } from 'src/storage';
 
 
 export const usePostGetChatsListQuery = (token: string, archive: 0 | 1, enabled: boolean) => {
 export const usePostGetChatsListQuery = (token: string, archive: 0 | 1, enabled: boolean) => {
   return useQuery<PostGetChatsListReturn, BaseAxiosError>({
   return useQuery<PostGetChatsListReturn, BaseAxiosError>({
     queryKey: chatQueryKeys.getChatsList(token, archive),
     queryKey: chatQueryKeys.getChatsList(token, archive),
     queryFn: async () => {
     queryFn: async () => {
       const response = await chatApi.getChatsList(token, archive);
       const response = await chatApi.getChatsList(token, archive);
+      storage.set('chats', JSON.stringify(response.data.conversations));
       return response.data;
       return response.data;
     },
     },
-    enabled
+    enabled,
+    initialData: () => {
+      const storedChats = storage.get('chats', StoreType.STRING) as string;
+      return storedChats
+        ? ({ conversations: JSON.parse(storedChats), result: 'OK' } as PostGetChatsListReturn)
+        : undefined;
+    }
   });
   });
 };
 };

+ 23 - 1
src/modules/api/chat/queries/use-post-get-conversation-with.tsx

@@ -2,8 +2,10 @@ import { useQuery } from '@tanstack/react-query';
 
 
 import { chatQueryKeys } from '../chat-query-keys';
 import { chatQueryKeys } from '../chat-query-keys';
 import { chatApi, type PostGetChatWithReturn } from '../chat-api';
 import { chatApi, type PostGetChatWithReturn } from '../chat-api';
+import NetInfo from '@react-native-community/netinfo';
 
 
 import type { BaseAxiosError } from '../../../../types';
 import type { BaseAxiosError } from '../../../../types';
+import { enforceStorageLimit, saveMessagesToStorage, storage, StoreType } from 'src/storage';
 
 
 export const usePostGetChatWithQuery = (
 export const usePostGetChatWithQuery = (
   token: string,
   token: string,
@@ -21,8 +23,28 @@ export const usePostGetChatWithQuery = (
         no_of_messages,
         no_of_messages,
         previous_than_message_id
         previous_than_message_id
       );
       );
+
+      const netInfoState = await NetInfo.fetch();
+
+      if (!response.data.messages.length && previous_than_message_id === -1) {
+        storage.remove(`chat_${uid}`);
+        storage.remove(`chat_${uid}_updatedAt`);
+      } else if (previous_than_message_id === -1 && netInfoState.isConnected) {
+        saveMessagesToStorage(uid, response.data.messages);
+      }
+
+      enforceStorageLimit();
+
       return response.data;
       return response.data;
     },
     },
-    enabled
+    enabled,
+    initialData: () => {
+      const storedMessages = storage.get(`chat_${uid}`, StoreType.STRING) as string;
+      let messages = storedMessages ? JSON.parse(storedMessages) : [];
+
+      return messages.length ? ({ messages, result: 'OK' } as PostGetChatWithReturn) : undefined;
+    },
+    initialDataUpdatedAt:
+      (storage.get(`chat_${uid}_updatedAt`, StoreType.NUMBER) as number) || undefined
   });
   });
 };
 };

+ 26 - 2
src/modules/api/chat/queries/use-post-get-group-conversation.tsx

@@ -4,6 +4,7 @@ import { chatQueryKeys } from '../chat-query-keys';
 import { chatApi, type PostGetGroupChatWithReturn } from '../chat-api';
 import { chatApi, type PostGetGroupChatWithReturn } from '../chat-api';
 
 
 import type { BaseAxiosError } from '../../../../types';
 import type { BaseAxiosError } from '../../../../types';
+import { enforceStorageLimit, saveMessagesToStorage, storage, StoreType } from 'src/storage';
 
 
 export const usePostGetGroupChatQuery = (
 export const usePostGetGroupChatQuery = (
   token: string,
   token: string,
@@ -13,7 +14,12 @@ export const usePostGetGroupChatQuery = (
   enabled: boolean
   enabled: boolean
 ) => {
 ) => {
   return useQuery<PostGetGroupChatWithReturn, BaseAxiosError>({
   return useQuery<PostGetGroupChatWithReturn, BaseAxiosError>({
-    queryKey: chatQueryKeys.getGroupChat(token, group_token, no_of_messages, previous_than_message_id),
+    queryKey: chatQueryKeys.getGroupChat(
+      token,
+      group_token,
+      no_of_messages,
+      previous_than_message_id
+    ),
     queryFn: async () => {
     queryFn: async () => {
       const response = await chatApi.getGroupChat(
       const response = await chatApi.getGroupChat(
         token,
         token,
@@ -21,8 +27,26 @@ export const usePostGetGroupChatQuery = (
         no_of_messages,
         no_of_messages,
         previous_than_message_id
         previous_than_message_id
       );
       );
+
+      if (!response.data.messages.length && previous_than_message_id === -1) {
+        storage.remove(`chat_${group_token}`);
+        storage.remove(`chat_${group_token}_updatedAt`);
+      } else if (previous_than_message_id === -1) {
+        saveMessagesToStorage(group_token, response.data);
+      }
+
+      enforceStorageLimit();
       return response.data;
       return response.data;
     },
     },
-    enabled
+    enabled,
+    initialData: () => {
+      const storedMessages = storage.get(`chat_${group_token}`, StoreType.STRING) as string;
+      
+      return storedMessages
+        ? (JSON.parse(storedMessages) as PostGetGroupChatWithReturn)
+        : undefined;
+    },
+    initialDataUpdatedAt:
+      (storage.get(`chat_${group_token}_updatedAt`, StoreType.NUMBER) as number) || undefined
   });
   });
 };
 };

+ 66 - 78
src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx

@@ -41,7 +41,13 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
 import Clipboard from '@react-native-clipboard/clipboard';
 import Clipboard from '@react-native-clipboard/clipboard';
 import { trigger } from 'react-native-haptic-feedback';
 import { trigger } from 'react-native-haptic-feedback';
 import ReactModal from 'react-native-modal';
 import ReactModal from 'react-native-modal';
-import { storage, StoreType } from 'src/storage';
+import {
+  editMessageInStorage,
+  sendMessageOffline,
+  checkAndSendSavedMessages,
+  storage,
+  StoreType
+} from 'src/storage';
 import {
 import {
   usePostDeleteMessageMutation,
   usePostDeleteMessageMutation,
   usePostGetChatWithQuery,
   usePostGetChatWithQuery,
@@ -72,8 +78,11 @@ import * as MediaLibrary from 'expo-media-library';
 import BanIcon from 'assets/icons/messages/ban.svg';
 import BanIcon from 'assets/icons/messages/ban.svg';
 import AttachmentsModal from '../Components/AttachmentsModal';
 import AttachmentsModal from '../Components/AttachmentsModal';
 import RenderMessageVideo from '../Components/renderMessageVideo';
 import RenderMessageVideo from '../Components/renderMessageVideo';
+import RenderMessageImage from '../Components/RenderMessageImage';
 import MessageLocation from '../Components/MessageLocation';
 import MessageLocation from '../Components/MessageLocation';
 import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
 import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
+import { useConnection } from 'src/contexts/ConnectionContext';
+import moment from 'moment';
 
 
 const options = {
 const options = {
   enableVibrateFallback: true,
   enableVibrateFallback: true,
@@ -84,6 +93,9 @@ const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭'];
 
 
 const ChatScreen = ({ route }: { route: any }) => {
 const ChatScreen = ({ route }: { route: any }) => {
   const token = storage.get('token', StoreType.STRING) as string;
   const token = storage.get('token', StoreType.STRING) as string;
+  const [isConnected, setIsConnected] = useState<boolean>(true);
+  const netInfo = useConnection();
+
   const {
   const {
     id,
     id,
     name,
     name,
@@ -166,6 +178,16 @@ const ChatScreen = ({ route }: { route: any }) => {
 
 
   const socket = useRef<WebSocket | null>(null);
   const socket = useRef<WebSocket | null>(null);
 
 
+  useEffect(() => {
+    if (netInfo && netInfo.isConnected !== null) {
+      setIsConnected(netInfo.isConnected);
+      if (netInfo.isConnected) {
+        checkAndSendSavedMessages();
+        refetch();
+      }
+    }
+  }, [netInfo]);
+
   const closeModal = () => {
   const closeModal = () => {
     setModalInfo({ ...modalInfo, visible: false });
     setModalInfo({ ...modalInfo, visible: false });
   };
   };
@@ -938,7 +960,7 @@ const ChatScreen = ({ route }: { route: any }) => {
         name: message.sender === id ? userName : 'Me'
         name: message.sender === id ? userName : 'Me'
       },
       },
       replyMessage:
       replyMessage:
-        message.reply_to_id !== -1
+        message.reply_to_id && message.reply_to_id !== -1
           ? {
           ? {
               text: message.reply_to.text,
               text: message.reply_to.text,
               id: message.reply_to.id,
               id: message.reply_to.id,
@@ -952,7 +974,7 @@ const ChatScreen = ({ route }: { route: any }) => {
       received: message.status === 3,
       received: message.status === 3,
       deleted: message.status === 4,
       deleted: message.status === 4,
       edited: isMessageEdited(message.edits),
       edited: isMessageEdited(message.edits),
-      isSending: false,
+      isSending: message?.isSending ? message.isSending : false,
       video:
       video:
         message.attachement !== -1 && message.attachement?.filetype?.startsWith('video')
         message.attachement !== -1 && message.attachement?.filetype?.startsWith('video')
           ? API_HOST + message.attachement?.attachment_link
           ? API_HOST + message.attachement?.attachment_link
@@ -1008,9 +1030,16 @@ const ChatScreen = ({ route }: { route: any }) => {
           const newMessages = mappedMessages.filter(
           const newMessages = mappedMessages.filter(
             (newMsg) => !previousMessages?.some((oldMsg) => oldMsg._id === newMsg._id)
             (newMsg) => !previousMessages?.some((oldMsg) => oldMsg._id === newMsg._id)
           );
           );
+          let unsentMapped = [];
+          if (prevThenMessageId === -1) {
+            const unsentMessages = storage.get(`unsent_${id}`, StoreType.STRING)
+              ? JSON.parse(storage.get(`unsent_${id}`, StoreType.STRING) as string)
+              : [];
+            unsentMapped = unsentMessages.map(mapApiMessageToGiftedMessage);
+          }
           return prevThenMessageId !== -1 && previousMessages
           return prevThenMessageId !== -1 && previousMessages
             ? GiftedChat.prepend(previousMessages, newMessages)
             ? GiftedChat.prepend(previousMessages, newMessages)
-            : mappedMessages;
+            : [...unsentMapped, ...mappedMessages];
         });
         });
 
 
         if (mappedMessages.length < 50) {
         if (mappedMessages.length < 50) {
@@ -1416,6 +1445,8 @@ const ChatScreen = ({ route }: { route: any }) => {
             },
             },
             {
             {
               onSuccess: (res) => {
               onSuccess: (res) => {
+                editMessageInStorage(id, editingMessage._id, newMessages[0].text, false);
+
                 const editedMessage = {
                 const editedMessage = {
                   _id: editingMessage._id,
                   _id: editingMessage._id,
                   text: newMessages[0].text
                   text: newMessages[0].text
@@ -1450,6 +1481,27 @@ const ChatScreen = ({ route }: { route: any }) => {
 
 
       setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message]));
       setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message]));
 
 
+      if (!isConnected) {
+        const staticMessage = {
+          id: message._id,
+          text: message.text,
+          isSending: true,
+          sender: +currentUserId,
+          recipient: id,
+          status: 1,
+          sent_datetime: moment(message.createdAt).utc().format('YYYY-MM-DD HH:mm:ss'),
+          reply_to_id: message.replyMessage ? message.replyMessage.id : -1,
+          reactions: '{}',
+          edits: '{}',
+          attachement: -1,
+          encrypted: 0
+        };
+        sendMessageOffline(id, staticMessage as any);
+        clearReplyMessage();
+
+        return;
+      }
+
       sendMessage(
       sendMessage(
         {
         {
           token,
           token,
@@ -1510,7 +1562,7 @@ const ChatScreen = ({ route }: { route: any }) => {
 
 
       clearReplyMessage();
       clearReplyMessage();
     },
     },
-    [replyMessage, editingMessage]
+    [replyMessage, editingMessage, isConnected]
   );
   );
 
 
   const addReaction = (messageId: number, reaction: string) => {
   const addReaction = (messageId: number, reaction: string) => {
@@ -1645,78 +1697,6 @@ const ChatScreen = ({ route }: { route: any }) => {
     }
     }
   }, [replyMessage]);
   }, [replyMessage]);
 
 
-  const handleOpenImage = async (uri: string, fileName: string) => {
-    const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
-    if (!dirExist.exists) {
-      await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
-    }
-
-    const fileUri = `${CACHED_ATTACHMENTS_DIR}${fileName}`;
-
-    const fileExists = await FileSystem.getInfoAsync(fileUri);
-    if (fileExists.exists && fileExists.size > 1024) {
-      setSelectedMedia(fileUri);
-
-      return;
-    }
-    setSelectedMedia(uri);
-
-    const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri, {
-      headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
-    });
-  };
-
-  const renderMessageImage = (props: any) => {
-    const { currentMessage } = props;
-    const leftMessage = currentMessage?.user?._id !== +currentUserId;
-
-    return (
-      <TouchableOpacity
-        onPress={() => {
-          if (!currentMessage.attachment.attachment_full_url?.startsWith('/')) {
-            setSelectedMedia(currentMessage.attachment.attachment_full_url);
-            return;
-          }
-          handleOpenImage(
-            API_HOST + currentMessage.attachment.attachment_full_url,
-            currentMessage.attachment?.filename
-          );
-        }}
-        onLongPress={() => handleLongPress(currentMessage, props)}
-        style={styles.imageContainer}
-        disabled={currentMessage.isSending}
-      >
-        <Image
-          key={currentMessage.image}
-          source={{
-            uri: currentMessage.image,
-            headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
-          }}
-          style={styles.chatImage}
-          resizeMode="cover"
-        />
-        {currentMessage.isSending && (
-          <View
-            style={{
-              position: 'absolute',
-              top: 0,
-              left: 0,
-              right: 0,
-              bottom: 0,
-              justifyContent: 'center',
-              alignItems: 'center'
-            }}
-          >
-            <ActivityIndicator
-              size="large"
-              color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
-            />
-          </View>
-        )}
-      </TouchableOpacity>
-    );
-  };
-
   const renderTicks = (message: CustomMessage) => {
   const renderTicks = (message: CustomMessage) => {
     if (message.user._id === id) return null;
     if (message.user._id === id) return null;
 
 
@@ -1966,7 +1946,15 @@ const ChatScreen = ({ route }: { route: any }) => {
             onSend={(newMessages: CustomMessage[]) => onSend(newMessages)}
             onSend={(newMessages: CustomMessage[]) => onSend(newMessages)}
             user={{ _id: +currentUserId, name: 'Me' }}
             user={{ _id: +currentUserId, name: 'Me' }}
             renderBubble={renderBubble}
             renderBubble={renderBubble}
-            renderMessageImage={renderMessageImage}
+            renderMessageImage={(props) => (
+              <RenderMessageImage
+                props={props}
+                token={token}
+                currentUserId={+currentUserId}
+                onLongPress={handleLongPress}
+                setSelectedMedia={setSelectedMedia}
+              />
+            )}
             renderInputToolbar={renderInputToolbar}
             renderInputToolbar={renderInputToolbar}
             renderCustomView={renderReplyMessageView}
             renderCustomView={renderReplyMessageView}
             isCustomViewBottom={false}
             isCustomViewBottom={false}

+ 110 - 0
src/screens/InAppScreens/MessagesScreen/Components/RenderMessageImage.tsx

@@ -0,0 +1,110 @@
+import React, { useState, useEffect } from 'react';
+import { View, ActivityIndicator, TouchableOpacity, Platform, Image } from 'react-native';
+import * as FileSystem from 'expo-file-system';
+import { Colors } from 'src/theme';
+import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
+import { API_HOST, APP_VERSION } from 'src/constants';
+import { styles } from '../ChatScreen/styles';
+
+const RenderMessageImage = ({
+  props,
+  token,
+  currentUserId,
+  onLongPress,
+  setSelectedMedia
+}: {
+  props: any;
+  token: string;
+  currentUserId: number;
+  onLongPress: (currentMessage: any, props: any) => any;
+  setSelectedMedia: (media: string) => void;
+}) => {
+  const { currentMessage } = props;
+  const leftMessage = currentMessage?.user?._id !== +currentUserId;
+  const fileUri = `${CACHED_ATTACHMENTS_DIR}${currentMessage.attachment?.filename}`;
+
+  const [isCached, setIsCached] = useState(false);
+
+  useEffect(() => {
+    const checkCache = async () => {
+      try {
+        const fileInfo = await FileSystem.getInfoAsync(fileUri);
+        setIsCached(fileInfo.exists && fileInfo.size > 1024);
+      } catch (error) {
+        setIsCached(false);
+      }
+    };
+
+    checkCache();
+  }, [currentMessage.attachment?.filename]);
+
+  const handleOpenImage = async (uri: string, fileName: string) => {
+    const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
+    if (!dirExist.exists) {
+      await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
+    }
+
+    const fileUri = `${CACHED_ATTACHMENTS_DIR}${fileName}`;
+
+    const fileExists = await FileSystem.getInfoAsync(fileUri);
+    if (fileExists.exists && fileExists.size > 1024) {
+      setSelectedMedia(fileUri);
+
+      return;
+    }
+    setSelectedMedia(uri);
+
+    await FileSystem.downloadAsync(uri, fileUri, {
+      headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
+    });
+  };
+
+  return (
+    <TouchableOpacity
+      onPress={() => {
+        if (!currentMessage.attachment.attachment_full_url?.startsWith('/')) {
+          setSelectedMedia(currentMessage.attachment.attachment_full_url);
+          return;
+        }
+        handleOpenImage(
+          API_HOST + currentMessage.attachment.attachment_full_url,
+          currentMessage.attachment?.filename
+        );
+      }}
+      onLongPress={() => onLongPress(currentMessage, props)}
+      style={styles.imageContainer}
+      disabled={currentMessage.isSending}
+    >
+      <Image
+        key={currentMessage.image}
+        source={{
+          uri: isCached ? fileUri : currentMessage.image,
+          headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
+        }}
+        loadingIndicatorSource={{ uri: currentMessage.image }}
+        style={styles.chatImage}
+        resizeMode="cover"
+      />
+      {currentMessage.isSending && (
+        <View
+          style={{
+            position: 'absolute',
+            top: 0,
+            left: 0,
+            right: 0,
+            bottom: 0,
+            justifyContent: 'center',
+            alignItems: 'center'
+          }}
+        >
+          <ActivityIndicator
+            size="large"
+            color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
+          />
+        </View>
+      )}
+    </TouchableOpacity>
+  );
+};
+
+export default RenderMessageImage;

+ 69 - 78
src/screens/InAppScreens/MessagesScreen/GroupChatScreen/index.tsx

@@ -45,7 +45,13 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
 import Clipboard from '@react-native-clipboard/clipboard';
 import Clipboard from '@react-native-clipboard/clipboard';
 import { trigger } from 'react-native-haptic-feedback';
 import { trigger } from 'react-native-haptic-feedback';
 import ReactModal from 'react-native-modal';
 import ReactModal from 'react-native-modal';
-import { storage, StoreType } from 'src/storage';
+import {
+  editMessageInStorage,
+  sendMessageOffline,
+  checkAndSendSavedMessages,
+  storage,
+  StoreType
+} from 'src/storage';
 import {
 import {
   usePostGetGroupChatQuery,
   usePostGetGroupChatQuery,
   usePostSendGroupMessageMutation,
   usePostSendGroupMessageMutation,
@@ -80,12 +86,15 @@ import * as MediaLibrary from 'expo-media-library';
 import BanIcon from 'assets/icons/messages/ban.svg';
 import BanIcon from 'assets/icons/messages/ban.svg';
 import AttachmentsModal from '../Components/AttachmentsModal';
 import AttachmentsModal from '../Components/AttachmentsModal';
 import RenderMessageVideo from '../Components/renderMessageVideo';
 import RenderMessageVideo from '../Components/renderMessageVideo';
+import RenderMessageImage from '../Components/RenderMessageImage';
 import MessageLocation from '../Components/MessageLocation';
 import MessageLocation from '../Components/MessageLocation';
 import GroupIcon from 'assets/icons/messages/group-chat.svg';
 import GroupIcon from 'assets/icons/messages/group-chat.svg';
 import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
 import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
 import GroupStatusModal from '../Components/GroupStatusModal';
 import GroupStatusModal from '../Components/GroupStatusModal';
 import PinIcon from 'assets/icons/messages/pin.svg';
 import PinIcon from 'assets/icons/messages/pin.svg';
 import MentionsList from '../Components/MentionsList';
 import MentionsList from '../Components/MentionsList';
+import { useConnection } from 'src/contexts/ConnectionContext';
+import moment from 'moment';
 
 
 const options = {
 const options = {
   enableVibrateFallback: true,
   enableVibrateFallback: true,
@@ -96,6 +105,9 @@ const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭'];
 
 
 const GroupChatScreen = ({ route }: { route: any }) => {
 const GroupChatScreen = ({ route }: { route: any }) => {
   const token = storage.get('token', StoreType.STRING) as string;
   const token = storage.get('token', StoreType.STRING) as string;
+  const [isConnected, setIsConnected] = useState<boolean>(true);
+  const netInfo = useConnection();
+
   const {
   const {
     group_token,
     group_token,
     name,
     name,
@@ -200,6 +212,16 @@ const GroupChatScreen = ({ route }: { route: any }) => {
 
 
   const socket = useRef<WebSocket | null>(null);
   const socket = useRef<WebSocket | null>(null);
 
 
+  useEffect(() => {
+    if (netInfo && netInfo.isConnected !== null) {
+      setIsConnected(netInfo.isConnected);
+      if (netInfo.isConnected) {
+        checkAndSendSavedMessages();
+        refetch();
+      }
+    }
+  }, [netInfo]);
+
   const closeModal = () => {
   const closeModal = () => {
     setModalInfo({ ...modalInfo, visible: false });
     setModalInfo({ ...modalInfo, visible: false });
   };
   };
@@ -981,7 +1003,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
       received: message.status === 3,
       received: message.status === 3,
       deleted: message.status === 4,
       deleted: message.status === 4,
       edited: isMessageEdited(message.edits),
       edited: isMessageEdited(message.edits),
-      isSending: false,
+      isSending: message?.isSending ? message.isSending : false,
       video:
       video:
         message.attachement !== -1 && message.attachement?.filetype?.startsWith('video')
         message.attachement !== -1 && message.attachement?.filetype?.startsWith('video')
           ? API_HOST + message.attachement?.attachment_link
           ? API_HOST + message.attachement?.attachment_link
@@ -1042,9 +1064,16 @@ const GroupChatScreen = ({ route }: { route: any }) => {
           const newMessages = mappedMessages.filter(
           const newMessages = mappedMessages.filter(
             (newMsg) => !previousMessages?.some((oldMsg) => oldMsg._id === newMsg._id)
             (newMsg) => !previousMessages?.some((oldMsg) => oldMsg._id === newMsg._id)
           );
           );
+          let unsentMapped = [];
+          if (prevThenMessageId === -1) {
+            const unsentMessages = storage.get(`unsent_${group_token}`, StoreType.STRING)
+              ? JSON.parse(storage.get(`unsent_${group_token}`, StoreType.STRING) as string)
+              : [];
+            unsentMapped = unsentMessages.map(mapApiMessageToGiftedMessage);
+          }
           return prevThenMessageId !== -1 && previousMessages
           return prevThenMessageId !== -1 && previousMessages
             ? GiftedChat.prepend(previousMessages, newMessages)
             ? GiftedChat.prepend(previousMessages, newMessages)
-            : mappedMessages;
+            : [...unsentMapped, ...mappedMessages];
         });
         });
 
 
         if (mappedMessages.length < 50) {
         if (mappedMessages.length < 50) {
@@ -1520,6 +1549,8 @@ const GroupChatScreen = ({ route }: { route: any }) => {
             },
             },
             {
             {
               onSuccess: () => {
               onSuccess: () => {
+                editMessageInStorage(group_token, editingMessage._id, editedText, true);
+
                 const editedMessage = {
                 const editedMessage = {
                   _id: editingMessage._id,
                   _id: editingMessage._id,
                   text: editedText
                   text: editedText
@@ -1563,6 +1594,29 @@ const GroupChatScreen = ({ route }: { route: any }) => {
         ])
         ])
       );
       );
 
 
+      if (!isConnected) {
+        const staticMessage = {
+          id: message._id,
+          text: transformMessageForServer(message.text),
+          isSending: true,
+          sender: +currentUserId,
+          status: 1,
+          sent_datetime: moment(message.createdAt).utc().format('YYYY-MM-DD HH:mm:ss'),
+          reply_to_id: message.replyMessage ? message.replyMessage.id : -1,
+          reactions: '{}',
+          edits: '{}',
+          attachement: -1,
+          encrypted: 0,
+          sender_avatar: null,
+          sender_name: 'Me'
+        };
+
+        sendMessageOffline(group_token, staticMessage as any);
+        clearReplyMessage();
+
+        return;
+      }
+
       sendMessage(
       sendMessage(
         {
         {
           token,
           token,
@@ -1590,7 +1644,7 @@ const GroupChatScreen = ({ route }: { route: any }) => {
 
 
       clearReplyMessage();
       clearReplyMessage();
     },
     },
-    [replyMessage, editingMessage]
+    [replyMessage, editingMessage, isConnected]
   );
   );
 
 
   const addReaction = (messageId: number, reaction: string) => {
   const addReaction = (messageId: number, reaction: string) => {
@@ -1749,78 +1803,6 @@ const GroupChatScreen = ({ route }: { route: any }) => {
     }
     }
   }, [replyMessage]);
   }, [replyMessage]);
 
 
-  const handleOpenImage = async (uri: string, fileName: string) => {
-    const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
-    if (!dirExist.exists) {
-      await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
-    }
-
-    const fileUri = `${CACHED_ATTACHMENTS_DIR}${fileName}`;
-
-    const fileExists = await FileSystem.getInfoAsync(fileUri);
-    if (fileExists.exists && fileExists.size > 1024) {
-      setSelectedMedia(fileUri);
-
-      return;
-    }
-    setSelectedMedia(uri);
-
-    const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri, {
-      headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
-    });
-  };
-
-  const renderMessageImage = (props: any) => {
-    const { currentMessage } = props;
-    const leftMessage = currentMessage?.user?._id !== +currentUserId;
-
-    return (
-      <TouchableOpacity
-        onPress={() => {
-          if (!currentMessage.attachment.attachment_full_url?.startsWith('/')) {
-            setSelectedMedia(currentMessage.attachment.attachment_full_url);
-            return;
-          }
-          handleOpenImage(
-            API_HOST + currentMessage.attachment.attachment_full_url,
-            currentMessage.attachment?.filename
-          );
-        }}
-        onLongPress={() => handleLongPress(currentMessage, props)}
-        style={styles.imageContainer}
-        disabled={currentMessage.isSending}
-      >
-        <Image
-          key={currentMessage.image}
-          source={{
-            uri: currentMessage.image,
-            headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
-          }}
-          style={styles.chatImage}
-          resizeMode="cover"
-        />
-        {currentMessage.isSending && (
-          <View
-            style={{
-              position: 'absolute',
-              top: 0,
-              left: 0,
-              right: 0,
-              bottom: 0,
-              justifyContent: 'center',
-              alignItems: 'center'
-            }}
-          >
-            <ActivityIndicator
-              size="large"
-              color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
-            />
-          </View>
-        )}
-      </TouchableOpacity>
-    );
-  };
-
   const renderTicks = (message: CustomMessage) => {
   const renderTicks = (message: CustomMessage) => {
     if (message.user._id !== +currentUserId) return null;
     if (message.user._id !== +currentUserId) return null;
 
 
@@ -2176,7 +2158,8 @@ const GroupChatScreen = ({ route }: { route: any }) => {
           (data &&
           (data &&
             data.settings &&
             data.settings &&
             data.settings.members_can_see_members === 0 &&
             data.settings.members_can_see_members === 0 &&
-            data.settings.admin === 0)) ? (
+            data.settings.admin === 0) ||
+          !isConnected) ? (
           <GiftedChat
           <GiftedChat
             messages={messages as CustomMessage[]}
             messages={messages as CustomMessage[]}
             text={text}
             text={text}
@@ -2201,7 +2184,15 @@ const GroupChatScreen = ({ route }: { route: any }) => {
             onSend={(newMessages: CustomMessage[]) => onSend(newMessages)}
             onSend={(newMessages: CustomMessage[]) => onSend(newMessages)}
             user={{ _id: +currentUserId, name: 'Me' }}
             user={{ _id: +currentUserId, name: 'Me' }}
             renderBubble={renderBubble}
             renderBubble={renderBubble}
-            renderMessageImage={renderMessageImage}
+            renderMessageImage={(props) => (
+              <RenderMessageImage
+                props={props}
+                token={token}
+                currentUserId={+currentUserId}
+                onLongPress={handleLongPress}
+                setSelectedMedia={setSelectedMedia}
+              />
+            )}
             showUserAvatar={true}
             showUserAvatar={true}
             onPressAvatar={(user) => {
             onPressAvatar={(user) => {
               navigation.navigate(
               navigation.navigate(

+ 2 - 0
src/screens/InAppScreens/MessagesScreen/types.ts

@@ -64,6 +64,7 @@ export type MessageSimple = {
   edits: string;
   edits: string;
   attachement: -1 | Attachement;
   attachement: -1 | Attachement;
   encrypted: 0 | 1;
   encrypted: 0 | 1;
+  isSending?: boolean;
 };
 };
 
 
 export type MessageGroupSimple = {
 export type MessageGroupSimple = {
@@ -82,6 +83,7 @@ export type MessageGroupSimple = {
   encrypted: 0 | 1;
   encrypted: 0 | 1;
   sender_avatar: string | null;
   sender_avatar: string | null;
   sender_name: string;
   sender_name: string;
+  isSending?: boolean;
 };
 };
 
 
 export type Message = MessageSimple & {
 export type Message = MessageSimple & {

+ 148 - 1
src/storage/mmkv.ts

@@ -1,4 +1,11 @@
+import { Message } from '@api/chat/chat-api';
 import { MMKV } from 'react-native-mmkv';
 import { MMKV } from 'react-native-mmkv';
+import axios from 'axios';
+import { API } from 'src/types';
+import { API_URL, APP_VERSION } from 'src/constants';
+import { Platform } from 'react-native';
+
+type CustomMessage = Message & { reply_to: Message };
 
 
 const storageMMKV = new MMKV();
 const storageMMKV = new MMKV();
 
 
@@ -21,7 +28,7 @@ const remove = (key: string) => {
   storageMMKV.delete(key);
   storageMMKV.delete(key);
 };
 };
 
 
-export const storage = { get, set, remove };
+export const storage = { get, set, remove, getAllKeys: () => storageMMKV.getAllKeys() };
 
 
 export enum StoreType {
 export enum StoreType {
   STRING = 'string',
   STRING = 'string',
@@ -35,3 +42,143 @@ export function loadData<T>(nameKey: string, key: string): T | null {
 
 
   return jsonData ? JSON.parse(jsonData) : null;
   return jsonData ? JSON.parse(jsonData) : null;
 }
 }
+
+export const saveMessagesToStorage = (chatId: string | number, data: any) => {
+  storage.set(`chat_${chatId}`, JSON.stringify(data));
+  storage.set(`chat_${chatId}_updatedAt`, Date.now());
+};
+
+export const editMessageInStorage = (
+  chatId: string | number,
+  messageId: number,
+  newText: string,
+  isGroup: boolean
+) => {
+  const storedMessages = storage.get(`chat_${chatId}`, StoreType.STRING) as string;
+  if (!storedMessages) return;
+
+  const data = JSON.parse(storedMessages);
+  const messages = isGroup ? data.messages : data;
+  const updatedMessages = messages.map((msg: Message) =>
+    msg.id === messageId ? { ...msg, text: newText, edits: '{[]}' } : msg
+  );
+
+  storage.set(`chat_${chatId}`, JSON.stringify(updatedMessages));
+};
+
+export const sendMessageOffline = (chatId: string | number, message: Message) => {
+  const unsentMessages = storage.get(`unsent_${chatId}`, StoreType.STRING)
+    ? JSON.parse(storage.get(`unsent_${chatId}`, StoreType.STRING) as string)
+    : [];
+  unsentMessages.push(message);
+  storage.set(`unsent_${chatId}`, JSON.stringify(unsentMessages));
+};
+
+export const checkAndSendSavedMessages = async () => {
+  const token = storage.get('token', StoreType.STRING) as string;
+  const chatKeys = storage.getAllKeys().filter((key) => key.startsWith('unsent_'));
+
+  for (const key of chatKeys) {
+    const chatId = key.replace('unsent_', '');
+    let unsentMessages = JSON.parse((storage.get(key, StoreType.STRING) as string) || '[]');
+
+    const successfullySent: any[] = [];
+
+    for (const message of unsentMessages) {
+      try {
+        if (!message || !message.text) continue;
+
+        let response;
+        if (!isNaN(+chatId)) {
+          response = await axios.postForm(
+            API_URL + '/' + API.SEND_MESSAGE,
+            {
+              token,
+              to_uid: chatId,
+              text: message.text,
+              reply_to_id: message.reply_to_id ? (message.reply_to_id as number) : -1
+            },
+            {
+              headers: {
+                Platform: Platform.OS,
+                'App-Version': APP_VERSION
+              }
+            }
+          );
+        } else {
+          response = await axios.postForm(
+            API_URL + '/' + API.SEND_GROUP_MESSAGE,
+            {
+              token,
+              to_group_token: chatId,
+              text: message.text,
+              reply_to_id: message.reply_to_id ? (message.reply_to_id as number) : -1
+            },
+            {
+              headers: {
+                Platform: Platform.OS,
+                'App-Version': APP_VERSION
+              }
+            }
+          );
+        }
+
+        if (response?.data?.result === 'OK') {
+          successfullySent.push(message.id);
+        }
+      } catch (error) {
+        console.error('Error checkAndSendSavedMessages func', message);
+      }
+    }
+
+    unsentMessages = unsentMessages.filter((msg: any) => !successfullySent.includes(msg.id));
+
+    if (unsentMessages.length > 0) {
+      storage.set(key, JSON.stringify(unsentMessages));
+    } else {
+      storage.remove(key);
+    }
+  }
+};
+
+const getStorageSize = () => {
+  const keys = storage
+    .getAllKeys()
+    .filter((key) => key.startsWith('chat_') && !key.endsWith('_updatedAt'));
+  let totalSize = 0;
+
+  keys.forEach((key) => {
+    const value = storage.get(key, StoreType.STRING) as string;
+    if (value) {
+      totalSize += new Blob([value]).size;
+    }
+  });
+
+  return totalSize;
+};
+
+export const enforceStorageLimit = () => {
+  let totalSize = getStorageSize();
+  const maxSize = 5 * 1024 * 1024;
+
+  if (totalSize > maxSize) {
+    const chatKeys = storage
+      .getAllKeys()
+      .filter((key) => key.startsWith('chat_') && !key.endsWith('_updatedAt'));
+
+    const chatTimestamps = chatKeys.map((chatKey) => ({
+      chatKey,
+      updatedAt: (storage.get(`${chatKey}_updatedAt`, StoreType.NUMBER) as number) || 0
+    }));
+
+    chatTimestamps.sort((a, b) => a.updatedAt - b.updatedAt);
+
+    for (const chat of chatTimestamps) {
+      storage.remove(chat.chatKey);
+      storage.remove(`${chat.chatKey}_updatedAt`);
+
+      totalSize = getStorageSize();
+      if (totalSize <= maxSize) break;
+    }
+  }
+};

+ 7 - 1
src/utils/backgroundLocation.ts

@@ -1,8 +1,9 @@
 import * as TaskManager from 'expo-task-manager';
 import * as TaskManager from 'expo-task-manager';
 import * as Location from 'expo-location';
 import * as Location from 'expo-location';
 import axios from 'axios';
 import axios from 'axios';
-import { storage, StoreType } from 'src/storage';
+import { checkAndSendSavedMessages, storage, StoreType } from 'src/storage';
 import { Platform } from 'react-native';
 import { Platform } from 'react-native';
+import NetInfo from '@react-native-community/netinfo';
 import { API_URL, APP_VERSION } from 'src/constants';
 import { API_URL, APP_VERSION } from 'src/constants';
 import { API } from 'src/types';
 import { API } from 'src/types';
 
 
@@ -40,6 +41,11 @@ TaskManager.defineTask(LOCATION_TASK_NAME, async ({ data, error }) => {
         storage.set('last_location_sent_time', now);
         storage.set('last_location_sent_time', now);
         storage.set('last_latitude', coords.latitude);
         storage.set('last_latitude', coords.latitude);
         storage.set('last_longitude', coords.longitude);
         storage.set('last_longitude', coords.longitude);
+        
+        const netInfoState = await NetInfo.fetch();
+        if (netInfoState.isConnected) {
+          checkAndSendSavedMessages();
+        }
 
 
         try {
         try {
           const response = await axios.postForm(
           const response = await axios.postForm(