import React, { useCallback, useEffect, useState } from "react"; import { Image, Pressable, Text, View } from "react-native"; import { Message } from "../../types"; import { playSingle, stopPlayback, onPlayingChange } from "../../services/audio"; import { ImageViewer } from "./ImageViewer"; import { useTheme } from "../../contexts/ThemeContext"; interface MessageBubbleProps { message: Message; } function formatDuration(ms?: number): string { if (!ms) return "0:00"; const totalSeconds = Math.floor(ms / 1000); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes}:${seconds.toString().padStart(2, "0")}`; } function formatTime(timestamp: number): string { const d = new Date(timestamp); return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } export function MessageBubble({ message }: MessageBubbleProps) { const [isPlaying, setIsPlaying] = useState(false); const [showViewer, setShowViewer] = useState(false); const { colors, isDark } = useTheme(); // Track whether THIS bubble's audio is playing via the singleton URI useEffect(() => { return onPlayingChange((uri) => { setIsPlaying(uri !== null && uri === message.audioUri); }); }, [message.audioUri]); const isUser = message.role === "user"; const isSystem = message.role === "system"; const handleVoicePress = useCallback(async () => { if (!message.audioUri) return; if (isPlaying) { // This bubble is playing — stop it await stopPlayback(); } else { // Play this bubble (stops anything else automatically) await playSingle(message.audioUri, () => {}); } }, [isPlaying, message.audioUri]); if (isSystem) { return ( {message.content} ); } const bubbleBg = isUser ? colors.accent : isDark ? "#252538" : colors.bgSecondary; const bubbleRadius = isUser ? { borderTopRightRadius: 4 } : { borderTopLeftRadius: 4 }; return ( {message.type === "image" && message.imageBase64 ? ( setShowViewer(true)}> {message.content ? ( {message.content} ) : null} setShowViewer(false)} /> ) : message.type === "voice" ? ( {isPlaying ? "\u23F8" : "\u25B6"} {Array.from({ length: 20 }).map((_, i) => ( ))} {formatDuration(message.duration)} {message.content ? ( {message.content} ) : null} ) : ( {message.content} )} {formatTime(message.timestamp)} {isUser && message.status === "error" && ( ! )} ); }