Viktoriia 1 неделя назад
Родитель
Сommit
7a1a7ba3a8

+ 53 - 11
src/components/CustomImageViewer/index.tsx

@@ -1,8 +1,9 @@
 import React from 'react';
 import React from 'react';
-import { Modal, StyleSheet, View, TouchableOpacity, ActivityIndicator } from 'react-native';
+import { Modal, StyleSheet, View, TouchableOpacity, ActivityIndicator, Text } from 'react-native';
 import ImageViewer from 'react-native-image-zoom-viewer';
 import ImageViewer from 'react-native-image-zoom-viewer';
 import { Colors } from 'src/theme';
 import { Colors } from 'src/theme';
 import CloseIcon from 'assets/icons/close.svg';
 import CloseIcon from 'assets/icons/close.svg';
+import SaveIcon from 'assets/icons/travels-screens/save.svg';
 
 
 interface Photo {
 interface Photo {
   uri: string;
   uri: string;
@@ -18,6 +19,8 @@ interface CustomImageViewerProps {
   onImageIndexChange?: (index: number) => void;
   onImageIndexChange?: (index: number) => void;
   renderFooter?: (currentIndex: number) => React.ReactElement;
   renderFooter?: (currentIndex: number) => React.ReactElement;
   loadingColor?: string;
   loadingColor?: string;
+  withSaveButton?: boolean;
+  onRequestSave?: () => void;
 }
 }
 
 
 export const CustomImageViewer: React.FC<CustomImageViewerProps> = ({
 export const CustomImageViewer: React.FC<CustomImageViewerProps> = ({
@@ -28,7 +31,9 @@ export const CustomImageViewer: React.FC<CustomImageViewerProps> = ({
   backgroundColor = Colors.DARK_BLUE,
   backgroundColor = Colors.DARK_BLUE,
   onImageIndexChange,
   onImageIndexChange,
   renderFooter,
   renderFooter,
-  loadingColor = Colors.WHITE
+  loadingColor = Colors.WHITE,
+  withSaveButton = false,
+  onRequestSave = () => {}
 }) => {
 }) => {
   const [currentIndex, setCurrentIndex] = React.useState(imageIndex);
   const [currentIndex, setCurrentIndex] = React.useState(imageIndex);
 
 
@@ -53,15 +58,30 @@ export const CustomImageViewer: React.FC<CustomImageViewerProps> = ({
     : undefined;
     : undefined;
 
 
   const renderCloseButton = () => (
   const renderCloseButton = () => (
-    <TouchableOpacity
-      style={styles.closeButton}
-      onPress={onRequestClose}
-      hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
-    >
-      <View style={styles.closeIconContainer}>
-        <CloseIcon fill={Colors.WHITE} height={16} width={16} />
-      </View>
-    </TouchableOpacity>
+    <View style={{ flex: 1 }}>
+      <TouchableOpacity
+        style={styles.closeButton}
+        onPress={onRequestClose}
+        hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+      >
+        <View style={styles.closeIconContainer}>
+          <CloseIcon fill={Colors.WHITE} height={16} width={16} />
+        </View>
+      </TouchableOpacity>
+
+      {withSaveButton ? (
+        <TouchableOpacity
+          style={styles.saveButton}
+          onPress={onRequestSave}
+          hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+        >
+          <View style={styles.saveIconContainer}>
+            <SaveIcon fill={Colors.WHITE} height={16} width={16} />
+            <Text style={styles.saveText}>Save</Text>
+          </View>
+        </TouchableOpacity>
+      ) : null}
+    </View>
   );
   );
 
 
   const renderLoadingIndicator = () => (
   const renderLoadingIndicator = () => (
@@ -99,6 +119,13 @@ const styles = StyleSheet.create({
     width: '100%'
     width: '100%'
   },
   },
   closeButton: {
   closeButton: {
+    position: 'absolute',
+    top: 50,
+    left: 6,
+    zIndex: 1000,
+    padding: 8
+  },
+  saveButton: {
     position: 'absolute',
     position: 'absolute',
     top: 50,
     top: 50,
     right: 6,
     right: 6,
@@ -113,6 +140,21 @@ const styles = StyleSheet.create({
     alignItems: 'center',
     alignItems: 'center',
     backgroundColor: 'rgba(15, 63, 79, 0.7)'
     backgroundColor: 'rgba(15, 63, 79, 0.7)'
   },
   },
+  saveIconContainer: {
+    height: 46,
+    borderRadius: 23,
+    justifyContent: 'center',
+    flexDirection: 'row',
+    alignItems: 'center',
+    backgroundColor: 'rgba(15, 63, 79, 0.7)',
+    paddingHorizontal: 15,
+    gap: 6
+  },
+  saveText: {
+    color: Colors.WHITE,
+    fontWeight: '600',
+    fontSize: 14
+  },
   loadingContainer: {
   loadingContainer: {
     flex: 1,
     flex: 1,
     justifyContent: 'center',
     justifyContent: 'center',

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

@@ -59,6 +59,7 @@ import {
   compressImageWithProgress,
   compressImageWithProgress,
   compressVideoWithProgress,
   compressVideoWithProgress,
   dismissChatNotifications,
   dismissChatNotifications,
+  downloadImageFromViewer,
   isMessageEdited
   isMessageEdited
 } from '../utils';
 } from '../utils';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
@@ -2162,6 +2163,8 @@ const ChatScreen = ({ route }: { route: any }) => {
           visible={!!selectedMedia}
           visible={!!selectedMedia}
           onRequestClose={() => setSelectedMedia(null)}
           onRequestClose={() => setSelectedMedia(null)}
           backgroundColor={Colors.DARK_BLUE}
           backgroundColor={Colors.DARK_BLUE}
+          withSaveButton={true}
+          onRequestSave={() => downloadImageFromViewer(selectedMedia, token)}
         />
         />
 
 
         <ReactModal
         <ReactModal

+ 7 - 8
src/screens/InAppScreens/MessagesScreen/GroupChatScreen/index.tsx

@@ -70,6 +70,7 @@ import {
   compressImageWithProgress,
   compressImageWithProgress,
   compressVideoWithProgress,
   compressVideoWithProgress,
   dismissChatNotifications,
   dismissChatNotifications,
+  downloadImageFromViewer,
   isMessageEdited
   isMessageEdited
 } from '../utils';
 } from '../utils';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
@@ -1200,14 +1201,10 @@ const GroupChatScreen = ({ route }: { route: any }) => {
         setHasMoreMessages(false);
         setHasMoreMessages(false);
       } else {
       } else {
         setExtraMessages((prev) => {
         setExtraMessages((prev) => {
-          const existingIds = new Set(
-            [...allMessages, ...prev].map((m) => m.messageId)
-          );
-        
-          const deduped = older.filter(
-            (m) => !existingIds.has(m.messageId)
-          );
-        
+          const existingIds = new Set([...allMessages, ...prev].map((m) => m.messageId));
+
+          const deduped = older.filter((m) => !existingIds.has(m.messageId));
+
           return [...prev, ...deduped];
           return [...prev, ...deduped];
         });
         });
       }
       }
@@ -2475,6 +2472,8 @@ const GroupChatScreen = ({ route }: { route: any }) => {
           visible={!!selectedMedia}
           visible={!!selectedMedia}
           onRequestClose={() => setSelectedMedia(null)}
           onRequestClose={() => setSelectedMedia(null)}
           backgroundColor={Colors.DARK_BLUE}
           backgroundColor={Colors.DARK_BLUE}
+          withSaveButton={true}
+          onRequestSave={() => downloadImageFromViewer(selectedMedia, token)}
         />
         />
 
 
         <ReactModal
         <ReactModal

+ 38 - 1
src/screens/InAppScreens/MessagesScreen/utils.ts

@@ -1,11 +1,14 @@
 import moment from 'moment';
 import moment from 'moment';
 import * as Notifications from 'expo-notifications';
 import * as Notifications from 'expo-notifications';
-import { Platform } from 'react-native';
+import { Alert, Platform } from 'react-native';
 import { NAVIGATION_PAGES } from 'src/types';
 import { NAVIGATION_PAGES } from 'src/types';
 import { usePushNotification } from 'src/contexts/PushNotificationContext';
 import { usePushNotification } from 'src/contexts/PushNotificationContext';
 import { CommonActions } from '@react-navigation/native';
 import { CommonActions } from '@react-navigation/native';
 import { storage, StoreType } from 'src/storage';
 import { storage, StoreType } from 'src/storage';
 import { Image as ImageCompressor, Video as VideoCompressor } from 'react-native-compressor';
 import { Image as ImageCompressor, Video as VideoCompressor } from 'react-native-compressor';
+import * as FileSystem from 'expo-file-system/legacy';
+import Share from 'react-native-share';
+import { APP_VERSION } from 'src/constants';
 
 
 export const formatDate = (dateString: Date): string => {
 export const formatDate = (dateString: Date): string => {
   const inputDate = moment.utc(dateString).local();
   const inputDate = moment.utc(dateString).local();
@@ -159,3 +162,37 @@ export const compressVideoWithProgress = (
     onProgress(progress);
     onProgress(progress);
   });
   });
 };
 };
+
+export async function downloadImageFromViewer(uri: string, token: string) {
+  if (!uri) return;
+
+  try {
+    if (uri.startsWith('file://')) {
+      await Share.open({
+        url: uri,
+        failOnCancel: false
+      });
+      return;
+    }
+
+    const fileExt = 'jpg';
+    const fileName = `image_${Date.now()}.${fileExt}`;
+    const fileUri = `${FileSystem.cacheDirectory}${fileName}`;
+
+    const { uri: localUri } = await FileSystem.downloadAsync(uri, fileUri, {
+      headers: {
+        Nmtoken: token,
+        'App-Version': APP_VERSION,
+        Platform: Platform.OS
+      }
+    });
+
+    await Share.open({
+      url: localUri,
+      type: 'image/jpeg',
+      failOnCancel: false
+    });
+  } catch (e) {
+    Alert.alert('Error', 'Failed to save image');
+  }
+}