feat: Attachments not sent immediately - add text/voice before sending

- File/photo selection stores as pending (not sent immediately)
- Preview bar shows pending attachment above input field
- User can add text message before sending (e.g. "Was siehst du?")
- Send button appears when attachment is pending (even without text)
- Placeholder changes to "Text zum Anhang (optional)..."
- X button to cancel pending attachment
- File + text sent together (file first, then chat message)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck 2026-04-11 10:05:50 +02:00
parent 8a6ee018ea
commit 5ad68b7dfc
1 changed files with 97 additions and 54 deletions

View File

@ -94,6 +94,7 @@ const ChatScreen: React.FC = () => {
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null); const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [searchVisible, setSearchVisible] = useState(false); const [searchVisible, setSearchVisible] = useState(false);
const [pendingAttachment, setPendingAttachment] = useState<{file: any, isPhoto: boolean} | null>(null);
const flatListRef = useRef<FlatList>(null); const flatListRef = useRef<FlatList>(null);
const messageIdCounter = useRef(0); const messageIdCounter = useRef(0);
@ -400,6 +401,13 @@ const ChatScreen: React.FC = () => {
const sendTextMessage = useCallback(async () => { const sendTextMessage = useCallback(async () => {
const text = inputText.trim(); const text = inputText.trim();
// Wenn pending Anhang vorhanden → Anhang + Text zusammen senden
if (pendingAttachment) {
sendPendingAttachment(text);
return;
}
if (!text) return; if (!text) return;
setInputText(''); setInputText('');
@ -419,7 +427,7 @@ const ChatScreen: React.FC = () => {
text, text,
...(location && { location }), ...(location && { location }),
}); });
}, [inputText, getCurrentLocation]); }, [inputText, getCurrentLocation, pendingAttachment, sendPendingAttachment]);
// Sprachaufnahme abgeschlossen // Sprachaufnahme abgeschlossen
const handleVoiceRecording = useCallback(async (result: RecordingResult) => { const handleVoiceRecording = useCallback(async (result: RecordingResult) => {
@ -441,87 +449,79 @@ const ChatScreen: React.FC = () => {
}); });
}, [getCurrentLocation]); }, [getCurrentLocation]);
// Datei senden // Datei auswaehlen → als pending speichern (nicht sofort senden)
const handleFileSelected = useCallback(async (file: FileData) => { const handleFileSelected = useCallback(async (file: FileData) => {
setShowFileUpload(false); setShowFileUpload(false);
const location = await getCurrentLocation(); setPendingAttachment({ file, isPhoto: false });
setInputText(''); // Focus auf Textfeld
}, []);
const isImage = file.type.startsWith('image/'); // Foto auswaehlen → als pending speichern (nicht sofort senden)
const handlePhotoSelected = useCallback(async (photo: PhotoData) => {
setShowCameraUpload(false);
setPendingAttachment({ file: photo, isPhoto: true });
setInputText(''); // Focus auf Textfeld
}, []);
// Pending Anhang + Text/Sprache senden
const sendPendingAttachment = useCallback(async (messageText: string) => {
if (!pendingAttachment) return;
const { file, isPhoto } = pendingAttachment;
const location = await getCurrentLocation();
const msgId = nextId(); const msgId = nextId();
let imageUri = isImage && file.base64 ? `data:${file.type};base64,${file.base64}` : file.uri;
// Chat-Nachricht erstellen
const isImage = isPhoto || (file.type && file.type.startsWith('image/'));
const name = isPhoto ? file.fileName : file.name;
const base64 = file.base64 || '';
const mimeType = file.type || '';
const imageUri = isImage && base64 ? `data:${mimeType};base64,${base64}` : file.uri;
const userMsg: ChatMessage = { const userMsg: ChatMessage = {
id: msgId, id: msgId,
sender: 'user', sender: 'user',
text: 'Anhang empfangen', text: messageText || 'Anhang',
timestamp: Date.now(), timestamp: Date.now(),
attachments: [{ attachments: [{
type: isImage ? 'image' : 'file', type: isImage ? 'image' : 'file',
name: file.name, name,
size: file.size, size: file.size,
uri: imageUri, uri: imageUri,
mimeType: file.type, mimeType,
}], }],
}; };
setMessages(prev => [...prev, userMsg]); setMessages(prev => [...prev, userMsg]);
// Anhang auf Disk speichern fuer Persistenz // Auf Disk speichern
if (file.base64) { if (base64) {
persistAttachment(file.base64, msgId, file.name).then(filePath => { persistAttachment(base64, msgId, name).then(filePath => {
setMessages(prev => prev.map(m => setMessages(prev => prev.map(m =>
m.id === msgId ? { ...m, attachments: m.attachments?.map(a => ({ ...a, uri: filePath })) } : m m.id === msgId ? { ...m, attachments: m.attachments?.map(a => ({ ...a, uri: filePath })) } : m
)); ));
}).catch(() => {}); }).catch(() => {});
} }
// Datei an RVS senden
rvs.send('file', { rvs.send('file', {
name: file.name, name,
type: file.type, type: mimeType,
size: file.size, size: file.size,
base64: file.base64, base64,
...(isPhoto && file.width && { width: file.width, height: file.height }),
...(location && { location }), ...(location && { location }),
}); });
}, [getCurrentLocation]);
// Foto senden // Wenn Text dabei ist, als separate Chat-Nachricht senden (damit ARIA weiss was zu tun ist)
const handlePhotoSelected = useCallback(async (photo: PhotoData) => { if (messageText) {
setShowCameraUpload(false); rvs.send('chat', {
const location = await getCurrentLocation(); text: messageText,
...(location && { location }),
const msgId = nextId(); });
const dataUri = photo.base64 ? `data:${photo.type};base64,${photo.base64}` : undefined;
const userMsg: ChatMessage = {
id: msgId,
sender: 'user',
text: 'Anhang empfangen',
timestamp: Date.now(),
attachments: [{
type: 'image',
name: photo.fileName,
uri: dataUri,
mimeType: photo.type,
}],
};
setMessages(prev => [...prev, userMsg]);
// Foto auf Disk speichern fuer Persistenz
if (photo.base64) {
persistAttachment(photo.base64, msgId, photo.fileName).then(filePath => {
setMessages(prev => prev.map(m =>
m.id === msgId ? { ...m, attachments: m.attachments?.map(a => ({ ...a, uri: filePath })) } : m
));
}).catch(() => {});
} }
rvs.send('file', { setPendingAttachment(null);
name: photo.fileName, setInputText('');
type: photo.type, }, [pendingAttachment, getCurrentLocation]);
base64: photo.base64,
width: photo.width,
height: photo.height,
...(location && { location }),
});
}, [getCurrentLocation]); }, [getCurrentLocation]);
// --- Rendering --- // --- Rendering ---
@ -670,6 +670,28 @@ const ChatScreen: React.FC = () => {
} }
/> />
{/* Pending Anhang Vorschau */}
{pendingAttachment && (
<View style={styles.pendingBar}>
{pendingAttachment.file.type?.startsWith('image/') || pendingAttachment.isPhoto ? (
<Image
source={{ uri: pendingAttachment.file.base64
? `data:${pendingAttachment.file.type};base64,${pendingAttachment.file.base64}`
: pendingAttachment.file.uri }}
style={styles.pendingThumb}
/>
) : (
<Text style={{fontSize: 24, marginRight: 8}}>{'\uD83D\uDCC4'}</Text>
)}
<Text style={styles.pendingName} numberOfLines={1}>
{pendingAttachment.isPhoto ? pendingAttachment.file.fileName : pendingAttachment.file.name}
</Text>
<TouchableOpacity onPress={() => setPendingAttachment(null)}>
<Text style={{color: '#FF3B30', fontSize: 18, paddingHorizontal: 8}}>X</Text>
</TouchableOpacity>
</View>
)}
{/* Eingabebereich */} {/* Eingabebereich */}
<View style={styles.inputContainer}> <View style={styles.inputContainer}>
{/* Datei-Buttons */} {/* Datei-Buttons */}
@ -692,7 +714,7 @@ const ChatScreen: React.FC = () => {
style={styles.textInput} style={styles.textInput}
value={inputText} value={inputText}
onChangeText={setInputText} onChangeText={setInputText}
placeholder="Nachricht an ARIA..." placeholder={pendingAttachment ? "Text zum Anhang (optional)..." : "Nachricht an ARIA..."}
placeholderTextColor="#555570" placeholderTextColor="#555570"
multiline multiline
maxLength={4000} maxLength={4000}
@ -701,7 +723,7 @@ const ChatScreen: React.FC = () => {
/> />
{/* Senden oder Sprache */} {/* Senden oder Sprache */}
{inputText.trim() ? ( {inputText.trim() || pendingAttachment ? (
<TouchableOpacity style={styles.sendButton} onPress={sendTextMessage}> <TouchableOpacity style={styles.sendButton} onPress={sendTextMessage}>
<Text style={styles.sendIcon}>{'\u2B06\uFE0F'}</Text> <Text style={styles.sendIcon}>{'\u2B06\uFE0F'}</Text>
</TouchableOpacity> </TouchableOpacity>
@ -932,6 +954,27 @@ const styles = StyleSheet.create({
wakeWordIcon: { wakeWordIcon: {
fontSize: 16, fontSize: 16,
}, },
pendingBar: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#1E1E2E',
paddingHorizontal: 12,
paddingVertical: 8,
borderTopWidth: 1,
borderTopColor: '#2A2A3E',
},
pendingThumb: {
width: 40,
height: 40,
borderRadius: 6,
marginRight: 8,
backgroundColor: '#0D0D1A',
},
pendingName: {
flex: 1,
color: '#E0E0F0',
fontSize: 13,
},
searchBar: { searchBar: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',