From af1543135d42adc2e97dc5243aeef7418cd3b00d Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 08:39:26 +0100
Subject: [PATCH] feat: dual address auto-switch, custom icon, notifications, image support

---
 components/chat/MessageBubble.tsx |  152 +++++++++++++++++++++++++++++++++-----------------
 1 files changed, 99 insertions(+), 53 deletions(-)

diff --git a/components/chat/MessageBubble.tsx b/components/chat/MessageBubble.tsx
index fd8c0b6..8d3bd9a 100644
--- a/components/chat/MessageBubble.tsx
+++ b/components/chat/MessageBubble.tsx
@@ -1,7 +1,9 @@
-import React, { useCallback, useState } from "react";
+import React, { useCallback, useEffect, useState } from "react";
 import { Image, Pressable, Text, View } from "react-native";
 import { Message } from "../../types";
-import { playAudio, stopPlayback } from "../../services/audio";
+import { playAudio, stopPlayback, onPlayingChange } from "../../services/audio";
+import { ImageViewer } from "./ImageViewer";
+import { useTheme } from "../../contexts/ThemeContext";
 
 interface MessageBubbleProps {
   message: Message;
@@ -22,6 +24,14 @@
 
 export function MessageBubble({ message }: MessageBubbleProps) {
   const [isPlaying, setIsPlaying] = useState(false);
+  const [showViewer, setShowViewer] = useState(false);
+  const { colors, isDark } = useTheme();
+
+  useEffect(() => {
+    return onPlayingChange((playing) => {
+      if (!playing) setIsPlaying(false);
+    });
+  }, []);
 
   const isUser = message.role === "user";
   const isSystem = message.role === "system";
@@ -40,40 +50,56 @@
 
   if (isSystem) {
     return (
-      <View className="items-center my-1 px-4">
-        <Text className="text-pai-text-muted text-xs">{message.content}</Text>
+      <View style={{ alignItems: "center", marginVertical: 4, paddingHorizontal: 16 }}>
+        <Text style={{ color: colors.textMuted, fontSize: 12 }}>{message.content}</Text>
       </View>
     );
   }
 
+  const bubbleBg = isUser
+    ? colors.accent
+    : isDark ? "#252538" : colors.bgSecondary;
+  const bubbleRadius = isUser
+    ? { borderTopRightRadius: 4 }
+    : { borderTopLeftRadius: 4 };
+
   return (
     <View
-      className={`flex-row my-1 px-3 ${isUser ? "justify-end" : "justify-start"}`}
+      style={{
+        flexDirection: "row",
+        marginVertical: 4,
+        paddingHorizontal: 12,
+        justifyContent: isUser ? "flex-end" : "flex-start",
+      }}
     >
       <View
-        className={`max-w-[78%] rounded-2xl px-4 py-3 ${
-          isUser
-            ? "bg-pai-accent rounded-tr-sm"
-            : "bg-pai-surface rounded-tl-sm"
-        }`}
+        style={{
+          maxWidth: "78%",
+          borderRadius: 16,
+          paddingHorizontal: 16,
+          paddingVertical: 12,
+          backgroundColor: bubbleBg,
+          ...bubbleRadius,
+        }}
       >
         {message.type === "image" && message.imageBase64 ? (
-          /* Image message */
           <View>
-            <Image
-              source={{ uri: `data:image/png;base64,${message.imageBase64}` }}
-              style={{
-                width: 260,
-                height: 180,
-                borderRadius: 10,
-                backgroundColor: "#14141F",
-              }}
-              resizeMode="contain"
-            />
+            <Pressable onPress={() => setShowViewer(true)}>
+              <Image
+                source={{ uri: `data:image/png;base64,${message.imageBase64}` }}
+                style={{
+                  width: 260,
+                  height: 180,
+                  borderRadius: 10,
+                  backgroundColor: colors.bgTertiary,
+                }}
+                resizeMode="contain"
+              />
+            </Pressable>
             {message.content ? (
               <Text
                 style={{
-                  color: isUser ? "#FFF" : "#9898B0",
+                  color: isUser ? "#FFF" : colors.textSecondary,
                   fontSize: 12,
                   marginTop: 4,
                 }}
@@ -81,74 +107,94 @@
                 {message.content}
               </Text>
             ) : null}
+            <ImageViewer
+              visible={showViewer}
+              imageBase64={message.imageBase64}
+              onClose={() => setShowViewer(false)}
+            />
           </View>
         ) : message.type === "voice" ? (
           <Pressable
             onPress={handleVoicePress}
-            className="flex-row items-center gap-3"
+            style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
           >
-            {/* Play/pause icon */}
             <View
-              className={`w-9 h-9 rounded-full items-center justify-center ${
-                isPlaying ? "bg-pai-voice" : isUser ? "bg-white/20" : "bg-pai-border"
-              }`}
+              style={{
+                width: 36,
+                height: 36,
+                borderRadius: 18,
+                alignItems: "center",
+                justifyContent: "center",
+                backgroundColor: isPlaying
+                  ? "#FF9F43"
+                  : isUser
+                  ? "rgba(255,255,255,0.2)"
+                  : colors.border,
+              }}
             >
-              <Text
-                className={`text-base ${isUser ? "text-white" : "text-pai-text"}`}
-              >
-                {isPlaying ? "⏸" : "▶"}
+              <Text style={{ fontSize: 14, color: isUser ? "#FFF" : colors.text }}>
+                {isPlaying ? "\u23F8" : "\u25B6"}
               </Text>
             </View>
 
-            {/* Waveform placeholder */}
-            <View className="flex-1 flex-row items-center gap-px h-8">
+            <View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 1, height: 32 }}>
               {Array.from({ length: 20 }).map((_, i) => (
                 <View
                   key={i}
-                  className={`flex-1 rounded-full ${
-                    isPlaying && i < 10
-                      ? "bg-pai-voice"
-                      : isUser
-                      ? "bg-white/50"
-                      : "bg-pai-text-muted"
-                  }`}
                   style={{
+                    flex: 1,
+                    borderRadius: 2,
+                    backgroundColor: isPlaying && i < 10
+                      ? "#FF9F43"
+                      : isUser
+                      ? "rgba(255,255,255,0.5)"
+                      : colors.textMuted,
                     height: `${20 + Math.sin(i * 0.8) * 60}%`,
                   }}
                 />
               ))}
             </View>
 
-            {/* Duration */}
             <Text
-              className={`text-xs ${
-                isUser ? "text-white/80" : "text-pai-text-secondary"
-              }`}
+              style={{
+                fontSize: 11,
+                color: isUser ? "rgba(255,255,255,0.8)" : colors.textSecondary,
+              }}
             >
               {formatDuration(message.duration)}
             </Text>
           </Pressable>
         ) : (
           <Text
-            className={`text-base leading-6 ${
-              isUser ? "text-white" : "text-pai-text"
-            }`}
+            style={{
+              fontSize: 16,
+              lineHeight: 24,
+              color: isUser ? "#FFF" : colors.text,
+            }}
           >
             {message.content}
           </Text>
         )}
 
-        {/* Timestamp + status */}
-        <View className={`flex-row items-center mt-1 gap-1 ${isUser ? "justify-end" : "justify-start"}`}>
+        <View
+          style={{
+            flexDirection: "row",
+            alignItems: "center",
+            marginTop: 4,
+            gap: 4,
+            justifyContent: isUser ? "flex-end" : "flex-start",
+          }}
+        >
           <Text
-            className={`text-2xs ${
-              isUser ? "text-white/60" : "text-pai-text-muted"
-            }`}
+            style={{
+              fontSize: 10,
+              color: isUser ? "rgba(255,255,255,0.6)" : colors.textMuted,
+            }}
           >
             {formatTime(message.timestamp)}
           </Text>
           {isUser && message.status === "error" && (
-            <Text className="text-2xs text-pai-error"> !</Text>
+            <Text style={{ fontSize: 10, color: colors.danger }}> !</Text>
           )}
         </View>
       </View>

--
Gitblit v1.3.1