import React, {useCallback, useMemo} from 'react' import { type GestureResponderEvent, type StyleProp, type TextStyle, View, } from 'react-native' import Animated, { LayoutAnimationConfig, LinearTransition, ZoomIn, ZoomOut, } from 'react-native-reanimated' import { AppBskyEmbedRecord, ChatBskyConvoDefs, RichText as RichTextAPI, } from '@atproto/api' import {type I18n} from '@lingui/core' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {isNative} from '#/platform/detection' import {useConvoActive} from '#/state/messages/convo' import {type ConvoItem} from '#/state/messages/convo/types' import {useSession} from '#/state/session' import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {atoms as a, native, useTheme} from '#/alf' import {isOnlyEmoji} from '#/alf/typography' import {ActionsWrapper} from '#/components/dms/ActionsWrapper' import {InlineLinkText} from '#/components/Link' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import {DateDivider} from './DateDivider' import {MessageItemEmbed} from './MessageItemEmbed' import {localDateString} from './util' let MessageItem = ({ item, }: { item: ConvoItem & {type: 'message' | 'pending-message'} }): React.ReactNode => { const t = useTheme() const {currentAccount} = useSession() const {_} = useLingui() const {convo} = useConvoActive() const {message, nextMessage, prevMessage} = item const isPending = item.type === 'pending-message' const isFromSelf = message.sender?.did === currentAccount?.did const nextIsMessage = ChatBskyConvoDefs.isMessageView(nextMessage) const isNextFromSelf = nextIsMessage && nextMessage.sender?.did === currentAccount?.did const isNextFromSameSender = isNextFromSelf === isFromSelf const isNewDay = useMemo(() => { if (!prevMessage) return true const thisDate = new Date(message.sentAt) const prevDate = new Date(prevMessage.sentAt) return localDateString(thisDate) !== localDateString(prevDate) }, [message, prevMessage]) const isLastMessageOfDay = useMemo(() => { if (!nextMessage || !nextIsMessage) return true const thisDate = new Date(message.sentAt) const prevDate = new Date(nextMessage.sentAt) return localDateString(thisDate) !== localDateString(prevDate) }, [message.sentAt, nextIsMessage, nextMessage]) const needsTail = isLastMessageOfDay || !isNextFromSameSender const isLastInGroup = useMemo(() => { // if this message is pending, it means the next message is pending too if (isPending && nextMessage) { return false } // or, if there's a 5 minute gap between this message and the next if (ChatBskyConvoDefs.isMessageView(nextMessage)) { const thisDate = new Date(message.sentAt) const nextDate = new Date(nextMessage.sentAt) const diff = nextDate.getTime() - thisDate.getTime() // 5 minutes return diff > 5 * 60 * 1000 } return true }, [message, nextMessage, isPending]) const pendingColor = t.palette.primary_200 const rt = useMemo(() => { return new RichTextAPI({text: message.text, facets: message.facets}) }, [message.text, message.facets]) const appliedReactions = ( {message.reactions && message.reactions.length > 0 && ( {message.reactions.map((reaction, _i, reactions) => { let label if (reaction.sender.did === currentAccount?.did) { label = _(msg`You reacted ${reaction.value}`) } else { const senderDid = reaction.sender.did const sender = convo.members.find( member => member.did === senderDid, ) if (sender) { label = _( msg`${sanitizeDisplayName( sender.displayName || sender.handle, )} reacted ${reaction.value}`, ) } else { label = _(msg`Someone reacted ${reaction.value}`) } } return ( 1 && native(ZoomOut.delay(200))} layout={native(LinearTransition.delay(300))} key={reaction.sender.did + reaction.value} style={[a.p_2xs]} accessible={true} accessibilityLabel={label} accessibilityHint={_( msg`Double tap or long press the message to add a reaction`, )}> {reaction.value} ) })} )} ) return ( <> {isNewDay && } {AppBskyEmbedRecord.isView(message.embed) && ( )} {rt.text.length > 0 && ( )} {isNative && appliedReactions} {!isNative && appliedReactions} {isLastInGroup && ( )} ) } MessageItem = React.memo(MessageItem) export {MessageItem} let MessageItemMetadata = ({ item, style, }: { item: ConvoItem & {type: 'message' | 'pending-message'} style: StyleProp }): React.ReactNode => { const t = useTheme() const {_} = useLingui() const {message} = item const handleRetry = useCallback( (e: GestureResponderEvent) => { if (item.type === 'pending-message' && item.retry) { e.preventDefault() item.retry() return false } }, [item], ) const relativeTimestamp = useCallback( (i18n: I18n, timestamp: string) => { const date = new Date(timestamp) const now = new Date() const time = i18n.date(date, { hour: 'numeric', minute: 'numeric', }) const diff = now.getTime() - date.getTime() // if under 30 seconds if (diff < 1000 * 30) { return _(msg`Now`) } return time }, [_], ) return ( {({timeElapsed}) => ( {timeElapsed} )} {item.type === 'pending-message' && item.failed && ( <> {' '} ·{' '} {_(msg`Failed to send`)} {item.retry && ( <> {' '} ·{' '} {_(msg`Retry`)} )} )} ) } MessageItemMetadata = React.memo(MessageItemMetadata) export {MessageItemMetadata}