浏览代码

chat screen refactor/android icon plugin/status bar translucent on map screen

Viktoriia 8 月之前
父节点
当前提交
249db2a208

+ 17 - 13
app.config.ts

@@ -10,7 +10,8 @@ const API_HOST = env.ENV === 'production' ? env.PRODUCTION_API_HOST : env.DEVELO
 const MAP_HOST = env.ENV === 'production' ? env.PRODUCTION_MAP_HOST : env.DEVELOPMENT_MAP_HOST;
 
 const GOOGLE_MAP_PLACES_APIKEY = env.GOOGLE_MAP_PLACES_APIKEY;
-const WEBSOCKET_URL = env.ENV === 'production' ? env.PRODUCTION_WEBSOCKET_URL : env.DEVELOPMENT_WEBSOCKET_URL;
+const WEBSOCKET_URL =
+  env.ENV === 'production' ? env.PRODUCTION_WEBSOCKET_URL : env.DEVELOPMENT_WEBSOCKET_URL;
 
 dotenv.config({
   path: path.resolve(process.cwd(), '.env')
@@ -54,7 +55,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     url: 'https://u.expo.dev/c31c6828-3c32-4c7a-aabc-f9b8336b3b66'
   },
   platforms: ['ios', 'android'],
-  assetBundlePatterns: ['**/*', "assets/db/*.db"],
+  assetBundlePatterns: ['**/*', 'assets/db/*.db'],
   ios: {
     supportsTablet: false,
     bundleIdentifier: env.PACKAGE_NAME_IOS, // com.nomadmania.app2
@@ -72,11 +73,13 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       NSPushNotificationsDescription:
         'This will allow NomadMania.com to send you notifications. Also you can disable it in app settings',
 
-      NSMicrophoneUsageDescription: "Nomadmania app needs access to the microphone to record audio.",
-      NSDocumentsFolderUsageDescription: "Nomadmania app needs access to the documents folder to select files.",
-      NSCameraUsageDescription: "Nomadmania app needs access to the camera to record video.",
+      NSMicrophoneUsageDescription:
+        'Nomadmania app needs access to the microphone to record audio.',
+      NSDocumentsFolderUsageDescription:
+        'Nomadmania app needs access to the documents folder to select files.',
+      NSCameraUsageDescription: 'Nomadmania app needs access to the camera to record video.',
       NSLocationWhenInUseUsageDescription:
-        'NomadMania app needs access to your location to show relevant data.',
+        'NomadMania app needs access to your location to show relevant data.'
     },
     privacyManifests: {
       NSPrivacyAccessedAPITypes: [
@@ -105,7 +108,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       'USER_FACING_NOTIFICATIONS',
       'INTERNET',
       'CAMERA',
-      "RECORD_AUDIO",
+      'RECORD_AUDIO',
       'MODIFY_AUDIO_SETTINGS'
     ],
     versionCode: 74 // next version submitted to Google Play needs to be higher than that 2.0.21
@@ -140,18 +143,19 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       'expo-asset',
       {
         assets: [
-          "./assets/db/nmRegions.db",
-          "./assets/db/darePlaces.db",
-          "./assets/db/nmCountries.db"
+          './assets/db/nmRegions.db',
+          './assets/db/darePlaces.db',
+          './assets/db/nmCountries.db'
         ]
       }
     ],
     'expo-font',
     [
-      "expo-av",
+      'expo-av',
       {
-        "microphonePermission": "Allow Nomadmania to access your microphone."
+        microphonePermission: 'Allow Nomadmania to access your microphone.'
       }
-    ]
+    ],
+    ['@react-native-firebase/messaging']
   ]
 });

+ 3 - 2
package.json

@@ -16,6 +16,7 @@
     "@react-native-clipboard/clipboard": "^1.14.2",
     "@react-native-community/datetimepicker": "8.0.1",
     "@react-native-community/netinfo": "11.3.1",
+    "@react-native-firebase/messaging": "^21.2.0",
     "@react-navigation/bottom-tabs": "^6.5.11",
     "@react-navigation/drawer": "^6.6.15",
     "@react-navigation/material-top-tabs": "^6.6.5",
@@ -63,8 +64,8 @@
     "react-native-gesture-handler": "~2.16.1",
     "react-native-get-random-values": "^1.11.0",
     "react-native-gifted-chat": "^2.6.3",
-    "react-native-haptic-feedback": "^2.3.2",
     "react-native-google-places-autocomplete": "^2.5.7",
+    "react-native-haptic-feedback": "^2.3.2",
     "react-native-image-viewing": "^0.2.2",
     "react-native-keyboard-aware-scroll-view": "^0.9.5",
     "react-native-linear-gradient": "^2.8.3",
@@ -86,8 +87,8 @@
     "react-native-tab-view": "^3.5.2",
     "react-native-video": "^6.5.0",
     "react-native-view-shot": "^3.7.0",
-    "react-native-wheel-pick": "^1.2.2",
     "react-native-walkthrough-tooltip": "^1.6.0",
+    "react-native-wheel-pick": "^1.2.2",
     "uuid": "^10.0.0",
     "yup": "^1.3.3",
     "zustand": "^4.4.7"

+ 10 - 5
src/modules/api/auth/auth-api.ts

@@ -28,11 +28,16 @@ export const authApi = {
     const formData = new FormData();
 
     formData.append('user', JSON.stringify(data.user));
-    formData.append('photo', {
-      type: data.photo.type,
-      uri: data.photo.uri,
-      name: data.photo.name
-    } as unknown as Blob);
+    if (data.photo && data.photo?.uri) {
+      formData.append('photo', {
+        type:
+          data.photo.type === 'image'
+            ? data.photo.type + '/' + data.photo.uri.split('.').pop()!
+            : data.photo.type,
+        uri: data.photo.uri,
+        name: data.photo.name
+      } as unknown as Blob);
+    }
 
     return request.postForm<PostRegisterUserReturn>(API.REGISTER, formData);
   },

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

@@ -62,6 +62,10 @@ export interface PostSendMessage {
   reply_to_id?: number;
 }
 
+export interface PostSendMessageReturn extends ResponseType {
+  message_id: number;
+}
+
 export interface PostMessagesReceivedOrRead {
   token: string;
   from_user: number;
@@ -128,7 +132,8 @@ export const chatApi = {
       no_of_messages,
       previous_than_message_id
     }),
-  sendMessage: (data: PostSendMessage) => request.postForm<ResponseType>(API.SEND_MESSAGE, data),
+  sendMessage: (data: PostSendMessage) =>
+    request.postForm<PostSendMessageReturn>(API.SEND_MESSAGE, data),
   messagesReceived: (data: PostMessagesReceivedOrRead) =>
     request.postForm<ResponseType>(API.MESSAGES_RECEIVED, data),
   messagesRead: (data: PostMessagesReceivedOrRead) =>

+ 9 - 8
src/modules/api/chat/queries/use-post-send-message.tsx

@@ -1,17 +1,18 @@
 import { useMutation } from '@tanstack/react-query';
 
 import { chatQueryKeys } from '../chat-query-keys';
-import { chatApi, type PostSendMessage } from '../chat-api';
+import { chatApi, type PostSendMessage, type PostSendMessageReturn } from '../chat-api';
 
 import type { BaseAxiosError } from '../../../../types';
-import { ResponseType } from '@api/response-type';
 
 export const usePostSendMessageMutation = () => {
-  return useMutation<ResponseType, BaseAxiosError, PostSendMessage, ResponseType>({
-    mutationKey: chatQueryKeys.sendMessage(),
-    mutationFn: async (data) => {
-      const response = await chatApi.sendMessage(data);
-      return response.data;
+  return useMutation<PostSendMessageReturn, BaseAxiosError, PostSendMessage, PostSendMessageReturn>(
+    {
+      mutationKey: chatQueryKeys.sendMessage(),
+      mutationFn: async (data) => {
+        const response = await chatApi.sendMessage(data);
+        return response.data;
+      }
     }
-  });
+  );
 };

+ 3 - 1
src/screens/InAppScreens/MapScreen/index.tsx

@@ -7,7 +7,8 @@ import {
   TextInput,
   TouchableOpacity,
   View,
-  Image
+  Image,
+  StatusBar
 } from 'react-native';
 import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
 import MapView, { Geojson, Marker, UrlTile } from 'react-native-maps';
@@ -868,6 +869,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
 
   return (
     <View style={styles.container}>
+      <StatusBar translucent backgroundColor="transparent" />
       <ClusteredMapView
         region={initialRegion}
         ref={mapRef}

+ 1 - 0
src/screens/InAppScreens/MapScreen/style.tsx

@@ -7,6 +7,7 @@ export const styles = StyleSheet.create({
     ...StyleSheet.absoluteFillObject,
     alignItems: 'center',
     justifyContent: 'flex-end',
+    paddingTop: 0
   },
   map: {
     ...StyleSheet.absoluteFillObject,

+ 254 - 282
src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx

@@ -17,7 +17,6 @@ import {
   GiftedChat,
   Bubble,
   InputToolbar,
-  Actions,
   IMessage,
   Send,
   BubbleProps,
@@ -26,13 +25,7 @@ import {
   MessageProps
 } from 'react-native-gifted-chat';
 import { MaterialCommunityIcons } from '@expo/vector-icons';
-import * as ImagePicker from 'expo-image-picker';
-import { useActionSheet } from '@expo/react-native-action-sheet';
-import {
-  GestureHandlerRootView,
-  LongPressGestureHandler,
-  Swipeable
-} from 'react-native-gesture-handler';
+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';
@@ -55,7 +48,6 @@ import {
 } from '@api/chat';
 import { CustomMessage, Message, Reaction } from '../types';
 import { API_HOST, WEBSOCKET_URL } from 'src/constants';
-import { getFontSize } from 'src/utils';
 import ReactionBar from '../Components/ReactionBar';
 import OptionsMenu from '../Components/OptionsMenu';
 import EmojiSelectorModal from '../Components/EmojiSelectorModal';
@@ -63,9 +55,9 @@ 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 * as Notifications from 'expo-notifications';
 import { usePushNotification } from 'src/contexts/PushNotificationContext';
 import ReactionsListModal from '../Components/ReactionsListModal';
+import { dismissChatNotifications } from '../utils';
 
 const options = {
   enableVibrateFallback: true,
@@ -75,14 +67,15 @@ const options = {
 const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭'];
 
 const ChatScreen = ({ route }: { route: any }) => {
+  const token = storage.get('token', StoreType.STRING) as string;
   const { id, name, avatar }: { id: number; name: string; avatar: string | null } = route.params;
+
   const currentUserId = storage.get('uid', StoreType.STRING) as number;
-  const token = storage.get('token', StoreType.STRING) as string;
   const insets = useSafeAreaInsets();
   const [messages, setMessages] = useState<CustomMessage[] | null>();
-  const { showActionSheetWithOptions } = useActionSheet();
   const navigation = useNavigation();
-  const { data: chatData, isFetching, refetch } = usePostGetChatWithQuery(token, id, -1, -1, true);
+  const [prevThenMessageId, setPrevThenMessageId] = useState<number>(-1);
+  const { data: chatData } = usePostGetChatWithQuery(token, id, 50, prevThenMessageId, true); // to do cache
   const { mutateAsync: sendMessage } = usePostSendMessageMutation();
 
   const swipeableRowRef = useRef<Swipeable | null>(null);
@@ -121,6 +114,8 @@ const ChatScreen = ({ route }: { route: 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 socket = useRef<WebSocket | null>(null);
 
@@ -128,103 +123,11 @@ const ChatScreen = ({ route }: { route: any }) => {
     setModalInfo({ ...modalInfo, visible: false });
   };
 
-  const dismissChatNotifications = async (chatWithUserId: number) => {
-    const { status } = await Notifications.getPermissionsAsync();
-    if (status !== 'granted' || !isSubscribed) {
-      setModalInfo({
-        visible: true,
-        type: 'success',
-        message:
-          'To use this feature we need your permission to access your notifications. You will be redirected to the notification settings screen where you need to enable them.',
-        action: () =>
-          // @ts-ignore
-          navigation.navigate(NAVIGATION_PAGES.MENU_DRAWER, {
-            screen: NAVIGATION_PAGES.NOTIFICATIONS
-          })
-      });
-      return;
-    }
-
-    const getNotificationData = (notification: Notifications.Notification) => {
-      if (Platform.OS === 'android') {
-        const data = notification.request.content.data;
-        if (data?.params) {
-          try {
-            return JSON.parse(data.params) ?? {};
-          } catch (error) {
-            console.error('Error parsing params:', error);
-            return {};
-          }
-        } else {
-          Notifications.dismissNotificationAsync(notification.request.identifier);
-          return {};
-        }
-      } else {
-        const data = (notification.request.trigger as Notifications.PushNotificationTrigger)
-          ?.payload;
-        if (data?.params) {
-          try {
-            return JSON.parse(data.params as string) ?? {};
-          } catch (error) {
-            console.error('Error parsing params:', error);
-            return {};
-          }
-        }
-      }
-    };
-
-    const clearNotificationsFromUser = async (userId: number) => {
-      const presentedNotifications = await Notifications.getPresentedNotificationsAsync();
-      presentedNotifications.forEach((notification) => {
-        const parsedParams = getNotificationData(notification);
-        const conversation_with_user = parsedParams?.id;
-
-        if (conversation_with_user === userId) {
-          Notifications.dismissNotificationAsync(notification.request.identifier);
-        }
-      });
-    };
-
-    await clearNotificationsFromUser(chatWithUserId);
-
-    Notifications.setNotificationHandler({
-      handleNotification: async (notification) => {
-        let conversation_with_user = 0;
-        const parsedParams = getNotificationData(notification);
-        conversation_with_user = parsedParams?.id;
-
-        if (conversation_with_user === chatWithUserId) {
-          return {
-            shouldShowAlert: false,
-            shouldPlaySound: false,
-            shouldSetBadge: false
-          };
-        }
-
-        return {
-          shouldShowAlert: true,
-          shouldPlaySound: false,
-          shouldSetBadge: false
-        };
-      }
-    });
-
-    return () => {
-      Notifications.setNotificationHandler({
-        handleNotification: async () => ({
-          shouldShowAlert: true,
-          shouldPlaySound: false,
-          shouldSetBadge: false
-        })
-      });
-    };
-  };
-
   useEffect(() => {
     let unsubscribe: any;
 
     const setupNotificationHandler = async () => {
-      unsubscribe = await dismissChatNotifications(id);
+      unsubscribe = await dismissChatNotifications(id, isSubscribed, setModalInfo, navigation);
     };
 
     setupNotificationHandler();
@@ -251,40 +154,188 @@ const ChatScreen = ({ route }: { route: any }) => {
     };
 
     return () => {
-      socket.current?.close();
+      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) {
-          refetch();
+        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 'new_reaction':
-        if (data.conversation_with === id) {
-          refetch();
+
+      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 sendWebSocketMessage = (action: string) => {
+  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 }));
+      }
+    }, 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) {
-      socket.current.send(JSON.stringify({ action, conversation_with: id }));
+      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));
     }
   };
 
@@ -346,11 +397,35 @@ const ChatScreen = ({ route }: { route: any }) => {
             setUnreadMessageIndex(0);
           }
         }
-        setMessages(mappedMessages);
+
+        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);
+        }
+
+        setIsLoadingEarlier(false);
       }
     }, [chatData])
   );
 
+  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[] }) => {
@@ -373,10 +448,9 @@ const ChatScreen = ({ route }: { route: any }) => {
           messages_id: newViewableUnreadMessages
         },
         {
-          onSuccess: (res) => {
+          onSuccess: () => {
             newViewableUnreadMessages.forEach((id) => sentToServer.current.add(id));
-            // sendWebSocketMessage('messages_read');
-            sendWebSocketMessage('new_message');
+            sendWebSocketMessage('messages_read', null, null, newViewableUnreadMessages);
           }
         }
       );
@@ -467,12 +541,26 @@ const ChatScreen = ({ route }: { route: any }) => {
       },
       {
         onSuccess: () => {
-          setMessages((prevMessages) =>
-            prevMessages ? prevMessages.filter((msg) => msg._id !== messageId) : []
+          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;
+              }) ?? []
           );
-          // sendWebSocketMessage('message_deleted');
-          refetch();
-          sendWebSocketMessage('new_message');
+          const messageToDelete = messages?.find((msg) => msg._id === messageId);
+          if (messageToDelete) {
+            sendWebSocketMessage('delete_message', messageToDelete, null, null);
+          }
         }
       }
     );
@@ -534,30 +622,20 @@ const ChatScreen = ({ route }: { route: any }) => {
 
     return (
       <View
-        style={{
-          flexDirection: 'row',
-          justifyContent: hasReactions ? 'space-between' : 'flex-end',
-          alignItems: 'center',
-          paddingHorizontal: 8,
-          paddingBottom: 6,
-          flexShrink: 1,
-          flexGrow: 1,
-          gap: 12
-        }}
+        style={[
+          styles.bottomContainer,
+          {
+            justifyContent: hasReactions ? 'space-between' : 'flex-end'
+          }
+        ]}
       >
         {hasReactions && (
           <TouchableOpacity
             style={[
+              styles.bottomCustomContainer,
               {
-                flexDirection: 'row',
-                alignItems: 'center',
-                flexShrink: 0,
                 backgroundColor:
-                  time.position === 'left' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.2)',
-                borderRadius: 12,
-                paddingHorizontal: 6,
-                paddingVertical: 4,
-                gap: 6
+                  time.position === 'left' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.2)'
               }
             ]}
             onPress={() =>
@@ -597,25 +675,8 @@ const ChatScreen = ({ route }: { route: any }) => {
             })}
           </TouchableOpacity>
         )}
-        <View
-          style={{
-            flexDirection: 'row',
-            gap: 4,
-            alignItems: 'center',
-            alignSelf: 'flex-end'
-          }}
-        >
-          <Text
-            style={{
-              color: Colors.LIGHT_GRAY,
-              fontSize: getFontSize(10),
-              fontWeight: '600',
-              paddingLeft: 8,
-              flexShrink: 0
-            }}
-          >
-            {formattedTime}
-          </Text>
+        <View style={styles.timeContainer}>
+          <Text style={styles.timeText}>{formattedTime}</Text>
           {renderTicks(time.currentMessage)}
         </View>
       </View>
@@ -678,6 +739,8 @@ const ChatScreen = ({ route }: { route: any }) => {
       }
       const message = { ...newMessages[0], pending: true };
 
+      setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message]));
+
       sendMessage(
         {
           token,
@@ -686,118 +749,29 @@ const ChatScreen = ({ route }: { route: any }) => {
           reply_to_id: replyMessage ? (replyMessage._id as number) : -1
         },
         {
-          onSuccess: () => sendWebSocketMessage('new_message'),
+          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)
         }
       );
 
-      setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message]));
       clearReplyMessage();
     },
     [replyMessage]
   );
 
-  const openActionSheet = () => {
-    const options = ['Open Camera', 'Select from gallery', 'Cancel'];
-    const cancelButtonIndex = 2;
-
-    showActionSheetWithOptions(
-      {
-        options,
-        cancelButtonIndex
-      },
-      async (buttonIndex) => {
-        if (buttonIndex === 0) {
-          openCamera();
-        } else if (buttonIndex === 1) {
-          openGallery();
-        }
-      }
-    );
-  };
-
-  const openCamera = async () => {
-    const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
-
-    if (permissionResult.granted === false) {
-      alert('Permission denied to access camera');
-      return;
-    }
-
-    const result = await ImagePicker.launchCameraAsync({
-      mediaTypes: ImagePicker.MediaTypeOptions.All,
-      quality: 1,
-      allowsEditing: true
-    });
-
-    if (!result.canceled) {
-      const newMedia = {
-        _id: Date.now().toString(),
-        createdAt: new Date(),
-        user: { _id: +currentUserId, name: 'Me' },
-        image: result.assets[0].type === 'image' ? result.assets[0].uri : null,
-        video: result.assets[0].type === 'video' ? result.assets[0].uri : null
-      };
-      setMessages((previousMessages) =>
-        GiftedChat.append(previousMessages ?? [], [newMedia as any])
-      );
-    }
-  };
-
-  const openGallery = async () => {
-    const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
-
-    if (permissionResult.granted === false) {
-      alert('Denied');
-      return;
-    }
-
-    const result = await ImagePicker.launchImageLibraryAsync({
-      mediaTypes: ImagePicker.MediaTypeOptions.All,
-      allowsMultipleSelection: true,
-      quality: 1
-    });
-
-    if (!result.canceled && result.assets) {
-      const imageMessages = result.assets.map((asset) => ({
-        _id: Date.now().toString() + asset.uri,
-        createdAt: new Date(),
-        user: { _id: +currentUserId, name: 'Me' },
-        image: asset.type === 'image' ? asset.uri : null,
-        video: asset.type === 'video' ? asset.uri : null
-      }));
-
-      setMessages((previousMessages) =>
-        GiftedChat.append(previousMessages ?? [], imageMessages as any[])
-      );
-    }
-  };
-
-  const renderMessageVideo = (props: BubbleProps<CustomMessage>) => {
-    const { currentMessage } = props;
-
-    if (currentMessage.video) {
-      return (
-        <LongPressGestureHandler
-          onHandlerStateChange={(event) => handleLongPress(currentMessage, props)}
-        >
-          <TouchableOpacity
-            onPress={() => setSelectedMedia(currentMessage.video)}
-            style={styles.mediaContainer}
-          >
-            <Video
-              source={{ uri: currentMessage.video }}
-              style={styles.chatMedia}
-              useNativeControls
-            />
-          </TouchableOpacity>
-        </LongPressGestureHandler>
-      );
-    }
-
-    return null;
-  };
-
   const addReaction = (messageId: number, reaction: string) => {
     if (!messages) return;
 
@@ -823,7 +797,12 @@ const ChatScreen = ({ route }: { route: any }) => {
     reactToMessage(
       { token, message_id: messageId, reaction: reaction, conversation_with_user: id },
       {
-        onSuccess: () => sendWebSocketMessage('new_reaction'),
+        onSuccess: () => {
+          const message = messages.find((msg) => msg._id === messageId);
+          if (message) {
+            sendWebSocketMessage('new_reaction', message, reaction);
+          }
+        },
         onError: (err) => console.log('err', err)
       }
     );
@@ -1063,14 +1042,7 @@ const ChatScreen = ({ route }: { route: any }) => {
   const renderScrollToBottom = () => {
     return (
       <TouchableOpacity
-        style={{
-          position: 'absolute',
-          bottom: -20,
-          right: -20,
-          backgroundColor: Colors.DARK_BLUE,
-          borderRadius: 20,
-          padding: 8
-        }}
+        style={styles.scrollToBottom}
         onPress={() => {
           if (flatList.current) {
             flatList.current.scrollToIndex({ index: 0, animated: true });
@@ -1164,15 +1136,7 @@ const ChatScreen = ({ route }: { route: any }) => {
             onInputTextChanged={(text) => handleTyping(text.length > 0)}
             isTyping={isTyping}
             renderSend={(props) => (
-              <View
-                style={{
-                  flexDirection: 'row',
-                  height: '100%',
-                  alignItems: 'center',
-                  justifyContent: 'center',
-                  paddingHorizontal: 14
-                }}
-              >
+              <View style={styles.sendBtn}>
                 {props.text?.trim() && (
                   <Send
                     {...props}
@@ -1198,7 +1162,6 @@ const ChatScreen = ({ route }: { route: any }) => {
             renderChatFooter={() => (
               <ReplyMessageBar clearReply={clearReplyMessage} message={replyMessage} />
             )}
-            // renderMessageVideo={renderMessageVideo}
             renderAvatar={null}
             maxComposerHeight={100}
             renderComposer={(props) => <Composer {...props} />}
@@ -1225,6 +1188,15 @@ const ChatScreen = ({ route }: { route: any }) => {
                 }
               }
             ]}
+            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} />

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

@@ -105,5 +105,51 @@ export const styles = StyleSheet.create({
     fontWeight: '600',
     textAlign: 'center',
     color: Colors.DARK_BLUE
+  },
+  scrollToBottom: {
+    position: 'absolute',
+    bottom: -20,
+    right: -20,
+    backgroundColor: Colors.DARK_BLUE,
+    borderRadius: 20,
+    padding: 8
+  },
+  sendBtn: {
+    flexDirection: 'row',
+    height: '100%',
+    alignItems: 'center',
+    justifyContent: 'center',
+    paddingHorizontal: 14
+  },
+  timeContainer: {
+    flexDirection: 'row',
+    gap: 4,
+    alignItems: 'center',
+    alignSelf: 'flex-end'
+  },
+  timeText: {
+    color: Colors.LIGHT_GRAY,
+    fontSize: getFontSize(10),
+    fontWeight: '600',
+    paddingLeft: 8,
+    flexShrink: 0
+  },
+  bottomContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 8,
+    paddingBottom: 6,
+    flexShrink: 1,
+    flexGrow: 1,
+    gap: 12
+  },
+  bottomCustomContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    flexShrink: 0,
+    borderRadius: 12,
+    paddingHorizontal: 6,
+    paddingVertical: 4,
+    gap: 6
   }
 });

+ 6 - 3
src/screens/InAppScreens/MessagesScreen/Components/ReactionsListModal.tsx

@@ -4,6 +4,7 @@ import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
 import { usePostUnreactToMessageMutation } from '@api/chat';
 import { getFontSize } from 'src/utils';
 import { Colors } from 'src/theme';
+import { CustomMessage } from '../types';
 
 const ReactionsListModal = () => {
   const [reactionsData, setReactionsData] = useState<{
@@ -13,7 +14,7 @@ const ReactionsListModal = () => {
     messageId: number;
     conversation_with_user: number;
     setMessages: (messages: any) => void;
-    sendWebSocketMessage: (message: string) => void;
+    sendWebSocketMessage: (action: string, data: any) => void;
   } | null>(null);
 
   const { mutateAsync: unreactToMessage } = usePostUnreactToMessageMutation();
@@ -26,7 +27,7 @@ const ReactionsListModal = () => {
       messageId: number;
       conversation_with_user: number;
       setMessages: (messages: any) => void;
-      sendWebSocketMessage: (message: string) => void;
+      sendWebSocketMessage: (action: string, data: any) => void;
     } | null
   ) => {
     setReactionsData(payload);
@@ -56,7 +57,9 @@ const ReactionsListModal = () => {
                 return msg;
               })
             );
-            reactionsData.sendWebSocketMessage('new_reaction');
+            reactionsData.sendWebSocketMessage('unreact', {
+              _id: reactionsData.messageId
+            } as unknown as CustomMessage);
           }
         }
       );

+ 12 - 9
src/screens/InAppScreens/MessagesScreen/index.tsx

@@ -1,12 +1,5 @@
 import React, { useState, useEffect, useRef, useCallback } from 'react';
-import {
-  View,
-  Text,
-  TouchableOpacity,
-  Image,
-  Platform,
-  TouchableHighlight,
-} from 'react-native';
+import { View, Text, TouchableOpacity, Image, Platform, TouchableHighlight } from 'react-native';
 import {
   AvatarWithInitials,
   HorizontalTabView,
@@ -72,7 +65,6 @@ const MessagesScreen = () => {
   const [blocked, setBlocked] = useState<Blocked[]>([]);
   const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
 
-
   const [filteredChats, setFilteredChats] = useState<{
     all: Chat[];
     unread: Chat[];
@@ -109,9 +101,20 @@ const MessagesScreen = () => {
     }, 500);
   };
 
+  useEffect(() => {
+    const pingInterval = setInterval(() => {
+      if (socket.current && socket.current.readyState === WebSocket.OPEN) {
+        socket.current.send(JSON.stringify({ action: 'ping', conversation_with: 0 }));
+      }
+    }, 50000);
+
+    return () => clearInterval(pingInterval);
+  }, []);
+
   const handleWebSocketMessage = (data: any) => {
     switch (data.action) {
       case 'new_message':
+      case 'messages_read':
         refetch();
         break;
       case 'is_typing':

+ 100 - 0
src/screens/InAppScreens/MessagesScreen/utils.ts

@@ -1,4 +1,8 @@
 import moment from 'moment';
+import * as Notifications from 'expo-notifications';
+import { Platform } from 'react-native';
+import { NAVIGATION_PAGES } from 'src/types';
+import { usePushNotification } from 'src/contexts/PushNotificationContext';
 
 export const formatDate = (dateString: Date): string => {
   const inputDate = moment.utc(dateString).local();
@@ -17,3 +21,99 @@ export const formatDate = (dateString: Date): string => {
 
   return inputDate.format('DD.MM');
 };
+
+export const dismissChatNotifications = async (
+  chatWithUserId: number,
+  isSubscribed: boolean,
+  setModalInfo: (data: any) => void,
+  navigation: any
+) => {
+  const { status } = await Notifications.getPermissionsAsync();
+  if (status !== 'granted' || !isSubscribed) {
+    setModalInfo({
+      visible: true,
+      type: 'success',
+      message:
+        'To use this feature we need your permission to access your notifications. You will be redirected to the notification settings screen where you need to enable them.',
+      action: () =>
+        // @ts-ignore
+        navigation.navigate(NAVIGATION_PAGES.MENU_DRAWER, {
+          screen: NAVIGATION_PAGES.NOTIFICATIONS
+        })
+    });
+    return;
+  }
+
+  const getNotificationData = (notification: Notifications.Notification) => {
+    if (Platform.OS === 'android') {
+      const data = notification.request.content.data;
+      if (data?.params) {
+        try {
+          return JSON.parse(data.params) ?? {};
+        } catch (error) {
+          console.error('Error parsing params:', error);
+          return {};
+        }
+      } else {
+        Notifications.dismissNotificationAsync(notification.request.identifier);
+        return {};
+      }
+    } else {
+      const data = (notification.request.trigger as Notifications.PushNotificationTrigger)?.payload;
+      if (data?.params) {
+        try {
+          return JSON.parse(data.params as string) ?? {};
+        } catch (error) {
+          console.error('Error parsing params:', error);
+          return {};
+        }
+      }
+    }
+  };
+
+  const clearNotificationsFromUser = async (userId: number) => {
+    const presentedNotifications = await Notifications.getPresentedNotificationsAsync();
+    presentedNotifications.forEach((notification) => {
+      const parsedParams = getNotificationData(notification);
+      const conversation_with_user = parsedParams?.id;
+
+      if (conversation_with_user === userId) {
+        Notifications.dismissNotificationAsync(notification.request.identifier);
+      }
+    });
+  };
+
+  await clearNotificationsFromUser(chatWithUserId);
+
+  Notifications.setNotificationHandler({
+    handleNotification: async (notification) => {
+      let conversation_with_user = 0;
+      const parsedParams = getNotificationData(notification);
+      conversation_with_user = parsedParams?.id;
+
+      if (conversation_with_user === chatWithUserId) {
+        return {
+          shouldShowAlert: false,
+          shouldPlaySound: false,
+          shouldSetBadge: false
+        };
+      }
+
+      return {
+        shouldShowAlert: true,
+        shouldPlaySound: false,
+        shouldSetBadge: false
+      };
+    }
+  });
+
+  return () => {
+    Notifications.setNotificationHandler({
+      handleNotification: async () => ({
+        shouldShowAlert: true,
+        shouldPlaySound: false,
+        shouldSetBadge: false
+      })
+    });
+  };
+};

+ 6 - 2
src/screens/RegisterScreen/EditAccount/index.tsx

@@ -103,14 +103,18 @@ const EditAccount = () => {
               validationSchema={SignUpSchema}
               onSubmit={async (values) => {
                 setIsSubmitting(true);
+
+                const dateObject = new Date(values.date_of_birth);
+                const formattedDate = dateObject.toISOString().split('T')[0];
+
                 const data = {
                   user: {
                     ...user,
                     first_name: values.first_name,
                     last_name: values.last_name,
-                    date_of_birth: values.date_of_birth,
+                    date_of_birth: formattedDate,
                     homebase: values.homebase,
-                    homebase2: values.homebase2
+                    homebase2: values.homebase2 ?? -1
                   },
                   photo: values.photo.uri
                     ? {