/** * MessageActionMenu — WhatsApp-Style Long-Press-Kontextmenü für Chat-Bubbles. * * Ersetzt den zentriert gerenderten @expo/react-native-action-sheet. Wird an * der gedrückten Bubble verankert (per measureInWindow gelieferte `anchor`-Rect): * - Emoji-Reaktions-Leiste OBEN (nur bei fremden Nachrichten) * - Aktions-Liste UNTEN (fremd: Antworten + Kopieren / eigen: Kopieren + Löschen) * Blur-Backdrop (iOS) / semi-transparent (Android). Smart-Position: Menü unter * der Bubble, oder darüber wenn unten kein Platz ist. */ import { useMemo, type ReactNode } from 'react'; import { Dimensions, Modal, Platform, Pressable, StyleSheet, Text, TouchableOpacity, View, useColorScheme, } from 'react-native'; import { BlurView } from 'expo-blur'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { useColors } from '../../lib/theme'; const IS_IOS = Platform.OS === 'ios'; const REACTION_EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '🙏', '👏']; export type AnchorRect = { x: number; y: number; width: number; height: number }; type Props = { visible: boolean; anchor: AnchorRect | null; isOwn: boolean; hasContent: boolean; /** Aktuelles eigenes Reaktions-Emoji auf dieser Message (für Highlight). */ myReaction?: string | null; /** Scharfe Kopie der gedrückten Bubble — bleibt über dem Blur sichtbar (WA-Stil). */ preview?: ReactNode; onClose: () => void; onReact: (emoji: string) => void; onReply: () => void; onCopy: () => void; onDelete: () => void; }; export function MessageActionMenu({ visible, anchor, isOwn, hasContent, myReaction, preview, onClose, onReact, onReply, onCopy, onDelete, }: Props) { const { t } = useTranslation(); const colors = useColors(); const scheme = useColorScheme(); const actions = useMemo(() => { const list: { key: string; label: string; icon: React.ComponentProps['name']; danger?: boolean; onPress: () => void; }[] = []; if (!isOwn) { list.push({ key: 'reply', label: t('chat.reply'), icon: 'arrow-undo-outline', onPress: onReply }); } if (hasContent) { list.push({ key: 'copy', label: t('chat.copy'), icon: 'copy-outline', onPress: onCopy }); } if (isOwn) { list.push({ key: 'delete', label: t('chat.delete'), icon: 'trash-outline', danger: true, onPress: onDelete }); } return list; }, [isOwn, hasContent, t, onReply, onCopy, onDelete]); if (!anchor) return null; const screenW = Dimensions.get('window').width; const screenH = Dimensions.get('window').height; const topSafe = 60; const showReactions = !isOwn; const barH = showReactions ? 60 : 0; const estMenuH = actions.length * 52 + 12; // Input-Bar + Safe-Area am unteren Bildschirmrand (Popup darf nie darunter) const bottomInset = 90; const usableH = screenH - bottomInset; // Menü unter der Bubble wenn genug Platz, sonst darüber — Input-Bar berücksichtigt const belowSpace = usableH - (anchor.y + anchor.height); const placeBelow = belowSpace >= estMenuH + 8; const menuTopIdeal = placeBelow ? anchor.y + anchor.height + 8 : anchor.y - estMenuH - 8; // Clampen: nie über Safe-Area oben, nie unter Input-Bar unten const menuTop = Math.min( Math.max(topSafe + barH + 8, menuTopIdeal), usableH - estMenuH, ); // Reaktions-Leiste über der Bubble — nie über topSafe, nie das Menü überlappen const barTopIdeal = Math.max(topSafe, anchor.y - barH - 4); const barTop = Math.min(barTopIdeal, menuTop - barH - 8); // Horizontale Ausrichtung an der Bubble-Seite. const sideStyle = isOwn ? { right: Math.max(12, screenW - (anchor.x + anchor.width)) } : { left: Math.max(12, anchor.x) }; return ( {IS_IOS ? ( ) : ( )} {/* Scharfe Kopie der gedrückten Bubble — bleibt über dem Blur sichtbar (WA-Stil) */} {preview && ( {preview} )} {/* Emoji-Reaktions-Leiste (nur fremde Nachrichten) */} {showReactions && ( {REACTION_EMOJIS.map((emoji) => ( { onReact(emoji); onClose(); }} style={styles.reactionBtn} > {emoji} ))} )} {/* Aktions-Liste */} {actions.map((a, i) => ( { a.onPress(); onClose(); }} style={[ styles.menuItem, i < actions.length - 1 && { borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.border }, ]} > {a.label} ))} ); } const styles = StyleSheet.create({ reactionBar: { position: 'absolute', flexDirection: 'row', alignItems: 'center', borderRadius: 26, paddingHorizontal: 6, paddingVertical: 5, gap: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.15, shadowRadius: 12, elevation: 8, }, reactionBtn: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', }, reactionEmoji: { fontSize: 26 }, reactionEmojiActive: { fontSize: 30 }, menu: { position: 'absolute', minWidth: 200, borderRadius: 14, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.16, shadowRadius: 16, elevation: 10, }, menuItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 18, paddingVertical: 15, }, menuLabel: { fontSize: 15, fontFamily: 'Nunito_600SemiBold' }, });