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