@@ -2,11 +2,12 @@ import * as FileSystem from 'expo-file-system';
22import { Image } from 'expo-image' ;
33import * as ImageManipulator from 'expo-image-manipulator' ;
44import * as ImagePicker from 'expo-image-picker' ;
5- import { CameraIcon , ChevronLeftIcon , ChevronRightIcon , ImageIcon , PlusIcon , XIcon } from 'lucide-react-native' ;
6- import React , { useCallback , useEffect , useMemo , useRef , useState , memo } from 'react' ;
5+ import { CameraIcon , ChevronLeftIcon , ChevronRightIcon , ImageIcon , PlusIcon , X } from 'lucide-react-native' ;
6+ import { useColorScheme } from 'nativewind' ;
7+ import React , { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
78import { useTranslation } from 'react-i18next' ;
8- import { Alert , Dimensions , Platform , StyleSheet , TouchableOpacity , View } from 'react-native' ;
9- import { KeyboardAwareScrollView } from 'react-native-keyboard-controller' ;
9+ import { Dimensions , Keyboard , Modal , SafeAreaView , StyleSheet , TouchableOpacity , View } from 'react-native' ;
10+ import { KeyboardStickyView } from 'react-native-keyboard-controller' ;
1011
1112import { Loading } from '@/components/common/loading' ;
1213import ZeroState from '@/components/common/zero-state' ;
@@ -17,9 +18,9 @@ import { type CallFileResultData } from '@/models/v4/callFiles/callFileResultDat
1718import { useLocationStore } from '@/stores/app/location-store' ;
1819import { useCallDetailStore } from '@/stores/calls/detail-store' ;
1920
20- import { Actionsheet , ActionsheetBackdrop , ActionsheetContent , ActionsheetDragIndicator , ActionsheetDragIndicatorWrapper , ActionsheetItem , ActionsheetItemText } from '../ui/actionsheet' ;
2121import { Box } from '../ui/box' ;
2222import { Button , ButtonIcon , ButtonText } from '../ui/button' ;
23+ import { Heading } from '../ui/heading' ;
2324import { HStack } from '../ui/hstack' ;
2425import { Input , InputField } from '../ui/input' ;
2526import { Text } from '../ui/text' ;
@@ -34,24 +35,14 @@ interface CallImagesModalProps {
3435
3536const { width } = Dimensions . get ( 'window' ) ;
3637
37- const styles = StyleSheet . create ( {
38- galleryImage : {
39- height : 256 , // h-64 equivalent
40- width : '100%' ,
41- borderRadius : 8 , // rounded-lg equivalent
42- } ,
43- previewImage : {
44- height : 256 , // h-64 equivalent
45- width : '100%' ,
46- borderRadius : 8 , // rounded-lg equivalent
47- } ,
48- } ) ;
49-
5038const CallImagesModal : React . FC < CallImagesModalProps > = ( { isOpen, onClose, callId } ) => {
5139 const { t } = useTranslation ( ) ;
5240 const { trackEvent } = useAnalytics ( ) ;
41+ const { colorScheme } = useColorScheme ( ) ;
5342 const { latitude, longitude } = useLocationStore ( ) ;
5443
44+ const isDark = colorScheme === 'dark' ;
45+
5546 const [ activeIndex , setActiveIndex ] = useState ( 0 ) ;
5647 const [ isUploading , setIsUploading ] = useState ( false ) ;
5748 const [ newImageNote , setNewImageNote ] = useState ( '' ) ;
@@ -81,17 +72,18 @@ const CallImagesModal: React.FC<CallImagesModalProps> = ({ isOpen, onClose, call
8172 fetchCallImages ( callId ) ;
8273 setActiveIndex ( 0 ) ; // Reset active index when opening
8374 setImageErrors ( new Set ( ) ) ; // Reset image errors
75+ } else {
76+ // Cleanup when modal closes to free memory
77+ setFullScreenImage ( null ) ;
78+ setSelectedImageInfo ( null ) ;
79+ setImageErrors ( new Set ( ) ) ;
80+ // Clear images from store to free memory
81+ clearImages ( ) ;
8482 }
8583
86- // Cleanup when modal closes to free memory
84+ // Unmount cleanup
8785 return ( ) => {
88- if ( ! isOpen ) {
89- setFullScreenImage ( null ) ;
90- setSelectedImageInfo ( null ) ;
91- setImageErrors ( new Set ( ) ) ;
92- // Clear images from store to free memory
93- clearImages ( ) ;
94- }
86+ clearImages ( ) ;
9587 } ;
9688 } , [ isOpen , callId , fetchCallImages , clearImages ] ) ;
9789
@@ -186,13 +178,29 @@ const CallImagesModal: React.FC<CallImagesModalProps> = ({ isOpen, onClose, call
186178 setSelectedImageInfo ( null ) ;
187179 setNewImageNote ( '' ) ;
188180 setIsAddingImage ( false ) ;
181+ Keyboard . dismiss ( ) ;
189182 } catch ( error ) {
190183 console . error ( 'Error uploading image:' , error ) ;
191184 } finally {
192185 setIsUploading ( false ) ;
193186 }
194187 } ;
195188
189+ const handleCancelAdd = useCallback ( ( ) => {
190+ setIsAddingImage ( false ) ;
191+ setSelectedImageInfo ( null ) ;
192+ setNewImageNote ( '' ) ;
193+ Keyboard . dismiss ( ) ;
194+ } , [ ] ) ;
195+
196+ const handleClose = useCallback ( ( ) => {
197+ setNewImageNote ( '' ) ;
198+ setSelectedImageInfo ( null ) ;
199+ setIsAddingImage ( false ) ;
200+ Keyboard . dismiss ( ) ;
201+ onClose ( ) ;
202+ } , [ onClose ] ) ;
203+
196204 const handleImageError = ( itemId : string , error : any ) => {
197205 console . error ( `Image failed to load for ${ itemId } :` , error ) ;
198206 setImageErrors ( ( prev ) => new Set ( [ ...prev , itemId ] ) ) ;
@@ -336,58 +344,60 @@ const CallImagesModal: React.FC<CallImagesModalProps> = ({ isOpen, onClose, call
336344 } ;
337345
338346 const renderAddImageContent = ( ) => (
339- < VStack className = "flex-1" >
347+ < >
340348 { /* Scrollable content area */ }
341- < VStack className = "flex-1 space-y-4 p-4" >
342- < HStack className = "items-center justify-between" >
343- < Text className = "text-lg font-bold" > { t ( 'callImages.add_new' ) } </ Text >
344- < TouchableOpacity
345- onPress = { ( ) => {
346- setIsAddingImage ( false ) ;
347- setSelectedImageInfo ( null ) ;
348- setNewImageNote ( '' ) ;
349- } }
350- >
351- < XIcon size = { 24 } />
352- </ TouchableOpacity >
353- </ HStack >
354-
349+ < View style = { styles . addImageContainer } >
355350 { selectedImageInfo ? (
356- < Box className = "flex-1 items-center justify-center" >
351+ < Box className = "items-center justify-center p-4 " >
357352 < Image source = { { uri : selectedImageInfo . uri } } style = { styles . previewImage } contentFit = "contain" transition = { 200 } cachePolicy = "memory-disk" />
358353 </ Box >
359354 ) : (
360- < VStack className = "flex-1 justify-center space-y-4" >
361- < ActionsheetItem onPress = { handleImageSelect } >
362- < HStack className = "items-center space-x-2 " >
363- < PlusIcon size = { 20 } />
364- < ActionsheetItemText > { t ( 'callImages.select_from_gallery' ) } </ ActionsheetItemText >
355+ < VStack className = "space-y-4 p -4" >
356+ < TouchableOpacity onPress = { handleImageSelect } style = { [ styles . imageOptionButton , isDark && styles . imageOptionButtonDark ] } >
357+ < HStack className = "items-center space-x-3 " >
358+ < PlusIcon size = { 24 } color = { isDark ? '#D1D5DB' : '#374151' } />
359+ < Text className = "text-base font-medium" > { t ( 'callImages.select_from_gallery' ) } </ Text >
365360 </ HStack >
366- </ ActionsheetItem >
367- < ActionsheetItem onPress = { handleCameraCapture } >
368- < HStack className = "items-center space-x-2 " >
369- < CameraIcon size = { 20 } />
370- < ActionsheetItemText > { t ( 'callImages.take_photo' ) } </ ActionsheetItemText >
361+ </ TouchableOpacity >
362+ < TouchableOpacity onPress = { handleCameraCapture } style = { [ styles . imageOptionButton , isDark && styles . imageOptionButtonDark ] } >
363+ < HStack className = "items-center space-x-3 " >
364+ < CameraIcon size = { 24 } color = { isDark ? '#D1D5DB' : '#374151' } />
365+ < Text className = "text-base font-medium" > { t ( 'callImages.take_photo' ) } </ Text >
371366 </ HStack >
372- </ ActionsheetItem >
367+ </ TouchableOpacity >
373368 </ VStack >
374369 ) }
375- </ VStack >
376-
377- { /* Fixed bottom section for input and save button */ }
378- { selectedImageInfo && (
379- < KeyboardAwareScrollView keyboardShouldPersistTaps = { Platform . OS === 'android' ? 'handled' : 'always' } showsVerticalScrollIndicator = { false } style = { { flexGrow : 0 } } >
380- < VStack className = "max-h-30 space-y-2 border-t border-gray-200 bg-white px-4 py-2 dark:border-gray-700 dark:bg-gray-800" >
381- < Input className = "w-full" size = "sm" >
382- < InputField placeholder = { t ( 'callImages.image_note' ) } value = { newImageNote } onChangeText = { setNewImageNote } testID = "image-note-input" />
383- </ Input >
384- < Button className = "mt-2 w-full" size = "sm" onPress = { handleUploadImage } isDisabled = { isUploading } testID = "upload-button" >
385- < ButtonText > { isUploading ? t ( 'common.uploading' ) : t ( 'callImages.upload' ) } </ ButtonText >
386- </ Button >
387- </ VStack >
388- </ KeyboardAwareScrollView >
370+ </ View >
371+
372+ { /* Fixed bottom section for input and save button - Sticks to keyboard */ }
373+ { selectedImageInfo ? (
374+ < KeyboardStickyView offset = { { opened : 0 , closed : 0 } } >
375+ < View style = { [ styles . footer , isDark && styles . footerDark ] } >
376+ < VStack space = "sm" className = "w-full" >
377+ < Text className = "font-medium" > { t ( 'callImages.image_note' ) } </ Text >
378+ < Input className = "w-full" >
379+ < InputField placeholder = { t ( 'callImages.image_note' ) } value = { newImageNote } onChangeText = { setNewImageNote } testID = "image-note-input" />
380+ </ Input >
381+ </ VStack >
382+
383+ < HStack space = "sm" className = "mt-3 w-full justify-between" >
384+ < Button variant = "outline" onPress = { handleCancelAdd } testID = "cancel-add-button" className = "flex-1" >
385+ < ButtonText > { t ( 'common.cancel' ) } </ ButtonText >
386+ </ Button >
387+ < Button onPress = { handleUploadImage } className = "flex-1 bg-blue-600 dark:bg-blue-500" isDisabled = { isUploading } testID = "upload-button" >
388+ < ButtonText > { isUploading ? t ( 'common.uploading' ) : t ( 'callImages.upload' ) } </ ButtonText >
389+ </ Button >
390+ </ HStack >
391+ </ View >
392+ </ KeyboardStickyView >
393+ ) : (
394+ < View style = { [ styles . footer , isDark && styles . footerDark ] } >
395+ < Button variant = "outline" onPress = { handleCancelAdd } testID = "cancel-add-button" className = "w-full" >
396+ < ButtonText > { t ( 'common.cancel' ) } </ ButtonText >
397+ </ Button >
398+ </ View >
389399 ) }
390- </ VStack >
400+ </ >
391401 ) ;
392402
393403 const renderImageGallery = ( ) => {
@@ -445,34 +455,99 @@ const CallImagesModal: React.FC<CallImagesModalProps> = ({ isOpen, onClose, call
445455 return renderImageGallery ( ) ;
446456 } ;
447457
458+ if ( ! isOpen ) {
459+ return null ;
460+ }
461+
448462 return (
449463 < >
450- < Actionsheet isOpen = { isOpen } onClose = { onClose } snapPoints = { [ 67 ] } >
451- < ActionsheetBackdrop />
452- < ActionsheetContent className = "rounded-t-3x bg-white dark:bg-gray-800" >
453- < ActionsheetDragIndicatorWrapper >
454- < ActionsheetDragIndicator />
455- </ ActionsheetDragIndicatorWrapper >
456- < Box className = "w-full p-4" >
457- < HStack className = "mb-4 items-center justify-between" >
458- < Text className = "text-xl font-bold" > { t ( 'callImages.title' ) } </ Text >
464+ < Modal visible = { isOpen } animationType = "slide" presentationStyle = "pageSheet" onRequestClose = { handleClose } >
465+ < SafeAreaView style = { [ styles . container , isDark && styles . containerDark ] } >
466+ { /* Header */ }
467+ < View style = { [ styles . header , isDark && styles . headerDark ] } >
468+ < Heading size = "lg" > { isAddingImage ? t ( 'callImages.add_new' ) : t ( 'callImages.title' ) } </ Heading >
469+ < HStack className = "items-center space-x-2" >
459470 { ! isAddingImage && ! isLoadingImages && (
460471 < Button size = "sm" variant = "outline" onPress = { ( ) => setIsAddingImage ( true ) } >
461472 < ButtonIcon as = { PlusIcon } />
462473 < ButtonText > { t ( 'callImages.add' ) } </ ButtonText >
463474 </ Button >
464475 ) }
476+ < TouchableOpacity onPress = { handleClose } style = { styles . closeButton } testID = "close-button" >
477+ < X size = { 24 } color = { isDark ? '#D1D5DB' : '#374151' } />
478+ </ TouchableOpacity >
465479 </ HStack >
480+ </ View >
466481
467- < View className = "min-h-[300px]" > { renderContent ( ) } </ View >
468- </ Box >
469- </ ActionsheetContent >
470- </ Actionsheet >
482+ { /* Content */ }
483+ < View style = { styles . contentContainer } > { renderContent ( ) } </ View >
484+ </ SafeAreaView >
485+ </ Modal >
471486
472487 { /* Full Screen Image Modal */ }
473488 < FullScreenImageModal isOpen = { ! ! fullScreenImage } onClose = { ( ) => setFullScreenImage ( null ) } imageSource = { fullScreenImage ?. source || { uri : '' } } imageName = { fullScreenImage ?. name } />
474489 </ >
475490 ) ;
476491} ;
477492
493+ const styles = StyleSheet . create ( {
494+ container : {
495+ flex : 1 ,
496+ backgroundColor : 'white' ,
497+ } ,
498+ containerDark : {
499+ backgroundColor : '#1F2937' ,
500+ } ,
501+ header : {
502+ flexDirection : 'row' ,
503+ alignItems : 'center' ,
504+ justifyContent : 'space-between' ,
505+ paddingHorizontal : 16 ,
506+ paddingVertical : 12 ,
507+ borderBottomWidth : 1 ,
508+ borderBottomColor : '#E5E7EB' ,
509+ } ,
510+ headerDark : {
511+ borderBottomColor : '#374151' ,
512+ } ,
513+ closeButton : {
514+ padding : 8 ,
515+ } ,
516+ contentContainer : {
517+ flex : 1 ,
518+ } ,
519+ addImageContainer : {
520+ flex : 1 ,
521+ } ,
522+ imageOptionButton : {
523+ padding : 16 ,
524+ borderRadius : 8 ,
525+ backgroundColor : '#F3F4F6' ,
526+ } ,
527+ imageOptionButtonDark : {
528+ backgroundColor : '#374151' ,
529+ } ,
530+ footer : {
531+ paddingHorizontal : 16 ,
532+ paddingVertical : 12 ,
533+ borderTopWidth : 1 ,
534+ borderTopColor : '#E5E7EB' ,
535+ backgroundColor : 'white' ,
536+ } ,
537+ footerDark : {
538+ borderTopColor : '#374151' ,
539+ backgroundColor : '#1F2937' ,
540+ } ,
541+ galleryImage : {
542+ height : 256 ,
543+ width : '100%' ,
544+ borderRadius : 8 ,
545+ } ,
546+ previewImage : {
547+ height : 256 ,
548+ width : '100%' ,
549+ borderRadius : 8 ,
550+ } ,
551+ } ) ;
552+
478553export default CallImagesModal ;
0 commit comments