A React Native app for the ultimate thinking partner.
at main 229 lines 6.3 kB view raw
1import React, { useState, useCallback, useRef } from 'react'; 2import { 3 View, 4 TextInput, 5 TouchableOpacity, 6 StyleSheet, 7 Platform, 8 Image, 9 Alert, 10 ScrollView, 11} from 'react-native'; 12import { Ionicons } from '@expo/vector-icons'; 13import * as ImagePicker from 'expo-image-picker'; 14 15interface MessageInputV2Props { 16 onSend: (text: string) => void; 17 disabled?: boolean; 18 theme: any; 19 selectedImages: Array<{ uri: string; base64: string; mediaType: string }>; 20 onAddImage: (image: { uri: string; base64: string; mediaType: string }) => void; 21 onRemoveImage: (index: number) => void; 22} 23 24export const MessageInputV2: React.FC<MessageInputV2Props> = ({ 25 onSend, 26 disabled = false, 27 theme, 28 selectedImages, 29 onAddImage, 30 onRemoveImage, 31}) => { 32 const [inputText, setInputText] = useState(''); 33 const inputRef = useRef<TextInput>(null); 34 35 const handleSend = useCallback(() => { 36 if ((inputText.trim() || selectedImages.length > 0) && !disabled) { 37 onSend(inputText); 38 setInputText(''); 39 } 40 }, [inputText, selectedImages, disabled, onSend]); 41 42 const pickImage = async () => { 43 try { 44 const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); 45 if (status !== 'granted') { 46 Alert.alert('Permission Required', 'Please allow access to your photo library.'); 47 return; 48 } 49 50 const result = await ImagePicker.launchImageLibraryAsync({ 51 mediaTypes: ['images'], 52 allowsMultipleSelection: false, 53 quality: 0.8, 54 base64: true, 55 }); 56 57 if (!result.canceled && result.assets && result.assets.length > 0) { 58 const asset = result.assets[0]; 59 if (asset.base64) { 60 const MAX_SIZE = 5 * 1024 * 1024; 61 if (asset.base64.length > MAX_SIZE) { 62 const sizeMB = (asset.base64.length / 1024 / 1024).toFixed(2); 63 Alert.alert( 64 'Image Too Large', 65 `This image is ${sizeMB}MB. Maximum allowed is 5MB.` 66 ); 67 return; 68 } 69 70 const mediaType = asset.uri.match(/\.(jpg|jpeg)$/i) ? 'image/jpeg' : 71 asset.uri.match(/\.png$/i) ? 'image/png' : 72 asset.uri.match(/\.gif$/i) ? 'image/gif' : 73 asset.uri.match(/\.webp$/i) ? 'image/webp' : 'image/jpeg'; 74 75 onAddImage({ 76 uri: asset.uri, 77 base64: asset.base64, 78 mediaType, 79 }); 80 } 81 } 82 } catch (error) { 83 console.error('Error picking image:', error); 84 Alert.alert('Error', 'Failed to pick image'); 85 } 86 }; 87 88 return ( 89 <View style={styles.container}> 90 {/* Selected Images Preview */} 91 {selectedImages.length > 0 && ( 92 <ScrollView 93 horizontal 94 style={styles.imagesPreview} 95 contentContainerStyle={styles.imagesPreviewContent} 96 > 97 {selectedImages.map((img, index) => ( 98 <View key={index} style={styles.imagePreviewContainer}> 99 <Image source={{ uri: img.uri }} style={styles.imagePreview} /> 100 <TouchableOpacity 101 style={styles.removeImageButton} 102 onPress={() => onRemoveImage(index)} 103 > 104 <Ionicons name="close-circle" size={24} color="#ff4444" /> 105 </TouchableOpacity> 106 </View> 107 ))} 108 </ScrollView> 109 )} 110 111 {/* Input Row */} 112 <View style={styles.inputRow}> 113 <TouchableOpacity 114 style={styles.attachButton} 115 onPress={pickImage} 116 disabled={disabled} 117 > 118 <Ionicons 119 name="image-outline" 120 size={24} 121 color={disabled ? theme.colors.text.tertiary : theme.colors.text.secondary} 122 /> 123 </TouchableOpacity> 124 125 <TextInput 126 ref={inputRef} 127 style={[ 128 styles.textInput, 129 { 130 color: theme.colors.text.primary, 131 backgroundColor: theme.colors.background.tertiary, 132 }, 133 ]} 134 placeholder="What's on your mind?" 135 placeholderTextColor={theme.colors.text.tertiary} 136 value={inputText} 137 onChangeText={setInputText} 138 multiline 139 maxLength={4000} 140 editable={!disabled} 141 onSubmitEditing={handleSend} 142 /> 143 144 <TouchableOpacity 145 style={[ 146 styles.sendButton, 147 (inputText.trim() || selectedImages.length > 0) && !disabled 148 ? { opacity: 1, backgroundColor: theme.colors.interactive.primary } 149 : { opacity: 0.4, backgroundColor: 'rgba(239, 160, 78, 0.1)' }, 150 ]} 151 onPress={handleSend} 152 disabled={disabled || (!inputText.trim() && selectedImages.length === 0)} 153 > 154 <Ionicons 155 name="send" 156 size={18} 157 color={(inputText.trim() || selectedImages.length > 0) && !disabled ? '#ffffff' : theme.colors.text.tertiary} 158 /> 159 </TouchableOpacity> 160 </View> 161 </View> 162 ); 163}; 164 165const styles = StyleSheet.create({ 166 container: { 167 width: '100%', 168 }, 169 imagesPreview: { 170 marginBottom: 8, 171 }, 172 imagesPreviewContent: { 173 paddingVertical: 4, 174 }, 175 imagePreviewContainer: { 176 marginRight: 8, 177 position: 'relative', 178 }, 179 imagePreview: { 180 width: 80, 181 height: 80, 182 borderRadius: 8, 183 }, 184 removeImageButton: { 185 position: 'absolute', 186 top: -8, 187 right: -8, 188 backgroundColor: 'rgba(0, 0, 0, 0.6)', 189 borderRadius: 12, 190 }, 191 inputRow: { 192 flexDirection: 'row', 193 alignItems: 'flex-end', 194 gap: 8, 195 }, 196 attachButton: { 197 width: 40, 198 height: 40, 199 borderRadius: 20, 200 justifyContent: 'center', 201 alignItems: 'center', 202 }, 203 textInput: { 204 flex: 1, 205 minHeight: 44, 206 maxHeight: 120, 207 paddingHorizontal: 18, 208 paddingVertical: 12, 209 borderRadius: 24, 210 fontSize: 15, 211 lineHeight: 20, 212 borderWidth: 0, 213 fontFamily: 'Lexend_400Regular', 214 ...(Platform.OS === 'web' && { 215 // @ts-ignore - outlineStyle is web-only 216 outlineStyle: 'none', 217 }), 218 }, 219 sendButton: { 220 width: 40, 221 height: 40, 222 borderRadius: 20, 223 justifyContent: 'center', 224 alignItems: 'center', 225 backgroundColor: 'rgba(239, 160, 78, 0.1)', 226 }, 227}); 228 229export default MessageInputV2;