/** * 长按菜单组件 - 气泡旁边弹出形式(类似手机QQ) */ import React, { useRef, useEffect } from 'react'; import { View, Modal, TouchableOpacity, Pressable, Animated, Alert, Clipboard, Dimensions, Platform, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useChatScreenStyles } from './styles'; import { LongPressMenuProps } from './types'; import { RECALL_TIME_LIMIT } from './constants'; import { extractTextFromSegments, ImageSegmentData } from '../../../../types/dto'; import { isStickerExists, addStickerFromUrl } from '@/services/message'; import { blurActiveElement } from '../../../../infrastructure/platform'; const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); export const LongPressMenu: React.FC = ({ visible, message, currentUserId, position, onClose, onReply, onRecall, onDelete, onAddSticker, onReport, memberMap, }) => { const styles = useChatScreenStyles(); const scaleAnimation = useRef(new Animated.Value(0)).current; const opacityAnimation = useRef(new Animated.Value(0)).current; // 显示动画 - 缩放弹出 useEffect(() => { if (visible) { blurActiveElement(); Animated.parallel([ Animated.spring(scaleAnimation, { toValue: 1, useNativeDriver: Platform.OS !== 'web', friction: 8, tension: 100, }), Animated.timing(opacityAnimation, { toValue: 1, duration: 150, useNativeDriver: Platform.OS !== 'web', }), ]).start(); } else { blurActiveElement(); Animated.parallel([ Animated.timing(scaleAnimation, { toValue: 0, duration: 150, useNativeDriver: Platform.OS !== 'web', }), Animated.timing(opacityAnimation, { toValue: 0, duration: 150, useNativeDriver: Platform.OS !== 'web', }), ]).start(); } }, [visible, scaleAnimation, opacityAnimation]); if (!message) return null; const isMyMessage = message.sender_id === currentUserId; const messageDate = new Date(message.created_at); const messageTime = Number.isNaN(messageDate.getTime()) ? 0 : messageDate.getTime(); const now = Date.now(); const canRecall = isMyMessage && message.status !== 'recalled' && messageTime > 0 && (now - messageTime) < RECALL_TIME_LIMIT; // 检查是否是图片消息 const segments = Array.isArray(message.segments) ? message.segments : []; const imageSegment = segments.find(s => s.type === 'image'); const isImageMessage = !!imageSegment; const imageUrl = imageSegment ? (imageSegment.data as ImageSegmentData)?.url : null; // 复制文本到剪贴板 const handleCopy = () => { const text = extractTextFromSegments(message.segments, memberMap); if (text) { Clipboard.setString(text); onClose(); } }; // 多选功能(这里简化为复制全部) const handleMultiSelect = () => { const text = extractTextFromSegments(message.segments, memberMap); if (text) { Clipboard.setString(text); onClose(); } }; const handleDelete = () => { Alert.alert( '确认删除', '删除后,该消息将从你的设备上移除。确定要删除吗?', [ { text: '取消', style: 'cancel' }, { text: '删除', style: 'destructive', onPress: () => { onDelete(message.id); onClose(); }, }, ] ); }; // 添加到表情 const handleAddSticker = async () => { if (!imageUrl) return; onClose(); // 检查是否已存在 const exists = await isStickerExists(imageUrl); if (exists) { Alert.alert('提示', '该表情已存在'); return; } try { const sticker = await addStickerFromUrl(imageUrl); if (sticker) { Alert.alert('成功', '已添加到表情'); onAddSticker?.(imageUrl); } else { Alert.alert('提示', '该表情已存在'); } } catch (error) { Alert.alert('失败', '添加表情失败,请重试'); } }; // 菜单项配置 - 类似QQ的横向排列 const menuItems = [ { id: 'reply', label: '回复', icon: 'reply', onPress: () => { onReply(message); onClose(); }, show: true, }, { id: 'copy', label: '复制', icon: 'content-copy', onPress: handleCopy, show: !message.is_system_notice && message.status !== 'recalled', }, { id: 'multiSelect', label: '多选', icon: 'checkbox-multiple-marked-outline', onPress: handleMultiSelect, show: true, }, { id: 'addSticker', label: '添加表情', icon: 'emoticon-happy-outline', onPress: handleAddSticker, show: isImageMessage && message.status !== 'recalled', }, { id: 'recall', label: '撤回', icon: 'undo', onPress: () => { onRecall(message.id); onClose(); }, show: canRecall, }, { id: 'delete', label: '删除', icon: 'delete-outline', onPress: handleDelete, show: true, }, ].filter(item => item.show); // 计算菜单位置 - 紧贴手指位置显示在气泡上方 const calculatePosition = () => { if (!position) { // 默认居中显示 return { top: SCREEN_HEIGHT / 2 - 50, left: SCREEN_WIDTH / 2 - 120, }; } const menuWidth = Math.max(menuItems.length * 56, 180); const menuHeight = 54; // 使用按压点位置(手指位置) const pressX = position.pressX ?? position.x + position.width / 2; const pressY = position.pressY ?? position.y; // 在手指位置上方显示菜单,留出 8px 间距 let top = pressY - menuHeight - 8; // 水平居中于手指位置 let left = pressX - menuWidth / 2; // 如果上方空间不足,显示在手指位置下方 if (top < 50) { top = pressY + 8; } // 确保不超出屏幕左右边界 const minLeft = 8; const maxLeft = SCREEN_WIDTH - menuWidth - 8; if (left < minLeft) left = minLeft; if (left > maxLeft) left = maxLeft; return { top, left }; }; const { top, left } = calculatePosition(); return ( { blurActiveElement(); onClose(); }} style={styles.qqMenuOverlay} > {}}> {/* 操作按钮横向排列 */} {menuItems.map((item, index) => ( {item.label} ))} ); }; export default LongPressMenu;