import { useMemo } from "react"; import { Text, View, StyleSheet, Image, Platform } from "react-native"; import Pressable from "@/components/ui/pressable-fix"; import { useColors } from "@/hooks/use-colors"; import type { ChatMessage } from "@/shared/types"; import { formatBytes, formatDuration } from "@/lib/chat-store"; import MaterialIcons from "@expo/vector-icons/MaterialIcons"; interface ChatBubbleProps { message: ChatMessage; onImagePress?: (uri: string) => void; onPlayVoice?: (message: ChatMessage) => void; isPlayingVoice?: boolean; } export function ChatBubble({ message, onImagePress, onPlayVoice, isPlayingVoice }: ChatBubbleProps) { const colors = useColors(); const isUser = message.role === "user"; // Stable waveform bar heights based on message id const waveHeights = useMemo(() => { let seed = 0; for (let i = 0; i < message.id.length; i++) seed = (seed * 31 + message.id.charCodeAt(i)) | 0; return Array.from({ length: 12 }, (_, i) => { seed = (seed * 16807 + i * 1013) % 2147483647; return 4 + (seed % 15); }); }, [message.id]); const bubbleStyle = [ styles.bubble, { backgroundColor: isUser ? colors.primary : colors.surface, borderColor: isUser ? colors.primary : colors.border, alignSelf: isUser ? "flex-end" as const : "flex-start" as const, }, ]; const textColor = isUser ? "#fff" : colors.foreground; const mutedColor = isUser ? "rgba(255,255,255,0.6)" : colors.muted; const timeStr = new Date(message.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); return ( {!isUser && ( T )} {message.contentType === "text" && ( {message.text} )} {message.contentType === "image" && ( message.uri && onImagePress?.(message.uri)} style={({ pressed }) => [pressed && { opacity: 0.8 }]} > {message.text ? ( {message.text} ) : null} )} {message.contentType === "voice" && ( onPlayVoice?.(message)} style={({ pressed }) => [styles.voiceRow, pressed && { opacity: 0.7 }]} > {waveHeights.map((h, i) => ( ))} {formatDuration(message.duration ?? 0)} )} {message.contentType === "file" && ( {message.fileName ?? "File"} {formatBytes(message.fileSize ?? 0)} )} {timeStr} ); } const styles = StyleSheet.create({ row: { flexDirection: "row", marginBottom: 8, paddingHorizontal: 12, alignItems: "flex-end", }, rowUser: { justifyContent: "flex-end", }, rowAssistant: { justifyContent: "flex-start", }, avatar: { width: 30, height: 30, borderRadius: 15, alignItems: "center", justifyContent: "center", marginRight: 8, }, avatarText: { color: "#fff", fontWeight: "700", fontSize: 14, }, bubble: { maxWidth: "78%", borderRadius: 16, borderWidth: 1, paddingHorizontal: 14, paddingVertical: 10, }, text: { fontSize: 15, lineHeight: 21, }, time: { fontSize: 10, marginTop: 4, textAlign: "right", }, image: { width: 220, height: 180, borderRadius: 10, }, voiceRow: { flexDirection: "row", alignItems: "center", gap: 8, minWidth: 160, }, waveform: { flex: 1, flexDirection: "row", alignItems: "center", gap: 2, height: 24, borderRadius: 4, paddingHorizontal: 4, }, waveBar: { width: 3, borderRadius: 1.5, }, duration: { fontSize: 12, minWidth: 32, }, fileRow: { flexDirection: "row", alignItems: "center", gap: 10, }, fileInfo: { flex: 1, }, fileName: { fontSize: 14, fontWeight: "600", }, fileSize: { fontSize: 11, marginTop: 2, }, });