Преглед на файлове

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

Viktoriia преди 4 месеца
родител
ревизия
1f7dc579e6
променени са 55 файла, в които са добавени 4139 реда и са изтрити 117 реда
  1. 4 0
      Route.tsx
  2. 10 0
      assets/icons/events/calendar-solid.svg
  3. 10 0
      assets/icons/messages/camera.svg
  4. 11 0
      assets/icons/messages/images.svg
  5. 10 0
      assets/icons/messages/location.svg
  6. 1 1
      assets/icons/messages/megaphone.svg
  7. 74 0
      assets/icons/un-25.svg
  8. 74 0
      assets/icons/un-75.svg
  9. 4 3
      package.json
  10. 4 1
      src/constants/constants.ts
  11. 38 0
      src/database/cacheService/index.ts
  12. 2 0
      src/database/index.ts
  13. 33 3
      src/modules/api/chat/chat-api.ts
  14. 2 0
      src/modules/api/ranking/ranking-api.tsx
  15. 2 0
      src/modules/api/regions/regions-api.tsx
  16. 2 0
      src/modules/api/series/series-api.tsx
  17. 1 0
      src/modules/api/user/queries/index.ts
  18. 25 0
      src/modules/api/user/queries/use-post-update-email.tsx
  19. 5 1
      src/modules/api/user/user-api.tsx
  20. 2 1
      src/modules/api/user/user-query-keys.tsx
  21. 8 0
      src/screens/InAppScreens/MapScreen/UsersListScreen/Profile/index.tsx
  22. 2 0
      src/screens/InAppScreens/MapScreen/UsersListScreen/index.tsx
  23. 505 28
      src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx
  24. 45 1
      src/screens/InAppScreens/MessagesScreen/ChatScreen/styles.tsx
  25. 1658 0
      src/screens/InAppScreens/MessagesScreen/ChatScreen/test.tsx
  26. 213 26
      src/screens/InAppScreens/MessagesScreen/Components/AttachmentsModal.tsx
  27. 154 0
      src/screens/InAppScreens/MessagesScreen/Components/ChatOptionsBlock.tsx
  28. 75 0
      src/screens/InAppScreens/MessagesScreen/Components/MessageLocation.tsx
  29. 11 0
      src/screens/InAppScreens/MessagesScreen/Components/OptionsMenu.tsx
  30. 8 3
      src/screens/InAppScreens/MessagesScreen/Components/ReplyMessageBar.tsx
  31. 192 0
      src/screens/InAppScreens/MessagesScreen/Components/RouteB.tsx
  32. 160 0
      src/screens/InAppScreens/MessagesScreen/Components/renderMessageVideo.tsx
  33. 81 0
      src/screens/InAppScreens/MessagesScreen/FullMapScreen/index.tsx
  34. 3 1
      src/screens/InAppScreens/MessagesScreen/index.tsx
  35. 19 5
      src/screens/InAppScreens/MessagesScreen/types.ts
  36. 40 29
      src/screens/InAppScreens/ProfileScreen/Components/PersonalInfo.tsx
  37. 8 0
      src/screens/InAppScreens/ProfileScreen/MyFriendsScreen/FriendsProfile/index.tsx
  38. 2 0
      src/screens/InAppScreens/ProfileScreen/MyFriendsScreen/index.tsx
  39. 79 4
      src/screens/InAppScreens/ProfileScreen/Profile/edit-personal-info.tsx
  40. 4 0
      src/screens/InAppScreens/ProfileScreen/ShareScreen/index.tsx
  41. 8 10
      src/screens/InAppScreens/ProfileScreen/index.tsx
  42. 8 0
      src/screens/InAppScreens/TravellersScreen/Components/Profile.tsx
  43. 4 0
      src/screens/InAppScreens/TravellersScreen/Components/SeriesRankingItem.tsx
  44. 2 0
      src/screens/InAppScreens/TravellersScreen/InHistoryScreen/index.tsx
  45. 2 0
      src/screens/InAppScreens/TravellersScreen/InMemoriamScreen/index.tsx
  46. 2 0
      src/screens/InAppScreens/TravellersScreen/LPIRankingScreen/index.tsx
  47. 2 0
      src/screens/InAppScreens/TravellersScreen/MasterRankingScreen/index.tsx
  48. 2 0
      src/screens/InAppScreens/TravellersScreen/index.tsx
  49. 2 0
      src/screens/InAppScreens/TravellersScreen/utils/types.ts
  50. 299 0
      src/screens/InAppScreens/TravelsScreen/EventsScreen/index.tsx
  51. 216 0
      src/screens/InAppScreens/TravelsScreen/EventsScreen/styles.tsx
  52. 2 0
      src/screens/InAppScreens/TravelsScreen/index.tsx
  53. 2 0
      src/types/api.ts
  54. 2 0
      src/types/navigation.ts
  55. 5 0
      src/utils/request.ts

+ 4 - 0
Route.tsx

@@ -94,7 +94,9 @@ import { Splash } from 'src/components/SplashSpinner';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 import LocationSharingScreen from 'src/screens/LocationSharingScreen';
 import { useFriendsNotificationsStore } from 'src/stores/friendsNotificationsStore';
+import EventsScreen from 'src/screens/InAppScreens/TravelsScreen/EventsScreen';
 import { NavigationProvider } from 'src/contexts/NavigationContext';
+import FullMapScreen from 'src/screens/InAppScreens/MessagesScreen/FullMapScreen';
 
 enableScreens();
 
@@ -305,6 +307,7 @@ const Route = () => {
             <ScreenStack.Screen name={NAVIGATION_PAGES.DARE} component={DareScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.FIXERS} component={FixersScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_FIXER} component={AddNewFixerScreen} />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.EVENTS} component={EventsScreen} />
             <ScreenStack.Screen
               name={NAVIGATION_PAGES.FIXERS_COMMENTS}
               component={FixersCommentsScreen}
@@ -413,6 +416,7 @@ const Route = () => {
           <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.FULL_MAP_VIEW} component={FullMapScreen} />
             <ScreenStack.Screen
               name={NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW}
               component={ProfileScreen}

+ 10 - 0
assets/icons/events/calendar-solid.svg

@@ -0,0 +1,10 @@
+<svg width="29" height="32" viewBox="0 0 29 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4524_47052)">
+<path d="M8.25 0C9.35625 0 10.25 0.89375 10.25 2V4H18.25V2C18.25 0.89375 19.1437 0 20.25 0C21.3563 0 22.25 0.89375 22.25 2V4H25.25C26.9062 4 28.25 5.34375 28.25 7V10H0.25V7C0.25 5.34375 1.59375 4 3.25 4H6.25V2C6.25 0.89375 7.14375 0 8.25 0ZM0.25 12H28.25V29C28.25 30.6562 26.9062 32 25.25 32H3.25C1.59375 32 0.25 30.6562 0.25 29V12ZM4.25 17V19C4.25 19.55 4.7 20 5.25 20H7.25C7.8 20 8.25 19.55 8.25 19V17C8.25 16.45 7.8 16 7.25 16H5.25C4.7 16 4.25 16.45 4.25 17ZM12.25 17V19C12.25 19.55 12.7 20 13.25 20H15.25C15.8 20 16.25 19.55 16.25 19V17C16.25 16.45 15.8 16 15.25 16H13.25C12.7 16 12.25 16.45 12.25 17ZM21.25 16C20.7 16 20.25 16.45 20.25 17V19C20.25 19.55 20.7 20 21.25 20H23.25C23.8 20 24.25 19.55 24.25 19V17C24.25 16.45 23.8 16 23.25 16H21.25ZM4.25 25V27C4.25 27.55 4.7 28 5.25 28H7.25C7.8 28 8.25 27.55 8.25 27V25C8.25 24.45 7.8 24 7.25 24H5.25C4.7 24 4.25 24.45 4.25 25ZM13.25 24C12.7 24 12.25 24.45 12.25 25V27C12.25 27.55 12.7 28 13.25 28H15.25C15.8 28 16.25 27.55 16.25 27V25C16.25 24.45 15.8 24 15.25 24H13.25ZM20.25 25V27C20.25 27.55 20.7 28 21.25 28H23.25C23.8 28 24.25 27.55 24.25 27V25C24.25 24.45 23.8 24 23.25 24H21.25C20.7 24 20.25 24.45 20.25 25Z" fill="#ED9334"/>
+</g>
+<defs>
+<clipPath id="clip0_4524_47052">
+<rect width="28" height="32" fill="white" transform="translate(0.25)"/>
+</clipPath>
+</defs>
+</svg>

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

@@ -0,0 +1,10 @@
+<svg width="37" height="36" viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4486_39236)">
+<path d="M10.9836 4.55625L10.2523 6.75H5C2.51797 6.75 0.5 8.76797 0.5 11.25V29.25C0.5 31.732 2.51797 33.75 5 33.75H32C34.482 33.75 36.5 31.732 36.5 29.25V11.25C36.5 8.76797 34.482 6.75 32 6.75H26.7477L26.0164 4.55625C25.5594 3.17813 24.2727 2.25 22.8172 2.25H14.1828C12.7273 2.25 11.4406 3.17813 10.9836 4.55625ZM18.5 13.5C20.2902 13.5 22.0071 14.2112 23.273 15.477C24.5388 16.7429 25.25 18.4598 25.25 20.25C25.25 22.0402 24.5388 23.7571 23.273 25.023C22.0071 26.2888 20.2902 27 18.5 27C16.7098 27 14.9929 26.2888 13.727 25.023C12.4612 23.7571 11.75 22.0402 11.75 20.25C11.75 18.4598 12.4612 16.7429 13.727 15.477C14.9929 14.2112 16.7098 13.5 18.5 13.5Z" fill="#ED9334"/>
+</g>
+<defs>
+<clipPath id="clip0_4486_39236">
+<rect width="36" height="36" fill="white" transform="translate(0.5)"/>
+</clipPath>
+</defs>
+</svg>

+ 11 - 0
assets/icons/messages/images.svg

@@ -0,0 +1,11 @@
+<svg width="42" height="36" viewBox="0 0 42 36" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4486_39219)">
+<path d="M38.041 27.5625C38.041 28.4977 38.7934 29.25 39.7285 29.25C40.6637 29.25 41.416 28.4977 41.416 27.5625V11.8125C41.416 6.53203 37.134 2.25 31.8535 2.25H9.35352C8.41836 2.25 7.66602 3.00234 7.66602 3.9375C7.66602 4.87266 8.41836 5.625 9.35352 5.625H31.8535C35.2707 5.625 38.041 8.39531 38.041 11.8125V27.5625Z" fill="#ED9334"/>
+<path d="M5.41602 9C2.93398 9 0.916016 11.018 0.916016 13.5V29.25C0.916016 31.732 2.93398 33.75 5.41602 33.75H30.166C32.648 33.75 34.666 31.732 34.666 29.25V13.5C34.666 11.018 32.648 9 30.166 9H5.41602ZM22.0098 16.5023L28.7598 26.6273C29.1043 27.1477 29.1395 27.8086 28.8441 28.357C28.5488 28.9055 27.9793 29.25 27.3535 29.25H17.2285H13.8535H8.22852C7.58164 29.25 6.99102 28.8773 6.70977 28.2937C6.42852 27.7102 6.50586 27.0141 6.91367 26.5078L11.4137 20.8828C11.7371 20.482 12.2152 20.25 12.7285 20.25C13.2418 20.25 13.727 20.482 14.0434 20.8828L15.2598 22.4016L19.1973 16.4953C19.5137 16.0313 20.041 15.75 20.6035 15.75C21.166 15.75 21.6934 16.0313 22.0098 16.5023ZM7.66602 15.75C7.66602 14.5074 8.67338 13.5 9.91602 13.5C11.1587 13.5 12.166 14.5074 12.166 15.75C12.166 16.9926 11.1587 18 9.91602 18C8.67338 18 7.66602 16.9926 7.66602 15.75Z" fill="#ED9334"/>
+</g>
+<defs>
+<clipPath id="clip0_4486_39219">
+<rect width="40.5" height="36" fill="white" transform="translate(0.916016)"/>
+</clipPath>
+</defs>
+</svg>

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

@@ -0,0 +1,10 @@
+<svg width="28" height="36" viewBox="0 0 28 36" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4486_39238)">
+<path d="M15.4984 35.1C19.1055 30.5859 27.332 19.6453 27.332 13.5C27.332 6.04688 21.2852 0 13.832 0C6.37891 0 0.332031 6.04688 0.332031 13.5C0.332031 19.6453 8.55859 30.5859 12.1656 35.1C13.0305 36.1758 14.6336 36.1758 15.4984 35.1ZM13.832 9C15.0255 9 16.1701 9.47411 17.014 10.318C17.8579 11.1619 18.332 12.3065 18.332 13.5C18.332 14.6935 17.8579 15.8381 17.014 16.682C16.1701 17.5259 15.0255 18 13.832 18C12.6386 18 11.494 17.5259 10.6501 16.682C9.80614 15.8381 9.33203 14.6935 9.33203 13.5C9.33203 12.3065 9.80614 11.1619 10.6501 10.318C11.494 9.47411 12.6386 9 13.832 9Z" fill="#ED9334"/>
+</g>
+<defs>
+<clipPath id="clip0_4486_39238">
+<rect width="27" height="36" fill="white" transform="translate(0.332031)"/>
+</clipPath>
+</defs>
+</svg>

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

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

+ 74 - 0
assets/icons/un-25.svg

@@ -0,0 +1,74 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4641_39600)">
+<path d="M10.0108 17.6041C9.26705 18.9053 6.72482 19.3094 5.74927 18.5572C6.02538 18.146 6.39802 17.8193 6.82969 17.6099C7.82982 17.1121 8.8451 17.0618 9.8758 17.5144C9.96969 17.546 10.0709 17.5421 10.1625 17.5034C11.4915 16.9685 12.7215 17.1415 13.8471 18.0765C13.9423 18.1625 14.0315 18.2562 14.1136 18.3568C14.1654 18.4156 14.2097 18.4815 14.2577 18.5443C13.3707 19.2952 10.8118 18.9578 10.0108 17.6041Z" fill="url(#paint0_linear_4641_39600)"/>
+<path d="M4.95599 15.7919C3.79654 16.4086 1.35404 15.4692 0.986816 14.1577C2.39404 13.5886 4.43029 14.3877 4.95599 15.7919Z" fill="url(#paint1_linear_4641_39600)"/>
+<path d="M12.6997 17.0961C13.6251 15.9588 15.8332 15.7356 17.0344 16.7361C16.2526 17.8153 13.7146 18.0502 12.6997 17.0961Z" fill="url(#paint2_linear_4641_39600)"/>
+<path d="M19.0247 14.162C18.6487 15.4512 16.2698 16.4026 15.0515 15.7996C15.5537 14.3951 17.6272 13.5862 19.0247 14.162Z" fill="url(#paint3_linear_4641_39600)"/>
+<path d="M17.9217 8.98208C17.0209 7.92414 17.2114 5.91706 18.5964 5C19.5691 5.95589 19.1588 8.37782 17.9217 8.98208Z" fill="url(#paint4_linear_4641_39600)"/>
+<path d="M2.97192 16.7359C4.15929 15.7407 6.36693 15.9471 7.30568 17.0924C6.3222 18.038 3.77831 17.8321 2.97192 16.7359Z" fill="url(#paint5_linear_4641_39600)"/>
+<path d="M1.39781 5C2.75072 5.83868 3.02225 7.89649 2.07933 8.98297C0.902805 8.40414 0.415166 6.03824 1.39781 5Z" fill="url(#paint6_linear_4641_39600)"/>
+<path d="M19.997 11.1201C20.0868 12.4819 18.1457 14.0835 16.8251 13.8678C16.7804 12.4891 18.366 11.0198 19.997 11.1201Z" fill="url(#paint7_linear_4641_39600)"/>
+<path d="M0 11.1228C1.38292 11.0091 3.12472 12.1954 3.20195 13.8719C2.78792 13.916 2.37014 13.8476 1.9875 13.6731C1.35931 13.3848 0.810973 12.9297 0.394861 12.3509C0.136667 11.9961 0.01875 11.5831 0 11.1228Z" fill="url(#paint8_linear_4641_39600)"/>
+<path d="M0.1023 7.94727C1.66147 8.30065 2.7098 10.1402 2.1805 11.5496C1.9298 11.432 1.66022 11.3482 1.4355 11.1932C0.641606 10.6379 0.157439 9.84831 0.0292443 8.84345C-0.00922792 8.55374 0.0742444 8.24933 0.1023 7.94727Z" fill="url(#paint9_linear_4641_39600)"/>
+<path d="M19.8502 7.96094C20.4146 9.11947 19.2323 11.2558 17.8268 11.5152C17.2909 10.3045 18.2371 8.33212 19.8502 7.96094Z" fill="url(#paint10_linear_4641_39600)"/>
+<path d="M8.94959 19.9993C8.82764 19.4374 8.96389 18.9694 9.34778 18.6178C9.90681 18.1041 10.1044 18.1055 10.6731 18.6285C11.0619 18.985 11.1907 19.5234 11.0424 19.9908C10.5093 19.87 10.2197 19.4738 10.0071 18.9431C9.79361 19.4578 9.50736 19.8661 8.94959 19.9993Z" fill="url(#paint11_linear_4641_39600)"/>
+<path d="M6.13376 12.62V11.5325L7.82429 9.7775C7.93121 9.6625 8.00916 9.5625 8.05816 9.4775C8.11162 9.3925 8.14726 9.3175 8.16507 9.2525C8.18289 9.1825 8.1918 9.1175 8.1918 9.0575C8.1918 8.9275 8.15394 8.8275 8.07821 8.7575C8.00248 8.6825 7.88889 8.645 7.73743 8.645C7.59934 8.645 7.46792 8.6875 7.3432 8.7725C7.21847 8.8525 7.12046 8.9725 7.04919 9.1325L5.85981 8.465C6.02908 8.105 6.28299 7.8125 6.62155 7.5875C6.9601 7.3625 7.38106 7.25 7.88443 7.25C8.25417 7.25 8.58158 7.3175 8.86668 7.4525C9.15177 7.5875 9.3745 7.7775 9.53487 8.0225C9.69524 8.2675 9.77542 8.5575 9.77542 8.8925C9.77542 9.0625 9.75537 9.2325 9.71528 9.4025C9.67964 9.5725 9.60392 9.7525 9.4881 9.9425C9.37228 10.1275 9.20077 10.335 8.97359 10.565L7.70402 11.8625L7.45679 11.2475H9.88233V12.62H6.13376Z" fill="#16335F"/>
+<path d="M12.0362 12.74C11.7154 12.74 11.3902 12.7 11.0606 12.62C10.731 12.54 10.4414 12.4225 10.192 12.2675L10.7265 10.9625C10.927 11.0925 11.1363 11.19 11.3546 11.255C11.5729 11.315 11.78 11.345 11.976 11.345C12.1542 11.345 12.3012 11.31 12.417 11.24C12.5329 11.17 12.5908 11.0675 12.5908 10.9325C12.5908 10.8575 12.5707 10.7925 12.5306 10.7375C12.4905 10.6775 12.4193 10.6325 12.3168 10.6025C12.2188 10.5725 12.074 10.5575 11.8825 10.5575H10.5261L10.7599 7.37H13.867V8.705H11.3145L12.1097 7.9475L11.9694 9.98L11.1742 9.2225H12.2634C12.7266 9.2225 13.0964 9.2975 13.3726 9.4475C13.6532 9.5975 13.8559 9.8 13.9806 10.055C14.1098 10.305 14.1744 10.585 14.1744 10.895C14.1744 11.205 14.0987 11.5025 13.9472 11.7875C13.8002 12.0675 13.5686 12.2975 13.2523 12.4775C12.9405 12.6525 12.5351 12.74 12.0362 12.74Z" fill="#16335F"/>
+<path d="M7.16152 2.48394L6.32054 2.36321L5.94429 1.61095C5.92249 1.56718 5.84901 1.56718 5.82749 1.61095L5.45082 2.36321L4.61012 2.48394C4.58554 2.48741 4.56512 2.5045 4.55749 2.52784C4.54971 2.55104 4.55596 2.5766 4.57387 2.59369L5.18249 3.17925L5.03874 4.00612C5.03471 4.03029 5.04457 4.05446 5.06485 4.06905C5.08485 4.08336 5.11179 4.08544 5.13346 4.07391L5.88582 3.68354L6.63832 4.07391C6.64763 4.07891 6.65832 4.08141 6.6686 4.08141C6.68221 4.08141 6.69568 4.07725 6.70679 4.06905C6.72665 4.05446 6.73721 4.03015 6.73277 4.00612L6.58915 3.17925L7.1979 2.59369C7.21554 2.5766 7.22221 2.55118 7.21429 2.52784C7.20638 2.5045 7.18624 2.48741 7.16152 2.48394Z" fill="#1651A8"/>
+<path d="M11.7244 1.18953L10.6202 1.03102L10.1261 0.0431356C10.0973 -0.0143785 10.0009 -0.0143785 9.97217 0.0431356L9.47814 1.03102L8.37384 1.18953C8.34147 1.19411 8.31467 1.21662 8.30439 1.24718C8.29453 1.27761 8.30272 1.31136 8.3262 1.33359L9.1255 2.10267L8.93675 3.18835C8.9312 3.22017 8.94439 3.25226 8.97064 3.27101C8.99731 3.29018 9.03245 3.29268 9.06106 3.27768L10.049 2.76492L11.0372 3.27768C11.0494 3.28407 11.0634 3.28727 11.0772 3.28727C11.0948 3.28727 11.1127 3.28199 11.1272 3.27101C11.1536 3.25212 11.167 3.22003 11.1612 3.18835L10.9729 2.10253L11.7722 1.33345C11.7954 1.31122 11.804 1.27761 11.7938 1.24704C11.7836 1.21662 11.7569 1.19411 11.7244 1.18953Z" fill="#1651A8"/>
+<path d="M15.5405 2.52784C15.5328 2.5045 15.5126 2.48741 15.4878 2.48394L14.6469 2.36321L14.2705 1.61095C14.249 1.56718 14.1753 1.56718 14.1535 1.61095L13.7771 2.36321L12.9363 2.48394C12.9117 2.48741 12.8914 2.5045 12.8835 2.52784C12.876 2.55104 12.882 2.5766 12.9001 2.59369L13.5088 3.17925L13.3652 4.00612C13.3608 4.03029 13.371 4.05446 13.3909 4.06905C13.4112 4.08336 13.4378 4.08544 13.4598 4.07391L14.212 3.68354L14.9644 4.07391C14.974 4.07891 14.9844 4.08141 14.9951 4.08141C15.0084 4.08141 15.022 4.07725 15.0333 4.06905C15.0533 4.05446 15.0634 4.03015 15.0591 4.00612L14.9153 3.17925L15.5241 2.59369C15.5419 2.5766 15.5481 2.55118 15.5405 2.52784Z" fill="#1651A8"/>
+</g>
+<defs>
+<linearGradient id="paint0_linear_4641_39600" x1="9.99901" y1="20.9768" x2="9.99901" y2="1.60167" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint1_linear_4641_39600" x1="9.99906" y1="20.976" x2="9.99906" y2="1.6009" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint2_linear_4641_39600" x1="9.99902" y1="20.9755" x2="9.99902" y2="1.60037" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint3_linear_4641_39600" x1="9.99902" y1="20.9763" x2="9.99902" y2="1.60116" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint4_linear_4641_39600" x1="9.99895" y1="20.9758" x2="9.99895" y2="1.60071" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint5_linear_4641_39600" x1="9.99916" y1="20.9758" x2="9.99916" y2="1.60065" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint6_linear_4641_39600" x1="9.99894" y1="20.9763" x2="9.99894" y2="1.60116" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint7_linear_4641_39600" x1="9.99904" y1="20.9766" x2="9.99904" y2="1.60151" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint8_linear_4641_39600" x1="9.99905" y1="20.9766" x2="9.99905" y2="1.60153" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint9_linear_4641_39600" x1="9.99899" y1="20.9757" x2="9.99899" y2="1.60061" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint10_linear_4641_39600" x1="9.99894" y1="20.9756" x2="9.99894" y2="1.60046" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint11_linear_4641_39600" x1="9.99904" y1="20.9755" x2="9.99904" y2="1.60036" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<clipPath id="clip0_4641_39600">
+<rect width="20" height="20" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 74 - 0
assets/icons/un-75.svg

@@ -0,0 +1,74 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4641_39569)">
+<path d="M10.0108 17.6041C9.26705 18.9053 6.72482 19.3094 5.74927 18.5572C6.02538 18.146 6.39802 17.8193 6.82969 17.6099C7.82982 17.1121 8.8451 17.0618 9.8758 17.5144C9.96969 17.546 10.0709 17.5421 10.1625 17.5034C11.4915 16.9685 12.7215 17.1415 13.8471 18.0765C13.9423 18.1625 14.0315 18.2562 14.1136 18.3568C14.1654 18.4156 14.2097 18.4815 14.2577 18.5443C13.3707 19.2952 10.8118 18.9578 10.0108 17.6041Z" fill="url(#paint0_linear_4641_39569)"/>
+<path d="M4.95599 15.7919C3.79654 16.4086 1.35404 15.4692 0.986816 14.1577C2.39404 13.5886 4.43029 14.3877 4.95599 15.7919Z" fill="url(#paint1_linear_4641_39569)"/>
+<path d="M12.6997 17.0961C13.6251 15.9588 15.8332 15.7356 17.0344 16.7361C16.2526 17.8153 13.7146 18.0502 12.6997 17.0961Z" fill="url(#paint2_linear_4641_39569)"/>
+<path d="M19.0247 14.162C18.6487 15.4512 16.2698 16.4026 15.0515 15.7996C15.5537 14.3951 17.6272 13.5862 19.0247 14.162Z" fill="url(#paint3_linear_4641_39569)"/>
+<path d="M17.9217 8.98208C17.0209 7.92414 17.2114 5.91706 18.5964 5C19.5691 5.95589 19.1588 8.37782 17.9217 8.98208Z" fill="url(#paint4_linear_4641_39569)"/>
+<path d="M2.97192 16.7359C4.15929 15.7407 6.36693 15.9471 7.30568 17.0924C6.3222 18.038 3.77831 17.8321 2.97192 16.7359Z" fill="url(#paint5_linear_4641_39569)"/>
+<path d="M1.39781 5C2.75072 5.83868 3.02225 7.89649 2.07933 8.98297C0.902805 8.40414 0.415166 6.03824 1.39781 5Z" fill="url(#paint6_linear_4641_39569)"/>
+<path d="M19.997 11.1201C20.0868 12.4819 18.1457 14.0835 16.8251 13.8678C16.7804 12.4891 18.366 11.0198 19.997 11.1201Z" fill="url(#paint7_linear_4641_39569)"/>
+<path d="M0 11.1228C1.38292 11.0091 3.12472 12.1954 3.20195 13.8719C2.78792 13.916 2.37014 13.8476 1.9875 13.6731C1.35931 13.3848 0.810973 12.9297 0.394861 12.3509C0.136667 11.9961 0.01875 11.5831 0 11.1228Z" fill="url(#paint8_linear_4641_39569)"/>
+<path d="M0.1023 7.94727C1.66147 8.30065 2.7098 10.1402 2.1805 11.5496C1.9298 11.432 1.66022 11.3482 1.4355 11.1932C0.641606 10.6379 0.157439 9.84831 0.0292443 8.84345C-0.00922792 8.55374 0.0742444 8.24933 0.1023 7.94727Z" fill="url(#paint9_linear_4641_39569)"/>
+<path d="M19.8502 7.96094C20.4146 9.11947 19.2323 11.2558 17.8268 11.5152C17.2909 10.3045 18.2371 8.33212 19.8502 7.96094Z" fill="url(#paint10_linear_4641_39569)"/>
+<path d="M8.94959 19.9993C8.82764 19.4374 8.96389 18.9694 9.34778 18.6178C9.90681 18.1041 10.1044 18.1055 10.6731 18.6285C11.0619 18.985 11.1907 19.5234 11.0424 19.9908C10.5093 19.87 10.2197 19.4738 10.0071 18.9431C9.79361 19.4578 9.50736 19.8661 8.94959 19.9993Z" fill="url(#paint11_linear_4641_39569)"/>
+<path d="M6.18821 12.6232L8.21245 7.89994L8.69551 8.65903H6.14987L6.85529 7.8386V9.44112H5.36011V7.25586H9.953V8.36766L8.16645 12.6232H6.18821Z" fill="#16335F"/>
+<path d="M12.1834 12.7459C11.8153 12.7459 11.4422 12.705 11.0639 12.6232C10.6856 12.5414 10.3534 12.4213 10.0671 12.2628L10.6805 10.9286C10.9105 11.0615 11.1508 11.1612 11.4013 11.2277C11.6517 11.289 11.8894 11.3197 12.1144 11.3197C12.3188 11.3197 12.4875 11.2839 12.6204 11.2123C12.7533 11.1408 12.8198 11.036 12.8198 10.898C12.8198 10.8213 12.7968 10.7548 12.7508 10.6986C12.7048 10.6373 12.623 10.5913 12.5054 10.5606C12.393 10.5299 12.2268 10.5146 12.007 10.5146H10.4505L10.7189 7.25586H14.2843V8.62069H11.3553L12.2677 7.84626L12.1067 9.92418L11.1943 9.14976H12.4441C12.9757 9.14976 13.4 9.22643 13.7169 9.37978C14.0389 9.53314 14.2715 9.74016 14.4146 10.0009C14.5629 10.2564 14.637 10.5427 14.637 10.8596C14.637 11.1766 14.5501 11.4807 14.3763 11.7721C14.2076 12.0583 13.9418 12.2935 13.5789 12.4775C13.2211 12.6564 12.7559 12.7459 12.1834 12.7459Z" fill="#16335F"/>
+<path d="M7.16152 2.48394L6.32054 2.36321L5.94429 1.61095C5.92249 1.56718 5.84901 1.56718 5.82749 1.61095L5.45082 2.36321L4.61012 2.48394C4.58554 2.48741 4.56512 2.5045 4.55749 2.52784C4.54971 2.55104 4.55596 2.5766 4.57387 2.59369L5.18249 3.17925L5.03874 4.00612C5.03471 4.03029 5.04457 4.05446 5.06485 4.06905C5.08485 4.08336 5.11179 4.08544 5.13346 4.07391L5.88582 3.68354L6.63832 4.07391C6.64763 4.07891 6.65832 4.08141 6.6686 4.08141C6.68221 4.08141 6.69568 4.07725 6.70679 4.06905C6.72665 4.05446 6.73721 4.03015 6.73277 4.00612L6.58915 3.17925L7.1979 2.59369C7.21554 2.5766 7.22221 2.55118 7.21429 2.52784C7.20638 2.5045 7.18624 2.48741 7.16152 2.48394Z" fill="#1651A8"/>
+<path d="M11.7244 1.18953L10.6202 1.03102L10.1261 0.0431356C10.0973 -0.0143785 10.0009 -0.0143785 9.97217 0.0431356L9.47814 1.03102L8.37384 1.18953C8.34147 1.19411 8.31467 1.21662 8.30439 1.24718C8.29453 1.27761 8.30272 1.31136 8.3262 1.33359L9.1255 2.10267L8.93675 3.18835C8.9312 3.22017 8.94439 3.25226 8.97064 3.27101C8.99731 3.29018 9.03245 3.29268 9.06106 3.27768L10.049 2.76492L11.0372 3.27768C11.0494 3.28407 11.0634 3.28727 11.0772 3.28727C11.0948 3.28727 11.1127 3.28199 11.1272 3.27101C11.1536 3.25212 11.167 3.22003 11.1612 3.18835L10.9729 2.10253L11.7722 1.33345C11.7954 1.31122 11.804 1.27761 11.7938 1.24704C11.7836 1.21662 11.7569 1.19411 11.7244 1.18953Z" fill="#1651A8"/>
+<path d="M15.5405 2.52784C15.5328 2.5045 15.5126 2.48741 15.4878 2.48394L14.6469 2.36321L14.2705 1.61095C14.249 1.56718 14.1753 1.56718 14.1535 1.61095L13.7771 2.36321L12.9363 2.48394C12.9117 2.48741 12.8914 2.5045 12.8835 2.52784C12.876 2.55104 12.882 2.5766 12.9001 2.59369L13.5088 3.17925L13.3652 4.00612C13.3608 4.03029 13.371 4.05446 13.3909 4.06905C13.4112 4.08336 13.4378 4.08544 13.4598 4.07391L14.212 3.68354L14.9644 4.07391C14.974 4.07891 14.9844 4.08141 14.9951 4.08141C15.0084 4.08141 15.022 4.07725 15.0333 4.06905C15.0533 4.05446 15.0634 4.03015 15.0591 4.00612L14.9153 3.17925L15.5241 2.59369C15.5419 2.5766 15.5481 2.55118 15.5405 2.52784Z" fill="#1651A8"/>
+</g>
+<defs>
+<linearGradient id="paint0_linear_4641_39569" x1="9.99901" y1="20.9768" x2="9.99901" y2="1.60167" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint1_linear_4641_39569" x1="9.99906" y1="20.976" x2="9.99906" y2="1.6009" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint2_linear_4641_39569" x1="9.99902" y1="20.9755" x2="9.99902" y2="1.60037" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint3_linear_4641_39569" x1="9.99902" y1="20.9763" x2="9.99902" y2="1.60116" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint4_linear_4641_39569" x1="9.99895" y1="20.9758" x2="9.99895" y2="1.60071" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint5_linear_4641_39569" x1="9.99916" y1="20.9758" x2="9.99916" y2="1.60065" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint6_linear_4641_39569" x1="9.99894" y1="20.9763" x2="9.99894" y2="1.60116" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint7_linear_4641_39569" x1="9.99904" y1="20.9766" x2="9.99904" y2="1.60151" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint8_linear_4641_39569" x1="9.99905" y1="20.9766" x2="9.99905" y2="1.60153" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint9_linear_4641_39569" x1="9.99899" y1="20.9757" x2="9.99899" y2="1.60061" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint10_linear_4641_39569" x1="9.99894" y1="20.9756" x2="9.99894" y2="1.60046" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<linearGradient id="paint11_linear_4641_39569" x1="9.99904" y1="20.9755" x2="9.99904" y2="1.60036" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F27127"/>
+<stop offset="1" stop-color="#FFBB31"/>
+</linearGradient>
+<clipPath id="clip0_4641_39569">
+<rect width="20" height="20" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 4 - 3
package.json

@@ -34,7 +34,7 @@
     "expo": "^51.0.9",
     "expo-asset": "~10.0.10",
     "expo-av": "^14.0.7",
-    "expo-blur": "^13.0.2",
+    "expo-blur": "~13.0.3",
     "expo-build-properties": "~0.12.5",
     "expo-checkbox": "~3.0.0",
     "expo-constants": "~16.0.2",
@@ -42,9 +42,10 @@
     "expo-file-system": "~17.0.1",
     "expo-font": "~12.0.10",
     "expo-image": "~1.13.0",
-    "expo-image-picker": "~15.0.7",
+    "expo-image-picker": "~15.1.0",
     "expo-location": "~17.0.1",
-    "expo-notifications": "~0.28.16",
+    "expo-media-library": "~16.0.5",
+    "expo-notifications": "~0.28.19",
     "expo-splash-screen": "~0.27.5",
     "expo-sqlite": "^14.0.6",
     "expo-status-bar": "~1.12.1",

+ 4 - 1
src/constants/constants.ts

@@ -1,7 +1,10 @@
 import { Platform, StatusBar } from 'react-native';
+import * as FileSystem from 'expo-file-system';
 
 const NOTCH_HEIGHT = 44;
 export const statusBarHeight =
   Platform.OS === 'ios' ? StatusBar.currentHeight || NOTCH_HEIGHT : StatusBar.currentHeight || 0;
 
-export const openstreetmapUrl = 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png';
+export const CACHED_ATTACHMENTS_DIR = `${FileSystem.documentDirectory}nomadmania-attachments/`;
+export const CACHE_EXPIRATION_DAYS = 14;
+export const CACHE_MAX_SIZE_MB = 200;

+ 38 - 0
src/database/cacheService/index.ts

@@ -0,0 +1,38 @@
+import * as FileSystem from 'expo-file-system';
+import {
+  CACHE_EXPIRATION_DAYS,
+  CACHE_MAX_SIZE_MB,
+  CACHED_ATTACHMENTS_DIR
+} from 'src/constants/constants';
+
+export const cleanCache = async () => {
+  try {
+    const dirInfo = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
+    if (!dirInfo.exists) return;
+
+    const files = await FileSystem.readDirectoryAsync(CACHED_ATTACHMENTS_DIR);
+    let totalSize = 0;
+    const now = Date.now();
+
+    for (const file of files) {
+      const filePath = `${CACHED_ATTACHMENTS_DIR}${file}`;
+      const fileInfo = await FileSystem.getInfoAsync(filePath);
+
+      if (fileInfo.exists) {
+        totalSize += fileInfo.size / (1024 * 1024);
+        const fileAgeDays = (now - fileInfo.modificationTime * 1000) / (1000 * 60 * 60 * 24);
+
+        if (fileAgeDays > CACHE_EXPIRATION_DAYS) {
+          await FileSystem.deleteAsync(filePath);
+        }
+      }
+    }
+
+    if (totalSize > CACHE_MAX_SIZE_MB) {
+      await FileSystem.deleteAsync(CACHED_ATTACHMENTS_DIR, { idempotent: true });
+      await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
+    }
+  } catch (error) {
+    console.error('Error cleaning cache:', error);
+  }
+};

+ 2 - 0
src/database/index.ts

@@ -10,6 +10,7 @@ import { fetchAndSaveStatistics } from './statisticsService';
 import { saveTriumphsData } from './triumphsService';
 import { saveSeriesRankingData } from './seriesRankingService';
 import { updateDarePlacesDb, updateNmRegionsDb } from 'src/db';
+import { cleanCache } from './cacheService';
 
 const db = SQLite.openDatabase('nomadManiaDb.db');
 const lastUpdateDate = storage.get('lastUpdateDate', StoreType.STRING) as string || '1990-01-01';
@@ -62,6 +63,7 @@ export const setupDatabaseAndSync = async (): Promise<void> => {
   await initializeDatabase();
   await syncDataWithServer();
   await updateStaticGeoJSON();
+  cleanCache();
 };
 
 export const updateMasterRanking = async () => {

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

@@ -34,6 +34,17 @@ export interface PostGetChatsListReturn extends ResponseType {
   }[];
 }
 
+interface Attachement {
+  id: number;
+  filename: string;
+  filetype: string;
+  attachment_small_url?: string;
+  attachment_full_url?: string;
+  lat?: number;
+  lng?: number;
+  attachment_link?: string;
+}
+
 interface Message {
   id: number;
   sender: number;
@@ -46,7 +57,7 @@ interface Message {
   reply_to_id: number;
   reactions: string;
   edits: string;
-  attachement: any;
+  attachement: -1 | Attachement;
   encrypted: 0 | 1;
 }
 
@@ -61,10 +72,12 @@ export interface PostSendMessage {
   to_uid: number;
   text: string;
   reply_to_id?: number;
+  attachment?: { uri: string; type: string; name?: string } | -1;
 }
 
 export interface PostSendMessageReturn extends ResponseType {
   message_id: number;
+  attachment?: any;
 }
 
 export interface PostMessagesReceivedOrRead {
@@ -138,8 +151,25 @@ export const chatApi = {
       no_of_messages,
       previous_than_message_id
     }),
-  sendMessage: (data: PostSendMessage) =>
-    request.postForm<PostSendMessageReturn>(API.SEND_MESSAGE, data),
+  sendMessage: (data: PostSendMessage) => {
+    const formData = new FormData();
+    formData.append('token', data.token);
+    formData.append('to_uid', data.to_uid.toString());
+    formData.append('text', data.text);
+    if (data.reply_to_id && data.reply_to_id !== undefined) {
+      formData.append('reply_to_id', data.reply_to_id.toString());
+    }
+    if (data.attachment && data.attachment !== -1) {
+      const { uri, type, name } = data.attachment;
+      formData.append('attachment', {
+        uri,
+        type: type === 'image' ? type + '/' + uri.split('.').pop()! : type,
+        name: name || 'file'
+      } as unknown as Blob);
+    }
+
+    return request.postForm<PostSendMessageReturn>(API.SEND_MESSAGE, formData);
+  },
   messagesReceived: (data: PostMessagesReceivedOrRead) =>
     request.postForm<ResponseType>(API.MESSAGES_RECEIVED, data),
   messagesRead: (data: PostMessagesReceivedOrRead) =>

+ 2 - 0
src/modules/api/ranking/ranking-api.tsx

@@ -25,7 +25,9 @@ export interface PostGetRanking extends ResponseType {
     flag2: string;
     badge_1281: number;
     badge_un: number;
+    badge_un_25: 0 | 1;
     badge_un_50: 0 | 1;
+    badge_un_75: 0 | 1;
     badge_un_100: 0 | 1;
     badge_un_150: 0 | 1;
     badge_un_193: 0 | 1;

+ 2 - 0
src/modules/api/regions/regions-api.tsx

@@ -81,7 +81,9 @@ export interface User {
   flag2: string | null;
   badge_1281: 0 | 1;
   badge_un: 0 | 1;
+  badge_un_25: 0 | 1;
   badge_un_50: 0 | 1;
+  badge_un_75: 0 | 1;
   badge_un_100: 0 | 1;
   badge_un_150: 0 | 1;
   badge_un_193: 0 | 1;

+ 2 - 0
src/modules/api/series/series-api.tsx

@@ -96,7 +96,9 @@ export interface RankingData {
     authenticated: 0 | 1;
     badge_nm: 0 | 1;
     badge_un: 0 | 1;
+    badge_un_25: 0 | 1;
     badge_un_50: 0 | 1;
+    badge_un_75: 0 | 1;
     badge_un_100: 0 | 1;
     badge_un_150: 0 | 1;
     badge_un_193: 0 | 1;

+ 1 - 0
src/modules/api/user/queries/index.ts

@@ -9,3 +9,4 @@ export * from './use-post-get-map-years';
 export * from './use-post-save-notification-token';
 export * from './use-post-get-update';
 export * from './use-post-authenticate';
+export * from './use-post-update-email';

+ 25 - 0
src/modules/api/user/queries/use-post-update-email.tsx

@@ -0,0 +1,25 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { userQueryKeys } from '../user-query-keys';
+import { userApi } from '../user-api';
+import { ResponseType } from '@api/response-type';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostUpdateEmailMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    {
+      token: string;
+      email: string;
+    },
+    ResponseType
+  >({
+    mutationKey: userQueryKeys.updateEmail(),
+    mutationFn: async (variables) => {
+      const response = await userApi.updateEmail(variables.token, variables.email);
+      return response.data;
+    }
+  });
+};

+ 5 - 1
src/modules/api/user/user-api.tsx

@@ -246,7 +246,9 @@ export interface PostGetProfileDataReturn extends ResponseType {
       badge_nm: number | null;
       badge_tbt: 0 | 1;
       badge_un: 0 | 1;
+      badge_un_25: 0 | 1;
       badge_un_50: 0 | 1;
+      badge_un_75: 0 | 1;
       badge_un_100: 0 | 1;
       badge_un_150: 0 | 1;
       badge_un_193: 0 | 1;
@@ -409,5 +411,7 @@ export const userApi = {
   getUpdate: <T extends string>(token: string, profile_id: number, type: T) =>
     request.postForm<PostGetUpdateReturn<T>>(API.GET_UPDATE, { token, profile_id, type }),
   autenticate: (token: string, profile_id: number) =>
-    request.postForm<ResponseType>(API.AUTHENTICATE, { token, profile_id })
+    request.postForm<ResponseType>(API.AUTHENTICATE, { token, profile_id }),
+  updateEmail: (token: string, email: string) =>
+    request.postForm<ResponseType>(API.UPDATE_EMAIL, { token, email })
 };

+ 2 - 1
src/modules/api/user/user-query-keys.tsx

@@ -9,5 +9,6 @@ export const userQueryKeys = {
   getMapYears: (userId: number) => ['getMapYears', userId] as const,
   setNotificationToken: () => ['setNotificationToken'] as const,
   getUpdate: (userId: number, type: string) => ['getUpdate', userId, type] as const,
-  autenticate: () => ['autenticate'] as const
+  autenticate: () => ['autenticate'] as const,
+  updateEmail: () => ['updateEmail'] as const,
 };

+ 8 - 0
src/screens/InAppScreens/MapScreen/UsersListScreen/Profile/index.tsx

@@ -8,7 +8,9 @@ import { AvatarWithInitials, WarningModal } from 'src/components';
 
 import TickIcon from 'assets/icons/tick.svg';
 import UNIcon from 'assets/icons/un_icon.svg';
+import UN25Icon from 'assets/icons/un-25.svg';
 import UN50Icon from 'assets/icons/un-50.svg';
+import UN75Icon from 'assets/icons/un-75.svg';
 import UN100Icon from 'assets/icons/un-100.svg';
 import UN150Icon from 'assets/icons/un-150.svg';
 import NMIcon from 'assets/icons/nm_icon.svg';
@@ -38,7 +40,9 @@ type Props = {
   badge_tbt?: number;
   badge_1281: number;
   badge_un: number;
+  badge_un_25: number;
   badge_un_50: number;
+  badge_un_75: number;
   badge_un_100: number;
   badge_un_150: number;
   auth: number;
@@ -59,7 +63,9 @@ export const Profile: FC<Props> = ({
   badge_tbt,
   badge_1281,
   badge_un,
+  badge_un_25,
   badge_un_50,
+  badge_un_75,
   badge_un_100,
   badge_un_150,
   auth,
@@ -135,7 +141,9 @@ export const Profile: FC<Props> = ({
                   {badge_1281 ? <NMIcon /> : null}
                   {badge_un_150 ? <UN150Icon /> : null}
                   {badge_un_100 ? <UN100Icon /> : null}
+                  {badge_un_75 ? <UN75Icon /> : null}
                   {badge_un_50 ? <UN50Icon /> : null}
+                  {badge_un_25 ? <UN25Icon /> : null}
                 </View>
               </View>
             </View>

+ 2 - 0
src/screens/InAppScreens/MapScreen/UsersListScreen/index.tsx

@@ -359,7 +359,9 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
             badge_tbt={item.badge_tbt}
             badge_1281={item.badge_1281}
             badge_un={item.badge_un}
+            badge_un_25={item.badge_un_25}
             badge_un_50={item.badge_un_50}
+            badge_un_75={item.badge_un_75}
             badge_un_100={item.badge_un_100}
             badge_un_150={item.badge_un_150}
             auth={item.auth}

+ 505 - 28
src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx

@@ -3,7 +3,6 @@ import {
   View,
   TouchableOpacity,
   Image,
-  Modal,
   Text,
   FlatList,
   Dimensions,
@@ -32,7 +31,7 @@ import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler'
 import { AvatarWithInitials, Header, WarningModal } from 'src/components';
 import { Colors } from 'src/theme';
 import { useFocusEffect, useNavigation } from '@react-navigation/native';
-import { Video } from 'expo-av';
+import { Audio } from 'expo-av';
 import ChatMessageBox from '../Components/ChatMessageBox';
 import ReplyMessageBar from '../Components/ReplyMessageBar';
 import { useSharedValue, withTiming } from 'react-native-reanimated';
@@ -62,9 +61,16 @@ import { usePushNotification } from 'src/contexts/PushNotificationContext';
 import ReactionsListModal from '../Components/ReactionsListModal';
 import { dismissChatNotifications } from '../utils';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
+import FileViewer from 'react-native-file-viewer';
+import * as FileSystem from 'expo-file-system';
+import ImageView from 'better-react-native-image-viewing';
+import * as MediaLibrary from 'expo-media-library';
 
 import BanIcon from 'assets/icons/messages/ban.svg';
 import AttachmentsModal from '../Components/AttachmentsModal';
+import RenderMessageVideo from '../Components/renderMessageVideo';
+import MessageLocation from '../Components/MessageLocation';
+import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
 
 const options = {
   enableVibrateFallback: true,
@@ -156,6 +162,418 @@ const ChatScreen = ({ route }: { route: any }) => {
     setModalInfo({ ...modalInfo, visible: false });
   };
 
+  useEffect(() => {
+    Audio.setAudioModeAsync({
+      allowsRecordingIOS: false,
+      staysActiveInBackground: false,
+      playsInSilentModeIOS: true,
+      shouldDuckAndroid: true,
+      playThroughEarpieceAndroid: false
+    });
+  }, []);
+
+  const onSendMedia = useCallback(
+    async (files: { uri: string; type: 'image' | 'video' }[]) => {
+      for (const file of files) {
+        const tempMessage: CustomMessage = {
+          _id: Date.now() + Math.random(),
+          text: '',
+          createdAt: new Date(),
+          user: { _id: +currentUserId, name: 'Me' },
+          reactions: {},
+          deleted: false,
+          attachment: {
+            id: -1,
+            filename: file.type,
+            filetype: file.type,
+            attachment_link: file.uri
+          },
+          pending: true,
+          isSending: true,
+          image: file.type === 'image' ? file.uri : undefined,
+          video: file.type === 'video' ? file.uri : undefined
+        };
+
+        if (replyMessage) {
+          tempMessage.replyMessage = {
+            text: replyMessage.text,
+            id: replyMessage._id,
+            name: replyMessage.user._id === id ? userName : 'Me'
+          };
+        }
+
+        setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [tempMessage]));
+
+        const messageData = {
+          token,
+          to_uid: id,
+          text: '',
+          reply_to_id: replyMessage ? (replyMessage._id as number) : -1,
+          attachment: {
+            uri: file.uri,
+            type: file.type,
+            name: file.uri.split('/').pop()
+          }
+        };
+
+        const res = await sendMessage(messageData, {
+          onSuccess: (res) => {
+            const { attachment, message_id } = res;
+
+            const newMessage = {
+              _id: message_id,
+              text: '',
+              attachment,
+              replyMessage: { ...tempMessage.replyMessage, sender: replyMessage?.user?._id },
+              image: file.type === 'image' ? API_HOST + attachment.attachment_full_url : undefined,
+              video: file.type === 'video' ? file.uri : undefined
+            };
+
+            setMessages((previousMessages) =>
+              (previousMessages ?? []).map((msg) =>
+                msg._id === tempMessage._id
+                  ? {
+                      ...msg,
+                      _id: res.message_id,
+                      attachment: res.attachment,
+                      isSending: false,
+                      image:
+                        res.attachment?.attachment_small_url && file.type === 'image'
+                          ? API_HOST + res.attachment.attachment_small_url
+                          : undefined,
+                      video: res.attachment?.attachment_link
+                        ? API_HOST + res.attachment.attachment_link
+                        : undefined
+                    }
+                  : msg
+              )
+            );
+
+            sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
+          }
+        });
+
+        clearReplyMessage();
+      }
+    },
+    [replyMessage]
+  );
+
+  const onSendLocation = useCallback(
+    async (coords: { latitude: number; longitude: number }) => {
+      const tempMessage: CustomMessage = {
+        _id: Date.now() + Math.random(),
+        text: '',
+        createdAt: new Date(),
+        user: { _id: +currentUserId, name: 'Me' },
+        pending: true,
+        deleted: false,
+        reactions: {},
+        attachment: {
+          id: -1,
+          filename: 'location.json',
+          filetype: 'nomadmania/location',
+          lat: coords.latitude,
+          lng: coords.longitude
+        }
+      };
+
+      if (replyMessage) {
+        tempMessage.replyMessage = {
+          text: replyMessage.text,
+          id: replyMessage._id,
+          name: replyMessage.user._id === id ? userName : 'Me'
+        };
+      }
+
+      setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [tempMessage]));
+
+      const locationData = JSON.stringify({ lat: coords.latitude, lng: coords.longitude });
+      const fileUri = FileSystem.documentDirectory + 'location.json';
+      await FileSystem.writeAsStringAsync(fileUri, locationData);
+
+      const locationFile = {
+        uri: fileUri,
+        type: 'application/json',
+        name: 'location.json'
+      };
+
+      const messageData = {
+        token,
+        to_uid: id,
+        text: tempMessage.text,
+        reply_to_id: replyMessage ? (replyMessage._id as number) : -1,
+        attachment: locationFile
+      };
+
+      sendMessage(messageData, {
+        onSuccess: async (res) => {
+          const { attachment, message_id } = res;
+
+          const newMessage = {
+            _id: message_id,
+            text: '',
+            attachment,
+            replyMessage: { ...tempMessage.replyMessage, sender: replyMessage?.user?._id }
+          };
+
+          setMessages((previousMessages) =>
+            (previousMessages ?? []).map((msg) =>
+              msg._id === tempMessage._id ? { ...msg, _id: res.message_id } : msg
+            )
+          );
+
+          sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
+          await FileSystem.deleteAsync(fileUri);
+        },
+        onError: async (err) => {
+          await FileSystem.deleteAsync(fileUri);
+        }
+      });
+
+      clearReplyMessage();
+    },
+    [replyMessage]
+  );
+
+  const onSendFile = useCallback(
+    (files: { uri: string; type: string; name?: string }[]) => {
+      const newMsgs = files.map((file) => {
+        const msg: CustomMessage = {
+          _id: Date.now() + Math.random(),
+          text: '',
+          createdAt: new Date(),
+          user: { _id: +currentUserId, name: 'Me' },
+          deleted: false,
+          reactions: {},
+          isSending: true,
+          attachment: {
+            id: -1,
+            filename: file.name ?? 'File',
+            filetype: file.type,
+            attachment_link: file.uri
+          }
+        };
+
+        if (replyMessage) {
+          msg.replyMessage = {
+            text: replyMessage.text,
+            id: replyMessage._id,
+            name: replyMessage.user._id === id ? userName : 'Me'
+          };
+        }
+
+        if (file.type.includes('image')) {
+          msg.image = file.uri;
+        } else if (file.type.includes('video')) {
+          msg.video = file.uri;
+        }
+
+        setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [msg]));
+
+        const messageData = {
+          token,
+          to_uid: id,
+          text: '',
+          reply_to_id: replyMessage ? (replyMessage._id as number) : -1,
+          attachment: {
+            uri: file.uri,
+            type: file.type,
+            name: file.name || file.uri.split('/').pop()
+          }
+        };
+
+        sendMessage(messageData, {
+          onSuccess: (res) => {
+            const { attachment, message_id } = res;
+
+            const newMessage = {
+              _id: message_id,
+              text: '',
+              attachment,
+              replyMessage: { ...msg.replyMessage, sender: replyMessage?.user?._id },
+              image: file.type === 'image' ? API_HOST + attachment.attachment_full_url : undefined,
+              video: file.type === 'video' ? file.uri : undefined
+            };
+
+            setMessages((previousMessages) =>
+              (previousMessages ?? []).map((prevMsg) =>
+                prevMsg._id === msg._id
+                  ? {
+                      ...prevMsg,
+                      _id: res.message_id,
+                      attachment: res.attachment,
+                      isSending: false,
+                      image:
+                        res.attachment?.attachment_small_url && file.type?.startsWith('image')
+                          ? API_HOST + res.attachment.attachment_small_url
+                          : undefined,
+                      video:
+                        res.attachment?.attachment_link && file.type?.startsWith('video')
+                          ? API_HOST + res.attachment.attachment_link
+                          : undefined
+                    }
+                  : prevMsg
+              )
+            );
+
+            sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
+          }
+        });
+
+        return msg;
+      });
+
+      clearReplyMessage();
+    },
+    [replyMessage]
+  );
+
+  async function openFileInApp(uri: string, fileName: string) {
+    try {
+      const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
+      if (!dirExist.exists) {
+        await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
+      }
+
+      const fileUri = `${CACHED_ATTACHMENTS_DIR}${fileName}`;
+
+      const fileExists = await FileSystem.getInfoAsync(fileUri);
+      if (fileExists.exists) {
+        await FileViewer.open(fileUri, {
+          showOpenWithDialog: true,
+          showAppsSuggestions: true
+        });
+
+        return;
+      }
+
+      const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri, {
+        headers: { Nmtoken: token }
+      });
+
+      await FileViewer.open(localUri, {
+        showOpenWithDialog: true,
+        showAppsSuggestions: true
+      });
+    } catch (err) {
+      console.warn('openFileInApp error:', err);
+      Alert.alert('Cannot open file', 'No application found to open this file.');
+    }
+  }
+
+  async function downloadFileToDevice(currentMessage: CustomMessage) {
+    if (!currentMessage.image && !currentMessage.video) {
+      return;
+    }
+
+    const fileUrl = currentMessage.video
+      ? currentMessage.video
+      : API_HOST + currentMessage.attachment?.attachment_full_url;
+    const fileType = currentMessage.attachment?.filetype || '';
+    const fileExt = fileType.split('/').pop() || '';
+    const fileName = currentMessage.attachment?.filename?.split('.')[0] || 'file';
+    const fileUri = `${FileSystem.cacheDirectory}${fileName}.${fileExt}`;
+
+    try {
+      const { status } = await MediaLibrary.requestPermissionsAsync();
+      if (status !== 'granted') {
+        return;
+      }
+
+      const downloadOptions = currentMessage.video ? { headers: { Nmtoken: token } } : undefined;
+      const { uri } = await FileSystem.downloadAsync(fileUrl, fileUri, downloadOptions);
+
+      await MediaLibrary.createAssetAsync(uri);
+
+      Alert.alert(
+        'Success',
+        `${fileType.startsWith('video') ? 'Video' : 'Image'} saved to gallery.`
+      );
+    } catch (error) {
+      Alert.alert('Error', 'Failed to download the file.');
+    }
+  }
+  const renderMessageFile = (props: BubbleProps<CustomMessage>) => {
+    const { currentMessage } = props;
+    const leftMessage = currentMessage?.user?._id !== +currentUserId;
+    if (!currentMessage?.attachment) return null;
+
+    const { attachment_link, filename } = currentMessage.attachment;
+    const fileName = filename ?? 'Attachment';
+    const uri = API_HOST + attachment_link;
+
+    return (
+      <TouchableOpacity
+        style={[
+          styles.fileContainer,
+          { backgroundColor: leftMessage ? 'rgba(15, 63, 79, 0.2)' : 'rgba(244, 244, 244, 0.2)' }
+        ]}
+        onPress={() => {
+          openFileInApp(uri, fileName);
+        }}
+        onLongPress={() => handleLongPress(currentMessage, props)}
+        disabled={currentMessage?.isSending}
+      >
+        {currentMessage?.isSending ? (
+          <ActivityIndicator
+            size="small"
+            color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
+          />
+        ) : (
+          <MaterialCommunityIcons
+            name="file"
+            size={32}
+            color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
+          />
+        )}
+        <Text
+          style={[
+            styles.fileNameText,
+            { color: leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT }
+          ]}
+        >
+          {fileName}
+        </Text>
+      </TouchableOpacity>
+    );
+  };
+
+  const renderMessageLocation = (props: BubbleProps<CustomMessage>) => {
+    const { currentMessage } = props;
+    if (!currentMessage?.attachment) return null;
+
+    const { lat, lng } = currentMessage.attachment;
+    if (!lat || !lng) return null;
+
+    return (
+      <View
+        style={[
+          {
+            alignItems: 'center',
+            borderRadius: 8,
+            marginVertical: 6,
+            marginHorizontal: 6,
+            width: 220
+          }
+        ]}
+      >
+        <MessageLocation props={props} lat={lat} lng={lng} onLongPress={handleLongPress} />
+      </View>
+    );
+  };
+
+  const onShareLiveLocation = useCallback(() => {
+    const liveMsg: IMessage = {
+      _id: 'live-loc-' + Date.now(),
+      text: 'Sharing live location...',
+      createdAt: new Date(),
+      user: { _id: +currentUserId, name: 'Me' },
+      system: false
+    };
+    setMessages((prev) => GiftedChat.append(prev, [liveMsg]));
+  }, []);
+
   useEffect(() => {
     let unsubscribe: any;
 
@@ -389,7 +807,7 @@ const ChatScreen = ({ route }: { route: any }) => {
           reply_to: message.replyMessage ?? null,
           reactions: message.reactions ?? '{}',
           status: 2,
-          attachement: -1
+          attachement: message.attachment ? message.attachment : -1
         };
       }
 
@@ -448,7 +866,16 @@ const ChatScreen = ({ route }: { route: any }) => {
       pending: message.status === 1,
       sent: message.status === 2,
       received: message.status === 3,
-      deleted: message.status === 4
+      deleted: message.status === 4,
+      isSending: false,
+      video:
+        message.attachement !== -1 && message.attachement?.filetype?.startsWith('video')
+          ? API_HOST + message.attachement?.attachment_link
+          : null,
+      image:
+        message.attachement !== -1 && message.attachement?.filetype?.startsWith('image')
+          ? API_HOST + message.attachement?.attachment_small_url
+          : null
     };
   };
 
@@ -659,7 +1086,10 @@ const ChatScreen = ({ route }: { route: any }) => {
                     text: 'This message was deleted',
                     pending: false,
                     sent: false,
-                    received: false
+                    received: false,
+                    attachment: null,
+                    image: undefined,
+                    video: undefined
                   };
                 }
                 return msg;
@@ -691,6 +1121,10 @@ const ChatScreen = ({ route }: { route: any }) => {
         handleDeleteMessage(selectedMessage.currentMessage?._id);
         setIsModalVisible(false);
         break;
+      case 'download':
+        downloadFileToDevice(selectedMessage.currentMessage);
+        setIsModalVisible(false);
+        break;
       default:
         break;
     }
@@ -815,6 +1249,15 @@ const ChatScreen = ({ route }: { route: any }) => {
             }}
             renderTicks={() => null}
             renderTime={renderTimeContainer}
+            renderCustomView={() =>
+              selectedMessage.currentMessage.attachment?.filetype === 'nomadmania/location'
+                ? renderMessageLocation(selectedMessage)
+                : selectedMessage.currentMessage.attachment &&
+                    !selectedMessage.currentMessage.image &&
+                    !selectedMessage.currentMessage.video
+                  ? renderMessageFile(selectedMessage)
+                  : renderReplyMessageView(selectedMessage)
+            }
           />
         </ScrollView>
       </View>
@@ -870,8 +1313,7 @@ const ChatScreen = ({ route }: { route: any }) => {
               )
             );
             sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
-          },
-          onError: (err) => console.log('err', err)
+          }
         }
       );
 
@@ -910,8 +1352,7 @@ const ChatScreen = ({ route }: { route: any }) => {
           if (message) {
             sendWebSocketMessage('new_reaction', message, reaction);
           }
-        },
-        onError: (err) => console.log('err', err)
+        }
       }
     );
 
@@ -1015,12 +1456,34 @@ const ChatScreen = ({ route }: { route: any }) => {
 
   const renderMessageImage = (props: any) => {
     const { currentMessage } = props;
+    const leftMessage = currentMessage?.user?._id !== +currentUserId;
+
     return (
       <TouchableOpacity
-        onPress={() => setSelectedMedia(currentMessage.image)}
+        onPress={() => setSelectedMedia(API_HOST + currentMessage.attachment.attachment_full_url)}
+        onLongPress={() => handleLongPress(currentMessage, props)}
         style={styles.imageContainer}
+        disabled={currentMessage.isSending}
       >
         <Image source={{ uri: currentMessage.image }} style={styles.chatImage} resizeMode="cover" />
+        {currentMessage.isSending && (
+          <View
+            style={{
+              position: 'absolute',
+              top: 0,
+              left: 0,
+              right: 0,
+              bottom: 0,
+              justifyContent: 'center',
+              alignItems: 'center'
+            }}
+          >
+            <ActivityIndicator
+              size="large"
+              color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
+            />
+          </View>
+        )}
       </TouchableOpacity>
     );
   };
@@ -1126,6 +1589,13 @@ const ChatScreen = ({ route }: { route: any }) => {
           onLongPress={() => handleLongPress(currentMessage, props)}
           renderTicks={() => null}
           renderTime={renderTimeContainer}
+          renderCustomView={() =>
+            currentMessage.attachment?.filetype === 'nomadmania/location'
+              ? renderMessageLocation(props)
+              : currentMessage.attachment && !currentMessage.image && !currentMessage.video
+                ? renderMessageFile(props)
+                : renderReplyMessageView(props)
+          }
         />
       </View>
     );
@@ -1136,7 +1606,12 @@ const ChatScreen = ({ route }: { route: any }) => {
       payload: {
         name: userName,
         uid: id,
-        setModalInfo
+        setModalInfo,
+        closeOptions: () => {},
+        onSendMedia,
+        onSendLocation,
+        onShareLiveLocation,
+        onSendFile
       } as any
     });
   };
@@ -1273,7 +1748,18 @@ const ChatScreen = ({ route }: { route: any }) => {
                 {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
               </View>
             )}
-            textInputProps={{ ...styles.composer, selectionColor: Colors.LIGHT_GRAY }}
+            renderMessageVideo={(props) => (
+              <RenderMessageVideo
+                props={props}
+                token={token}
+                currentUserId={+currentUserId}
+                onLongPress={handleLongPress}
+              />
+            )}
+            textInputProps={{
+              ...styles.composer,
+              selectionColor: Colors.LIGHT_GRAY
+            }}
             placeholder=""
             renderMessage={(props) => (
               <ChatMessageBox
@@ -1325,22 +1811,13 @@ const ChatScreen = ({ route }: { route: any }) => {
           <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
         )}
 
-        <Modal visible={!!selectedMedia} transparent={true}>
-          <View style={styles.modalContainer}>
-            {selectedMedia && selectedMedia?.includes('.mp4') ? (
-              <Video
-                source={{ uri: selectedMedia }}
-                style={styles.fullScreenMedia}
-                useNativeControls
-              />
-            ) : (
-              <Image source={{ uri: selectedMedia ?? '' }} style={styles.fullScreenMedia} />
-            )}
-            <TouchableOpacity onPress={() => setSelectedMedia(null)} style={styles.closeButton}>
-              <MaterialCommunityIcons name="close" size={30} color="white" />
-            </TouchableOpacity>
-          </View>
-        </Modal>
+        <ImageView
+          images={[{ uri: selectedMedia, cache: 'force-cache' }]}
+          imageIndex={0}
+          visible={!!selectedMedia}
+          onRequestClose={() => setSelectedMedia(null)}
+          backgroundColor={Colors.DARK_BLUE}
+        />
 
         <ReactModal
           isVisible={isModalVisible}

+ 45 - 1
src/screens/InAppScreens/MessagesScreen/ChatScreen/styles.tsx

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

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

@@ -0,0 +1,1658 @@
+import React, { useState, useCallback, useEffect, useRef } from 'react';
+import {
+  View,
+  TouchableOpacity,
+  Image,
+  Modal,
+  Text,
+  FlatList,
+  Dimensions,
+  Alert,
+  ScrollView,
+  Linking,
+  ActivityIndicator,
+  AppState,
+  AppStateStatus,
+  TextInput,
+  Keyboard,
+  Platform,
+  UIManager,
+  LayoutAnimation,
+  Animated,
+  Easing,
+  InputAccessoryView,
+  KeyboardAvoidingView
+} from 'react-native';
+import {
+  GiftedChat,
+  Bubble,
+  InputToolbar,
+  IMessage,
+  Send,
+  BubbleProps,
+  Composer,
+  TimeProps,
+  MessageProps,
+  Actions
+} from 'react-native-gifted-chat';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler';
+import { AvatarWithInitials, Header, WarningModal } from 'src/components';
+import { Colors } from 'src/theme';
+import { useFocusEffect, useNavigation } from '@react-navigation/native';
+import { ResizeMode, Video } from 'expo-av';
+import ChatMessageBox from '../Components/ChatMessageBox';
+import ReplyMessageBar from '../Components/ReplyMessageBar';
+import { useSharedValue, withTiming } from 'react-native-reanimated';
+import { BlurView } from 'expo-blur';
+import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
+import Clipboard from '@react-native-clipboard/clipboard';
+import { trigger } from 'react-native-haptic-feedback';
+import ReactModal from 'react-native-modal';
+import { storage, StoreType } from 'src/storage';
+import {
+  usePostDeleteMessageMutation,
+  usePostGetChatWithQuery,
+  usePostMessagesReadMutation,
+  usePostReactToMessageMutation,
+  usePostSendMessageMutation
+} from '@api/chat';
+import { CustomMessage, Message, Reaction } from '../types';
+import { API_HOST, WEBSOCKET_URL } from 'src/constants';
+import ReactionBar from '../Components/ReactionBar';
+import OptionsMenu from '../Components/OptionsMenu';
+import EmojiSelectorModal from '../Components/EmojiSelectorModal';
+import { styles } from './styles';
+import SendIcon from 'assets/icons/messages/send.svg';
+import { SheetManager } from 'react-native-actions-sheet';
+import { NAVIGATION_PAGES } from 'src/types';
+import { usePushNotification } from 'src/contexts/PushNotificationContext';
+import ReactionsListModal from '../Components/ReactionsListModal';
+import { dismissChatNotifications } from '../utils';
+import { useMessagesStore } from 'src/stores/unreadMessagesStore';
+import * as Location from 'expo-location';
+
+import BanIcon from 'assets/icons/messages/ban.svg';
+import AttachmentsModal from '../Components/AttachmentsModal';
+import MegaphoneIcon from 'assets/icons/messages/megaphone.svg';
+import ChatOptionsBlock from '../Components/ChatOptionsBlock';
+
+// if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
+//   UIManager.setLayoutAnimationEnabledExperimental(false);
+// }
+
+const options = {
+  enableVibrateFallback: true,
+  ignoreAndroidSystemSettings: false
+};
+
+const reactionEmojis = ['👍', '❤️', '😂', '😮', '😭'];
+
+const ChatScreen = ({ route }: { route: any }) => {
+  const token = storage.get('token', StoreType.STRING) as string;
+  const {
+    id,
+    name,
+    avatar,
+    userType
+  }: {
+    id: number;
+    name: string;
+    avatar: string | null;
+    userType: 'normal' | 'not_exist' | 'blocked';
+  } = route.params;
+  const userName =
+    userType === 'blocked'
+      ? 'Account is blocked'
+      : userType === 'not_exist'
+        ? 'Account does not exist'
+        : name;
+
+  const currentUserId = storage.get('uid', StoreType.STRING) as number;
+  const insets = useSafeAreaInsets();
+  const [messages, setMessages] = useState<CustomMessage[] | null>();
+  const navigation = useNavigation();
+  const [prevThenMessageId, setPrevThenMessageId] = useState<number>(-1);
+  const {
+    data: chatData,
+    refetch,
+    isFetching
+  } = usePostGetChatWithQuery(token, id, 50, prevThenMessageId, true);
+  const { mutateAsync: sendMessage } = usePostSendMessageMutation();
+
+  const swipeableRowRef = useRef<Swipeable | null>(null);
+  const messageContainerRef = useRef<FlatList<IMessage> | null>(null);
+  const [selectedMedia, setSelectedMedia] = useState<any>(null);
+
+  const [replyMessage, setReplyMessage] = useState<CustomMessage | null>(null);
+  const [modalInfo, setModalInfo] = useState({
+    visible: false,
+    type: 'confirm',
+    message: '',
+    action: () => {},
+    buttonTitle: '',
+    title: ''
+  });
+
+  const [selectedMessage, setSelectedMessage] = useState<BubbleProps<CustomMessage> | null>(null);
+  const [emojiSelectorVisible, setEmojiSelectorVisible] = useState(false);
+  const [messagePosition, setMessagePosition] = useState<{
+    x: number;
+    y: number;
+    width: number;
+    height: number;
+    isMine: boolean;
+  } | null>(null);
+
+  const [isModalVisible, setIsModalVisible] = useState(false);
+  const [unreadMessageIndex, setUnreadMessageIndex] = useState<number | null>(null);
+  const { mutateAsync: markMessagesAsRead } = usePostMessagesReadMutation();
+  const { mutateAsync: deleteMessage } = usePostDeleteMessageMutation();
+  const { mutateAsync: reactToMessage } = usePostReactToMessageMutation();
+
+  const [highlightedMessageId, setHighlightedMessageId] = useState<number | null>(null);
+  const [isRerendering, setIsRerendering] = useState<boolean>(false);
+  const [isTyping, setIsTyping] = useState<boolean>(false);
+
+  const messageRefs = useRef<{ [key: string]: any }>({});
+  const flatList = useRef<FlatList | null>(null);
+  const scrollY = useSharedValue(0);
+  const { isSubscribed } = usePushNotification();
+  const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
+  const [hasMoreMessages, setHasMoreMessages] = useState(true);
+  const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
+
+  const [showOptions, setShowOptions] = useState<boolean>(false);
+  const [keyboardHeight, setKeyboardHeight] = useState<number>(0);
+  const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
+  const [lastKeyboardHeight, setLastKeyboardHeight] = useState(300);
+
+  const appState = useRef(AppState.currentState);
+  const textInputRef = useRef<TextInput>(null);
+
+  const socket = useRef<WebSocket | null>(null);
+
+  const closeModal = () => {
+    setModalInfo({ ...modalInfo, visible: false });
+  };
+
+  const translateY = useRef(new Animated.Value(0)).current;
+  const keyboardAnimDuration = useRef(250);
+
+  // useEffect(() => {
+  //   const onKeyboardWillShow = (e: any) => {
+  //     setIsKeyboardOpen(true);
+  //     setLastKeyboardHeight(e.endCoordinates.height - insets.bottom || 300);
+  //     keyboardAnimDuration.current = e.duration;
+  //   };
+
+  //   const onKeyboardWillHide = (e: any) => {
+  //     setIsKeyboardOpen(false);
+  //   };
+
+  //   const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
+  //   const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
+
+  //   const kbShowSub = Keyboard.addListener(showEvent, onKeyboardWillShow);
+  //   const kbHideSub = Keyboard.addListener(hideEvent, onKeyboardWillHide);
+
+  //   return () => {
+  //     kbShowSub.remove();
+  //     kbHideSub.remove();
+  //   };
+  // }, [insets.bottom, showOptions, translateY]);
+
+  // useEffect(() => {
+  //   let toValue = 0;
+  //   if (isKeyboardOpen) {
+  //     toValue = keyboardHeight;
+  //   } else if (showOptions) {
+  //     toValue = keyboardHeight;
+  //   } else {
+  //     toValue = 0;
+  //   }
+
+  //   Animated.timing(translateY, {
+  //     toValue,
+  //     duration: keyboardAnimDuration.current ?? 250,
+  //     easing: Easing.inOut(Easing.ease),
+  //     useNativeDriver: false
+  //   }).start();
+  // }, [isKeyboardOpen, showOptions, keyboardHeight]);
+
+  const handleInputFocus = () => {
+    // if (!showOptions) {
+    //   setImmediate(() => {
+    //     if (Platform.OS === 'ios') {
+    //       LayoutAnimation.configureNext({
+    //         duration: keyboardAnimDuration.current ?? 250,
+    //         update: { type: 'keyboard' }
+    //       });
+    //     }
+    //     setShowOptions(true);
+    //     Animated.timing(translateY, {
+    //       toValue: keyboardHeight,
+    //       duration: keyboardAnimDuration.current ?? 250,
+    //       easing: Easing.inOut(Easing.ease),
+    //       useNativeDriver: false
+    //     }).start();
+    //   });
+    // }
+  };
+  const toggleOptions = useCallback(() => {
+    if (showOptions && !isKeyboardOpen) {
+      textInputRef.current?.focus();
+    } else {
+      if (isKeyboardOpen) {
+        Keyboard.dismiss();
+        setShowOptions(true);
+      } else {
+        if (Platform.OS === 'ios') {
+          LayoutAnimation.configureNext({
+            duration: keyboardAnimDuration.current ?? 250,
+            update: { type: 'keyboard' }
+          });
+        }
+        !showOptions && setShowOptions(true);
+      }
+    }
+  }, [showOptions, isKeyboardOpen]);
+
+  // const handleTouchOutside = () => {
+  //   if (showOptions) {
+  //     if (Platform.OS === 'ios') {
+  //       LayoutAnimation.configureNext({
+  //         duration: keyboardAnimDuration.current ?? 250,
+  //         update: { type: 'keyboard' }
+  //       });
+  //     }
+  //     setShowOptions(false);
+  //   }
+  //   Keyboard.dismiss();
+  // };
+
+  const onSendMedia = useCallback((files: { uri: string; type: 'image' }[]) => {
+    const newMsgs = files.map((file) => {
+      const msg: IMessage = {
+        _id: Date.now() + Math.random(),
+        text: '',
+        createdAt: new Date(),
+        user: { _id: +currentUserId, name: 'Me' }
+      };
+
+      if (file.type === 'image') {
+        msg.image = file.uri;
+      } else if (file.type === 'video') {
+        msg.video = file.uri;
+      }
+      return msg;
+    });
+
+    setMessages((prev) => GiftedChat.append(prev, newMsgs));
+  }, []);
+
+  const onSendLocation = useCallback((coords: { latitude: number; longitude: number }) => {
+    const locMsg: IMessage = {
+      _id: Date.now() + Math.random(),
+      text: `Location: lat=${coords.latitude}, lon=${coords.longitude}`,
+      createdAt: new Date(),
+      user: { _id: +currentUserId, name: 'Me' },
+      location: coords
+    };
+    setMessages((prev) => GiftedChat.append(prev, [locMsg]));
+  }, []);
+
+  const onShareLiveLocation = useCallback(() => {
+    const liveMsg: IMessage = {
+      _id: 'live-loc-' + Date.now(),
+      text: 'Sharing live location...',
+      createdAt: new Date(),
+      user: { _id: +currentUserId, name: 'Me' },
+      system: false
+    };
+    setMessages((prev) => GiftedChat.append(prev, [liveMsg]));
+  }, []);
+
+  const renderMessageVideo = (props: any) => {
+    const { currentMessage } = props;
+    if (!currentMessage?.video) return null;
+
+    return (
+      <View style={{ width: 200, height: 200, backgroundColor: '#000' }}>
+        <Video
+          source={{ uri: currentMessage.video }}
+          style={{ flex: 1 }}
+          useNativeControls
+          resizeMode={ResizeMode.CONTAIN}
+        />
+      </View>
+    );
+  };
+
+  const renderOptionsView = () => {
+    // if (!showOptions) return null;
+    return (
+      <Animated.View
+        style={{
+          transform: [{ translateY }]
+        }}
+      >
+        <ChatOptionsBlock
+          blockHeight={lastKeyboardHeight}
+          closeOptions={() => setShowOptions(false)}
+          onSendMedia={onSendMedia}
+          onSendLocation={onSendLocation}
+          onShareLiveLocation={onShareLiveLocation}
+        />
+      </Animated.View>
+    );
+  };
+
+  useEffect(() => {
+    let unsubscribe: any;
+
+    const setupNotificationHandler = async () => {
+      unsubscribe = await dismissChatNotifications(id, isSubscribed, setModalInfo, navigation);
+    };
+
+    setupNotificationHandler();
+
+    return () => {
+      if (unsubscribe) unsubscribe();
+      updateUnreadMessagesCount();
+    };
+  }, [id]);
+
+  useEffect(() => {
+    socket.current = new WebSocket(WEBSOCKET_URL);
+
+    socket.current.onopen = () => {
+      socket.current?.send(JSON.stringify({ token }));
+    };
+
+    socket.current.onmessage = (event) => {
+      const data = JSON.parse(event.data);
+      handleWebSocketMessage(data);
+    };
+
+    socket.current.onclose = () => {
+      console.log('WebSocket connection closed chat screen');
+    };
+
+    return () => {
+      if (socket.current) {
+        socket.current.close();
+        socket.current = null;
+      }
+    };
+  }, [token]);
+
+  useEffect(() => {
+    const handleAppStateChange = async (nextAppState: AppStateStatus) => {
+      if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
+        if (!socket.current || socket.current.readyState === WebSocket.CLOSED) {
+          socket.current = new WebSocket(WEBSOCKET_URL);
+          socket.current.onopen = () => {
+            socket.current?.send(JSON.stringify({ token }));
+          };
+          socket.current.onmessage = (event) => {
+            const data = JSON.parse(event.data);
+            handleWebSocketMessage(data);
+          };
+        }
+
+        await dismissChatNotifications(id, isSubscribed, setModalInfo, navigation);
+      }
+    };
+
+    const subscription = AppState.addEventListener('change', handleAppStateChange);
+
+    return () => {
+      subscription.remove();
+      if (socket.current) {
+        socket.current.close();
+        socket.current = null;
+      }
+    };
+  }, [token]);
+
+  const handleWebSocketMessage = (data: any) => {
+    switch (data.action) {
+      case 'new_message':
+        if (data.conversation_with === id && data.message) {
+          const newMessage = mapApiMessageToGiftedMessage(data.message);
+          setMessages((previousMessages) => {
+            const messageExists =
+              previousMessages && previousMessages.some((msg) => msg._id === newMessage._id);
+            if (!messageExists) {
+              return GiftedChat.append(previousMessages ?? [], [newMessage]);
+            }
+            return previousMessages;
+          });
+        }
+        break;
+
+      case 'new_reaction':
+        if (data.conversation_with === id && data.reaction) {
+          updateMessageWithReaction(data.reaction);
+        }
+        break;
+
+      case 'unreact':
+        if (data.conversation_with === id && data.unreacted_message_id) {
+          removeReactionFromMessage(data.unreacted_message_id);
+        }
+        break;
+
+      case 'delete_message':
+        if (data.conversation_with === id && data.deleted_message_id) {
+          removeDeletedMessage(data.deleted_message_id);
+        }
+        break;
+
+      case 'is_typing':
+        if (data.conversation_with === id) {
+          setIsTyping(true);
+        }
+        break;
+
+      case 'stopped_typing':
+        if (data.conversation_with === id) {
+          setIsTyping(false);
+        }
+        break;
+
+      case 'messages_read':
+        if (data.conversation_with === id && data.read_messages_ids) {
+          setMessages(
+            (prevMessages) =>
+              prevMessages?.map((msg) => {
+                if (data.read_messages_ids.includes(msg._id)) {
+                  return { ...msg, received: true };
+                }
+                return msg;
+              }) ?? []
+          );
+        }
+        break;
+
+      default:
+        break;
+    }
+  };
+
+  const updateMessageWithReaction = (reactionData: any) => {
+    setMessages(
+      (prevMessages) =>
+        prevMessages?.map((msg) => {
+          if (msg._id === reactionData.message_id) {
+            const updatedReactions = [
+              ...(Array.isArray(msg.reactions)
+                ? msg.reactions?.filter((r: any) => r.uid !== reactionData.uid)
+                : []),
+              reactionData
+            ];
+            return { ...msg, reactions: updatedReactions };
+          }
+          return msg;
+        }) ?? []
+    );
+  };
+
+  const removeReactionFromMessage = (messageId: number) => {
+    setMessages(
+      (prevMessages) =>
+        prevMessages?.map((msg) => {
+          if (msg._id === messageId) {
+            const updatedReactions = Array.isArray(msg.reactions)
+              ? msg.reactions?.filter((r: any) => r.uid !== id)
+              : [];
+            return { ...msg, reactions: updatedReactions };
+          }
+          return msg;
+        }) ?? []
+    );
+  };
+
+  const removeDeletedMessage = (messageId: number) => {
+    setMessages(
+      (prevMessages) =>
+        prevMessages?.map((msg) => {
+          if (msg._id === messageId) {
+            return {
+              ...msg,
+              deleted: true,
+              text: 'This message was deleted',
+              pending: false,
+              sent: false,
+              received: false
+            };
+          }
+          return msg;
+        }) ?? []
+    );
+  };
+
+  useEffect(() => {
+    const pingInterval = setInterval(() => {
+      if (socket.current && socket.current.readyState === WebSocket.OPEN) {
+        socket.current.send(JSON.stringify({ action: 'ping', conversation_with: id }));
+      } else {
+        socket.current = new WebSocket(WEBSOCKET_URL);
+        socket.current.onopen = () => {
+          socket.current?.send(JSON.stringify({ token }));
+        };
+        socket.current.onmessage = (event) => {
+          const data = JSON.parse(event.data);
+          handleWebSocketMessage(data);
+        };
+
+        return () => {
+          if (socket.current) {
+            socket.current.close();
+            socket.current = null;
+          }
+        };
+      }
+    }, 50000);
+
+    return () => clearInterval(pingInterval);
+  }, []);
+
+  const sendWebSocketMessage = (
+    action: string,
+    message: CustomMessage | null = null,
+    reaction: string | null = null,
+    readMessagesIds: number[] | null = null
+  ) => {
+    if (socket.current && socket.current.readyState === WebSocket.OPEN) {
+      const data: any = {
+        action,
+        conversation_with: id
+      };
+
+      if (action === 'new_message' && message) {
+        data.message = {
+          id: message._id,
+          text: message.text,
+          sender: +currentUserId,
+          sent_datetime: new Date().toISOString().replace('T', ' ').substring(0, 19),
+          reply_to_id: message.replyMessage?.id ?? -1,
+          reply_to: message.replyMessage ?? null,
+          reactions: message.reactions ?? '{}',
+          status: 2,
+          attachement: -1
+        };
+      }
+
+      if (action === 'new_reaction' && message && reaction) {
+        data.reaction = {
+          message_id: message._id,
+          reaction,
+          uid: +currentUserId,
+          datetime: new Date().toISOString()
+        };
+      }
+
+      if (action === 'unreact' && message) {
+        data.message_id = message._id;
+      }
+
+      if (action === 'delete_message' && message) {
+        data.message_id = message._id;
+      }
+
+      if (action === 'messages_read' && readMessagesIds) {
+        data.messages_ids = readMessagesIds;
+      }
+
+      socket.current.send(JSON.stringify(data));
+    }
+  };
+
+  const handleTyping = (isTyping: boolean) => {
+    if (isTyping) {
+      sendWebSocketMessage('is_typing');
+    } else {
+      sendWebSocketMessage('stopped_typing');
+    }
+  };
+
+  const mapApiMessageToGiftedMessage = (message: Message): CustomMessage => {
+    return {
+      _id: message.id,
+      text: message.text,
+      createdAt: new Date(message.sent_datetime + 'Z'),
+      user: {
+        _id: message.sender,
+        name: message.sender === id ? userName : 'Me'
+      },
+      replyMessage:
+        message.reply_to_id !== -1
+          ? {
+              text: message.reply_to.text,
+              id: message.reply_to.id,
+              name: message.reply_to.sender === id ? userName : 'Me'
+            }
+          : null,
+      reactions: JSON.parse(message.reactions || '{}'),
+      attachment: message.attachement !== -1 ? message.attachement : null,
+      pending: message.status === 1,
+      sent: message.status === 2,
+      received: message.status === 3,
+      deleted: message.status === 4
+    };
+  };
+
+  useFocusEffect(
+    useCallback(() => {
+      refetch();
+    }, [])
+  );
+
+  useFocusEffect(
+    useCallback(() => {
+      if (chatData?.messages) {
+        const mappedMessages = chatData.messages.map(mapApiMessageToGiftedMessage);
+
+        if (unreadMessageIndex === null && !isFetching) {
+          const firstUnreadIndex = mappedMessages.findLastIndex(
+            (msg) => !msg.received && !msg?.deleted && msg.user._id === id
+          );
+
+          if (firstUnreadIndex !== -1) {
+            setUnreadMessageIndex(firstUnreadIndex);
+
+            const unreadMarker: any = {
+              _id: 'unreadMarker',
+              text: 'Unread messages',
+              system: true
+            };
+
+            mappedMessages.splice(firstUnreadIndex + 1, 0, unreadMarker);
+            setTimeout(() => {
+              if (flatList.current) {
+                flatList.current.scrollToIndex({
+                  index: firstUnreadIndex,
+                  animated: true,
+                  viewPosition: 0.5
+                });
+              }
+            }, 500);
+          } else {
+            setUnreadMessageIndex(0);
+          }
+        }
+
+        setMessages((previousMessages) => {
+          const newMessages = mappedMessages.filter(
+            (newMsg) => !previousMessages?.some((oldMsg) => oldMsg._id === newMsg._id)
+          );
+          return prevThenMessageId !== -1 && previousMessages
+            ? GiftedChat.prepend(previousMessages, newMessages)
+            : mappedMessages;
+        });
+
+        if (mappedMessages.length < 50) {
+          setHasMoreMessages(false);
+        }
+
+        if (mappedMessages.length === 0 && !modalInfo.visible) {
+          setTimeout(() => {
+            textInputRef.current?.focus();
+          }, 500);
+        }
+
+        setIsLoadingEarlier(false);
+      }
+    }, [chatData])
+  );
+
+  useEffect(() => {
+    if (messages?.length === 0 && !modalInfo.visible) {
+      setTimeout(() => {
+        textInputRef.current?.focus();
+      }, 500);
+    }
+  }, [modalInfo]);
+
+  const loadEarlierMessages = async () => {
+    if (!hasMoreMessages || isLoadingEarlier || !messages) return;
+
+    setIsLoadingEarlier(true);
+
+    const previousMessageId = messages[messages.length - 1]._id;
+
+    setPrevThenMessageId(previousMessageId);
+  };
+
+  const sentToServer = useRef<Set<number>>(new Set());
+
+  const handleViewableItemsChanged = ({ viewableItems }: { viewableItems: any[] }) => {
+    const newViewableUnreadMessages = viewableItems
+      .filter(
+        (item) =>
+          !item.item.received &&
+          !item.item.deleted &&
+          !item.item.system &&
+          item.item.user._id === id &&
+          !sentToServer.current.has(item.item._id)
+      )
+      .map((item) => item.item._id);
+
+    if (newViewableUnreadMessages.length > 0) {
+      markMessagesAsRead(
+        {
+          token,
+          from_user: id,
+          messages_id: newViewableUnreadMessages
+        },
+        {
+          onSuccess: () => {
+            newViewableUnreadMessages.forEach((id) => sentToServer.current.add(id));
+            sendWebSocketMessage('messages_read', null, null, newViewableUnreadMessages);
+          }
+        }
+      );
+    }
+  };
+
+  const renderSystemMessage = (props: any) => {
+    if (props.currentMessage._id === 'unreadMarker') {
+      return (
+        <View style={styles.unreadMessagesContainer}>
+          <Text style={styles.unreadMessagesText}>{props.currentMessage.text}</Text>
+        </View>
+      );
+    }
+    return null;
+  };
+
+  const clearReplyMessage = () => setReplyMessage(null);
+
+  const handleLongPress = (message: CustomMessage, props: BubbleProps<CustomMessage>) => {
+    const messageRef = messageRefs.current[message._id];
+
+    setSelectedMessage(props);
+    trigger('impactMedium', options);
+
+    const isMine = message.user._id === +currentUserId;
+
+    if (messageRef) {
+      messageRef.measureInWindow((x: number, y: number, width: number, height: number) => {
+        const screenHeight = Dimensions.get('window').height;
+        const spaceAbove = y - insets.top;
+        const spaceBelow = screenHeight - (y + height) - insets.bottom * 2;
+
+        let finalY = y;
+        scrollY.value = 0;
+
+        if (isNaN(y) || isNaN(height)) {
+          console.error("Invalid measurement values for 'y' or 'height'", { y, height });
+          return;
+        }
+
+        if (spaceBelow < 160) {
+          const extraShift = 160 - spaceBelow;
+          finalY -= extraShift;
+        }
+
+        if (spaceAbove < 50) {
+          const extraShift = 50 - spaceAbove;
+          finalY += extraShift;
+        }
+
+        if (spaceBelow < 160 || spaceAbove < 50) {
+          const targetY = screenHeight / 2 - height / 2;
+          scrollY.value = withTiming(finalY - finalY);
+        }
+
+        if (height > Dimensions.get('window').height - 200) {
+          finalY = 100;
+        }
+
+        finalY = isNaN(finalY) ? 0 : finalY;
+
+        setMessagePosition({ x, y: finalY, width, height, isMine });
+        setIsModalVisible(true);
+      });
+    }
+  };
+
+  const openEmojiSelector = () => {
+    SheetManager.show('emoji-selector');
+    trigger('impactLight', options);
+  };
+
+  const closeEmojiSelector = () => {
+    SheetManager.hide('emoji-selector');
+  };
+
+  const handleReactionPress = (emoji: string, messageId: number) => {
+    addReaction(messageId, emoji);
+  };
+
+  const handleDeleteMessage = (messageId: number) => {
+    deleteMessage(
+      {
+        token,
+        message_id: messageId,
+        conversation_with_user: id
+      },
+      {
+        onSuccess: () => {
+          setMessages(
+            (prevMessages) =>
+              prevMessages?.map((msg) => {
+                if (msg._id === messageId) {
+                  return {
+                    ...msg,
+                    deleted: true,
+                    text: 'This message was deleted',
+                    pending: false,
+                    sent: false,
+                    received: false
+                  };
+                }
+                return msg;
+              }) ?? []
+          );
+          const messageToDelete = messages?.find((msg) => msg._id === messageId);
+          if (messageToDelete) {
+            sendWebSocketMessage('delete_message', messageToDelete, null, null);
+          }
+        }
+      }
+    );
+  };
+
+  const handleOptionPress = (option: string) => {
+    if (!selectedMessage) return;
+
+    switch (option) {
+      case 'reply':
+        setReplyMessage(selectedMessage.currentMessage);
+        setIsModalVisible(false);
+        break;
+      case 'copy':
+        Clipboard.setString(selectedMessage?.currentMessage?.text ?? '');
+        setIsModalVisible(false);
+        Alert.alert('Copied');
+        break;
+      case 'delete':
+        handleDeleteMessage(selectedMessage.currentMessage?._id);
+        setIsModalVisible(false);
+        break;
+      default:
+        break;
+    }
+    closeEmojiSelector();
+  };
+
+  const openReactionList = (
+    reactions: { uid: number; name: string; reaction: string }[],
+    messageId: number
+  ) => {
+    SheetManager.show('reactions-list-modal', {
+      payload: {
+        users: reactions,
+        currentUserId: +currentUserId,
+        token,
+        messageId,
+        conversation_with_user: id,
+        setMessages,
+        sendWebSocketMessage
+      } as any
+    });
+  };
+
+  const renderTimeContainer = (time: TimeProps<CustomMessage>) => {
+    const createdAt = new Date(time.currentMessage.createdAt);
+
+    const formattedTime = createdAt.toLocaleTimeString([], {
+      hour: '2-digit',
+      minute: '2-digit',
+      hour12: true
+    });
+
+    const hasReactions =
+      time.currentMessage.reactions &&
+      Array.isArray(time.currentMessage.reactions) &&
+      time.currentMessage.reactions.length > 0;
+
+    return (
+      <View
+        style={[
+          styles.bottomContainer,
+          {
+            justifyContent: hasReactions ? 'space-between' : 'flex-end'
+          }
+        ]}
+      >
+        {hasReactions && (
+          <TouchableOpacity
+            style={[
+              styles.bottomCustomContainer,
+              {
+                backgroundColor:
+                  time.position === 'left' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.2)'
+              }
+            ]}
+            onPress={() =>
+              Array.isArray(time.currentMessage.reactions) &&
+              openReactionList(
+                time.currentMessage.reactions.map((reaction) => ({
+                  ...reaction,
+                  name: reaction.uid === id ? userName : 'Me'
+                })),
+                time.currentMessage._id
+              )
+            }
+          >
+            {Object.entries(
+              (Array.isArray(time.currentMessage.reactions)
+                ? time.currentMessage.reactions
+                : []
+              ).reduce(
+                (acc: Record<string, { count: number }>, { reaction }: { reaction: string }) => {
+                  if (!acc[reaction]) {
+                    acc[reaction] = { count: 0 };
+                  }
+                  acc[reaction].count += 1;
+                  return acc;
+                },
+                {}
+              )
+            ).map(([emoji, { count }]: any) => {
+              return (
+                <View key={emoji}>
+                  <Text style={{}}>
+                    {emoji}
+                    {(count as number) > 1 ? ` ${count}` : ''}
+                  </Text>
+                </View>
+              );
+            })}
+          </TouchableOpacity>
+        )}
+        <View style={styles.timeContainer}>
+          <Text style={styles.timeText}>{formattedTime}</Text>
+          {renderTicks(time.currentMessage)}
+        </View>
+      </View>
+    );
+  };
+
+  const renderSelectedMessage = () =>
+    selectedMessage && (
+      <View
+        style={{
+          maxHeight: '80%',
+          width: messagePosition?.width,
+          position: 'absolute',
+          top: messagePosition?.y,
+          left: messagePosition?.x
+        }}
+      >
+        <ScrollView>
+          <Bubble
+            {...selectedMessage}
+            wrapperStyle={{
+              right: { backgroundColor: Colors.DARK_BLUE },
+              left: { backgroundColor: Colors.FILL_LIGHT }
+            }}
+            textStyle={{
+              right: { color: Colors.WHITE },
+              left: { color: Colors.DARK_BLUE }
+            }}
+            renderTicks={() => null}
+            renderTime={renderTimeContainer}
+          />
+        </ScrollView>
+      </View>
+    );
+
+  const handleBackgroundPress = () => {
+    setIsModalVisible(false);
+    setSelectedMessage(null);
+    closeEmojiSelector();
+  };
+
+  useFocusEffect(
+    useCallback(() => {
+      navigation?.getParent()?.setOptions({
+        tabBarStyle: {
+          display: 'none'
+        }
+      });
+    }, [navigation])
+  );
+
+  const onSend = useCallback(
+    (newMessages: CustomMessage[] = []) => {
+      if (replyMessage) {
+        newMessages[0].replyMessage = {
+          text: replyMessage.text,
+          id: replyMessage._id,
+          name: replyMessage.user._id === id ? userName : 'Me'
+        };
+      }
+      const message = { ...newMessages[0], pending: true };
+
+      setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [message]));
+
+      sendMessage(
+        {
+          token,
+          to_uid: id,
+          text: message.text,
+          reply_to_id: replyMessage ? (replyMessage._id as number) : -1
+        },
+        {
+          onSuccess: (res) => {
+            const newMessage = {
+              _id: res.message_id,
+              text: message.text,
+              replyMessage: { ...message.replyMessage, sender: replyMessage?.user?._id }
+            };
+
+            setMessages((previousMessages) =>
+              (previousMessages ?? []).map((msg) =>
+                msg._id === message._id ? { ...msg, _id: res.message_id } : msg
+              )
+            );
+            sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
+          },
+          onError: (err) => console.log('err', err)
+        }
+      );
+
+      clearReplyMessage();
+    },
+    [replyMessage]
+  );
+
+  const addReaction = (messageId: number, reaction: string) => {
+    if (!messages) return;
+
+    const updatedMessages = messages.map((msg: any) => {
+      if (msg._id === messageId) {
+        const updatedReactions: Reaction[] = [
+          ...(Array.isArray(msg.reactions)
+            ? msg.reactions?.filter((r: Reaction) => r.uid !== +currentUserId)
+            : []),
+          { datetime: new Date().toISOString(), reaction: reaction, uid: +currentUserId }
+        ];
+
+        return {
+          ...msg,
+          reactions: updatedReactions
+        };
+      }
+      return msg;
+    });
+
+    setMessages(updatedMessages);
+
+    reactToMessage(
+      { token, message_id: messageId, reaction: reaction, conversation_with_user: id },
+      {
+        onSuccess: () => {
+          const message = messages.find((msg) => msg._id === messageId);
+          if (message) {
+            sendWebSocketMessage('new_reaction', message, reaction);
+          }
+        },
+        onError: (err) => console.log('err', err)
+      }
+    );
+
+    setIsModalVisible(false);
+  };
+
+  const updateRowRef = useCallback(
+    (ref: any) => {
+      if (
+        ref &&
+        replyMessage &&
+        ref.props.children.props.currentMessage?._id === replyMessage._id
+      ) {
+        swipeableRowRef.current = ref;
+      }
+    },
+    [replyMessage]
+  );
+
+  const renderReplyMessageView = (props: BubbleProps<CustomMessage>) => {
+    if (!props.currentMessage) {
+      return null;
+    }
+    const { currentMessage } = props;
+
+    if (!currentMessage || !currentMessage?.replyMessage) {
+      return null;
+    }
+
+    return (
+      <TouchableOpacity
+        style={[
+          styles.replyMessageContainer,
+          {
+            backgroundColor:
+              currentMessage.user._id === id ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.2)',
+            borderColor: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE
+          }
+        ]}
+        onPress={() => {
+          if (currentMessage?.replyMessage?.id) {
+            scrollToMessage(currentMessage.replyMessage.id);
+          }
+        }}
+      >
+        <View style={styles.replyContent}>
+          <Text
+            style={[
+              styles.replyAuthorName,
+              { color: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE }
+            ]}
+          >
+            {currentMessage.replyMessage.name}
+          </Text>
+
+          <Text
+            numberOfLines={1}
+            style={[
+              styles.replyMessageText,
+              { color: currentMessage.user._id === id ? Colors.DARK_BLUE : Colors.WHITE }
+            ]}
+          >
+            {currentMessage.replyMessage.text}
+          </Text>
+        </View>
+      </TouchableOpacity>
+    );
+  };
+
+  const scrollToMessage = (messageId: number) => {
+    if (!messages) return;
+
+    const messageIndex = messages.findIndex((message) => message._id === messageId);
+
+    if (messageIndex !== -1 && flatList.current) {
+      flatList.current.scrollToIndex({
+        index: messageIndex,
+        animated: true,
+        viewPosition: 0.5
+      });
+
+      setHighlightedMessageId(messageId);
+    }
+  };
+
+  useEffect(() => {
+    if (highlightedMessageId && isRerendering) {
+      setTimeout(() => {
+        setHighlightedMessageId(null);
+        setIsRerendering(false);
+      }, 1500);
+    }
+  }, [highlightedMessageId, isRerendering]);
+
+  useEffect(() => {
+    if (replyMessage && swipeableRowRef.current) {
+      swipeableRowRef.current.close();
+      swipeableRowRef.current = null;
+    }
+  }, [replyMessage]);
+
+  const renderMessageImage = (props: any) => {
+    const { currentMessage } = props;
+    return (
+      <TouchableOpacity
+        onPress={() => setSelectedMedia(currentMessage.image)}
+        style={styles.imageContainer}
+      >
+        <Image source={{ uri: currentMessage.image }} style={styles.chatImage} resizeMode="cover" />
+      </TouchableOpacity>
+    );
+  };
+
+  const renderTicks = (message: CustomMessage) => {
+    if (message.user._id === id) return null;
+
+    return message.received ? (
+      <View>
+        <MaterialCommunityIcons name="check-all" size={16} color={Colors.WHITE} />
+      </View>
+    ) : message.sent ? (
+      <View>
+        <MaterialCommunityIcons name="check" size={16} color={Colors.WHITE} />
+      </View>
+    ) : message.pending ? (
+      <View>
+        <MaterialCommunityIcons name="check" size={16} color={Colors.LIGHT_GRAY} />
+      </View>
+    ) : null;
+  };
+
+  const renderBubble = (props: BubbleProps<CustomMessage>) => {
+    const { currentMessage } = props;
+
+    if (currentMessage.deleted) {
+      const text = currentMessage.text.length
+        ? props.currentMessage.text
+        : 'This message was deleted';
+
+      return (
+        <View>
+          <Bubble
+            {...props}
+            renderTime={() => null}
+            currentMessage={{
+              ...props.currentMessage,
+              text: text
+            }}
+            renderMessageText={() => (
+              <View style={{ paddingHorizontal: 12, paddingVertical: 6 }}>
+                <Text style={{ color: Colors.LIGHT_GRAY, fontStyle: 'italic', fontSize: 12 }}>
+                  {text}
+                </Text>
+              </View>
+            )}
+            wrapperStyle={{
+              right: {
+                backgroundColor: Colors.DARK_BLUE
+              },
+              left: {
+                backgroundColor: Colors.FILL_LIGHT
+              }
+            }}
+            textStyle={{
+              left: {
+                color: Colors.DARK_BLUE
+              },
+              right: {
+                color: Colors.WHITE
+              }
+            }}
+          />
+        </View>
+      );
+    }
+
+    const isHighlighted = currentMessage._id === highlightedMessageId;
+    const backgroundColor = isHighlighted
+      ? Colors.ORANGE
+      : currentMessage.user._id === +currentUserId
+        ? Colors.DARK_BLUE
+        : Colors.FILL_LIGHT;
+
+    return (
+      <View
+        key={`${currentMessage._id}-${isHighlighted ? 'highlighted' : 'normal'}`}
+        ref={(ref) => {
+          if (ref && currentMessage) {
+            messageRefs.current[currentMessage._id] = ref;
+          }
+        }}
+        collapsable={false}
+      >
+        <Bubble
+          {...props}
+          wrapperStyle={{
+            right: {
+              backgroundColor: backgroundColor
+            },
+            left: {
+              backgroundColor: backgroundColor
+            }
+          }}
+          textStyle={{
+            left: {
+              color: Colors.DARK_BLUE
+            },
+            right: {
+              color: Colors.FILL_LIGHT
+            }
+          }}
+          onLongPress={() => handleLongPress(currentMessage, props)}
+          renderTicks={() => null}
+          renderTime={renderTimeContainer}
+        />
+      </View>
+    );
+  };
+
+  const openAttachmentsModal = () => {
+    SheetManager.show('chat-attachments', {
+      payload: {
+        name: userName,
+        uid: id,
+        setModalInfo,
+        closeOptions: () => setShowOptions(false),
+        onSendMedia,
+        onSendLocation,
+        onShareLiveLocation
+      } as any
+    });
+  };
+
+  const renderInputToolbar = (props: any) => (
+    <View>
+      <InputToolbar
+        {...props}
+        renderActions={() => (
+          <Actions
+            icon={() => (
+              <MaterialCommunityIcons
+                name={!isKeyboardOpen && showOptions ? 'keyboard-outline' : 'plus'}
+                size={28}
+                color={Colors.DARK_BLUE}
+              />
+            )}
+            onPressActionButton={openAttachmentsModal}
+          />
+        )}
+        containerStyle={{
+          backgroundColor: Colors.FILL_LIGHT
+        }}
+      />
+      {/* <InputAccessoryView nativeID={'inputAccessoryViewID'}>
+    <InputToolbar
+        {...props}
+        renderActions={() => (
+          <Actions
+            icon={() => (
+              <MaterialCommunityIcons
+                name={!isKeyboardOpen && showOptions ? 'keyboard-outline' : 'plus'}
+                size={28}
+                color={Colors.DARK_BLUE}
+              />
+            )}
+            onPressActionButton={toggleOptions}
+          />
+        )}
+        containerStyle={{
+          backgroundColor: Colors.FILL_LIGHT
+        }}
+      />
+    </InputAccessoryView> */}
+
+      {/* {showOptions && renderOptionsView()} */}
+    </View>
+  );
+
+  const renderScrollToBottom = () => {
+    return (
+      <TouchableOpacity
+        style={styles.scrollToBottom}
+        onPress={() => {
+          if (flatList.current) {
+            flatList.current.scrollToIndex({ index: 0, animated: true });
+          }
+        }}
+      >
+        <MaterialCommunityIcons name="chevron-down" size={24} color={Colors.WHITE} />
+      </TouchableOpacity>
+    );
+  };
+
+  const shouldUpdateMessage = (
+    props: MessageProps<IMessage>,
+    nextProps: MessageProps<IMessage>
+  ) => {
+    setIsRerendering(true);
+    const currentId = nextProps.currentMessage._id;
+    return currentId === highlightedMessageId;
+  };
+
+  return (
+    <SafeAreaView
+      edges={['top']}
+      style={{
+        height: '100%'
+      }}
+    >
+      <KeyboardAvoidingView
+        style={{ flex: 1 }}
+        behavior={'height'}
+        keyboardVerticalOffset={Platform.select({ android: 34 })}
+      >
+        <View style={{ paddingHorizontal: '5%' }}>
+          <Header
+            label={userName}
+            textColor={userType !== 'normal' ? Colors.RED : Colors.DARK_BLUE}
+            rightElement={
+              <TouchableOpacity
+                onPress={() =>
+                  navigation.navigate(
+                    ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: id }] as never)
+                  )
+                }
+                disabled={userType !== 'normal'}
+              >
+                {avatar && userType === 'normal' ? (
+                  <Image source={{ uri: API_HOST + avatar }} style={styles.avatar} />
+                ) : userType === 'normal' ? (
+                  <AvatarWithInitials
+                    text={
+                      name
+                        .split(/ (.+)/)
+                        .map((n) => n[0])
+                        .join('') ?? ''
+                    }
+                    flag={API_HOST + 'flag.png'}
+                    size={30}
+                    fontSize={12}
+                  />
+                ) : (
+                  <BanIcon fill={Colors.RED} width={30} height={30} />
+                )}
+              </TouchableOpacity>
+            }
+          />
+        </View>
+
+        <GestureHandlerRootView style={styles.container}>
+          {messages ? (
+            <GiftedChat
+              messages={messages as CustomMessage[]}
+              listViewProps={{
+                ref: flatList,
+                // onTouchStart: handleTouchOutside,
+                // onTouchEnd: handleTouchOutside,
+                showsVerticalScrollIndicator: false,
+                initialNumToRender: 30,
+                onViewableItemsChanged: handleViewableItemsChanged,
+                viewabilityConfig: { itemVisiblePercentThreshold: 50 },
+                onScrollToIndexFailed: (info: any) => {
+                  const wait = new Promise((resolve) => setTimeout(resolve, 300));
+                  wait.then(() => {
+                    flatList.current?.scrollToIndex({
+                      index: info.index,
+                      animated: true,
+                      viewPosition: 0.5
+                    });
+                  });
+                }
+              }}
+              renderSystemMessage={renderSystemMessage}
+              onSend={(newMessages: CustomMessage[]) => onSend(newMessages)}
+              user={{ _id: +currentUserId, name: 'Me' }}
+              renderBubble={renderBubble}
+              renderMessageImage={renderMessageImage}
+              renderInputToolbar={renderInputToolbar}
+              renderCustomView={renderReplyMessageView}
+              isCustomViewBottom={false}
+              messageContainerRef={messageContainerRef}
+              minComposerHeight={34}
+              onInputTextChanged={(text) => handleTyping(text.length > 0)}
+              textInputRef={textInputRef}
+              isTyping={isTyping}
+              renderSend={(props) => (
+                <View style={styles.sendBtn}>
+                  {props.text?.trim() && (
+                    <Send
+                      {...props}
+                      containerStyle={{
+                        justifyContent: 'center'
+                      }}
+                    >
+                      <SendIcon fill={Colors.DARK_BLUE} />
+                    </Send>
+                  )}
+                  {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
+                </View>
+              )}
+              renderMessageVideo={renderMessageVideo}
+              textInputProps={{
+                ...styles.composer,
+                selectionColor: Colors.LIGHT_GRAY,
+                onFocus: handleInputFocus
+              }}
+              isKeyboardInternallyHandled={false}
+              placeholder=""
+              renderMessage={(props) => (
+                <ChatMessageBox
+                  {...(props as MessageProps<CustomMessage>)}
+                  updateRowRef={updateRowRef}
+                  setReplyOnSwipeOpen={setReplyMessage}
+                />
+              )}
+              renderChatFooter={() => (
+                <ReplyMessageBar clearReply={clearReplyMessage} message={replyMessage} />
+              )}
+              renderAvatar={null}
+              maxComposerHeight={100}
+              renderComposer={(props) => <Composer {...props} />}
+              keyboardShouldPersistTaps="handled"
+              renderChatEmpty={() => (
+                <View style={styles.emptyChat}>
+                  <Text
+                    style={styles.emptyChatText}
+                  >{`No messages yet.\nFeel free to start the conversation.`}</Text>
+                </View>
+              )}
+              shouldUpdateMessage={shouldUpdateMessage}
+              scrollToBottom={true}
+              scrollToBottomComponent={renderScrollToBottom}
+              scrollToBottomStyle={{ backgroundColor: 'transparent' }}
+              parsePatterns={(linkStyle) => [
+                {
+                  type: 'url',
+                  style: { color: Colors.ORANGE, textDecorationLine: 'underline' },
+                  onPress: (url: string) => Linking.openURL(url),
+                  onLongPress: (url: string) => {
+                    Clipboard.setString(url ?? '');
+                    Alert.alert('Link copied');
+                  }
+                }
+              ]}
+              infiniteScroll={true}
+              loadEarlier={hasMoreMessages}
+              isLoadingEarlier={isLoadingEarlier}
+              onLoadEarlier={loadEarlierMessages}
+              renderLoadEarlier={() => (
+                <View style={{ paddingVertical: 20 }}>
+                  <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
+                </View>
+              )}
+            />
+          ) : (
+            <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
+          )}
+
+          <Modal visible={!!selectedMedia} transparent={true}>
+            <View style={styles.modalContainer}>
+              {selectedMedia && selectedMedia?.includes('.mp4') ? (
+                <Video
+                  source={{ uri: selectedMedia }}
+                  style={styles.fullScreenMedia}
+                  useNativeControls
+                />
+              ) : (
+                <Image source={{ uri: selectedMedia ?? '' }} style={styles.fullScreenMedia} />
+              )}
+              <TouchableOpacity onPress={() => setSelectedMedia(null)} style={styles.closeButton}>
+                <MaterialCommunityIcons name="close" size={30} color="white" />
+              </TouchableOpacity>
+            </View>
+          </Modal>
+
+          <ReactModal
+            isVisible={isModalVisible}
+            onBackdropPress={handleBackgroundPress}
+            style={styles.reactModalContainer}
+            animationIn="fadeIn"
+            animationOut="fadeOut"
+            useNativeDriver
+            backdropColor="transparent"
+          >
+            <BlurView
+              intensity={80}
+              style={styles.modalBackground}
+              experimentalBlurMethod="dimezisBlurView"
+            >
+              <TouchableOpacity
+                style={styles.modalBackground}
+                activeOpacity={1}
+                onPress={handleBackgroundPress}
+              >
+                <ReactionBar
+                  messagePosition={messagePosition}
+                  selectedMessage={selectedMessage}
+                  reactionEmojis={reactionEmojis}
+                  handleReactionPress={handleReactionPress}
+                  openEmojiSelector={openEmojiSelector}
+                />
+                {renderSelectedMessage()}
+                <OptionsMenu
+                  selectedMessage={selectedMessage}
+                  handleOptionPress={handleOptionPress}
+                  messagePosition={messagePosition}
+                />
+                <EmojiSelectorModal
+                  visible={emojiSelectorVisible}
+                  selectedMessage={selectedMessage}
+                  addReaction={addReaction}
+                  closeEmojiSelector={closeEmojiSelector}
+                />
+              </TouchableOpacity>
+            </BlurView>
+          </ReactModal>
+
+          <WarningModal
+            isVisible={modalInfo.visible}
+            onClose={closeModal}
+            type={modalInfo.type}
+            message={modalInfo.message}
+            buttonTitle={modalInfo.buttonTitle}
+            title={modalInfo.title}
+            action={() => {
+              modalInfo.action();
+              closeModal();
+            }}
+          />
+          <AttachmentsModal />
+          <ReactionsListModal />
+          <Animated.View
+            style={{
+              transform: [{ translateY }]
+            }}
+          >
+            {/* {renderOptionsView()} */}
+          </Animated.View>
+        </GestureHandlerRootView>
+      </KeyboardAvoidingView>
+      {showOptions && renderOptionsView()}
+      <View
+        style={{
+          height: insets.bottom,
+          backgroundColor: Colors.FILL_LIGHT
+        }}
+      />
+    </SafeAreaView>
+  );
+};
+
+export default ChatScreen;

+ 213 - 26
src/screens/InAppScreens/MessagesScreen/Components/AttachmentsModal.tsx

@@ -1,25 +1,39 @@
-import React, { useState } from 'react';
+import React, { useRef, useState } from 'react';
 import { StyleSheet, TouchableOpacity, View, Text } from 'react-native';
-import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
+import ActionSheet, { Route, SheetManager, useSheetRouter } from 'react-native-actions-sheet';
 import { getFontSize } from 'src/utils';
 import { Colors } from 'src/theme';
 import { WarningProps } from '../types';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 import { usePostReportConversationMutation } from '@api/chat';
+import * as ImagePicker from 'expo-image-picker';
+import * as DocumentPicker from 'react-native-document-picker';
+
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import RouteB from './RouteB';
 
 import MegaphoneIcon from 'assets/icons/messages/megaphone.svg';
+import LocationIcon from 'assets/icons/messages/location.svg';
+import CameraIcon from 'assets/icons/messages/camera.svg';
+import ImagesIcon from 'assets/icons/messages/images.svg';
+import { storage, StoreType } from 'src/storage';
 
 const AttachmentsModal = () => {
   const insets = useSafeAreaInsets();
-  const [chatData, setChatData] = useState<any>(null);
+  const token = storage.get('token', StoreType.STRING) as string;
   const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState<WarningProps | null>(null);
   const { mutateAsync: reportUser } = usePostReportConversationMutation();
+  const [data, setData] = useState<any | null>(null);
+
+  const chatDataRef = useRef<any>(null);
 
   const handleSheetOpen = (payload: any) => {
-    setChatData(payload);
+    chatDataRef.current = payload;
+    setData(payload);
   };
 
   const handleReport = async () => {
+    const chatData = chatDataRef.current;
     if (!chatData) return;
 
     setShouldOpenWarningModal({
@@ -28,7 +42,7 @@ const AttachmentsModal = () => {
       message: `Are you sure you want to report ${chatData.name}?\nIf you proceed, the chat history with ${chatData.name} will become visible to NomadMania admins for investigation.`,
       action: async () => {
         await reportUser({
-          token: chatData.token,
+          token,
           reported_user_id: chatData.uid
         });
       }
@@ -40,22 +54,188 @@ const AttachmentsModal = () => {
     }, 300);
   };
 
+  const handleOpenGallery = async () => {
+    const chatData = chatDataRef.current;
+    if (!chatData) return;
+    try {
+      const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
+      if (!perm.granted) {
+        console.warn('Permission for gallery not granted');
+        return;
+      }
+
+      const result = await ImagePicker.launchImageLibraryAsync({
+        mediaTypes: ImagePicker.MediaTypeOptions.All,
+        allowsMultipleSelection: true,
+        quality: 1,
+        selectionLimit: 4
+      });
+
+      if (!result.canceled && result.assets) {
+        const files = result.assets.map((asset) => ({
+          uri: asset.uri,
+          type: asset.type === 'video' ? 'video' : 'image'
+        }));
+        chatData.onSendMedia(files);
+      }
+      SheetManager.hide('chat-attachments');
+    } catch (err) {
+      console.warn('Gallery error: ', err);
+    }
+  };
+
+  const handleOpenCamera = async () => {
+    const chatData = chatDataRef.current;
+    if (!chatData) return;
+    try {
+      const perm = await ImagePicker.requestCameraPermissionsAsync();
+      if (!perm.granted) {
+        console.warn('Permission for camera not granted');
+        return;
+      }
+
+      const result = await ImagePicker.launchCameraAsync({
+        mediaTypes: ImagePicker.MediaTypeOptions.Images,
+        quality: 1
+      });
+
+      if (!result.canceled && result.assets) {
+        const files = result.assets.map((asset) => ({
+          uri: asset.uri,
+          type: asset.type === 'video' ? 'video' : 'image'
+        }));
+        chatData.onSendMedia(files);
+      }
+      SheetManager.hide('chat-attachments');
+    } catch (err) {
+      console.warn('Camera error: ', err);
+    }
+  };
+
+  const handleShareLiveLocation = () => {
+    const chatData = chatDataRef.current;
+    if (!chatData) return;
+    chatData.onShareLiveLocation();
+    SheetManager.hide('chat-attachments');
+  };
+
+  const handleSendFile = async () => {
+    const chatData = chatDataRef.current;
+    if (!chatData) return;
+
+    try {
+      const res = await DocumentPicker.pick({
+        type: [DocumentPicker.types.allFiles],
+        allowMultiSelection: false
+      });
+
+      let file = {
+        uri: res[0].uri,
+        name: res[0].name,
+        type: res[0].type
+      };
+
+      if ((file.name && !file.name.includes('.')) || !file.type) {
+        file = {
+          ...file,
+          type: file.type || 'application/octet-stream'
+        };
+      }
+
+      if (chatData.onSendFile) {
+        chatData.onSendFile([file]);
+      }
+    } catch (err) {
+      if (DocumentPicker.isCancel(err)) {
+      } else {
+        console.warn('DocumentPicker error:', err);
+      }
+    }
+
+    SheetManager.hide('chat-attachments');
+  };
+
+  const RouteA = () => {
+    const router = useSheetRouter('chat-attachments');
+    return (
+      <View
+        style={[
+          styles.container,
+          { paddingBottom: 8 + insets.bottom, backgroundColor: Colors.FILL_LIGHT }
+        ]}
+      >
+        <View style={styles.optionRow}>
+          <TouchableOpacity style={styles.optionItem} onPress={handleOpenGallery}>
+            <ImagesIcon height={36} />
+            <Text style={styles.optionLabel}>Gallery</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity style={styles.optionItem} onPress={handleOpenCamera}>
+            <CameraIcon height={36} />
+            <Text style={styles.optionLabel}>Camera</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity
+            style={styles.optionItem}
+            onPress={() => {
+              router?.navigate('route-b');
+            }}
+          >
+            <LocationIcon height={36} />
+            <Text style={styles.optionLabel}>Location</Text>
+          </TouchableOpacity>
+
+          {/* <TouchableOpacity style={styles.optionItem} onPress={handleShareLiveLocation}>
+            <MaterialCommunityIcons name="navigation" size={36} color={Colors.ORANGE} />
+            <Text style={styles.optionLabel}>Live</Text>
+          </TouchableOpacity> */}
+
+          <TouchableOpacity style={styles.optionItem} onPress={handleSendFile}>
+            <MaterialCommunityIcons name="file" size={36} color={Colors.ORANGE} />
+            <Text style={styles.optionLabel}>File</Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity style={styles.optionItem} onPress={handleReport}>
+            <MegaphoneIcon fill={Colors.RED} width={36} height={36} />
+            <Text style={styles.optionLabel}>Report</Text>
+          </TouchableOpacity>
+          <View style={styles.optionItem}></View>
+        </View>
+      </View>
+    );
+  };
+
+  const routes: Route[] = [
+    {
+      name: 'route-a',
+      component: RouteA
+    },
+    {
+      name: 'route-b',
+      component: RouteB,
+      params: { onSendLocation: data?.onSendLocation, insetsBottom: insets.bottom }
+    }
+  ];
+
   return (
     <ActionSheet
       id="chat-attachments"
-      gestureEnabled={true}
+      // gestureEnabled={true}
       containerStyle={{
-        borderTopLeftRadius: 15,
-        borderTopRightRadius: 15
+        backgroundColor: Colors.FILL_LIGHT
       }}
-      defaultOverlayOpacity={0.5}
+      enableRouterBackNavigation={true}
+      routes={routes}
+      initialRoute="route-a"
+      defaultOverlayOpacity={0}
+      indicatorStyle={{ backgroundColor: Colors.WHITE }}
       onBeforeShow={(sheetRef) => {
         const payload = sheetRef || null;
         handleSheetOpen(payload);
       }}
       onClose={() => {
         if (shouldOpenWarningModal) {
-          chatData?.setModalInfo({
+          chatDataRef.current?.setModalInfo({
             visible: true,
             type: 'delete',
             title: shouldOpenWarningModal.title,
@@ -65,22 +245,7 @@ const AttachmentsModal = () => {
           });
         }
       }}
-    >
-      <View
-        style={{
-          backgroundColor: 'white',
-          paddingHorizontal: 16,
-          gap: 16,
-          paddingTop: 8,
-          paddingBottom: 8 + insets.bottom
-        }}
-      >
-        <TouchableOpacity style={[styles.option, styles.dangerOption]} onPress={handleReport}>
-          <Text style={[styles.optionText, styles.dangerText]}>Report {chatData?.name}</Text>
-          <MegaphoneIcon fill={Colors.RED} />
-        </TouchableOpacity>
-      </View>
-    </ActionSheet>
+    />
   );
 };
 
@@ -102,6 +267,28 @@ const styles = StyleSheet.create({
   },
   dangerText: {
     color: Colors.RED
+  },
+  container: {
+    backgroundColor: Colors.WHITE
+  },
+  optionRow: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    paddingHorizontal: '5%',
+    marginVertical: 20,
+    flexWrap: 'wrap'
+  },
+  optionItem: {
+    width: '30%',
+    paddingVertical: 8,
+    marginBottom: 12,
+    alignItems: 'center'
+  },
+  optionLabel: {
+    marginTop: 6,
+    fontSize: 12,
+    color: Colors.DARK_BLUE,
+    fontWeight: '700'
   }
 });
 

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

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

+ 75 - 0
src/screens/InAppScreens/MessagesScreen/Components/MessageLocation.tsx

@@ -0,0 +1,75 @@
+import React, { useRef } from 'react';
+import { View, TouchableOpacity, StyleSheet } from 'react-native';
+import * as MapLibreRN from '@maplibre/maplibre-react-native';
+import { useNavigation } from '@react-navigation/native';
+import { Colors } from 'src/theme';
+import { VECTOR_MAP_HOST } from 'src/constants';
+import { NAVIGATION_PAGES } from 'src/types';
+
+const MessageLocation = ({
+  props,
+  lat,
+  lng,
+  onLongPress
+}: {
+  props: any;
+  lat: number;
+  lng: number;
+  onLongPress: (currentMessage: any, props: any) => void;
+}) => {
+  const navigation = useNavigation();
+  const mapRef = useRef<MapLibreRN.MapViewRef>(null);
+  const cameraRef = useRef<MapLibreRN.CameraRef>(null);
+
+  return (
+    <TouchableOpacity
+      style={styles.container}
+      onPress={() =>
+        navigation.navigate(...([NAVIGATION_PAGES.FULL_MAP_VIEW, { lat, lng }] as never))
+      }
+      onLongPress={() => onLongPress(props.currentMessage, props)}
+    >
+      <MapLibreRN.MapView
+        ref={mapRef}
+        style={styles.map}
+        mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps.json'}
+        rotateEnabled={false}
+        attributionEnabled={false}
+        scrollEnabled={false}
+        zoomEnabled={false}
+        pitchEnabled={false}
+      >
+        <MapLibreRN.Camera
+          ref={cameraRef}
+          defaultSettings={{ centerCoordinate: [lng, lat], zoomLevel: 10 }}
+        />
+        <MapLibreRN.MarkerView coordinate={[lng, lat]}>
+          <View
+            style={{
+              width: 20,
+              height: 20,
+              borderRadius: 10,
+              backgroundColor: Colors.ORANGE,
+              borderWidth: 2,
+              borderColor: Colors.WHITE
+            }}
+          />
+        </MapLibreRN.MarkerView>
+      </MapLibreRN.MapView>
+    </TouchableOpacity>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    width: '100%',
+    height: 150,
+    borderRadius: 10,
+    overflow: 'hidden'
+  },
+  map: {
+    flex: 1
+  }
+});
+
+export default MessageLocation;

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

@@ -42,10 +42,21 @@ const OptionsMenu: React.FC<OptionsMenuProps> = ({
         <Text style={styles.optionText}>Reply</Text>
         <MaterialCommunityIcons name="reply" size={20} color={Colors.DARK_BLUE} />
       </TouchableOpacity>
+
+      {selectedMessage.currentMessage?.image || selectedMessage.currentMessage?.video ? (
+        <TouchableOpacity style={styles.optionButton} onPress={() => handleOptionPress('download')}>
+          <Text style={styles.optionText}>
+            {selectedMessage.currentMessage?.image ? 'Download image' : 'Download video'}
+          </Text>
+          <MaterialCommunityIcons name="download" size={20} color={Colors.DARK_BLUE} />
+        </TouchableOpacity>
+      ) : null}
+
       <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} />

+ 8 - 3
src/screens/InAppScreens/MessagesScreen/Components/ReplyMessageBar.tsx

@@ -2,12 +2,12 @@ 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';
+import { CustomMessage } from '../types';
 
 type ReplyMessageBarProps = {
   clearReply: () => void;
-  message: IMessage | null;
+  message: CustomMessage | null;
 };
 
 const replyMessageBarHeight = 50;
@@ -15,6 +15,11 @@ const replyMessageBarHeight = 50;
 const ReplyMessageBar = ({ clearReply, message }: ReplyMessageBarProps) => {
   if (!message) return null;
 
+  const text =
+    message?.attachment && message?.attachment?.filename
+      ? message.attachment.filename
+      : message.text;
+
   return (
     <Animated.View
       entering={FadeInDown}
@@ -49,7 +54,7 @@ const ReplyMessageBar = ({ clearReply, message }: ReplyMessageBarProps) => {
           {message.user.name}
         </Text>
         <Text style={{ color: Colors.DARK_BLUE, paddingLeft: 10, paddingTop: 5 }} numberOfLines={1}>
-          {message.text}
+          {text}
         </Text>
       </View>
 

+ 192 - 0
src/screens/InAppScreens/MessagesScreen/Components/RouteB.tsx

@@ -0,0 +1,192 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+import * as Location from 'expo-location';
+import * as MapLibreRN from '@maplibre/maplibre-react-native';
+import { SheetManager, useSheetRouteParams, useSheetRouter } from 'react-native-actions-sheet';
+import { Colors } from 'src/theme';
+import { VECTOR_MAP_HOST } from 'src/constants';
+import { ButtonVariants } from 'src/types/components';
+import { Button } from 'src/components';
+
+const RouteB = () => {
+  const router = useSheetRouter('chat-attachments');
+  const params = useSheetRouteParams('chat-attachments', 'route-b');
+  const {
+    onSendLocation,
+    insetsBottom
+  }: {
+    onSendLocation: (coords: { latitude: number; longitude: number }) => void;
+    insetsBottom: number;
+  } = params;
+
+  const [currentLocation, setCurrentLocation] = useState<{
+    latitude: number;
+    longitude: number;
+  } | null>(null);
+  const [selectedLocation, setSelectedLocation] = useState<{
+    latitude: number;
+    longitude: number;
+  } | null>(null);
+  const [loading, setLoading] = useState(true);
+  const cameraRef = useRef<MapLibreRN.CameraRef | null>(null);
+  const [mapDimensions, setMapDimensions] = useState({ width: 0, height: 0, x: 0, y: 0 });
+
+  useEffect(() => {
+    const getLocation = async () => {
+      try {
+        const { status } = await Location.requestForegroundPermissionsAsync();
+        if (status !== 'granted') {
+          return;
+        }
+
+        const location = await Location.getCurrentPositionAsync({});
+        const coords = {
+          latitude: location.coords.latitude,
+          longitude: location.coords.longitude
+        };
+        setCurrentLocation(coords);
+        setSelectedLocation(coords);
+      } catch (err) {
+        console.warn('Error fetching location:', err);
+      }
+    };
+
+    getLocation();
+  }, []);
+
+  useEffect(() => {
+    if (currentLocation) {
+      const timeoutId = setTimeout(() => {
+        if (cameraRef.current) {
+          cameraRef.current.setCamera({
+            centerCoordinate: [currentLocation.longitude, currentLocation.latitude],
+            zoomLevel: 14,
+            animationDuration: 500
+          });
+          setLoading(false);
+        } else {
+          console.warn('Camera ref is not available.');
+        }
+      }, 500);
+
+      return () => clearTimeout(timeoutId);
+    }
+  }, [currentLocation]);
+
+  const confirmLocation = () => {
+    if (selectedLocation) {
+      onSendLocation(selectedLocation);
+    }
+    SheetManager.hide('location-picker');
+    SheetManager.hide('chat-attachments');
+  };
+
+  const sendCurrentLocation = () => {
+    if (currentLocation) {
+      onSendLocation(currentLocation);
+    }
+    SheetManager.hide('location-picker');
+    SheetManager.hide('chat-attachments');
+  };
+
+  const handleRegionChange = (event: any) => {
+    const { geometry } = event;
+    if (geometry) {
+      const [longitude, latitude] = geometry.coordinates;
+      setSelectedLocation({ latitude, longitude });
+    }
+  };
+
+  return (
+    <View style={[styles.container, { paddingBottom: 8 + insetsBottom }]}>
+      <View
+        style={styles.mapContainer}
+        onLayout={(event) => {
+          const { x, y, width, height } = event.nativeEvent.layout;
+          setMapDimensions({ x, y, width, height });
+        }}
+      >
+        <MapLibreRN.MapView
+          style={{ flex: 1 }}
+          mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps.json'}
+          compassEnabled={false}
+          rotateEnabled={false}
+          attributionEnabled={false}
+          onRegionDidChange={handleRegionChange}
+        >
+          <MapLibreRN.Camera ref={cameraRef} />
+          {currentLocation && (
+            <MapLibreRN.PointAnnotation
+              id="currentLocation"
+              coordinate={[currentLocation.longitude, currentLocation.latitude]}
+            >
+              <View style={styles.currentLocationMarker} />
+            </MapLibreRN.PointAnnotation>
+          )}
+        </MapLibreRN.MapView>
+      </View>
+
+      <View
+        style={[
+          styles.centerMarker,
+          { left: mapDimensions.width / 2 - 12, top: mapDimensions.height / 2 - 12 }
+        ]}
+      >
+        <View style={styles.selectedLocationMarker} />
+      </View>
+
+      <View style={styles.mapActions}>
+        <Button
+          children="Send my location"
+          onPress={sendCurrentLocation}
+          variant={ButtonVariants.FILL}
+          disabled={loading}
+        />
+        <Button children="Confirm" onPress={confirmLocation} />
+        <Button
+          children="Close"
+          onPress={() => router?.goBack()}
+          variant={ButtonVariants.OPACITY}
+          containerStyles={{ backgroundColor: Colors.WHITE, borderColor: '#B7C6CB' }}
+          textStyles={{ color: Colors.DARK_BLUE }}
+        />
+      </View>
+    </View>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    backgroundColor: Colors.FILL_LIGHT,
+    minHeight: 600,
+    gap: 16
+  },
+  mapContainer: { flex: 1 },
+  mapActions: {
+    flexDirection: 'column',
+    gap: 8,
+    paddingHorizontal: 16
+  },
+  currentLocationMarker: {
+    width: 20,
+    height: 20,
+    backgroundColor: Colors.ORANGE,
+    borderRadius: 10,
+    borderWidth: 2,
+    borderColor: Colors.WHITE
+  },
+  selectedLocationMarker: {
+    width: 24,
+    height: 24,
+    backgroundColor: Colors.ORANGE,
+    borderRadius: 12,
+    borderWidth: 2,
+    borderColor: Colors.WHITE
+  },
+  centerMarker: {
+    position: 'absolute',
+    zIndex: 1000
+  }
+});
+
+export default RouteB;

+ 160 - 0
src/screens/InAppScreens/MessagesScreen/Components/renderMessageVideo.tsx

@@ -0,0 +1,160 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { View, ActivityIndicator, TouchableOpacity } from 'react-native';
+import { ResizeMode, Video } from 'expo-av';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import * as FileSystem from 'expo-file-system';
+import { Colors } from 'src/theme';
+import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
+import { API_HOST } from 'src/constants';
+
+const RenderMessageVideo = ({
+  props,
+  token,
+  currentUserId,
+  onLongPress
+}: {
+  props: any;
+  token: string;
+  currentUserId: number;
+  onLongPress: (currentMessage: any, props: any) => any;
+}) => {
+  const { currentMessage } = props;
+
+  if (!currentMessage?.video) return null;
+  const leftMessage = currentMessage?.user?._id !== currentUserId;
+
+  const videoRef = useRef<Video>(null);
+  const [isPlaying, setIsPlaying] = useState(false);
+  const [isBuffering, setIsBuffering] = useState(true);
+  const [videoUri, setVideoUri] = useState<string | null>(null);
+  const [isVideoLoaded, setIsVideoLoaded] = useState(false);
+
+  const downloadVideo = async (videoUrl: string) => {
+    try {
+      const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
+      if (!dirExist.exists) {
+        await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
+      }
+
+      const videoPath = `${CACHED_ATTACHMENTS_DIR}${currentMessage.attachment.filename}`;
+
+      const videoExists = await FileSystem.getInfoAsync(videoPath);
+      if (videoExists.exists) {
+        setVideoUri(videoPath);
+        setIsVideoLoaded(true);
+        return videoPath;
+      }
+
+      const downloadResult = await FileSystem.downloadAsync(videoUrl, videoPath, {
+        headers: {
+          Nmtoken: token
+        }
+      });
+
+      setVideoUri(downloadResult.uri);
+      setIsVideoLoaded(true);
+
+      return downloadResult.uri;
+    } catch (error) {
+      console.error('Error downloading video:', error);
+      return null;
+    }
+  };
+
+  useEffect(() => {
+    const loadVideo = async () => {
+      if (currentMessage?.video && !currentMessage?.isSending) {
+        await downloadVideo(currentMessage.video);
+      }
+    };
+
+    loadVideo();
+  }, [currentMessage.video, currentMessage.isSending]);
+
+  const handlePlaybackStatusUpdate = (playbackStatus: any) => {
+    if (!playbackStatus.isLoaded) {
+      setIsPlaying(false);
+      setIsBuffering(false);
+      return;
+    }
+
+    setIsPlaying(playbackStatus.isPlaying);
+    setIsBuffering(playbackStatus.isBuffering ?? false);
+  };
+
+  const handlePlayPress = async () => {
+    if (videoRef.current && isVideoLoaded) {
+      await videoRef.current.presentFullscreenPlayer();
+      await videoRef.current.playAsync();
+    }
+  };
+
+  return (
+    <View
+      style={{
+        width: 200,
+        height: 200,
+        padding: 6,
+        borderRadius: 10
+      }}
+    >
+      {videoUri ? (
+        <Video
+          ref={videoRef}
+          source={{ uri: videoUri }}
+          style={{ flex: 1, borderRadius: 10 }}
+          resizeMode={ResizeMode.CONTAIN}
+          useNativeControls
+          isMuted={false}
+          volume={1.0}
+          shouldCorrectPitch
+          onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
+          posterStyle={{ resizeMode: 'cover', width: '100%', height: '100%' }}
+          usePoster={true}
+          posterSource={{ uri: API_HOST + currentMessage.attachment.attachment_small_url }}
+        />
+      ) : null}
+
+      {isBuffering && (
+        <View
+          style={{
+            position: 'absolute',
+            top: 0,
+            left: 0,
+            right: 0,
+            bottom: 0,
+            alignItems: 'center',
+            justifyContent: 'center'
+          }}
+        >
+          <ActivityIndicator
+            size="large"
+            color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
+          />
+        </View>
+      )}
+
+      {!isPlaying && !isBuffering && videoUri && (
+        <TouchableOpacity
+          style={{
+            position: 'absolute',
+            top: 0,
+            left: 0,
+            right: 0,
+            bottom: 0,
+            alignItems: 'center',
+            justifyContent: 'center'
+          }}
+          onPress={handlePlayPress}
+          onLongPress={() => onLongPress(currentMessage, props)}
+        >
+          <View style={{ backgroundColor: 'rgba(15, 63, 79, 0.4)', borderRadius: 50 }}>
+            <MaterialCommunityIcons name="play" size={60} color={Colors.WHITE} />
+          </View>
+        </TouchableOpacity>
+      )}
+    </View>
+  );
+};
+
+export default RenderMessageVideo;

+ 81 - 0
src/screens/InAppScreens/MessagesScreen/FullMapScreen/index.tsx

@@ -0,0 +1,81 @@
+import React, { useRef } from 'react';
+import { View, StyleSheet, StatusBar, TouchableOpacity } from 'react-native';
+import * as MapLibreRN from '@maplibre/maplibre-react-native';
+import { VECTOR_MAP_HOST } from 'src/constants';
+import { Colors } from 'src/theme';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useNavigation } from '@react-navigation/native';
+import ChevronLeft from 'assets/icons/chevron-left.svg';
+
+const FullMapScreen = ({ route }: { route: any }) => {
+  const { lat, lng } = route.params;
+  const navigation = useNavigation();
+  const mapRef = useRef<MapLibreRN.MapViewRef>(null);
+  const cameraRef = useRef<MapLibreRN.CameraRef>(null);
+
+  return (
+    <SafeAreaView style={{ height: '100%' }}>
+      <StatusBar translucent backgroundColor="transparent" />
+
+      <MapLibreRN.MapView
+        ref={mapRef}
+        style={styles.map}
+        mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps.json'}
+        rotateEnabled={false}
+        attributionEnabled={false}
+      >
+        <MapLibreRN.Camera
+          ref={cameraRef}
+          defaultSettings={{ centerCoordinate: [lng, lat], zoomLevel: 12 }}
+        />
+        <MapLibreRN.MarkerView coordinate={[lng, lat]}>
+          <View style={styles.marker} />
+        </MapLibreRN.MarkerView>
+      </MapLibreRN.MapView>
+      <TouchableOpacity
+        onPress={() => {
+          navigation.goBack();
+        }}
+        style={styles.backButtonContainer}
+      >
+        <View style={styles.backButton}>
+          <ChevronLeft fill={Colors.WHITE} />
+        </View>
+      </TouchableOpacity>
+    </SafeAreaView>
+  );
+};
+
+const styles = StyleSheet.create({
+  map: {
+    ...StyleSheet.absoluteFillObject
+  },
+  backButtonContainer: {
+    position: 'absolute',
+    width: 50,
+    height: 50,
+    top: 50,
+    left: 10,
+    justifyContent: 'center',
+    alignItems: 'center',
+    zIndex: 2
+  },
+  backButton: {
+    width: 42,
+    height: 42,
+    borderRadius: 21,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: 'rgba(0, 0, 0, 0.3)'
+  },
+  marker: {
+    width: 20,
+    height: 20,
+    borderRadius: 10,
+    backgroundColor: Colors.ORANGE,
+    borderWidth: 2,
+    borderColor: Colors.WHITE
+  }
+});
+
+export default FullMapScreen;

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

@@ -362,7 +362,9 @@ const MessagesScreen = () => {
                   <TypingIndicator />
                 ) : (
                   <Text numberOfLines={2} style={styles.chatMessage}>
-                    {item.short}
+                    {item.attachement_name && item.attachement_name.length
+                      ? item.attachement_name
+                      : item.short}
                   </Text>
                 )}
 

+ 19 - 5
src/screens/InAppScreens/MessagesScreen/types.ts

@@ -37,6 +37,17 @@ export type ChatProps = {
   userType: 'normal' | 'not_exist' | 'blocked';
 };
 
+export interface Attachement {
+  id: number;
+  filename: string;
+  filetype: string;
+  attachment_small_url?: string;
+  attachment_full_url?: string;
+  lat?: number;
+  lng?: number;
+  attachment_link?: string;
+}
+
 export type MessageSimple = {
   id: number;
   sender: number;
@@ -46,10 +57,10 @@ export type MessageSimple = {
   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
+  reply_to_id: number;
+  reactions: string;
+  edits: string;
+  attachement: -1 | Attachement;
   encrypted: 0 | 1;
 };
 
@@ -65,8 +76,11 @@ export interface CustomMessage extends IMessage {
     name: string;
   } | null;
   deleted: boolean;
-  attachment: string | null;
+  attachment: Attachement | null;
   reactions: Reaction[] | {};
+  image?: string;
+  video?: string;
+  isSending?: boolean;
 }
 
 export type Reaction = {

+ 40 - 29
src/screens/InAppScreens/ProfileScreen/Components/PersonalInfo.tsx

@@ -20,6 +20,7 @@ import WHSIcon from 'assets/icons/travels-section/whs.svg';
 import ArrowIcon from 'assets/icons/next.svg';
 import UNPIcon from 'assets/icons/travels-section/unp.svg';
 import AuthIcon from 'assets/icons/authenticate-user.svg';
+import SendIcon from 'assets/icons/messages/send.svg';
 
 import { styles } from './styles';
 import { InfoItem } from './InfoItem';
@@ -58,6 +59,7 @@ type PersonalInfoProps = {
     lastSeenFlag: string | null;
     canBeAuthenticated: 0 | 1;
     setCanBeAuthenticated: (value: 0 | 1) => void;
+    goToChat: () => void;
   };
   updates: {
     un_visited: number;
@@ -248,7 +250,7 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
   const maxAvatars = Math.floor(availableWidth / (AVATAR_SIZE - AVATAR_MARGIN)) - 2;
 
   return (
-    <>
+    <View>
       <View style={styles.wrapper}>
         <View style={styles.scoreContainer}>
           {scores.map((score, index) => {
@@ -272,33 +274,42 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
           })}
         </View>
 
-        {isPublicView && token ? (
-          <View style={{ gap: 8, flexDirection: 'row', alignItems: 'flex-end' }}>
-            <FriendStatus
-              status={friendStatus}
-              data={data}
-              setModalInfo={setModalInfo}
-              handleSendFriendRequest={handleSendFriendRequest}
-              handleUpdateFriendStatus={handleUpdateFriendStatus}
-              authButton={data.canBeAuthenticated}
-            />
-            {data.canBeAuthenticated ? (
-              <TouchableOpacity
-                onPress={() =>
-                  setModalInfo({
-                    isVisible: true,
-                    type: 'authenticate',
-                    message: `Please confirm that you have personally met ${data.firstName} ${data.lastName}.`,
-                    action: handleAuthenticate,
-                    title: ''
-                  })
-                }
-                style={styles.authBtn}
-              >
-                <AuthIcon />
-                <Text style={styles.authText}>Authenticate</Text>
-              </TouchableOpacity>
-            ) : null}
+        {data.ownProfile === 0 && token ? (
+          <View style={{ gap: 8 }}>
+            <TouchableOpacity
+              style={[styles.authBtn, { backgroundColor: Colors.ORANGE }]}
+              onPress={data.goToChat}
+            >
+              <SendIcon fill={Colors.WHITE} height={15} />
+              <Text style={[styles.authText, { color: Colors.WHITE }]}>Send message</Text>
+            </TouchableOpacity>
+            <View style={{ gap: 8, flexDirection: 'row', alignItems: 'flex-end' }}>
+              <FriendStatus
+                status={friendStatus}
+                data={data}
+                setModalInfo={setModalInfo}
+                handleSendFriendRequest={handleSendFriendRequest}
+                handleUpdateFriendStatus={handleUpdateFriendStatus}
+                authButton={data.canBeAuthenticated}
+              />
+              {data.canBeAuthenticated ? (
+                <TouchableOpacity
+                  onPress={() =>
+                    setModalInfo({
+                      isVisible: true,
+                      type: 'authenticate',
+                      message: `Please confirm that you have personally met ${data.firstName} ${data.lastName}.`,
+                      action: handleAuthenticate,
+                      title: ''
+                    })
+                  }
+                  style={styles.authBtn}
+                >
+                  <AuthIcon />
+                  <Text style={styles.authText}>Authenticate</Text>
+                </TouchableOpacity>
+              ) : null}
+            </View>
           </View>
         ) : null}
 
@@ -569,6 +580,6 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
         onClose={() => setModalInfo({ ...modalInfo, isVisible: false })}
         title=""
       />
-    </>
+    </View>
   );
 };

+ 8 - 0
src/screens/InAppScreens/ProfileScreen/MyFriendsScreen/FriendsProfile/index.tsx

@@ -15,7 +15,9 @@ import { getFontSize } from 'src/utils';
 
 import TickIcon from 'assets/icons/tick.svg';
 import UNIcon from 'assets/icons/un_icon.svg';
+import UN25Icon from 'assets/icons/un-25.svg';
 import UN50Icon from 'assets/icons/un-50.svg';
+import UN75Icon from 'assets/icons/un-75.svg';
 import UN100Icon from 'assets/icons/un-100.svg';
 import UN150Icon from 'assets/icons/un-150.svg';
 import NMIcon from 'assets/icons/nm_icon.svg';
@@ -36,7 +38,9 @@ type Props = {
   badge_tbt?: number;
   badge_1281: number;
   badge_un: number;
+  badge_un_25: number;
   badge_un_50: number;
+  badge_un_75: number;
   badge_un_100: number;
   badge_un_150: number;
   auth: number;
@@ -59,7 +63,9 @@ export const FriendsProfile: FC<Props> = ({
   active_score,
   badge_1281,
   badge_un,
+  badge_un_25,
   badge_un_50,
+  badge_un_75,
   badge_un_100,
   badge_un_150,
   auth,
@@ -248,7 +254,9 @@ export const FriendsProfile: FC<Props> = ({
                   {badge_1281 ? <NMIcon /> : null}
                   {badge_un_150 ? <UN150Icon /> : null}
                   {badge_un_100 ? <UN100Icon /> : null}
+                  {badge_un_75 ? <UN75Icon /> : null}
                   {badge_un_50 ? <UN50Icon /> : null}
+                  {badge_un_25 ? <UN25Icon /> : null}
                 </View>
               </View>
             </View>

+ 2 - 0
src/screens/InAppScreens/ProfileScreen/MyFriendsScreen/index.tsx

@@ -269,7 +269,9 @@ const MyFriendsScreen: FC<Props> = ({ navigation }) => {
                   badge_tbt={item.badge_tbt}
                   badge_1281={item.badge_1281}
                   badge_un={item.badge_un}
+                  badge_un_25={item.badge_un_25}
                   badge_un_50={item.badge_un_50}
+                  badge_un_75={item.badge_un_75}
                   badge_un_100={item.badge_un_100}
                   badge_un_150={item.badge_un_150}
                   auth={item.auth}

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

@@ -1,5 +1,5 @@
 import React, { useState } from 'react';
-import { ScrollView, View } from 'react-native';
+import { ScrollView, View, Text, Linking, StyleSheet } from 'react-native';
 import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
 import { Formik } from 'formik';
 import * as yup from 'yup';
@@ -10,6 +10,7 @@ import { Image } from 'expo-image';
 import {
   usePostGetProfileQuery,
   usePostSetProfileMutation,
+  usePostUpdateEmailMutation,
   userQueryKeys,
   type PostSetProfileData
 } from '@api/user';
@@ -46,6 +47,7 @@ import { useDeleteUserMutation } from '@api/app';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 import { useAvatarStore } from 'src/stores/avatarVersionStore';
 import { useFriendsNotificationsStore } from 'src/stores/friendsNotificationsStore';
+import { getFontSize } from 'src/utils';
 
 const ProfileSchema = yup.object({
   username: yup.string().optional(),
@@ -61,7 +63,8 @@ const ProfileSchema = yup.object({
   i: yup.string().optional(),
   y: yup.string().optional(),
   www: yup.string().optional(),
-  other: yup.string().optional()
+  other: yup.string().optional(),
+  new_email: yup.string().email().optional()
 });
 
 export const EditPersonalInfo = () => {
@@ -70,6 +73,7 @@ export const EditPersonalInfo = () => {
   const { mutate: deleteUser } = useDeleteUserMutation();
 
   const { mutate: updateProfile, data: updateResponse, reset } = usePostSetProfileMutation();
+  const { mutate: updateEmail } = usePostUpdateEmailMutation();
 
   const navigation = useNavigation();
   const queryClient = useQueryClient();
@@ -80,6 +84,7 @@ export const EditPersonalInfo = () => {
   );
   const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
   const [isSubmitting, setIsSubmitting] = useState(false);
+  const [isNewEmailSubmitted, setIsNewEmailSubmitted] = useState(false);
   const { incrementAvatarVersion } = useAvatarStore();
 
   const regions = useGetRegionsWithFlagQuery(true);
@@ -128,10 +133,26 @@ export const EditPersonalInfo = () => {
     deleteUser({ token }, { onSuccess: handleLogout });
   };
 
+  const handleVerifyEmailChange = async (newEmail: string) => {
+    setIsSubmitting(true);
+    await updateEmail(
+      { token, email: newEmail },
+      {
+        onSuccess: () => {
+          setIsSubmitting(false);
+          setIsNewEmailSubmitted(true);
+        },
+        onError: () => {
+          setIsSubmitting(false);
+        }
+      }
+    );
+  };
+
   return (
     <PageWrapper>
+      <Header label={'Edit Personal Info'} />
       <ScrollView showsVerticalScrollIndicator={false}>
-        <Header label={'Edit Personal Info'} />
         <KeyboardAwareScrollView>
           <Formik
             validationSchema={ProfileSchema}
@@ -154,7 +175,8 @@ export const EditPersonalInfo = () => {
                 type: '',
                 uri: '',
                 name: ''
-              }
+              },
+              new_email: ''
             }}
             onSubmit={async (values) => {
               setIsSubmitting(true);
@@ -244,6 +266,51 @@ export const EditPersonalInfo = () => {
                   onBlur={props.handleBlur('email')}
                   formikError={props.touched.email && props.errors.email}
                 />
+                <Input
+                  editable={!isNewEmailSubmitted}
+                  header={'Change email address'}
+                  placeholder={'new_email@address.com'}
+                  inputMode={'email'}
+                  onChange={props.handleChange('new_email')}
+                  value={props.values.new_email}
+                  onBlur={props.handleBlur('new_email')}
+                  formikError={props.touched.new_email && props.errors.new_email}
+                />
+                {!props.errors.new_email && props.values.new_email && !isNewEmailSubmitted ? (
+                  <Button
+                    onPress={() => handleVerifyEmailChange(props.values.new_email)}
+                    disabled={isSubmitting}
+                    variant={ButtonVariants.FILL}
+                  >
+                    Verify email change
+                  </Button>
+                ) : (
+                  <Button onPress={() => {}} disabled={true} variant={ButtonVariants.OPACITY}>
+                    Verify email change
+                  </Button>
+                )}
+
+                {isNewEmailSubmitted ? (
+                  <Text style={styles.text}>
+                    Confirmation message sent. Please check your old email inbox.
+                  </Text>
+                ) : (
+                  <Text style={styles.text}>
+                    We will send a confirmation message to your old email address. Once you click
+                    the link in that message, your new email will become active. If you don't have
+                    access to your old email mailbox, please contact us{' '}
+                    <Text
+                      style={{
+                        color: Colors.ORANGE
+                      }}
+                      onPress={() => Linking.openURL('https://nomadmania.com/contact/')}
+                    >
+                      here
+                    </Text>
+                    .
+                  </Text>
+                )}
+
                 <BigText>General Info</BigText>
                 <Input
                   header={'First name'}
@@ -390,3 +457,11 @@ export const EditPersonalInfo = () => {
     </PageWrapper>
   );
 };
+
+const styles = StyleSheet.create({
+  text: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(12),
+    fontFamily: 'redhat-600'
+  }
+});

+ 4 - 0
src/screens/InAppScreens/ProfileScreen/ShareScreen/index.tsx

@@ -12,7 +12,9 @@ import Logo from 'assets/images/logo.svg';
 import TickIcon from 'assets/icons/tick.svg';
 import UNIcon from 'assets/icons/un_icon.svg';
 import NMIcon from 'assets/icons/nm_icon.svg';
+import UN25Icon from 'assets/icons/un-25.svg';
 import UN50Icon from 'assets/icons/un-50.svg';
+import UN75Icon from 'assets/icons/un-75.svg';
 import UN100Icon from 'assets/icons/un-100.svg';
 import UN150Icon from 'assets/icons/un-150.svg';
 import { styles } from './styles';
@@ -95,7 +97,9 @@ const PreviewScreen = ({ route }: { route: any }) => {
                       {data.badge_nm ? <NMIcon /> : null}
                       {data.badge_un_150 ? <UN150Icon /> : null}
                       {data.badge_un_100 ? <UN100Icon /> : null}
+                      {data.badge_un_75 ? <UN75Icon /> : null}
                       {data.badge_un_50 ? <UN50Icon /> : null}
+                      {data.badge_un_25 ? <UN25Icon /> : null}
                     </View>
                   </View>
                 </View>

+ 8 - 10
src/screens/InAppScreens/ProfileScreen/index.tsx

@@ -31,7 +31,9 @@ import TBTIcon from '../../../../assets/icons/tbt.svg';
 import TickIcon from '../../../../assets/icons/tick.svg';
 import UNIcon from '../../../../assets/icons/un_icon.svg';
 import NMIcon from '../../../../assets/icons/nm_icon.svg';
+import UN25Icon from '../../../../assets/icons/un-25.svg';
 import UN50Icon from '../../../../assets/icons/un-50.svg';
+import UN75Icon from '../../../../assets/icons/un-75.svg';
 import UN100Icon from '../../../../assets/icons/un-100.svg';
 import UN150Icon from '../../../../assets/icons/un-150.svg';
 import ChevronIcon from '../../../../assets/icons/chevron-left.svg';
@@ -319,7 +321,9 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
                   {data.user_data.badge_nm ? <NMIcon /> : null}
                   {data.user_data.badge_un_150 ? <UN150Icon /> : null}
                   {data.user_data.badge_un_100 ? <UN100Icon /> : null}
+                  {data.user_data.badge_un_75 ? <UN75Icon /> : null}
                   {data.user_data.badge_un_50 ? <UN50Icon /> : null}
+                  {data.user_data.badge_un_25 ? <UN25Icon /> : null}
 
                   {data.user_data.badge_ghost ? (
                     <Tooltip
@@ -355,7 +359,9 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
                           auth: data.user_data.auth,
                           badge_un: data.user_data.badge_un,
                           badge_nm: data.user_data.badge_nm,
+                          badge_un_25: data.user_data.badge_un_25,
                           badge_un_50: data.user_data.badge_un_50,
+                          badge_un_75: data.user_data.badge_un_75,
                           badge_un_100: data.user_data.badge_un_100,
                           badge_un_150: data.user_data.badge_un_150,
                           scores: data.scores
@@ -383,16 +389,7 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
                     />
                   </TouchableOpacity>
                 </>
-              ) : (
-                <TouchableOpacity style={styles.settings} onPress={handleGoToChat}>
-                  <CommentsIcon
-                    width={20}
-                    height={20}
-                    fill={Colors.DARK_BLUE}
-                    style={{ alignSelf: 'center' }}
-                  />
-                </TouchableOpacity>
-              )}
+              ) : null}
             </View>
 
             {hasActiveLinks() && (
@@ -446,6 +443,7 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
             isFriend,
             canBeAuthenticated,
             setCanBeAuthenticated,
+            goToChat: handleGoToChat,
             friendDbId: data.friend_db_id,
             ownProfile: data.own_profile,
             locationSharing: data.location_sharing,

+ 8 - 0
src/screens/InAppScreens/TravellersScreen/Components/Profile.tsx

@@ -18,7 +18,9 @@ import { adaptiveStyle, Colors } from '../../../../theme';
 import TickIcon from '../../../../../assets/icons/TickIcon';
 import UNIcon from '../../../../../assets/icons/UNIcon';
 import NMIcon from '../../../../../assets/icons/NMIcon';
+import UN25Icon from '../../../../../assets/icons/un-25.svg';
 import UN50Icon from '../../../../../assets/icons/un-50.svg';
+import UN75Icon from '../../../../../assets/icons/un-75.svg';
 import UN100Icon from '../../../../../assets/icons/un-100.svg';
 import UN150Icon from '../../../../../assets/icons/un-150.svg';
 import TBTIcon from '../../../../../assets/icons/tbt.svg';
@@ -42,7 +44,9 @@ type Props = {
   badge_tbt?: number;
   badge_1281: number;
   badge_un: number;
+  badge_un_25: number;
   badge_un_50: number;
+  badge_un_75: number;
   badge_un_100: number;
   badge_un_150: number;
   auth: number;
@@ -64,7 +68,9 @@ export const Profile: FC<Props> = ({
   badge_tbt,
   badge_1281,
   badge_un,
+  badge_un_25,
   badge_un_50,
+  badge_un_75,
   badge_un_100,
   badge_un_150,
   auth,
@@ -289,7 +295,9 @@ export const Profile: FC<Props> = ({
                     {badge_1281 ? <NMIcon isBlackAndWhite={index === -1} /> : null}
                     {badge_un_150 ? <UN150Icon /> : null}
                     {badge_un_100 ? <UN100Icon /> : null}
+                    {badge_un_75 ? <UN75Icon /> : null}
                     {badge_un_50 ? <UN50Icon /> : null}
+                    {badge_un_25 ? <UN25Icon /> : null}
                   </View>
                 </View>
               </View>

+ 4 - 0
src/screens/InAppScreens/TravellersScreen/Components/SeriesRankingItem.tsx

@@ -15,7 +15,9 @@ import formatNumber from '../../TravelsScreen/utils/formatNumber';
 import TickIcon from 'assets/icons/tick.svg';
 import UNIcon from 'assets/icons/un_icon.svg';
 import NMIcon from 'assets/icons/nm_icon.svg';
+import UN25Icon from 'assets/icons/un-25.svg';
 import UN50Icon from 'assets/icons/un-50.svg';
+import UN75Icon from 'assets/icons/un-75.svg';
 import UN100Icon from 'assets/icons/un-100.svg';
 import UN150Icon from 'assets/icons/un-150.svg';
 import ArrowIcon from 'assets/icons/arrow-bold.svg';
@@ -98,7 +100,9 @@ const SeriesRankingItem = React.memo(
                   {item.badge_nm === 1 ? <NMIcon /> : null}
                   {item.badge_un_150 ? <UN150Icon /> : null}
                   {item.badge_un_100 ? <UN100Icon /> : null}
+                  {item.badge_un_75 ? <UN75Icon /> : null}
                   {item.badge_un_50 ? <UN50Icon /> : null}
+                  {item.badge_un_25 ? <UN25Icon /> : null}
                 </View>
               </View>
 

+ 2 - 0
src/screens/InAppScreens/TravellersScreen/InHistoryScreen/index.tsx

@@ -68,7 +68,9 @@ const InHistoryScreen = () => {
             badge_tbt={item.badge_tbt}
             badge_1281={item.badge_1281}
             badge_un={item.badge_un}
+            badge_un_25={item.badge_un_25}
             badge_un_50={item.badge_un_50}
+            badge_un_75={item.badge_un_75}
             badge_un_100={item.badge_un_100}
             badge_un_150={item.badge_un_150}
             auth={item.auth}

+ 2 - 0
src/screens/InAppScreens/TravellersScreen/InMemoriamScreen/index.tsx

@@ -100,7 +100,9 @@ const InMemoriamScreen = () => {
             badge_tbt={item.badge_tbt}
             badge_1281={item.badge_1281}
             badge_un={item.badge_un}
+            badge_un_25={item.badge_un_25}
             badge_un_50={item.badge_un_50}
+            badge_un_75={item.badge_un_75}
             badge_un_100={item.badge_un_100}
             badge_un_150={item.badge_un_150}
             auth={item.auth}

+ 2 - 0
src/screens/InAppScreens/TravellersScreen/LPIRankingScreen/index.tsx

@@ -100,7 +100,9 @@ const LPIRankingScreen = () => {
             badge_tbt={item.badge_tbt}
             badge_1281={item.badge_1281}
             badge_un={item.badge_un}
+            badge_un_25={item.badge_un_25}
             badge_un_50={item.badge_un_50}
+            badge_un_75={item.badge_un_75}
             badge_un_100={item.badge_un_100}
             badge_un_150={item.badge_un_150}
             auth={item.auth}

+ 2 - 0
src/screens/InAppScreens/TravellersScreen/MasterRankingScreen/index.tsx

@@ -160,7 +160,9 @@ const MasterRankingScreen = () => {
             badge_tbt={item.badge_tbt}
             badge_1281={item.badge_1281}
             badge_un={item.badge_un}
+            badge_un_25={item.badge_un_25}
             badge_un_50={item.badge_un_50}
+            badge_un_75={item.badge_un_75}
             badge_un_100={item.badge_un_100}
             badge_un_150={item.badge_un_150}
             auth={item.auth}

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

@@ -140,7 +140,9 @@ export interface Ranking {
   flag2: string | null;
   badge_1281: number;
   badge_un: number;
+  badge_un_25: number;
   badge_un_50: number;
+  badge_un_75: number;
   badge_un_100: number;
   badge_un_150: number;
   badge_un_193: number;

+ 2 - 0
src/screens/InAppScreens/TravellersScreen/utils/types.ts

@@ -48,7 +48,9 @@ export interface SeriesRanking {
   authenticated: 0 | 1;
   badge_nm: 0 | 1;
   badge_un: 0 | 1;
+  badge_un_25: 0 | 1;
   badge_un_50: 0 | 1;
+  badge_un_75: 0 | 1;
   badge_un_100: 0 | 1;
   badge_un_150: 0 | 1;
   badge_un_193: 0 | 1;

+ 299 - 0
src/screens/InAppScreens/TravelsScreen/EventsScreen/index.tsx

@@ -0,0 +1,299 @@
+import React, { FC, useCallback, useEffect, useState } from 'react';
+import { View, Text, Image, TouchableOpacity, Linking, Dimensions, FlatList } from 'react-native';
+import ImageView from 'better-react-native-image-viewing';
+import { styles } from './styles';
+import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
+import { Colors } from 'src/theme';
+import { styles as ButtonStyles } from 'src/components/RegionPopup/style';
+
+import { ScrollView } from 'react-native-gesture-handler';
+import { NAVIGATION_PAGES } from 'src/types';
+import { API_HOST } from 'src/constants';
+import { StoreType, storage } from 'src/storage';
+import { ButtonVariants } from 'src/types/components';
+import formatNumber from '../../TravelsScreen/utils/formatNumber';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+
+import ChevronLeft from 'assets/icons/chevron-left.svg';
+import MapSvg from 'assets/icons/travels-screens/map-location.svg';
+import AddImgSvg from 'assets/icons/travels-screens/add-img.svg';
+import ShareIcon from 'assets/icons/share.svg';
+
+const testData = [
+  {
+    id: 1,
+    title: 'Attachment 1',
+    url: ''
+  },
+  {
+    id: 2,
+    title: 'Attachment 2',
+    url: ''
+  },
+  {
+    id: 3,
+    title: 'Attachment 3',
+    url: ''
+  },
+  {
+    id: 4,
+    title: 'Attachment 4',
+    url: ''
+  }
+];
+
+const EventsScreen = () => {
+  const token = (storage.get('token', StoreType.STRING) as string) ?? null;
+  const navigation = useNavigation();
+  const [isLoading, setIsLoading] = useState(true);
+  const fileWidth = Dimensions.get('window').width / 5;
+
+  return (
+    <View style={styles.container}>
+      <TouchableOpacity
+        onPress={() => {
+          navigation.goBack();
+        }}
+        style={styles.backButton}
+      >
+        <View style={styles.chevronWrapper}>
+          <ChevronLeft fill={Colors.WHITE} />
+        </View>
+      </TouchableOpacity>
+
+      <ScrollView
+        contentContainerStyle={{}}
+        nestedScrollEnabled={true}
+        showsVerticalScrollIndicator={false}
+      >
+        <View style={styles.emptyImage}>
+          <Image
+            source={require('../../../../../assets/images/logo-opacity.png')}
+            style={{ width: 100, height: 100 }}
+          />
+          <Text style={styles.emptyImageText}>No image available at this location</Text>
+        </View>
+        <TouchableOpacity onPress={() => {}} style={styles.addPhotoButton}>
+          <View style={styles.chevronWrapper}>
+            <AddImgSvg fill={Colors.WHITE} width={20} height={20} />
+          </View>
+        </TouchableOpacity>
+
+        <TouchableOpacity
+          onPress={() => {
+            // route.params?.isTravelsScreen || route.params?.isProfileScreen
+            //   ? navigation.dispatch(
+            //       CommonActions.reset({
+            //         index: 1,
+            //         routes: [
+            //           {
+            //             name: NAVIGATION_PAGES.IN_APP_MAP_TAB,
+            //             state: {
+            //               routes: [
+            //                 {
+            //                   name: NAVIGATION_PAGES.MAP_TAB,
+            //                   params: { id: regionId, type: type === 'nm' ? 'regions' : 'places' }
+            //                 }
+            //               ]
+            //             }
+            //           }
+            //         ]
+            //       })
+            //     )
+            //   : navigation.goBack();
+          }}
+          style={styles.goToMapBtn}
+        >
+          <View style={styles.chevronWrapper}>
+            <MapSvg fill={Colors.WHITE} />
+          </View>
+        </TouchableOpacity>
+
+        <View style={styles.wrapper}>
+          <View style={styles.nameContainer}>
+            <Text style={styles.title}>Meeting in San Clemente</Text>
+            <View style={[ButtonStyles.btnContainer, { gap: 2, flex: 0 }]}>
+              <TouchableOpacity
+                style={[ButtonStyles.btn, ButtonStyles.markVisitedButton]}
+                onPress={() => {}}
+              >
+                <Text style={[ButtonStyles.markVisitedText, { fontFamily: 'redhat-700' }]}>
+                  JOIN
+                </Text>
+              </TouchableOpacity>
+
+              <TouchableOpacity
+                onPress={() => {}}
+                style={{
+                  alignItems: 'center',
+                  justifyContent: 'center',
+                  padding: 8,
+                  paddingRight: 0
+                }}
+              >
+                <ShareIcon
+                  width={20}
+                  height={20}
+                  fill={Colors.DARK_BLUE}
+                  style={{ alignSelf: 'center' }}
+                />
+              </TouchableOpacity>
+            </View>
+          </View>
+
+          <View style={styles.divider} />
+
+          <View style={styles.stats}>
+            {true ? (
+              <View style={{ gap: 8, flex: 1 }}>
+                <Text style={[styles.travelSeriesTitle, { fontSize: 12 }]}>Host</Text>
+                <TouchableOpacity
+                  style={[styles.statItem, { justifyContent: 'flex-start' }]}
+                  onPress={() =>
+                    navigation.navigate(
+                      ...([
+                        NAVIGATION_PAGES.USERS_LIST,
+                        {
+                          id: 720,
+                          isFromHere: true,
+                          type: 'nm'
+                        }
+                      ] as never)
+                    )
+                  }
+                >
+                  <View style={styles.userImageContainer}>
+                    <Image
+                      source={{ uri: API_HOST }}
+                      style={[styles.userImage, { marginLeft: 0 }]}
+                    />
+                    <View style={{ justifyContent: 'space-between' }}>
+                      <Text
+                        style={{
+                          fontFamily: 'montserrat-700',
+                          fontSize: 12,
+                          color: Colors.DARK_BLUE
+                        }}
+                      >
+                        Harry Mitsidis
+                      </Text>
+                      <Text style={{ fontWeight: '600', fontSize: 12, color: Colors.DARK_BLUE }}>
+                        NM: <Text style={{ fontFamily: 'montserrat-700' }}>1284</Text> / UN:{' '}
+                        <Text style={{ fontFamily: 'montserrat-700' }}>193</Text>
+                      </Text>
+                    </View>
+                  </View>
+                </TouchableOpacity>
+              </View>
+            ) : (
+              <View style={[styles.statItem, { justifyContent: 'flex-start' }]} />
+            )}
+
+            {true ? (
+              <View style={{ gap: 8, flex: 1 }}>
+                <Text style={[styles.travelSeriesTitle, { fontSize: 12 }]}>Participants</Text>
+
+                <TouchableOpacity
+                  style={[styles.statItem, { justifyContent: 'flex-end' }]}
+                  onPress={() =>
+                    navigation.navigate(
+                      ...([
+                        NAVIGATION_PAGES.USERS_LIST,
+                        {
+                          id: 720,
+                          isFromHere: false,
+                          type: 'nm'
+                        }
+                      ] as never)
+                    )
+                  }
+                >
+                  <View style={styles.userImageContainer}>
+                    {/* {data?.data.users_who_visited_region &&
+                    data?.data.users_who_visited_region.length > 0 &&
+                    data?.data.users_who_visited_region?.map((user, index) => (
+                      <Image
+                        key={index}
+                        source={{ uri: API_HOST + user }}
+                        style={[styles.userImage]}
+                      />
+                    ))} */}
+                    <View style={styles.userCountContainer}>
+                      <Text style={styles.userCount}>
+                        {/* {formatNumber(data?.data.users_who_visited_region_count ?? 0)} */}
+                      </Text>
+                    </View>
+                  </View>
+                </TouchableOpacity>
+              </View>
+            ) : (
+              <View style={[styles.statItem, { justifyContent: 'flex-end' }]} />
+            )}
+          </View>
+
+          <View style={[styles.divider]} />
+
+          <View style={{ gap: 16 }}>
+            <Text style={styles.travelSeriesTitle}>Details</Text>
+            <Text style={{ fontWeight: '600', fontSize: 13, color: Colors.DARK_BLUE }}>
+              Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum
+              has been the industry's standard dummy text ever since the 1500s, when an unknown
+              printer took a galley of type and scrambled it to make a type specimen book. It has
+              survived not only five centuries, but also the leap into electronic typesetting,
+              remaining essentially unchanged. It was popularised in the 1960s with the release of
+              Letraset sheets containing Lorem Ipsum passages, and more recently with desktop
+              publishing software like Aldus PageMaker including versions of Lorem Ipsum.
+            </Text>
+          </View>
+
+          <View style={{ gap: 16 }}>
+            <Text style={styles.travelSeriesTitle}>Photos</Text>
+          </View>
+
+          <View style={{ gap: 16 }}>
+            <Text style={styles.travelSeriesTitle}>Attachments</Text>
+            <FlatList
+              data={testData}
+              renderItem={({ item }) => (
+                <TouchableOpacity
+                  style={{ gap: 4 }}
+                  onPress={() => {
+                    // Linking.openURL(item.url);
+                  }}
+                >
+                  <View
+                    style={{
+                      backgroundColor: Colors.FILL_LIGHT,
+                      borderRadius: 8,
+                      alignItems: 'center',
+                      justifyContent: 'center',
+                      height: fileWidth,
+                      width: fileWidth
+                    }}
+                  >
+                    <MaterialCommunityIcons name="file" size={36} color={Colors.DARK_BLUE} />
+                  </View>
+                  <Text style={{ fontSize: 12, fontWeight: '600', color: Colors.DARK_BLUE }}>
+                    {item.title}
+                  </Text>
+                </TouchableOpacity>
+              )}
+              style={{ flex: 1 }}
+              contentContainerStyle={{
+                flex: 1,
+                gap: 8
+              }}
+              keyExtractor={(item) => item.id.toString()}
+              numColumns={4}
+              columnWrapperStyle={{ justifyContent: 'space-between' }}
+              showsVerticalScrollIndicator={false}
+              scrollEnabled={false}
+            />
+          </View>
+        </View>
+      </ScrollView>
+    </View>
+  );
+};
+
+export default EventsScreen;

+ 216 - 0
src/screens/InAppScreens/TravelsScreen/EventsScreen/styles.tsx

@@ -0,0 +1,216 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from '../../../../theme';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: 'white',
+    position: 'relative'
+  },
+  chevronWrapper: {
+    width: 42,
+    height: 42,
+    borderRadius: 21,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: 'rgba(0, 0, 0, 0.3)'
+  },
+  backButton: {
+    position: 'absolute',
+    width: 50,
+    height: 50,
+    top: 50,
+    left: 10,
+    justifyContent: 'center',
+    alignItems: 'center',
+    zIndex: 2
+  },
+  emptyImage: {
+    height: 220,
+    width: '100%',
+    marginBottom: 12,
+    backgroundColor: Colors.FILL_LIGHT,
+    alignItems: 'center',
+    justifyContent: 'center',
+    gap: 16,
+    paddingTop: 24,
+    shadowColor: '#00000026',
+    shadowOffset: { width: 0, height: 0 },
+    shadowOpacity: 0.15,
+    shadowRadius: 8,
+    elevation: 8
+  },
+  emptyImageText: { fontWeight: '600', color: '#808080', fontSize: 12 },
+  imageFooter: { paddingBottom: 50, paddingHorizontal: 16, gap: 16 },
+  imageDescription: { color: Colors.WHITE, textAlign: 'center', fontWeight: '600' },
+  imageOwner: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    gap: 4,
+    backgroundColor: 'rgba(255, 255, 255, 0.8)',
+    paddingVertical: 8,
+    paddingHorizontal: 12,
+    borderRadius: 8,
+    alignSelf: 'center'
+  },
+  imageOwnerText: { color: Colors.DARK_BLUE, fontFamily: 'montserrat-700' },
+  wrapper: {
+    marginLeft: '5%',
+    marginRight: '5%',
+    gap: 16
+  },
+  divider: {
+    height: 1,
+    width: '100%',
+    backgroundColor: Colors.DARK_LIGHT
+  },
+  nameContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center'
+  },
+  title: {
+    fontSize: 18,
+    fontFamily: 'montserrat-700',
+    color: Colors.DARK_BLUE,
+    flex: 1
+  },
+  subtitle: {
+    fontSize: 12,
+    fontWeight: '600',
+    color: Colors.DARK_BLUE
+  },
+  stats: {
+    flexDirection: 'row',
+    justifyContent: 'space-between'
+  },
+  icon: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    backgroundColor: Colors.DARK_BLUE,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  statItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 4,
+    flex: 1
+  },
+  statText: {
+    fontSize: 14,
+    color: 'gray'
+  },
+  statNumber: {
+    fontSize: 16,
+    fontWeight: 'bold'
+  },
+  travelSeriesTitle: {
+    fontSize: 14,
+    fontFamily: 'montserrat-700',
+    textTransform: 'uppercase',
+    color: Colors.DARK_BLUE
+  },
+  userImageContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 4
+  },
+  userImage: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    marginLeft: -6,
+    borderWidth: 1,
+    borderColor: Colors.DARK_LIGHT,
+    resizeMode: 'cover'
+  },
+  userCountContainer: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    backgroundColor: Colors.DARK_LIGHT,
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginLeft: -6
+  },
+  userCount: {
+    fontSize: 12,
+    color: Colors.DARK_BLUE,
+    lineHeight: 24
+  },
+  modalView: {
+    paddingHorizontal: 8,
+    paddingVertical: 24,
+    alignItems: 'center'
+  },
+  infoTitle: {
+    color: Colors.DARK_BLUE,
+    fontSize: 16,
+    fontWeight: '700',
+    textAlign: 'center',
+    marginBottom: 16
+  },
+  infoText: {
+    color: Colors.DARK_BLUE,
+    fontSize: 14,
+    fontWeight: '400',
+    textAlign: 'left',
+    marginBottom: 24
+  },
+  btnContainer: {
+    borderColor: Colors.DARK_BLUE,
+    backgroundColor: Colors.WHITE,
+    width: '60%'
+  },
+  infoContent: { flexDirection: 'row', alignItems: 'center', gap: 4 },
+  visitedButtonText: {
+    color: Colors.DARK_BLUE,
+    fontWeight: 'bold',
+    fontSize: 13
+  },
+  durationContainer: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    gap: 12
+  },
+  durationItem: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    gap: 4
+  },
+  durationIconActive: {
+    backgroundColor: Colors.WHITE
+  },
+  durationIconInactive: {
+    backgroundColor: Colors.LIGHT_GRAY
+  },
+  visitDuration: {
+    color: Colors.DARK_BLUE,
+    fontWeight: '600',
+    fontSize: getFontSize(11)
+  },
+  goToMapBtn: {
+    position: 'absolute',
+    width: 50,
+    height: 50,
+    top: 50,
+    right: 10,
+    justifyContent: 'center',
+    alignItems: 'center',
+    zIndex: 2
+  },
+  addPhotoButton: {
+    position: 'absolute',
+    width: 50,
+    height: 50,
+    top: 160,
+    right: 10,
+    justifyContent: 'center',
+    alignItems: 'center',
+    zIndex: 2
+  }
+});

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

@@ -15,6 +15,7 @@ import EarthIcon from '../../../../assets/icons/travels-section/earth.svg';
 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 CalendarIcon from 'assets/icons/events/calendar-solid.svg';
 import InfoIcon from 'assets/icons/info-solid.svg';
 import { getFontSize } from 'src/utils';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -34,6 +35,7 @@ const TravelsScreen = () => {
     { label: 'Trips', icon: TripIcon, page: NAVIGATION_PAGES.TRIPS },
     { label: 'Photos', icon: ImagesIcon, page: NAVIGATION_PAGES.PHOTOS },
     { label: 'Fixers', icon: FixersIcon, page: NAVIGATION_PAGES.FIXERS }
+    // { label: 'Events', icon: CalendarIcon, page: NAVIGATION_PAGES.EVENTS }
   ];
 
   const handlePress = (page: string) => {

+ 2 - 0
src/types/api.ts

@@ -161,6 +161,7 @@ export enum API_ENDPOINT {
   AUTHENTICATE = 'authenticate',
   REPORT_CONVERSATION = 'report-conversation',
   GET_USERS_COUNT = 'get-users-on-map-count',
+  UPDATE_EMAIL = 'update-email',
 }
 
 export enum API {
@@ -295,6 +296,7 @@ export enum API {
   AUTHENTICATE = `${API_ROUTE.USER}/${API_ENDPOINT.AUTHENTICATE}`,
   REPORT_CONVERSATION = `${API_ROUTE.CHAT}/${API_ENDPOINT.REPORT_CONVERSATION}`,
   GET_USERS_COUNT = `${API_ROUTE.LOCATION}/${API_ENDPOINT.GET_USERS_COUNT}`,
+  UPDATE_EMAIL = `${API_ROUTE.USER}/${API_ENDPOINT.UPDATE_EMAIL}`,
 }
 
 export type BaseAxiosError = AxiosError;

+ 2 - 0
src/types/navigation.ts

@@ -71,4 +71,6 @@ export enum NAVIGATION_PAGES {
   CHATS_LIST = 'inAppChatsList',
   CHAT = 'inAppChat',
   LOCATION_SHARING = 'inAppLocationSharing',
+  EVENTS = 'inAppEvents',
+  FULL_MAP_VIEW = 'inAppFullMapView',
 }

+ 5 - 0
src/utils/request.ts

@@ -17,6 +17,11 @@ export const setupInterceptors = ({
     (config) => {
       config.headers['App-Version'] = APP_VERSION;
       config.headers['Platform'] = Platform.OS;
+
+      if (config.data instanceof FormData) {
+        config.timeout = 0;
+      }
+
       return config;
     },
     (error) => {