Viktoriia 9 months ago
parent
commit
75c5460604
33 changed files with 2233 additions and 12 deletions
  1. 16 1
      Route.tsx
  2. 14 3
      app.config.ts
  3. 3 0
      assets/icons/messages/archive.svg
  4. 4 0
      assets/icons/messages/chat-plus.svg
  5. 4 0
      assets/icons/messages/check-read.svg
  6. 3 0
      assets/icons/messages/check-unread.svg
  7. 3 0
      assets/icons/messages/dots.svg
  8. 3 0
      assets/icons/messages/pin.svg
  9. 13 0
      package.json
  10. 5 3
      src/components/HorizontalTabView/index.tsx
  11. 3 0
      src/components/TabBarButton/index.tsx
  12. 80 0
      src/modules/api/chat/chat-api.ts
  13. 11 0
      src/modules/api/chat/chat-query-keys.tsx
  14. 3 0
      src/modules/api/chat/index.ts
  15. 4 0
      src/modules/api/chat/queries/index.ts
  16. 17 0
      src/modules/api/chat/queries/use-post-get-conversation-list.tsx
  17. 28 0
      src/modules/api/chat/queries/use-post-get-conversation-with.tsx
  18. 17 0
      src/modules/api/chat/queries/use-post-search-users.tsx
  19. 17 0
      src/modules/api/chat/queries/use-post-send-message.tsx
  20. 969 0
      src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx
  21. 0 0
      src/screens/InAppScreens/MessagesScreen/ChatScreen/styles.tsx
  22. 99 0
      src/screens/InAppScreens/MessagesScreen/Components/ChatMessageBox.tsx
  23. 130 0
      src/screens/InAppScreens/MessagesScreen/Components/MoreModal.tsx
  24. 96 0
      src/screens/InAppScreens/MessagesScreen/Components/ReplyMessageBar.tsx
  25. 146 0
      src/screens/InAppScreens/MessagesScreen/Components/SearchUsersModal.tsx
  26. 144 0
      src/screens/InAppScreens/MessagesScreen/Components/SwipeableRow.tsx
  27. 339 0
      src/screens/InAppScreens/MessagesScreen/index.tsx
  28. 0 0
      src/screens/InAppScreens/MessagesScreen/styles.tsx
  29. 43 0
      src/screens/InAppScreens/MessagesScreen/types.ts
  30. 2 1
      src/screens/InAppScreens/TravellersScreen/index.tsx
  31. 2 1
      src/screens/InAppScreens/TravelsScreen/index.tsx
  32. 12 3
      src/types/api.ts
  33. 3 0
      src/types/navigation.ts

+ 16 - 1
Route.tsx

@@ -90,6 +90,8 @@ 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';
 
 enableScreens();
 
@@ -232,7 +234,7 @@ const Route = () => {
     cardStyle: { backgroundColor: 'white' },
     unmountOnBlur: true,
     gestureEnabled: Platform.OS === 'ios' ? true : false,
-    lazy: true
+    lazy: false,
   });
 
   const regionViewScreenOptions = {
@@ -407,6 +409,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}>

+ 14 - 3
app.config.ts

@@ -68,7 +68,10 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       NSPhotoLibraryAddUsageDescription:
         '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'
+        'This will allow NomadMania.com to send you notifications. Also you can disable it in app settings',
+      NSMicrophoneUsageDescription: "Nomadmania app needs access to the microphone to record audio.",
+      NSDocumentsFolderUsageDescription: "Nomadmania app needs access to the documents folder to select files.",
+      NSCameraUsageDescription: "Nomadmania app needs access to the camera to record video."
     },
     privacyManifests: {
       NSPrivacyAccessedAPITypes: [
@@ -96,7 +99,9 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       'NOTIFICATIONS',
       'USER_FACING_NOTIFICATIONS',
       'INTERNET',
-      'CAMERA'
+      'CAMERA',
+      "RECORD_AUDIO",
+      'MODIFY_AUDIO_SETTINGS'
     ],
     versionCode: 70 // next version submitted to Google Play needs to be higher than that 2.0.17
   },
@@ -126,6 +131,12 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
         url: 'https://sentry.io/'
       }
     ],
-    ['expo-asset', 'expo-font']
+    ['expo-asset', '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>

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

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

+ 13 - 0
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",
@@ -48,14 +52,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.6",
+    "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",
@@ -65,12 +76,14 @@
     "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-walkthrough-tooltip": "^1.6.0",
     "yup": "^1.3.3",

+ 5 - 3
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';
@@ -17,7 +17,8 @@ export const HorizontalTabView = ({
   onDoubleClick,
   lazy = false,
   withNotification = false,
-  maxTabHeight
+  maxTabHeight,
+  tabBarStyle = {}
 }: {
   index: number;
   setIndex: React.Dispatch<React.SetStateAction<number>>;
@@ -28,6 +29,7 @@ export const HorizontalTabView = ({
   lazy?: boolean;
   withNotification?: boolean;
   maxTabHeight?: number;
+  tabBarStyle?: StyleProp<ViewStyle>;
 }) => {
   const renderTabBar = (props: any) => (
     <TabBar
@@ -50,7 +52,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/TabBarButton/index.tsx

@@ -7,6 +7,7 @@ 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';
@@ -21,6 +22,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:

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

@@ -0,0 +1,80 @@
+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
+export interface PostGetChatsListReturn extends ResponseType {
+  conversations: {
+    uid: number;
+    name: string;
+    avatar: string | null;
+    short: string;
+    sent_by: number;
+    updated: Date;
+    status: 1 | 2 | 3;
+    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;
+  }[];
+}
+
+interface Message {
+  id: number;
+  sender: number;
+  recipient: number;
+  text: string;
+  status: 1 | 2 | 3;
+  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 {
+  to_uid: number;
+  text: string;
+}
+
+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<ResponseType>(API.SEND_MESSAGE, data)
+};

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

@@ -0,0 +1,11 @@
+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
+};

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

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

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

@@ -0,0 +1,4 @@
+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';

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

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

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

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

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

@@ -0,0 +1,969 @@
+import React, { useState, useCallback, useEffect, useRef } from 'react';
+import {
+  View,
+  TouchableOpacity,
+  Image,
+  Modal,
+  StyleSheet,
+  Text,
+  FlatList,
+  Dimensions,
+  Alert,
+  ScrollView
+} from 'react-native';
+import {
+  GiftedChat,
+  Bubble,
+  InputToolbar,
+  Actions,
+  IMessage,
+  Send,
+  BubbleProps,
+  QuickRepliesProps,
+  Composer
+} from 'react-native-gifted-chat';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import EmojiSelector from 'react-native-emoji-selector';
+import * as ImagePicker from 'expo-image-picker';
+import { useActionSheet } from '@expo/react-native-action-sheet';
+import { ActionSheetProvider } from '@expo/react-native-action-sheet';
+import {
+  GestureHandlerRootView,
+  LongPressGestureHandler,
+  Swipeable
+} from 'react-native-gesture-handler';
+import { Header, PageWrapper } 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 Animated, {
+  FadeIn,
+  FadeOut,
+  SlideInDown,
+  SlideOutDown,
+  useSharedValue,
+  withTiming
+} from 'react-native-reanimated';
+import { BlurView } from 'expo-blur';
+import { 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 { usePostGetChatWithQuery, usePostSendMessageMutation } from '@api/chat';
+import { Message } from '../types';
+import { API_HOST } from 'src/constants';
+
+const options = {
+  enableVibrateFallback: true,
+  ignoreAndroidSystemSettings: false
+};
+
+const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭'];
+
+const ChatScreen = ({ route }: { route: any }) => {
+  const { id, name, avatar }: { id: number; name: string; avatar: string | null } = route.params;
+  const currentUserId = storage.get('uid', StoreType.STRING) as number;
+  const token = storage.get('token', StoreType.STRING) as string;
+  const insets = useSafeAreaInsets();
+  const [messages, setMessages] = useState<IMessage[]>([]);
+  const { showActionSheetWithOptions } = useActionSheet();
+  const navigation = useNavigation();
+  const { data: chatData, isFetching, refetch } = usePostGetChatWithQuery(token, id, -1, -1, true);
+  const { mutateAsync: sendMessage } = usePostSendMessageMutation();
+
+  const swipeableRowRef = useRef<Swipeable | null>(null);
+  const messageContainerRef = useRef<FlatList<IMessage> | null>(null);
+  const [selectedMedia, setSelectedMedia] = useState(null);
+
+  const [replyMessage, setReplyMessage] = useState<IMessage | null>(null);
+
+  const [selectedMessage, setSelectedMessage] = useState<IMessage | null>(null);
+  const [showReactions, setShowReactions] = useState<number | 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 messageRefs = useRef<{ [key: string]: any }>({});
+  const scrollY = useSharedValue(0);
+
+  const mapApiMessageToGiftedMessage = (message: Message): IMessage => {
+    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 } : 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
+    };
+  };
+
+  useFocusEffect(
+    useCallback(() => {
+      if (chatData?.messages) {
+        const mappedMessages = chatData.messages.map(mapApiMessageToGiftedMessage);
+        setMessages(mappedMessages);
+      }
+    }, [chatData])
+  );
+
+  const clearReplyMessage = () => setReplyMessage(null);
+
+  const handleLongPress = (message: IMessage, props) => {
+    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 < 150) {
+          const extraShift = 150 - spaceBelow;
+          finalY -= extraShift;
+        }
+
+        if (spaceAbove < 50) {
+          const extraShift = 50 - spaceAbove;
+          finalY += extraShift;
+        }
+
+        if (spaceBelow < 150 || 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 = () => {
+    setEmojiSelectorVisible(true);
+    trigger('impactLight', options);
+  };
+
+  const closeEmojiSelector = () => {
+    setEmojiSelectorVisible(false);
+  };
+
+  const handleReactionPress = (emoji: string, messageId: number) => {
+    if (emoji === '+') {
+      openEmojiSelector();
+    } else {
+      // addReaction(messageId, emoji);
+    }
+  };
+
+  const handleOptionPress = (option: string) => {
+    switch (option) {
+      case 'reply':
+        Alert.alert(option);
+        break;
+      case 'copy':
+        Clipboard.setString(selectedMessage?.currentMessage?.text ?? '');
+        Alert.alert('copied');
+        break;
+      case 'delete':
+        setMessages((prevMessages) =>
+          prevMessages.filter((msg) => msg._id !== selectedMessage?.currentMessage?._id)
+        );
+        Alert.alert('deleted');
+        break;
+      case 'pin':
+        Alert.alert(option);
+        break;
+      default:
+        break;
+    }
+    closeEmojiSelector();
+  };
+
+  const renderReactionsBar = () =>
+    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 // + padding
+          }
+        ]}
+      >
+        {reactionEmojis.map((emoji) => (
+          <TouchableOpacity
+            key={emoji}
+            onPress={() => handleReactionPress(emoji, selectedMessage?.currentMessage?._id)}
+          >
+            <Text style={styles.reactionEmoji}>{emoji}</Text>
+          </TouchableOpacity>
+        ))}
+        <TouchableOpacity
+          onPress={() => openEmojiSelector()}
+          style={{
+            alignItems: 'center',
+            justifyContent: 'center',
+            backgroundColor: Colors.FILL_LIGHT,
+            borderRadius: 15,
+            borderWidth: 1,
+            borderColor: Colors.BORDER_LIGHT,
+            width: 30,
+            height: 30
+          }}
+        >
+          <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />
+        </TouchableOpacity>
+      </Animated.View>
+    );
+
+  const renderOptionsMenu = () =>
+    selectedMessage &&
+    messagePosition && (
+      <Animated.View
+        entering={FadeIn}
+        exiting={FadeOut}
+        style={[
+          styles.optionsMenu,
+          {
+            top: messagePosition.y + messagePosition.height + 10,
+            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 renderSelectedMessage = () =>
+    selectedMessage && (
+      <Animated.View
+        style={{
+          maxHeight: '80%',
+          position: 'absolute',
+          top: messagePosition?.y,
+          left: messagePosition?.x
+        }}
+      >
+        <ScrollView>
+          <Bubble
+            {...selectedMessage}
+            currentMessage={selectedMessage?.currentMessage}
+            wrapperStyle={{
+              right: { backgroundColor: Colors.DARK_BLUE },
+              left: { backgroundColor: Colors.FILL_LIGHT }
+            }}
+            textStyle={{
+              right: { color: Colors.FILL_LIGHT },
+              left: { color: Colors.DARK_BLUE }
+            }}
+            renderTicks={(message: IMessage) => {
+              return message.user._id === +currentUserId && message.received ? (
+                <View style={{ paddingRight: 8, bottom: 2 }}>
+                  <MaterialCommunityIcons name="check-all" size={16} color={Colors.WHITE} />
+                </View>
+              ) : message.user._id === +currentUserId && message.sent ? (
+                <View style={{ paddingRight: 8, bottom: 2 }}>
+                  <MaterialCommunityIcons name="check" size={16} color={Colors.WHITE} />
+                </View>
+              ) : message.user._id === +currentUserId && message.pending ? (
+                <View style={{ paddingRight: 8, bottom: 2 }}>
+                  <MaterialCommunityIcons name="check" size={16} color={Colors.WHITE} />
+                </View>
+              ) : null;
+            }}
+          />
+        </ScrollView>
+      </Animated.View>
+    );
+
+  const handleBackgroundPress = () => {
+    setIsModalVisible(false);
+    setSelectedMessage(null);
+    closeEmojiSelector();
+  };
+
+  useEffect(() => {
+    navigation?.getParent()?.setOptions({
+      tabBarStyle: {
+        display: 'none'
+      }
+    });
+  }, [navigation]);
+
+  useEffect(() => {
+    const intervalId = setInterval(() => {
+      refetch();
+    }, 5000);
+
+    return () => clearInterval(intervalId);
+  }, [refetch]);
+
+  const onSend = useCallback(
+    (newMessages: IMessage[] = []) => {
+      if (replyMessage) {
+        newMessages[0].replyMessage = {
+          text: replyMessage.text
+        };
+      }
+      const message = { ...newMessages[0], pending: true };
+
+      sendMessage(
+        { to_uid: id, text: message.text },
+        {
+          onSuccess: (res) => console.log('res', res),
+          onError: (err) => console.log('err', err)
+        }
+      );
+
+      setMessages((previousMessages) => GiftedChat.append(previousMessages, [message]));
+      clearReplyMessage();
+    },
+    [replyMessage]
+  );
+
+  const openActionSheet = () => {
+    const options = ['Open Camera', 'Select from gallery', 'Cancel'];
+    const cancelButtonIndex = 2;
+
+    showActionSheetWithOptions(
+      {
+        options,
+        cancelButtonIndex
+      },
+      async (buttonIndex) => {
+        if (buttonIndex === 0) {
+          openCamera();
+        } else if (buttonIndex === 1) {
+          openGallery();
+        }
+      }
+    );
+  };
+
+  const openCamera = async () => {
+    const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
+
+    if (permissionResult.granted === false) {
+      alert('Permission denied to access camera');
+      return;
+    }
+
+    const result = await ImagePicker.launchCameraAsync({
+      mediaTypes: ImagePicker.MediaTypeOptions.All,
+      quality: 1,
+      allowsEditing: true
+    });
+
+    if (!result.canceled) {
+      const newMedia = {
+        _id: Date.now().toString(),
+        createdAt: new Date(),
+        user: { _id: +currentUserId, name: 'Me' },
+        image: result.assets[0].type === 'image' ? result.assets[0].uri : null,
+        video: result.assets[0].type === 'video' ? result.assets[0].uri : null
+      };
+      setMessages((previousMessages) => GiftedChat.append(previousMessages, [newMedia as any]));
+    }
+  };
+
+  const openGallery = async () => {
+    const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
+
+    if (permissionResult.granted === false) {
+      alert('Denied');
+      return;
+    }
+
+    const result = await ImagePicker.launchImageLibraryAsync({
+      mediaTypes: ImagePicker.MediaTypeOptions.All,
+      allowsMultipleSelection: true,
+      quality: 1
+    });
+
+    if (!result.canceled && result.assets) {
+      const imageMessages = result.assets.map((asset) => ({
+        _id: Date.now().toString() + asset.uri,
+        createdAt: new Date(),
+        user: { _id: +currentUserId, name: 'Me' },
+        image: asset.type === 'image' ? asset.uri : null,
+        video: asset.type === 'video' ? asset.uri : null
+      }));
+
+      setMessages((previousMessages) =>
+        GiftedChat.append(previousMessages, imageMessages as IMessage[])
+      );
+    }
+  };
+
+  const renderMessageVideo = (props: any) => {
+    const { currentMessage } = props;
+
+    if (currentMessage.video) {
+      return (
+        <LongPressGestureHandler onHandlerStateChange={(event) => handleLongPress(currentMessage)}>
+          <TouchableOpacity
+            onPress={() => setSelectedMedia(currentMessage.video)}
+            style={styles.mediaContainer}
+          >
+            <Video
+              source={{ uri: currentMessage.video }}
+              style={styles.chatMedia}
+              useNativeControls
+            />
+          </TouchableOpacity>
+        </LongPressGestureHandler>
+      );
+    }
+
+    return null;
+  };
+
+  const addReaction = (messageId: number, reaction: any) => {
+    const updatedMessages = messages.map((msg: any) => {
+      if (msg._id === messageId) {
+        return {
+          ...msg,
+          reactions: [...(msg.reactions ?? []), reaction]
+        };
+      }
+      return msg;
+    });
+    setMessages(updatedMessages);
+    setShowReactions(null);
+  };
+
+  const updateRowRef = useCallback(
+    (ref: any) => {
+      if (
+        ref &&
+        replyMessage &&
+        ref.props.children.props.currentMessage?._id === replyMessage._id
+      ) {
+        swipeableRowRef.current = ref;
+      }
+    },
+    [replyMessage]
+  );
+
+  const renderReplyMessageView = (props: BubbleProps<IMessage>) =>
+    props.currentMessage &&
+    props.currentMessage?.replyMessage && (
+      <View style={styles.replyMessageContainer}>
+        <Text>{props.currentMessage.replyMessage.text}</Text>
+        <View style={styles.replyMessageDivider} />
+      </View>
+    );
+
+  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 renderBubble = (props: any) => {
+    const { currentMessage } = props;
+
+    return (
+      <View
+        ref={(ref) => {
+          if (ref && currentMessage) {
+            messageRefs.current[currentMessage._id] = ref;
+          }
+        }}
+        collapsable={false}
+      >
+        <Bubble
+          key={`${currentMessage._id}`}
+          {...props}
+          wrapperStyle={{
+            right: {
+              backgroundColor: Colors.DARK_BLUE
+            },
+            left: {
+              backgroundColor: Colors.FILL_LIGHT
+            }
+          }}
+          textStyle={{
+            left: {
+              color: Colors.DARK_BLUE
+            },
+            right: {
+              color: Colors.FILL_LIGHT
+            }
+          }}
+          onLongPress={() => handleLongPress(currentMessage, props)}
+          renderTicks={(message: IMessage) => {
+            return message.user._id === +currentUserId && message.received ? (
+              <View style={{ paddingRight: 8, bottom: 2 }}>
+                <MaterialCommunityIcons name="check-all" size={16} color={Colors.WHITE} />
+              </View>
+            ) : message.user._id === +currentUserId && message.sent ? (
+              <View style={{ paddingRight: 8, bottom: 2 }}>
+                <MaterialCommunityIcons name="check" size={16} color={Colors.WHITE} />
+              </View>
+            ) : message.user._id === +currentUserId && message.pending ? (
+              <View style={{ paddingRight: 8, bottom: 2 }}>
+                <MaterialCommunityIcons name="check" size={16} color={Colors.LIGHT_GRAY} />
+              </View>
+            ) : null;
+          }}
+          renderQuickReplies={(quickReplies: QuickRepliesProps<IMessage>) => null}
+          // renderQuickReplies={(quickReplies: QuickRepliesProps<IMessage>) => (
+          //   <View style={{height: 20, width: 20, backgroundColor: 'green', bottom: 0, right: 0}}>
+          //     <Text>{currentMessage.quickReplies.values[0].title}</Text>
+          //   </View>
+
+          // )}
+        />
+      </View>
+    );
+  };
+
+  const renderInputToolbar = (props: any) => (
+    <InputToolbar
+      {...props}
+      renderActions={() => (
+        <Actions
+          icon={() => <MaterialCommunityIcons name="plus" size={28} color={Colors.DARK_BLUE} />}
+          // onPressActionButton={openActionSheet}
+        />
+      )}
+    />
+  );
+
+  const renderEmojiSelector = () => (
+    <Animated.View
+      entering={SlideInDown}
+      exiting={SlideOutDown}
+      style={styles.emojiSelectorContainer}
+    >
+      <EmojiSelector
+        onEmojiSelected={(emoji) => {
+          addReaction(selectedMessage?._id, emoji);
+          closeEmojiSelector();
+        }}
+        showSearchBar={true}
+        columns={8}
+      />
+      <TouchableOpacity style={styles.closeModalButton} onPress={closeEmojiSelector}>
+        <MaterialCommunityIcons name="close" size={30} color={Colors.DARK_BLUE} />
+      </TouchableOpacity>
+    </Animated.View>
+  );
+
+  if (!messages.length) return null;
+
+  return (
+    <PageWrapper style={{ marginLeft: 0, marginRight: 0 }}>
+      <View style={{ paddingHorizontal: '5%' }}>
+        <Header
+          label={name}
+          rightElement={
+            <Image
+              source={{ uri: API_HOST + avatar }}
+              style={{
+                width: 30,
+                height: 30,
+                borderRadius: 15,
+                borderWidth: 1,
+                borderColor: Colors.FILL_LIGHT
+              }}
+            />
+          }
+        />
+      </View>
+
+      <GestureHandlerRootView style={styles.container}>
+        <GiftedChat
+          messages={messages}
+          listViewProps={{
+            showsVerticalScrollIndicator: false,
+            initialNumToRender: 20
+          }}
+          onSend={(newMessages: IMessage[]) => onSend(newMessages)}
+          user={{ _id: +currentUserId, name: 'Me' }}
+          renderBubble={renderBubble}
+          renderMessageImage={renderMessageImage}
+          renderInputToolbar={renderInputToolbar}
+          messageContainerRef={messageContainerRef}
+          renderSend={(props) => (
+            <View
+              style={{
+                flexDirection: 'row',
+                height: 44,
+                alignItems: 'center',
+                justifyContent: 'center',
+                gap: 14,
+                paddingHorizontal: 14
+              }}
+            >
+              {props.text?.trim() && (
+                <Send
+                  {...props}
+                  containerStyle={{
+                    justifyContent: 'center'
+                  }}
+                >
+                  <MaterialCommunityIcons name="send" size={28} color={Colors.DARK_BLUE} />
+                </Send>
+              )}
+              {!props.text?.trim() && (
+                <>
+                  {/* <MaterialCommunityIcons
+                    name="microphone-outline"
+                    size={28}
+                    color={Colors.DARK_BLUE}
+                  />
+
+                  <MaterialCommunityIcons
+                    name="camera-outline"
+                    size={28}
+                    color={Colors.DARK_BLUE}
+                  /> */}
+                  <MaterialCommunityIcons name="send" size={28} color={Colors.LIGHT_GRAY} />
+                </>
+              )}
+            </View>
+          )}
+          textInputProps={styles.composer}
+          placeholder=""
+          renderMessage={(props) => (
+            <ChatMessageBox
+              {...props}
+              updateRowRef={updateRowRef}
+              setReplyOnSwipeOpen={setReplyMessage}
+            />
+          )}
+          renderChatFooter={() => (
+            <ReplyMessageBar clearReply={clearReplyMessage} message={replyMessage} />
+          )}
+          renderCustomView={renderReplyMessageView}
+          renderMessageVideo={renderMessageVideo}
+          renderAvatar={null}
+          maxComposerHeight={100}
+          renderComposer={(props) => <Composer {...props} />}
+          isCustomViewBottom={true}
+          keyboardShouldPersistTaps="handled"
+          // inverted={true}
+          // isTyping={true}
+        />
+
+        <Modal visible={!!selectedMedia} transparent={true}>
+          <View style={styles.modalContainer}>
+            {selectedMedia && selectedMedia?.includes('.mp4') ? (
+              <Video
+                source={{ uri: selectedMedia }}
+                style={styles.fullScreenMedia}
+                // resizeMode="cover"
+                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}
+            >
+              {renderReactionsBar()}
+              {renderSelectedMessage()}
+              {renderOptionsMenu()}
+              {emojiSelectorVisible ? renderEmojiSelector() : null}
+            </TouchableOpacity>
+          </BlurView>
+        </ReactModal>
+      </GestureHandlerRootView>
+    </PageWrapper>
+  );
+};
+
+const styles = StyleSheet.create({
+  emojiSelectorContainer: {
+    position: 'absolute',
+    bottom: 0,
+    width: '100%',
+    height: '50%',
+    backgroundColor: 'white',
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    shadowColor: '#000',
+    shadowOpacity: 0.3,
+    shadowOffset: { width: 0, height: -2 },
+    shadowRadius: 5,
+    elevation: 5,
+    padding: 10
+  },
+
+  modalBackground: {
+    flex: 1
+  },
+  modalContent: {
+    backgroundColor: 'transparent'
+  },
+  reactionBar: {
+    position: 'absolute',
+    width: Dimensions.get('window').width * 0.75,
+    flexDirection: 'row',
+    backgroundColor: 'rgba(255, 255, 255, 0.9)',
+    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
+  },
+  closeModalButton: {
+    position: 'absolute',
+    top: 10,
+    right: 10
+  },
+  optionsMenu: {
+    position: 'absolute',
+    backgroundColor: 'rgba(255, 255, 255, 0.9)',
+    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
+  },
+  mediaContainer: {
+    borderRadius: 10,
+    overflow: 'hidden',
+    margin: 5
+  },
+  chatMedia: {
+    width: 200,
+    height: 200,
+    borderRadius: 10
+  },
+  fullScreenMedia: {
+    width: '90%',
+    height: '80%'
+  },
+  audioContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    padding: 10,
+    backgroundColor: '#f1f1f1',
+    borderRadius: 10,
+    margin: 5
+  },
+  progressBar: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    alignItems: 'center'
+  },
+  bar: {
+    width: 5,
+    backgroundColor: 'gray',
+    marginHorizontal: 1
+  },
+  replyMessageContainer: {
+    padding: 8,
+    paddingBottom: 0
+  },
+  replyMessageDivider: {
+    borderBottomWidth: 1,
+    borderBottomColor: 'lightgrey',
+    paddingTop: 6
+  },
+  composer: {
+    backgroundColor: Colors.FILL_LIGHT,
+    borderRadius: 15,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY,
+    paddingHorizontal: 10,
+    fontSize: 16,
+    marginVertical: 4
+  },
+  container: {
+    flex: 1,
+    backgroundColor: 'white'
+  },
+  imageContainer: {
+    borderRadius: 10,
+    overflow: 'hidden',
+    margin: 5
+  },
+  chatImage: {
+    width: 200,
+    height: 200,
+    borderRadius: 10
+  },
+  replyContainer: {
+    backgroundColor: '#f1f1f1',
+    padding: 5,
+    marginBottom: 5,
+    borderRadius: 5
+  },
+  replyText: {
+    color: '#333',
+    fontStyle: 'italic'
+  },
+  reactionsContainer: {
+    flexDirection: 'row',
+    justifyContent: 'flex-start',
+    paddingHorizontal: 5,
+    marginTop: -10
+  },
+  reactionText: {
+    fontSize: 16,
+    marginHorizontal: 5
+  },
+  reactionsBubble: {
+    backgroundColor: '#fff',
+    borderRadius: 20,
+    padding: 5,
+    flexDirection: 'row',
+    shadowColor: '#000',
+    shadowOpacity: 0.3,
+    shadowOffset: { width: 0, height: 2 },
+    shadowRadius: 5,
+    elevation: 5,
+    marginBottom: 10
+  },
+  replyInputContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    backgroundColor: '#f1f1f1',
+    paddingHorizontal: 10,
+    paddingVertical: 5,
+    borderRadius: 10
+  },
+  replyInputText: {
+    flex: 1,
+    color: 'gray'
+  },
+  modalContainer: {
+    flex: 1,
+    backgroundColor: 'rgba(0, 0, 0, 0.9)',
+    justifyContent: 'center',
+    alignItems: 'center'
+  },
+  reactModalContainer: {
+    justifyContent: 'flex-end',
+    margin: 0
+  },
+  fullScreenImage: {
+    width: '90%',
+    height: '80%'
+  },
+  closeButton: {
+    position: 'absolute',
+    top: 40,
+    right: 20
+  }
+});
+
+export default ChatScreen;

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


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

@@ -0,0 +1,99 @@
+import React from 'react';
+import { View, StyleSheet, Animated } from 'react-native';
+import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler';
+import { IMessage, 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';
+
+type ChatMessageBoxProps = {
+  setReplyOnSwipeOpen: (message: IMessage) => void;
+  updateRowRef: (ref: any) => void;
+} & MessageProps<IMessage>;
+
+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 });
+      trigger('impactMedium', options);
+    }
+  };
+
+  return (
+    <GestureHandlerRootView>
+      <Swipeable
+        ref={updateRowRef}
+        friction={2}
+        rightThreshold={40}
+        renderRightActions={renderRightAction}
+        onSwipeableOpen={onSwipeOpenAction}
+      >
+        <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;

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

@@ -0,0 +1,130 @@
+import React, { useState } from 'react';
+import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native';
+import ActionSheet from 'react-native-actions-sheet';
+import { Colors } from 'src/theme';
+import { Ionicons } from '@expo/vector-icons';
+import { API_HOST } from 'src/constants';
+import { getFontSize } from 'src/utils';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { ChatProps } from '../types';
+import { useNavigation } from '@react-navigation/native';
+import { NAVIGATION_PAGES } from 'src/types';
+
+const MoreModal = () => {
+  const insets = useSafeAreaInsets();
+  const navigation = useNavigation();
+
+  const [chatData, setChatData] = useState<ChatProps | null>(null);
+
+  const handleSheetOpen = (payload: ChatProps | null) => {
+    setChatData(payload);
+  };
+
+  return (
+    <ActionSheet
+      id="more-modal"
+      gestureEnabled={true}
+      onBeforeShow={(sheetRef) => {
+        const payload = sheetRef || null;
+        handleSheetOpen(payload);
+      }}
+      containerStyle={styles.sheetContainer}
+      defaultOverlayOpacity={0.5}
+      indicatorStyle={{ backgroundColor: 'transparent' }}
+    >
+      {chatData && (
+        <View style={[styles.container, { paddingBottom: 8 + insets.bottom }]}>
+          <TouchableOpacity
+            style={styles.header}
+            onPress={() =>
+              navigation.navigate(
+                ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: chatData.id }] as never)
+              )
+            }
+          >
+            <Image source={{ uri: API_HOST + chatData?.avatar }} style={styles.avatar} />
+            <Text style={styles.name}>{chatData.name}</Text>
+          </TouchableOpacity>
+
+          <View style={styles.optionsContainer}>
+            <TouchableOpacity style={styles.option}>
+              <Text style={styles.optionText}>Mute</Text>
+              <Ionicons name="notifications-off-outline" size={18} color={Colors.DARK_BLUE} />
+            </TouchableOpacity>
+          </View>
+
+          <View style={[styles.optionsContainer, { paddingVertical: 0, gap: 0 }]}>
+            <TouchableOpacity style={[styles.option, styles.dangerOption]}>
+              <Text style={[styles.optionText, styles.dangerText]}>Block {chatData.name}</Text>
+              <Ionicons name="ban-outline" size={18} color={Colors.RED} />
+            </TouchableOpacity>
+
+            <TouchableOpacity style={[styles.option, styles.dangerOption]}>
+              <Text style={[styles.optionText, styles.dangerText]}>Delete chat</Text>
+              <Ionicons name="trash-outline" size={18} color={Colors.RED} />
+            </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;

+ 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 }}>
+          {message.text.length > 40 ? `${message.text.substring(0, 40)}...` : 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
+  }
+});

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

@@ -0,0 +1,146 @@
+import React, { useEffect, useState } from 'react';
+import {
+  View,
+  TextInput,
+  Text,
+  Image,
+  TouchableOpacity,
+  StyleSheet,
+  ActivityIndicator,
+  Platform
+} from 'react-native';
+import ActionSheet 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';
+
+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={() =>
+        navigation.navigate(
+          ...([
+            NAVIGATION_PAGES.CHAT,
+            {
+              id: item.user_id,
+              name: item.first_name + ' ' + item.last_name,
+              avatar: item.avatar
+            }
+          ] as never)
+        )
+      }
+    >
+      <Image source={{ uri: API_HOST + item.avatar }} style={styles.avatar} />
+      <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}>
+        <TextInput
+          style={styles.searchInput}
+          placeholder="Search nomads"
+          value={searchQuery}
+          onChangeText={(text) => {
+            setSearchQuery(text);
+          }}
+        />
+
+        {isFetching ? (
+          <ActivityIndicator size="large" color="#0000ff" />
+        ) : (
+          <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
+  },
+  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: 10,
+    borderBottomColor: '#ccc',
+    borderBottomWidth: 1
+  },
+  avatar: {
+    width: 48,
+    height: 48,
+    borderRadius: 24,
+    borderWidth: 1,
+    borderColor: Colors.FILL_LIGHT,
+    marginRight: 15
+  },
+  textContainer: {
+    flex: 1
+  },
+  name: {
+    fontSize: 16,
+    fontWeight: 'bold'
+  }
+});
+
+export default SearchModal;

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

@@ -0,0 +1,144 @@
+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 { ChatProps } from '../types';
+
+interface AppleStyleSwipeableRowProps extends PropsWithChildren<unknown> {
+  chat: ChatProps;
+  onRowOpen: (ref: any) => void;
+}
+
+const SwipeableRow: React.FC<AppleStyleSwipeableRowProps> = ({ children, chat, onRowOpen }) => {
+  const swipeableRow = useRef<Swipeable>(null);
+
+  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 = () => {
+      close();
+      if (text === 'More') {
+        SheetManager.show('more-modal', {
+          payload: {
+            id: chat.id,
+            name: chat.name,
+            avatar: chat.avatar
+          } as any
+        });
+      }
+    };
+
+    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 = () => {
+      close();
+      alert(text);
+    };
+
+    return (
+      <Animated.View style={{ flex: 1, transform: [{ translateX: trans }] }}>
+        <RectButton style={[styles.rightAction, { backgroundColor: color }]} onPress={pressHandler}>
+          <PinIcon height={18} width={18} />
+          <Text style={styles.actionText}>{text}</Text>
+        </RectButton>
+      </Animated.View>
+    );
+  };
+
+  const renderRightActions = (progress: Animated.AnimatedInterpolation<number>) => (
+    <View
+      style={{
+        width: 168,
+        flexDirection: 'row'
+      }}
+    >
+      {renderRightAction('Archive', 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('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;

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

@@ -0,0 +1,339 @@
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import {
+  View,
+  Text,
+  TouchableOpacity,
+  StyleSheet,
+  Image,
+  Platform,
+  TouchableHighlight
+} from 'react-native';
+import { HorizontalTabView, Input, PageWrapper } 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 } from 'src/constants';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
+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 { usePostGetChatsListQuery } from '@api/chat';
+import moment from 'moment';
+import { Chat } from './types';
+
+type Routes = {
+  key: 'all' | 'unread' | 'archived';
+  title: string;
+};
+
+const MessagesScreen = () => {
+  const navigation = useNavigation();
+  const token = storage.get('token', StoreType.STRING) as string;
+  const { data: chatsData, refetch } = usePostGetChatsListQuery(token, 0, true);
+  const [chats, setChats] = useState<Chat[]>([]);
+  const [index, setIndex] = useState(0);
+  const [filteredChats, setFilteredChats] = useState<Chat[]>([]);
+  const [search, setSearch] = useState('');
+  const openRowRef = useRef<any>(null);
+
+  const routes: Routes[] = [
+    { key: 'all', title: 'All' },
+    { key: 'unread', title: 'Unread' },
+    { key: 'archived', title: 'Archived' }
+  ];
+
+  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]);
+
+  useFocusEffect(
+    useCallback(() => {
+      refetch();
+    }, [])
+  );
+
+  const filterChatsByTab = () => {
+    if (index === 1) {
+      setFilteredChats(
+        chats
+          .filter((chat: Chat) => chat.unread_count > 0)
+          .sort((a: Chat, b: Chat) => (b.pin === 1 ? 1 : -1))
+      );
+    } else if (index === 2) {
+      setFilteredChats([]);
+    } else {
+      setFilteredChats(chats.sort((a: Chat, b: Chat) => (b.pin === 1 ? 1 : -1)));
+    }
+  };
+
+  useEffect(() => {
+    filterChatsByTab();
+  }, [index, chats]);
+
+  const handlePinChat = (id: number) => {
+    const updatedChats = chats.map((chat: Chat) =>
+      chat.uid === id ? { ...chat, pin: !chat.pin } : chat
+    );
+    setChats(updatedChats as any);
+  };
+
+  const handleDeleteChat = (id: number) => {
+    const updatedChats = chats.filter((chat: Chat) => chat.uid !== id);
+    setChats(updatedChats);
+  };
+
+  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(newData);
+      setSearch(text);
+    } else {
+      filterChatsByTab();
+      setSearch(text);
+    }
+  };
+
+  const formatDate = (dateString: Date) => {
+    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');
+  };
+
+  const renderChatItem = ({ item }: { item: Chat }) => {
+    return (
+      <SwipeableRow
+        chat={{ id: item.uid, name: item.name, avatar: item.avatar }}
+        onRowOpen={handleRowOpen}
+      >
+        <TouchableHighlight
+          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}>
+            <Image source={{ uri: API_HOST + item.avatar }} style={styles.avatar} />
+
+            <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.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 }]}>
+                <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>
+    );
+  };
+
+  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: any }) => (
+          <FlashList
+            viewabilityConfig={{
+              waitForInteraction: true,
+              itemVisiblePercentThreshold: 50,
+              minimumViewTime: 1000
+            }}
+            data={filteredChats}
+            renderItem={renderChatItem}
+            keyExtractor={(item, index) => `${item.uid}-${index}`}
+            estimatedItemSize={78}
+            removeClippedSubviews={true}
+          />
+        )}
+      />
+
+      <SearchModal />
+      <MoreModal />
+    </PageWrapper>
+  );
+};
+
+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
+  }
+});
+
+export default MessagesScreen;

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


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

@@ -0,0 +1,43 @@
+export type Chat = {
+  uid: number;
+  name: string;
+  avatar: string | null;
+  short: string;
+  sent_by: number;
+  updated: Date;
+  status: 1 | 2 | 3;
+  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;
+};
+
+export type ChatProps = {
+  id: number;
+  name: string;
+  avatar: string | null;
+};
+
+export type MessageSimple = {
+  id: number;
+  sender: number;
+  recipient: number;
+  text: string;
+  status: 1 | 2 | 3;
+  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;
+};

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

+ 12 - 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,11 @@ 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'
 }
 
 export enum API {
@@ -224,7 +229,11 @@ 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}`
 }
 
 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'
 }