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 (
)
}
function ExpandedLinkImage({
attachment,
}: {
attachment: DenormalizedExpandedLinkAttachmentResource
}) {
const { title = '', imageUrl, imageHeight, imageWidth } = attachment.attributes
const styles = useStyles({ imageWidth, imageHeight })
return (
)
}
function MessageAttachmentImage({
attachment,
}: {
attachment: DenormalizedMessageAttachmentResource
}) {
const { url, urlMedium, filename, metadata = {} } = attachment.attributes
const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height })
return (
)
}
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,
},
})
}