From c23dfe16e95713e7058137308bdbc28419609a39 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 11:54:15 +0100
Subject: [PATCH] feat: typing indicator, message deletion, chain playback, autoplay guard

---
 components/chat/MessageBubble.tsx |   40 +++++++++++++++++++++++++++++++++-------
 1 files changed, 33 insertions(+), 7 deletions(-)

diff --git a/components/chat/MessageBubble.tsx b/components/chat/MessageBubble.tsx
index 5edf2f8..e7ad9fb 100644
--- a/components/chat/MessageBubble.tsx
+++ b/components/chat/MessageBubble.tsx
@@ -1,5 +1,5 @@
 import React, { useCallback, useEffect, useState } from "react";
-import { Image, Pressable, Text, View } from "react-native";
+import { ActionSheetIOS, Alert, Image, Platform, Pressable, Text, View } from "react-native";
 import { Message } from "../../types";
 import { playSingle, stopPlayback, onPlayingChange } from "../../services/audio";
 import { ImageViewer } from "./ImageViewer";
@@ -7,6 +7,8 @@
 
 interface MessageBubbleProps {
   message: Message;
+  onDelete?: (id: string) => void;
+  onPlayVoice?: (id: string) => void;
 }
 
 function formatDuration(ms?: number): string {
@@ -22,10 +24,31 @@
   return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
 }
 
-export function MessageBubble({ message }: MessageBubbleProps) {
+export function MessageBubble({ message, onDelete, onPlayVoice }: MessageBubbleProps) {
   const [isPlaying, setIsPlaying] = useState(false);
   const [showViewer, setShowViewer] = useState(false);
   const { colors, isDark } = useTheme();
+
+  const handleLongPress = useCallback(() => {
+    if (!onDelete) return;
+    if (Platform.OS === "ios") {
+      ActionSheetIOS.showActionSheetWithOptions(
+        {
+          options: ["Cancel", "Delete Message"],
+          destructiveButtonIndex: 1,
+          cancelButtonIndex: 0,
+        },
+        (index) => {
+          if (index === 1) onDelete(message.id);
+        },
+      );
+    } else {
+      Alert.alert("Delete Message", "Remove this message?", [
+        { text: "Cancel", style: "cancel" },
+        { text: "Delete", style: "destructive", onPress: () => onDelete(message.id) },
+      ]);
+    }
+  }, [onDelete, message.id]);
 
   // Track whether THIS bubble's audio is playing via the singleton URI
   useEffect(() => {
@@ -41,13 +64,14 @@
     if (!message.audioUri) return;
 
     if (isPlaying) {
-      // This bubble is playing — stop it
       await stopPlayback();
+    } else if (onPlayVoice) {
+      // Let parent handle chain playback (plays this + subsequent chunks)
+      onPlayVoice(message.id);
     } else {
-      // Play this bubble (stops anything else automatically)
       await playSingle(message.audioUri, () => {});
     }
-  }, [isPlaying, message.audioUri]);
+  }, [isPlaying, message.audioUri, onPlayVoice, message.id]);
 
   if (isSystem) {
     return (
@@ -65,7 +89,9 @@
     : { borderTopLeftRadius: 4 };
 
   return (
-    <View
+    <Pressable
+      onLongPress={handleLongPress}
+      delayLongPress={500}
       style={{
         flexDirection: "row",
         marginVertical: 4,
@@ -213,6 +239,6 @@
           )}
         </View>
       </View>
-    </View>
+    </Pressable>
   );
 }

--
Gitblit v1.3.1