Matthias Nott
2026-03-07 af1543135d42adc2e97dc5243aeef7418cd3b00d
components/chat/MessageBubble.tsx
....@@ -1,7 +1,9 @@
1
-import React, { useCallback, useState } from "react";
1
+import React, { useCallback, useEffect, useState } from "react";
22 import { Image, Pressable, Text, View } from "react-native";
33 import { Message } from "../../types";
4
-import { playAudio, stopPlayback } from "../../services/audio";
4
+import { playAudio, stopPlayback, onPlayingChange } from "../../services/audio";
5
+import { ImageViewer } from "./ImageViewer";
6
+import { useTheme } from "../../contexts/ThemeContext";
57
68 interface MessageBubbleProps {
79 message: Message;
....@@ -22,6 +24,14 @@
2224
2325 export function MessageBubble({ message }: MessageBubbleProps) {
2426 const [isPlaying, setIsPlaying] = useState(false);
27
+ const [showViewer, setShowViewer] = useState(false);
28
+ const { colors, isDark } = useTheme();
29
+
30
+ useEffect(() => {
31
+ return onPlayingChange((playing) => {
32
+ if (!playing) setIsPlaying(false);
33
+ });
34
+ }, []);
2535
2636 const isUser = message.role === "user";
2737 const isSystem = message.role === "system";
....@@ -40,40 +50,56 @@
4050
4151 if (isSystem) {
4252 return (
43
- <View className="items-center my-1 px-4">
44
- <Text className="text-pai-text-muted text-xs">{message.content}</Text>
53
+ <View style={{ alignItems: "center", marginVertical: 4, paddingHorizontal: 16 }}>
54
+ <Text style={{ color: colors.textMuted, fontSize: 12 }}>{message.content}</Text>
4555 </View>
4656 );
4757 }
4858
59
+ const bubbleBg = isUser
60
+ ? colors.accent
61
+ : isDark ? "#252538" : colors.bgSecondary;
62
+ const bubbleRadius = isUser
63
+ ? { borderTopRightRadius: 4 }
64
+ : { borderTopLeftRadius: 4 };
65
+
4966 return (
5067 <View
51
- className={`flex-row my-1 px-3 ${isUser ? "justify-end" : "justify-start"}`}
68
+ style={{
69
+ flexDirection: "row",
70
+ marginVertical: 4,
71
+ paddingHorizontal: 12,
72
+ justifyContent: isUser ? "flex-end" : "flex-start",
73
+ }}
5274 >
5375 <View
54
- className={`max-w-[78%] rounded-2xl px-4 py-3 ${
55
- isUser
56
- ? "bg-pai-accent rounded-tr-sm"
57
- : "bg-pai-surface rounded-tl-sm"
58
- }`}
76
+ style={{
77
+ maxWidth: "78%",
78
+ borderRadius: 16,
79
+ paddingHorizontal: 16,
80
+ paddingVertical: 12,
81
+ backgroundColor: bubbleBg,
82
+ ...bubbleRadius,
83
+ }}
5984 >
6085 {message.type === "image" && message.imageBase64 ? (
61
- /* Image message */
6286 <View>
63
- <Image
64
- source={{ uri: `data:image/png;base64,${message.imageBase64}` }}
65
- style={{
66
- width: 260,
67
- height: 180,
68
- borderRadius: 10,
69
- backgroundColor: "#14141F",
70
- }}
71
- resizeMode="contain"
72
- />
87
+ <Pressable onPress={() => setShowViewer(true)}>
88
+ <Image
89
+ source={{ uri: `data:image/png;base64,${message.imageBase64}` }}
90
+ style={{
91
+ width: 260,
92
+ height: 180,
93
+ borderRadius: 10,
94
+ backgroundColor: colors.bgTertiary,
95
+ }}
96
+ resizeMode="contain"
97
+ />
98
+ </Pressable>
7399 {message.content ? (
74100 <Text
75101 style={{
76
- color: isUser ? "#FFF" : "#9898B0",
102
+ color: isUser ? "#FFF" : colors.textSecondary,
77103 fontSize: 12,
78104 marginTop: 4,
79105 }}
....@@ -81,74 +107,94 @@
81107 {message.content}
82108 </Text>
83109 ) : null}
110
+ <ImageViewer
111
+ visible={showViewer}
112
+ imageBase64={message.imageBase64}
113
+ onClose={() => setShowViewer(false)}
114
+ />
84115 </View>
85116 ) : message.type === "voice" ? (
86117 <Pressable
87118 onPress={handleVoicePress}
88
- className="flex-row items-center gap-3"
119
+ style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
89120 >
90
- {/* Play/pause icon */}
91121 <View
92
- className={`w-9 h-9 rounded-full items-center justify-center ${
93
- isPlaying ? "bg-pai-voice" : isUser ? "bg-white/20" : "bg-pai-border"
94
- }`}
122
+ style={{
123
+ width: 36,
124
+ height: 36,
125
+ borderRadius: 18,
126
+ alignItems: "center",
127
+ justifyContent: "center",
128
+ backgroundColor: isPlaying
129
+ ? "#FF9F43"
130
+ : isUser
131
+ ? "rgba(255,255,255,0.2)"
132
+ : colors.border,
133
+ }}
95134 >
96
- <Text
97
- className={`text-base ${isUser ? "text-white" : "text-pai-text"}`}
98
- >
99
- {isPlaying ? "⏸" : "▶"}
135
+ <Text style={{ fontSize: 14, color: isUser ? "#FFF" : colors.text }}>
136
+ {isPlaying ? "\u23F8" : "\u25B6"}
100137 </Text>
101138 </View>
102139
103
- {/* Waveform placeholder */}
104
- <View className="flex-1 flex-row items-center gap-px h-8">
140
+ <View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 1, height: 32 }}>
105141 {Array.from({ length: 20 }).map((_, i) => (
106142 <View
107143 key={i}
108
- className={`flex-1 rounded-full ${
109
- isPlaying && i < 10
110
- ? "bg-pai-voice"
111
- : isUser
112
- ? "bg-white/50"
113
- : "bg-pai-text-muted"
114
- }`}
115144 style={{
145
+ flex: 1,
146
+ borderRadius: 2,
147
+ backgroundColor: isPlaying && i < 10
148
+ ? "#FF9F43"
149
+ : isUser
150
+ ? "rgba(255,255,255,0.5)"
151
+ : colors.textMuted,
116152 height: `${20 + Math.sin(i * 0.8) * 60}%`,
117153 }}
118154 />
119155 ))}
120156 </View>
121157
122
- {/* Duration */}
123158 <Text
124
- className={`text-xs ${
125
- isUser ? "text-white/80" : "text-pai-text-secondary"
126
- }`}
159
+ style={{
160
+ fontSize: 11,
161
+ color: isUser ? "rgba(255,255,255,0.8)" : colors.textSecondary,
162
+ }}
127163 >
128164 {formatDuration(message.duration)}
129165 </Text>
130166 </Pressable>
131167 ) : (
132168 <Text
133
- className={`text-base leading-6 ${
134
- isUser ? "text-white" : "text-pai-text"
135
- }`}
169
+ style={{
170
+ fontSize: 16,
171
+ lineHeight: 24,
172
+ color: isUser ? "#FFF" : colors.text,
173
+ }}
136174 >
137175 {message.content}
138176 </Text>
139177 )}
140178
141
- {/* Timestamp + status */}
142
- <View className={`flex-row items-center mt-1 gap-1 ${isUser ? "justify-end" : "justify-start"}`}>
179
+ <View
180
+ style={{
181
+ flexDirection: "row",
182
+ alignItems: "center",
183
+ marginTop: 4,
184
+ gap: 4,
185
+ justifyContent: isUser ? "flex-end" : "flex-start",
186
+ }}
187
+ >
143188 <Text
144
- className={`text-2xs ${
145
- isUser ? "text-white/60" : "text-pai-text-muted"
146
- }`}
189
+ style={{
190
+ fontSize: 10,
191
+ color: isUser ? "rgba(255,255,255,0.6)" : colors.textMuted,
192
+ }}
147193 >
148194 {formatTime(message.timestamp)}
149195 </Text>
150196 {isUser && message.status === "error" && (
151
- <Text className="text-2xs text-pai-error"> !</Text>
197
+ <Text style={{ fontSize: 10, color: colors.danger }}> !</Text>
152198 )}
153199 </View>
154200 </View>