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,
},
});
}