import React, { createContext, useCallback, useRef, useState } from 'react'; import type { FlatList } from 'react-native'; import { useGroupChannelHandler } from '@sendbird/uikit-tools'; import { ContextValue, Logger, NOOP, SendbirdFileMessage, SendbirdGroupChannel, SendbirdMessage, SendbirdUser, SendbirdUserMessage, getGroupChannelChatAvailableState, isDifferentChannel, useFreshCallback, } from '@sendbird/uikit-utils'; import ProviderLayout from '../../../components/ProviderLayout'; import { MESSAGE_FOCUS_ANIMATION_DELAY } from '../../../constants'; import { useLocalization, useSendbirdChat } from '../../../hooks/useContext'; import type { PubSub } from '../../../utils/pubsub'; import type { GroupChannelContextsType, GroupChannelModule, GroupChannelPubSubContextPayload } from '../types'; import { GroupChannelProps } from '../types'; export const GroupChannelContexts: GroupChannelContextsType = { Fragment: createContext({ headerTitle: '', channel: {} as SendbirdGroupChannel, setMessageToEdit: NOOP, setMessageToReply: NOOP, }), TypingIndicator: createContext({ typingUsers: [] as SendbirdUser[], }), PubSub: createContext({ publish: NOOP, subscribe: () => NOOP, } as PubSub), MessageList: createContext({ flatListRef: { current: null }, scrollToMessage: () => false, lazyScrollToBottom: () => { // noop }, lazyScrollToIndex: () => { // noop }, lazyScrollToMessageId: () => { // noop }, } as MessageListContextValue), }; export const GroupChannelContextsProvider: GroupChannelModule['Provider'] = ({ children, channel, enableTypingIndicator, keyboardAvoidOffset = 0, groupChannelPubSub, messages, onUpdateSearchItem, onPressReplyMessageInThread, }) => { if (!channel) throw new Error('GroupChannel is not provided to GroupChannelModule'); const { STRINGS } = useLocalization(); const { currentUser, sdk, sbOptions } = useSendbirdChat(); const [typingUsers, setTypingUsers] = useState([]); const [messageToEdit, setMessageToEdit] = useState(); const [messageToReply, setMessageToReply] = useState(); const { flatListRef, lazyScrollToIndex, lazyScrollToBottom, scrollToMessage, lazyScrollToMessageId } = useScrollActions({ messages, onUpdateSearchItem, }); const updateInputMode = (mode: 'send' | 'edit' | 'reply', message?: SendbirdUserMessage | SendbirdFileMessage) => { if (mode === 'send' || !message) { setMessageToEdit(undefined); setMessageToReply(undefined); return; } else if (mode === 'edit') { setMessageToEdit(message); setMessageToReply(undefined); return; } else if (mode === 'reply') { setMessageToEdit(undefined); setMessageToReply(message); return; } }; const onPressMessageToReply = (parentMessage?: SendbirdUserMessage | SendbirdFileMessage) => { if (sbOptions.uikit.groupChannel.channel.replyType === 'thread' && parentMessage) { onPressReplyMessageInThread?.(parentMessage, Number.MAX_SAFE_INTEGER); } else if (sbOptions.uikit.groupChannel.channel.replyType === 'quote_reply') { updateInputMode('reply', parentMessage); } }; useGroupChannelHandler(sdk, { onMessageDeleted(_, messageId) { if (messageToReply?.messageId === messageId) { setMessageToReply(undefined); } }, onChannelFrozen(frozenChannel) { if (frozenChannel.url === channel.url) { if (frozenChannel.isGroupChannel() && getGroupChannelChatAvailableState(channel).frozen) { setMessageToReply(undefined); } } }, onUserMuted(mutedChannel, user) { if (mutedChannel.url === channel.url && user.userId === sdk.currentUser?.userId) { setMessageToReply(undefined); } }, onTypingStatusUpdated(eventChannel) { if (isDifferentChannel(channel, eventChannel)) return; if (!enableTypingIndicator) return; setTypingUsers(eventChannel.getTypingUsers()); }, }); return ( updateInputMode('edit', message), []), messageToReply, setMessageToReply: useCallback((message) => onPressMessageToReply(message), []), }} > {children} ); }; type MessageListContextValue = ContextValue; const useScrollActions = (params: Pick) => { const { messages, onUpdateSearchItem } = params; const flatListRef = useRef>(null); const messagesRef = useRef(messages); messagesRef.current = messages; // FIXME: Workaround, should run after data has been applied to UI. const lazyScrollToBottom = useFreshCallback((params) => { if (!flatListRef.current) { logFlatListRefWarning(); return; } setTimeout(() => { flatListRef.current?.scrollToOffset({ offset: 0, animated: params?.animated ?? false }); }, params?.timeout ?? 0); }); // FIXME: Workaround, should run after data has been applied to UI. const lazyScrollToIndex = useFreshCallback((params) => { if (!flatListRef.current) { logFlatListRefWarning(); return; } setTimeout(() => { flatListRef.current?.scrollToIndex({ index: params?.index ?? 0, animated: params?.animated ?? false, viewPosition: params?.viewPosition ?? 0.5, }); }, params?.timeout ?? 0); }); // FIXME: Workaround, should run after data has been applied to UI. const lazyScrollToMessageId = useFreshCallback((params) => { if (!flatListRef.current) { logFlatListRefWarning(); return; } setTimeout(() => { let messageIndex = 0; if (params?.messageId) { const foundMessageIndex = messagesRef.current.findIndex((it) => it.messageId === params.messageId); if (foundMessageIndex > -1) { messageIndex = foundMessageIndex; } else { Logger.warn('Message with messageId not found:', params.messageId); return; } } flatListRef.current?.scrollToIndex({ index: messageIndex, animated: params?.animated ?? false, viewPosition: params?.viewPosition ?? 0.5, }); }, params?.timeout ?? 0); }); const scrollToMessage = useFreshCallback((messageId, options) => { if (!flatListRef.current) { logFlatListRefWarning(); return false; } const foundMessageIndex = messages.findIndex((it) => it.messageId === messageId); const isIncludedInList = foundMessageIndex > -1; if (isIncludedInList) { if (options?.focusAnimated) { setTimeout( () => onUpdateSearchItem({ startingPoint: messages[foundMessageIndex].createdAt }), MESSAGE_FOCUS_ANIMATION_DELAY, ); } lazyScrollToIndex({ index: foundMessageIndex, animated: true, timeout: 0, viewPosition: options?.viewPosition, }); return true; } else { return false; } }); return { flatListRef, lazyScrollToIndex, lazyScrollToBottom, scrollToMessage, lazyScrollToMessageId, }; }; const logFlatListRefWarning = () => { Logger.warn( 'Cannot find flatListRef.current, please render FlatList and pass the flatListRef' + 'or please try again after FlatList has been rendered.', ); };