Matthias Nott
2026-03-07 0e888d62af1434fef231e11a5c307a5b48a8deb1
components/chat/MessageBubble.tsx
....@@ -1,7 +1,7 @@
11 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, onPlayingChange } from "../../services/audio";
4
+import { playSingle, stopPlayback, onPlayingChange } from "../../services/audio";
55 import { ImageViewer } from "./ImageViewer";
66 import { useTheme } from "../../contexts/ThemeContext";
77
....@@ -27,11 +27,12 @@
2727 const [showViewer, setShowViewer] = useState(false);
2828 const { colors, isDark } = useTheme();
2929
30
+ // Track whether THIS bubble's audio is playing via the singleton URI
3031 useEffect(() => {
31
- return onPlayingChange((playing) => {
32
- if (!playing) setIsPlaying(false);
32
+ return onPlayingChange((uri) => {
33
+ setIsPlaying(uri !== null && uri === message.audioUri);
3334 });
34
- }, []);
35
+ }, [message.audioUri]);
3536
3637 const isUser = message.role === "user";
3738 const isSystem = message.role === "system";
....@@ -40,11 +41,11 @@
4041 if (!message.audioUri) return;
4142
4243 if (isPlaying) {
44
+ // This bubble is playing — stop it
4345 await stopPlayback();
44
- setIsPlaying(false);
4546 } else {
46
- setIsPlaying(true);
47
- await playAudio(message.audioUri, () => setIsPlaying(false));
47
+ // Play this bubble (stops anything else automatically)
48
+ await playSingle(message.audioUri, () => {});
4849 }
4950 }, [isPlaying, message.audioUri]);
5051
....@@ -114,56 +115,70 @@
114115 />
115116 </View>
116117 ) : message.type === "voice" ? (
117
- <Pressable
118
- onPress={handleVoicePress}
119
- style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
120
- >
121
- <View
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
- }}
118
+ <View>
119
+ <Pressable
120
+ onPress={handleVoicePress}
121
+ style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
134122 >
135
- <Text style={{ fontSize: 14, color: isUser ? "#FFF" : colors.text }}>
136
- {isPlaying ? "\u23F8" : "\u25B6"}
123
+ <View
124
+ style={{
125
+ width: 36,
126
+ height: 36,
127
+ borderRadius: 18,
128
+ alignItems: "center",
129
+ justifyContent: "center",
130
+ backgroundColor: isPlaying
131
+ ? "#FF9F43"
132
+ : isUser
133
+ ? "rgba(255,255,255,0.2)"
134
+ : colors.border,
135
+ }}
136
+ >
137
+ <Text style={{ fontSize: 14, color: isUser ? "#FFF" : colors.text }}>
138
+ {isPlaying ? "\u23F8" : "\u25B6"}
139
+ </Text>
140
+ </View>
141
+
142
+ <View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 1, height: 32 }}>
143
+ {Array.from({ length: 20 }).map((_, i) => (
144
+ <View
145
+ key={i}
146
+ style={{
147
+ flex: 1,
148
+ borderRadius: 2,
149
+ backgroundColor: isPlaying && i < 10
150
+ ? "#FF9F43"
151
+ : isUser
152
+ ? "rgba(255,255,255,0.5)"
153
+ : colors.textMuted,
154
+ height: `${20 + Math.sin(i * 0.8) * 60}%`,
155
+ }}
156
+ />
157
+ ))}
158
+ </View>
159
+
160
+ <Text
161
+ style={{
162
+ fontSize: 11,
163
+ color: isUser ? "rgba(255,255,255,0.8)" : colors.textSecondary,
164
+ }}
165
+ >
166
+ {formatDuration(message.duration)}
137167 </Text>
138
- </View>
139
-
140
- <View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 1, height: 32 }}>
141
- {Array.from({ length: 20 }).map((_, i) => (
142
- <View
143
- key={i}
144
- 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,
152
- height: `${20 + Math.sin(i * 0.8) * 60}%`,
153
- }}
154
- />
155
- ))}
156
- </View>
157
-
158
- <Text
159
- style={{
160
- fontSize: 11,
161
- color: isUser ? "rgba(255,255,255,0.8)" : colors.textSecondary,
162
- }}
163
- >
164
- {formatDuration(message.duration)}
165
- </Text>
166
- </Pressable>
168
+ </Pressable>
169
+ {message.content ? (
170
+ <Text
171
+ style={{
172
+ fontSize: 14,
173
+ lineHeight: 20,
174
+ marginTop: 8,
175
+ color: isUser ? "rgba(255,255,255,0.9)" : colors.textSecondary,
176
+ }}
177
+ >
178
+ {message.content}
179
+ </Text>
180
+ ) : null}
181
+ </View>
167182 ) : (
168183 <Text
169184 style={{