From 5db84bd89c8808b0895c7206e8a6a58043f9f8dc Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 18:04:16 +0100
Subject: [PATCH] feat: heartbeat fix, copy messages, copy/share images, hide unknown duration

---
 components/chat/MessageBubble.tsx |   48 +++++++++++++++++++++++++++++-------------------
 1 files changed, 29 insertions(+), 19 deletions(-)

diff --git a/components/chat/MessageBubble.tsx b/components/chat/MessageBubble.tsx
index e7ad9fb..ae83792 100644
--- a/components/chat/MessageBubble.tsx
+++ b/components/chat/MessageBubble.tsx
@@ -1,5 +1,6 @@
 import React, { useCallback, useEffect, useState } from "react";
 import { ActionSheetIOS, Alert, Image, Platform, Pressable, Text, View } from "react-native";
+import * as Clipboard from "expo-clipboard";
 import { Message } from "../../types";
 import { playSingle, stopPlayback, onPlayingChange } from "../../services/audio";
 import { ImageViewer } from "./ImageViewer";
@@ -11,8 +12,8 @@
   onPlayVoice?: (id: string) => void;
 }
 
-function formatDuration(ms?: number): string {
-  if (!ms) return "0:00";
+function formatDuration(ms?: number): string | null {
+  if (!ms || ms <= 0) return null;
   const totalSeconds = Math.floor(ms / 1000);
   const minutes = Math.floor(totalSeconds / 60);
   const seconds = totalSeconds % 60;
@@ -30,25 +31,32 @@
   const { colors, isDark } = useTheme();
 
   const handleLongPress = useCallback(() => {
-    if (!onDelete) return;
+    const hasText = !!message.content;
     if (Platform.OS === "ios") {
+      const options = ["Cancel"];
+      if (hasText) options.push("Copy");
+      if (onDelete) options.push("Delete Message");
+      const destructiveIndex = onDelete ? options.indexOf("Delete Message") : undefined;
+
       ActionSheetIOS.showActionSheetWithOptions(
         {
-          options: ["Cancel", "Delete Message"],
-          destructiveButtonIndex: 1,
+          options,
+          destructiveButtonIndex: destructiveIndex,
           cancelButtonIndex: 0,
         },
         (index) => {
-          if (index === 1) onDelete(message.id);
+          const selected = options[index];
+          if (selected === "Copy") Clipboard.setStringAsync(message.content ?? "");
+          else if (selected === "Delete Message") onDelete?.(message.id);
         },
       );
     } else {
-      Alert.alert("Delete Message", "Remove this message?", [
-        { text: "Cancel", style: "cancel" },
-        { text: "Delete", style: "destructive", onPress: () => onDelete(message.id) },
-      ]);
+      const buttons: any[] = [{ text: "Cancel", style: "cancel" }];
+      if (hasText) buttons.push({ text: "Copy", onPress: () => Clipboard.setStringAsync(message.content ?? "") });
+      if (onDelete) buttons.push({ text: "Delete", style: "destructive", onPress: () => onDelete(message.id) });
+      Alert.alert("Message", undefined, buttons);
     }
-  }, [onDelete, message.id]);
+  }, [onDelete, message.id, message.content]);
 
   // Track whether THIS bubble's audio is playing via the singleton URI
   useEffect(() => {
@@ -183,14 +191,16 @@
                 ))}
               </View>
 
-              <Text
-                style={{
-                  fontSize: 11,
-                  color: isUser ? "rgba(255,255,255,0.8)" : colors.textSecondary,
-                }}
-              >
-                {formatDuration(message.duration)}
-              </Text>
+              {formatDuration(message.duration) && (
+                <Text
+                  style={{
+                    fontSize: 11,
+                    color: isUser ? "rgba(255,255,255,0.8)" : colors.textSecondary,
+                  }}
+                >
+                  {formatDuration(message.duration)}
+                </Text>
+              )}
             </Pressable>
             {message.content ? (
               <Text

--
Gitblit v1.3.1