A React Native app for the ultimate thinking partner.
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;