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