import React, { useContext, useEffect, useRef } from 'react'; import type { GroupChannelMessageProps, RegexTextPattern } from '@sendbird/uikit-react-native-foundation'; import { Box, GroupChannelMessage, Text, TypingIndicatorBubble, useUIKitTheme, } from '@sendbird/uikit-react-native-foundation'; import { SendbirdAdminMessage, SendbirdFileMessage, SendbirdMessage, SendbirdUserMessage, calcMessageGrouping, getMessageType, isMyMessage, isVoiceMessage, shouldRenderParentMessage, shouldRenderReaction, useIIFE, } from '@sendbird/uikit-utils'; import { VOICE_MESSAGE_META_ARRAY_DURATION_KEY } from '../../constants'; import { GroupChannelContexts } from '../../domain/groupChannel/module/moduleContext'; import type { GroupChannelProps } from '../../domain/groupChannel/types'; import { useLocalization, usePlatformService, useSBUHandlers, useSendbirdChat } from '../../hooks/useContext'; import { TypingIndicatorType } from '../../types'; import { ReactionAddons } from '../ReactionAddons'; import GroupChannelMessageDateSeparator from './GroupChannelMessageDateSeparator'; import GroupChannelMessageFocusAnimation from './GroupChannelMessageFocusAnimation'; import GroupChannelMessageNewLine from './GroupChannelMessageNewLine'; import GroupChannelMessageOutgoingStatus from './GroupChannelMessageOutgoingStatus'; import GroupChannelMessageParentMessage from './GroupChannelMessageParentMessage'; import GroupChannelMessageReplyInfo from './GroupChannelMessageReplyInfo'; const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'] = ({ channel, message, onPress, onLongPress, onPressParentMessage, onShowUserProfile, onReplyInThreadMessage, enableMessageGrouping, focused, prevMessage, nextMessage, isFirstUnreadMessage, hideParentMessage, }) => { const handlers = useSBUHandlers(); const playerUnsubscribes = useRef<(() => void)[]>([]); const { palette } = useUIKitTheme(); const { sbOptions, currentUser, mentionManager, voiceMessageStatusManager } = useSendbirdChat(); const { STRINGS } = useLocalization(); const { mediaService, playerService } = usePlatformService(); const { groupWithPrev, groupWithNext } = calcMessageGrouping( Boolean(enableMessageGrouping), message, prevMessage, nextMessage, sbOptions.uikit.groupChannel.channel.replyType === 'thread', shouldRenderParentMessage(message, hideParentMessage), ); const variant = isMyMessage(message, currentUser?.userId) ? 'outgoing' : 'incoming'; const reactionChildren = useIIFE(() => { const configs = sbOptions.uikitWithAppInfo.groupChannel.channel; if ( shouldRenderReaction(channel, channel.isSuper ? configs.enableReactionsSupergroup : configs.enableReactions) && message.reactions && message.reactions.length > 0 ) { return ; } return null; }); const replyInfo = useIIFE(() => { if (sbOptions.uikit.groupChannel.channel.replyType !== 'thread') return null; if (!channel || !message.threadInfo || !message.threadInfo.replyCount) return null; return ; }); const resetPlayer = async () => { playerUnsubscribes.current.forEach((unsubscribe) => { try { unsubscribe(); } catch {} }); playerUnsubscribes.current.length = 0; await playerService.reset(); }; const messageProps: Omit, 'message'> = { channel, variant, onPress, onLongPress, onPressURL: (url) => handlers.onOpenURL(url), onPressAvatar: () => { if ('sender' in message) onShowUserProfile?.(message.sender); }, onPressMentionedUser: (mentionedUser) => { if (mentionedUser) onShowUserProfile?.(mentionedUser); }, onToggleVoiceMessage: async (state, setState) => { if (isVoiceMessage(message) && message.sendingStatus === 'succeeded') { if (playerService.uri === message.url) { if (playerService.state === 'playing') { await playerService.pause(); } else { await playerService.play(message.url); } } else { if (playerService.state !== 'idle') { await resetPlayer(); } const shouldSeekToTime = state.duration > state.currentTime && state.currentTime > 0; let seekFinished = !shouldSeekToTime; const forPlayback = playerService.addPlaybackListener(({ stopped, currentTime, duration }) => { voiceMessageStatusManager.setCurrentTime(message.channelUrl, message.messageId, stopped ? 0 : currentTime); if (seekFinished) { setState((prevState) => ({ ...prevState, currentTime: stopped ? 0 : currentTime, duration })); } }); const forState = playerService.addStateListener((state) => { switch (state) { case 'preparing': setState((prevState) => ({ ...prevState, status: 'preparing' })); break; case 'playing': setState((prevState) => ({ ...prevState, status: 'playing' })); break; case 'idle': case 'paused': { setState((prevState) => ({ ...prevState, status: 'paused' })); break; } case 'stopped': setState((prevState) => ({ ...prevState, status: 'paused' })); break; } }); playerUnsubscribes.current.push(forPlayback, forState); await playerService.play(message.url); if (shouldSeekToTime) { await playerService.seek(state.currentTime); seekFinished = true; } } } }, groupedWithPrev: groupWithPrev, groupedWithNext: groupWithNext, children: reactionChildren, replyInfo: replyInfo, sendingStatus: isMyMessage(message, currentUser?.userId) ? ( ) : null, parentMessage: shouldRenderParentMessage(message, hideParentMessage) ? ( ) : null, strings: { edited: STRINGS.GROUP_CHANNEL.MESSAGE_BUBBLE_EDITED_POSTFIX, senderName: ('sender' in message && message.sender.nickname) || STRINGS.LABELS.USER_NO_NAME, sentDate: STRINGS.GROUP_CHANNEL.MESSAGE_BUBBLE_TIME(message), fileName: message.isFileMessage() ? STRINGS.GROUP_CHANNEL.MESSAGE_BUBBLE_FILE_TITLE(message) : '', unknownTitle: STRINGS.GROUP_CHANNEL.MESSAGE_BUBBLE_UNKNOWN_TITLE(message), unknownDescription: STRINGS.GROUP_CHANNEL.MESSAGE_BUBBLE_UNKNOWN_DESC(message), }, }; const userMessageProps: { renderRegexTextChildren: (message: SendbirdUserMessage) => string; regexTextPatterns: RegexTextPattern[]; } = { renderRegexTextChildren: (message) => { if ( mentionManager.shouldUseMentionedMessageTemplate(message, sbOptions.uikit.groupChannel.channel.enableMention) ) { return message.mentionedMessageTemplate ?? ''; } else { return message.message; } }, regexTextPatterns: [ { regex: mentionManager.templateRegex, replacer({ match, groups, parentProps, index, keyPrefix }) { const user = message.mentionedUsers?.find((it) => it.userId === groups[2]); if (user) { const mentionColor = !isMyMessage(message, currentUser?.userId) && user.userId === currentUser?.userId ? palette.onBackgroundLight01 : parentProps?.color; return ( messageProps.onPressMentionedUser?.(user)} onLongPress={messageProps.onLongPress} suppressHighlighting style={[ parentProps?.style, { fontWeight: '700' }, user.userId === currentUser?.userId && { backgroundColor: palette.highlight }, ]} > {`${mentionManager.asMentionedMessageText(user)}`} ); } return match; }, }, ], }; const renderMessage = () => { switch (getMessageType(message)) { case 'admin': { return ; } case 'user': case 'user.opengraph': { if (message.ogMetaData && sbOptions.uikitWithAppInfo.groupChannel.channel.enableOgtag) { return ( ); } else { return ( ); } } case 'file': case 'file.audio': { return ; } case 'file.image': { return ; } case 'file.video': { return ( mediaService.getVideoThumbnail({ url: uri, timeMills: 1000 })} {...messageProps} /> ); } case 'file.voice': { return ( { if (isVoiceMessage(message) && playerService.uri === message.url) { resetPlayer().catch((_) => {}); } }} {...messageProps} /> ); } case 'unknown': default: { return ; } } }; const messageGap = useIIFE(() => { if (message.isAdminMessage()) { if (nextMessage?.isAdminMessage()) { return 8; } else { return 16; } } else if (nextMessage && shouldRenderParentMessage(nextMessage, hideParentMessage)) { return 16; } else if (groupWithNext) { return 2; } else { return 16; } }); const shouldRenderNewLine = sbOptions.uikit.groupChannel.channel.enableMarkAsUnread && isFirstUnreadMessage; return ( {renderMessage()} ); }; export const GroupChannelTypingIndicatorBubble = () => { const { sbOptions } = useSendbirdChat(); const { publish } = useContext(GroupChannelContexts.PubSub); const { typingUsers } = useContext(GroupChannelContexts.TypingIndicator); const shouldRenderBubble = useIIFE(() => { if (typingUsers.length === 0) return false; if (!sbOptions.uikit.groupChannel.channel.enableTypingIndicator) return false; if (!sbOptions.uikit.groupChannel.channel.typingIndicatorTypes.has(TypingIndicatorType.Bubble)) return false; return true; }); useEffect(() => { if (shouldRenderBubble) publish({ type: 'TYPING_BUBBLE_RENDERED' }); }, [shouldRenderBubble]); if (!shouldRenderBubble) return null; return ( ); }; export default React.memo(GroupChannelMessageRenderer);