import { View, Text, TouchableOpacity, StyleSheet, } from 'react-native'; import { Image } from 'expo-image'; import * as Clipboard from 'expo-clipboard'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { useActionSheet } from '@expo/react-native-action-sheet'; import { useColors } from '../../lib/theme'; import { useThemeStore } from '../../stores/theme'; import { UserAvatar } from '../UserAvatar'; export type ChatMsg = { id: string; userId: string; nickname?: string | null; avatar?: string | null; content: string; replyTo?: { id: string; userId: string; nickname?: string | null; content: string; attachmentType?: string | null; } | null; attachmentUrl?: string | null; attachmentType?: string | null; attachmentName?: string | null; likesCount: number; likedByMe?: boolean; createdAt: string; isOwn: boolean; readAt?: string | null; }; type Props = { msg: ChatMsg; showName?: boolean; isFirstInGroup?: boolean; isLastInGroup?: boolean; hideReadStatus?: boolean; /** Direct-Message-Mode: Likes als boolean-Herz (Insta-Style) statt Count, kein Avatar-Spalte-Whatever */ isDM?: boolean; onReply: (msg: ChatMsg) => void; onLike: (msg: ChatMsg) => void; onOpenImage: (url: string) => void; }; function formatTime(ts: string) { return new Date(ts).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); } function useBubbleColors() { const colorScheme = useThemeStore((s) => s.colorScheme); const isDark = colorScheme === 'dark'; return { ownBg: isDark ? '#1e4d3a' : '#D1F4CC', ownText: isDark ? '#e8f5e2' : '#0a0a0a', otherBg: isDark ? '#2c2c2e' : '#ffffff', otherText: isDark ? '#ffffff' : '#0a0a0a', replyBarColor: '#25D366', readColor: '#34B7F1', }; } export function ChatBubble({ msg, showName = false, isFirstInGroup = true, isLastInGroup = true, hideReadStatus = false, isDM = false, onReply, onLike, onOpenImage, }: Props) { const { t } = useTranslation(); const colors = useColors(); const styles = makeStyles(colors); const bubbleColors = useBubbleColors(); const { showActionSheetWithOptions } = useActionSheet(); function openActions() { const hasContent = msg.content !== ''; const likeLabel = msg.likedByMe ? t('chat.unlike') : t('chat.like'); const options: string[] = [t('chat.reply'), likeLabel]; if (hasContent) options.push(t('chat.copy')); options.push(t('common.cancel')); const cancelButtonIndex = options.length - 1; showActionSheetWithOptions( { options, cancelButtonIndex, title: hasContent ? msg.content.length > 60 ? msg.content.slice(0, 60) + '…' : msg.content : undefined, }, (selected?: number) => { if (selected === undefined || selected === cancelButtonIndex) return; if (selected === 0) onReply(msg); else if (selected === 1) onLike(msg); else if (selected === 2 && hasContent) copyContent(); }, ); } const isImageOnly = !!msg.attachmentUrl && msg.attachmentType === 'image' && !msg.content && !msg.replyTo; const replyHasAttachment = msg.replyTo?.attachmentType === 'image'; const ownBubbleRadius = { borderTopLeftRadius: 14, borderTopRightRadius: isFirstInGroup ? 14 : 4, borderBottomLeftRadius: 14, borderBottomRightRadius: isLastInGroup ? 4 : 4, }; const otherBubbleRadius = { borderTopLeftRadius: isFirstInGroup ? 14 : 4, borderTopRightRadius: 14, borderBottomLeftRadius: isLastInGroup ? 4 : 4, borderBottomRightRadius: 14, }; const bubbleBg = msg.isOwn ? bubbleColors.ownBg : bubbleColors.otherBg; const bubbleText = msg.isOwn ? bubbleColors.ownText : bubbleColors.otherText; function copyContent() { if (msg.content) Clipboard.setStringAsync(msg.content); } return ( <> {!msg.isOwn && ( {isLastInGroup ? ( ) : null} )} {showName && !msg.isOwn && isFirstInGroup && ( {msg.nickname ?? '?'} )} {msg.replyTo && ( {msg.replyTo.nickname ?? '?'} {replyHasAttachment && ( )}{' '} {msg.replyTo.content || (replyHasAttachment ? t('chat.image_attachment') : '…')} )} {msg.attachmentUrl && msg.attachmentType === 'image' && ( onOpenImage(msg.attachmentUrl!)} activeOpacity={0.7} style={[styles.imageWrap, msg.content ? { marginBottom: 4 } : null]} > {isImageOnly && ( {!isDM && msg.likesCount > 0 && ( {msg.likesCount} )} {formatTime(msg.createdAt)} )} )} {msg.attachmentUrl && msg.attachmentType !== 'image' && ( {msg.attachmentName ?? t('chat.file_attachment')} )} {msg.content !== '' && ( {msg.content} )} {!isImageOnly && ( {!isDM && msg.likesCount > 0 && ( {msg.likesCount} )} {formatTime(msg.createdAt)} {msg.isOwn && !hideReadStatus && ( )} )} {/* Insta-Style: kleines Herz-Badge hängt unter der Bubble (nur DM, nur wenn liked) */} {isDM && msg.likedByMe && ( onLike(msg)} activeOpacity={0.7} hitSlop={8} style={[ styles.dmHeartBadge, { alignSelf: msg.isOwn ? 'flex-end' : 'flex-start', marginRight: msg.isOwn ? 8 : 0, marginLeft: msg.isOwn ? 0 : 8, }, ]} > )} ); } function makeStyles(colors: ReturnType) { return StyleSheet.create({ row: { flexDirection: 'row', paddingHorizontal: 10, }, avatarSlot: { width: 32, marginRight: 6, justifyContent: 'flex-end', }, bubbleCol: { maxWidth: '76%', }, nickname: { fontSize: 11, fontFamily: 'Nunito_700Bold', color: '#25D366', marginBottom: 3, marginLeft: 12, }, bubble: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 6, shadowColor: '#000', shadowOpacity: 0.08, shadowRadius: 3, shadowOffset: { width: 0, height: 1 }, elevation: 1, }, bubbleOtherBorder: { borderWidth: StyleSheet.hairlineWidth, borderColor: 'rgba(0,0,0,0.06)', }, replyPreview: { borderLeftWidth: 4, borderRadius: 6, paddingHorizontal: 8, paddingVertical: 4, marginBottom: 6, }, imageWrap: { borderRadius: 10, overflow: 'hidden', position: 'relative', }, image: { width: 220, height: 220, backgroundColor: colors.surfaceElevated, }, imageTimeOverlay: { position: 'absolute', bottom: 6, right: 6, backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 10, paddingHorizontal: 6, paddingVertical: 2, flexDirection: 'row', alignItems: 'center', }, content: { fontSize: 14, lineHeight: 21, fontFamily: 'Nunito_400Regular', }, footer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 3, marginTop: 4, alignSelf: 'flex-end', }, dmHeartBadge: { marginTop: -6, backgroundColor: colors.bg, borderRadius: 999, paddingHorizontal: 4, paddingVertical: 3, shadowColor: '#000', shadowOpacity: 0.12, shadowOffset: { width: 0, height: 1 }, shadowRadius: 2, elevation: 2, }, }); }