diff --git a/package.json b/package.json index 6e1edb3..48cf998 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "expo-file-system": "~18.1.11", "expo-font": "~13.3.2", "expo-image": "~2.4.0", + "expo-image-manipulator": "~13.1.7", "expo-image-picker": "~16.1.4", "expo-keep-awake": "~14.1.4", "expo-linking": "~7.1.7", diff --git a/src/components/calls/call-images-modal.tsx b/src/components/calls/call-images-modal.tsx index 7a3c185..3fd7af3 100644 --- a/src/components/calls/call-images-modal.tsx +++ b/src/components/calls/call-images-modal.tsx @@ -1,19 +1,21 @@ -import { useFocusEffect } from '@react-navigation/native'; import * as FileSystem from 'expo-file-system'; +import { Image } from 'expo-image'; +import * as ImageManipulator from 'expo-image-manipulator'; import * as ImagePicker from 'expo-image-picker'; import { CameraIcon, ChevronLeftIcon, ChevronRightIcon, ImageIcon, PlusIcon, XIcon } from 'lucide-react-native'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Dimensions, FlatList, Platform, TouchableOpacity, View } from 'react-native'; +import { Alert, Dimensions, FlatList, type ImageSourcePropType, Platform, StyleSheet, TouchableOpacity, View } from 'react-native'; import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; import { Loading } from '@/components/common/loading'; import ZeroState from '@/components/common/zero-state'; -import { Image } from '@/components/ui/image'; import { useAnalytics } from '@/hooks/use-analytics'; import { useAuthStore } from '@/lib'; import { type CallFileResultData } from '@/models/v4/callFiles/callFileResultData'; +import { useLocationStore } from '@/stores/app/location-store'; import { useCallDetailStore } from '@/stores/calls/detail-store'; +import { useToastStore } from '@/stores/toast/store'; import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetDragIndicator, ActionsheetDragIndicatorWrapper, ActionsheetItem, ActionsheetItemText } from '../ui/actionsheet'; import { Box } from '../ui/box'; @@ -22,6 +24,7 @@ import { HStack } from '../ui/hstack'; import { Input, InputField } from '../ui/input'; import { Text } from '../ui/text'; import { VStack } from '../ui/vstack'; +import FullScreenImageModal from './full-screen-image-modal'; interface CallImagesModalProps { isOpen: boolean; @@ -31,49 +34,48 @@ interface CallImagesModalProps { const { width } = Dimensions.get('window'); +const styles = StyleSheet.create({ + galleryImage: { + height: 256, // h-64 equivalent + width: '100%', + borderRadius: 8, // rounded-lg equivalent + }, + previewImage: { + height: 256, // h-64 equivalent + width: '100%', + borderRadius: 8, // rounded-lg equivalent + }, +}); + const CallImagesModal: React.FC = ({ isOpen, onClose, callId }) => { const { t } = useTranslation(); const { trackEvent } = useAnalytics(); + const { latitude, longitude } = useLocationStore(); + const { showToast } = useToastStore(); + const [activeIndex, setActiveIndex] = useState(0); const [isUploading, setIsUploading] = useState(false); - const [newImageName, setNewImageName] = useState(''); - const [selectedImage, setSelectedImage] = useState(null); + const [newImageNote, setNewImageNote] = useState(''); + const [selectedImageInfo, setSelectedImageInfo] = useState<{ uri: string; filename: string } | null>(null); const [isAddingImage, setIsAddingImage] = useState(false); const [imageErrors, setImageErrors] = useState>(new Set()); + const [fullScreenImage, setFullScreenImage] = useState<{ source: ImageSourcePropType; name?: string } | null>(null); const flatListRef = useRef(null); - // Track if modal was actually opened to avoid false close events - const wasModalOpenRef = useRef(false); - const { callImages, isLoadingImages, errorImages, fetchCallImages, uploadCallImage } = useCallDetailStore(); - // Filter valid images and memoize to prevent re-filtering on every render + // Filter out images without proper data or URL const validImages = useMemo(() => { if (!callImages) return []; - return callImages.filter((item) => item && (item.Data?.trim() || item.Url?.trim())); - }, [callImages]); - // Track analytics when modal becomes visible - useFocusEffect( - useCallback(() => { - if (isOpen) { - wasModalOpenRef.current = true; - try { - trackEvent('call_images_modal_viewed', { - timestamp: new Date().toISOString(), - callId, - imageCount: validImages.length, - hasImages: Boolean(validImages.length), - isLoading: isLoadingImages, - hasError: Boolean(errorImages), - }); - } catch (error) { - // Analytics errors should not break the component - console.warn('Failed to track call images modal analytics:', error); - } - } - }, [isOpen, trackEvent, callId, validImages.length, isLoadingImages, errorImages]) - ); + const filtered = callImages.filter((image: CallFileResultData) => { + const hasValidData = image.Data && image.Data.trim() !== ''; + const hasValidUrl = image.Url && image.Url.trim() !== ''; + return hasValidData || hasValidUrl; + }); + + return filtered; + }, [callImages]); useEffect(() => { if (isOpen && callId) { @@ -83,6 +85,19 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call } }, [isOpen, callId, fetchCallImages]); + // Track when call images modal is opened/rendered + useEffect(() => { + if (isOpen) { + trackEvent('call_images_modal_opened', { + callId: callId, + hasExistingImages: validImages.length > 0, + imagesCount: validImages.length, + isLoadingImages: isLoadingImages, + hasError: !!errorImages, + }); + } + }, [isOpen, trackEvent, callId, validImages.length, isLoadingImages, errorImages]); + // Reset active index when valid images change useEffect(() => { if (activeIndex >= validImages.length && validImages.length > 0) { @@ -93,7 +108,7 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call const handleImageSelect = async () => { const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (permissionResult.granted === false) { - alert(t('common.permission_denied')); + showToast('error', t('common.permission_denied')); return; } const result = await ImagePicker.launchImageLibraryAsync({ @@ -101,154 +116,102 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call allowsEditing: true, quality: 0.8, }); - if (!result.canceled && result.assets.length > 0) { - const firstAsset = result.assets[0]; - if (firstAsset) { - setSelectedImage(firstAsset.uri); - } + if (!result.canceled) { + const asset = result.assets[0]; + const filename = asset.fileName || `image_${Date.now()}.png`; + setSelectedImageInfo({ uri: asset.uri, filename }); } }; const handleCameraCapture = async () => { const permissionResult = await ImagePicker.requestCameraPermissionsAsync(); if (permissionResult.granted === false) { - alert(t('common.permission_denied')); + showToast('error', t('common.permission_denied')); return; } const result = await ImagePicker.launchCameraAsync({ allowsEditing: true, quality: 0.8, }); - if (!result.canceled && result.assets.length > 0) { - const firstAsset = result.assets[0]; - if (firstAsset) { - setSelectedImage(firstAsset.uri); - } + if (!result.canceled) { + const asset = result.assets[0]; + const filename = `camera_${Date.now()}.png`; + setSelectedImageInfo({ uri: asset.uri, filename }); } }; const handleUploadImage = async () => { - if (!selectedImage) return; + if (!selectedImageInfo) return; setIsUploading(true); - - // Track upload attempt try { - trackEvent('call_images_upload_attempted', { - timestamp: new Date().toISOString(), - callId, - imageName: newImageName || t('callImages.default_name'), - }); - } catch (error) { - console.warn('Failed to track image upload attempt analytics:', error); - } + // Manipulate image to ensure PNG format and proper compression + const manipulatedImage = await ImageManipulator.manipulateAsync( + selectedImageInfo.uri, + [{ resize: { width: 1024 } }], // Resize to max width of 1024px while maintaining aspect ratio + { + compress: 0.8, + format: ImageManipulator.SaveFormat.PNG, // Ensure PNG format + } + ); - try { - const base64Image = await FileSystem.readAsStringAsync(selectedImage, { + // Read the manipulated image as base64 + const base64Image = await FileSystem.readAsStringAsync(manipulatedImage.uri, { encoding: FileSystem.EncodingType.Base64, }); + // Get current location if available + const currentLatitude = latitude; + const currentLongitude = longitude; + await uploadCallImage( callId, useAuthStore.getState().userId!, - '', - newImageName || t('callImages.default_name'), - null, //lat - null, //lon + newImageNote || '', // Use note for the note field + selectedImageInfo.filename, // Use filename for the name field + currentLatitude, // Current latitude + currentLongitude, // Current longitude base64Image ); - - // Track successful upload - try { - trackEvent('call_images_upload_completed', { - timestamp: new Date().toISOString(), - callId, - imageName: newImageName || t('callImages.default_name'), - success: true, - }); - } catch (error) { - console.warn('Failed to track image upload completion analytics:', error); - } - - setSelectedImage(null); - setNewImageName(''); + setSelectedImageInfo(null); + setNewImageNote(''); setIsAddingImage(false); + showToast('success', t('callImages.upload_success')); } catch (error) { console.error('Error uploading image:', error); - - // Track failed upload - try { - trackEvent('call_images_upload_completed', { - timestamp: new Date().toISOString(), - callId, - imageName: newImageName || t('callImages.default_name'), - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } catch (analyticsError) { - console.warn('Failed to track image upload failure analytics:', analyticsError); - } + showToast('error', t('callImages.upload_error')); } finally { setIsUploading(false); } }; - const handleImageError = (itemId: string, errorInfo?: any) => { - console.log(`Image loading failed for ${itemId}:`, errorInfo); + const handleImageError = (itemId: string, error: any) => { + console.error(`Image failed to load for ${itemId}:`, error); setImageErrors((prev) => new Set([...prev, itemId])); - - // Track image loading errors - try { - trackEvent('call_images_load_error', { - timestamp: new Date().toISOString(), - callId, - imageId: itemId, - error: errorInfo?.error?.message || 'Image failed to load', - }); - } catch (error) { - console.warn('Failed to track image load error analytics:', error); - } }; - // Helper function to test if URL is accessible - const testImageUrl = async (url: string) => { - try { - const response = await fetch(url, { method: 'HEAD' }); - console.log(`URL ${url} accessibility test:`, { - status: response.status, - statusText: response.statusText, - headers: Object.fromEntries(response.headers.entries()), - }); - return response.ok; - } catch (error) { - console.log(`URL ${url} fetch test failed:`, error); - return false; - } - }; + // Reset active index when valid images change const renderImageItem = ({ item, index }: { item: CallFileResultData; index: number }) => { if (!item) return null; - // Use Data field if available (base64), otherwise fall back to Url - let imageSource: { uri: string } | null = null; const hasError = imageErrors.has(item.Id); + let imageSource: { uri: string } | null = null; if (item.Data && item.Data.trim() !== '') { // Use Data as base64 image const mimeType = item.Mime || 'image/png'; // Default to png if no mime type imageSource = { uri: `data:${mimeType};base64,${item.Data}` }; } else if (item.Url && item.Url.trim() !== '') { - // Fall back to URL - add logging to debug URL issues - console.log(`Loading image from URL: ${item.Url} for item ${item.Id}`); - imageSource = { uri: item.Url }; - - // Test URL accessibility (don't await, just for debugging) - testImageUrl(item.Url); + // Use URL directly since it's unauthenticated + const url = item.Url.trim(); + imageSource = { uri: url }; } + // Show error state if there's an error or no valid image source if (!imageSource || hasError) { return ( - + {t('callImages.failed_to_load')} @@ -264,31 +227,39 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call ); } + // At this point, imageSource is guaranteed to be non-null return ( - - { - console.log(`Full error details for ${item.Id}:`, error, 'URL:', item.Url); - handleImageError(item.Id, error); - }} - onLoad={() => { - console.log(`Image loaded successfully for ${item.Id}`); - // Remove from error set if it loads successfully - setImageErrors((prev) => { - const newSet = new Set(prev); - newSet.delete(item.Id); - return newSet; - }); - }} - onLoadStart={() => { - console.log(`Starting to load image for ${item.Id}:`, imageSource?.uri); + + { + setFullScreenImage({ source: imageSource, name: item.Name }); }} - /> + testID={`image-${item.Id}-touchable`} + activeOpacity={0.7} + style={{ width: '100%' }} + delayPressIn={0} + delayPressOut={0} + > + { + handleImageError(item.Id, 'expo-image load error'); + }} + onLoad={() => { + // Remove from error set if it loads successfully + setImageErrors((prev) => { + const newSet = new Set(prev); + newSet.delete(item.Id); + return newSet; + }); + }} + /> + {item.Name || ''} {item.Timestamp || ''} @@ -304,21 +275,6 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call const handlePrevious = () => { const newIndex = Math.max(0, activeIndex - 1); setActiveIndex(newIndex); - - // Track navigation analytics - try { - trackEvent('call_images_navigation', { - timestamp: new Date().toISOString(), - callId, - direction: 'previous', - fromIndex: activeIndex, - toIndex: newIndex, - totalImages: validImages.length, - }); - } catch (error) { - console.warn('Failed to track image navigation analytics:', error); - } - try { flatListRef.current?.scrollToIndex({ index: newIndex, @@ -332,21 +288,6 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call const handleNext = () => { const newIndex = Math.min(validImages.length - 1, activeIndex + 1); setActiveIndex(newIndex); - - // Track navigation analytics - try { - trackEvent('call_images_navigation', { - timestamp: new Date().toISOString(), - callId, - direction: 'next', - fromIndex: activeIndex, - toIndex: newIndex, - totalImages: validImages.length, - }); - } catch (error) { - console.warn('Failed to track image navigation analytics:', error); - } - try { flatListRef.current?.scrollToIndex({ index: newIndex, @@ -357,26 +298,6 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call } }; - // Handle modal close with analytics tracking - const handleClose = useCallback(() => { - // Only track close analytics if modal was actually opened - if (wasModalOpenRef.current) { - try { - trackEvent('call_images_modal_closed', { - timestamp: new Date().toISOString(), - callId, - wasManualClose: true, - currentImageIndex: activeIndex, - totalImages: validImages.length, - }); - } catch (error) { - console.warn('Failed to track call images modal close analytics:', error); - } - wasModalOpenRef.current = false; - } - onClose(); - }, [onClose, trackEvent, callId, activeIndex, validImages.length]); - const renderPagination = () => { if (!validImages || validImages.length <= 1) return null; @@ -405,33 +326,28 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call }; const renderAddImageContent = () => ( - - + + {/* Scrollable content area */} + {t('callImages.add_new')} { setIsAddingImage(false); - setSelectedImage(null); - setNewImageName(''); + setSelectedImageInfo(null); + setNewImageNote(''); }} > - {selectedImage ? ( - - - - - - + {selectedImageInfo ? ( + + ) : ( - + @@ -447,7 +363,21 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call )} - + + {/* Fixed bottom section for input and save button */} + {selectedImageInfo && ( + + + + + + + + + )} + ); const renderImageGallery = () => { @@ -460,13 +390,17 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call ref={flatListRef} data={validImages} renderItem={renderImageItem} - keyExtractor={(item) => item?.Id || `image-${Math.random()}`} + keyExtractor={(item, index) => item?.Id || `image-${index}-${item?.Name || 'unknown'}`} horizontal pagingEnabled showsHorizontalScrollIndicator={false} onViewableItemsChanged={handleViewableItemsChanged} - viewabilityConfig={{ itemVisiblePercentThreshold: 50 }} + viewabilityConfig={{ + itemVisiblePercentThreshold: 50, + minimumViewTime: 100, + }} snapToInterval={width} + snapToAlignment="start" decelerationRate="fast" className="w-full" contentContainerStyle={{ paddingHorizontal: 0 }} @@ -475,14 +409,16 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call offset: width * index, index, })} - initialNumToRender={1} - maxToRenderPerBatch={1} - windowSize={3} - removeClippedSubviews={true} + initialNumToRender={3} + maxToRenderPerBatch={3} + windowSize={5} + removeClippedSubviews={false} initialScrollIndex={0} - maintainVisibleContentPosition={{ - minIndexForVisible: 0, - autoscrollToTopThreshold: 10, + onScrollToIndexFailed={(info) => { + const wait = new Promise((resolve) => setTimeout(resolve, 500)); + wait.then(() => { + flatListRef.current?.scrollToIndex({ index: info.index, animated: true }); + }); }} ListEmptyComponent={() => ( @@ -517,27 +453,32 @@ const CallImagesModal: React.FC = ({ isOpen, onClose, call }; return ( - - - - - - - - - {t('callImages.title')} - {!isAddingImage && !isLoadingImages && ( - - )} - + <> + + + + + + + + + {t('callImages.title')} + {!isAddingImage && !isLoadingImages && ( + + )} + + + {renderContent()} + + + - {renderContent()} - - - + {/* Full Screen Image Modal */} + setFullScreenImage(null)} imageSource={fullScreenImage?.source || { uri: '' }} imageName={fullScreenImage?.name} /> + ); }; diff --git a/src/components/calls/full-screen-image-modal.tsx b/src/components/calls/full-screen-image-modal.tsx new file mode 100644 index 0000000..e001cb5 --- /dev/null +++ b/src/components/calls/full-screen-image-modal.tsx @@ -0,0 +1,173 @@ +import { XIcon } from 'lucide-react-native'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Dimensions, type ImageSourcePropType, StatusBar, TouchableOpacity } from 'react-native'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import Animated, { interpolate, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { Image } from '@/components/ui/image'; +import { Modal, ModalBackdrop, ModalContent } from '@/components/ui/modal'; + +interface FullScreenImageModalProps { + isOpen: boolean; + onClose: () => void; + imageSource: ImageSourcePropType; + imageName?: string; +} + +const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + +const FullScreenImageModal: React.FC = ({ isOpen, onClose, imageSource, imageName }) => { + const { t } = useTranslation(); + const insets = useSafeAreaInsets(); + + // Animation values + const scale = useSharedValue(1); + const translateX = useSharedValue(0); + const translateY = useSharedValue(0); + const savedScale = useSharedValue(1); + const savedTranslateX = useSharedValue(0); + const savedTranslateY = useSharedValue(0); + + // Reset animation values when modal opens + React.useEffect(() => { + if (isOpen) { + scale.value = 1; + translateX.value = 0; + translateY.value = 0; + savedScale.value = 1; + savedTranslateX.value = 0; + savedTranslateY.value = 0; + } + }, [isOpen, scale, translateX, translateY, savedScale, savedTranslateX, savedTranslateY]); + + const clampTranslation = (translation: number, dimension: number, scaleFactor: number) => { + 'worklet'; + const scaledDimension = dimension * scaleFactor; + const maxTranslation = Math.max(0, (scaledDimension - dimension) / 2); + return Math.max(-maxTranslation, Math.min(maxTranslation, translation)); + }; + + const pinchGesture = Gesture.Pinch() + .onUpdate((event) => { + const newScale = savedScale.value * event.scale; + scale.value = Math.max(1, Math.min(5, newScale)); // Limit scale between 1 and 5 + }) + .onEnd(() => { + savedScale.value = scale.value; + + // Clamp translations based on new scale + const clampedX = clampTranslation(translateX.value, screenWidth, scale.value); + const clampedY = clampTranslation(translateY.value, screenHeight, scale.value); + + translateX.value = withTiming(clampedX); + translateY.value = withTiming(clampedY); + savedTranslateX.value = clampedX; + savedTranslateY.value = clampedY; + }); + + const panGesture = Gesture.Pan() + .onUpdate((event) => { + const newTranslateX = savedTranslateX.value + event.translationX; + const newTranslateY = savedTranslateY.value + event.translationY; + + translateX.value = clampTranslation(newTranslateX, screenWidth, scale.value); + translateY.value = clampTranslation(newTranslateY, screenHeight, scale.value); + }) + .onEnd(() => { + savedTranslateX.value = translateX.value; + savedTranslateY.value = translateY.value; + }); + + const doubleTapGesture = Gesture.Tap() + .numberOfTaps(2) + .onEnd(() => { + 'worklet'; + if (scale.value > 1) { + // Reset to original size + scale.value = withTiming(1); + translateX.value = withTiming(0); + translateY.value = withTiming(0); + savedScale.value = 1; + savedTranslateX.value = 0; + savedTranslateY.value = 0; + } else { + // Zoom in to 2x + scale.value = withTiming(2); + savedScale.value = 2; + } + }); + + const composedGesture = Gesture.Simultaneous(Gesture.Simultaneous(pinchGesture, panGesture), doubleTapGesture); + + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [{ scale: scale.value }, { translateX: translateX.value }, { translateY: translateY.value }], + }; + }); + + const closeButtonOpacity = useAnimatedStyle(() => { + // Hide close button when zoomed in significantly + const opacity = interpolate(scale.value, [1, 2], [1, 0.3], 'clamp'); + return { + opacity, + }; + }); + + return ( + + + + + + ); +}; + +export default FullScreenImageModal; diff --git a/src/translations/ar.json b/src/translations/ar.json index a636dfa..b0727c1 100644 --- a/src/translations/ar.json +++ b/src/translations/ar.json @@ -131,13 +131,16 @@ "error": "خطأ في الحصول على الصور", "failed_to_load": "فشل في تحميل الصورة", "image_name": "اسم الصورة", + "image_note": "ملاحظة الصورة", "loading": "جاري التحميل...", "no_images": "لا توجد صور متاحة", "no_images_description": "أضف صورًا إلى مكالمتك للمساعدة في التوثيق والتواصل", "select_from_gallery": "اختر من المعرض", "take_photo": "التقط صورة", "title": "صور المكالمة", - "upload": "رفع" + "upload": "رفع", + "upload_error": "فشل في رفع الصورة. يرجى المحاولة مرة أخرى.", + "upload_success": "تم رفع الصورة بنجاح" }, "callNotes": { "addNote": "إضافة ملاحظة", diff --git a/src/translations/en.json b/src/translations/en.json index 8cb362a..523aa69 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -131,13 +131,16 @@ "error": "Error getting images", "failed_to_load": "Failed to load image", "image_name": "Image Name", + "image_note": "Image Note", "loading": "Loading...", "no_images": "No images available", "no_images_description": "Add images to your call to help with documentation and communication", "select_from_gallery": "Select from Gallery", "take_photo": "Take Photo", "title": "Call Images", - "upload": "Upload" + "upload": "Upload", + "upload_error": "Failed to upload image. Please try again.", + "upload_success": "Image uploaded successfully" }, "callNotes": { "addNote": "Add Note", diff --git a/src/translations/es.json b/src/translations/es.json index 4c932ed..6c34af9 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -131,13 +131,16 @@ "error": "Error al obtener imágenes", "failed_to_load": "Error al cargar la imagen", "image_name": "Nombre de la imagen", + "image_note": "Nota de imagen", "loading": "Cargando...", "no_images": "No hay imágenes disponibles", "no_images_description": "Añade imágenes a tu llamada para ayudar con la documentación y comunicación", "select_from_gallery": "Seleccionar de la galería", "take_photo": "Tomar foto", "title": "Imágenes de la llamada", - "upload": "Subir" + "upload": "Subir", + "upload_error": "Error al subir la imagen. Por favor, inténtalo de nuevo.", + "upload_success": "Imagen subida exitosamente" }, "callNotes": { "addNote": "Añadir nota", diff --git a/yarn.lock b/yarn.lock index 5098505..295e7d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7502,6 +7502,13 @@ expo-image-loader@~5.1.0: resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-5.1.0.tgz#f7d65f9b9a9714eaaf5d50a406cb34cb25262153" integrity sha512-sEBx3zDQIODWbB5JwzE7ZL5FJD+DK3LVLWBVJy6VzsqIA6nDEnSFnsnWyCfCTSvbGigMATs1lgkC2nz3Jpve1Q== +expo-image-manipulator@~13.1.7: + version "13.1.7" + resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-13.1.7.tgz#e891ce9b49d75962eafdf5b7d670116583379e76" + integrity sha512-DBy/Xdd0E/yFind14x36XmwfWuUxOHI/oH97/giKjjPaRc2dlyjQ3tuW3x699hX6gAs9Sixj5WEJ1qNf3c8sag== + dependencies: + expo-image-loader "~5.1.0" + expo-image-picker@~16.1.4: version "16.1.4" resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-16.1.4.tgz#d4ac2d1f64f6ec9347c3f64f8435b40e6e4dcc40"