fixed, long chats not loading to end, saved attachments in local folder on android., if file missing redownload over shared folde via rvs server, andord app added settingss for local storage path, updated readme
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
Modal,
|
||||
} from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import RNFS from 'react-native-fs';
|
||||
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
||||
import audioService from '../services/audio';
|
||||
import wakeWordService from '../services/wakeword';
|
||||
@@ -34,8 +35,9 @@ interface Attachment {
|
||||
type: 'image' | 'file' | 'audio';
|
||||
name: string;
|
||||
size?: number;
|
||||
uri?: string; // Lokaler Pfad oder data URI fuer Anzeige
|
||||
uri?: string; // Lokaler Pfad (file://) fuer Anzeige
|
||||
mimeType?: string;
|
||||
serverPath?: string; // Pfad auf dem Server (/shared/uploads/...) fuer Re-Download
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
@@ -50,6 +52,33 @@ interface ChatMessage {
|
||||
|
||||
const CHAT_STORAGE_KEY = 'aria_chat_messages';
|
||||
const MAX_STORED_MESSAGES = 500;
|
||||
const DEFAULT_ATTACHMENT_DIR = `${RNFS.DocumentDirectoryPath}/chat_attachments`;
|
||||
const STORAGE_PATH_KEY = 'aria_attachment_storage_path';
|
||||
|
||||
async function getAttachmentDir(): Promise<string> {
|
||||
try {
|
||||
const saved = await AsyncStorage.getItem(STORAGE_PATH_KEY);
|
||||
return saved || DEFAULT_ATTACHMENT_DIR;
|
||||
} catch { return DEFAULT_ATTACHMENT_DIR; }
|
||||
}
|
||||
|
||||
/** Speichert Base64-Daten als Datei, gibt file:// Pfad zurueck */
|
||||
async function persistAttachment(base64Data: string, msgId: string, fileName: string): Promise<string> {
|
||||
const cacheDir = await getAttachmentDir();
|
||||
await RNFS.mkdir(cacheDir);
|
||||
// Dateiendung aus originalem Dateinamen oder Fallback
|
||||
const ext = fileName.includes('.') ? fileName.split('.').pop() : 'bin';
|
||||
const safeName = `${msgId}_${fileName.replace(/[^a-zA-Z0-9._-]/g, '_')}`;
|
||||
const filePath = `${cacheDir}/${safeName}`;
|
||||
await RNFS.writeFile(filePath, base64Data, 'base64');
|
||||
return `file://${filePath}`;
|
||||
}
|
||||
|
||||
/** Prueft ob eine lokale Datei noch existiert */
|
||||
async function checkFileExists(uri: string): Promise<boolean> {
|
||||
if (!uri || !uri.startsWith('file://')) return false;
|
||||
return RNFS.exists(uri.replace('file://', ''));
|
||||
}
|
||||
|
||||
// --- Komponente ---
|
||||
|
||||
@@ -96,6 +125,40 @@ const ChatScreen: React.FC = () => {
|
||||
// RVS-Nachrichten abonnieren
|
||||
useEffect(() => {
|
||||
const unsubMessage = rvs.onMessage((message: RVSMessage) => {
|
||||
// file_saved: Bridge meldet Server-Pfad — in Attachment merken fuer Re-Download
|
||||
if (message.type === 'file_saved') {
|
||||
const serverPath = (message.payload.serverPath as string) || '';
|
||||
const name = (message.payload.name as string) || '';
|
||||
if (serverPath) {
|
||||
setMessages(prev => prev.map(m => ({
|
||||
...m,
|
||||
attachments: m.attachments?.map(a =>
|
||||
a.name === name && !a.serverPath ? { ...a, serverPath } : a
|
||||
),
|
||||
})));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// file_response: Re-Download von Server — lokal speichern
|
||||
if (message.type === 'file_response') {
|
||||
const reqId = (message.payload.requestId as string) || '';
|
||||
const b64 = (message.payload.base64 as string) || '';
|
||||
const serverPath = (message.payload.serverPath as string) || '';
|
||||
if (b64 && reqId) {
|
||||
const fileName = (message.payload.name as string) || 'download';
|
||||
persistAttachment(b64, reqId, fileName).then(filePath => {
|
||||
setMessages(prev => prev.map(m => ({
|
||||
...m,
|
||||
attachments: m.attachments?.map(a =>
|
||||
a.serverPath === serverPath ? { ...a, uri: filePath } : a
|
||||
),
|
||||
})));
|
||||
}).catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// STT-Ergebnis: Spracheingabe-Placeholder mit transkribiertem Text ersetzen
|
||||
if (message.type === 'stt_result') {
|
||||
const sttText = (message.payload.text as string) || '';
|
||||
@@ -216,7 +279,14 @@ const ChatScreen: React.FC = () => {
|
||||
// Chat-Verlauf in AsyncStorage speichern (letzte N Nachrichten)
|
||||
useEffect(() => {
|
||||
if (messages.length === 0) return;
|
||||
const toStore = messages.slice(-MAX_STORED_MESSAGES);
|
||||
// Nur file:// URIs speichern, data: URIs rausfiltern (zu gross)
|
||||
const toStore = messages.slice(-MAX_STORED_MESSAGES).map(msg => ({
|
||||
...msg,
|
||||
attachments: msg.attachments?.map(att => ({
|
||||
...att,
|
||||
uri: att.uri?.startsWith('file://') ? att.uri : undefined,
|
||||
})),
|
||||
}));
|
||||
AsyncStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(toStore)).catch(err =>
|
||||
console.error('[Chat] Fehler beim Speichern:', err),
|
||||
);
|
||||
@@ -310,8 +380,11 @@ const ChatScreen: React.FC = () => {
|
||||
const location = await getCurrentLocation();
|
||||
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const msgId = nextId();
|
||||
let imageUri = isImage && file.base64 ? `data:${file.type};base64,${file.base64}` : file.uri;
|
||||
|
||||
const userMsg: ChatMessage = {
|
||||
id: nextId(),
|
||||
id: msgId,
|
||||
sender: 'user',
|
||||
text: 'Anhang empfangen',
|
||||
timestamp: Date.now(),
|
||||
@@ -319,12 +392,21 @@ const ChatScreen: React.FC = () => {
|
||||
type: isImage ? 'image' : 'file',
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
uri: isImage && file.base64 ? `data:${file.type};base64,${file.base64}` : file.uri,
|
||||
uri: imageUri,
|
||||
mimeType: file.type,
|
||||
}],
|
||||
};
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
|
||||
// Anhang auf Disk speichern fuer Persistenz
|
||||
if (file.base64) {
|
||||
persistAttachment(file.base64, msgId, file.name).then(filePath => {
|
||||
setMessages(prev => prev.map(m =>
|
||||
m.id === msgId ? { ...m, attachments: m.attachments?.map(a => ({ ...a, uri: filePath })) } : m
|
||||
));
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
rvs.send('file', {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
@@ -339,20 +421,32 @@ const ChatScreen: React.FC = () => {
|
||||
setShowCameraUpload(false);
|
||||
const location = await getCurrentLocation();
|
||||
|
||||
const msgId = nextId();
|
||||
const dataUri = photo.base64 ? `data:${photo.type};base64,${photo.base64}` : undefined;
|
||||
|
||||
const userMsg: ChatMessage = {
|
||||
id: nextId(),
|
||||
id: msgId,
|
||||
sender: 'user',
|
||||
text: 'Anhang empfangen',
|
||||
timestamp: Date.now(),
|
||||
attachments: [{
|
||||
type: 'image',
|
||||
name: photo.fileName,
|
||||
uri: photo.base64 ? `data:${photo.type};base64,${photo.base64}` : undefined,
|
||||
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', {
|
||||
name: photo.fileName,
|
||||
type: photo.type,
|
||||
@@ -382,7 +476,30 @@ const ChatScreen: React.FC = () => {
|
||||
source={{ uri: att.uri }}
|
||||
style={styles.attachmentImage}
|
||||
resizeMode="contain"
|
||||
onError={() => {
|
||||
// Bild nicht mehr verfuegbar — Placeholder setzen
|
||||
setMessages(prev => prev.map(m =>
|
||||
m.id === item.id ? { ...m, attachments: m.attachments?.map((a, i) =>
|
||||
i === idx ? { ...a, uri: undefined } : a
|
||||
)} : m
|
||||
));
|
||||
}}
|
||||
/>
|
||||
) : att.type === 'image' && !att.uri ? (
|
||||
<TouchableOpacity
|
||||
style={styles.attachmentFile}
|
||||
onPress={() => {
|
||||
if (att.serverPath) {
|
||||
rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: item.id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text style={styles.attachmentFileIcon}>{'\uD83D\uDDBC\uFE0F'}</Text>
|
||||
<Text style={styles.attachmentFileName} numberOfLines={1}>{att.name}</Text>
|
||||
<Text style={styles.attachmentFileSize}>
|
||||
{att.serverPath ? '(tippen zum Laden)' : '(nicht verfuegbar)'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={styles.attachmentFile}>
|
||||
<Text style={styles.attachmentFileIcon}>
|
||||
@@ -393,6 +510,12 @@ const ChatScreen: React.FC = () => {
|
||||
</Text>
|
||||
<Text style={styles.attachmentFileName} numberOfLines={1}>{att.name}</Text>
|
||||
{att.size ? <Text style={styles.attachmentFileSize}>{Math.round(att.size / 1024)}KB</Text> : null}
|
||||
{!att.uri && att.serverPath && (
|
||||
<TouchableOpacity onPress={() => rvs.send('file_request' as any, { serverPath: att.serverPath, requestId: item.id })}>
|
||||
<Text style={[styles.attachmentFileSize, {color: '#0096FF'}]}>(laden)</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!att.uri && !att.serverPath && <Text style={styles.attachmentFileSize}>(nicht verfuegbar)</Text>}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user