| .. | .. |
|---|
| 1 | 1 | import React, { useCallback, useEffect, useState } from "react"; |
|---|
| 2 | | -import { Image, Pressable, Text, View } from "react-native"; |
|---|
| 2 | +import { ActionSheetIOS, Alert, Image, Platform, Pressable, Text, View } from "react-native"; |
|---|
| 3 | 3 | import { Message } from "../../types"; |
|---|
| 4 | 4 | import { playSingle, stopPlayback, onPlayingChange } from "../../services/audio"; |
|---|
| 5 | 5 | import { ImageViewer } from "./ImageViewer"; |
|---|
| .. | .. |
|---|
| 7 | 7 | |
|---|
| 8 | 8 | interface MessageBubbleProps { |
|---|
| 9 | 9 | message: Message; |
|---|
| 10 | + onDelete?: (id: string) => void; |
|---|
| 11 | + onPlayVoice?: (id: string) => void; |
|---|
| 10 | 12 | } |
|---|
| 11 | 13 | |
|---|
| 12 | 14 | function formatDuration(ms?: number): string { |
|---|
| .. | .. |
|---|
| 22 | 24 | return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); |
|---|
| 23 | 25 | } |
|---|
| 24 | 26 | |
|---|
| 25 | | -export function MessageBubble({ message }: MessageBubbleProps) { |
|---|
| 27 | +export function MessageBubble({ message, onDelete, onPlayVoice }: MessageBubbleProps) { |
|---|
| 26 | 28 | const [isPlaying, setIsPlaying] = useState(false); |
|---|
| 27 | 29 | const [showViewer, setShowViewer] = useState(false); |
|---|
| 28 | 30 | const { colors, isDark } = useTheme(); |
|---|
| 31 | + |
|---|
| 32 | + const handleLongPress = useCallback(() => { |
|---|
| 33 | + if (!onDelete) return; |
|---|
| 34 | + if (Platform.OS === "ios") { |
|---|
| 35 | + ActionSheetIOS.showActionSheetWithOptions( |
|---|
| 36 | + { |
|---|
| 37 | + options: ["Cancel", "Delete Message"], |
|---|
| 38 | + destructiveButtonIndex: 1, |
|---|
| 39 | + cancelButtonIndex: 0, |
|---|
| 40 | + }, |
|---|
| 41 | + (index) => { |
|---|
| 42 | + if (index === 1) onDelete(message.id); |
|---|
| 43 | + }, |
|---|
| 44 | + ); |
|---|
| 45 | + } else { |
|---|
| 46 | + Alert.alert("Delete Message", "Remove this message?", [ |
|---|
| 47 | + { text: "Cancel", style: "cancel" }, |
|---|
| 48 | + { text: "Delete", style: "destructive", onPress: () => onDelete(message.id) }, |
|---|
| 49 | + ]); |
|---|
| 50 | + } |
|---|
| 51 | + }, [onDelete, message.id]); |
|---|
| 29 | 52 | |
|---|
| 30 | 53 | // Track whether THIS bubble's audio is playing via the singleton URI |
|---|
| 31 | 54 | useEffect(() => { |
|---|
| .. | .. |
|---|
| 41 | 64 | if (!message.audioUri) return; |
|---|
| 42 | 65 | |
|---|
| 43 | 66 | if (isPlaying) { |
|---|
| 44 | | - // This bubble is playing — stop it |
|---|
| 45 | 67 | await stopPlayback(); |
|---|
| 68 | + } else if (onPlayVoice) { |
|---|
| 69 | + // Let parent handle chain playback (plays this + subsequent chunks) |
|---|
| 70 | + onPlayVoice(message.id); |
|---|
| 46 | 71 | } else { |
|---|
| 47 | | - // Play this bubble (stops anything else automatically) |
|---|
| 48 | 72 | await playSingle(message.audioUri, () => {}); |
|---|
| 49 | 73 | } |
|---|
| 50 | | - }, [isPlaying, message.audioUri]); |
|---|
| 74 | + }, [isPlaying, message.audioUri, onPlayVoice, message.id]); |
|---|
| 51 | 75 | |
|---|
| 52 | 76 | if (isSystem) { |
|---|
| 53 | 77 | return ( |
|---|
| .. | .. |
|---|
| 65 | 89 | : { borderTopLeftRadius: 4 }; |
|---|
| 66 | 90 | |
|---|
| 67 | 91 | return ( |
|---|
| 68 | | - <View |
|---|
| 92 | + <Pressable |
|---|
| 93 | + onLongPress={handleLongPress} |
|---|
| 94 | + delayLongPress={500} |
|---|
| 69 | 95 | style={{ |
|---|
| 70 | 96 | flexDirection: "row", |
|---|
| 71 | 97 | marginVertical: 4, |
|---|
| .. | .. |
|---|
| 213 | 239 | )} |
|---|
| 214 | 240 | </View> |
|---|
| 215 | 241 | </View> |
|---|
| 216 | | - </View> |
|---|
| 242 | + </Pressable> |
|---|
| 217 | 243 | ); |
|---|
| 218 | 244 | } |
|---|