import { memo } from 'react'; import type { ReactElement } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import type { Message } from '@altricade/core'; import { OPTIMISTIC_SEQ } from '@altricade/core'; import { useTheme } from '@/theme'; import { spacing, radius, fontSize } from '@/theme'; import { MessageMeta } from './MessageMeta'; import type { SendStatus } from './MessageMeta'; import { ReplyQuote } from './ReplyQuote'; import { ForwardedHeader } from './ForwardedHeader'; import { Reactions } from './Reactions'; import { MediaContent } from './MediaContent'; export interface BubbleCallbacks { onLongPress: (message: Message) => void; onReplyPress: (messageId: string) => void; onToggleReaction: (message: Message, emoji: string) => void; onOpenViewer: (url: string, isVideo: boolean) => void; onPressUser: (username: string) => void; } interface Props extends BubbleCallbacks { message: Message; meId: string; isGroup: boolean; peerReadSeq: number; highlighted: boolean; } const statusOf = (message: Message, outgoing: boolean, peerReadSeq: number): SendStatus => { if (message.seq === OPTIMISTIC_SEQ) return 'pending'; if (outgoing && peerReadSeq >= message.seq) return 'read'; return 'sent'; }; const MessageBubbleInner = ({ message, meId, isGroup, peerReadSeq, highlighted, onLongPress, onReplyPress, onToggleReaction, onOpenViewer, onPressUser, }: Props): ReactElement => { const { colors } = useTheme(); const outgoing = message.senderId === meId; const deleted = message.deletedAt !== null; const showSender = isGroup && !outgoing && !deleted; const bubbleColor = outgoing ? colors.bubbleOut : colors.bubbleIn; const textColor = outgoing ? colors.onAccent : colors.text; const hasText = message.content.length > 0 && !deleted; return ( { if (!deleted) { onLongPress(message); } }} delayLongPress={280} style={[ styles.bubble, { backgroundColor: bubbleColor, borderColor: colors.border }, outgoing ? styles.bubbleOut : styles.bubbleIn, highlighted && { borderColor: colors.accent, borderWidth: 2 }, ]} > {message.forwarded ? ( ) : null} {showSender ? ( {message.sender.displayName} ) : null} {message.replyTo !== null ? ( { if (message.replyTo !== null) { onReplyPress(message.replyTo.id); } }} /> ) : null} {message.media !== null && !deleted ? ( ) : null} {deleted ? ( Message deleted ) : hasText ? ( {message.content} ) : null} {!deleted ? ( { onToggleReaction(message, emoji); }} /> ) : null} ); }; export const MessageBubble = memo(MessageBubbleInner); const styles = StyleSheet.create({ line: { paddingHorizontal: spacing.md, marginVertical: 2, maxWidth: '100%' }, lineOut: { alignItems: 'flex-end' }, lineIn: { alignItems: 'flex-start' }, bubble: { maxWidth: '82%', paddingHorizontal: spacing.md, paddingVertical: spacing.sm, borderRadius: radius.lg, gap: 2, }, bubbleOut: { borderBottomRightRadius: radius.sm }, bubbleIn: { borderBottomLeftRadius: radius.sm, borderWidth: StyleSheet.hairlineWidth }, sender: { fontSize: fontSize.sm, fontWeight: '700', marginBottom: 1 }, text: { fontSize: fontSize.base, lineHeight: 21 }, deleted: { fontSize: fontSize.base, fontStyle: 'italic' }, media: { marginBottom: 2 }, });