Matthias Nott
2026-03-07 5db84bd89c8808b0895c7206e8a6a58043f9f8dc
components/chat/MessageBubble.tsx
....@@ -1,5 +1,6 @@
11 import React, { useCallback, useEffect, useState } from "react";
22 import { ActionSheetIOS, Alert, Image, Platform, Pressable, Text, View } from "react-native";
3
+import * as Clipboard from "expo-clipboard";
34 import { Message } from "../../types";
45 import { playSingle, stopPlayback, onPlayingChange } from "../../services/audio";
56 import { ImageViewer } from "./ImageViewer";
....@@ -11,8 +12,8 @@
1112 onPlayVoice?: (id: string) => void;
1213 }
1314
14
-function formatDuration(ms?: number): string {
15
- if (!ms) return "0:00";
15
+function formatDuration(ms?: number): string | null {
16
+ if (!ms || ms <= 0) return null;
1617 const totalSeconds = Math.floor(ms / 1000);
1718 const minutes = Math.floor(totalSeconds / 60);
1819 const seconds = totalSeconds % 60;
....@@ -30,25 +31,32 @@
3031 const { colors, isDark } = useTheme();
3132
3233 const handleLongPress = useCallback(() => {
33
- if (!onDelete) return;
34
+ const hasText = !!message.content;
3435 if (Platform.OS === "ios") {
36
+ const options = ["Cancel"];
37
+ if (hasText) options.push("Copy");
38
+ if (onDelete) options.push("Delete Message");
39
+ const destructiveIndex = onDelete ? options.indexOf("Delete Message") : undefined;
40
+
3541 ActionSheetIOS.showActionSheetWithOptions(
3642 {
37
- options: ["Cancel", "Delete Message"],
38
- destructiveButtonIndex: 1,
43
+ options,
44
+ destructiveButtonIndex: destructiveIndex,
3945 cancelButtonIndex: 0,
4046 },
4147 (index) => {
42
- if (index === 1) onDelete(message.id);
48
+ const selected = options[index];
49
+ if (selected === "Copy") Clipboard.setStringAsync(message.content ?? "");
50
+ else if (selected === "Delete Message") onDelete?.(message.id);
4351 },
4452 );
4553 } else {
46
- Alert.alert("Delete Message", "Remove this message?", [
47
- { text: "Cancel", style: "cancel" },
48
- { text: "Delete", style: "destructive", onPress: () => onDelete(message.id) },
49
- ]);
54
+ const buttons: any[] = [{ text: "Cancel", style: "cancel" }];
55
+ if (hasText) buttons.push({ text: "Copy", onPress: () => Clipboard.setStringAsync(message.content ?? "") });
56
+ if (onDelete) buttons.push({ text: "Delete", style: "destructive", onPress: () => onDelete(message.id) });
57
+ Alert.alert("Message", undefined, buttons);
5058 }
51
- }, [onDelete, message.id]);
59
+ }, [onDelete, message.id, message.content]);
5260
5361 // Track whether THIS bubble's audio is playing via the singleton URI
5462 useEffect(() => {
....@@ -183,14 +191,16 @@
183191 ))}
184192 </View>
185193
186
- <Text
187
- style={{
188
- fontSize: 11,
189
- color: isUser ? "rgba(255,255,255,0.8)" : colors.textSecondary,
190
- }}
191
- >
192
- {formatDuration(message.duration)}
193
- </Text>
194
+ {formatDuration(message.duration) && (
195
+ <Text
196
+ style={{
197
+ fontSize: 11,
198
+ color: isUser ? "rgba(255,255,255,0.8)" : colors.textSecondary,
199
+ }}
200
+ >
201
+ {formatDuration(message.duration)}
202
+ </Text>
203
+ )}
194204 </Pressable>
195205 {message.content ? (
196206 <Text