import { useNavigation } from '@react-navigation/native' import { some } from 'lodash' import React, { memo } from 'react' import { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native' import Animated from 'react-native-reanimated' import { useAnimatedMessageBackgroundColor, useFontScale, useScalableNumberOfLines, useTheme, } from '../../hooks' import { useConversationMessage } from '../../hooks/use_conversation_message' import { useLiveRelativeTime } from '../../hooks/use_live_relative_time' import { MessageResource } from '../../types' import { DenormalizedAttachmentResource, DenormalizedGiphyAttachmentResource, DenormalizedExpandedLinkAttachmentResource, DenormalizedMessageAttachmentResource, } from '../../types/resources/denormalized_attachment_resource' import { assertKeysAreNumbers, pluralize } from '../../utils' import { stripEmptyTextCharacters } from '../../utils/message_text' import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL, MAX_FONT_SIZE_MULTIPLIER, MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH, platformFontWeightMedium, } from '../../utils/styles' import { Avatar, Icon, IconProps, Image, Text } from '../display' import { TheirReplyConnector, MyReplyConnector } from './reply_connectors' interface ReplyShadowMessageProps extends MessageResource { messageId: string conversation_id: number inReplyScreen?: boolean isReplyShadowMessage: boolean nextRendersAuthor: boolean } export const ReplyShadowMessage = memo(function ReplyShadowMessageRow({ conversation_id, inReplyScreen, messageId, replyRoot, isReplyShadowMessage, nextRendersAuthor, }: ReplyShadowMessageProps) { const fetched = useConversationMessage({ conversation_id, messageId, enabled: !replyRoot }) const message = replyRoot ?? fetched.message if (inReplyScreen) return null if (!message && fetched.isLoading) return if (!message) return const enrichedMessage = { ...message, isReplyShadowMessage, nextRendersAuthor, } return }) interface ShadowMessageContentProps extends MessageResource { conversation_id: number } function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageContentProps) { const { deletedAt, author, attachments } = message const text = stripEmptyTextCharacters(message.text ?? '') const styles = useStyles(message) const { colors } = useTheme() const navigation = useNavigation() const timestamp = useLiveRelativeTime(message.createdAt) const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0) const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } = useAnimatedMessageBackgroundColor() const scalableNumberOfLines = useScalableNumberOfLines(2) const replyCountText = pluralize(message.replyCount, 'reply') const isDeleted = !!deletedAt const messageText = isDeleted ? 'Message deleted' : text const handleNavigateToReplies = () => { navigation.navigate('ConversationReply', { conversation_id, reply_root_id: message.replyRootId ?? message.id, reply_root_author_name: message.author.name, }) } const attachmentLabel = some(attachments) ? pluralize(attachments.length, 'attachment') : '' const accessibilityLabel = `${author?.name || ''} Reply Preview ${attachmentLabel} ${ messageText || '' } ${timestamp} ${replyCountText}` return ( {!message.mine && ( )} setMessageBubbleHeight(e.nativeEvent.layout.height)} > {text && ( {messageText} )} {replyCountText} {message.mine && ( )} ) } function MessageAttachmentImagery({ attachments, }: { attachments: DenormalizedAttachmentResource[] }) { if (!attachments || attachments.length === 0) return null const attachment = attachments[0] if (attachment.type === 'giphy') { return } if (attachment.type === 'ExpandedLink') { return } if (attachment.type === 'MessageAttachment') { const contentType = attachment.attributes?.contentType const basicType = contentType?.split('/')[0] switch (basicType) { case 'image': return case 'video': return case 'audio': return case 'application': return default: return null } } return null } function GiphyImage({ attachment }: { attachment: DenormalizedGiphyAttachmentResource }) { const { title, giphy } = attachment const { url } = giphy.fixedWidth const { width, height } = assertKeysAreNumbers(giphy.fixedWidth) const styles = useStyles({ imageWidth: width, imageHeight: height }) return ( {title} ) } function ExpandedLinkImage({ attachment, }: { attachment: DenormalizedExpandedLinkAttachmentResource }) { const { title = '', imageUrl, imageHeight, imageWidth } = attachment.attributes const styles = useStyles({ imageWidth, imageHeight }) return ( {title} ) } function MessageAttachmentImage({ attachment, }: { attachment: DenormalizedMessageAttachmentResource }) { const { url, urlMedium, filename, metadata = {} } = attachment.attributes const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height }) return ( {filename} ) } function MessageAttachmentIcon({ iconName }: { iconName: IconProps['name'] }) { const styles = useStyles() return ( ) } // TODO: The mine prop is not used yet, but in the future we will be adding a way to find the `mine` value for this fallback. function ShadowMessageFallback({ text, mine }: { text: string; mine?: boolean }) { const styles = useStyles({ mine }) return ( {text} ) } interface StylesProps { imageWidth?: number imageHeight?: number mine?: boolean } const useStyles = ({ mine, imageWidth = 32, imageHeight = 32 }: StylesProps = {}) => { const { colors } = useTheme() const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER }) const { width } = useWindowDimensions() const tabletWidth = width >= 744 // Smallest iPad Mini's width return StyleSheet.create({ message: { gap: 8, flexDirection: mine ? 'row-reverse' : 'row', paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL, }, messageContent: { flex: 1, gap: 4, marginBottom: 12, }, avatarWrapper: { width: MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH, alignItems: 'center', }, avatar: { marginBottom: 8, }, avatarOpacity: { opacity: 0.5, }, messageBubble: { flexDirection: 'row', alignSelf: mine ? 'flex-end' : 'flex-start', alignItems: 'center', gap: 8, borderColor: colors.borderColorDefaultBase, borderWidth: 1, borderRadius: 8, maxWidth: tabletWidth ? 360 : '80%', paddingVertical: 6, paddingHorizontal: 8, }, messageText: { color: colors.textColorDefaultPlaceholder, flexShrink: 1, }, messageMeta: { flexDirection: 'row', justifyContent: mine ? 'flex-end' : 'flex-start', }, replyCountText: { color: colors.interaction, fontWeight: platformFontWeightMedium, }, imageWrapper: { width: 32 * fontScale, aspectRatio: imageWidth / imageHeight, opacity: 0.5, }, image: { borderRadius: 4, }, attachmentIcon: { color: colors.iconColorDefaultDim, fontSize: 16, }, }) }