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:
parent
8a6ee018ea
commit
5ad68b7dfc
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue