From 0e888d62af1434fef231e11a5c307a5b48a8deb1 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 10:49:07 +0100
Subject: [PATCH] feat: singleton audio, transcript reflection, voice persistence
---
components/chat/MessageBubble.tsx | 125 +++++++++++++++++++++++------------------
1 files changed, 70 insertions(+), 55 deletions(-)
diff --git a/components/chat/MessageBubble.tsx b/components/chat/MessageBubble.tsx
index 8d3bd9a..5edf2f8 100644
--- a/components/chat/MessageBubble.tsx
+++ b/components/chat/MessageBubble.tsx
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from "react";
import { Image, Pressable, Text, View } from "react-native";
import { Message } from "../../types";
-import { playAudio, stopPlayback, onPlayingChange } from "../../services/audio";
+import { playSingle, stopPlayback, onPlayingChange } from "../../services/audio";
import { ImageViewer } from "./ImageViewer";
import { useTheme } from "../../contexts/ThemeContext";
@@ -27,11 +27,12 @@
const [showViewer, setShowViewer] = useState(false);
const { colors, isDark } = useTheme();
+ // Track whether THIS bubble's audio is playing via the singleton URI
useEffect(() => {
- return onPlayingChange((playing) => {
- if (!playing) setIsPlaying(false);
+ return onPlayingChange((uri) => {
+ setIsPlaying(uri !== null && uri === message.audioUri);
});
- }, []);
+ }, [message.audioUri]);
const isUser = message.role === "user";
const isSystem = message.role === "system";
@@ -40,11 +41,11 @@
if (!message.audioUri) return;
if (isPlaying) {
+ // This bubble is playing — stop it
await stopPlayback();
- setIsPlaying(false);
} else {
- setIsPlaying(true);
- await playAudio(message.audioUri, () => setIsPlaying(false));
+ // Play this bubble (stops anything else automatically)
+ await playSingle(message.audioUri, () => {});
}
}, [isPlaying, message.audioUri]);
@@ -114,56 +115,70 @@
/>
</View>
) : message.type === "voice" ? (
- <Pressable
- onPress={handleVoicePress}
- style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
- >
- <View
- style={{
- width: 36,
- height: 36,
- borderRadius: 18,
- alignItems: "center",
- justifyContent: "center",
- backgroundColor: isPlaying
- ? "#FF9F43"
- : isUser
- ? "rgba(255,255,255,0.2)"
- : colors.border,
- }}
+ <View>
+ <Pressable
+ onPress={handleVoicePress}
+ style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
>
- <Text style={{ fontSize: 14, color: isUser ? "#FFF" : colors.text }}>
- {isPlaying ? "\u23F8" : "\u25B6"}
+ <View
+ style={{
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: isPlaying
+ ? "#FF9F43"
+ : isUser
+ ? "rgba(255,255,255,0.2)"
+ : colors.border,
+ }}
+ >
+ <Text style={{ fontSize: 14, color: isUser ? "#FFF" : colors.text }}>
+ {isPlaying ? "\u23F8" : "\u25B6"}
+ </Text>
+ </View>
+
+ <View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 1, height: 32 }}>
+ {Array.from({ length: 20 }).map((_, i) => (
+ <View
+ key={i}
+ 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>
+
+ <Text
+ style={{
+ fontSize: 11,
+ color: isUser ? "rgba(255,255,255,0.8)" : colors.textSecondary,
+ }}
+ >
+ {formatDuration(message.duration)}
</Text>
- </View>
-
- <View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 1, height: 32 }}>
- {Array.from({ length: 20 }).map((_, i) => (
- <View
- key={i}
- 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>
-
- <Text
- style={{
- fontSize: 11,
- color: isUser ? "rgba(255,255,255,0.8)" : colors.textSecondary,
- }}
- >
- {formatDuration(message.duration)}
- </Text>
- </Pressable>
+ </Pressable>
+ {message.content ? (
+ <Text
+ style={{
+ fontSize: 14,
+ lineHeight: 20,
+ marginTop: 8,
+ color: isUser ? "rgba(255,255,255,0.9)" : colors.textSecondary,
+ }}
+ >
+ {message.content}
+ </Text>
+ ) : null}
+ </View>
) : (
<Text
style={{
--
Gitblit v1.3.1