import { Pressable } from '@ankhorage/surface'; import React from 'react'; import type { MessageBubbleAvatar, MessageBubbleDirection, MessageBubbleProps, MessageBubbleStatus, } from '../../../../types/chat'; import { Avatar } from '../../../avatar/public'; import { View } from '../../../layout/public'; import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope'; import { useZoraTheme } from '../../../theme/composition/useZoraTheme'; import { Text } from '../../../typography/public'; function resolveAvatarName({ avatar, authorName, }: { avatar: MessageBubbleAvatar | undefined; authorName: React.ReactNode | undefined; }): string | undefined { if (avatar?.name) return avatar.name; return typeof authorName === 'string' ? authorName : undefined; } function resolvePadding(compact: boolean) { return compact ? { px: 'm' as const, py: 's' as const } : { px: 'm' as const, py: 'm' as const }; } function resolveStatusLabel(status: MessageBubbleStatus): string { switch (status) { case 'sending': return 'Sending'; case 'sent': return 'Sent'; case 'delivered': return 'Delivered'; case 'read': return 'Read'; case 'failed': return 'Failed'; default: return status; } } function isMessageBubbleStatus( status: MessageBubbleProps['status'], ): status is MessageBubbleStatus { return ( status === 'sending' || status === 'sent' || status === 'delivered' || status === 'read' || status === 'failed' ); } function resolveStatus(status: MessageBubbleProps['status']) { if (status == null) return null; return isMessageBubbleStatus(status) ? resolveStatusLabel(status) : status; } function resolveBubbleStyles({ direction, disabled, hovered, pressed, selected, theme, }: { direction: MessageBubbleDirection; disabled: boolean; hovered: boolean; pressed: boolean; selected: boolean; theme: ReturnType['theme']; }) { const borderColor = selected ? theme.semantics.border.focus : theme.semantics.border.default; if (direction === 'system') { return { bg: selected ? theme.semantics.neutral.surface : 'transparent', borderColor, borderWidth: selected ? 1 : 0, opacity: disabled ? 0.72 : 1, }; } const interactiveBg = pressed ? theme.semantics.neutral.surfaceActive : hovered ? theme.semantics.neutral.surfaceHover : undefined; return { bg: interactiveBg ?? (direction === 'outgoing' ? theme.semantics.neutral.surface : theme.semantics.surface.raised), borderColor, borderWidth: selected ? 1 : 0, opacity: disabled ? 0.72 : 1, }; } function MessageAvatar({ avatar, authorName, compact, }: { avatar: MessageBubbleAvatar; authorName: React.ReactNode | undefined; compact: boolean; }) { const avatarName = resolveAvatarName({ avatar, authorName }); return ( ); } function MessageBubbleInner({ themeId: _themeId, mode: _mode, interactionPolicy, testID, direction = 'incoming', text, children, author, timestamp, meta, status, leading, trailing, footer, selected = false, disabled = false, compact = false, accessibilityLabel, onPress, }: MessageBubbleProps) { const { theme } = useZoraTheme(); const padding = resolvePadding(compact); const isInteractive = Boolean(onPress); const isOutgoing = direction === 'outgoing'; const isSystem = direction === 'system'; const renderedStatus = resolveStatus(status); const authorName = author?.name; const authorAvatar = author?.avatar; const hasAuthorName = authorName != null && !isOutgoing && !isSystem; const hasAvatar = authorAvatar != null && !isOutgoing && !isSystem; const hasMetaRow = timestamp != null || meta != null || renderedStatus != null; const renderBubble = ({ pressed, hovered }: { pressed: boolean; hovered: boolean }) => { const styles = resolveBubbleStyles({ direction, disabled, hovered, pressed, selected, theme }); return ( {hasAuthorName ? ( {authorName} ) : null} {text != null ? ( {text} ) : null} {children != null ? {children} : null} {hasMetaRow ? ( {timestamp != null ? ( {timestamp} ) : null} {meta != null ? ( {meta} ) : null} {renderedStatus != null ? ( {renderedStatus} ) : null} ) : null} ); }; const bubbleContent = isInteractive ? ( {(state) => renderBubble({ pressed: state.pressed, hovered: state.hovered })} ) : ( {renderBubble({ pressed: false, hovered: false })} ); return ( {!isOutgoing && !isSystem ? ( {leading ?? (hasAvatar ? ( ) : null)} ) : null} {isOutgoing && trailing ? {trailing} : null} {bubbleContent} {isOutgoing && leading ? {leading} : null} {!isOutgoing && !isSystem && trailing ? {trailing} : null} {footer != null ? ( {footer} ) : null} ); } /*** * Message bubble pattern for chat UIs with direction and status styling. * */ export const MessageBubble = withZoraThemeScope(MessageBubbleInner);