| .. | .. |
|---|
| 1 | 1 | import React, { useCallback, useEffect, useState } from "react"; |
|---|
| 2 | 2 | import { Image, Pressable, Text, View } from "react-native"; |
|---|
| 3 | 3 | import { Message } from "../../types"; |
|---|
| 4 | | -import { playAudio, stopPlayback, onPlayingChange } from "../../services/audio"; |
|---|
| 4 | +import { playSingle, stopPlayback, onPlayingChange } from "../../services/audio"; |
|---|
| 5 | 5 | import { ImageViewer } from "./ImageViewer"; |
|---|
| 6 | 6 | import { useTheme } from "../../contexts/ThemeContext"; |
|---|
| 7 | 7 | |
|---|
| .. | .. |
|---|
| 27 | 27 | const [showViewer, setShowViewer] = useState(false); |
|---|
| 28 | 28 | const { colors, isDark } = useTheme(); |
|---|
| 29 | 29 | |
|---|
| 30 | + // Track whether THIS bubble's audio is playing via the singleton URI |
|---|
| 30 | 31 | useEffect(() => { |
|---|
| 31 | | - return onPlayingChange((playing) => { |
|---|
| 32 | | - if (!playing) setIsPlaying(false); |
|---|
| 32 | + return onPlayingChange((uri) => { |
|---|
| 33 | + setIsPlaying(uri !== null && uri === message.audioUri); |
|---|
| 33 | 34 | }); |
|---|
| 34 | | - }, []); |
|---|
| 35 | + }, [message.audioUri]); |
|---|
| 35 | 36 | |
|---|
| 36 | 37 | const isUser = message.role === "user"; |
|---|
| 37 | 38 | const isSystem = message.role === "system"; |
|---|
| .. | .. |
|---|
| 40 | 41 | if (!message.audioUri) return; |
|---|
| 41 | 42 | |
|---|
| 42 | 43 | if (isPlaying) { |
|---|
| 44 | + // This bubble is playing — stop it |
|---|
| 43 | 45 | await stopPlayback(); |
|---|
| 44 | | - setIsPlaying(false); |
|---|
| 45 | 46 | } else { |
|---|
| 46 | | - setIsPlaying(true); |
|---|
| 47 | | - await playAudio(message.audioUri, () => setIsPlaying(false)); |
|---|
| 47 | + // Play this bubble (stops anything else automatically) |
|---|
| 48 | + await playSingle(message.audioUri, () => {}); |
|---|
| 48 | 49 | } |
|---|
| 49 | 50 | }, [isPlaying, message.audioUri]); |
|---|
| 50 | 51 | |
|---|
| .. | .. |
|---|
| 114 | 115 | /> |
|---|
| 115 | 116 | </View> |
|---|
| 116 | 117 | ) : 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 }} |
|---|
| 134 | 122 | > |
|---|
| 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)} |
|---|
| 137 | 167 | </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> |
|---|
| 167 | 182 | ) : ( |
|---|
| 168 | 183 | <Text |
|---|
| 169 | 184 | style={{ |
|---|