| .. | .. |
|---|
| 1 | 1 | import React, { useCallback, useEffect, useState } from "react"; |
|---|
| 2 | 2 | import { ActionSheetIOS, Alert, Image, Platform, Pressable, Text, View } from "react-native"; |
|---|
| 3 | +import * as Clipboard from "expo-clipboard"; |
|---|
| 3 | 4 | import { Message } from "../../types"; |
|---|
| 4 | 5 | import { playSingle, stopPlayback, onPlayingChange } from "../../services/audio"; |
|---|
| 5 | 6 | import { ImageViewer } from "./ImageViewer"; |
|---|
| .. | .. |
|---|
| 11 | 12 | onPlayVoice?: (id: string) => void; |
|---|
| 12 | 13 | } |
|---|
| 13 | 14 | |
|---|
| 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; |
|---|
| 16 | 17 | const totalSeconds = Math.floor(ms / 1000); |
|---|
| 17 | 18 | const minutes = Math.floor(totalSeconds / 60); |
|---|
| 18 | 19 | const seconds = totalSeconds % 60; |
|---|
| .. | .. |
|---|
| 30 | 31 | const { colors, isDark } = useTheme(); |
|---|
| 31 | 32 | |
|---|
| 32 | 33 | const handleLongPress = useCallback(() => { |
|---|
| 33 | | - if (!onDelete) return; |
|---|
| 34 | + const hasText = !!message.content; |
|---|
| 34 | 35 | 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 | + |
|---|
| 35 | 41 | ActionSheetIOS.showActionSheetWithOptions( |
|---|
| 36 | 42 | { |
|---|
| 37 | | - options: ["Cancel", "Delete Message"], |
|---|
| 38 | | - destructiveButtonIndex: 1, |
|---|
| 43 | + options, |
|---|
| 44 | + destructiveButtonIndex: destructiveIndex, |
|---|
| 39 | 45 | cancelButtonIndex: 0, |
|---|
| 40 | 46 | }, |
|---|
| 41 | 47 | (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); |
|---|
| 43 | 51 | }, |
|---|
| 44 | 52 | ); |
|---|
| 45 | 53 | } 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); |
|---|
| 50 | 58 | } |
|---|
| 51 | | - }, [onDelete, message.id]); |
|---|
| 59 | + }, [onDelete, message.id, message.content]); |
|---|
| 52 | 60 | |
|---|
| 53 | 61 | // Track whether THIS bubble's audio is playing via the singleton URI |
|---|
| 54 | 62 | useEffect(() => { |
|---|
| .. | .. |
|---|
| 183 | 191 | ))} |
|---|
| 184 | 192 | </View> |
|---|
| 185 | 193 | |
|---|
| 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 | + )} |
|---|
| 194 | 204 | </Pressable> |
|---|
| 195 | 205 | {message.content ? ( |
|---|
| 196 | 206 | <Text |
|---|