import React, { JSX } from "react"; import { View, Text, TouchableOpacity, StyleProp, ViewStyle, Platform, StyleSheet } from "react-native"; import { CometChat } from "@cometchat/chat-sdk-react-native"; import { useTheme } from "../../../theme"; import { Styles } from "./style"; import { getCometChatTranslation } from "../../resources/CometChatLocalizeNew/LocalizationManager"; import { Icon } from "../../icons/Icon"; import { stripMarkdown, preparePreviewText } from "../MarkdownUtils"; import { CometChatRichTextFormatter } from "../../formatters/CometChatRichTextFormatter"; import { applyMentionsFormatting } from "../MessageUtils"; const t = getCometChatTranslation(); /** Module-level formatter instance — reused across renders (SRP, no GC churn) */ const previewFormatter = new CometChatRichTextFormatter(); /** * Props for CometChatMessagePreview component */ interface CometChatMessagePreviewProps { // Old interface (for backward compatibility) messagePreviewTitle?: string; messagePreviewSubtitle?: string; closeIconURL?: any; onCloseClick?: () => void; // New interface (message-based) message?: CometChat.BaseMessage; theme?: any; showCloseIcon?: boolean; style?: StyleProp; // Icon support subtitleIcon?: JSX.Element; // Title style override titleStyle?: any; // Deleted message indicator isDeletedMessage?: boolean; // Mentions style override for reply preview context mentionsStyle?: any; } /** * * CometChatMessagePreview * */ const CometChatMessagePreview = (props: CometChatMessagePreviewProps) => { const { messagePreviewTitle, messagePreviewSubtitle, closeIconURL, onCloseClick, message, theme: propTheme, showCloseIcon = false, style, subtitleIcon, titleStyle, isDeletedMessage = false, mentionsStyle, } = props; const theme = useTheme(); const finalTheme = propTheme || theme; // Helper function to get message preview title (sender name) const getMessagePreviewTitle = (): string => { if (messagePreviewTitle) return messagePreviewTitle; if (!message) return ""; try { if (message && typeof message.getSender === 'function') { const sender = message.getSender(); if (sender && typeof sender.getName === 'function') { return sender.getName() || ""; } } // Fallback for cases where getSender is not available if (message && (message as any).sender && (message as any).sender.name) { return (message as any).sender.name; } } catch (error) { console.warn("Error getting message sender:", error); } return ""; }; // Helper function to get message preview subtitle/content const getMessagePreviewSubtitle = (): string | JSX.Element => { if (messagePreviewSubtitle) return messagePreviewSubtitle; if (!message) return ""; if (props.isDeletedMessage) { return t("MESSAGE_IS_DELETED") || "Message deleted"; } try { const messageType = typeof message.getType === 'function' ? message.getType() : (message as any).type; const messageCategory = typeof message.getCategory === 'function' ? message.getCategory() : (message as any).category; if (messageCategory === CometChat.CATEGORY_MESSAGE) { switch (messageType) { case CometChat.MESSAGE_TYPE.TEXT: const textMessage = message as CometChat.TextMessage; let text = (typeof textMessage.getText === 'function' ? textMessage.getText() : (textMessage as any).text) || ""; // Prepare text for preview: collapse code blocks, strip block markers, flatten to single line const previewResult = preparePreviewText(text); const cleanText = previewResult.text; // Set inline code styles for the preview context — uses theme's textPrimary // which is white for outgoing (set by getReplyView) and dark for composer tray. previewFormatter.setStyle({ inlineCodeStyle: { fontSize: 14, fontWeight: '400', color: finalTheme?.color?.textPrimary as string || '#141414', }, inlineCodeContainerStyle: { backgroundColor: finalTheme?.color?.previewInlineCodeBackground ?? finalTheme?.color?.background3 ?? 'rgba(120, 120, 128, 0.18)', borderRadius: 4, borderWidth: 0.5, borderColor: finalTheme?.color?.borderDefault ?? 'rgba(120, 120, 128, 0.3)', }, }); const formatted = previewFormatter.getFormattedText(cleanText || null); // Resolve subtitle content let subtitleContent: string | JSX.Element; if (formatted && typeof formatted !== 'string') { subtitleContent = formatted; } else { subtitleContent = (formatted as string) || stripMarkdown(text); } // Apply mentions formatter using shared helper (DRY — same as conversation list) // mentionsStyle prop is passed from the caller (e.g., getReplyView in MessageDataSource) subtitleContent = applyMentionsFormatting(textMessage, subtitleContent, text, mentionsStyle); // For code blocks (first rich block): render compact code block container if (previewResult.codeBlockFirstLine !== null) { isCodeBlockPreview = true; codeBlockLine = previewResult.codeBlockFirstLine; } // For blockquotes: set flag for render section to add container styling if (previewResult.isBlockquote) { isBlockquotePreview = true; } // For list items: set flag so render shows prefix + content with ellipsis if (previewResult.listPrefix) { isListPreview = true; listPrefix = previewResult.listPrefix; } return subtitleContent; case CometChat.MESSAGE_TYPE.IMAGE: const imageMessage = message as CometChat.MediaMessage; const imageAttachment = typeof imageMessage.getAttachment === 'function' ? imageMessage.getAttachment() : (imageMessage as any).attachment; return imageAttachment?.getName?.() || imageAttachment?.name || t("MESSAGE_IMAGE") || "Image"; case CometChat.MESSAGE_TYPE.VIDEO: const videoMessage = message as CometChat.MediaMessage; const videoAttachment = typeof videoMessage.getAttachment === 'function' ? videoMessage.getAttachment() : (videoMessage as any).attachment; return videoAttachment?.getName?.() || videoAttachment?.name || t("MESSAGE_VIDEO") || "Video"; case CometChat.MESSAGE_TYPE.AUDIO: const audioMessage = message as CometChat.MediaMessage; const audioAttachment = typeof audioMessage.getAttachment === 'function' ? audioMessage.getAttachment() : (audioMessage as any).attachment; return audioAttachment?.getName?.() || audioAttachment?.name || t("MESSAGE_AUDIO") || "Audio"; case CometChat.MESSAGE_TYPE.FILE: const fileMessage = message as CometChat.MediaMessage; const fileAttachment = typeof fileMessage.getAttachment === 'function' ? fileMessage.getAttachment() : (fileMessage as any).attachment; return fileAttachment?.getName?.() || fileAttachment?.name || t("MESSAGE_FILE") || "File"; default: return messageType || t("MESSAGE") || "Message"; } } else if (messageCategory === CometChat.CATEGORY_CUSTOM) { // Handle custom message types with proper formatting if (messageType === "extension_sticker") { return "Sticker"; } else if (messageType === "extension_collaborative_whiteboard" || messageType === "extension_whiteboard") { return "Collaborative Whiteboard"; } else if (messageType === "extension_document") { return "Document"; } else if (messageType === "extension_poll") { return "Poll"; } else if (messageType === "meeting") { return "Meeting"; } else if (messageType === "extension_location") { return "Location"; } else { // Convert snake_case or camelCase to Title Case, but remove "extension_" prefix if (messageType) { let formattedType = messageType; // Remove "extension_" prefix if present if (formattedType.startsWith('extension_')) { formattedType = formattedType.replace('extension_', ''); } return formattedType .replace(/[_-]/g, ' ') .replace(/([a-z])([A-Z])/g, '$1 $2') .split(' ') .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); } return t("CUSTOM_MESSAGE") || "Custom Message"; } } else if (messageCategory === CometChat.CATEGORY_ACTION) { return t("ACTION_MESSAGE") || "Action"; } return t("MESSAGE") || "Message"; } catch (error) { console.warn("Error getting message preview subtitle:", error); return t("MESSAGE") || "Message"; } }; // Helper function to get auto-generated icon for attachment types const getAutoIcon = (): JSX.Element | null => { if (subtitleIcon) return null; // Don't auto-generate if icon is manually provided if (!message) { return null; } try { const messageType = typeof message.getType === 'function' ? message.getType() : (message as any).type; const messageCategory = typeof message.getCategory === 'function' ? message.getCategory() : (message as any).category; // Use theme colors safely const iconColor = finalTheme?.palette?.getAccent600?.() || finalTheme?.color?.iconSecondary || finalTheme?.colors?.accent || "#666666"; // fallback color if (messageCategory === CometChat.CATEGORY_MESSAGE) { switch (messageType) { case CometChat.MESSAGE_TYPE.IMAGE: return ; case CometChat.MESSAGE_TYPE.VIDEO: return ; case CometChat.MESSAGE_TYPE.AUDIO: return ; case CometChat.MESSAGE_TYPE.FILE: return ; default: } } else if (messageCategory === CometChat.CATEGORY_CUSTOM) { // Handle custom message types with appropriate icons let iconMessageType = messageType; // Normalize the message type by removing "extension_" prefix if present if (iconMessageType && iconMessageType.startsWith('extension_')) { iconMessageType = iconMessageType.replace('extension_', ''); } switch (iconMessageType) { case "sticker": return ; case "collaborative_whiteboard": case "whiteboard": return ; case "document": return ; case "poll": return ; case "meeting": return ; case "location": return ; default: return ; } } } catch (error) { console.warn("Error getting auto icon:", error); } return null; }; // Mutable flags set by getMessagePreviewSubtitle when block types are detected let isBlockquotePreview = false; let isCodeBlockPreview = false; let isListPreview = false; let listPrefix = ''; let codeBlockLine = ''; let messageText: string | JSX.Element = getMessagePreviewSubtitle(); let title = getMessagePreviewTitle(); let autoIcon = getAutoIcon(); const shouldShowClose = showCloseIcon || onCloseClick; const containerStyle = style ? [Styles(finalTheme).editPreviewContainerStyle, style] : Styles(finalTheme).editPreviewContainerStyle; const iconToShow = subtitleIcon || autoIcon; // Determine if subtitle is rich (JSX) or plain string const isRichSubtitle = typeof messageText !== 'string'; // Detect if the formatter returned a View root (block-level content that // slipped through preparePreviewText). View can't nest inside Text, so we // render it in a height-constrained View wrapper instead. const isViewRoot = isRichSubtitle && React.isValidElement(messageText) && (messageText as React.ReactElement).type === View; return ( {/* */} {title} {shouldShowClose && ( {})} > )} {iconToShow && {iconToShow}} {isCodeBlockPreview ? ( {codeBlockLine + '..'} ) : isBlockquotePreview ? ( {messageText} ) : isListPreview ? ( {listPrefix}{messageText}{'...'} ) : isViewRoot ? ( {messageText} ) : ( {messageText} )} ); }; export { CometChatMessagePreview }; // Static styles for message preview block-level elements. // Theme-dependent values (colors) are applied inline via style array merging. const previewBlockStyles = StyleSheet.create({ codeBlockRow: { flex: 1, flexDirection: 'row', alignItems: 'center', }, codeBlockBadge: { borderRadius: 4, borderWidth: 1, paddingHorizontal: 8, paddingVertical: 3, flexShrink: 1, }, codeBlockText: { fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', fontSize: 12, }, blockquoteRow: { flex: 1, flexDirection: 'row', alignItems: 'stretch', backgroundColor: 'rgba(104, 81, 214, 0.08)', borderRadius: 8, minHeight: 26, paddingVertical: 2, }, blockquoteBar: { width: 4, borderRadius: 2, marginVertical: 4, marginLeft: 4, }, blockquoteText: { flex: 1, marginBottom: 0, paddingHorizontal: 6, lineHeight: 22, }, flexOne: { flex: 1, }, viewRootContainer: { flex: 1, overflow: 'hidden', maxHeight: 28, }, });