Messenger/packages/mobile/src/features/messaging/ui/MessageBubble.tsx
Заид Омар Медхат | Zaid Omar Medhat f653b083aa f
2026-07-13 12:32:00 +05:00

149 lines
4.8 KiB
TypeScript

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 (
<View style={[styles.line, outgoing ? styles.lineOut : styles.lineIn]}>
<Pressable
onLongPress={() => {
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 ? (
<ForwardedHeader origin={message.forwardedFrom} outgoing={outgoing} onPressUser={onPressUser} />
) : null}
{showSender ? (
<Text style={[styles.sender, { color: colors.accent }]} numberOfLines={1}>
{message.sender.displayName}
</Text>
) : null}
{message.replyTo !== null ? (
<ReplyQuote
reply={message.replyTo}
outgoing={outgoing}
onPress={() => {
if (message.replyTo !== null) {
onReplyPress(message.replyTo.id);
}
}}
/>
) : null}
{message.media !== null && !deleted ? (
<View style={styles.media}>
<MediaContent
conversationId={message.conversationId}
messageId={message.id}
media={message.media}
outgoing={outgoing}
onOpenViewer={onOpenViewer}
/>
</View>
) : null}
{deleted ? (
<Text style={[styles.deleted, { color: outgoing ? colors.onAccentMuted : colors.textFaint }]}>
Message deleted
</Text>
) : hasText ? (
<Text style={[styles.text, { color: textColor }]}>{message.content}</Text>
) : null}
<MessageMeta
createdAt={message.createdAt}
edited={message.editedAt !== null}
outgoing={outgoing}
status={statusOf(message, outgoing, peerReadSeq)}
/>
</Pressable>
{!deleted ? (
<Reactions
reactions={message.reactions}
outgoing={outgoing}
onToggle={(emoji) => {
onToggleReaction(message, emoji);
}}
/>
) : null}
</View>
);
};
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 },
});