فهرست منبع

attachments func

Viktoriia 4 ماه پیش
والد
کامیت
2f8909d48b

+ 2 - 0
Route.tsx

@@ -96,6 +96,7 @@ 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();
 
@@ -415,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/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>

+ 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 () => {

+ 298 - 243
src/screens/InAppScreens/MessagesScreen/ChatScreen/index.tsx

@@ -3,7 +3,6 @@ import {
   View,
   TouchableOpacity,
   Image,
-  Modal,
   Text,
   FlatList,
   Dimensions,
@@ -13,8 +12,7 @@ import {
   ActivityIndicator,
   AppState,
   AppStateStatus,
-  TextInput,
-  Animated
+  TextInput
 } from 'react-native';
 import {
   GiftedChat,
@@ -33,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 { ResizeMode, Video, Audio, AVPlaybackStatus } 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';
@@ -50,7 +48,7 @@ import {
   usePostReactToMessageMutation,
   usePostSendMessageMutation
 } from '@api/chat';
-import { CustomMessage, Message, Reaction, Attachement } from '../types';
+import { CustomMessage, Message, Reaction } from '../types';
 import { API_HOST, WEBSOCKET_URL } from 'src/constants';
 import ReactionBar from '../Components/ReactionBar';
 import OptionsMenu from '../Components/OptionsMenu';
@@ -63,14 +61,16 @@ 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 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 ChatOptionsBlock from '../Components/ChatOptionsBlock';
+import RenderMessageVideo from '../Components/renderMessageVideo';
+import MessageLocation from '../Components/MessageLocation';
+import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
 
 const options = {
   enableVibrateFallback: true,
@@ -153,10 +153,6 @@ const ChatScreen = ({ route }: { route: any }) => {
   const [hasMoreMessages, setHasMoreMessages] = useState(true);
   const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
 
-  const [isPlaying, setIsPlaying] = useState(false);
-  const [isBuffering, setIsBuffering] = useState(false);
-  const videoRef = useRef<Video>(null);
-
   const appState = useRef(AppState.currentState);
   const textInputRef = useRef<TextInput>(null);
 
@@ -177,21 +173,37 @@ const ChatScreen = ({ route }: { route: any }) => {
   }, []);
 
   const onSendMedia = useCallback(
-    (files: { uri: string; type: 'image' | 'video' }[]) => {
-      const newMsgs = files.map((file) => {
-        const msg: IMessage = {
+    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' }
+          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 (file.type === 'image') {
-          msg.image = file.uri;
-        } else if (file.type === 'video') {
-          msg.video = file.uri;
+        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,
@@ -204,35 +216,45 @@ const ChatScreen = ({ route }: { route: any }) => {
           }
         };
 
-        console.log('messageData', messageData);
-
-        sendMessage(messageData, {
+        const res = await sendMessage(messageData, {
           onSuccess: (res) => {
-            console.log('res', res);
+            const { attachment, message_id } = res;
+
             const newMessage = {
-              _id: res.message_id,
+              _id: message_id,
               text: '',
-              attachment: res.attachment,
-              replyMessage: replyMessage
-                ? { text: replyMessage.text, id: replyMessage._id, sender: replyMessage.user?._id }
-                : undefined
+              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 === msg._id ? { ...msg, _id: res.message_id } : msg
-            //   )
-            // );
+            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);
-          },
-          onError: (err) => console.log('err', err)
+          }
         });
 
-        return msg;
-      });
-      clearReplyMessage();
-
-      setMessages((prev) => GiftedChat.append(prev, newMsgs));
+        clearReplyMessage();
+      }
     },
     [replyMessage]
   );
@@ -243,14 +265,33 @@ const ChatScreen = ({ route }: { route: any }) => {
 
   const onSendLocation = useCallback(
     (coords: { latitude: number; longitude: number }) => {
-      const locMsg: IMessage = {
+      const tempMessage: CustomMessage = {
         _id: Date.now() + Math.random(),
-        text: `Location: lat=${coords.latitude}, lon=${coords.longitude}`,
+        text: '',
         createdAt: new Date(),
         user: { _id: +currentUserId, name: 'Me' },
-        location: coords
+        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 locationFile = {
@@ -262,37 +303,33 @@ const ChatScreen = ({ route }: { route: any }) => {
       const messageData = {
         token,
         to_uid: id,
-        text: locMsg.text,
+        text: tempMessage.text,
         reply_to_id: replyMessage ? (replyMessage._id as number) : -1,
         attachment: locationFile
       };
 
-      console.log('messageData', messageData);
-
       sendMessage(messageData, {
         onSuccess: (res) => {
-          console.log('res', res);
+          const { attachment, message_id } = res;
+
           const newMessage = {
-            _id: res.message_id,
-            text: locMsg.text,
-            attachment: res.attachment,
-            replyMessage: replyMessage
-              ? { text: replyMessage.text, id: replyMessage._id, sender: replyMessage.user?._id }
-              : undefined
+            _id: message_id,
+            text: '',
+            attachment,
+            replyMessage: { ...tempMessage.replyMessage, sender: replyMessage?.user?._id }
           };
 
-          // setMessages((previousMessages) =>
-          //   (previousMessages ?? []).map((msg) =>
-          //     msg._id === locMsg._id ? { ...msg, _id: res.message_id } : msg
-          //   )
-          // );
-          // sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
-        },
-        onError: (err) => console.log('err', err)
+          setMessages((previousMessages) =>
+            (previousMessages ?? []).map((msg) =>
+              msg._id === tempMessage._id ? { ...msg, _id: res.message_id } : msg
+            )
+          );
+
+          sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
+        }
       });
-      clearReplyMessage();
 
-      setMessages((prev) => GiftedChat.append(prev, [locMsg]));
+      clearReplyMessage();
     },
     [replyMessage]
   );
@@ -300,25 +337,38 @@ const ChatScreen = ({ route }: { route: any }) => {
   const onSendFile = useCallback(
     (files: { uri: string; type: string; name?: string }[]) => {
       const newMsgs = files.map((file) => {
-        const msg: IMessage = {
+        const msg: CustomMessage = {
           _id: Date.now() + Math.random(),
           text: '',
           createdAt: new Date(),
-          user: { _id: +currentUserId, name: 'Me' }
+          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;
-        } else {
-          msg.attachment = {
-            uri: file.uri,
-            type: file.type,
-            name: file.name || 'Attachment'
-          };
         }
 
+        setMessages((previousMessages) => GiftedChat.append(previousMessages ?? [], [msg]));
+
         const messageData = {
           token,
           to_uid: id,
@@ -331,46 +381,76 @@ const ChatScreen = ({ route }: { route: any }) => {
           }
         };
 
-        console.log('messageData', messageData);
-
         sendMessage(messageData, {
           onSuccess: (res) => {
-            console.log('res', res);
+            const { attachment, message_id } = res;
+
             const newMessage = {
-              _id: res.message_id,
+              _id: message_id,
               text: '',
-              attachment: res.attachment,
-              replyMessage: replyMessage
-                ? { text: replyMessage.text, id: replyMessage._id, sender: replyMessage.user?._id }
-                : undefined
+              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((msg) =>
-            //     msg._id === msg._id ? { ...msg, _id: res.message_id } : msg
-            //   )
-            // );
+            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);
-          },
-          onError: (err) => console.log('err', err)
+          }
         });
 
         return msg;
       });
 
       clearReplyMessage();
-      setMessages((prev) => GiftedChat.append(prev, newMsgs));
     },
     [replyMessage]
   );
 
   async function openFileInApp(uri: string, fileName: string) {
     try {
-      // const fileUri = FileSystem.cacheDirectory + encodeURIComponent(fileName);
+      const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
+      if (!dirExist.exists) {
+        await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
+      }
 
-      // const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri);
+      const fileUri = `${CACHED_ATTACHMENTS_DIR}${fileName}`;
 
-      await FileViewer.open(uri, {
+      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
       });
@@ -380,28 +460,46 @@ const ChatScreen = ({ route }: { route: any }) => {
     }
   }
 
-  async function downloadFileToDevice(uri: string, fileName: string) {
+  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 downloadFolder = FileSystem.documentDirectory || FileSystem.cacheDirectory;
-      const fileUri = downloadFolder + encodeURIComponent(fileName);
+      const { status } = await MediaLibrary.requestPermissionsAsync();
+      if (status !== 'granted') {
+        return;
+      }
 
-      const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri);
+      const downloadOptions = currentMessage.video ? { headers: { Nmtoken: token } } : undefined;
+      const { uri } = await FileSystem.downloadAsync(fileUrl, fileUri, downloadOptions);
 
-      Alert.alert('File Saved', `File has been saved to:\n${localUri}`);
-    } catch (err) {
-      console.warn('downloadFileToDevice error:', err);
-      Alert.alert('Error', 'Could not download the file.');
+      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 { uri, type, name } = currentMessage.attachment;
-    // const fileName = name ?? 'Attachment';
-    const fileName = 'Attachment';
+    const { attachment_link, filename } = currentMessage.attachment;
+    const fileName = filename ?? 'Attachment';
+    const uri = API_HOST + attachment_link;
 
     return (
       <TouchableOpacity
@@ -409,14 +507,24 @@ const ChatScreen = ({ route }: { route: any }) => {
           styles.fileContainer,
           { backgroundColor: leftMessage ? 'rgba(15, 63, 79, 0.2)' : 'rgba(244, 244, 244, 0.2)' }
         ]}
-        // onPress={() => openFileInApp(uri, fileName)}
-        // onLongPress={() => downloadFileToDevice(uri, fileName)}
+        onPress={() => {
+          openFileInApp(uri, fileName);
+        }}
+        onLongPress={() => handleLongPress(currentMessage, props)}
+        disabled={currentMessage?.isSending}
       >
-        <MaterialCommunityIcons
-          name="file"
-          size={32}
-          color={leftMessage ? Colors.DARK_BLUE : Colors.FILL_LIGHT}
-        />
+        {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,
@@ -429,6 +537,30 @@ const ChatScreen = ({ route }: { route: any }) => {
     );
   };
 
+  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(),
@@ -440,89 +572,6 @@ const ChatScreen = ({ route }: { route: any }) => {
     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: Colors.DARK_BLUE }}>
-        <Video
-          ref={videoRef}
-          source={{ uri: currentMessage.video, headers: { Nmtoken: token } }}
-          style={{ flex: 1 }}
-          useNativeControls
-          resizeMode={ResizeMode.CONTAIN}
-          // isLooping
-          isMuted={false}
-          volume={1.0}
-          shouldCorrectPitch
-          onPlaybackStatusUpdate={(playbackStatus) => {
-            if (!playbackStatus.isLoaded) {
-              setIsPlaying(false);
-              setIsBuffering(false);
-              return;
-            }
-
-            setIsPlaying(playbackStatus.isPlaying);
-            setIsBuffering(playbackStatus.isBuffering ?? false);
-          }}
-        />
-
-        {isBuffering && (
-          <View
-            style={{
-              position: 'absolute',
-              top: 0,
-              left: 0,
-              right: 0,
-              bottom: 0,
-              alignItems: 'center',
-              justifyContent: 'center'
-            }}
-          >
-            <ActivityIndicator size="large" color="#FFF" />
-          </View>
-        )}
-
-        {isBuffering && (
-          <View
-            style={{
-              position: 'absolute',
-              top: 0,
-              left: 0,
-              right: 0,
-              bottom: 0,
-              alignItems: 'center',
-              justifyContent: 'center'
-            }}
-          >
-            <ActivityIndicator size="large" color="#FFF" />
-          </View>
-        )}
-
-        {!isPlaying && !isBuffering ? (
-          <TouchableOpacity
-            style={{
-              position: 'absolute',
-              top: 0,
-              left: 0,
-              right: 0,
-              bottom: 0,
-              alignItems: 'center',
-              justifyContent: 'center'
-            }}
-            onPress={async () => {
-              await videoRef.current?.presentFullscreenPlayer();
-              await videoRef.current?.playAsync();
-            }}
-          >
-            <MaterialCommunityIcons name="play-circle-outline" size={48} color="#FFF" />
-          </TouchableOpacity>
-        ) : null}
-      </View>
-    );
-  };
-
   useEffect(() => {
     let unsubscribe: any;
 
@@ -756,7 +805,7 @@ const ChatScreen = ({ route }: { route: any }) => {
           reply_to: message.replyMessage ?? null,
           reactions: message.reactions ?? '{}',
           status: 2,
-          attachement: -1
+          attachement: message.attachment ? message.attachment : -1
         };
       }
 
@@ -816,12 +865,13 @@ const ChatScreen = ({ route }: { route: any }) => {
       sent: message.status === 2,
       received: message.status === 3,
       deleted: message.status === 4,
+      isSending: false,
       video:
-        message.attachement !== -1 && message.attachement?.filetype.startsWith('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')
+        message.attachement !== -1 && message.attachement?.filetype?.startsWith('image')
           ? API_HOST + message.attachement?.attachment_small_url
           : null
     };
@@ -837,7 +887,6 @@ const ChatScreen = ({ route }: { route: any }) => {
     useCallback(() => {
       if (chatData?.messages) {
         const mappedMessages = chatData.messages.map(mapApiMessageToGiftedMessage);
-        console.log('mappedMessages', mappedMessages[0]);
 
         if (unreadMessageIndex === null && !isFetching) {
           const firstUnreadIndex = mappedMessages.findLastIndex(
@@ -1035,7 +1084,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;
@@ -1068,8 +1120,7 @@ const ChatScreen = ({ route }: { route: any }) => {
         setIsModalVisible(false);
         break;
       case 'download':
-        console.log('download');
-        downloadFileToDevice(selectedMessage.currentMessage?.image, 'Attachment');
+        downloadFileToDevice(selectedMessage.currentMessage);
         setIsModalVisible(false);
         break;
       default:
@@ -1196,6 +1247,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)
+                  : null
+            }
           />
         </ScrollView>
       </View>
@@ -1251,8 +1311,7 @@ const ChatScreen = ({ route }: { route: any }) => {
               )
             );
             sendWebSocketMessage('new_message', newMessage as unknown as CustomMessage);
-          },
-          onError: (err) => console.log('err', err)
+          }
         }
       );
 
@@ -1291,8 +1350,7 @@ const ChatScreen = ({ route }: { route: any }) => {
           if (message) {
             sendWebSocketMessage('new_reaction', message, reaction);
           }
-        },
-        onError: (err) => console.log('err', err)
+        }
       }
     );
 
@@ -1396,13 +1454,34 @@ const ChatScreen = ({ route }: { route: any }) => {
 
   const renderMessageImage = (props: any) => {
     const { currentMessage } = props;
+    const leftMessage = currentMessage?.user?._id !== +currentUserId;
+
     return (
       <TouchableOpacity
         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>
     );
   };
@@ -1477,44 +1556,6 @@ const ChatScreen = ({ route }: { route: any }) => {
         ? Colors.DARK_BLUE
         : Colors.FILL_LIGHT;
 
-    if (currentMessage.attachment) {
-      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}
-            renderCustomView={() => renderMessageFile(props)}
-          />
-        </View>
-      );
-    }
-
     return (
       <View
         key={`${currentMessage._id}-${isHighlighted ? 'highlighted' : 'normal'}`}
@@ -1546,6 +1587,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)
+                : null
+          }
         />
       </View>
     );
@@ -1698,7 +1746,14 @@ const ChatScreen = ({ route }: { route: any }) => {
                 {!props.text?.trim() && <SendIcon fill={Colors.LIGHT_GRAY} />}
               </View>
             )}
-            renderMessageVideo={renderMessageVideo}
+            renderMessageVideo={(props) => (
+              <RenderMessageVideo
+                props={props}
+                token={token}
+                currentUserId={+currentUserId}
+                onLongPress={handleLongPress}
+              />
+            )}
             textInputProps={{
               ...styles.composer,
               selectionColor: Colors.LIGHT_GRAY
@@ -1755,7 +1810,7 @@ const ChatScreen = ({ route }: { route: any }) => {
         )}
 
         <ImageView
-          images={[{ uri: selectedMedia }]}
+          images={[{ uri: selectedMedia, cache: 'force-cache' }]}
           imageIndex={0}
           visible={!!selectedMedia}
           onRequestClose={() => setSelectedMedia(null)}

+ 4 - 4
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,
@@ -154,7 +154,7 @@ export const styles = StyleSheet.create({
   },
   optionsContainer: {
     width: '100%',
-    backgroundColor: Colors.FILL_LIGHT,
+    backgroundColor: Colors.FILL_LIGHT
     // borderTopWidth: 1,
     // borderTopColor: '#ccc'
   },
@@ -182,8 +182,8 @@ export const styles = StyleSheet.create({
     alignItems: 'center',
     padding: 6,
     borderRadius: 8,
-    marginVertical: 8,
-    marginHorizontal: 8
+    marginVertical: 6,
+    marginHorizontal: 6
   },
   fileNameText: {
     marginLeft: 4,

+ 14 - 9
src/screens/InAppScreens/MessagesScreen/Components/AttachmentsModal.tsx

@@ -1,5 +1,5 @@
-import React, { useCallback, useRef, useState } from 'react';
-import { StyleSheet, TouchableOpacity, View, Text, Button } from 'react-native';
+import React, { useRef, useState } from 'react';
+import { StyleSheet, TouchableOpacity, View, Text } from 'react-native';
 import ActionSheet, { Route, SheetManager, useSheetRouter } from 'react-native-actions-sheet';
 import { getFontSize } from 'src/utils';
 import { Colors } from 'src/theme';
@@ -9,12 +9,18 @@ import { usePostReportConversationMutation } from '@api/chat';
 import * as ImagePicker from 'expo-image-picker';
 import * as DocumentPicker from 'react-native-document-picker';
 
-import MegaphoneIcon from 'assets/icons/messages/megaphone.svg';
 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 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);
@@ -36,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
         });
       }
@@ -141,7 +147,6 @@ const AttachmentsModal = () => {
       }
     } catch (err) {
       if (DocumentPicker.isCancel(err)) {
-        console.log('User canceled document picker');
       } else {
         console.warn('DocumentPicker error:', err);
       }
@@ -161,12 +166,12 @@ const AttachmentsModal = () => {
       >
         <View style={styles.optionRow}>
           <TouchableOpacity style={styles.optionItem} onPress={handleOpenGallery}>
-            <MaterialCommunityIcons name="image" size={36} color={Colors.ORANGE} />
+            <ImagesIcon height={36} />
             <Text style={styles.optionLabel}>Gallery</Text>
           </TouchableOpacity>
 
           <TouchableOpacity style={styles.optionItem} onPress={handleOpenCamera}>
-            <MaterialCommunityIcons name="camera" size={36} color={Colors.ORANGE} />
+            <CameraIcon height={36} />
             <Text style={styles.optionLabel}>Camera</Text>
           </TouchableOpacity>
 
@@ -176,7 +181,7 @@ const AttachmentsModal = () => {
               router?.navigate('route-b');
             }}
           >
-            <MaterialCommunityIcons name="map-marker" size={36} color={Colors.ORANGE} />
+            <LocationIcon height={36} />
             <Text style={styles.optionLabel}>Location</Text>
           </TouchableOpacity>
 
@@ -215,7 +220,7 @@ const AttachmentsModal = () => {
   return (
     <ActionSheet
       id="chat-attachments"
-      gestureEnabled={true}
+      // gestureEnabled={true}
       containerStyle={{
         backgroundColor: Colors.FILL_LIGHT
       }}

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

+ 5 - 3
src/screens/InAppScreens/MessagesScreen/Components/OptionsMenu.tsx

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

+ 9 - 22
src/screens/InAppScreens/MessagesScreen/Components/RouteB.tsx

@@ -1,5 +1,5 @@
 import React, { useEffect, useRef, useState } from 'react';
-import { StyleSheet, View, Text, ActivityIndicator, TouchableOpacity } from 'react-native';
+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';
@@ -27,7 +27,7 @@ const RouteB = () => {
     latitude: number;
     longitude: number;
   } | null>(null);
-  const [isLoading, setIsLoading] = useState(true);
+  const [loading, setLoading] = useState(true);
   const cameraRef = useRef<MapLibreRN.CameraRef | null>(null);
 
   useEffect(() => {
@@ -35,8 +35,6 @@ const RouteB = () => {
       try {
         const { status } = await Location.requestForegroundPermissionsAsync();
         if (status !== 'granted') {
-          console.warn('Permission for location not granted');
-          setIsLoading(false);
           return;
         }
 
@@ -49,8 +47,6 @@ const RouteB = () => {
         setSelectedLocation(coords);
       } catch (err) {
         console.warn('Error fetching location:', err);
-      } finally {
-        setIsLoading(false);
       }
     };
 
@@ -66,6 +62,7 @@ const RouteB = () => {
             zoomLevel: 14,
             animationDuration: 500
           });
+          setLoading(false);
         } else {
           console.warn('Camera ref is not available.');
         }
@@ -99,15 +96,6 @@ const RouteB = () => {
     }
   };
 
-  if (isLoading) {
-    return (
-      <View style={styles.loadingContainer}>
-        <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
-        <Text>Loading your location...</Text>
-      </View>
-    );
-  }
-
   return (
     <View style={[styles.container, { paddingBottom: 8 + insetsBottom }]}>
       <MapLibreRN.MapView
@@ -134,7 +122,12 @@ const RouteB = () => {
       </MapLibreRN.MapView>
 
       <View style={styles.mapActions}>
-        <Button children="Send my location" onPress={sendCurrentLocation} />
+        <Button
+          children="Send my location"
+          onPress={sendCurrentLocation}
+          variant={ButtonVariants.FILL}
+          disabled={loading}
+        />
         <Button children="Confirm" onPress={confirmLocation} />
         <Button
           children="Close"
@@ -182,12 +175,6 @@ const styles = StyleSheet.create({
     marginLeft: -12,
     marginTop: -12,
     zIndex: 1000
-  },
-  loadingContainer: {
-    flex: 1,
-    justifyContent: 'center',
-    alignItems: 'center',
-    backgroundColor: Colors.FILL_LIGHT
   }
 });
 

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

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

@@ -0,0 +1,82 @@
+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={{
+              width: 20,
+              height: 20,
+              borderRadius: 10,
+              backgroundColor: Colors.ORANGE,
+              borderWidth: 2,
+              borderColor: Colors.WHITE
+            }}
+          />
+        </MapLibreRN.MarkerView>
+      </MapLibreRN.MapView>
+      <TouchableOpacity
+        onPress={() => {
+          navigation.goBack();
+        }}
+        style={{
+          position: 'absolute',
+          width: 50,
+          height: 50,
+          top: 50,
+          left: 10,
+          justifyContent: 'center',
+          alignItems: 'center',
+          zIndex: 2
+        }}
+      >
+        <View
+          style={{
+            width: 42,
+            height: 42,
+            borderRadius: 21,
+            justifyContent: 'center',
+            alignItems: 'center',
+            backgroundColor: 'rgba(0, 0, 0, 0.3)'
+          }}
+        >
+          <ChevronLeft fill={Colors.WHITE} />
+        </View>
+      </TouchableOpacity>
+    </SafeAreaView>
+  );
+};
+
+const styles = StyleSheet.create({
+  map: {
+    ...StyleSheet.absoluteFillObject
+  }
+});
+
+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>
                 )}
 

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

@@ -78,6 +78,9 @@ export interface CustomMessage extends IMessage {
   deleted: boolean;
   attachment: Attachement | null;
   reactions: Reaction[] | {};
+  image?: string;
+  video?: string;
+  isSending?: boolean;
 }
 
 export type Reaction = {

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

@@ -34,8 +34,8 @@ const TravelsScreen = () => {
     { label: 'Earth', icon: EarthIcon, page: NAVIGATION_PAGES.EARTH },
     { 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 }
+    { label: 'Fixers', icon: FixersIcon, page: NAVIGATION_PAGES.FIXERS }
+    // { label: 'Events', icon: CalendarIcon, page: NAVIGATION_PAGES.EVENTS }
   ];
 
   const handlePress = (page: string) => {

+ 1 - 0
src/types/navigation.ts

@@ -72,4 +72,5 @@ export enum NAVIGATION_PAGES {
   CHAT = 'inAppChat',
   LOCATION_SHARING = 'inAppLocationSharing',
   EVENTS = 'inAppEvents',
+  FULL_MAP_VIEW = 'inAppFullMapView',
 }