Explorar o código

Merge branch 'chat' of Viktoriia/nomadmania-app into dev

Viktoriia hai 8 meses
pai
achega
6cee167bfc
Modificáronse 86 ficheiros con 4575 adicións e 96 borrados
  1. 2 2
      App.tsx
  2. 20 1
      Route.tsx
  3. 27 8
      app.config.ts
  4. 3 0
      assets/icons/messages/archive.svg
  5. 10 0
      assets/icons/messages/ban.svg
  6. 10 0
      assets/icons/messages/bell-slash.svg
  7. 4 0
      assets/icons/messages/chat-plus.svg
  8. 4 0
      assets/icons/messages/check-read.svg
  9. 3 0
      assets/icons/messages/check-unread.svg
  10. 2 0
      assets/icons/messages/comments.svg
  11. 3 0
      assets/icons/messages/dots.svg
  12. 3 0
      assets/icons/messages/pin.svg
  13. 10 0
      assets/icons/messages/send.svg
  14. 3 0
      assets/icons/messages/unpin.svg
  15. BIN=BIN
      assets/notification-android-icon.png
  16. 13 1
      package.json
  17. 4 0
      src/components/ErrorModal/index.tsx
  18. 2 1
      src/components/FlatList/index.tsx
  19. 13 4
      src/components/HorizontalTabView/index.tsx
  20. 3 0
      src/components/MenuDrawer/index.tsx
  21. 45 0
      src/components/MessagesDot/index.tsx
  22. 2 1
      src/components/PageWrapper/index.tsx
  23. 1 2
      src/components/PageWrapper/styles.ts
  24. 10 7
      src/components/SplashSpinner/index.tsx
  25. 28 3
      src/components/TabBarButton/index.tsx
  26. 4 2
      src/components/WarningModal/index.tsx
  27. 1 0
      src/components/index.ts
  28. 2 0
      src/constants/secrets.ts
  29. 1 1
      src/contexts/NotificationContext.tsx
  30. 31 6
      src/contexts/PushNotificationContext.tsx
  31. 10 5
      src/modules/api/auth/auth-api.ts
  32. 157 0
      src/modules/api/chat/chat-api.ts
  33. 23 0
      src/modules/api/chat/chat-query-keys.tsx
  34. 3 0
      src/modules/api/chat/index.ts
  35. 16 0
      src/modules/api/chat/queries/index.ts
  36. 17 0
      src/modules/api/chat/queries/use-post-delete-conversation.tsx
  37. 17 0
      src/modules/api/chat/queries/use-post-delete-message.tsx
  38. 17 0
      src/modules/api/chat/queries/use-post-get-blocked.tsx
  39. 17 0
      src/modules/api/chat/queries/use-post-get-conversation-list.tsx
  40. 28 0
      src/modules/api/chat/queries/use-post-get-conversation-with.tsx
  41. 21 0
      src/modules/api/chat/queries/use-post-get-new-messages-present.tsx
  42. 17 0
      src/modules/api/chat/queries/use-post-messages-read.tsx
  43. 17 0
      src/modules/api/chat/queries/use-post-messages-received.tsx
  44. 17 0
      src/modules/api/chat/queries/use-post-react-to-message.tsx
  45. 17 0
      src/modules/api/chat/queries/use-post-search-users.tsx
  46. 18 0
      src/modules/api/chat/queries/use-post-send-message.tsx
  47. 17 0
      src/modules/api/chat/queries/use-post-set-archive.tsx
  48. 17 0
      src/modules/api/chat/queries/use-post-set-block.tsx
  49. 17 0
      src/modules/api/chat/queries/use-post-set-mute.tsx
  50. 17 0
      src/modules/api/chat/queries/use-post-set-pin.tsx
  51. 17 0
      src/modules/api/chat/queries/use-post-unreact-to-message.tsx
  52. 1 1
      src/modules/api/notifications/notifications-api.ts
  53. 3 1
      src/screens/InAppScreens/MapScreen/index.tsx
  54. 1 0
      src/screens/InAppScreens/MapScreen/style.tsx
  55. 1355 0
      src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx
  56. 155 0
      src/screens/InAppScreens/MessagesScreen/ChatScreen/styles.tsx
  57. 104 0
      src/screens/InAppScreens/MessagesScreen/Components/ChatMessageBox.tsx
  58. 70 0
      src/screens/InAppScreens/MessagesScreen/Components/EmojiSelectorModal.tsx
  59. 245 0
      src/screens/InAppScreens/MessagesScreen/Components/MoreModal.tsx
  60. 84 0
      src/screens/InAppScreens/MessagesScreen/Components/OptionsMenu.tsx
  61. 91 0
      src/screens/InAppScreens/MessagesScreen/Components/ReactionBar.tsx
  62. 141 0
      src/screens/InAppScreens/MessagesScreen/Components/ReactionsListModal.tsx
  63. 96 0
      src/screens/InAppScreens/MessagesScreen/Components/ReplyMessageBar.tsx
  64. 161 0
      src/screens/InAppScreens/MessagesScreen/Components/SearchUsersModal.tsx
  65. 105 0
      src/screens/InAppScreens/MessagesScreen/Components/SwipeableBlockedRow.tsx
  66. 183 0
      src/screens/InAppScreens/MessagesScreen/Components/SwipeableRow.tsx
  67. 12 0
      src/screens/InAppScreens/MessagesScreen/constants.ts
  68. 496 0
      src/screens/InAppScreens/MessagesScreen/index.tsx
  69. 86 0
      src/screens/InAppScreens/MessagesScreen/styles.tsx
  70. 87 0
      src/screens/InAppScreens/MessagesScreen/types.ts
  71. 119 0
      src/screens/InAppScreens/MessagesScreen/utils.ts
  72. 4 0
      src/screens/InAppScreens/ProfileScreen/Profile/edit-personal-info.tsx
  73. 44 1
      src/screens/InAppScreens/ProfileScreen/index.tsx
  74. 2 1
      src/screens/InAppScreens/TravellersScreen/index.tsx
  75. 2 1
      src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/index.tsx
  76. 3 2
      src/screens/InAppScreens/TravelsScreen/EarthScreen/index.tsx
  77. 2 5
      src/screens/InAppScreens/TravelsScreen/SuggestSeriesScreen/index.tsx
  78. 2 1
      src/screens/InAppScreens/TravelsScreen/index.tsx
  79. 4 0
      src/screens/LoginScreen/index.tsx
  80. 59 31
      src/screens/NotificationsScreen/index.tsx
  81. 6 2
      src/screens/RegisterScreen/EditAccount/index.tsx
  82. 5 3
      src/screens/WelcomeScreen/index.tsx
  83. 16 0
      src/stores/chatStore.ts
  84. 31 0
      src/stores/unreadMessagesStore.ts
  85. 36 3
      src/types/api.ts
  86. 3 0
      src/types/navigation.ts

+ 2 - 2
App.tsx

@@ -16,13 +16,13 @@ import { ErrorModal } from 'src/components';
 import { NotificationProvider } from 'src/contexts/NotificationContext';
 import React from 'react';
 
-const routingInstrumentation = new Sentry.ReactNavigationInstrumentation({
+const routingInstrumentation = Sentry.reactNavigationIntegration({
   enableTimeToInitialDisplay: true
 });
 
 Sentry.init({
   dsn: 'https://c9b37005f4be22a17a582603ebc17598@o4507781200543744.ingest.de.sentry.io/4507781253824592',
-  integrations: [new Sentry.ReactNativeTracing({ routingInstrumentation })],
+  integrations: [Sentry.reactNativeTracingIntegration({ routingInstrumentation })],
   debug: false,
   ignoreErrors: ['Network Error', 'ECONNABORTED', 'timeout of 10000ms exceeded']
 });

+ 20 - 1
Route.tsx

@@ -90,7 +90,10 @@ import NotificationsListScreen from 'src/screens/NotificationsScreen/Notificatio
 import FriendsNotificationsScreen from 'src/screens/NotificationsScreen/FriendsNotificactionsScreen';
 import MessagesNotificationsScreen from 'src/screens/NotificationsScreen/MessagesNotificationsScreen';
 import SystemNotificationsScreen from 'src/screens/NotificationsScreen/SystemNotificationsScreen';
+import MessagesScreen from 'src/screens/InAppScreens/MessagesScreen';
+import ChatScreen from 'src/screens/InAppScreens/MessagesScreen/ChatScreen';
 import { Splash } from 'src/components/SplashSpinner';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 enableScreens();
 
@@ -116,6 +119,7 @@ const Route = () => {
   const [dbLoaded, setDbLoaded] = useState(false);
   const uid = storage.get('uid', StoreType.STRING);
   const { updateNotificationStatus } = useNotification();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
 
   const checkNmToken = async () => {
     if (token && uid) {
@@ -142,6 +146,7 @@ const Route = () => {
     storage.remove('visitedTilesUrl');
     storage.remove('filterSettings');
     updateNotificationStatus();
+    updateUnreadMessagesCount();
   };
 
   const fetchAndSaveUserInfo = async () => {
@@ -163,6 +168,7 @@ const Route = () => {
       // await checkTokenAndUpdate();
       await openDatabases();
       setDbLoaded(true);
+      updateUnreadMessagesCount();
       await findFastestServer();
     };
 
@@ -233,7 +239,7 @@ const Route = () => {
     cardStyle: { backgroundColor: 'white' },
     unmountOnBlur: true,
     gestureEnabled: Platform.OS === 'ios' ? true : false,
-    lazy: true
+    lazy: false,
   });
 
   const regionViewScreenOptions = {
@@ -408,6 +414,19 @@ const Route = () => {
           </ScreenStack.Navigator>
         )}
       </BottomTab.Screen>
+      <BottomTab.Screen name={NAVIGATION_PAGES.IN_APP_MESSAGES_TAB}>
+        {() => (
+          <ScreenStack.Navigator screenOptions={screenOptions}>
+            <ScreenStack.Screen name={NAVIGATION_PAGES.CHATS_LIST} component={MessagesScreen} />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.CHAT} component={ChatScreen} />
+            <ScreenStack.Screen
+              name={NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW}
+              component={ProfileScreen}
+            />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.USERS_MAP} component={UsersMapScreen} />
+          </ScreenStack.Navigator>
+        )}
+      </BottomTab.Screen>
       <BottomTab.Screen name={NAVIGATION_PAGES.MENU_DRAWER}>
         {() => (
           <ScreenStack.Navigator screenOptions={screenOptions}>

+ 27 - 8
app.config.ts

@@ -10,6 +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;
 
 dotenv.config({
   path: path.resolve(process.cwd(), '.env')
@@ -33,6 +35,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     API_HOST: API_HOST,
     MAP_HOST: MAP_HOST,
     GOOGLE_MAP_PLACES_APIKEY: GOOGLE_MAP_PLACES_APIKEY,
+    WEBSOCKET_URL: WEBSOCKET_URL,
     eas: {
       projectId: env.EAS_PROJECT_ID
     }
@@ -45,14 +48,20 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     resizeMode: 'cover',
     backgroundColor: '#ffffff'
   },
+  androidStatusBar: {
+    backgroundColor: 'transparent',
+    translucent: true,
+    barStyle: 'dark-content'
+  },
   notification: {
-    icon: './assets/notification-icon.png'
+    icon: './assets/notification-android-icon.png',
+    color: '#FFFFFF'
   },
   updates: {
     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
@@ -69,8 +78,11 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
         'Enable NomadMania.com to access your photo library to upload your profile picture. Any violence, excess of nudity, stolen picture, or scam is forbidden',
       NSPushNotificationsDescription:
         'This will allow NomadMania.com to send you notifications. Also you can disable it in app settings',
+      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: [
@@ -98,7 +110,8 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       'NOTIFICATIONS',
       'USER_FACING_NOTIFICATIONS',
       'INTERNET',
-      'CAMERA'
+      'CAMERA',
+      'MODIFY_AUDIO_SETTINGS'
     ],
     versionCode: 74 // next version submitted to Google Play needs to be higher than that 2.0.21
   },
@@ -132,12 +145,18 @@ 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-font',
+    [
+      'expo-av',
+      {
+        microphonePermission: 'Allow Nomadmania to access your microphone.'
+      }
+    ]
   ]
 });

+ 3 - 0
assets/icons/messages/archive.svg

@@ -0,0 +1,3 @@
+<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1.125 0.125H16.875C17.4973 0.125 18 0.627734 18 1.25V2.375C18 2.99727 17.4973 3.5 16.875 3.5H1.125C0.502734 3.5 0 2.99727 0 2.375V1.25C0 0.627734 0.502734 0.125 1.125 0.125ZM1.125 4.625H16.875V13.625C16.875 14.866 15.866 15.875 14.625 15.875H3.375C2.13398 15.875 1.125 14.866 1.125 13.625V4.625ZM5.625 7.4375C5.625 7.74687 5.87813 8 6.1875 8H11.8125C12.1219 8 12.375 7.74687 12.375 7.4375C12.375 7.12813 12.1219 6.875 11.8125 6.875H6.1875C5.87813 6.875 5.625 7.12813 5.625 7.4375Z" fill="#0F3F4F"/>
+</svg>

+ 10 - 0
assets/icons/messages/ban.svg

@@ -0,0 +1,10 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4126_37135)">
+<path d="M12.9094 14.502L3.49805 5.09063C2.71055 6.19102 2.25 7.54102 2.25 9C2.25 12.7266 5.27344 15.75 9 15.75C10.459 15.75 11.809 15.2895 12.9094 14.502ZM14.502 12.9094C15.2895 11.809 15.75 10.459 15.75 9C15.75 5.27344 12.7266 2.25 9 2.25C7.54102 2.25 6.19102 2.71055 5.09063 3.49805L14.502 12.9094ZM0 9C0 6.61305 0.948212 4.32387 2.63604 2.63604C4.32387 0.948212 6.61305 0 9 0C11.3869 0 13.6761 0.948212 15.364 2.63604C17.0518 4.32387 18 6.61305 18 9C18 11.3869 17.0518 13.6761 15.364 15.364C13.6761 17.0518 11.3869 18 9 18C6.61305 18 4.32387 17.0518 2.63604 15.364C0.948212 13.6761 0 11.3869 0 9Z"/>
+</g>
+<defs>
+<clipPath id="clip0_4126_37135">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 10 - 0
assets/icons/messages/bell-slash.svg

@@ -0,0 +1,10 @@
+<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4126_37122)">
+<path d="M1.09141 0.944377C0.798908 0.713752 0.374221 0.767189 0.143596 1.05969C-0.0870294 1.35219 -0.0335919 1.77688 0.258908 2.0075L16.9089 15.0575C17.2014 15.2881 17.6261 15.2347 17.8567 14.9422C18.0873 14.6497 18.0339 14.225 17.7414 13.9944L15.2608 12.0509C15.3367 11.8316 15.2973 11.5869 15.1511 11.4041L14.732 10.8809C13.9361 9.8825 13.5002 8.64219 13.5002 7.36531V6.42594C13.5002 4.29406 11.9392 2.52781 9.90016 2.20438V1.70094C9.90016 1.20313 9.49797 0.800939 9.00016 0.800939C8.50235 0.800939 8.10016 1.20313 8.10016 1.70094V2.20438C6.86547 2.40125 5.80797 3.12406 5.16391 4.13656L1.09141 0.944377ZM6.23547 4.97469C6.73891 4.09438 7.68672 3.50094 8.77516 3.50094H9.00016H9.22516C10.8395 3.50094 12.1502 4.81156 12.1502 6.42594V7.36531C12.1502 8.285 12.3302 9.18781 12.6761 10.0231L6.23547 4.97469ZM11.4245 12.5009L9.71172 11.1509H4.73641C5.33266 10.2284 5.70391 9.17375 5.81641 8.0825L4.50016 7.0475V7.36813C4.50016 8.645 4.06422 9.88531 3.26828 10.8809L2.84922 11.4041C2.6861 11.6066 2.65516 11.885 2.76766 12.1184C2.88016 12.3519 3.11641 12.5009 3.37516 12.5009H11.4245ZM10.8002 13.4009H9.00016H7.20016C7.20016 13.8791 7.3886 14.3375 7.7261 14.675C8.0636 15.0125 8.52203 15.2009 9.00016 15.2009C9.47828 15.2009 9.93672 15.0125 10.2742 14.675C10.6117 14.3375 10.8002 13.8791 10.8002 13.4009Z" fill="#0F3F4F"/>
+</g>
+<defs>
+<clipPath id="clip0_4126_37122">
+<rect width="18" height="14.4" fill="white" transform="translate(0 0.800781)"/>
+</clipPath>
+</defs>
+</svg>

+ 4 - 0
assets/icons/messages/chat-plus.svg

@@ -0,0 +1,4 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.09086 1.90625C3.99586 2.28114 0.00409926 5.75808 0.00409926 9.996C0.00409926 11.7577 0.695477 13.3866 1.86735 14.7187C1.79313 15.6757 1.4221 16.5274 1.03147 17.1758C0.968974 17.2813 0.902506 17.3866 0.832194 17.4921H0.820521C0.816615 17.4999 0.812678 17.5038 0.808772 17.5116C0.750178 17.6015 0.687723 17.6914 0.621317 17.7813C0.484599 17.9688 0.340036 18.1445 0.17988 18.3086C0.00409961 18.4883 -0.0506033 18.7539 0.0470527 18.9882C0.144709 19.2226 0.371303 19.375 0.625208 19.375C0.824427 19.375 1.0236 19.3633 1.22282 19.3438C1.23063 19.3438 1.2424 19.3398 1.25021 19.3398C1.42599 19.3242 1.59393 19.2969 1.7658 19.2656C1.79705 19.2578 1.82832 19.2539 1.85957 19.2461C2.55488 19.1094 3.22674 18.875 3.81658 18.6171C4.71502 18.2265 5.47287 17.7616 5.93771 17.4218C7.17989 17.871 8.55488 18.121 10.0041 18.121C15.4227 18.121 19.8308 14.6204 19.9962 10.2499C19.3502 11.0953 18.5138 11.788 17.552 12.2623C16.4154 14.4661 13.6685 16.246 10.0041 16.246C8.76973 16.246 7.60954 16.0312 6.57438 15.6562C5.98844 15.4452 5.33611 15.5391 4.83221 15.9063V15.9101C4.50799 16.1445 3.95719 16.4882 3.29313 16.7968C3.51188 16.2226 3.67988 15.5742 3.73457 14.8671C3.77363 14.3593 3.60954 13.8593 3.2736 13.4804C2.36345 12.4492 1.8791 11.246 1.8791 9.996C1.8791 7.33719 4.2655 4.65179 8.00894 3.93414C8.25525 3.19527 8.62371 2.51131 9.09086 1.90625Z" fill="#0F3F4F"/>
+<path d="M14.5332 0.623047C17.5535 0.623047 20.0019 3.07149 20.0019 6.0918C20.0019 9.1121 17.5535 11.5605 14.5332 11.5605C11.5129 11.5605 9.06445 9.1121 9.06445 6.0918C9.06445 3.07149 11.5129 0.623047 14.5332 0.623047ZM15.1408 3.66124C15.1408 3.32704 14.8674 3.0536 14.5332 3.0536C14.199 3.0536 13.9256 3.32704 13.9256 3.66124V5.48416H12.1026C11.7684 5.48416 11.495 5.7576 11.495 6.0918C11.495 6.426 11.7684 6.69944 12.1026 6.69944H13.9256V8.52235C13.9256 8.85655 14.199 9.12999 14.5332 9.12999C14.8674 9.12999 15.1408 8.85655 15.1408 8.52235V6.69944H16.9638C17.298 6.69944 17.5714 6.426 17.5714 6.0918C17.5714 5.7576 17.298 5.48416 16.9638 5.48416H15.1408V3.66124Z" fill="#0F3F4F"/>
+</svg>

+ 4 - 0
assets/icons/messages/check-read.svg

@@ -0,0 +1,4 @@
+<svg width="18" height="10" viewBox="0 0 18 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7185 0.702768C12.9889 0.97313 12.9889 1.41147 12.7185 1.68183L5.10325 9.29706C4.8329 9.56742 4.39456 9.56742 4.12421 9.29706L0.662729 5.83559C0.392371 5.56523 0.392371 5.1269 0.662729 4.85654C0.933088 4.58618 1.37142 4.58618 1.64178 4.85654L4.61373 7.82849L11.7394 0.702768C12.0098 0.432411 12.4482 0.432411 12.7185 0.702768Z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M17.7968 0.702768C18.0672 0.973131 18.0672 1.41147 17.7968 1.68183L10.1816 9.29706C9.91127 9.56742 9.47292 9.56742 9.20257 9.29706L7.66992 7.76847L8.64897 6.78942L9.6921 7.82849L16.8178 0.702768C17.0882 0.432411 17.5265 0.432411 17.7968 0.702768Z"/>
+</svg>

+ 3 - 0
assets/icons/messages/check-unread.svg

@@ -0,0 +1,3 @@
+<svg width="13" height="10" viewBox="0 0 13 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7969 0.702772C13.0672 0.973135 13.0672 1.41148 12.7969 1.68184L5.18147 9.29723C4.9111 9.56759 4.47276 9.56759 4.2024 9.29723L0.740858 5.83569C0.470495 5.56533 0.470495 5.12698 0.740858 4.85662C1.01122 4.58626 1.44957 4.58626 1.71993 4.85662L4.69193 7.82862L11.8178 0.702772C12.0881 0.432409 12.5265 0.432409 12.7969 0.702772Z"/>
+</svg>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 2 - 0
assets/icons/messages/comments.svg


+ 3 - 0
assets/icons/messages/dots.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="4" viewBox="0 0 16 4" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0.40625 2C0.40625 1.47786 0.613671 0.977096 0.982884 0.607884C1.3521 0.238671 1.85286 0.03125 2.375 0.03125C2.89715 0.03125 3.3979 0.238671 3.76712 0.607884C4.13633 0.977096 4.34375 1.47786 4.34375 2C4.34375 2.52215 4.13633 3.0229 3.76712 3.39212C3.3979 3.76133 2.89715 3.96875 2.375 3.96875C1.85286 3.96875 1.3521 3.76133 0.982884 3.39212C0.613671 3.0229 0.40625 2.52215 0.40625 2ZM6.03125 2C6.03125 1.47786 6.23867 0.977096 6.60788 0.607884C6.9771 0.238671 7.47786 0.03125 8 0.03125C8.52215 0.03125 9.0229 0.238671 9.39212 0.607884C9.76133 0.977096 9.96875 1.47786 9.96875 2C9.96875 2.52215 9.76133 3.0229 9.39212 3.39212C9.0229 3.76133 8.52215 3.96875 8 3.96875C7.47786 3.96875 6.9771 3.76133 6.60788 3.39212C6.23867 3.0229 6.03125 2.52215 6.03125 2ZM13.625 0.03125C14.1471 0.03125 14.6479 0.238671 15.0171 0.607884C15.3863 0.977096 15.5938 1.47786 15.5938 2C15.5938 2.52215 15.3863 3.0229 15.0171 3.39212C14.6479 3.76133 14.1471 3.96875 13.625 3.96875C13.1029 3.96875 12.6021 3.76133 12.2329 3.39212C11.8637 3.0229 11.6562 2.52215 11.6562 2C11.6562 1.47786 11.8637 0.977096 12.2329 0.607884C12.6021 0.238671 13.1029 0.03125 13.625 0.03125Z" fill="#0F3F4F"/>
+</svg>

+ 3 - 0
assets/icons/messages/pin.svg

@@ -0,0 +1,3 @@
+<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.76888 0.393091C10.2578 -0.0958074 11.0477 -0.0958073 11.5366 0.393091L18.6077 7.46416C19.0966 7.95306 19.0966 8.74303 18.6077 9.23193C18.1188 9.72082 17.3288 9.72082 16.8399 9.23193L16.0251 8.41709L12.2465 12.8255C12.7106 14.3888 12.5918 16.1096 11.8267 17.6371L11.7714 17.7476C11.5919 18.1094 11.2494 18.358 10.8544 18.4216C10.4594 18.4851 10.0534 18.3553 9.76888 18.0708L0.930044 9.23193C0.645544 8.94743 0.515723 8.54692 0.579253 8.14641C0.642781 7.7459 0.894136 7.40615 1.25321 7.22938L1.3637 7.17413C2.89116 6.40902 4.61197 6.29025 6.17534 6.75429L10.5837 2.97569L9.76888 2.16086C9.27998 1.67196 9.27998 0.881989 9.76888 0.393091ZM3.58169 13.6513L5.34946 15.4191L2.69781 18.0708C2.20891 18.5597 1.41894 18.5597 0.930044 18.0708C0.441146 17.5819 0.441146 16.7919 0.930044 16.303L3.58169 13.6513Z" fill="#0F3F4F"/>
+</svg>

+ 10 - 0
assets/icons/messages/send.svg

@@ -0,0 +1,10 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4211_36895)">
+<path d="M17.5117 0.197009C17.8668 0.443103 18.0531 0.868494 17.9863 1.29388L15.7363 15.9189C15.6836 16.2599 15.4761 16.5587 15.1738 16.7275C14.8715 16.8962 14.5094 16.9173 14.1894 16.7837L9.98474 15.0365L7.57654 17.6415C7.26365 17.9826 6.77146 18.0951 6.33904 17.9263C5.90662 17.7576 5.62537 17.3392 5.62537 16.8751V13.9361C5.62537 13.7954 5.6781 13.6619 5.77302 13.5564L11.6652 7.12982C11.8691 6.90834 11.8621 6.56732 11.6511 6.35638C11.4402 6.14545 11.0992 6.13138 10.8777 6.33178L3.72693 12.6845L0.622632 11.1306C0.249976 10.9443 0.010913 10.5716 0.000366172 10.1568C-0.0101807 9.74193 0.207788 9.35521 0.566382 9.14779L16.3164 0.147791C16.6926 -0.0666624 17.1566 -0.0455687 17.5117 0.197009Z"/>
+</g>
+<defs>
+<clipPath id="clip0_4211_36895">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 3 - 0
assets/icons/messages/unpin.svg

@@ -0,0 +1,3 @@
+<svg width="15" height="26" viewBox="0 0 15 26" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.33061 1.24179C5.282 0.830789 4.90635 0.539108 4.49534 0.587721C4.08433 0.636335 3.79265 1.01199 3.84127 1.42299L6.66969 24.7575C6.71831 25.1685 7.09396 25.4602 7.50496 25.4116C7.91597 25.363 8.20765 24.9873 8.15904 24.5763L7.4453 18.6874C7.67069 18.8952 7.98005 18.988 8.28278 18.9371C8.59877 18.8863 8.87498 18.6852 9.0164 18.398L9.0606 18.3096C9.67269 17.0876 9.7677 15.711 9.39647 14.4603L12.4194 10.9336L13.0712 11.5854C13.4623 11.9766 14.0943 11.9766 14.4854 11.5854C14.8766 11.1943 14.8766 10.5623 14.4854 10.1712L8.82858 4.51437C8.43746 4.12325 7.80548 4.12325 7.41436 4.51437C7.02325 4.90549 7.02325 5.53747 7.41436 5.92859L8.06623 6.58045L6.17472 8.20238L5.33061 1.24179ZM3.98711 15.2293L3.29326 9.3912C2.41379 9.34701 1.51444 9.52599 0.690221 9.93921L0.601833 9.9834C0.312361 10.127 0.113487 10.401 0.0626636 10.717C0.0118406 11.033 0.115697 11.3578 0.343297 11.5854L3.98711 15.2293ZM2.46462 15.121L0.343297 17.2423C-0.0478218 17.6334 -0.0478218 18.2654 0.343297 18.6565C0.734416 19.0476 1.36639 19.0476 1.75751 18.6565L3.87883 16.5352L2.46462 15.121Z" fill="#0F3F4F"/>
+</svg>

BIN=BIN
assets/notification-android-icon.png


+ 13 - 1
package.json

@@ -12,6 +12,8 @@
     "postinstall": "patch-package"
   },
   "dependencies": {
+    "@expo/config-plugins": "^8.0.8",
+    "@react-native-clipboard/clipboard": "^1.14.2",
     "@react-native-community/datetimepicker": "8.0.1",
     "@react-native-community/netinfo": "11.3.1",
     "@react-navigation/bottom-tabs": "^6.5.11",
@@ -29,6 +31,8 @@
     "dotenv": "^16.3.1",
     "expo": "^51.0.9",
     "expo-asset": "~10.0.10",
+    "expo-av": "^14.0.7",
+    "expo-blur": "^13.0.2",
     "expo-build-properties": "~0.12.5",
     "expo-checkbox": "~3.0.0",
     "expo-constants": "~16.0.2",
@@ -49,15 +53,21 @@
     "promise": "^8.3.0",
     "react": "18.2.0",
     "react-native": "0.74.5",
+    "react-native-actions-sheet": "^0.9.7",
     "react-native-animated-pagination-dot": "^0.4.0",
     "react-native-calendars": "^1.1304.1",
     "react-native-color-matrix-image-filters": "^7.0.1",
     "react-native-device-detection": "^0.2.1",
+    "react-native-document-picker": "^9.3.1",
+    "react-native-emoji-selector": "^0.2.0",
     "react-native-gesture-handler": "~2.16.1",
     "react-native-get-random-values": "^1.11.0",
+    "react-native-gifted-chat": "^2.6.3",
     "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",
     "react-native-map-clustering": "^3.4.2",
     "react-native-maps": "1.14.0",
     "react-native-mmkv": "^2.11.0",
@@ -67,15 +77,17 @@
     "react-native-progress": "^5.0.1",
     "react-native-reanimated": "~3.10.1",
     "react-native-reanimated-carousel": "^3.5.1",
+    "react-native-render-html": "^6.3.4",
     "react-native-safe-area-context": "4.10.5",
     "react-native-screens": "3.31.1",
     "react-native-searchable-dropdown-kj": "^1.9.1",
     "react-native-share": "^10.2.1",
     "react-native-svg": "15.2.0",
     "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"

+ 4 - 0
src/components/ErrorModal/index.tsx

@@ -13,11 +13,14 @@ import { CommonActions, useNavigation } from '@react-navigation/native';
 import { NAVIGATION_PAGES } from 'src/types';
 import { storage } from 'src/storage';
 import { useNotification } from 'src/contexts/NotificationContext';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 export const ErrorModal = () => {
   const { error, hideError, navigateToLogin } = useError();
   const navigation = useNavigation();
   const { updateNotificationStatus } = useNotification();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
+
 
   const handleClose = () => {
     if (navigateToLogin) {
@@ -27,6 +30,7 @@ export const ErrorModal = () => {
       storage.remove('visitedTilesUrl');
       storage.remove('filterSettings');
       updateNotificationStatus();
+      updateUnreadMessagesCount();
 
       navigation.dispatch(
         CommonActions.reset({

+ 2 - 1
src/components/FlatList/index.tsx

@@ -1,5 +1,6 @@
 import React, { FC, useCallback, useEffect, useState } from 'react';
-import { FlatList as List, SafeAreaView, View } from 'react-native';
+import { FlatList as List, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
 import { Input } from '../Input';
 import { styles } from './styles';
 import { Item, ItemData } from './item';

+ 13 - 4
src/components/HorizontalTabView/index.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { Text, TouchableOpacity, View } from 'react-native';
+import { StyleProp, Text, View, ViewStyle } from 'react-native';
 import { Route, TabBar, TabView } from 'react-native-tab-view';
 
 import { styles } from './styles';
@@ -7,6 +7,7 @@ import { Colors } from 'src/theme';
 
 import MarkToUpIcon from '../../../assets/icons/mark-to-up.svg';
 import BlinkingDot from '../BlinkingDot';
+import BanIcon from 'assets/icons/messages/ban.svg';
 
 export const HorizontalTabView = ({
   index,
@@ -17,7 +18,8 @@ export const HorizontalTabView = ({
   onDoubleClick,
   lazy = false,
   withNotification = false,
-  maxTabHeight
+  maxTabHeight,
+  tabBarStyle = {}
 }: {
   index: number;
   setIndex: React.Dispatch<React.SetStateAction<number>>;
@@ -28,6 +30,7 @@ export const HorizontalTabView = ({
   lazy?: boolean;
   withNotification?: boolean;
   maxTabHeight?: number;
+  tabBarStyle?: StyleProp<ViewStyle>;
 }) => {
   const renderTabBar = (props: any) => (
     <TabBar
@@ -35,7 +38,13 @@ export const HorizontalTabView = ({
       renderLabel={({ route, focused }) => (
         <>
           <View style={[styles.tabLabelContainer, focused ? styles.tabLabelFocused : null]}>
-            <Text style={[styles.label, focused ? styles.labelFocused : null]}>{route.title}</Text>
+            {route.icon ? (
+              <BanIcon width={15} height={15} fill={focused ? Colors.WHITE : Colors.DARK_BLUE} />
+            ) : (
+              <Text style={[styles.label, focused ? styles.labelFocused : null]}>
+                {route.title}
+              </Text>
+            )}
             {withMark ? (
               <MarkToUpIcon
                 height={16}
@@ -50,7 +59,7 @@ export const HorizontalTabView = ({
       )}
       scrollEnabled={true}
       indicatorStyle={styles.indicator}
-      style={styles.tabBar}
+      style={[styles.tabBar, tabBarStyle]}
       activeColor={Colors.ORANGE}
       inactiveColor={Colors.DARK_BLUE}
       tabStyle={[styles.tabStyle, maxTabHeight ? { maxHeight: maxTabHeight } : {}]}

+ 3 - 0
src/components/MenuDrawer/index.tsx

@@ -19,6 +19,7 @@ import BellIcon from 'assets/icons/notifications/bell-solid.svg';
 
 import { APP_VERSION, FASTEST_MAP_HOST } from 'src/constants';
 import { useNotification } from 'src/contexts/NotificationContext';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 export const MenuDrawer = (props: any) => {
   const { mutate: deleteUser } = useDeleteUserMutation();
@@ -31,6 +32,7 @@ export const MenuDrawer = (props: any) => {
     action: () => {}
   });
   const { updateNotificationStatus } = useNotification();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
 
   const openModal = (type: string, message: string, action: any) => {
     setModalInfo({
@@ -52,6 +54,7 @@ export const MenuDrawer = (props: any) => {
     storage.remove('visitedTilesUrl');
     storage.remove('filterSettings');
     updateNotificationStatus();
+    updateUnreadMessagesCount();
     navigation.dispatch(
       CommonActions.reset({
         index: 1,

+ 45 - 0
src/components/MessagesDot/index.tsx

@@ -0,0 +1,45 @@
+import React from 'react';
+import { DimensionValue, Text, View } from 'react-native';
+import { Colors } from 'src/theme';
+
+const MessagesDot = ({
+  messagesCount,
+  right = 0,
+  top = -2
+}: {
+  messagesCount: number;
+  right?: DimensionValue;
+  top?: DimensionValue;
+}) => {
+  return (
+    <View
+      style={[
+        {
+          position: 'absolute',
+          right,
+          top
+        }
+      ]}
+    >
+      <View
+        style={{
+          paddingHorizontal: 4,
+          paddingVertical: 2,
+          backgroundColor: Colors.ORANGE,
+          borderRadius: 10,
+          minWidth: 18,
+          justifyContent: 'center',
+          alignItems: 'center',
+          borderWidth: 1,
+          borderColor: Colors.WHITE
+        }}
+      >
+        <Text style={{ fontFamily: 'montserrat-700', fontSize: 10, color: Colors.WHITE }}>
+          {messagesCount > 99 ? '99+' : messagesCount}
+        </Text>
+      </View>
+    </View>
+  );
+};
+
+export default MessagesDot;

+ 2 - 1
src/components/PageWrapper/index.tsx

@@ -1,5 +1,6 @@
 import React, { FC, ReactNode } from 'react';
-import { SafeAreaView, StyleProp, ViewStyle } from 'react-native';
+import { StyleProp, ViewStyle } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
 
 import { styles } from './styles';
 

+ 1 - 2
src/components/PageWrapper/styles.ts

@@ -1,10 +1,9 @@
-import { Platform, StatusBar, StyleSheet } from 'react-native';
+import { StyleSheet } from 'react-native';
 
 export const styles = StyleSheet.create({
   wrapper: {
     marginLeft: '5%',
     marginRight: '5%',
     height: '100%',
-    paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0
   }
 });

+ 10 - 7
src/components/SplashSpinner/index.tsx

@@ -1,14 +1,17 @@
 import React from 'react';
-import { ActivityIndicator, ImageBackground } from 'react-native';
+import { ActivityIndicator, ImageBackground, StatusBar, View } from 'react-native';
 import { Colors } from 'src/theme';
 
 export const Splash = () => {
   return (
-    <ImageBackground
-      source={require('../../../assets/loading-screen.png')}
-      style={{ display: 'flex', height: '100%', justifyContent: 'center', alignItems: 'center' }}
-    >
-      <ActivityIndicator size={'large'} color={Colors.WHITE} style={{ paddingTop: '70%' }} />
-    </ImageBackground>
+    <View style={{ flex: 1 }}>
+      <StatusBar translucent backgroundColor="transparent" />
+      <ImageBackground
+        source={require('../../../assets/loading-screen.png')}
+        style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
+      >
+        <ActivityIndicator size={'large'} color={Colors.WHITE} style={{ paddingTop: '70%' }} />
+      </ImageBackground>
+    </View>
   );
 };

+ 28 - 3
src/components/TabBarButton/index.tsx

@@ -7,11 +7,17 @@ import MapIcon from '../../../assets/icons/bottom-navigation/map.svg';
 import TravellersIcon from '../../../assets/icons/bottom-navigation/travellers.svg';
 import GlobeIcon from '../../../assets/icons/bottom-navigation/globe-solid.svg';
 import MenuIcon from '../../../assets/icons/menu.svg';
+import MessagesIcon from '../../../assets/icons/bottom-navigation/messages.svg';
 
 import { Colors } from '../../theme';
 import { styles } from './style';
 import BlinkingDot from '../BlinkingDot';
 import { useNotification } from 'src/contexts/NotificationContext';
+import MessagesDot from '../MessagesDot';
+import { storage, StoreType } from 'src/storage';
+import { WarningModal } from '../WarningModal';
+import { useState } from 'react';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 const getTabIcon = (routeName: string) => {
   switch (routeName) {
@@ -21,6 +27,8 @@ const getTabIcon = (routeName: string) => {
       return TravellersIcon;
     case NAVIGATION_PAGES.IN_APP_TRAVELS_TAB:
       return GlobeIcon;
+    case NAVIGATION_PAGES.IN_APP_MESSAGES_TAB:
+      return MessagesIcon;
     case NAVIGATION_PAGES.MENU_DRAWER:
       return MenuIcon;
     default:
@@ -40,15 +48,24 @@ const TabBarButton = ({
   navigation: any;
 }) => {
   const IconComponent: React.FC<SvgProps> | null = getTabIcon(label);
+  const token = storage.get('token', StoreType.STRING);
   const { isNotificationActive } = useNotification();
+  const unreadMessagesCount = useMessagesStore((state) => state.unreadMessagesCount);
+  const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
 
   let currentColor = focused ? Colors.DARK_BLUE : Colors.LIGHT_GRAY;
 
   return (
     <TouchableWithoutFeedback
-      onPress={() =>
-        label === NAVIGATION_PAGES.MENU_DRAWER ? (navigation as any)?.openDrawer() : onPress()
-      }
+      onPress={() => {
+        if (label === NAVIGATION_PAGES.MENU_DRAWER) {
+          (navigation as any)?.openDrawer();
+        } else if (label === NAVIGATION_PAGES.IN_APP_MESSAGES_TAB && !token) {
+          setIsWarningModalVisible(true);
+        } else {
+          onPress();
+        }
+      }}
     >
       <View style={styles.buttonStyle}>
         <View style={{ alignItems: 'center' }}>
@@ -56,8 +73,16 @@ const TabBarButton = ({
           {label === NAVIGATION_PAGES.IN_APP_TRAVELLERS_TAB && isNotificationActive && (
             <BlinkingDot diameter={8} />
           )}
+          {label === NAVIGATION_PAGES.IN_APP_MESSAGES_TAB && unreadMessagesCount > 0 && (
+            <MessagesDot messagesCount={unreadMessagesCount} />
+          )}
           <Text style={[styles.labelStyle, { color: currentColor }]}>{label}</Text>
         </View>
+        <WarningModal
+          type={'unauthorized'}
+          isVisible={isWarningModalVisible}
+          onClose={() => setIsWarningModalVisible(false)}
+        />
       </View>
     </TouchableWithoutFeedback>
   );

+ 4 - 2
src/components/WarningModal/index.tsx

@@ -19,7 +19,8 @@ export const WarningModal = ({
   message,
   title,
   action,
-  onModalHide
+  onModalHide,
+  buttonTitle
 }: {
   isVisible: boolean;
   onClose: () => void;
@@ -28,6 +29,7 @@ export const WarningModal = ({
   title?: string;
   action?: () => void;
   onModalHide?: () => void;
+  buttonTitle?: string;
 }) => {
   const navigation = useNavigation();
 
@@ -84,7 +86,7 @@ export const WarningModal = ({
           borderColor: Colors.DARK_BLUE
         },
         {
-          text: 'Delete',
+          text: buttonTitle ?? 'Delete',
           textColor: Colors.WHITE,
           color: Colors.RED,
           action: () => {

+ 1 - 0
src/components/index.ts

@@ -19,3 +19,4 @@ export * from './EditNmModal';
 export * from './MenuDrawer';
 export * from './ErrorModal';
 export * from './BlinkingDot';
+export * from './MessagesDot';

+ 2 - 0
src/constants/secrets.ts

@@ -17,3 +17,5 @@ export const APP_VERSION = Constants?.expoConfig?.version ?? Constants?.manifest
 
 export const GOOGLE_MAP_PLACES_APIKEY =
   extra?.GOOGLE_MAP_PLACES_APIKEY || Constants?.expoConfig?.extra?.GOOGLE_MAP_PLACES_APIKEY;
+
+export const WEBSOCKET_URL = extra?.WEBSOCKET_URL || Constants?.expoConfig?.extra?.WEBSOCKET_URL;

+ 1 - 1
src/contexts/NotificationContext.tsx

@@ -19,7 +19,7 @@ export const NotificationProvider = ({ children }: { children: React.ReactNode }
       try {
         const data = await fetchFriendsNotification(token as string);
         const isActive = data && data.active;
-        
+
         if (typeof isActive === 'boolean') {
           setIsNotificationActive(isActive);
           storage.set('friendsNotification', isActive);

+ 31 - 6
src/contexts/PushNotificationContext.tsx

@@ -1,10 +1,11 @@
 import React, { useEffect, useState, useContext, createContext } from 'react';
 import * as Notifications from 'expo-notifications';
 import { storage, StoreType } from 'src/storage';
-import { Linking, Platform } from 'react-native';
+import { AppState, AppStateStatus, Linking, Platform } from 'react-native';
 import { CommonActions, useNavigation } from '@react-navigation/native';
 import { NAVIGATION_PAGES } from 'src/types';
 import { usePostSetSettingsMutation } from '@api/notifications';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 const PushNotificationContext = createContext<{
   isSubscribed: boolean;
@@ -25,15 +26,33 @@ export const PushNotificationProvider = ({ children }: { children: React.ReactNo
   );
   const { mutateAsync: setNotificationsSettings } = usePostSetSettingsMutation();
   const navigation = useNavigation();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
+  const [appState, setAppState] = useState(AppState.currentState);
 
   const lastNotificationResponse = Notifications.useLastNotificationResponse();
 
+  useEffect(() => {
+    const handleAppStateChange = (nextAppState: AppStateStatus) => {
+      if (appState.match(/inactive|background/) && nextAppState === 'active' && isSubscribed) {
+        Notifications.getBadgeCountAsync().then((badgeCount) => {
+          if (badgeCount > 0) {
+            Notifications.setBadgeCountAsync(-1);
+          }
+        });
+        updateUnreadMessagesCount();
+      }
+      setAppState(nextAppState);
+    };
+
+    const subscription = AppState.addEventListener('change', handleAppStateChange);
+
+    return () => {
+      subscription.remove();
+    };
+  }, [appState]);
+
   useEffect(() => {
     if (lastNotificationResponse && Platform.OS === 'android') {
-      console.log(
-        'lastNotificationResponse',
-        lastNotificationResponse.notification.request.content.data
-      );
       const data = lastNotificationResponse.notification.request.content.data;
 
       if (data?.screen && data?.parentScreen) {
@@ -111,7 +130,7 @@ export const PushNotificationProvider = ({ children }: { children: React.ReactNo
       });
 
       const notificationListener = Notifications.addNotificationReceivedListener((notification) => {
-        console.log('Notification received');
+        updateUnreadMessagesCount();
       });
 
       const responseListener = Notifications.addNotificationResponseReceivedListener((response) => {
@@ -197,6 +216,12 @@ export const PushNotificationProvider = ({ children }: { children: React.ReactNo
         }
       });
 
+      Notifications.getBadgeCountAsync().then((badgeCount) => {
+        if (badgeCount > 0) {
+          Notifications.setBadgeCountAsync(-1);
+        }
+      });
+
       return () => {
         notificationListener.remove();
         responseListener.remove();

+ 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);
   },

+ 157 - 0
src/modules/api/chat/chat-api.ts

@@ -0,0 +1,157 @@
+import { request } from '../../../utils';
+import { API } from '../../../types';
+import { ResponseType } from '../response-type';
+
+export interface PostSearchUsersReturn extends ResponseType {
+  data: {
+    user_id: number;
+    first_name: string;
+    last_name: string;
+    avatar: string | null;
+  }[];
+}
+
+// status: 1 -sent; 2 -received; 3 -read; 4 - deleted
+export interface PostGetChatsListReturn extends ResponseType {
+  conversations: {
+    uid: number;
+    name: string;
+    avatar: string | null;
+    short: string;
+    sent_by: number;
+    updated: Date;
+    status: 1 | 2 | 3 | 4;
+    unread_count: number;
+    last_message_id: number;
+    pin: 0 | 1;
+    pin_order: number;
+    archive: 0 | 1;
+    archive_order: number;
+    attachement_name: string;
+    encrypted: 0 | 1;
+    muted: 0 | 1;
+  }[];
+}
+
+interface Message {
+  id: number;
+  sender: number;
+  recipient: number;
+  text: string;
+  status: 1 | 2 | 3 | 4;
+  sent_datetime: Date;
+  received_datetime: Date | null;
+  read_datetime: Date | null;
+  reply_to_id: number;
+  reactions: string;
+  edits: string;
+  attachement: any;
+  encrypted: 0 | 1;
+}
+
+export interface PostGetChatWithReturn extends ResponseType {
+  messages: (Message & {
+    reply_to: Message;
+  })[];
+}
+
+export interface PostSendMessage {
+  token: string;
+  to_uid: number;
+  text: string;
+  reply_to_id?: number;
+}
+
+export interface PostSendMessageReturn extends ResponseType {
+  message_id: number;
+}
+
+export interface PostMessagesReceivedOrRead {
+  token: string;
+  from_user: number;
+  messages_id: number[];
+}
+
+export interface PostReactToMessage {
+  token: string;
+  message_id: number;
+  reaction: string;
+  conversation_with_user: number;
+}
+
+export interface PostUnreactToMessage {
+  token: string;
+  message_id: number;
+  conversation_with_user: number;
+}
+
+export interface PostDeleteMessage {
+  token: string;
+  message_id: number;
+  conversation_with_user: number;
+}
+
+export interface PostDeleteChat {
+  token: string;
+  conversation_with_user: number;
+}
+
+export interface PostSetSettings {
+  token: string;
+  value: 0 | 1;
+  conversation_with_user: number;
+}
+
+export interface PostGetBlockedReturn extends ResponseType {
+  blocked: {
+    id: number;
+    first_name: string;
+    last_name: string;
+    avatar: string | null;
+  }[];
+}
+
+export interface PostGetUnreadCountReturn extends ResponseType {
+  unread_conversations: number;
+}
+
+export const chatApi = {
+  searchUsers: (token: string, search: string) =>
+    request.postForm<PostSearchUsersReturn>(API.SEARCH_USERS, { token, search }),
+  getChatsList: (token: string, archive: 0 | 1) =>
+    request.postForm<PostGetChatsListReturn>(API.GET_CHATS_LIST, { token, archive }),
+  getChatWith: (
+    token: string,
+    uid: number,
+    no_of_messages: number,
+    previous_than_message_id: number
+  ) =>
+    request.postForm<PostGetChatWithReturn>(API.GET_CHAT_WITH, {
+      token,
+      uid,
+      no_of_messages,
+      previous_than_message_id
+    }),
+  sendMessage: (data: PostSendMessage) =>
+    request.postForm<PostSendMessageReturn>(API.SEND_MESSAGE, data),
+  messagesReceived: (data: PostMessagesReceivedOrRead) =>
+    request.postForm<ResponseType>(API.MESSAGES_RECEIVED, data),
+  messagesRead: (data: PostMessagesReceivedOrRead) =>
+    request.postForm<ResponseType>(API.MESSAGES_READ, data),
+  reactToMessage: (data: PostReactToMessage) =>
+    request.postForm<ResponseType>(API.REACT_TO_MESSAGE, data),
+  deleteMessage: (data: PostDeleteMessage) =>
+    request.postForm<ResponseType>(API.DELETE_MESSAGE, data),
+  setPin: (data: PostSetSettings) => request.postForm<ResponseType>(API.SET_PIN, data),
+  setArchive: (data: PostSetSettings) => request.postForm<ResponseType>(API.SET_ARCHIVE, data),
+  setBlock: (data: PostSetSettings) => request.postForm<ResponseType>(API.SET_BLOCK, data),
+  setMute: (data: PostSetSettings) => request.postForm<ResponseType>(API.SET_MUTE, data),
+  deleteChat: (data: PostDeleteChat) => request.postForm<ResponseType>(API.DELETE_CHAT, data),
+  unreactToMessage: (data: PostUnreactToMessage) =>
+    request.postForm<ResponseType>(API.UNREACT_TO_MESSAGE, data),
+  getBlocked: (token: string) => request.postForm<PostGetBlockedReturn>(API.GET_BLOCKED, { token }),
+  getUnreadMessagesCount: (token: string) =>
+    request.postForm<PostGetUnreadCountReturn>(API.GET_UNREAD_MESSAGES_PRESENT, {
+      token
+    })
+};

+ 23 - 0
src/modules/api/chat/chat-query-keys.tsx

@@ -0,0 +1,23 @@
+export const chatQueryKeys = {
+  searchUsers: (token: string, search: string) => ['searchUsers', token, search] as const,
+  getChatsList: (token: string, archive: 0 | 1) => ['getChatsList', token, archive] as const,
+  getChatWith: (
+    token: string,
+    uid: number,
+    no_of_messages: number,
+    previous_than_message_id: number
+  ) => ['getChatWith', token, uid, no_of_messages, previous_than_message_id] as const,
+  sendMessage: () => ['sendMessage'] as const,
+  messagesReceived: () => ['messagesReceived'] as const,
+  messagesRead: () => ['messagesRead'] as const,
+  reactToMessage: () => ['reactToMessage'] as const,
+  deleteMessage: () => ['deleteMessage'] as const,
+  setPin: () => ['setPin'] as const,
+  setArchive: () => ['setArchive'] as const,
+  setBlock: () => ['setBlock'] as const,
+  setMute: () => ['setMute'] as const,
+  deleteChat: () => ['deleteChat'] as const,
+  unreactToMessage: () => ['unreactToMessage'] as const,
+  getBlocked: (token: string) => ['getBlocked', token] as const,
+  getUnreadMessagesCount: (token: string) => ['getUnreadMessagesCount', token] as const
+};

+ 3 - 0
src/modules/api/chat/index.ts

@@ -0,0 +1,3 @@
+export * from './queries';
+export * from './chat-api';
+export * from './chat-query-keys';

+ 16 - 0
src/modules/api/chat/queries/index.ts

@@ -0,0 +1,16 @@
+export * from './use-post-search-users';
+export * from './use-post-get-conversation-list';
+export * from './use-post-get-conversation-with';
+export * from './use-post-send-message';
+export * from './use-post-messages-received';
+export * from './use-post-messages-read';
+export * from './use-post-react-to-message';
+export * from './use-post-delete-message';
+export * from './use-post-set-pin';
+export * from './use-post-set-archive';
+export * from './use-post-set-mute';
+export * from './use-post-set-block';
+export * from './use-post-delete-conversation';
+export * from './use-post-unreact-to-message';
+export * from './use-post-get-blocked';
+export * from './use-post-get-new-messages-present';

+ 17 - 0
src/modules/api/chat/queries/use-post-delete-conversation.tsx

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

+ 17 - 0
src/modules/api/chat/queries/use-post-delete-message.tsx

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

+ 17 - 0
src/modules/api/chat/queries/use-post-get-blocked.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { chatQueryKeys } from '../chat-query-keys';
+import { chatApi, type PostGetBlockedReturn } from '../chat-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetBlockedQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetBlockedReturn, BaseAxiosError>({
+    queryKey: chatQueryKeys.getBlocked(token),
+    queryFn: async () => {
+      const response = await chatApi.getBlocked(token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/chat/queries/use-post-get-conversation-list.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { chatQueryKeys } from '../chat-query-keys';
+import { chatApi, type PostGetChatsListReturn } from '../chat-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetChatsListQuery = (token: string, archive: 0 | 1, enabled: boolean) => {
+  return useQuery<PostGetChatsListReturn, BaseAxiosError>({
+    queryKey: chatQueryKeys.getChatsList(token, archive),
+    queryFn: async () => {
+      const response = await chatApi.getChatsList(token, archive);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 28 - 0
src/modules/api/chat/queries/use-post-get-conversation-with.tsx

@@ -0,0 +1,28 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { chatQueryKeys } from '../chat-query-keys';
+import { chatApi, type PostGetChatWithReturn } from '../chat-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetChatWithQuery = (
+  token: string,
+  uid: number,
+  no_of_messages: number,
+  previous_than_message_id: number,
+  enabled: boolean
+) => {
+  return useQuery<PostGetChatWithReturn, BaseAxiosError>({
+    queryKey: chatQueryKeys.getChatWith(token, uid, no_of_messages, previous_than_message_id),
+    queryFn: async () => {
+      const response = await chatApi.getChatWith(
+        token,
+        uid,
+        no_of_messages,
+        previous_than_message_id
+      );
+      return response.data;
+    },
+    enabled
+  });
+};

+ 21 - 0
src/modules/api/chat/queries/use-post-get-new-messages-present.tsx

@@ -0,0 +1,21 @@
+import { chatQueryKeys } from '../chat-query-keys';
+import { type PostGetUnreadCountReturn, chatApi } from '../chat-api';
+import { queryClient } from 'src/utils/queryClient';
+
+export const fetchUnreadMessagesCount = async (token: string) => {
+  try {
+    const data: PostGetUnreadCountReturn = await queryClient.fetchQuery({
+      queryKey: chatQueryKeys.getUnreadMessagesCount(token),
+      queryFn: async () => {
+        const response = await chatApi.getUnreadMessagesCount(token);
+        return response.data;
+      },
+      gcTime: 0,
+      staleTime: 0
+    });
+
+    return data;
+  } catch (error) {
+    console.error('Failed to fetch unread messages count:', error);
+  }
+};

+ 17 - 0
src/modules/api/chat/queries/use-post-messages-read.tsx

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

+ 17 - 0
src/modules/api/chat/queries/use-post-messages-received.tsx

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

+ 17 - 0
src/modules/api/chat/queries/use-post-react-to-message.tsx

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

+ 17 - 0
src/modules/api/chat/queries/use-post-search-users.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { chatQueryKeys } from '../chat-query-keys';
+import { chatApi, type PostSearchUsersReturn } from '../chat-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostSearchUsers = (token: string, search: string, enabled: boolean) => {
+  return useQuery<PostSearchUsersReturn, BaseAxiosError>({
+    queryKey: chatQueryKeys.searchUsers(token, search),
+    queryFn: async () => {
+      const response = await chatApi.searchUsers(token, search);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 18 - 0
src/modules/api/chat/queries/use-post-send-message.tsx

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

+ 17 - 0
src/modules/api/chat/queries/use-post-set-archive.tsx

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

+ 17 - 0
src/modules/api/chat/queries/use-post-set-block.tsx

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

+ 17 - 0
src/modules/api/chat/queries/use-post-set-mute.tsx

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

+ 17 - 0
src/modules/api/chat/queries/use-post-set-pin.tsx

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

+ 17 - 0
src/modules/api/chat/queries/use-post-unreact-to-message.tsx

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

+ 1 - 1
src/modules/api/notifications/notifications-api.ts

@@ -4,7 +4,7 @@ import { ResponseType } from '../response-type';
 
 export interface PostGetSettings {
   settings: {
-    name: 'app-ios' | 'app-android' | 'app-friends' | 'email-friends';
+    name: 'app-ios' | 'app-android' | 'app-friends' | 'email-friends' | 'app-messages';
     active: 0 | 1;
   }[];
 }

+ 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,

+ 1355 - 0
src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx

@@ -0,0 +1,1355 @@
+import React, { useState, useCallback, useEffect, useRef } from 'react';
+import {
+  View,
+  TouchableOpacity,
+  Image,
+  Modal,
+  Text,
+  FlatList,
+  Dimensions,
+  Alert,
+  ScrollView,
+  Linking,
+  ActivityIndicator,
+  AppState,
+  AppStateStatus
+} from 'react-native';
+import {
+  GiftedChat,
+  Bubble,
+  InputToolbar,
+  IMessage,
+  Send,
+  BubbleProps,
+  Composer,
+  TimeProps,
+  MessageProps
+} from 'react-native-gifted-chat';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler';
+import { AvatarWithInitials, Header, WarningModal } from 'src/components';
+import { Colors } from 'src/theme';
+import { useFocusEffect, useNavigation } from '@react-navigation/native';
+import { Video } from 'expo-av';
+import ChatMessageBox from '../Components/ChatMessageBox';
+import ReplyMessageBar from '../Components/ReplyMessageBar';
+import { useSharedValue, withTiming } from 'react-native-reanimated';
+import { BlurView } from 'expo-blur';
+import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
+import Clipboard from '@react-native-clipboard/clipboard';
+import { trigger } from 'react-native-haptic-feedback';
+import ReactModal from 'react-native-modal';
+import { storage, StoreType } from 'src/storage';
+import {
+  usePostDeleteMessageMutation,
+  usePostGetChatWithQuery,
+  usePostMessagesReadMutation,
+  usePostReactToMessageMutation,
+  usePostSendMessageMutation
+} from '@api/chat';
+import { CustomMessage, Message, Reaction } from '../types';
+import { API_HOST, WEBSOCKET_URL } from 'src/constants';
+import ReactionBar from '../Components/ReactionBar';
+import OptionsMenu from '../Components/OptionsMenu';
+import EmojiSelectorModal from '../Components/EmojiSelectorModal';
+import { styles } from './styles';
+import SendIcon from 'assets/icons/messages/send.svg';
+import { SheetManager } from 'react-native-actions-sheet';
+import { NAVIGATION_PAGES } from 'src/types';
+import { usePushNotification } from 'src/contexts/PushNotificationContext';
+import ReactionsListModal from '../Components/ReactionsListModal';
+import { dismissChatNotifications } from '../utils';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
+
+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 }: { id: number; name: string; avatar: string | null } = route.params;
+
+  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: () => {}
+  });
+
+  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 appState = useRef(AppState.currentState);
+
+  const socket = useRef<WebSocket | null>(null);
+
+  const closeModal = () => {
+    setModalInfo({ ...modalInfo, visible: false });
+  };
+
+  useEffect(() => {
+    let unsubscribe: any;
+
+    const setupNotificationHandler = async () => {
+      unsubscribe = await dismissChatNotifications(id, isSubscribed, setModalInfo, navigation);
+    };
+
+    setupNotificationHandler();
+
+    return () => {
+      if (unsubscribe) unsubscribe();
+      updateUnreadMessagesCount();
+    };
+  }, [id]);
+
+  useEffect(() => {
+    socket.current = new WebSocket(WEBSOCKET_URL);
+
+    socket.current.onopen = () => {
+      socket.current?.send(JSON.stringify({ token }));
+    };
+
+    socket.current.onmessage = (event) => {
+      const data = JSON.parse(event.data);
+      handleWebSocketMessage(data);
+    };
+
+    socket.current.onclose = () => {
+      console.log('WebSocket connection closed chat screen');
+    };
+
+    return () => {
+      if (socket.current) {
+        socket.current.close();
+        socket.current = null;
+      }
+    };
+  }, [token]);
+
+  useEffect(() => {
+    const handleAppStateChange = async (nextAppState: AppStateStatus) => {
+      if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
+        if (!socket.current || socket.current.readyState === WebSocket.CLOSED) {
+          socket.current = new WebSocket(WEBSOCKET_URL);
+          socket.current.onopen = () => {
+            socket.current?.send(JSON.stringify({ token }));
+          };
+          socket.current.onmessage = (event) => {
+            const data = JSON.parse(event.data);
+            handleWebSocketMessage(data);
+          };
+        }
+
+        await dismissChatNotifications(id, isSubscribed, setModalInfo, navigation);
+      }
+    };
+
+    const subscription = AppState.addEventListener('change', handleAppStateChange);
+
+    return () => {
+      subscription.remove();
+      if (socket.current) {
+        socket.current.close();
+        socket.current = null;
+      }
+    };
+  }, [token]);
+
+  const handleWebSocketMessage = (data: any) => {
+    switch (data.action) {
+      case 'new_message':
+        if (data.conversation_with === id && data.message) {
+          const newMessage = mapApiMessageToGiftedMessage(data.message);
+          setMessages((previousMessages) => {
+            const messageExists =
+              previousMessages && previousMessages.some((msg) => msg._id === newMessage._id);
+            if (!messageExists) {
+              return GiftedChat.append(previousMessages ?? [], [newMessage]);
+            }
+            return previousMessages;
+          });
+        }
+        break;
+
+      case 'new_reaction':
+        if (data.conversation_with === id && data.reaction) {
+          updateMessageWithReaction(data.reaction);
+        }
+        break;
+
+      case 'unreact':
+        if (data.conversation_with === id && data.unreacted_message_id) {
+          removeReactionFromMessage(data.unreacted_message_id);
+        }
+        break;
+
+      case 'delete_message':
+        if (data.conversation_with === id && data.deleted_message_id) {
+          removeDeletedMessage(data.deleted_message_id);
+        }
+        break;
+
+      case 'is_typing':
+        if (data.conversation_with === id) {
+          setIsTyping(true);
+        }
+        break;
+
+      case 'stopped_typing':
+        if (data.conversation_with === id) {
+          setIsTyping(false);
+        }
+        break;
+
+      case 'messages_read':
+        if (data.conversation_with === id && data.read_messages_ids) {
+          setMessages(
+            (prevMessages) =>
+              prevMessages?.map((msg) => {
+                if (data.read_messages_ids.includes(msg._id)) {
+                  return { ...msg, received: true };
+                }
+                return msg;
+              }) ?? []
+          );
+        }
+        break;
+
+      default:
+        break;
+    }
+  };
+
+  const updateMessageWithReaction = (reactionData: any) => {
+    setMessages(
+      (prevMessages) =>
+        prevMessages?.map((msg) => {
+          if (msg._id === reactionData.message_id) {
+            const updatedReactions = [
+              ...(Array.isArray(msg.reactions)
+                ? msg.reactions?.filter((r: any) => r.uid !== reactionData.uid)
+                : []),
+              reactionData
+            ];
+            return { ...msg, reactions: updatedReactions };
+          }
+          return msg;
+        }) ?? []
+    );
+  };
+
+  const removeReactionFromMessage = (messageId: number) => {
+    setMessages(
+      (prevMessages) =>
+        prevMessages?.map((msg) => {
+          if (msg._id === messageId) {
+            const updatedReactions = Array.isArray(msg.reactions)
+              ? msg.reactions?.filter((r: any) => r.uid !== id)
+              : [];
+            return { ...msg, reactions: updatedReactions };
+          }
+          return msg;
+        }) ?? []
+    );
+  };
+
+  const removeDeletedMessage = (messageId: number) => {
+    setMessages(
+      (prevMessages) =>
+        prevMessages?.map((msg) => {
+          if (msg._id === messageId) {
+            return {
+              ...msg,
+              deleted: true,
+              text: 'This message was deleted',
+              pending: false,
+              sent: false,
+              received: false
+            };
+          }
+          return msg;
+        }) ?? []
+    );
+  };
+
+  useEffect(() => {
+    const pingInterval = setInterval(() => {
+      if (socket.current && socket.current.readyState === WebSocket.OPEN) {
+        socket.current.send(JSON.stringify({ action: 'ping', conversation_with: id }));
+      } else {
+        socket.current = new WebSocket(WEBSOCKET_URL);
+        socket.current.onopen = () => {
+          socket.current?.send(JSON.stringify({ token }));
+        };
+        socket.current.onmessage = (event) => {
+          const data = JSON.parse(event.data);
+          handleWebSocketMessage(data);
+        };
+
+        return () => {
+          if (socket.current) {
+            socket.current.close();
+            socket.current = null;
+          }
+        };
+      }
+    }, 50000);
+
+    return () => clearInterval(pingInterval);
+  }, []);
+
+  const sendWebSocketMessage = (
+    action: string,
+    message: CustomMessage | null = null,
+    reaction: string | null = null,
+    readMessagesIds: number[] | null = null
+  ) => {
+    if (socket.current && socket.current.readyState === WebSocket.OPEN) {
+      const data: any = {
+        action,
+        conversation_with: id
+      };
+
+      if (action === 'new_message' && message) {
+        data.message = {
+          id: message._id,
+          text: message.text,
+          sender: +currentUserId,
+          sent_datetime: new Date().toISOString().replace('T', ' ').substring(0, 19),
+          reply_to_id: message.replyMessage?.id ?? -1,
+          reply_to: message.replyMessage ?? null,
+          reactions: message.reactions ?? '{}',
+          status: 2,
+          attachement: -1
+        };
+      }
+
+      if (action === 'new_reaction' && message && reaction) {
+        data.reaction = {
+          message_id: message._id,
+          reaction,
+          uid: +currentUserId,
+          datetime: new Date().toISOString()
+        };
+      }
+
+      if (action === 'unreact' && message) {
+        data.message_id = message._id;
+      }
+
+      if (action === 'delete_message' && message) {
+        data.message_id = message._id;
+      }
+
+      if (action === 'messages_read' && readMessagesIds) {
+        data.messages_ids = readMessagesIds;
+      }
+
+      socket.current.send(JSON.stringify(data));
+    }
+  };
+
+  const handleTyping = (isTyping: boolean) => {
+    if (isTyping) {
+      sendWebSocketMessage('is_typing');
+    } else {
+      sendWebSocketMessage('stopped_typing');
+    }
+  };
+
+  const mapApiMessageToGiftedMessage = (message: Message): CustomMessage => {
+    return {
+      _id: message.id,
+      text: message.text,
+      createdAt: new Date(message.sent_datetime + 'Z'),
+      user: {
+        _id: message.sender,
+        name: message.sender === id ? name : 'Me'
+      },
+      replyMessage:
+        message.reply_to_id !== -1
+          ? {
+              text: message.reply_to.text,
+              id: message.reply_to.id,
+              name: message.reply_to.sender === id ? name : '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);
+        }
+
+        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[] }) => {
+    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 ? name : '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 ? name : '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 renderInputToolbar = (props: any) => (
+    <InputToolbar
+      {...props}
+      renderActions={() =>
+        // <Actions
+        //   icon={() => <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />}
+        //   // onPressActionButton={openActionSheet}
+        // />
+        null
+      }
+      containerStyle={{
+        backgroundColor: Colors.FILL_LIGHT
+      }}
+    />
+  );
+
+  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%'
+      }}
+    >
+      <View style={{ paddingHorizontal: '5%' }}>
+        <Header
+          label={name}
+          rightElement={
+            <TouchableOpacity
+              onPress={() =>
+                navigation.navigate(
+                  ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: id }] as never)
+                )
+              }
+            >
+              {avatar ? (
+                <Image source={{ uri: API_HOST + avatar }} style={styles.avatar} />
+              ) : (
+                <AvatarWithInitials
+                  text={
+                    name
+                      .split(/ (.+)/)
+                      .map((n) => n[0])
+                      .join('') ?? ''
+                  }
+                  flag={API_HOST + 'flag.png'}
+                  size={30}
+                  fontSize={12}
+                />
+              )}
+            </TouchableOpacity>
+          }
+        />
+      </View>
+
+      <GestureHandlerRootView style={styles.container}>
+        {messages ? (
+          <GiftedChat
+            messages={messages as CustomMessage[]}
+            listViewProps={{
+              ref: flatList,
+              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)}
+            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>
+            )}
+            textInputProps={{ ...styles.composer, selectionColor: Colors.LIGHT_GRAY }}
+            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}
+          action={() => {
+            modalInfo.action();
+            closeModal();
+          }}
+        />
+        <ReactionsListModal />
+      </GestureHandlerRootView>
+      <View
+        style={{
+          height: insets.bottom,
+          backgroundColor: Colors.FILL_LIGHT
+        }}
+      />
+    </SafeAreaView>
+  );
+};
+
+export default ChatScreen;

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

@@ -0,0 +1,155 @@
+import { Platform } from 'react-native';
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({
+  replyContent: {},
+  replyAuthorName: {
+    fontWeight: '600',
+    fontSize: getFontSize(13)
+  },
+  replyMessageText: {
+    fontWeight: '400',
+    fontSize: getFontSize(13)
+  },
+  unreadMessagesContainer: {
+    padding: 8,
+    backgroundColor: Colors.FILL_LIGHT,
+    alignItems: 'center'
+  },
+  unreadMessagesText: {
+    color: Colors.DARK_BLUE,
+    fontWeight: '700',
+    fontSize: getFontSize(12)
+  },
+  modalBackground: {
+    flex: 1
+  },
+  mediaContainer: {
+    borderRadius: 10,
+    overflow: 'hidden',
+    margin: 5
+  },
+  chatMedia: {
+    width: 200,
+    height: 200,
+    borderRadius: 10
+  },
+  fullScreenMedia: {
+    width: '90%',
+    height: '80%'
+  },
+  replyMessageContainer: {
+    borderLeftWidth: 2,
+    borderRadius: 2,
+    margin: 8,
+    marginBottom: 0,
+    padding: 4,
+    paddingLeft: 6,
+    gap: 2
+  },
+  composer: {
+    backgroundColor: Colors.WHITE,
+    borderRadius: 12,
+    borderWidth: 0.5,
+    borderColor: Colors.LIGHT_GRAY,
+    paddingHorizontal: 10,
+    fontSize: 16,
+    marginBottom: 5
+  },
+  container: {
+    flex: 1,
+    backgroundColor: 'white'
+  },
+  imageContainer: {
+    borderRadius: 10,
+    overflow: 'hidden',
+    margin: 5
+  },
+  chatImage: {
+    width: 200,
+    height: 200,
+    borderRadius: 10
+  },
+  modalContainer: {
+    flex: 1,
+    backgroundColor: 'rgba(0, 0, 0, 0.9)',
+    justifyContent: 'center',
+    alignItems: 'center'
+  },
+  reactModalContainer: {
+    justifyContent: 'flex-end',
+    margin: 0
+  },
+  closeButton: {
+    position: 'absolute',
+    top: 40,
+    right: 20
+  },
+  avatar: {
+    width: 30,
+    height: 30,
+    borderRadius: 15,
+    borderWidth: 1,
+    borderColor: Colors.FILL_LIGHT
+  },
+  emptyChat: {
+    flex: 1,
+    alignItems: 'center',
+    justifyContent: 'center',
+    transform: Platform.OS === 'ios' ? [{ scaleY: -1 }] : [{ scaleY: -1 }, { scaleX: -1 }]
+  },
+  emptyChatText: {
+    fontSize: getFontSize(14),
+    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
+  }
+});

+ 104 - 0
src/screens/InAppScreens/MessagesScreen/Components/ChatMessageBox.tsx

@@ -0,0 +1,104 @@
+import React from 'react';
+import { View, StyleSheet, Animated } from 'react-native';
+import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler';
+import { Message, MessageProps } from 'react-native-gifted-chat';
+import { isSameDay, isSameUser } from 'react-native-gifted-chat/lib/utils';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { Colors } from 'src/theme';
+import { trigger } from 'react-native-haptic-feedback';
+import { CustomMessage } from '../types';
+
+type ChatMessageBoxProps = {
+  setReplyOnSwipeOpen: (message: CustomMessage) => void;
+  updateRowRef: (ref: any) => void;
+} & MessageProps<CustomMessage>;
+
+const options = {
+  enableVibrateFallback: true,
+  ignoreAndroidSystemSettings: false
+};
+
+const ChatMessageBox = ({ setReplyOnSwipeOpen, updateRowRef, ...props }: ChatMessageBoxProps) => {
+  const isNextMyMessage =
+    props.currentMessage &&
+    props.nextMessage &&
+    isSameUser(props.currentMessage, props.nextMessage) &&
+    isSameDay(props.currentMessage, props.nextMessage);
+
+  const renderRightAction = (progressAnimatedValue: Animated.AnimatedInterpolation<number>) => {
+    const size = progressAnimatedValue.interpolate({
+      inputRange: [0, 1, 100],
+      outputRange: [0, 1, 1]
+    });
+    const trans = progressAnimatedValue.interpolate({
+      inputRange: [0, 1, 2],
+      outputRange: [0, -12, -20]
+    });
+
+    return (
+      <Animated.View
+        style={[
+          styles.container,
+          { transform: [{ scale: size }, { translateX: trans }] },
+          isNextMyMessage ? styles.defaultBottomOffset : styles.bottomOffsetNext,
+          props.position === 'right' && styles.leftOffsetValue
+        ]}
+      >
+        <View style={styles.replyImageWrapper}>
+          <MaterialCommunityIcons name="reply-circle" size={20} color={Colors.DARK_BLUE} />
+        </View>
+      </Animated.View>
+    );
+  };
+
+  const onSwipeOpenAction = () => {
+    if (props.currentMessage) {
+      setReplyOnSwipeOpen({ ...props.currentMessage });
+    }
+  };
+
+  if (props.currentMessage?.deleted || props.currentMessage?.system) {
+    return <Message {...props} />;
+  }
+
+  return (
+    <GestureHandlerRootView>
+      <Swipeable
+        ref={updateRowRef}
+        friction={2}
+        rightThreshold={40}
+        renderRightActions={renderRightAction}
+        onSwipeableOpen={onSwipeOpenAction}
+        onSwipeableWillOpen={() => trigger('impactMedium', options)}
+      >
+        <Message {...props} />
+      </Swipeable>
+    </GestureHandlerRootView>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    width: 40
+  },
+  replyImageWrapper: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center'
+  },
+  replyImage: {
+    width: 20,
+    height: 20
+  },
+  defaultBottomOffset: {
+    marginBottom: 2
+  },
+  bottomOffsetNext: {
+    marginBottom: 10
+  },
+  leftOffsetValue: {
+    marginLeft: 16
+  }
+});
+
+export default ChatMessageBox;

+ 70 - 0
src/screens/InAppScreens/MessagesScreen/Components/EmojiSelectorModal.tsx

@@ -0,0 +1,70 @@
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+import EmojiSelector from 'react-native-emoji-selector';
+import ActionSheet from 'react-native-actions-sheet';
+
+interface EmojiSelectorModalProps {
+  visible: boolean;
+  addReaction: (messageId: number, emoji: string) => void;
+  selectedMessage: any;
+  closeEmojiSelector: () => void;
+}
+
+const EmojiSelectorModal: React.FC<EmojiSelectorModalProps> = ({
+  addReaction,
+  selectedMessage,
+  closeEmojiSelector
+}) => (
+  <ActionSheet
+    id="emoji-selector"
+    gestureEnabled={true}
+    containerStyle={{
+      borderTopLeftRadius: 15,
+      borderTopRightRadius: 15,
+      height: '60%',
+      padding: 10
+    }}
+    defaultOverlayOpacity={0.5}
+  >
+    <View style={{ height: '100%', width: '100%', paddingTop: 8 }}>
+      {/* <View style={{ alignItems: 'flex-end' }}>
+        <TouchableOpacity style={{}} onPress={closeEmojiSelector}>
+          <MaterialCommunityIcons name="close" size={28} color={Colors.LIGHT_GRAY} />
+        </TouchableOpacity>
+      </View> */}
+      <EmojiSelector
+        onEmojiSelected={(emoji) => {
+          addReaction(selectedMessage?.currentMessage?._id, emoji);
+          closeEmojiSelector();
+        }}
+        showSearchBar={true}
+        columns={8}
+      />
+    </View>
+  </ActionSheet>
+);
+
+const styles = StyleSheet.create({
+  emojiSelectorContainer: {
+    position: 'absolute',
+    bottom: 0,
+    width: '100%',
+    height: '50%',
+    backgroundColor: 'white',
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    shadowColor: '#000',
+    shadowOpacity: 0.1,
+    shadowOffset: { width: 0, height: -2 },
+    shadowRadius: 4,
+    elevation: 5,
+    padding: 10
+  },
+  closeModalButton: {
+    position: 'absolute',
+    top: 10,
+    right: 10
+  }
+});
+
+export default EmojiSelectorModal;

+ 245 - 0
src/screens/InAppScreens/MessagesScreen/Components/MoreModal.tsx

@@ -0,0 +1,245 @@
+import React, { useState } from 'react';
+import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native';
+import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
+import { Colors } from 'src/theme';
+import { API_HOST } from 'src/constants';
+import { getFontSize } from 'src/utils';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { ChatProps, WarningProps } from '../types';
+import { useNavigation } from '@react-navigation/native';
+import { NAVIGATION_PAGES } from 'src/types';
+import {
+  usePostDeleteChatMutation,
+  usePostSetBlockMutation,
+  usePostSetMuteMutation
+} from '@api/chat';
+import { useChatStore } from 'src/stores/chatStore';
+import TrashIcon from 'assets/icons/travels-screens/trash-solid.svg';
+import BanIcon from 'assets/icons/messages/ban.svg';
+import BellSlashIcon from 'assets/icons/messages/bell-slash.svg';
+import { AvatarWithInitials } from 'src/components';
+
+const MoreModal = () => {
+  const insets = useSafeAreaInsets();
+  const navigation = useNavigation();
+  const { setIsWarningModalVisible } = useChatStore();
+
+  const [chatData, setChatData] = useState<
+    | (ChatProps & {
+        token: string;
+        refetch: () => void;
+        refetchBlocked: () => void;
+      })
+    | null
+  >(null);
+  const { mutateAsync: muteUser } = usePostSetMuteMutation();
+  const { mutateAsync: blockUser } = usePostSetBlockMutation();
+  const { mutateAsync: deleteChat } = usePostDeleteChatMutation();
+
+  const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState<WarningProps | null>(null);
+
+  const handleSheetOpen = (
+    payload:
+      | (ChatProps & {
+          token: string;
+          refetch: () => void;
+          refetchBlocked: () => void;
+        })
+      | null
+  ) => {
+    setChatData(payload);
+  };
+
+  const handleMute = async () => {
+    if (!chatData) return;
+
+    await muteUser(
+      {
+        token: chatData.token,
+        value: chatData.muted === 1 ? 0 : 1,
+        conversation_with_user: chatData.uid
+      },
+      {
+        onSuccess: () => {
+          setChatData({ ...chatData, muted: chatData.muted === 1 ? 0 : 1 });
+        }
+      }
+    );
+    chatData.refetch();
+  };
+
+  const handleBlock = async () => {
+    if (!chatData) return;
+
+    setShouldOpenWarningModal({
+      title: 'Block user',
+      buttonTitle: 'Block',
+      message: `Are you sure you want to block ${chatData?.name}?\nThis user will be blocked and you will not be able to send or receive messages from him/her.`,
+      action: async () => {
+        await blockUser({
+          token: chatData.token,
+          value: 1,
+          conversation_with_user: chatData.uid
+        });
+
+        chatData.refetch();
+        chatData.refetchBlocked();
+      }
+    });
+
+    setTimeout(() => {
+      SheetManager.hide('more-modal');
+      setShouldOpenWarningModal(null);
+    }, 300);
+  };
+
+  const handleDelete = async () => {
+    if (!chatData) return;
+
+    setShouldOpenWarningModal({
+      title: 'Delete conversation',
+      message: `Are you sure you want to delete conversation with ${chatData?.name}?\nThis conversation will be deleted for both sides.`,
+      action: async () => {
+        await deleteChat({
+          token: chatData.token,
+          conversation_with_user: chatData.uid
+        });
+
+        chatData.refetch();
+      }
+    });
+
+    setTimeout(() => {
+      SheetManager.hide('more-modal');
+      setShouldOpenWarningModal(null);
+    }, 300);
+  };
+
+  return (
+    <ActionSheet
+      id="more-modal"
+      gestureEnabled={true}
+      onBeforeShow={(sheetRef) => {
+        const payload = sheetRef || null;
+        handleSheetOpen(payload);
+      }}
+      onClose={() => {
+        if (shouldOpenWarningModal) {
+          setIsWarningModalVisible(shouldOpenWarningModal);
+        }
+      }}
+      containerStyle={styles.sheetContainer}
+      defaultOverlayOpacity={0.5}
+      indicatorStyle={{ backgroundColor: 'transparent' }}
+    >
+      {chatData && (
+        <View style={[styles.container, { paddingBottom: 8 + insets.bottom }]}>
+          <TouchableOpacity
+            style={styles.header}
+            onPress={() => {
+              SheetManager.hide('more-modal');
+              navigation.navigate(
+                ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: chatData.uid }] as never)
+              );
+            }}
+          >
+            {chatData?.avatar ? (
+              <Image source={{ uri: API_HOST + chatData.avatar }} style={styles.avatar} />
+            ) : (
+              <AvatarWithInitials
+                text={
+                  chatData.name
+                    .split(/ (.+)/)
+                    .map((n) => n[0])
+                    .join('') ?? ''
+                }
+                flag={API_HOST + 'flag.png'}
+                size={32}
+                fontSize={12}
+              />
+            )}
+            <Text style={styles.name}>{chatData.name}</Text>
+          </TouchableOpacity>
+
+          <View style={styles.optionsContainer}>
+            <TouchableOpacity style={styles.option} onPress={handleMute}>
+              <Text style={styles.optionText}>{chatData.muted === 1 ? 'Unmute' : 'Mute'}</Text>
+              <BellSlashIcon fill={Colors.DARK_BLUE} />
+            </TouchableOpacity>
+          </View>
+
+          <View style={[styles.optionsContainer, { paddingVertical: 0, gap: 0 }]}>
+            <TouchableOpacity style={[styles.option, styles.dangerOption]} onPress={handleBlock}>
+              <Text style={[styles.optionText, styles.dangerText]}>Block {chatData.name}</Text>
+              <BanIcon fill={Colors.RED} />
+            </TouchableOpacity>
+
+            <TouchableOpacity style={[styles.option, styles.dangerOption]} onPress={handleDelete}>
+              <Text style={[styles.optionText, styles.dangerText]}>Delete chat</Text>
+              <TrashIcon fill={Colors.RED} width={18} height={18} />
+            </TouchableOpacity>
+          </View>
+        </View>
+      )}
+    </ActionSheet>
+  );
+};
+
+const styles = StyleSheet.create({
+  sheetContainer: {
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15
+  },
+  container: {
+    backgroundColor: 'white',
+    paddingHorizontal: 16,
+    paddingTop: 8,
+    gap: 16
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    borderBottomWidth: 1,
+    borderBottomColor: Colors.FILL_LIGHT,
+    gap: 8
+  },
+  avatar: {
+    width: 32,
+    height: 32,
+    borderRadius: 16,
+    borderWidth: 1,
+    borderColor: Colors.FILL_LIGHT
+  },
+  name: {
+    fontSize: getFontSize(14),
+    fontFamily: 'montserrat-700',
+    color: Colors.DARK_BLUE
+  },
+  optionsContainer: {
+    paddingVertical: 10,
+    paddingHorizontal: 8,
+    gap: 16,
+    borderRadius: 8,
+    backgroundColor: Colors.FILL_LIGHT
+  },
+  option: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between'
+  },
+  optionText: {
+    fontSize: getFontSize(12),
+    fontWeight: '600',
+    color: Colors.DARK_BLUE
+  },
+  dangerOption: {
+    paddingVertical: 10,
+    borderBottomWidth: 1,
+    borderBlockColor: Colors.WHITE
+  },
+  dangerText: {
+    color: Colors.RED
+  }
+});
+
+export default MoreModal;

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

@@ -0,0 +1,84 @@
+import React from 'react';
+import { TouchableOpacity, Text, StyleSheet, Dimensions } from 'react-native';
+import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { Colors } from 'src/theme';
+
+interface MessagePosition {
+  x: number;
+  y: number;
+  isMine: boolean;
+  width: number;
+  height: number;
+}
+
+interface OptionsMenuProps {
+  messagePosition: MessagePosition | null;
+  selectedMessage: any;
+  handleOptionPress: (option: string) => void;
+}
+
+const OptionsMenu: React.FC<OptionsMenuProps> = ({
+  messagePosition,
+  selectedMessage,
+  handleOptionPress
+}) =>
+  selectedMessage &&
+  messagePosition && (
+    <Animated.View
+      entering={FadeIn}
+      exiting={FadeOut}
+      style={[
+        styles.optionsMenu,
+        {
+          top: messagePosition.y + messagePosition.height + 8,
+          left: messagePosition.isMine
+            ? Dimensions.get('window').width - Dimensions.get('window').width * 0.75 - 8
+            : messagePosition.x
+        }
+      ]}
+    >
+      <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('reply')}>
+        <Text style={styles.optionText}>Reply</Text>
+        <MaterialCommunityIcons name="reply" 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} />
+      </TouchableOpacity>
+      {/* <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('pin')}>
+        <Text style={styles.optionText}>Pin message</Text>
+        <MaterialCommunityIcons name="pin" size={20} color={Colors.DARK_BLUE} />
+      </TouchableOpacity> */}
+    </Animated.View>
+  );
+
+const styles = StyleSheet.create({
+  optionsMenu: {
+    position: 'absolute',
+    backgroundColor: 'rgba(255, 255, 255, 1)',
+    borderRadius: 10,
+    padding: 8,
+    shadowColor: '#000',
+    shadowOpacity: 0.3,
+    shadowOffset: { width: 0, height: 2 },
+    shadowRadius: 5,
+    elevation: 5,
+    width: Dimensions.get('window').width * 0.75
+  },
+  optionButton: {
+    paddingVertical: 10,
+    paddingHorizontal: 12,
+    flexDirection: 'row',
+    justifyContent: 'space-between'
+  },
+  optionText: {
+    fontSize: 16
+  }
+});
+
+export default OptionsMenu;

+ 91 - 0
src/screens/InAppScreens/MessagesScreen/Components/ReactionBar.tsx

@@ -0,0 +1,91 @@
+import React from 'react';
+import { View, TouchableOpacity, Text, StyleSheet, Dimensions } from 'react-native';
+import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { Colors } from 'src/theme';
+
+interface MessagePosition {
+  x: number;
+  y: number;
+  isMine: boolean;
+  width: number;
+  height: number;
+}
+
+interface ReactionBarProps {
+  messagePosition: MessagePosition | null;
+  selectedMessage: any;
+  reactionEmojis: string[];
+  handleReactionPress: (emoji: string, messageId: number) => void;
+  openEmojiSelector: () => void;
+}
+
+const ReactionBar: React.FC<ReactionBarProps> = ({
+  messagePosition,
+  selectedMessage,
+  reactionEmojis,
+  handleReactionPress,
+  openEmojiSelector
+}) =>
+  selectedMessage &&
+  messagePosition && (
+    <Animated.View
+      entering={FadeIn}
+      exiting={FadeOut}
+      style={[
+        styles.reactionBar,
+        {
+          top: messagePosition.y - 50, // - reaction bar height
+          left: messagePosition.isMine
+            ? Dimensions.get('window').width - Dimensions.get('window').width * 0.75 - 8 // reaction bar width
+            : messagePosition.x
+        }
+      ]}
+    >
+      {reactionEmojis.map((emoji) => (
+        <TouchableOpacity
+          key={emoji}
+          onPress={() => handleReactionPress(emoji, selectedMessage?.currentMessage?._id)}
+        >
+          <Text style={styles.reactionEmoji}>{emoji}</Text>
+        </TouchableOpacity>
+      ))}
+      <TouchableOpacity onPress={() => openEmojiSelector()} style={styles.addReactionButton}>
+        <MaterialCommunityIcons name="plus" size={28} color={Colors.LIGHT_GRAY} />
+      </TouchableOpacity>
+    </Animated.View>
+  );
+
+const styles = StyleSheet.create({
+  reactionBar: {
+    position: 'absolute',
+    width: Dimensions.get('window').width * 0.75,
+    flexDirection: 'row',
+    backgroundColor: 'rgba(255, 255, 255, 1)',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    borderRadius: 20,
+    padding: 5,
+    paddingHorizontal: 12,
+    shadowColor: '#000',
+    shadowOpacity: 0.3,
+    shadowOffset: { width: 0, height: 2 },
+    shadowRadius: 5,
+    elevation: 5
+  },
+  reactionEmoji: {
+    fontSize: 28
+  },
+  addReactionButton: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: Colors.FILL_LIGHT,
+    borderRadius: 15,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY,
+    width: 30,
+    height: 30
+  }
+});
+
+export default ReactionBar;

+ 141 - 0
src/screens/InAppScreens/MessagesScreen/Components/ReactionsListModal.tsx

@@ -0,0 +1,141 @@
+import React, { useState } from 'react';
+import { View, Text, TouchableOpacity, FlatList, StyleSheet } from 'react-native';
+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<{
+    users: { uid: number; name: string; reaction: string }[];
+    currentUserId: number;
+    token: string;
+    messageId: number;
+    conversation_with_user: number;
+    setMessages: (messages: any) => void;
+    sendWebSocketMessage: (action: string, data: any) => void;
+  } | null>(null);
+
+  const { mutateAsync: unreactToMessage } = usePostUnreactToMessageMutation();
+
+  const handleSheetOpen = (
+    payload: {
+      users: { uid: number; name: string; reaction: string }[];
+      currentUserId: number;
+      token: string;
+      messageId: number;
+      conversation_with_user: number;
+      setMessages: (messages: any) => void;
+      sendWebSocketMessage: (action: string, data: any) => void;
+    } | null
+  ) => {
+    setReactionsData(payload);
+  };
+
+  const handleUnreact = () => {
+    if (reactionsData) {
+      unreactToMessage(
+        {
+          token: reactionsData.token,
+          message_id: reactionsData.messageId,
+          conversation_with_user: reactionsData.conversation_with_user
+        },
+        {
+          onSuccess: () => {
+            SheetManager.hide('reactions-list-modal');
+            reactionsData.setMessages((prevMessages: any) =>
+              prevMessages?.map((msg: any) => {
+                if (msg._id === reactionsData.messageId) {
+                  return {
+                    ...msg,
+                    reactions: Array.isArray(msg.reactions)
+                      ? msg.reactions.filter((r: any) => r.uid !== reactionsData.currentUserId)
+                      : []
+                  };
+                }
+                return msg;
+              })
+            );
+            reactionsData.sendWebSocketMessage('unreact', {
+              _id: reactionsData.messageId
+            } as unknown as CustomMessage);
+          }
+        }
+      );
+    }
+  };
+
+  return (
+    <ActionSheet
+      id="reactions-list-modal"
+      gestureEnabled={true}
+      onBeforeShow={(sheetRef) => {
+        const payload = sheetRef || null;
+        handleSheetOpen(payload);
+      }}
+      containerStyle={styles.sheetContainer}
+      defaultOverlayOpacity={0.5}
+      indicatorStyle={{ backgroundColor: Colors.FILL_LIGHT }}
+    >
+      <View style={styles.container}>
+        <FlatList
+          data={reactionsData?.users || []}
+          keyExtractor={(item) => item.uid.toString()}
+          renderItem={({ item }) => {
+            const isUserReacted = item.uid === reactionsData?.currentUserId;
+            return (
+              <TouchableOpacity
+                style={styles.userItem}
+                onPress={handleUnreact}
+                disabled={!isUserReacted}
+              >
+                <View style={{ gap: 2 }}>
+                  <Text style={styles.userName}>{item.name}</Text>
+                  {isUserReacted && <Text style={styles.unreactText}>Tap to unreact</Text>}
+                </View>
+                <Text style={styles.reaction}>{item.reaction}</Text>
+              </TouchableOpacity>
+            );
+          }}
+        />
+      </View>
+    </ActionSheet>
+  );
+};
+
+const styles = StyleSheet.create({
+  sheetContainer: {
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    padding: 16,
+    paddingTop: 0,
+    height: '40%'
+  },
+  container: {
+    paddingTop: 12
+  },
+  userItem: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingVertical: 12,
+    borderBottomWidth: 1,
+    borderBottomColor: Colors.FILL_LIGHT
+  },
+  userName: {
+    fontSize: getFontSize(14),
+    color: Colors.DARK_BLUE,
+    fontFamily: 'montserrat-600'
+  },
+  reaction: {
+    fontSize: 20
+  },
+  unreactText: {
+    color: Colors.LIGHT_GRAY,
+    fontSize: getFontSize(12),
+    fontWeight: '600'
+  }
+});
+
+export default ReactionsListModal;

+ 96 - 0
src/screens/InAppScreens/MessagesScreen/Components/ReplyMessageBar.tsx

@@ -0,0 +1,96 @@
+import React from 'react';
+import { Text, StyleSheet, View, TouchableOpacity } from 'react-native';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { Colors } from 'src/theme';
+import { IMessage } from 'react-native-gifted-chat';
+import Animated, { FadeInDown, FadeOutDown } from 'react-native-reanimated';
+
+type ReplyMessageBarProps = {
+  clearReply: () => void;
+  message: IMessage | null;
+};
+
+const replyMessageBarHeight = 50;
+
+const ReplyMessageBar = ({ clearReply, message }: ReplyMessageBarProps) => {
+  if (!message) return null;
+
+  return (
+    <Animated.View
+      entering={FadeInDown}
+      exiting={FadeOutDown}
+      style={{
+        height: 50,
+        flexDirection: 'row',
+        backgroundColor: Colors.FILL_LIGHT
+      }}
+    >
+      <View
+        style={{
+          height: 50,
+          width: 6,
+          backgroundColor: Colors.ORANGE
+        }}
+      ></View>
+      <View style={styles.replyImageContainer}>
+        <MaterialCommunityIcons name="reply" size={20} color={Colors.DARK_BLUE} />
+      </View>
+
+      <View style={{ flex: 1 }}>
+        <Text
+          style={{
+            color: Colors.DARK_BLUE,
+            fontWeight: '600',
+            paddingLeft: 10,
+            paddingTop: 5,
+            fontSize: 15
+          }}
+        >
+          {message.user.name}
+        </Text>
+        <Text style={{ color: Colors.DARK_BLUE, paddingLeft: 10, paddingTop: 5 }} numberOfLines={1}>
+          {message.text}
+        </Text>
+      </View>
+
+      <View style={{ alignItems: 'flex-end', justifyContent: 'center' }}>
+        <TouchableOpacity style={styles.crossButton} onPress={clearReply}>
+          <MaterialCommunityIcons name="close-circle-outline" size={24} color={Colors.DARK_BLUE} />
+        </TouchableOpacity>
+      </View>
+    </Animated.View>
+  );
+};
+
+export default ReplyMessageBar;
+
+const styles = StyleSheet.create({
+  container: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 8,
+    borderBottomWidth: 1,
+    borderBottomColor: 'lightgrey',
+    height: replyMessageBarHeight
+  },
+  replyImage: {
+    width: 20,
+    height: 20
+  },
+  replyImageContainer: {
+    paddingLeft: 8,
+    height: '100%',
+    justifyContent: 'center'
+  },
+  crossButtonIcon: {
+    width: 24,
+    height: 24
+  },
+  crossButton: {
+    padding: 4,
+    paddingRight: 10
+  },
+  messageContainer: {
+    flex: 1
+  }
+});

+ 161 - 0
src/screens/InAppScreens/MessagesScreen/Components/SearchUsersModal.tsx

@@ -0,0 +1,161 @@
+import React, { useEffect, useState } from 'react';
+import {
+  View,
+  Text,
+  Image,
+  TouchableOpacity,
+  StyleSheet,
+  ActivityIndicator,
+  Platform
+} from 'react-native';
+import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
+import { FlashList } from '@shopify/flash-list';
+import { storage, StoreType } from 'src/storage';
+import { usePostSearchUsers } from '@api/chat';
+import { API_HOST } from 'src/constants';
+import { Colors } from 'src/theme';
+import { useNavigation } from '@react-navigation/native';
+import { NAVIGATION_PAGES } from 'src/types';
+import { getFontSize } from 'src/utils';
+import { AvatarWithInitials, Input } from 'src/components';
+
+import SearchIcon from 'assets/icons/search.svg';
+
+const SearchModal = () => {
+  const navigation = useNavigation();
+  const token = storage.get('token', StoreType.STRING) as string;
+  const [searchQuery, setSearchQuery] = useState('');
+  const { data: searchResult, isFetching } = usePostSearchUsers(
+    token,
+    searchQuery,
+    searchQuery.length > 1
+  );
+
+  useEffect(() => {}, [searchResult]);
+
+  const renderItem = ({ item }: { item: any }) => (
+    <TouchableOpacity
+      style={styles.itemContainer}
+      onPress={() => {
+        SheetManager.hide('search-modal');
+        navigation.navigate(
+          ...([
+            NAVIGATION_PAGES.CHAT,
+            {
+              id: item.user_id,
+              name: item.first_name + ' ' + item.last_name,
+              avatar: item.avatar
+            }
+          ] as never)
+        );
+      }}
+    >
+      {item.avatar ? (
+        <Image source={{ uri: API_HOST + item.avatar }} style={styles.avatar} />
+      ) : (
+        <AvatarWithInitials
+          text={`${item.first_name[0] ?? ''}${item.last_name[0] ?? ''}`}
+          flag={API_HOST + item.homebase_flag}
+          size={30}
+          fontSize={12}
+        />
+      )}
+
+      <View style={styles.textContainer}>
+        <Text style={styles.name}>
+          {item.first_name} {item.last_name}
+        </Text>
+      </View>
+    </TouchableOpacity>
+  );
+
+  return (
+    <ActionSheet
+      id="search-modal"
+      gestureEnabled={Platform.OS === 'ios'}
+      onClose={() => setSearchQuery('')}
+      containerStyle={styles.sheetContainer}
+      defaultOverlayOpacity={0.5}
+      indicatorStyle={{ backgroundColor: 'transparent' }}
+    >
+      <View style={styles.container}>
+        <Input
+          inputMode={'search'}
+          placeholder={'Search nomads'}
+          onChange={(text) => {
+            setSearchQuery(text);
+          }}
+          value={searchQuery}
+          icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
+        />
+
+        {isFetching ? (
+          <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
+        ) : (
+          <View style={{ flex: 1 }}>
+            <FlashList
+              viewabilityConfig={{
+                waitForInteraction: true,
+                itemVisiblePercentThreshold: 50,
+                minimumViewTime: 1000
+              }}
+              data={searchResult?.data || []}
+              renderItem={renderItem}
+              keyExtractor={(item) => item.user_id.toString()}
+              estimatedItemSize={100}
+              extraData={searchResult}
+              showsVerticalScrollIndicator={false}
+              refreshing={isFetching}
+            />
+          </View>
+        )}
+      </View>
+    </ActionSheet>
+  );
+};
+
+const styles = StyleSheet.create({
+  sheetContainer: {
+    height: '80%',
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    paddingHorizontal: 16,
+    paddingVertical: 12
+  },
+  container: {
+    backgroundColor: 'white',
+    gap: 12,
+    height: '100%'
+  },
+  searchInput: {
+    height: 40,
+    borderColor: '#ccc',
+    borderWidth: 1,
+    borderRadius: 8,
+    paddingHorizontal: 10,
+    marginBottom: 10
+  },
+  itemContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 12,
+    gap: 8
+  },
+  avatar: {
+    width: 30,
+    height: 30,
+    borderRadius: 15,
+    borderWidth: 1,
+    borderColor: Colors.FILL_LIGHT
+  },
+  textContainer: {
+    flex: 1
+  },
+  name: {
+    fontSize: getFontSize(14),
+    color: Colors.DARK_BLUE,
+    fontFamily: 'montserrat-700'
+  }
+});
+
+export default SearchModal;

+ 105 - 0
src/screens/InAppScreens/MessagesScreen/Components/SwipeableBlockedRow.tsx

@@ -0,0 +1,105 @@
+import React, { PropsWithChildren, useRef } from 'react';
+import { Animated, StyleSheet, Text, View } from 'react-native';
+import { RectButton, Swipeable } from 'react-native-gesture-handler';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
+
+import { Blocked } from '../types';
+import { usePostSetBlockMutation } from '@api/chat';
+import BanIcon from 'assets/icons/messages/ban.svg';
+
+interface AppleStyleSwipeableRowProps extends PropsWithChildren<unknown> {
+  data: Blocked;
+  token: string;
+  onRowOpen: (ref: any) => void;
+  refetchBlocked: () => void;
+}
+
+const SwipeableBlockedRow: React.FC<AppleStyleSwipeableRowProps> = ({
+  children,
+  data,
+  token,
+  onRowOpen,
+  refetchBlocked
+}) => {
+  const swipeableRow = useRef<Swipeable>(null);
+  const { mutateAsync: unBlockUser } = usePostSetBlockMutation();
+
+  const close = () => {
+    swipeableRow.current?.close();
+  };
+
+  const renderRightAction = (
+    text: string,
+    color: string,
+    x: number,
+    progress: Animated.AnimatedInterpolation<number>
+  ) => {
+    const trans = progress.interpolate({
+      inputRange: [0, 1],
+      outputRange: [x, 0]
+    });
+
+    const pressHandler = async () => {
+      close();
+      await unBlockUser({
+        token,
+        value: 0,
+        conversation_with_user: data.id
+      });
+      refetchBlocked();
+    };
+
+    return (
+      <Animated.View style={{ flex: 1, transform: [{ translateX: trans }] }}>
+        <RectButton style={[styles.rightAction, { backgroundColor: color }]} onPress={pressHandler}>
+          <BanIcon height={18} width={18} fill={Colors.WHITE} />
+          <Text style={styles.actionText}>{text}</Text>
+        </RectButton>
+      </Animated.View>
+    );
+  };
+
+  const renderRightActions = (progress: Animated.AnimatedInterpolation<number>) => (
+    <View
+      style={{
+        width: 84,
+        flexDirection: 'row'
+      }}
+    >
+      {renderRightAction('Unblock', Colors.RED, 84, progress)}
+    </View>
+  );
+
+  return (
+    <Swipeable
+      ref={swipeableRow}
+      friction={2}
+      enableTrackpadTwoFingerGesture
+      rightThreshold={40}
+      renderRightActions={renderRightActions}
+      onSwipeableOpenStartDrag={() => {
+        onRowOpen(swipeableRow.current);
+      }}
+    >
+      {children}
+    </Swipeable>
+  );
+};
+
+const styles = StyleSheet.create({
+  actionText: {
+    color: Colors.WHITE,
+    fontSize: getFontSize(12),
+    fontWeight: '600',
+    backgroundColor: 'transparent'
+  },
+  rightAction: {
+    alignItems: 'center',
+    flex: 1,
+    justifyContent: 'center',
+    gap: 8
+  }
+});
+
+export default SwipeableBlockedRow;

+ 183 - 0
src/screens/InAppScreens/MessagesScreen/Components/SwipeableRow.tsx

@@ -0,0 +1,183 @@
+import React, { PropsWithChildren, useRef } from 'react';
+import { Animated, StyleSheet, Text, View } from 'react-native';
+import { RectButton, Swipeable } from 'react-native-gesture-handler';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
+import { SheetManager } from 'react-native-actions-sheet';
+
+import PinIcon from 'assets/icons/messages/pin.svg';
+import ArchiveIcon from 'assets/icons/messages/archive.svg';
+import DotsIcon from 'assets/icons/messages/dots.svg';
+import UnpinIcon from 'assets/icons/messages/unpin.svg';
+import { ChatProps } from '../types';
+import { usePostSetArchiveMutation, usePostSetPinMutation } from '@api/chat';
+import { useChatStore } from 'src/stores/chatStore';
+
+interface AppleStyleSwipeableRowProps extends PropsWithChildren<unknown> {
+  chat: ChatProps;
+  token: string;
+  onRowOpen: (ref: any) => void;
+  refetch: () => void;
+  refetchBlocked: () => void;
+}
+
+const SwipeableRow: React.FC<AppleStyleSwipeableRowProps> = ({
+  children,
+  chat,
+  token,
+  onRowOpen,
+  refetch,
+  refetchBlocked
+}) => {
+  const swipeableRow = useRef<Swipeable>(null);
+  const { setSelectedChat } = useChatStore();
+
+  const { mutateAsync: pinChat } = usePostSetPinMutation();
+  const { mutateAsync: archiveChat } = usePostSetArchiveMutation();
+
+  const close = () => {
+    swipeableRow.current?.close();
+  };
+
+  const renderRightAction = (
+    text: string,
+    color: string,
+    x: number,
+    progress: Animated.AnimatedInterpolation<number>
+  ) => {
+    const trans = progress.interpolate({
+      inputRange: [0, 1],
+      outputRange: [x, 0]
+    });
+
+    const pressHandler = async () => {
+      close();
+      if (text === 'More') {
+        setSelectedChat(chat);
+        SheetManager.show('more-modal', {
+          payload: {
+            uid: chat.uid,
+            name: chat.name,
+            avatar: chat.avatar,
+            muted: chat.muted,
+            token: token,
+            refetch,
+            refetchBlocked
+          } as any
+        });
+      } else {
+        await archiveChat({
+          token,
+          value: chat.archive === 1 ? 0 : 1,
+          conversation_with_user: chat.uid
+        });
+        refetch();
+      }
+    };
+
+    return (
+      <Animated.View style={{ flex: 1, transform: [{ translateX: trans }] }}>
+        <RectButton style={[styles.rightAction, { backgroundColor: color }]} onPress={pressHandler}>
+          {text === 'More' ? (
+            <DotsIcon height={18} width={18} />
+          ) : (
+            <ArchiveIcon height={18} width={18} />
+          )}
+          <Text style={styles.actionText}>{text}</Text>
+        </RectButton>
+      </Animated.View>
+    );
+  };
+
+  const renderLeftAction = (
+    text: string,
+    color: string,
+    x: number,
+    progress: Animated.AnimatedInterpolation<number>
+  ) => {
+    const trans = progress.interpolate({
+      inputRange: [0, 1],
+      outputRange: [-x, 0]
+    });
+
+    const pressHandler = async () => {
+      close();
+      await pinChat({
+        token,
+        value: chat.pin === 1 ? 0 : 1,
+        conversation_with_user: chat.uid
+      });
+      refetch();
+    };
+
+    return (
+      <Animated.View style={{ flex: 1, transform: [{ translateX: trans }] }}>
+        <RectButton style={[styles.rightAction, { backgroundColor: color }]} onPress={pressHandler}>
+          {chat.pin === 1 ? <UnpinIcon width={18} /> : <PinIcon width={18} />}
+          <Text style={styles.actionText}>{text}</Text>
+        </RectButton>
+      </Animated.View>
+    );
+  };
+
+  const renderRightActions = (progress: Animated.AnimatedInterpolation<number>) => (
+    <View
+      style={{
+        width: 168,
+        flexDirection: 'row'
+      }}
+    >
+      {renderRightAction(
+        chat.archive === 0 ? 'Archive' : 'Unarchive',
+        Colors.BORDER_LIGHT,
+        168,
+        progress
+      )}
+      {renderRightAction('More', Colors.FILL_LIGHT, 112, progress)}
+    </View>
+  );
+
+  const renderLeftActions = (progress: Animated.AnimatedInterpolation<number>) => (
+    <View
+      style={{
+        width: 84,
+        flexDirection: 'row'
+      }}
+    >
+      {renderLeftAction(chat.pin === 1 ? 'Unpin' : 'Pin', Colors.FILL_LIGHT, 84, progress)}
+    </View>
+  );
+
+  return (
+    <Swipeable
+      ref={swipeableRow}
+      friction={2}
+      enableTrackpadTwoFingerGesture
+      rightThreshold={40}
+      renderRightActions={renderRightActions}
+      renderLeftActions={renderLeftActions}
+      onSwipeableOpenStartDrag={() => {
+        onRowOpen(swipeableRow.current);
+      }}
+    >
+      {children}
+    </Swipeable>
+  );
+};
+
+const styles = StyleSheet.create({
+  actionText: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(12),
+    fontWeight: '600',
+    backgroundColor: 'transparent'
+  },
+  rightAction: {
+    alignItems: 'center',
+    flex: 1,
+    justifyContent: 'center',
+    gap: 12
+  }
+});
+
+export default SwipeableRow;

+ 12 - 0
src/screens/InAppScreens/MessagesScreen/constants.ts

@@ -0,0 +1,12 @@
+import { Routes } from './types';
+
+export const routes: Routes[] = [
+  { key: 'all', title: 'All' },
+  { key: 'unread', title: 'Unread' },
+  { key: 'archived', title: 'Archived' },
+  {
+    key: 'blocked',
+    title: 'Blocked',
+    icon: 'ban'
+  }
+];

+ 496 - 0
src/screens/InAppScreens/MessagesScreen/index.tsx

@@ -0,0 +1,496 @@
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import {
+  View,
+  Text,
+  TouchableOpacity,
+  Image,
+  Platform,
+  TouchableHighlight,
+  AppState,
+  AppStateStatus
+} from 'react-native';
+import {
+  AvatarWithInitials,
+  HorizontalTabView,
+  Input,
+  PageWrapper,
+  WarningModal
+} from 'src/components';
+import { NAVIGATION_PAGES } from 'src/types';
+import { useFocusEffect, useNavigation } from '@react-navigation/native';
+
+import AddChatIcon from 'assets/icons/messages/chat-plus.svg';
+import { API_HOST, WEBSOCKET_URL } from 'src/constants';
+import { Colors } from 'src/theme';
+import SwipeableRow from './Components/SwipeableRow';
+import { FlashList } from '@shopify/flash-list';
+
+import ReadIcon from 'assets/icons/messages/check-read.svg';
+import UnreadIcon from 'assets/icons/messages/check-unread.svg';
+import SearchModal from './Components/SearchUsersModal';
+import { SheetManager } from 'react-native-actions-sheet';
+import MoreModal from './Components/MoreModal';
+import SearchIcon from 'assets/icons/search.svg';
+import { storage, StoreType } from 'src/storage';
+import { usePostGetBlockedQuery, usePostGetChatsListQuery } from '@api/chat';
+import { Blocked, Chat } from './types';
+
+import PinIcon from 'assets/icons/messages/pin.svg';
+import { formatDate } from './utils';
+import { routes } from './constants';
+import { styles } from './styles';
+import { useChatStore } from 'src/stores/chatStore';
+import BellSlashIcon from 'assets/icons/messages/bell-slash.svg';
+import BanIcon from 'assets/icons/messages/ban.svg';
+import SwipeableBlockedRow from './Components/SwipeableBlockedRow';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
+
+const TypingIndicator = () => {
+  const [dots, setDots] = useState('');
+
+  useEffect(() => {
+    const interval = setInterval(() => {
+      setDots((prevDots) => {
+        if (prevDots.length >= 3) {
+          return '';
+        }
+        return prevDots + '.';
+      });
+    }, 500);
+
+    return () => clearInterval(interval);
+  }, []);
+
+  return <Text style={styles.typingText}>Typing{dots}</Text>;
+};
+
+const MessagesScreen = () => {
+  const navigation = useNavigation();
+  const token = storage.get('token', StoreType.STRING) as string;
+  const [chats, setChats] = useState<Chat[]>([]);
+  const [index, setIndex] = useState(0);
+  const { data: chatsData, refetch } = usePostGetChatsListQuery(token, index === 2 ? 1 : 0, true);
+  const { data: blockedData, refetch: refetchBlocked } = usePostGetBlockedQuery(token, true);
+  const [blocked, setBlocked] = useState<Blocked[]>([]);
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
+
+  const [filteredChats, setFilteredChats] = useState<{
+    all: Chat[];
+    unread: Chat[];
+    archived: Chat[];
+    blocked: Blocked[];
+  }>({ all: [], unread: [], archived: [], blocked: [] });
+  const [search, setSearch] = useState('');
+  const openRowRef = useRef<any>(null);
+  const { isWarningModalVisible, setIsWarningModalVisible } = useChatStore();
+  const [typingUsers, setTypingUsers] = useState<{ [key: string]: boolean }>({});
+
+  const appState = useRef(AppState.currentState);
+
+  const socket = useRef<WebSocket | null>(null);
+
+  const initializeSocket = () => {
+    if (socket.current) {
+      socket.current.close();
+    }
+
+    setTimeout(() => {
+      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');
+      };
+    }, 500);
+  };
+
+  useEffect(() => {
+    const handleAppStateChange = (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);
+          };
+        }
+      }
+    };
+
+    const subscription = AppState.addEventListener('change', handleAppStateChange);
+
+    return () => {
+      subscription.remove();
+      if (socket.current) {
+        socket.current.close();
+        socket.current = null;
+      }
+    };
+  }, [token]);
+
+  useEffect(() => {
+    const pingInterval = setInterval(() => {
+      if (socket.current && socket.current.readyState === WebSocket.OPEN) {
+        socket.current.send(JSON.stringify({ action: 'ping', conversation_with: 0 }));
+      } else {
+        initializeSocket();
+
+        return () => {
+          if (socket.current) {
+            socket.current.close();
+            socket.current = null;
+          }
+        };
+      }
+    }, 50000);
+
+    return () => clearInterval(pingInterval);
+  }, []);
+
+  const handleWebSocketMessage = (data: any) => {
+    switch (data.action) {
+      case 'new_message':
+      case 'messages_read':
+        refetch();
+        break;
+      case 'is_typing':
+        if (data.conversation_with) {
+          setTypingUsers((prev) => ({
+            ...prev,
+            [data.conversation_with]: true
+          }));
+        }
+        break;
+      case 'stopped_typing':
+        if (data.conversation_with) {
+          setTypingUsers((prev) => ({
+            ...prev,
+            [data.conversation_with]: false
+          }));
+        }
+        break;
+      default:
+        break;
+    }
+  };
+
+  const handleRowOpen = (ref: any) => {
+    if (openRowRef.current && openRowRef.current !== ref) {
+      openRowRef.current.close();
+    }
+    openRowRef.current = ref;
+  };
+
+  useFocusEffect(() => {
+    navigation.getParent()?.setOptions({
+      tabBarStyle: {
+        display: 'flex',
+        ...Platform.select({
+          android: {
+            height: 58
+          }
+        })
+      }
+    });
+  });
+
+  useEffect(() => {
+    if (chatsData && chatsData.conversations) {
+      setChats(chatsData.conversations);
+    }
+  }, [chatsData]);
+
+  useEffect(() => {
+    if (blockedData && blockedData.blocked) {
+      setBlocked(blockedData.blocked);
+    }
+  }, [blockedData]);
+
+  useFocusEffect(
+    useCallback(() => {
+      refetch();
+      initializeSocket();
+      updateUnreadMessagesCount();
+
+      return () => {
+        if (socket.current) {
+          socket.current.close();
+          socket.current = null;
+        }
+      };
+    }, [token])
+  );
+
+  const filterChatsByTab = () => {
+    let filteredList = chats;
+
+    if (index === 3) {
+      setFilteredChats((prev) => ({ ...prev, blocked }));
+      return;
+    }
+
+    if (index === 1) {
+      filteredList = chats.filter((chat) => chat.unread_count > 0);
+    }
+
+    filteredList.sort((a, b) => {
+      if (b.pin - a.pin !== 0) {
+        return b.pin - a.pin;
+      }
+      if (b.pin_order - a.pin_order !== 0) {
+        return b.pin_order - a.pin_order;
+      }
+
+      return new Date(b.updated).getTime() - new Date(a.updated).getTime();
+    });
+    setFilteredChats((prev) => ({ ...prev, [routes[index].key]: filteredList }));
+  };
+
+  useEffect(() => {
+    filterChatsByTab();
+  }, [chats, index, blocked]);
+
+  const searchFilter = (text: string) => {
+    if (text) {
+      const newData =
+        chats?.filter((item: Chat) => {
+          const itemData = item.short ? item.short.toLowerCase() : ''.toLowerCase();
+          const textData = text.toLowerCase();
+          return itemData.indexOf(textData) > -1;
+        }) ?? [];
+      setFilteredChats((prev) => ({ ...prev, [routes[index].key]: newData }));
+      setSearch(text);
+    } else {
+      filterChatsByTab();
+      setSearch(text);
+    }
+  };
+
+  const renderChatItem = ({ item }: { item: Chat }) => {
+    return (
+      <SwipeableRow
+        chat={{
+          uid: item.uid,
+          name: item.name,
+          avatar: item.avatar,
+          pin: item.pin,
+          archive: item.archive,
+          muted: item.muted
+        }}
+        token={token}
+        onRowOpen={handleRowOpen}
+        refetch={refetch}
+        refetchBlocked={refetchBlocked}
+      >
+        <TouchableHighlight
+          key={`${item.uid}-${typingUsers[item.uid]}`}
+          activeOpacity={0.8}
+          onPress={() =>
+            navigation.navigate(
+              ...([
+                NAVIGATION_PAGES.CHAT,
+                {
+                  id: item.uid,
+                  name: item.name,
+                  avatar: item.avatar
+                }
+              ] as never)
+            )
+          }
+          underlayColor={Colors.FILL_LIGHT}
+        >
+          <View style={styles.chatItem}>
+            {item.avatar ? (
+              <Image source={{ uri: API_HOST + item.avatar }} style={styles.avatar} />
+            ) : (
+              <AvatarWithInitials
+                text={
+                  item.name
+                    .split(/ (.+)/)
+                    .map((n) => n[0])
+                    .join('') ?? ''
+                }
+                flag={API_HOST + item?.flag}
+                size={54}
+              />
+            )}
+
+            <View style={{ flex: 1, gap: 6 }}>
+              <View style={[styles.rowContainer, { alignItems: 'center' }]}>
+                <Text style={styles.chatName}>{item.name}</Text>
+
+                <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
+                  {item.pin === 1 ? <PinIcon height={12} fill={Colors.DARK_BLUE} /> : null}
+                  {item.muted === 1 ? <BellSlashIcon height={12} fill={Colors.DARK_BLUE} /> : null}
+
+                  {item.sent_by !== item.uid && item.status === 3 ? (
+                    <ReadIcon fill={Colors.DARK_BLUE} />
+                  ) : item.sent_by !== item.uid && (item.status === 2 || item.status === 1) ? (
+                    <UnreadIcon fill={Colors.LIGHT_GRAY} />
+                  ) : null}
+                  <Text style={styles.chatTime}>{formatDate(item.updated)}</Text>
+                </View>
+              </View>
+
+              <View style={[styles.rowContainer, { flex: 1, gap: 6 }]}>
+                {typingUsers[item.uid] ? (
+                  <TypingIndicator />
+                ) : (
+                  <Text numberOfLines={2} style={styles.chatMessage}>
+                    {item.short}
+                  </Text>
+                )}
+
+                {item.unread_count > 0 ? (
+                  <View style={styles.unreadBadge}>
+                    <Text style={styles.unreadText}>
+                      {item.unread_count > 99 ? '99+' : item.unread_count}
+                    </Text>
+                  </View>
+                ) : null}
+              </View>
+            </View>
+          </View>
+        </TouchableHighlight>
+      </SwipeableRow>
+    );
+  };
+
+  const renderBlockedItem = ({ item }: { item: Blocked }) => {
+    return (
+      <SwipeableBlockedRow
+        data={{
+          id: item.id,
+          first_name: item.first_name,
+          last_name: item.last_name,
+          avatar: item.avatar
+        }}
+        token={token}
+        onRowOpen={handleRowOpen}
+        refetchBlocked={refetchBlocked}
+      >
+        <TouchableHighlight
+          activeOpacity={0.8}
+          onPress={() =>
+            navigation.navigate(
+              ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: item.id }] as never)
+            )
+          }
+          underlayColor={Colors.FILL_LIGHT}
+        >
+          <View style={[styles.chatItem, { alignItems: 'center' }]}>
+            {item.avatar ? (
+              <Image
+                source={{ uri: API_HOST + item.avatar }}
+                style={[styles.avatar, { width: 30, height: 30, borderRadius: 15 }]}
+              />
+            ) : (
+              <AvatarWithInitials
+                text={item.first_name[0] + item.last_name[0]}
+                flag={API_HOST + item?.flag}
+                size={32}
+                fontSize={12}
+              />
+            )}
+
+            <View style={{ flex: 1, gap: 6 }}>
+              <View style={[styles.rowContainer, { alignItems: 'center' }]}>
+                <Text style={styles.chatName}>{item.first_name + ' ' + item.last_name}</Text>
+
+                <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
+                  <BanIcon height={12} fill={Colors.RED} />
+                </View>
+              </View>
+            </View>
+          </View>
+        </TouchableHighlight>
+      </SwipeableBlockedRow>
+    );
+  };
+
+  return (
+    <PageWrapper style={{ flex: 1, marginLeft: 0, marginRight: 0, gap: 12 }}>
+      <View style={styles.header}>
+        <View style={{ width: 30 }} />
+        <Text style={styles.title}>Messages</Text>
+        <TouchableOpacity
+          onPress={() => SheetManager.show('search-modal')}
+          style={{ width: 30, alignItems: 'flex-end' }}
+        >
+          <AddChatIcon />
+        </TouchableOpacity>
+      </View>
+
+      {/* <View style={[{ paddingHorizontal: '4%' }]}>
+        <Input
+          inputMode={'search'}
+          placeholder={'Search'}
+          onChange={(text) => searchFilter(text)}
+          value={search}
+          icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
+          height={38}
+        />
+      </View> */}
+
+      <HorizontalTabView
+        index={index}
+        setIndex={setIndex}
+        routes={routes}
+        tabBarStyle={{ paddingHorizontal: '4%' }}
+        renderScene={({ route }: { route: { key: keyof typeof filteredChats } }) =>
+          route.key === 'blocked' ? (
+            <FlashList
+              viewabilityConfig={{
+                waitForInteraction: true,
+                itemVisiblePercentThreshold: 50,
+                minimumViewTime: 1000
+              }}
+              data={filteredChats[route.key]}
+              renderItem={renderBlockedItem}
+              keyExtractor={(item, index) => `${item.id}-${index}`}
+              estimatedItemSize={50}
+            />
+          ) : (
+            <FlashList
+              viewabilityConfig={{
+                waitForInteraction: true,
+                itemVisiblePercentThreshold: 50,
+                minimumViewTime: 1000
+              }}
+              data={filteredChats[route.key]}
+              renderItem={renderChatItem}
+              keyExtractor={(item, index) => `${item.uid}-${index}`}
+              estimatedItemSize={78}
+              extraData={typingUsers}
+            />
+          )
+        }
+      />
+
+      <SearchModal />
+      <MoreModal />
+      <WarningModal
+        type={'delete'}
+        buttonTitle={isWarningModalVisible?.buttonTitle ?? 'Delete'}
+        isVisible={!!isWarningModalVisible}
+        onClose={() => setIsWarningModalVisible(null)}
+        title={isWarningModalVisible?.title}
+        message={isWarningModalVisible?.message}
+        action={isWarningModalVisible?.action}
+      />
+    </PageWrapper>
+  );
+};
+
+export default MessagesScreen;

+ 86 - 0
src/screens/InAppScreens/MessagesScreen/styles.tsx

@@ -0,0 +1,86 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({
+  title: { color: Colors.DARK_BLUE, fontFamily: 'redhat-700', fontSize: getFontSize(14) },
+  header: {
+    alignItems: 'center',
+    paddingVertical: 16,
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    marginLeft: '5%',
+    marginRight: '5%'
+  },
+  chatItem: {
+    flexDirection: 'row',
+    paddingVertical: 12,
+    backgroundColor: Colors.WHITE,
+    borderBottomWidth: 1,
+    borderBottomColor: Colors.FILL_LIGHT,
+    gap: 8,
+    paddingHorizontal: '4%'
+  },
+  avatar: {
+    width: 54,
+    height: 54,
+    borderRadius: 27,
+    borderWidth: 1,
+    borderColor: Colors.FILL_LIGHT
+  },
+  rowContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between'
+  },
+  chatName: {
+    flex: 1,
+    fontFamily: 'montserrat-700',
+    fontSize: getFontSize(14),
+    color: Colors.DARK_BLUE
+  },
+  chatMessage: {
+    flex: 1,
+    fontSize: getFontSize(12),
+    fontWeight: '600',
+    color: Colors.DARK_BLUE,
+    height: '100%'
+  },
+  chatTimeContainer: {
+    justifyContent: 'center',
+    alignItems: 'flex-end'
+  },
+  chatTime: {
+    fontSize: getFontSize(12),
+    fontWeight: '600',
+    color: Colors.LIGHT_GRAY
+  },
+  unreadBadge: {
+    backgroundColor: Colors.ORANGE,
+    borderRadius: 9,
+    paddingHorizontal: 6,
+    alignItems: 'center',
+    justifyContent: 'center',
+    height: 18,
+    minWidth: 18
+  },
+  unreadText: {
+    color: Colors.WHITE,
+    fontSize: getFontSize(11),
+    fontFamily: 'montserrat-700'
+  },
+  swipeActions: {
+    flexDirection: 'row',
+    alignItems: 'center'
+  },
+  swipeButton: {
+    paddingHorizontal: 20
+  },
+  typingText: {
+    flex: 1,
+    fontSize: getFontSize(12),
+    color: Colors.LIGHT_GRAY,
+    fontStyle: 'italic',
+    height: '100%'
+  },
+});

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

@@ -0,0 +1,87 @@
+import { IMessage } from 'react-native-gifted-chat';
+
+export type Chat = {
+  uid: number;
+  name: string;
+  avatar: string | null;
+  short: string;
+  sent_by: number;
+  updated: Date;
+  status: 1 | 2 | 3 | 4;
+  unread_count: number;
+  last_message_id: number;
+  pin: 0 | 1;
+  pin_order: number;
+  archive: 0 | 1;
+  archive_order: number;
+  attachement_name: string;
+  encrypted: 0 | 1;
+  muted: 0 | 1;
+};
+
+export type Blocked = {
+  id: number;
+  first_name: string;
+  last_name: string;
+  avatar: string | null;
+};
+
+export type ChatProps = {
+  uid: number;
+  name: string;
+  avatar: string | null;
+  pin: 0 | 1;
+  archive: 0 | 1;
+  muted: 0 | 1;
+};
+
+export type MessageSimple = {
+  id: number;
+  sender: number;
+  recipient: number;
+  text: string;
+  status: 1 | 2 | 3 | 4;
+  sent_datetime: Date;
+  received_datetime: Date | null;
+  read_datetime: Date | null;
+  reply_to_id: number; // -1 if not a reply
+  reactions: string; // JSON object '{}'
+  edits: string; // JSON object '{}'
+  attachement: any; // -1 if no attachment
+  encrypted: 0 | 1;
+};
+
+export type Message = MessageSimple & {
+  reply_to: MessageSimple;
+};
+
+export interface CustomMessage extends IMessage {
+  _id: number;
+  replyMessage?: {
+    text: string;
+    id: number;
+    name: string;
+  } | null;
+  deleted: boolean;
+  attachment: string | null;
+  reactions: Reaction[] | {};
+}
+
+export type Reaction = {
+  datetime: Date;
+  reaction: string;
+  uid: number;
+};
+
+export type Routes = {
+  key: 'all' | 'unread' | 'archived' | 'blocked';
+  title: string;
+  icon?: string;
+};
+
+export type WarningProps = {
+  title: string;
+  buttonTitle?: string;
+  message: string;
+  action: () => void;
+};

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

@@ -0,0 +1,119 @@
+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();
+  const today = moment().local();
+  const yesterday = moment().local().subtract(1, 'days');
+
+  if (inputDate.isSame(today, 'day')) {
+    return inputDate.format('HH:mm');
+  }
+  if (inputDate.isSame(yesterday, 'day')) {
+    return 'yesterday';
+  }
+  if (!inputDate.isSame(today, 'year')) {
+    return inputDate.format('DD.MM.YYYY');
+  }
+
+  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
+      })
+    });
+  };
+};

+ 4 - 0
src/screens/InAppScreens/ProfileScreen/Profile/edit-personal-info.tsx

@@ -44,6 +44,7 @@ import { ButtonVariants } from 'src/types/components';
 import { NAVIGATION_PAGES } from 'src/types';
 import { useDeleteUserMutation } from '@api/app';
 import { useNotification } from 'src/contexts/NotificationContext';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 const ProfileSchema = yup.object({
   username: yup.string().optional(),
@@ -74,6 +75,8 @@ export const EditPersonalInfo = () => {
 
   const { data, error } = usePostGetProfileQuery(String(token), true);
   const { updateNotificationStatus } = useNotification();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
+
 
   const regions = useGetRegionsWithFlagQuery(true);
   const [modalInfo, setModalInfo] = useState({
@@ -108,6 +111,7 @@ export const EditPersonalInfo = () => {
     storage.remove('visitedTilesUrl');
     storage.remove('filterSettings');
     updateNotificationStatus();
+    updateUnreadMessagesCount();
     navigation.dispatch(
       CommonActions.reset({
         index: 1,

+ 44 - 1
src/screens/InAppScreens/ProfileScreen/index.tsx

@@ -36,6 +36,7 @@ import UN150Icon from '../../../../assets/icons/un-150.svg';
 import ChevronIcon from '../../../../assets/icons/chevron-left.svg';
 import ShareIcon from '../../../../assets/icons/share.svg';
 import UnverifiedIcon from '../../../../assets/icons/unverified.svg';
+import CommentsIcon from '../../../../assets/icons/messages/comments.svg';
 
 import { ProfileStyles, ScoreStyles, TBTStyles } from '../TravellersScreen/Components/styles';
 import UnauthenticatedProfileScreen from './UnauthenticatedProfileScreen';
@@ -192,6 +193,39 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
     );
   };
 
+  const handleGoToChat = () => {
+    navigation.dispatch(
+      CommonActions.reset({
+        index: 1,
+        routes: [
+          {
+            name: 'DrawerApp',
+            state: {
+              routes: [
+                {
+                  name: NAVIGATION_PAGES.IN_APP_MESSAGES_TAB,
+                  state: {
+                    routes: [
+                      { name: NAVIGATION_PAGES.CHATS_LIST },
+                      {
+                        name: NAVIGATION_PAGES.CHAT,
+                        params: {
+                          id: route.params?.userId,
+                          name: data.user_data.first_name + ' ' + data.user_data.last_name,
+                          avatar: '/img/avatars/' + data.user_data.avatar
+                        }
+                      }
+                    ]
+                  }
+                }
+              ]
+            }
+          }
+        ]
+      })
+    );
+  };
+
   return (
     <PageWrapper>
       <Header label="Profile" />
@@ -327,7 +361,16 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
                     />
                   </TouchableOpacity>
                 </>
-              ) : null}
+              ) : (
+                <TouchableOpacity style={styles.settings} onPress={handleGoToChat}>
+                  <CommentsIcon
+                    width={20}
+                    height={20}
+                    fill={Colors.DARK_BLUE}
+                    style={{ alignSelf: 'center' }}
+                  />
+                </TouchableOpacity>
+              )}
             </View>
 
             {hasActiveLinks() && (

+ 2 - 1
src/screens/InAppScreens/TravellersScreen/index.tsx

@@ -19,6 +19,7 @@ import InfoIcon from 'assets/icons/info-solid.svg';
 import FriendsIcon from 'assets/icons/friends.svg';
 import BlinkingDot from 'src/components/BlinkingDot';
 import { useNotification } from 'src/contexts/NotificationContext';
+import { getFontSize } from 'src/utils';
 
 const TravellersScreen = () => {
   const navigation = useNavigation();
@@ -107,7 +108,7 @@ const styles = StyleSheet.create({
     fontSize: 13,
     fontWeight: 'bold'
   },
-  title: { color: Colors.DARK_BLUE, fontSize: 14, fontWeight: '600' },
+  title: { color: Colors.DARK_BLUE, fontFamily: 'redhat-700', fontSize: getFontSize(14) },
   header: {
     alignItems: 'center',
     paddingVertical: 16,

+ 2 - 1
src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/index.tsx

@@ -1,5 +1,6 @@
 import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { SafeAreaView, View, Text, Platform, TouchableOpacity } from 'react-native';
+import { View, Text, Platform, TouchableOpacity } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
 import MapView, { Geojson, UrlTile } from 'react-native-maps';
 import * as turf from '@turf/turf';
 import { Feature } from '@turf/turf';

+ 3 - 2
src/screens/InAppScreens/TravelsScreen/EarthScreen/index.tsx

@@ -1,5 +1,6 @@
 import React, { useEffect, useState } from 'react';
-import { View, Platform, SafeAreaView, Text, TouchableOpacity } from 'react-native';
+import { View, Platform, Text, StatusBar } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
 import MapView, { Geojson, UrlTile } from 'react-native-maps';
 
 import kye from '../../../../../assets/geojson/kye.json';
@@ -172,7 +173,7 @@ const EarthScreen = () => {
         )}
       </View>
 
-      <View style={styles.container}>
+      <View style={[styles.container, { paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 }]}>
         <MapView
           style={styles.map}
           showsMyLocationButton={false}

+ 2 - 5
src/screens/InAppScreens/TravelsScreen/SuggestSeriesScreen/index.tsx

@@ -1,16 +1,14 @@
 import React, { useEffect, useRef, useState } from 'react';
 import {
-  SafeAreaView,
   View,
   Text,
-  Platform,
   TouchableOpacity,
-  StatusBar,
   KeyboardAvoidingView,
   TouchableWithoutFeedback,
   Keyboard,
   ScrollView
 } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
 import MapView, { Marker } from 'react-native-maps';
 import ReactModal from 'react-native-modal';
 import axios from 'axios';
@@ -186,8 +184,7 @@ const SuggestSeriesScreen = ({ navigation }: { navigation: any }) => {
   return (
     <SafeAreaView
       style={{
-        height: '100%',
-        paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0
+        height: '100%'
       }}
     >
       <View style={styles.wrapper}>

+ 2 - 1
src/screens/InAppScreens/TravelsScreen/index.tsx

@@ -16,6 +16,7 @@ import TripIcon from '../../../../assets/icons/travels-section/trip.svg';
 import ImagesIcon from '../../../../assets/icons/travels-section/images.svg';
 import FixersIcon from '../../../../assets/icons/travels-section/fixers.svg';
 import InfoIcon from 'assets/icons/info-solid.svg';
+import { getFontSize } from 'src/utils';
 
 const TravelsScreen = () => {
   const [isModalVisible, setIsModalVisible] = useState(false);
@@ -111,7 +112,7 @@ const styles = StyleSheet.create({
     fontSize: 13,
     fontWeight: 'bold'
   },
-  title: { color: Colors.DARK_BLUE, fontSize: 14, fontWeight: '600' },
+  title: { color: Colors.DARK_BLUE, fontFamily: 'redhat-700', fontSize: getFontSize(14) },
   header: {
     alignItems: 'center',
     paddingVertical: 16,

+ 4 - 0
src/screens/LoginScreen/index.tsx

@@ -15,6 +15,7 @@ import { fetchAndSaveStatistics } from 'src/database/statisticsService';
 import { useNetInfo } from '@react-native-community/netinfo';
 import { useNotification } from 'src/contexts/NotificationContext';
 import { usePostGetProfileInfoDataQuery } from '@api/user';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 
 type Props = {
   navigation: NavigationProp<any>;
@@ -31,6 +32,8 @@ const LoginScreen: FC<Props> = ({ navigation }) => {
 
   const { data, mutate: userLogin } = useLoginMutation();
   const { updateNotificationStatus } = useNotification();
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
+
   const { data: profileData } = usePostGetProfileInfoDataQuery(
     data?.token || '',
     data?.uid ? +data.uid : 0,
@@ -51,6 +54,7 @@ const LoginScreen: FC<Props> = ({ navigation }) => {
       storage.set('uid', data.uid.toString());
       storage.set('isFirstLaunch', false);
       updateNotificationStatus();
+      updateUnreadMessagesCount();
       updateLocalData(data.token);
     }
   }, [data]);

+ 59 - 31
src/screens/NotificationsScreen/index.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import { View, Linking, Text, Switch, Platform, TouchableOpacity, AppState } from 'react-native';
 import * as Notifications from 'expo-notifications';
 
@@ -15,7 +15,7 @@ import PeopleIcon from 'assets/icons/notifications/people-group-solid.svg';
 import ChatIcon from 'assets/icons/notifications/messages.svg';
 import GearIcon from 'assets/icons/notifications/gear-solid.svg';
 import BellIcon from 'assets/icons/notifications/bell-solid.svg';
-import { useGetSettingsQuery } from '@api/notifications';
+import { useGetSettingsQuery, usePostSetSettingsMutation } from '@api/notifications';
 import { useFocusEffect } from '@react-navigation/native';
 
 const NotificationsScreen = ({ navigation }: { navigation: any }) => {
@@ -24,6 +24,7 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
 
   const { mutateAsync: saveNotificationToken } = usePostSaveNotificationTokenMutation();
   const { isSubscribed, toggleSubscription, unsubscribeFromNotifications } = usePushNotification();
+  const { mutateAsync: setNotificationsSettings } = usePostSetSettingsMutation();
   const [modalInfo, setModalInfo] = useState({
     visible: false,
     type: 'confirm',
@@ -34,6 +35,7 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
   const [initialPermissionStatus, setInitialPermissionStatus] = useState<
     'granted' | 'denied' | 'undetermined' | null
   >(null);
+  const [isEnabledMessages, setIsEnabledMessages] = useState(false);
 
   const closeModal = () => {
     setModalInfo({ ...modalInfo, visible: false });
@@ -42,16 +44,26 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
   useEffect(() => {
     if (notificationsSettings) {
       const { settings } = notificationsSettings;
-      const isServerSubscribed =
-        Platform.OS === 'ios'
-          ? settings.some((setting) => setting.name === 'app-ios' && setting.active === 1)
-          : settings.some((setting) => setting.name === 'app-android' && setting.active === 1);
-      // if (isServerSubscribed !== isSubscribed) {
-      //   toggleSubscription();
-      // }
+      setIsEnabledMessages(
+        settings.some((setting) => setting.name === 'app-messages' && setting.active === 1)
+      );
     }
   }, [notificationsSettings]);
 
+  const handleSubscribe = async () => {
+    const deviceData = await registerForPushNotificationsAsync();
+
+    if (deviceData?.notificationToken) {
+      toggleSubscription();
+      await saveNotificationToken({
+        token,
+        platform: deviceData.platform,
+        n_token: deviceData.notificationToken
+      });
+      refetchData();
+    }
+  };
+
   useEffect(() => {
     const subscription = AppState.addEventListener('change', async (nextAppState) => {
       if (nextAppState === 'active' && initialPermissionStatus !== null) {
@@ -120,20 +132,6 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
     }
   };
 
-  const handleSubscribe = async () => {
-    const deviceData = await registerForPushNotificationsAsync();
-
-    if (deviceData?.notificationToken) {
-      toggleSubscription();
-      await saveNotificationToken({
-        token,
-        platform: deviceData.platform,
-        n_token: deviceData.notificationToken
-      });
-      refetchData();
-    }
-  };
-
   async function registerForPushNotificationsAsync() {
     const existingStatus = await checkNotificationPermissions();
     let finalStatus = existingStatus;
@@ -168,6 +166,17 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
     return { notificationToken: deviceData.data ?? '', platform: deviceData.type ?? '' };
   }
 
+  const handleToggleMessages = async () => {
+    const dataToUpdate = { 'app-messages': isEnabledMessages ? 0 : 1 };
+    setIsEnabledMessages(!isEnabledMessages);
+
+    await setNotificationsSettings({
+      token,
+      settings: JSON.stringify(dataToUpdate)
+    });
+    refetchData();
+  };
+
   return (
     <PageWrapper>
       <Header label="Notifications" />
@@ -229,19 +238,38 @@ const NotificationsScreen = ({ navigation }: { navigation: any }) => {
         disabled={!isSubscribed}
       />
 
-      {/* <MenuButton
-        label="Messages"
-        icon={
+      <TouchableOpacity
+        style={[
+          styles.alignStyle,
+          styles.buttonWrapper,
+          {
+            justifyContent: 'space-between'
+          }
+        ]}
+        onPress={handleToggleMessages}
+        disabled={!isSubscribed}
+      >
+        <View style={styles.alignStyle}>
           <ChatIcon
             fill={isSubscribed ? Colors.DARK_BLUE : Colors.LIGHT_GRAY}
             width={20}
             height={20}
           />
-        }
-        red={false}
-        buttonFn={() => navigation.navigate(NAVIGATION_PAGES.MESSAGES_NOTIFICATIONS as never)}
-        disabled={!isSubscribed}
-      /> */}
+          <Text style={[styles.buttonLabel, !isSubscribed ? { color: Colors.LIGHT_GRAY } : {}]}>
+            Messages
+          </Text>
+        </View>
+        <View>
+          <Switch
+            trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+            thumbColor={Colors.WHITE}
+            onValueChange={handleToggleMessages}
+            value={isEnabledMessages && isSubscribed}
+            style={{ transform: 'scale(0.8)' }}
+            disabled={!isSubscribed}
+          />
+        </View>
+      </TouchableOpacity>
 
       {/* <MenuButton
         label="System"

+ 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
                     ? {

+ 5 - 3
src/screens/WelcomeScreen/index.tsx

@@ -1,5 +1,6 @@
 import { FC } from 'react';
-import { ImageBackground, SafeAreaView, View, Text } from 'react-native';
+import { ImageBackground, View, Text, StatusBar } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
 import type { NavigationProp } from '@react-navigation/native';
 
 import { Button } from '../../components/';
@@ -14,9 +15,10 @@ type Props = {
 
 const WelcomeScreen: FC<Props> = ({ navigation }) => {
   return (
-    <View>
+    <View style={{ flex: 1 }}>
+      <StatusBar translucent backgroundColor="transparent" />
       <ImageBackground
-        style={{ height: '100%' }}
+        style={{ flex: 1 }}
         source={require('../../../assets/images/welcome-background.png')}
       >
         <View style={styles.overlay} />

+ 16 - 0
src/stores/chatStore.ts

@@ -0,0 +1,16 @@
+import { create } from 'zustand';
+import { ChatProps, WarningProps } from 'src/screens/InAppScreens/MessagesScreen/types';
+
+interface ChatStore {
+  selectedChat: ChatProps | null;
+  setSelectedChat: (chat: ChatProps | null) => void;
+  isWarningModalVisible: WarningProps | null;
+  setIsWarningModalVisible: (value: WarningProps | null) => void;
+}
+
+export const useChatStore = create<ChatStore>((set) => ({
+  selectedChat: null,
+  setSelectedChat: (chat) => set({ selectedChat: chat }),
+  isWarningModalVisible: null,
+  setIsWarningModalVisible: (value) => set({ isWarningModalVisible: value })
+}));

+ 31 - 0
src/stores/unreadMessagesStore.ts

@@ -0,0 +1,31 @@
+import { fetchUnreadMessagesCount } from '@api/chat';
+import { storage, StoreType } from 'src/storage';
+import { create } from 'zustand';
+
+interface MessagesState {
+  unreadMessagesCount: number;
+  setUnreadMessagesCount: (count: number) => void;
+  updateUnreadMessagesCount: () => Promise<void>;
+}
+
+export const useMessagesStore = create<MessagesState>((set) => ({
+  unreadMessagesCount: (storage.get('unreadMessagesCount', StoreType.NUMBER) as number) ?? 0,
+  setUnreadMessagesCount: (count: number) => set({ unreadMessagesCount: count }),
+  updateUnreadMessagesCount: async () => {
+    const token = storage.get('token', StoreType.STRING);
+    if (token) {
+      try {
+        const messagesData = await fetchUnreadMessagesCount(token as string);
+        if (messagesData && typeof messagesData.unread_conversations !== 'undefined') {
+          set({ unreadMessagesCount: messagesData.unread_conversations });
+          storage.set('unreadMessagesCount', messagesData.unread_conversations);
+        }
+      } catch (error) {
+        console.error('Failed to fetch unread conversations count', error);
+      }
+    } else {
+      set({ unreadMessagesCount: 0 });
+      storage.set('unreadMessagesCount', 0);
+    }
+  }
+}));

+ 36 - 3
src/types/api.ts

@@ -21,7 +21,8 @@ export enum API_ROUTE {
   FRIENDS = 'friends',
   COUNTRIES = 'countries',
   FIXERS = 'fixers',
-  NOTIFICATIONS = 'notifications'
+  NOTIFICATIONS = 'notifications',
+  CHAT = 'chat'
 }
 
 export enum API_ENDPOINT {
@@ -123,7 +124,23 @@ export enum API_ENDPOINT {
   EDIT_FIXER = 'edit-fixer',
   GET_UPDATE = 'get-update',
   GET_NOTIFICATIONS_SETTINGS = 'get-settings',
-  SET_NOTIFICATIONS_SETTINGS = 'set-settings'
+  SET_NOTIFICATIONS_SETTINGS = 'set-settings',
+  SEARCH_USERS = 'search-users',
+  GET_CHATS_LIST = 'get-conversation-list',
+  GET_CHAT_WITH = 'get-conversation-with',
+  SEND_MESSAGE = 'send-message',
+  MESSAGES_RECEIVED = 'messages-received',
+  MESSAGES_READ = 'messages-read',
+  REACT_TO_MESSAGE = 'react-to-message',
+  DELETE_MESSAGE = 'delete-message',
+  SET_PIN = 'set-pin',
+  SET_ARCHIVE = 'set-archive',
+  SET_BLOCK = 'set-block',
+  SET_MUTE = 'set-mute',
+  DELETE_CHAT = 'delete-conversation',
+  UNREACT_TO_MESSAGE = 'unreact-to-message',
+  GET_BLOCKED = 'get-blocked',
+  GET_UNREAD_MESSAGES_PRESENT = 'new-messages-present'
 }
 
 export enum API {
@@ -224,7 +241,23 @@ export enum API {
   EDIT_FIXER = `${API_ROUTE.FIXERS}/${API_ENDPOINT.EDIT_FIXER}`,
   GET_UPDATE = `${API_ROUTE.PROFILE}/${API_ENDPOINT.GET_UPDATE}`,
   GET_NOTIFICATIONS_SETTINGS = `${API_ROUTE.NOTIFICATIONS}/${API_ENDPOINT.GET_NOTIFICATIONS_SETTINGS}`,
-  SET_NOTIFICATIONS_SETTINGS = `${API_ROUTE.NOTIFICATIONS}/${API_ENDPOINT.SET_NOTIFICATIONS_SETTINGS}`
+  SET_NOTIFICATIONS_SETTINGS = `${API_ROUTE.NOTIFICATIONS}/${API_ENDPOINT.SET_NOTIFICATIONS_SETTINGS}`,
+  SEARCH_USERS = `${API_ROUTE.CHAT}/${API_ENDPOINT.SEARCH_USERS}`,
+  GET_CHATS_LIST = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_CHATS_LIST}`,
+  GET_CHAT_WITH = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_CHAT_WITH}`,
+  SEND_MESSAGE = `${API_ROUTE.CHAT}/${API_ENDPOINT.SEND_MESSAGE}`,
+  MESSAGES_RECEIVED = `${API_ROUTE.CHAT}/${API_ENDPOINT.MESSAGES_RECEIVED}`,
+  MESSAGES_READ = `${API_ROUTE.CHAT}/${API_ENDPOINT.MESSAGES_READ}`,
+  REACT_TO_MESSAGE = `${API_ROUTE.CHAT}/${API_ENDPOINT.REACT_TO_MESSAGE}`,
+  DELETE_MESSAGE = `${API_ROUTE.CHAT}/${API_ENDPOINT.DELETE_MESSAGE}`,
+  SET_PIN = `${API_ROUTE.CHAT}/${API_ENDPOINT.SET_PIN}`,
+  SET_ARCHIVE = `${API_ROUTE.CHAT}/${API_ENDPOINT.SET_ARCHIVE}`,
+  SET_BLOCK = `${API_ROUTE.CHAT}/${API_ENDPOINT.SET_BLOCK}`,
+  SET_MUTE = `${API_ROUTE.CHAT}/${API_ENDPOINT.SET_MUTE}`,
+  DELETE_CHAT = `${API_ROUTE.CHAT}/${API_ENDPOINT.DELETE_CHAT}`,
+  UNREACT_TO_MESSAGE = `${API_ROUTE.CHAT}/${API_ENDPOINT.UNREACT_TO_MESSAGE}`,
+  GET_BLOCKED = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_BLOCKED}`,
+  GET_UNREAD_MESSAGES_PRESENT = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_UNREAD_MESSAGES_PRESENT}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 3 - 0
src/types/navigation.ts

@@ -67,4 +67,7 @@ export enum NAVIGATION_PAGES {
   FRIENDS_NOTIFICATIONS = 'inAppFriendsNotifications',
   MESSAGES_NOTIFICATIONS = 'inAppMessagesNotifications',
   SYSTEM_NOTIFICATIONS = 'inAppSystemNotifications',
+  IN_APP_MESSAGES_TAB = 'Messages',
+  CHATS_LIST = 'inAppChatsList',
+  CHAT = 'inAppChat'
 }

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio