import React, { useCallback, useEffect, useMemo } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import { AttachmentManagerState, DraftMessage, LocalMessage, TextComposerState, Thread, ThreadState, } from 'stream-chat'; import { useChatContext, useTheme, useTranslationContext } from '../../contexts'; import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext'; import { ThreadListItemProvider, useThreadListItemContext, } from '../../contexts/threadsContext/ThreadListItemContext'; import { useThreadsContext } from '../../contexts/threadsContext/ThreadsContext'; import { useStateStore } from '../../hooks'; import { primitives } from '../../theme'; import { getDateString } from '../../utils/i18n/getDateString'; import { useChannelPreviewDisplayPresence } from '../ChannelPreview/hooks'; import { useChannelPreviewDisplayName } from '../ChannelPreview/hooks/useChannelPreviewDisplayName'; import { BadgeNotification, UserAvatarStack } from '../ui'; import { UserAvatar } from '../ui/Avatar/UserAvatar'; export type ThreadListItemProps = { thread: Thread; timestampTranslationKey?: string; }; export const attachmentTypeIconMap = { audio: '🔈', file: '📄', image: '📷', video: '🎥', voiceRecording: '🎙️', } as const; const textComposerStateSelector = (state: TextComposerState) => ({ text: state.text, }); const attachmentManagerStateSelector = (state: AttachmentManagerState) => ({ attachments: state.attachments, }); export const ThreadListItemComponent = () => { const { channel, dateString, deletedAtDateString, draftMessage, lastReply, ownUnreadMessageCount, parentMessage, thread, } = useThreadListItemContext(); const online = useChannelPreviewDisplayPresence(channel); const displayName = useChannelPreviewDisplayName(channel); const { onThreadSelect } = useThreadsContext(); const { ThreadListItemMessagePreview, ThreadMessagePreviewDeliveryStatus } = useComponentsContext(); const { theme: { semantics }, } = useTheme(); const styles = useStyles(); const { t } = useTranslationContext(); const shouldRenderPreview = !!draftMessage || !!lastReply; return ( { if (onThreadSelect) { onThreadSelect( { thread: parentMessage as LocalMessage, threadInstance: thread }, channel, ); } }} style={({ pressed }) => [ styles.container, { backgroundColor: pressed ? semantics.backgroundUtilityPressed : 'transparent' }, ]} testID='thread-list-item' > {lastReply?.user ? ( ) : null} {displayName || 'N/A'} {shouldRenderPreview ? ( {!draftMessage ? ( ) : null} ) : null} {parentMessage?.reply_count === 1 ? t('1 Reply') : t('{{ replyCount }} Replies', { replyCount: parentMessage?.reply_count, })} {deletedAtDateString ?? dateString} {ownUnreadMessageCount > 0 && !deletedAtDateString ? ( ) : null} ); }; export const ThreadListItem = (props: ThreadListItemProps) => { const { client } = useChatContext(); const { t, tDateTimeParser } = useTranslationContext(); const { thread, timestampTranslationKey = 'timestamp/ThreadListItem' } = props; const { ThreadListItem: ThreadListItemOverride } = useComponentsContext(); const { text: draftText } = useStateStore( thread.messageComposer.textComposer.state, textComposerStateSelector, ); const { attachments } = useStateStore( thread.messageComposer.attachmentManager.state, attachmentManagerStateSelector, ); const selector = useCallback( (nextValue: ThreadState) => ({ channel: nextValue.channel, deletedAt: nextValue.deletedAt, lastReply: nextValue.replies.at(-1), ownUnreadMessageCount: (client.userID && nextValue.read[client.userID]?.unreadMessageCount) || 0, parentMessage: nextValue.parentMessage, }) as const, [client], ); const { channel, deletedAt, lastReply, ownUnreadMessageCount, parentMessage } = useStateStore( thread.state, selector, ); const timestamp = lastReply?.created_at; useEffect(() => { const unsubscribe = thread.messageComposer.registerDraftEventSubscriptions(); return () => unsubscribe(); }, [thread.messageComposer]); const draftMessage = useMemo(() => { if (thread.messageComposer.compositionIsEmpty) { return undefined; } if (!draftText && !attachments?.length) { return undefined; } return { attachments, id: thread.messageComposer.id, text: draftText ?? '', }; }, [attachments, draftText, thread.messageComposer]); // TODO: Please rethink this, we have the same line of code in about 5 places in the SDK. const dateString = useMemo( () => getDateString({ date: timestamp, t, tDateTimeParser, timestampTranslationKey, }), [timestamp, t, tDateTimeParser, timestampTranslationKey], ); const deletedAtDateString = useMemo( () => getDateString({ date: deletedAt as Date | undefined, t, tDateTimeParser, timestampTranslationKey, }), [deletedAt, t, tDateTimeParser, timestampTranslationKey], ); return ( ); }; const useStyles = () => { const { theme: { threadListItem, semantics }, } = useTheme(); return useMemo( () => StyleSheet.create({ wrapper: { flex: 1, padding: primitives.spacingXxs, borderBottomWidth: 1, borderBottomColor: semantics.borderCoreSubtle, ...threadListItem.wrapper, }, container: { flexDirection: 'row', gap: primitives.spacingSm, padding: primitives.spacingSm, borderRadius: primitives.radiusLg, ...threadListItem.container, }, channelName: { color: semantics.textTertiary, fontSize: primitives.typographyFontSizeSm, fontWeight: primitives.typographyFontWeightSemiBold, lineHeight: primitives.typographyLineHeightNormal, textAlign: 'left', ...threadListItem.channelName, }, content: { flex: 1, gap: primitives.spacingXs, ...threadListItem.content, }, previewMessageContainer: { flexDirection: 'row', alignItems: 'center', gap: primitives.spacingXxs, ...threadListItem.previewMessageContainer, }, lowerRow: { flexDirection: 'row', alignItems: 'center', gap: primitives.spacingXs, ...threadListItem.lowerRow, }, messageRepliesText: { color: semantics.textLink, fontSize: primitives.typographyFontSizeSm, fontWeight: primitives.typographyFontWeightSemiBold, lineHeight: primitives.typographyLineHeightNormal, ...threadListItem.messageRepliesText, }, dateText: { color: semantics.textTertiary, fontSize: primitives.typographyFontSizeSm, fontWeight: primitives.typographyFontWeightRegular, lineHeight: primitives.typographyLineHeightNormal, ...threadListItem.dateText, }, unreadBubbleWrapper: { ...threadListItem.unreadBubbleWrapper, }, }), [semantics, threadListItem], ); };