Matthias Nott
2026-03-07 c23dfe16e95713e7058137308bdbc28419609a39
components/chat/MessageBubble.tsx
....@@ -1,5 +1,5 @@
11 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";
33 import { Message } from "../../types";
44 import { playSingle, stopPlayback, onPlayingChange } from "../../services/audio";
55 import { ImageViewer } from "./ImageViewer";
....@@ -7,6 +7,8 @@
77
88 interface MessageBubbleProps {
99 message: Message;
10
+ onDelete?: (id: string) => void;
11
+ onPlayVoice?: (id: string) => void;
1012 }
1113
1214 function formatDuration(ms?: number): string {
....@@ -22,10 +24,31 @@
2224 return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
2325 }
2426
25
-export function MessageBubble({ message }: MessageBubbleProps) {
27
+export function MessageBubble({ message, onDelete, onPlayVoice }: MessageBubbleProps) {
2628 const [isPlaying, setIsPlaying] = useState(false);
2729 const [showViewer, setShowViewer] = useState(false);
2830 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]);
2952
3053 // Track whether THIS bubble's audio is playing via the singleton URI
3154 useEffect(() => {
....@@ -41,13 +64,14 @@
4164 if (!message.audioUri) return;
4265
4366 if (isPlaying) {
44
- // This bubble is playing — stop it
4567 await stopPlayback();
68
+ } else if (onPlayVoice) {
69
+ // Let parent handle chain playback (plays this + subsequent chunks)
70
+ onPlayVoice(message.id);
4671 } else {
47
- // Play this bubble (stops anything else automatically)
4872 await playSingle(message.audioUri, () => {});
4973 }
50
- }, [isPlaying, message.audioUri]);
74
+ }, [isPlaying, message.audioUri, onPlayVoice, message.id]);
5175
5276 if (isSystem) {
5377 return (
....@@ -65,7 +89,9 @@
6589 : { borderTopLeftRadius: 4 };
6690
6791 return (
68
- <View
92
+ <Pressable
93
+ onLongPress={handleLongPress}
94
+ delayLongPress={500}
6995 style={{
7096 flexDirection: "row",
7197 marginVertical: 4,
....@@ -213,6 +239,6 @@
213239 )}
214240 </View>
215241 </View>
216
- </View>
242
+ </Pressable>
217243 );
218244 }