Skip to content

Commit ed433bc

Browse files
committed
RU-T46 PR#193 Fixes
1 parent 3033632 commit ed433bc

File tree

7 files changed

+306
-200
lines changed

7 files changed

+306
-200
lines changed

src/components/calls/call-files-modal.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as Sharing from 'expo-sharing';
55
import { Download, File, X } from 'lucide-react-native';
66
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
77
import { useTranslation } from 'react-i18next';
8-
import { Alert, Pressable } from 'react-native';
8+
import { Alert, Pressable, useColorScheme } from 'react-native';
99
import { ScrollView } from 'react-native-gesture-handler';
1010

1111
import { getCallAttachmentFile } from '@/api/calls/callFiles';
@@ -31,6 +31,7 @@ interface CallFilesModalProps {
3131
export const CallFilesModal: React.FC<CallFilesModalProps> = ({ isOpen, onClose, callId }) => {
3232
const { t } = useTranslation();
3333
const { trackEvent } = useAnalytics();
34+
const colorScheme = useColorScheme();
3435
const { callFiles, isLoadingFiles, errorFiles, fetchCallFiles, clearFiles } = useCallDetailStore();
3536
const [downloadingFiles, setDownloadingFiles] = useState<Record<string, number>>({});
3637

@@ -49,11 +50,9 @@ export const CallFilesModal: React.FC<CallFilesModalProps> = ({ isOpen, onClose,
4950

5051
// Cleanup when modal closes to free memory
5152
return () => {
52-
if (!isOpen) {
53-
setDownloadingFiles({});
54-
// Clear files from store to free memory
55-
clearFiles();
56-
}
53+
setDownloadingFiles({});
54+
// Clear files from store to free memory
55+
clearFiles();
5756
};
5857
}, [isOpen, callId, fetchCallFiles, clearFiles]);
5958

@@ -250,8 +249,8 @@ export const CallFilesModal: React.FC<CallFilesModalProps> = ({ isOpen, onClose,
250249
onChange={handleSheetChanges}
251250
backdropComponent={renderBackdrop}
252251
enablePanDownToClose={true}
253-
handleIndicatorStyle={{ backgroundColor: '#D1D5DB' }}
254-
backgroundStyle={{ backgroundColor: 'white' }}
252+
handleIndicatorStyle={{ backgroundColor: colorScheme === 'dark' ? '#4B5563' : '#D1D5DB' }}
253+
backgroundStyle={{ backgroundColor: colorScheme === 'dark' ? '#1F2937' : 'white' }}
255254
>
256255
<BottomSheetView style={{ flex: 1 }} testID="call-files-modal">
257256
{/* Fixed Header */}

src/components/calls/call-images-modal.tsx

Lines changed: 156 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import * as FileSystem from 'expo-file-system';
22
import { Image } from 'expo-image';
33
import * as ImageManipulator from 'expo-image-manipulator';
44
import * 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';
78
import { 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

1112
import { Loading } from '@/components/common/loading';
1213
import ZeroState from '@/components/common/zero-state';
@@ -17,9 +18,9 @@ import { type CallFileResultData } from '@/models/v4/callFiles/callFileResultDat
1718
import { useLocationStore } from '@/stores/app/location-store';
1819
import { useCallDetailStore } from '@/stores/calls/detail-store';
1920

20-
import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetDragIndicator, ActionsheetDragIndicatorWrapper, ActionsheetItem, ActionsheetItemText } from '../ui/actionsheet';
2121
import { Box } from '../ui/box';
2222
import { Button, ButtonIcon, ButtonText } from '../ui/button';
23+
import { Heading } from '../ui/heading';
2324
import { HStack } from '../ui/hstack';
2425
import { Input, InputField } from '../ui/input';
2526
import { Text } from '../ui/text';
@@ -34,24 +35,14 @@ interface CallImagesModalProps {
3435

3536
const { 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-
5038
const 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+
478553
export default CallImagesModal;

0 commit comments

Comments
 (0)