diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index 463814f..fd45fe8 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -994,6 +994,7 @@ const ChatScreen: React.FC = () => { // file_from_aria: ARIA hat eine Datei rausgegeben → als ARIA-Bubble anzeigen if (message.type === 'file_from_aria') { const p = message.payload || {}; + const sp = (p.serverPath as string) || ''; const ariaMsg: ChatMessage = { id: nextId(), sender: 'aria', @@ -1004,10 +1005,20 @@ const ChatScreen: React.FC = () => { name: (p.name as string) || 'datei', size: (p.size as number) || 0, mimeType: (p.mimeType as string) || '', - serverPath: (p.serverPath as string) || '', + serverPath: sp, }], }; - setMessages(prev => capMessages([...prev, ariaMsg])); + setMessages(prev => { + // Falls die Chat-Bubble mit dieser Datei schon da ist (Event-Reihen- + // folge kann variieren, und die chat-Payload traegt die files jetzt + // selbst), keine doppelte Solo-Bubble anlegen. + if (sp && prev.some(m => + m.sender === 'aria' && m.attachments?.some(a => a.serverPath === sp) + )) { + return prev; + } + return capMessages([...prev, ariaMsg]); + }); return; } @@ -1156,26 +1167,53 @@ const ChatScreen: React.FC = () => { const text = (message.payload.text as string) || ''; const ts = message.timestamp; + // ARIA-Dateien, die DIREKT an dieser Chat-Bubble haengen (Bridge schickt + // sie jetzt in der chat-Payload mit — vorher kamen sie nur als separates + // file_from_aria-Event, wodurch der Anhang live nicht an der Nachricht + // erschien, sondern erst nach einem Seitenwechsel/Reload). + const incomingFiles = Array.isArray((message.payload as any).files) + ? (message.payload as any).files as Array : []; + const fileAttachments: Attachment[] = incomingFiles.map(f => ({ + type: (typeof f.mimeType === 'string' && f.mimeType.startsWith('image/')) ? 'image' : 'file', + name: f.name || 'datei', + size: f.size || 0, + mimeType: f.mimeType || '', + serverPath: f.serverPath || '', + })); + const incomingPaths = new Set( + fileAttachments.map(a => a.serverPath).filter(Boolean) as string[] + ); // Duplikat-Schutz: gleicher Text innerhalb 5s ignorieren setMessages(prev => { const isDuplicate = prev.some(m => m.sender === 'aria' && m.text === text && Math.abs(m.timestamp - ts) < 5000 ); if (isDuplicate) return prev; + const payloadAtts = (message.payload.attachments as Attachment[] | undefined) || []; + const mergedAtts = [...payloadAtts, ...fileAttachments]; const ariaMsg: ChatMessage = { id: nextId(), sender: 'aria', text, timestamp: ts, - attachments: message.payload.attachments as Attachment[] | undefined, + attachments: mergedAtts.length ? mergedAtts : undefined, messageId: (message.payload.messageId as string) || undefined, backupTs: (message.payload.backupTs as number) || undefined, projectId: ((message.payload as any).projectId as string) || '', answeredBy: ((message.payload as any).answeredBy as string) || '', }; + // Die separate file_from_aria-Bubble (leerer Text, nur Datei), die kurz + // zuvor fuer DIESE Datei ankam, entfernen — sonst doppelt (einmal solo, + // einmal an der Text-Bubble). Nur solche mit passendem serverPath. + const deduped = incomingPaths.size + ? prev.filter(m => !( + m.sender === 'aria' && !m.text && (m.attachments?.length) && + m.attachments.every(a => a.serverPath && incomingPaths.has(a.serverPath)) + )) + : prev; // ARIA hat geantwortet → alle User-Bubbles davor als 'delivered' // markieren (WhatsApp-Doppelhaken ✓✓). Brain hat sie verarbeitet. - return capMessages([...prev, ariaMsg]).map(m => + return capMessages([...deduped, ariaMsg]).map(m => m.sender === 'user' && (m.deliveryStatus === 'sent' || m.deliveryStatus === 'sending') ? { ...m, deliveryStatus: 'delivered' } diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index 377d2ec..b55cfa2 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -1560,6 +1560,13 @@ class ARIABridge: "text": display_text, "sender": "aria", "messageId": message_id, + # ARIA-Dateien DIREKT an der Chat-Bubble mitschicken — sonst kommt + # der Anhang live nur als separates file_from_aria-Event (eigene + # Bubble) und taucht an der Nachricht erst nach einem Seiten- + # wechsel auf (Reload aus chat_backup, das die files kennt). Selbe + # Struktur wie im Backup, damit App live == Reload rendert. + "files": [{"serverPath": f["serverPath"], "name": f["name"], + "mimeType": f["mimeType"], "size": f["size"]} for f in aria_files], # backupTs = der ts in chat_backup.jsonl. Wird von Clients als # Bubble-ID fuer das Mülltonne-Loeschen verwendet (delete_message_request). "backupTs": assistant_backup_ts,