diff --git a/src/modules/Thread/components/ParentMessageInfo/ParentMessageInfoItem.tsx b/src/modules/Thread/components/ParentMessageInfo/ParentMessageInfoItem.tsx index 3b55d524b..c990decae 100644 --- a/src/modules/Thread/components/ParentMessageInfo/ParentMessageInfoItem.tsx +++ b/src/modules/Thread/components/ParentMessageInfo/ParentMessageInfoItem.tsx @@ -28,7 +28,6 @@ import Icon, { IconTypes, IconColors } from '../../../../ui/Icon'; import TextButton from '../../../../ui/TextButton'; import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext'; import EmojiReactions from '../../../../ui/EmojiReactions'; -import { useThreadContext } from '../../context/ThreadProvider'; import VoiceMessageItemBody from '../../../../ui/VoiceMessageItemBody'; import TextFragment from '../../../Message/components/TextFragment'; import { tokenizeMessage } from '../../../Message/utils/tokens/tokenize'; @@ -39,6 +38,7 @@ import { useFileInfoListWithUploaded } from '../../../Channel/context/hooks/useF import { Colors } from '../../../../utils/color'; import type { OnBeforeDownloadFileMessageType } from '../../../GroupChannel/context/types'; import { openURL } from '../../../../utils/utils'; +import useThread from '../../context/useThread'; export interface ParentMessageInfoItemProps { className?: string; @@ -59,12 +59,16 @@ export default function ParentMessageInfoItem({ const currentUserId = stores?.userStore?.user?.userId; const { stringSet } = useLocalization(); const { - currentChannel, - emojiContainer, - nicknamesMap, - toggleReaction, - filterEmojiCategoryIds, - } = useThreadContext(); + state: { + currentChannel, + emojiContainer, + nicknamesMap, + filterEmojiCategoryIds, + }, + actions: { + toggleReaction, + }, + } = useThread(); const { isMobile } = useMediaQueryContext(); const isReactionEnabled = config.groupChannel.enableReactions; diff --git a/src/modules/Thread/components/ParentMessageInfo/index.tsx b/src/modules/Thread/components/ParentMessageInfo/index.tsx index 3c4042785..f3238927c 100644 --- a/src/modules/Thread/components/ParentMessageInfo/index.tsx +++ b/src/modules/Thread/components/ParentMessageInfo/index.tsx @@ -10,7 +10,6 @@ import { getSenderName, SendableMessageType } from '../../../../utils'; import { getIsReactionEnabled } from '../../../../utils/getIsReactionEnabled'; import { useLocalization } from '../../../../lib/LocalizationContext'; import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext'; -import { useThreadContext } from '../../context/ThreadProvider'; import { useUserProfileContext } from '../../../../lib/UserProfileContext'; import SuggestedMentionList from '../SuggestedMentionList'; @@ -32,6 +31,7 @@ import { getCaseResolvedReplyType } from '../../../../lib/utils/resolvedReplyTyp import { classnames } from '../../../../utils/utils'; import { MessageMenu, MessageMenuProps } from '../../../../ui/MessageMenu'; import useElementObserver from '../../../../hooks/useElementObserver'; +import useThread from '../../context/useThread'; export interface ParentMessageInfoProps { className?: string; @@ -49,20 +49,24 @@ export default function ParentMessageInfo({ const userId = stores.userStore.user?.userId ?? ''; const { dateLocale, stringSet } = useLocalization(); const { - currentChannel, - parentMessage, - allThreadMessages, - emojiContainer, - toggleReaction, - updateMessage, - deleteMessage, - onMoveToParentMessage, - onHeaderActionClick, - isMuted, - isChannelFrozen, - onBeforeDownloadFileMessage, - filterEmojiCategoryIds, - } = useThreadContext(); + state: { + currentChannel, + parentMessage, + allThreadMessages, + emojiContainer, + onMoveToParentMessage, + onHeaderActionClick, + isMuted, + isChannelFrozen, + onBeforeDownloadFileMessage, + filterEmojiCategoryIds, + }, + actions: { + toggleReaction, + updateMessage, + deleteMessage, + }, + } = useThread(); const { isMobile } = useMediaQueryContext(); const isMenuMounted = useElementObserver( diff --git a/src/modules/Thread/components/RemoveMessageModal.tsx b/src/modules/Thread/components/RemoveMessageModal.tsx index f5db80bf9..fbcfaabd6 100644 --- a/src/modules/Thread/components/RemoveMessageModal.tsx +++ b/src/modules/Thread/components/RemoveMessageModal.tsx @@ -3,9 +3,9 @@ import React, { useContext } from 'react'; import Modal from '../../../ui/Modal'; import { ButtonTypes } from '../../../ui/Button'; import { LocalizationContext } from '../../../lib/LocalizationContext'; -import { useThreadContext } from '../context/ThreadProvider'; import { SendableMessageType } from '../../../utils'; import { getModalDeleteMessageTitle } from '../../../ui/Label/stringFormatterUtils'; +import useThread from '../context/useThread'; export interface RemoveMessageProps { onCancel: () => void; // rename to onClose @@ -21,8 +21,10 @@ const RemoveMessage: React.FC = (props: RemoveMessageProps) } = props; const { stringSet } = useContext(LocalizationContext); const { - deleteMessage, - } = useThreadContext(); + actions: { + deleteMessage, + }, + } = useThread(); return ( ; export const SuggestedMentionList = (props: SuggestedMentionListProps) => { - const { currentChannel } = useThreadContext(); + const { + state: { + currentChannel, + }, + } = useThread(); return ( diff --git a/src/modules/Thread/components/ThreadMessageInput/index.tsx b/src/modules/Thread/components/ThreadMessageInput/index.tsx index e51827a8a..e586350b2 100644 --- a/src/modules/Thread/components/ThreadMessageInput/index.tsx +++ b/src/modules/Thread/components/ThreadMessageInput/index.tsx @@ -5,7 +5,6 @@ import './index.scss'; import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext'; import { useMediaQueryContext } from '../../../../lib/MediaQueryContext'; -import { useThreadContext } from '../../context/ThreadProvider'; import { useLocalization } from '../../../../lib/LocalizationContext'; import MessageInput from '../../../../ui/MessageInput'; @@ -19,6 +18,7 @@ import { useHandleUploadFiles } from '../../../Channel/context/hooks/useHandleUp import { isDisabledBecauseFrozen, isDisabledBecauseMuted } from '../../../Channel/context/utils'; import { User } from '@sendbird/chat'; import { classnames } from '../../../../utils/utils'; +import useThread from '../../context/useThread'; export interface ThreadMessageInputProps { className?: string; @@ -45,23 +45,27 @@ const ThreadMessageInput = ( const { isMobile } = useMediaQueryContext(); const { stringSet } = useLocalization(); const { isOnline, userMention, logger, groupChannel } = config; - const threadContext = useThreadContext(); + const threadContext = useThread(); const { - currentChannel, - parentMessage, - sendMessage, - sendFileMessage, - sendVoiceMessage, - sendMultipleFilesMessage, - isMuted, - isChannelFrozen, - allThreadMessages, + state: { + currentChannel, + parentMessage, + isMuted, + isChannelFrozen, + allThreadMessages, + }, + actions: { + sendMessage, + sendFileMessage, + sendVoiceMessage, + sendMultipleFilesMessage, + }, } = threadContext; const messageInputRef = useRef(); const isMentionEnabled = groupChannel.enableMention; const isVoiceMessageEnabled = groupChannel.enableVoiceMessage; - const isMultipleFilesMessageEnabled = threadContext.isMultipleFilesMessageEnabled ?? config.isMultipleFilesMessageEnabled; + const isMultipleFilesMessageEnabled = threadContext.state.isMultipleFilesMessageEnabled ?? config.isMultipleFilesMessageEnabled; const threadInputDisabled = props.disabled || !isOnline diff --git a/src/modules/Thread/components/ThreadUI/index.tsx b/src/modules/Thread/components/ThreadUI/index.tsx index 915da2908..05f1cbf34 100644 --- a/src/modules/Thread/components/ThreadUI/index.tsx +++ b/src/modules/Thread/components/ThreadUI/index.tsx @@ -5,7 +5,6 @@ import './index.scss'; import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext'; import { useLocalization } from '../../../../lib/LocalizationContext'; import { getChannelTitle } from '../../../GroupChannel/components/GroupChannelHeader/utils'; -import { useThreadContext } from '../../context/ThreadProvider'; import { ParentMessageStateTypes, ThreadListStateTypes } from '../../types'; import ParentMessageInfo from '../ParentMessageInfo'; import ThreadHeader from '../ThreadHeader'; @@ -19,6 +18,7 @@ import { isAboutSame } from '../../context/utils'; import { MessageProvider } from '../../../Message/context/MessageProvider'; import { SendableMessageType, getHTMLTextDirection } from '../../../../utils'; import { classnames } from '../../../../utils/utils'; +import useThread from '../../context/useThread'; export interface ThreadUIProps { renderHeader?: () => React.ReactElement; @@ -59,18 +59,22 @@ const ThreadUI: React.FC = ({ stringSet, } = useLocalization(); const { - currentChannel, - allThreadMessages, - parentMessage, - parentMessageState, - threadListState, - hasMorePrev, - hasMoreNext, - fetchPrevThreads, - fetchNextThreads, - onHeaderActionClick, - onMoveToParentMessage, - } = useThreadContext(); + state: { + currentChannel, + allThreadMessages, + parentMessage, + parentMessageState, + threadListState, + hasMorePrev, + hasMoreNext, + onHeaderActionClick, + onMoveToParentMessage, + }, + actions: { + fetchPrevThreads, + fetchNextThreads, + }, + } = useThread(); const replyCount = allThreadMessages.length; const isByMe = currentUserId === parentMessage?.sender?.userId; diff --git a/src/modules/Thread/context/ThreadProvider.tsx b/src/modules/Thread/context/ThreadProvider.tsx index 28c727c4c..bb4da9a93 100644 --- a/src/modules/Thread/context/ThreadProvider.tsx +++ b/src/modules/Thread/context/ThreadProvider.tsx @@ -1,38 +1,28 @@ -import React, { useReducer, useMemo, useEffect } from 'react'; -import { type EmojiCategory } from '@sendbird/chat'; -import { GroupChannel } from '@sendbird/chat/groupChannel'; +import React, { useMemo, useEffect, useRef } from 'react'; +import { type EmojiCategory, EmojiContainer } from '@sendbird/chat'; +import { GroupChannel, Member } from '@sendbird/chat/groupChannel'; import type { - BaseMessage, FileMessage, - FileMessageCreateParams, MultipleFilesMessage, + FileMessageCreateParams, MultipleFilesMessageCreateParams, UserMessageCreateParams, } from '@sendbird/chat/message'; import { getNicknamesMapFromMembers, getParentMessageFrom } from './utils'; import { UserProfileProvider, UserProfileProviderProps } from '../../../lib/UserProfileContext'; -import { CustomUseReducerDispatcher } from '../../../lib/SendbirdState'; import useSendbirdStateContext from '../../../hooks/useSendbirdStateContext'; -import threadReducer from './dux/reducer'; -import { ThreadContextActionTypes } from './dux/actionTypes'; -import threadInitialState, { ThreadContextInitialState } from './dux/initialState'; - import type { OnBeforeDownloadFileMessageType } from '../../GroupChannel/context/types'; import useGetChannel from './hooks/useGetChannel'; import useGetAllEmoji from './hooks/useGetAllEmoji'; import useGetParentMessage from './hooks/useGetParentMessage'; import useHandleThreadPubsubEvents from './hooks/useHandleThreadPubsubEvents'; import useHandleChannelEvents from './hooks/useHandleChannelEvents'; -import useSendFileMessageCallback from './hooks/useSendFileMessage'; -import useUpdateMessageCallback from './hooks/useUpdateMessageCallback'; -import useDeleteMessageCallback from './hooks/useDeleteMessageCallback'; -import useToggleReactionCallback from './hooks/useToggleReactionsCallback'; -import useSendUserMessageCallback, { SendMessageParams } from './hooks/useSendUserMessageCallback'; -import useResendMessageCallback from './hooks/useResendMessageCallback'; -import useSendVoiceMessageCallback from './hooks/useSendVoiceMessageCallback'; -import { PublishingModuleType, useSendMultipleFilesMessage } from './hooks/useSendMultipleFilesMessage'; -import { SendableMessageType } from '../../../utils'; -import { useThreadFetchers } from './hooks/useThreadFetchers'; +import { CoreMessageType, SendableMessageType } from '../../../utils'; +import { createStore } from '../../../utils/storeManager'; +import { ChannelStateTypes, ParentMessageStateTypes, ThreadListStateTypes } from '../types'; +import { useStore } from '../../../hooks/useStore'; +import useSetCurrentUserId from './hooks/useSetCurrentUserId'; +import useThread from './useThread'; export interface ThreadProviderProps extends Pick { @@ -49,25 +39,80 @@ export interface ThreadProviderProps extends isMultipleFilesMessageEnabled?: boolean; filterEmojiCategoryIds?: (message: SendableMessageType) => EmojiCategory['id'][]; } -export interface ThreadProviderInterface extends ThreadProviderProps, ThreadContextInitialState { - // hooks for fetching threads - fetchPrevThreads: (callback?: (messages?: Array) => void) => void; - fetchNextThreads: (callback?: (messages?: Array) => void) => void; - toggleReaction: ReturnType; - sendMessage: (props: SendMessageParams) => void; - sendFileMessage: (file: File, quoteMessage?: SendableMessageType) => Promise; - sendVoiceMessage: ReturnType; - sendMultipleFilesMessage: (files: Array, quoteMessage?: SendableMessageType) => Promise, - resendMessage: (failedMessage: SendableMessageType) => void; - updateMessage: ReturnType; - deleteMessage: (message: SendableMessageType) => Promise; + +export interface ThreadState { + channelUrl: string; + message: SendableMessageType | null; + onHeaderActionClick?: () => void; + onMoveToParentMessage?: (props: { message: SendableMessageType, channel: GroupChannel }) => void; + onBeforeSendUserMessage?: (message: string, quotedMessage?: SendableMessageType) => UserMessageCreateParams; + onBeforeSendFileMessage?: (file: File, quotedMessage?: SendableMessageType) => FileMessageCreateParams; + onBeforeSendVoiceMessage?: (file: File, quotedMessage?: SendableMessageType) => FileMessageCreateParams; + onBeforeSendMultipleFilesMessage?: (files: Array, quotedMessage?: SendableMessageType) => MultipleFilesMessageCreateParams; + onBeforeDownloadFileMessage?: OnBeforeDownloadFileMessageType; + isMultipleFilesMessageEnabled?: boolean; + filterEmojiCategoryIds?: (message: SendableMessageType) => EmojiCategory['id'][]; + currentChannel: GroupChannel; + allThreadMessages: Array; + localThreadMessages: Array; + parentMessage: SendableMessageType; + channelState: ChannelStateTypes; + parentMessageState: ParentMessageStateTypes; + threadListState: ThreadListStateTypes; + hasMorePrev: boolean; + hasMoreNext: boolean; + emojiContainer: EmojiContainer; + isMuted: boolean; + isChannelFrozen: boolean; + currentUserId: string; + typingMembers: Member[]; nicknamesMap: Map; } -const ThreadContext = React.createContext(null); -export const ThreadProvider = (props: ThreadProviderProps) => { +const initialState = { + channelUrl: '', + message: null, + onHeaderActionClick: undefined, + onMoveToParentMessage: undefined, + onBeforeSendUserMessage: undefined, + onBeforeSendFileMessage: undefined, + onBeforeSendVoiceMessage: undefined, + onBeforeSendMultipleFilesMessage: undefined, + onBeforeDownloadFileMessage: undefined, + isMultipleFilesMessageEnabled: undefined, + filterEmojiCategoryIds: undefined, + currentChannel: null, + allThreadMessages: [], + localThreadMessages: [], + parentMessage: null, + channelState: ChannelStateTypes.NIL, + parentMessageState: ParentMessageStateTypes.NIL, + threadListState: ThreadListStateTypes.NIL, + hasMorePrev: false, + hasMoreNext: false, + emojiContainer: {} as EmojiContainer, + isMuted: false, + isChannelFrozen: false, + currentUserId: '', + typingMembers: [], + nicknamesMap: null, +}; + +export const ThreadContext = React.createContext> | null>(null); + +export const InternalThreadProvider: React.FC> = ({ children }) => { + const storeRef = useRef(createStore(initialState)); + + return ( + + {children} + + ); +}; + +export const ThreadManager: React.FC> = (props) => { const { - children, + message, channelUrl, onHeaderActionClick, onMoveToParentMessage, @@ -79,8 +124,19 @@ export const ThreadProvider = (props: ThreadProviderProps) => { isMultipleFilesMessageEnabled, filterEmojiCategoryIds, } = props; - const propsMessage = props?.message; - const propsParentMessage = getParentMessageFrom(propsMessage); + + const { + state: { + currentChannel, + parentMessage, + }, + actions: { + initializeThreadFetcher, + }, + } = useThread(); + const { updateState } = useThreadStore(); + + const propsParentMessage = getParentMessageFrom(message); // Context from SendbirdProvider const globalStore = useSendbirdStateContext(); const { stores, config } = globalStore; @@ -92,185 +148,95 @@ export const ThreadProvider = (props: ThreadProviderProps) => { // // config const { logger, pubSub } = config; - const isMentionEnabled = config.groupChannel.enableMention; - const isReactionEnabled = config.groupChannel.enableReactions; - - // dux of Thread - const [threadStore, threadDispatcher] = useReducer( - threadReducer, - threadInitialState, - ) as [ThreadContextInitialState, CustomUseReducerDispatcher]; - const { - currentChannel, - allThreadMessages, - localThreadMessages, - parentMessage, - channelState, - threadListState, - parentMessageState, - hasMorePrev, - hasMoreNext, - emojiContainer, - isMuted, - isChannelFrozen, - currentUserId, - typingMembers, - }: ThreadContextInitialState = threadStore; - // Initialization - useEffect(() => { - threadDispatcher({ - type: ThreadContextActionTypes.INIT_USER_ID, - payload: user?.userId, - }); - }, [user]); + useSetCurrentUserId({ user }); useGetChannel({ channelUrl, sdkInit, - message: propsMessage, - }, { sdk, logger, threadDispatcher }); + message, + }, { sdk, logger }); useGetParentMessage({ channelUrl, sdkInit, parentMessage: propsParentMessage, - }, { sdk, logger, threadDispatcher }); - useGetAllEmoji({ sdk }, { logger, threadDispatcher }); + }, { sdk, logger }); + useGetAllEmoji({ sdk }, { logger }); // Handle channel events useHandleChannelEvents({ sdk, currentChannel, - }, { logger, threadDispatcher }); + }, { logger }); useHandleThreadPubsubEvents({ sdkInit, currentChannel, parentMessage, - }, { logger, pubSub, threadDispatcher }); - - const { initialize, loadPrevious, loadNext } = useThreadFetchers({ - parentMessage, - // anchorMessage should be null when parentMessage doesn't exist - anchorMessage: propsMessage?.messageId !== propsParentMessage?.messageId ? propsMessage || undefined : undefined, - logger, - isReactionEnabled, - threadDispatcher, - threadListState, - oldestMessageTimeStamp: allThreadMessages[0]?.createdAt || 0, - latestMessageTimeStamp: allThreadMessages[allThreadMessages.length - 1]?.createdAt || 0, - }); + }, { logger, pubSub }); useEffect(() => { if (stores.sdkStore.initialized && config.isOnline) { - initialize(); + initializeThreadFetcher(); } - }, [stores.sdkStore.initialized, config.isOnline, initialize]); + }, [stores.sdkStore.initialized, config.isOnline, initializeThreadFetcher]); - const toggleReaction = useToggleReactionCallback({ currentChannel }, { logger }); + // memo + const nicknamesMap: Map = useMemo(() => ( + (config.groupChannel.replyType !== 'none' && currentChannel) + ? getNicknamesMapFromMembers(currentChannel?.members) + : new Map() + ), [currentChannel?.members]); - // Send Message Hooks - const sendMessage = useSendUserMessageCallback({ - isMentionEnabled, - currentChannel, + useEffect(() => { + updateState({ + channelUrl, + message, + onHeaderActionClick, + onMoveToParentMessage, + onBeforeSendUserMessage, + onBeforeSendFileMessage, + onBeforeSendVoiceMessage, + onBeforeSendMultipleFilesMessage, + onBeforeDownloadFileMessage, + isMultipleFilesMessageEnabled, + filterEmojiCategoryIds, + nicknamesMap, + }); + }, [ + channelUrl, + message, + onHeaderActionClick, + onMoveToParentMessage, onBeforeSendUserMessage, - }, { - logger, - pubSub, - threadDispatcher, - }); - const sendFileMessage = useSendFileMessageCallback({ - currentChannel, onBeforeSendFileMessage, - }, { - logger, - pubSub, - threadDispatcher, - }); - const sendVoiceMessage = useSendVoiceMessageCallback({ - currentChannel, onBeforeSendVoiceMessage, - }, { - logger, - pubSub, - threadDispatcher, - }); - const [sendMultipleFilesMessage] = useSendMultipleFilesMessage({ - currentChannel, onBeforeSendMultipleFilesMessage, - publishingModules: [PublishingModuleType.THREAD], - }, { - logger, - pubSub, - }); + onBeforeDownloadFileMessage, + isMultipleFilesMessageEnabled, + filterEmojiCategoryIds, + nicknamesMap, + ]); - const resendMessage = useResendMessageCallback({ - currentChannel, - }, { logger, pubSub, threadDispatcher }); - const updateMessage = useUpdateMessageCallback({ - currentChannel, - isMentionEnabled, - }, { logger, pubSub, threadDispatcher }); - const deleteMessage = useDeleteMessageCallback( - { currentChannel, threadDispatcher }, - { logger }, - ); + return null; +}; - // memo - const nicknamesMap: Map = useMemo(() => ( - (config.groupChannel.replyType !== 'none' && currentChannel) - ? getNicknamesMapFromMembers(currentChannel?.members) - : new Map() - ), [currentChannel?.members]); +export const ThreadProvider = (props: ThreadProviderProps) => { + const { children } = props; return ( - - {/* UserProfileProvider */} - - {children} - - + + + {/* UserProfileProvider */} + + {children} + + ); }; export const useThreadContext = () => { - const context = React.useContext(ThreadContext); - if (!context) throw new Error('ThreadContext not found. Use within the Thread module'); - return context; + const { state, actions } = useThread(); + return { ...state, ...actions }; +}; + +const useThreadStore = () => { + return useStore(ThreadContext, state => state, initialState); }; diff --git a/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx b/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx new file mode 100644 index 000000000..01b5e866c --- /dev/null +++ b/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx @@ -0,0 +1,215 @@ +import React from 'react'; +import { act, waitFor } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { ThreadProvider } from '../ThreadProvider'; +import useThread from '../useThread'; +import { SendableMessageType } from '../../../../utils'; + +const mockChannel = { + url: 'test-channel', + members: [{ userId: '1', nickname: 'user1' }], + updateUserMessage: jest.fn().mockImplementation(async () => mockNewMessage), +}; + +const mockNewMessage = { + messageId: 42, + message: 'new message', +}; + +const mockGetChannel = jest.fn().mockResolvedValue(mockChannel); + +jest.mock('../../../../hooks/useSendbirdStateContext', () => ({ + __esModule: true, + default: jest.fn(() => ({ + stores: { + sdkStore: { + sdk: { + groupChannel: { + getChannel: mockGetChannel, + }, + }, + initialized: true, + }, + userStore: { user: { userId: 'test-user-id' } }, + }, + config: { + logger: console, + pubSub: { + publish: jest.fn(), + }, + groupChannel: { + enableMention: true, + enableReactions: true, + }, + }, + })), +})); + +describe('ThreadProvider', () => { + const initialMockMessage = { + messageId: 1, + } as SendableMessageType; + + it('provides the correct initial state', async () => { + const wrapper = ({ children }) => ( + {children} + ); + + await act(async () => { + const { result } = renderHook(() => useThread(), { wrapper }); + await waitFor(() => { + expect(result.current.state.message).toBe(initialMockMessage); + }); + }); + + }); + + it('provides correct actions through useThread hook', async () => { + const wrapper = ({ children }) => ( + {children} + ); + + await act(async () => { + const { result } = renderHook(() => useThread(), { wrapper }); + await waitFor(() => { + expect(result.current.actions).toHaveProperty('toggleReaction'); + expect(result.current.actions).toHaveProperty('sendMessage'); + expect(result.current.actions).toHaveProperty('sendFileMessage'); + expect(result.current.actions).toHaveProperty('sendVoiceMessage'); + expect(result.current.actions).toHaveProperty('sendMultipleFilesMessage'); + expect(result.current.actions).toHaveProperty('resendMessage'); + expect(result.current.actions).toHaveProperty('initializeThreadFetcher'); + expect(result.current.actions).toHaveProperty('fetchPrevThreads'); + expect(result.current.actions).toHaveProperty('fetchNextThreads'); + expect(result.current.actions).toHaveProperty('updateMessage'); + expect(result.current.actions).toHaveProperty('deleteMessage'); + expect(result.current.actions).toHaveProperty('setCurrentUserId'); + expect(result.current.actions).toHaveProperty('getChannelStart'); + expect(result.current.actions).toHaveProperty('getChannelSuccess'); + expect(result.current.actions).toHaveProperty('getChannelFailure'); + expect(result.current.actions).toHaveProperty('getParentMessageStart'); + expect(result.current.actions).toHaveProperty('getParentMessageSuccess'); + expect(result.current.actions).toHaveProperty('getParentMessageFailure'); + expect(result.current.actions).toHaveProperty('setEmojiContainer'); + expect(result.current.actions).toHaveProperty('onMessageReceived'); + expect(result.current.actions).toHaveProperty('onMessageUpdated'); + expect(result.current.actions).toHaveProperty('onMessageDeleted'); + expect(result.current.actions).toHaveProperty('onMessageDeletedByReqId'); + expect(result.current.actions).toHaveProperty('onReactionUpdated'); + expect(result.current.actions).toHaveProperty('onUserMuted'); + expect(result.current.actions).toHaveProperty('onUserUnmuted'); + expect(result.current.actions).toHaveProperty('onUserBanned'); + expect(result.current.actions).toHaveProperty('onUserUnbanned'); + expect(result.current.actions).toHaveProperty('onUserLeft'); + expect(result.current.actions).toHaveProperty('onChannelFrozen'); + expect(result.current.actions).toHaveProperty('onChannelUnfrozen'); + expect(result.current.actions).toHaveProperty('onOperatorUpdated'); + expect(result.current.actions).toHaveProperty('onTypingStatusUpdated'); + expect(result.current.actions).toHaveProperty('sendMessageStart'); + expect(result.current.actions).toHaveProperty('sendMessageSuccess'); + expect(result.current.actions).toHaveProperty('sendMessageFailure'); + expect(result.current.actions).toHaveProperty('resendMessageStart'); + expect(result.current.actions).toHaveProperty('onFileInfoUpdated'); + expect(result.current.actions).toHaveProperty('initializeThreadListStart'); + expect(result.current.actions).toHaveProperty('initializeThreadListSuccess'); + expect(result.current.actions).toHaveProperty('initializeThreadListFailure'); + expect(result.current.actions).toHaveProperty('getPrevMessagesStart'); + expect(result.current.actions).toHaveProperty('getPrevMessagesSuccess'); + expect(result.current.actions).toHaveProperty('getPrevMessagesFailure'); + expect(result.current.actions).toHaveProperty('getNextMessagesStart'); + expect(result.current.actions).toHaveProperty('getNextMessagesSuccess'); + expect(result.current.actions).toHaveProperty('getNextMessagesFailure'); + }); + }); + + }); + + it('updates state when props change', async () => { + const wrapper = ({ children }) => ( + {}}> + {children} + + ); + + await act(async () => { + const { result } = renderHook(() => useThread(), { wrapper }); + + await waitFor(() => { + expect(result.current.state.currentChannel).not.toBe(undefined); + }); + + result.current.actions.setCurrentUserId('new-user-id'); + + await waitFor(() => { + expect(result.current.state.currentUserId).toEqual('new-user-id'); + }); + }); + }); + + // it('calls sendMessage correctly', async () => { + // const wrapper = ({ children }) => ( + // + // {children} + // + // ); + // + // const { result } = renderHook(() => useThreadContext(), { wrapper }); + // const sendMessageMock = jest.fn(); + // + // result.current.sendMessage({ message: 'Test Message' }); + // + // expect(sendMessageMock).toHaveBeenCalledWith({ message: 'Test Message' }); + // }); + // + // it('handles channel events correctly', () => { + // const wrapper = ({ children }) => ( + // + // {children} + // + // ); + // + // render(); + // // Add assertions for handling channel events + // }); + // + // it('updates state when nicknamesMap is updated', async () => { + // const wrapper = ({ children }) => ( + // + // {children} + // + // ); + // + // const { result } = renderHook(() => useThreadContext(), { wrapper }); + // + // await act(async () => { + // result.current.updateState({ + // nicknamesMap: new Map([['user1', 'User One'], ['user2', 'User Two']]), + // }); + // await waitFor(() => { + // expect(result.current.nicknamesMap.get('user1')).toBe('User One'); + // }); + // }); + // }); + // + // it('calls onMoveToParentMessage when provided', async () => { + // const onMoveToParentMessageMock = jest.fn(); + // const wrapper = ({ children }) => ( + // + // {children} + // + // ); + // + // const { result } = renderHook(() => useThreadContext(), { wrapper }); + // + // await act(async () => { + // result.current.onMoveToParentMessage({ message: { messageId: 1 }, channel: {} }); + // await waitFor(() => { + // expect(onMoveToParentMessageMock).toHaveBeenCalled(); + // }); + // }); + // }); +}); diff --git a/src/modules/Thread/context/dux/actionTypes.ts b/src/modules/Thread/context/dux/actionTypes.ts deleted file mode 100644 index 93e845f6c..000000000 --- a/src/modules/Thread/context/dux/actionTypes.ts +++ /dev/null @@ -1,48 +0,0 @@ -export enum ThreadContextActionTypes { - // initialize - INIT_USER_ID = 'INIT_USER_ID', - // channel - GET_CHANNEL_START = 'GET_CHANNEL_START', - GET_CHANNEL_SUCCESS = 'GET_CHANNEL_SUCCESS', - GET_CHANNEL_FAILURE = 'GET_CHANNEL_FAILURE', - // emojis - SET_EMOJI_CONTAINER = 'SET_EMOJI_CONTAINER', - // parent message - GET_PARENT_MESSAGE_START = 'GET_PARENT_MESSAGE_START', - GET_PARENT_MESSAGE_SUCCESS = 'GET_PARENT_MESSAGE_SUCCESS', - GET_PARENT_MESSAGE_FAILURE = 'GET_PARENT_MESSAGE_FAILURE', - // fetch threads - INITIALIZE_THREAD_LIST_START = 'INITIALIZE_THREAD_LIST_START', - INITIALIZE_THREAD_LIST_SUCCESS = 'INITIALIZE_THREAD_LIST_SUCCESS', - INITIALIZE_THREAD_LIST_FAILURE = 'INITIALIZE_THREAD_LIST_FAILURE', - GET_PREV_MESSAGES_START = 'GET_PREV_MESSAGES_START', - GET_PREV_MESSAGES_SUCESS = 'GET_PREV_MESSAGES_SUCESS', - GET_PREV_MESSAGES_FAILURE = 'GET_PREV_MESSAGES_FAILURE', - GET_NEXT_MESSAGES_START = 'GET_NEXT_MESSAGES_START', - GET_NEXT_MESSAGES_SUCESS = 'GET_NEXT_MESSAGES_SUCESS', - GET_NEXT_MESSAGES_FAILURE = 'GET_NEXT_MESSAGES_FAILURE', - // handle messages - SEND_MESSAGE_START = 'SEND_MESSAGE_START', - SEND_MESSAGE_SUCESS = 'SEND_MESSAGE_SUCESS', - SEND_MESSAGE_FAILURE = 'SEND_MESSAGE_FAILURE', - RESEND_MESSAGE_START = 'RESEND_MESSAGE_START', - ON_MESSAGE_DELETED_BY_REQ_ID = 'ON_MESSAGE_DELETED_BY_REQ_ID', - // event handlers - message status change - ON_MESSAGE_RECEIVED = 'ON_MESSAGE_RECEIVED', - ON_MESSAGE_UPDATED = 'ON_MESSAGE_UPDATED', - ON_MESSAGE_DELETED = 'ON_MESSAGE_DELETED', - ON_REACTION_UPDATED = 'ON_REACTION_UPDATED', - ON_FILE_INFO_UPLOADED = 'ON_FILE_INFO_UPLOADED', - // event handlers - user status change - ON_USER_MUTED = 'ON_USER_MUTED', - ON_USER_UNMUTED = 'ON_USER_UNMUTED', - ON_USER_BANNED = 'ON_USER_BANNED', - ON_USER_UNBANNED = 'ON_USER_UNBANNED', - ON_USER_LEFT = 'ON_USER_LEFT', - // event handler - channel status change - ON_CHANNEL_FROZEN = 'ON_CHANNEL_FROZEN', - ON_CHANNEL_UNFROZEN = 'ON_CHANNEL_UNFROZEN', - ON_OPERATOR_UPDATED = 'ON_OPERATOR_UPDATED', - // event handler - typing status change - ON_TYPING_STATUS_UPDATED = 'ON_TYPING_STATUS_UPDATED', -} diff --git a/src/modules/Thread/context/dux/initialState.ts b/src/modules/Thread/context/dux/initialState.ts deleted file mode 100644 index 8529c53d0..000000000 --- a/src/modules/Thread/context/dux/initialState.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { EmojiContainer } from '@sendbird/chat'; -import { GroupChannel, Member } from '@sendbird/chat/groupChannel'; -import { - ChannelStateTypes, - ParentMessageStateTypes, - ThreadListStateTypes, -} from '../../types'; -import { CoreMessageType, SendableMessageType } from '../../../../utils'; - -export interface ThreadContextInitialState { - currentChannel: GroupChannel; - allThreadMessages: Array; - localThreadMessages: Array; - parentMessage: SendableMessageType; - channelState: ChannelStateTypes; - parentMessageState: ParentMessageStateTypes; - threadListState: ThreadListStateTypes; - hasMorePrev: boolean; - hasMoreNext: boolean; - emojiContainer: EmojiContainer; - isMuted: boolean; - isChannelFrozen: boolean; - currentUserId: string; - typingMembers: Member[]; -} - -const initialState: ThreadContextInitialState = { - currentChannel: null, - allThreadMessages: [], - localThreadMessages: [], - parentMessage: null, - channelState: ChannelStateTypes.NIL, - parentMessageState: ParentMessageStateTypes.NIL, - threadListState: ThreadListStateTypes.NIL, - hasMorePrev: false, - hasMoreNext: false, - emojiContainer: {} as EmojiContainer, - isMuted: false, - isChannelFrozen: false, - currentUserId: '', - typingMembers: [], -}; - -export default initialState; diff --git a/src/modules/Thread/context/dux/reducer.ts b/src/modules/Thread/context/dux/reducer.ts deleted file mode 100644 index 68cbade91..000000000 --- a/src/modules/Thread/context/dux/reducer.ts +++ /dev/null @@ -1,403 +0,0 @@ -import { GroupChannel } from '@sendbird/chat/groupChannel'; -import { MultipleFilesMessage, ReactionEvent, UserMessage } from '@sendbird/chat/message'; -import { NEXT_THREADS_FETCH_SIZE, PREV_THREADS_FETCH_SIZE } from '../../consts'; -import { ChannelStateTypes, ParentMessageStateTypes, ThreadListStateTypes } from '../../types'; -import { compareIds } from '../utils'; -import { ThreadContextActionTypes as actionTypes } from './actionTypes'; -import { ThreadContextInitialState } from './initialState'; -import { SendableMessageType } from '../../../../utils'; - -interface ActionInterface { - type: actionTypes; - payload?: any; -} - -export default function reducer( - state: ThreadContextInitialState, - action: ActionInterface, -): ThreadContextInitialState { - switch (action.type) { - // initialize - case actionTypes.INIT_USER_ID: { - return { - ...state, - currentUserId: action.payload, - }; - } - case actionTypes.GET_CHANNEL_START: { - return { - ...state, - channelState: ChannelStateTypes.LOADING, - currentChannel: null, - }; - } - case actionTypes.GET_CHANNEL_SUCCESS: { - const groupChannel = action.payload.groupChannel as GroupChannel; - return { - ...state, - channelState: ChannelStateTypes.INITIALIZED, - currentChannel: groupChannel, - // only support in normal group channel - isMuted: groupChannel?.members?.find((member) => member?.userId === state.currentUserId)?.isMuted || false, - isChannelFrozen: groupChannel?.isFrozen || false, - }; - } - case actionTypes.GET_CHANNEL_FAILURE: { - return { - ...state, - channelState: ChannelStateTypes.INVALID, - currentChannel: null, - }; - } - case actionTypes.SET_EMOJI_CONTAINER: { - const { emojiContainer } = action.payload; - return { - ...state, - emojiContainer: emojiContainer, - }; - } - case actionTypes.GET_PARENT_MESSAGE_START: { - return { - ...state, - parentMessageState: ParentMessageStateTypes.LOADING, - parentMessage: null, - }; - } - case actionTypes.GET_PARENT_MESSAGE_SUCCESS: { - return { - ...state, - parentMessageState: ParentMessageStateTypes.INITIALIZED, - parentMessage: action.payload.parentMessage, - }; - } - case actionTypes.GET_PARENT_MESSAGE_FAILURE: { - return { - ...state, - parentMessageState: ParentMessageStateTypes.INVALID, - parentMessage: null, - }; - } - // fetch threads - case actionTypes.INITIALIZE_THREAD_LIST_START: { - return { - ...state, - threadListState: ThreadListStateTypes.LOADING, - allThreadMessages: [], - }; - } - case actionTypes.INITIALIZE_THREAD_LIST_SUCCESS: { - const { parentMessage, anchorMessage, threadedMessages } = action.payload; - const anchorMessageCreatedAt = (!anchorMessage?.messageId) ? parentMessage?.createdAt : anchorMessage?.createdAt; - const anchorIndex = threadedMessages.findIndex((message) => message?.createdAt > anchorMessageCreatedAt); - const prevThreadMessages = anchorIndex > -1 ? threadedMessages.slice(0, anchorIndex) : threadedMessages; - const anchorThreadMessage = anchorMessage?.messageId ? [anchorMessage] : []; - const nextThreadMessages = anchorIndex > -1 ? threadedMessages.slice(anchorIndex) : []; - return { - ...state, - threadListState: ThreadListStateTypes.INITIALIZED, - hasMorePrev: anchorIndex === -1 || anchorIndex === PREV_THREADS_FETCH_SIZE, - hasMoreNext: threadedMessages.length - anchorIndex === NEXT_THREADS_FETCH_SIZE, - allThreadMessages: [prevThreadMessages, anchorThreadMessage, nextThreadMessages].flat(), - }; - } - case actionTypes.INITIALIZE_THREAD_LIST_FAILURE: { - return { - ...state, - threadListState: ThreadListStateTypes.INVALID, - allThreadMessages: [], - }; - } - case actionTypes.GET_NEXT_MESSAGES_START: { - return { - ...state, - }; - } - case actionTypes.GET_NEXT_MESSAGES_SUCESS: { - const { threadedMessages } = action.payload; - return { - ...state, - hasMoreNext: threadedMessages.length === NEXT_THREADS_FETCH_SIZE, - allThreadMessages: [...state.allThreadMessages, ...threadedMessages], - }; - } - case actionTypes.GET_NEXT_MESSAGES_FAILURE: { - return { - ...state, - hasMoreNext: false, - }; - } - case actionTypes.GET_PREV_MESSAGES_START: { - return { - ...state, - }; - } - case actionTypes.GET_PREV_MESSAGES_SUCESS: { - const { threadedMessages } = action.payload; - return { - ...state, - hasMorePrev: threadedMessages.length === PREV_THREADS_FETCH_SIZE, - allThreadMessages: [...threadedMessages, ...state.allThreadMessages], - }; - } - case actionTypes.GET_PREV_MESSAGES_FAILURE: { - return { - ...state, - hasMorePrev: false, - }; - } - // event handlers - message status change - case actionTypes.ON_MESSAGE_RECEIVED: { - const { channel, message }: { channel: GroupChannel, message: SendableMessageType } = action.payload; - - if ( - state.currentChannel?.url !== channel?.url - || state.hasMoreNext - || message?.parentMessage?.messageId !== state?.parentMessage?.messageId - ) { - return state; - } - const isAlreadyReceived = state.allThreadMessages.findIndex((m) => ( - m.messageId === message.messageId - )) > -1; - return { - ...state, - parentMessage: state.parentMessage?.messageId === message?.messageId ? message : state.parentMessage, - allThreadMessages: isAlreadyReceived - ? state.allThreadMessages.map((m) => ( - m.messageId === message.messageId ? message : m - )) - : [ - ...state.allThreadMessages.filter((m) => (m as SendableMessageType)?.reqId !== message?.reqId), - message, - ], - }; - } - case actionTypes.ON_MESSAGE_UPDATED: { - const { channel, message } = action.payload; - if (state.currentChannel?.url !== channel?.url) { - return state; - } - return { - ...state, - parentMessage: state.parentMessage?.messageId === message?.messageId - ? message - : state.parentMessage, - allThreadMessages: state.allThreadMessages?.map((msg) => ( - (msg?.messageId === message?.messageId) ? message : msg - )), - }; - } - case actionTypes.ON_MESSAGE_DELETED: { - const { channel, messageId } = action.payload; - if (state.currentChannel?.url !== channel?.url) { - return state; - } - if (state?.parentMessage?.messageId === messageId) { - return { - ...state, - parentMessage: null, - parentMessageState: ParentMessageStateTypes.NIL, - allThreadMessages: [], - }; - } - return { - ...state, - allThreadMessages: state.allThreadMessages?.filter((msg) => ( - msg?.messageId !== messageId - )), - localThreadMessages: state.localThreadMessages?.filter((msg) => ( - msg?.messageId !== messageId - )), - }; - } - case actionTypes.ON_MESSAGE_DELETED_BY_REQ_ID: { - return { - ...state, - localThreadMessages: state.localThreadMessages.filter((m) => ( - !compareIds((m as SendableMessageType).reqId, action.payload) - )), - }; - } - case actionTypes.ON_REACTION_UPDATED: { - const reactionEvent = action.payload?.reactionEvent as ReactionEvent; - if (state?.parentMessage?.messageId === reactionEvent?.messageId) { - state.parentMessage?.applyReactionEvent?.(reactionEvent); - } - return { - ...state, - allThreadMessages: state.allThreadMessages.map((m) => { - if (reactionEvent?.messageId === m?.messageId) { - m?.applyReactionEvent?.(reactionEvent); - return m; - } - return m; - }), - }; - } - // event handlers - user status change - case actionTypes.ON_USER_MUTED: { - const { channel, user } = action.payload; - if (state.currentChannel?.url !== channel?.url || state.currentUserId !== user?.userId) { - return state; - } - return { - ...state, - isMuted: true, - }; - } - case actionTypes.ON_USER_UNMUTED: { - const { channel, user } = action.payload; - if (state.currentChannel?.url !== channel?.url || state.currentUserId !== user?.userId) { - return state; - } - return { - ...state, - isMuted: false, - }; - } - case actionTypes.ON_USER_BANNED: { - return { - ...state, - channelState: ChannelStateTypes.NIL, - threadListState: ThreadListStateTypes.NIL, - parentMessageState: ParentMessageStateTypes.NIL, - currentChannel: null, - parentMessage: null, - allThreadMessages: [], - hasMorePrev: false, - hasMoreNext: false, - }; - } - case actionTypes.ON_USER_UNBANNED: { - return { - ...state, - }; - } - case actionTypes.ON_USER_LEFT: { - return { - ...state, - channelState: ChannelStateTypes.NIL, - threadListState: ThreadListStateTypes.NIL, - parentMessageState: ParentMessageStateTypes.NIL, - currentChannel: null, - parentMessage: null, - allThreadMessages: [], - hasMorePrev: false, - hasMoreNext: false, - }; - } - // event handler - channel status change - case actionTypes.ON_CHANNEL_FROZEN: { - return { - ...state, - isChannelFrozen: true, - }; - } - case actionTypes.ON_CHANNEL_UNFROZEN: { - return { - ...state, - isChannelFrozen: false, - }; - } - case actionTypes.ON_OPERATOR_UPDATED: { - const { channel } = action.payload; - if (channel?.url === state.currentChannel?.url) { - return { - ...state, - currentChannel: channel, - }; - } - return state; - } - // message - case actionTypes.SEND_MESSAGE_START: { - const { message } = action.payload; - return { - ...state, - localThreadMessages: [ - ...state.localThreadMessages, - message, - ], - }; - } - case actionTypes.SEND_MESSAGE_SUCESS: { - const { message } = action.payload; - return { - ...state, - allThreadMessages: [ - ...state.allThreadMessages.filter((m) => ( - !compareIds((m as UserMessage)?.reqId, message?.reqId) - )), - message, - ], - localThreadMessages: state.localThreadMessages.filter((m) => ( - !compareIds((m as UserMessage)?.reqId, message?.reqId) - )), - }; - } - case actionTypes.SEND_MESSAGE_FAILURE: { - const { message } = action.payload; - return { - ...state, - localThreadMessages: state.localThreadMessages.map((m) => ( - compareIds((m as UserMessage)?.reqId, message?.reqId) - ? message - : m - )), - }; - } - case actionTypes.RESEND_MESSAGE_START: { - const { message } = action.payload; - return { - ...state, - localThreadMessages: state.localThreadMessages.map((m) => ( - compareIds((m as UserMessage)?.reqId, message?.reqId) - ? message - : m - )), - }; - } - case actionTypes.ON_FILE_INFO_UPLOADED: { - const { channelUrl, requestId, index, uploadableFileInfo, error } = action.payload; - if (!compareIds(channelUrl, state.currentChannel?.url)) { - return state; - } - /** - * We don't have to do anything here because - * onFailed() will be called so handle error there instead. - */ - if (error) return state; - const { localThreadMessages } = state; - const messageToUpdate = localThreadMessages.find((message) => compareIds(hasReqId(message) && message.reqId, requestId), - ); - const fileInfoList = (messageToUpdate as MultipleFilesMessage) - .messageParams?.fileInfoList; - if (Array.isArray(fileInfoList)) { - fileInfoList[index] = uploadableFileInfo; - } - return { - ...state, - localThreadMessages, - }; - } - case actionTypes.ON_TYPING_STATUS_UPDATED: { - const { channel, typingMembers } = action.payload; - if (!compareIds(channel.url, state.currentChannel?.url)) { - return state; - } - return { - ...state, - typingMembers, - }; - } - default: { - return state; - } - } -} - -function hasReqId( - message: T, -): message is T & { reqId: string } { - return 'reqId' in message; -} diff --git a/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts b/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts index 30c113931..021ca7d27 100644 --- a/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts @@ -1,12 +1,12 @@ import { GroupChannel } from '@sendbird/chat/groupChannel'; import { useCallback } from 'react'; -import { CustomUseReducerDispatcher, Logger } from '../../../../lib/SendbirdState'; -import { ThreadContextActionTypes } from '../dux/actionTypes'; +import { Logger } from '../../../../lib/SendbirdState'; import { SendableMessageType } from '../../../../utils'; interface DynamicProps { currentChannel: GroupChannel | null; - threadDispatcher: CustomUseReducerDispatcher; + onMessageDeletedByReqId: (reqId: string | number) => void, + onMessageDeleted: (channel: GroupChannel, messageId: number) => void, } interface StaticProps { logger: Logger; @@ -14,7 +14,8 @@ interface StaticProps { export default function useDeleteMessageCallback({ currentChannel, - threadDispatcher, + onMessageDeletedByReqId, + onMessageDeleted, }: DynamicProps, { logger, }: StaticProps): (message: SendableMessageType) => Promise { @@ -26,10 +27,7 @@ export default function useDeleteMessageCallback({ // Message is only on local if (sendingStatus === 'failed' || sendingStatus === 'pending') { logger.info('Thread | useDeleteMessageCallback: Deleted message from local:', message); - threadDispatcher({ - type: ThreadContextActionTypes.ON_MESSAGE_DELETED_BY_REQ_ID, - payload: message.reqId, - }); + onMessageDeletedByReqId(message.reqId); resolve(); } @@ -37,10 +35,7 @@ export default function useDeleteMessageCallback({ currentChannel?.deleteMessage?.(message) .then(() => { logger.info('Thread | useDeleteMessageCallback: Deleting message success!', message); - threadDispatcher({ - type: ThreadContextActionTypes.ON_MESSAGE_DELETED, - payload: { message, channel: currentChannel }, - }); + onMessageDeleted(currentChannel, message.messageId); resolve(); }) .catch((err) => { diff --git a/src/modules/Thread/context/hooks/useGetAllEmoji.ts b/src/modules/Thread/context/hooks/useGetAllEmoji.ts index 65a01ed37..e0a44a846 100644 --- a/src/modules/Thread/context/hooks/useGetAllEmoji.ts +++ b/src/modules/Thread/context/hooks/useGetAllEmoji.ts @@ -1,31 +1,32 @@ import { useEffect } from 'react'; -import { CustomUseReducerDispatcher, Logger } from '../../../../lib/SendbirdState'; -import { ThreadContextActionTypes } from '../dux/actionTypes'; +import { Logger } from '../../../../lib/SendbirdState'; import { SdkStore } from '../../../../lib/types'; +import useThread from '../useThread'; interface DanamicPrpos { sdk: SdkStore['sdk']; } interface StaticProps { logger: Logger; - threadDispatcher: CustomUseReducerDispatcher; } export default function useGetAllEmoji({ sdk, }: DanamicPrpos, { logger, - threadDispatcher, }: StaticProps): void { + const { + actions: { + setEmojiContainer, + }, + } = useThread(); + useEffect(() => { if (sdk?.getAllEmoji) { // validation check sdk?.getAllEmoji() .then((emojiContainer) => { logger.info('Thread | useGetAllEmoji: Getting emojis succeeded.', emojiContainer); - threadDispatcher({ - type: ThreadContextActionTypes.SET_EMOJI_CONTAINER, - payload: { emojiContainer }, - }); + setEmojiContainer(emojiContainer); }) .catch((error) => { logger.info('Thread | useGetAllEmoji: Getting emojis failed.', error); diff --git a/src/modules/Thread/context/hooks/useGetChannel.ts b/src/modules/Thread/context/hooks/useGetChannel.ts index 19b0d25f0..7319ba6a8 100644 --- a/src/modules/Thread/context/hooks/useGetChannel.ts +++ b/src/modules/Thread/context/hooks/useGetChannel.ts @@ -1,9 +1,9 @@ import { useEffect } from 'react'; import { Logger } from '../../../../lib/SendbirdState'; -import { ThreadContextActionTypes } from '../dux/actionTypes'; import { SendableMessageType } from '../../../../utils'; import { SdkStore } from '../../../../lib/types'; +import useThread from '../useThread'; interface DynamicProps { channelUrl: string; @@ -14,7 +14,6 @@ interface DynamicProps { interface StaticProps { sdk: SdkStore['sdk']; logger: Logger; - threadDispatcher: (props: { type: string, payload: any }) => void; } export default function useGetChannel({ @@ -24,29 +23,28 @@ export default function useGetChannel({ }: DynamicProps, { sdk, logger, - threadDispatcher, }: StaticProps): void { + const { + actions: { + getChannelStart, + getChannelSuccess, + getChannelFailure, + }, + } = useThread(); + useEffect(() => { // validation check if (sdkInit && channelUrl && sdk?.groupChannel) { - threadDispatcher({ - type: ThreadContextActionTypes.GET_CHANNEL_START, - payload: null, - }); + logger.info('Thread | useInitialize: Get channel started'); + getChannelStart(); sdk.groupChannel.getChannel?.(channelUrl) .then((groupChannel) => { logger.info('Thread | useInitialize: Get channel succeeded', groupChannel); - threadDispatcher({ - type: ThreadContextActionTypes.GET_CHANNEL_SUCCESS, - payload: { groupChannel }, - }); + getChannelSuccess(groupChannel); }) .catch((error) => { logger.info('Thread | useInitialize: Get channel failed', error); - threadDispatcher({ - type: ThreadContextActionTypes.GET_CHANNEL_FAILURE, - payload: error, - }); + getChannelFailure(); }); } }, [message, sdkInit]); diff --git a/src/modules/Thread/context/hooks/useGetParentMessage.ts b/src/modules/Thread/context/hooks/useGetParentMessage.ts index bc1b05185..adfcd491d 100644 --- a/src/modules/Thread/context/hooks/useGetParentMessage.ts +++ b/src/modules/Thread/context/hooks/useGetParentMessage.ts @@ -1,10 +1,10 @@ import { useEffect } from 'react'; -import { CustomUseReducerDispatcher, Logger } from '../../../../lib/SendbirdState'; +import { Logger } from '../../../../lib/SendbirdState'; import { BaseMessage, MessageRetrievalParams } from '@sendbird/chat/message'; -import { ThreadContextActionTypes } from '../dux/actionTypes'; import { ChannelType } from '@sendbird/chat'; import { SdkStore } from '../../../../lib/types'; +import useThread from '../useThread'; interface DynamicProps { channelUrl: string; @@ -15,7 +15,6 @@ interface DynamicProps { interface StaticProps { sdk: SdkStore['sdk']; logger: Logger; - threadDispatcher: CustomUseReducerDispatcher; } export default function useGetParentMessage({ @@ -25,15 +24,19 @@ export default function useGetParentMessage({ }: DynamicProps, { sdk, logger, - threadDispatcher, }: StaticProps): void { + const { + actions: { + getParentMessageStart, + getParentMessageSuccess, + getParentMessageFailure, + }, + } = useThread(); + useEffect(() => { // validation check if (sdkInit && sdk?.message?.getMessage && parentMessage) { - threadDispatcher({ - type: ThreadContextActionTypes.GET_PARENT_MESSAGE_START, - payload: null, - }); + getParentMessageStart(); const params: MessageRetrievalParams = { channelUrl, channelType: ChannelType.GROUP, @@ -49,17 +52,12 @@ export default function useGetParentMessage({ logger.info('Thread | useGetParentMessage: Get parent message succeeded.', parentMessage); // @ts-ignore parentMsg.ogMetaData = parentMessage?.ogMetaData || null;// ogMetaData is not included for now - threadDispatcher({ - type: ThreadContextActionTypes.GET_PARENT_MESSAGE_SUCCESS, - payload: { parentMessage: parentMsg }, - }); + // @ts-ignore + getParentMessageSuccess(parentMsg); }) .catch((error) => { logger.info('Thread | useGetParentMessage: Get parent message failed.', error); - threadDispatcher({ - type: ThreadContextActionTypes.GET_PARENT_MESSAGE_FAILURE, - payload: error, - }); + getParentMessageFailure(); }); } }, [sdkInit, parentMessage?.messageId]); diff --git a/src/modules/Thread/context/hooks/useHandleChannelEvents.ts b/src/modules/Thread/context/hooks/useHandleChannelEvents.ts index 363a7e3e2..2cd11c4cb 100644 --- a/src/modules/Thread/context/hooks/useHandleChannelEvents.ts +++ b/src/modules/Thread/context/hooks/useHandleChannelEvents.ts @@ -1,11 +1,11 @@ import { GroupChannel, GroupChannelHandler } from '@sendbird/chat/groupChannel'; import { useEffect } from 'react'; -import { CustomUseReducerDispatcher, Logger } from '../../../../lib/SendbirdState'; +import { Logger } from '../../../../lib/SendbirdState'; import uuidv4 from '../../../../utils/uuid'; -import { ThreadContextActionTypes } from '../dux/actionTypes'; import { SdkStore } from '../../../../lib/types'; import compareIds from '../../../../utils/compareIds'; -import * as messageActions from '../../../Channel/context/dux/actionTypes'; +import useThread from '../useThread'; +import { SendableMessageType } from '../../../../utils'; interface DynamicProps { sdk: SdkStore['sdk']; @@ -13,7 +13,6 @@ interface DynamicProps { } interface StaticProps { logger: Logger; - threadDispatcher: CustomUseReducerDispatcher; } export default function useHandleChannelEvents({ @@ -21,8 +20,25 @@ export default function useHandleChannelEvents({ currentChannel, }: DynamicProps, { logger, - threadDispatcher, }: StaticProps): void { + const { + actions: { + onMessageReceived, + onMessageUpdated, + onMessageDeleted, + onReactionUpdated, + onUserMuted, + onUserUnmuted, + onUserBanned, + onUserUnbanned, + onUserLeft, + onChannelFrozen, + onChannelUnfrozen, + onOperatorUpdated, + onTypingStatusUpdated, + }, + } = useThread(); + useEffect(() => { const handlerId = uuidv4(); // validation check @@ -33,101 +49,59 @@ export default function useHandleChannelEvents({ // message status change onMessageReceived(channel, message) { logger.info('Thread | useHandleChannelEvents: onMessageReceived', { channel, message }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_MESSAGE_RECEIVED, - payload: { channel, message }, - }); + onMessageReceived(channel as GroupChannel, message as SendableMessageType); }, onMessageUpdated(channel, message) { logger.info('Thread | useHandleChannelEvents: onMessageUpdated', { channel, message }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_MESSAGE_UPDATED, - payload: { channel, message }, - }); + onMessageUpdated(channel as GroupChannel, message as SendableMessageType); }, onMessageDeleted(channel, messageId) { logger.info('Thread | useHandleChannelEvents: onMessageDeleted', { channel, messageId }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_MESSAGE_DELETED, - payload: { channel, messageId }, - }); + onMessageDeleted(channel as GroupChannel, messageId); }, onReactionUpdated(channel, reactionEvent) { logger.info('Thread | useHandleChannelEvents: onReactionUpdated', { channel, reactionEvent }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_REACTION_UPDATED, - payload: { channel, reactionEvent }, - }); + onReactionUpdated(reactionEvent); }, // user status change onUserMuted(channel, user) { logger.info('Thread | useHandleChannelEvents: onUserMuted', { channel, user }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_USER_MUTED, - payload: { channel, user }, - }); + onUserMuted(channel as GroupChannel, user); }, onUserUnmuted(channel, user) { logger.info('Thread | useHandleChannelEvents: onUserUnmuted', { channel, user }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_USER_UNMUTED, - payload: { channel, user }, - }); + onUserUnmuted(channel as GroupChannel, user); }, onUserBanned(channel, user) { logger.info('Thread | useHandleChannelEvents: onUserBanned', { channel, user }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_USER_BANNED, - payload: { channel, user }, - }); + onUserBanned(); }, onUserUnbanned(channel, user) { logger.info('Thread | useHandleChannelEvents: onUserUnbanned', { channel, user }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_USER_UNBANNED, - payload: { channel, user }, - }); + onUserUnbanned(); }, onUserLeft(channel, user) { logger.info('Thread | useHandleChannelEvents: onUserLeft', { channel, user }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_USER_LEFT, - payload: { channel, user }, - }); + onUserLeft(); }, // channel status change onChannelFrozen(channel) { logger.info('Thread | useHandleChannelEvents: onChannelFrozen', { channel }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_CHANNEL_FROZEN, - payload: { channel }, - }); + onChannelFrozen(); }, onChannelUnfrozen(channel) { logger.info('Thread | useHandleChannelEvents: onChannelUnfrozen', { channel }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_CHANNEL_UNFROZEN, - payload: { channel }, - }); + onChannelUnfrozen(); }, onOperatorUpdated(channel, users) { logger.info('Thread | useHandleChannelEvents: onOperatorUpdated', { channel, users }); - threadDispatcher({ - type: ThreadContextActionTypes.ON_OPERATOR_UPDATED, - payload: { channel, users }, - }); + onOperatorUpdated(channel as GroupChannel); }, onTypingStatusUpdated: (channel) => { if (compareIds(channel?.url, currentChannel.url)) { logger.info('Channel | onTypingStatusUpdated', { channel }); const typingMembers = channel.getTypingUsers(); - threadDispatcher({ - type: messageActions.ON_TYPING_STATUS_UPDATED, - payload: { - channel, - typingMembers, - }, - }); + onTypingStatusUpdated(channel as GroupChannel, typingMembers); } }, }; diff --git a/src/modules/Thread/context/hooks/useHandleThreadPubsubEvents.ts b/src/modules/Thread/context/hooks/useHandleThreadPubsubEvents.ts index 8498c0387..022bac16b 100644 --- a/src/modules/Thread/context/hooks/useHandleThreadPubsubEvents.ts +++ b/src/modules/Thread/context/hooks/useHandleThreadPubsubEvents.ts @@ -1,12 +1,11 @@ import { useEffect } from 'react'; import { GroupChannel } from '@sendbird/chat/groupChannel'; -import { CustomUseReducerDispatcher, Logger } from '../../../../lib/SendbirdState'; +import { Logger } from '../../../../lib/SendbirdState'; import topics, { PUBSUB_TOPICS, SBUGlobalPubSub } from '../../../../lib/pubSub/topics'; import { scrollIntoLast } from '../utils'; -import { ThreadContextActionTypes } from '../dux/actionTypes'; import { SendableMessageType } from '../../../../utils'; -import * as channelActions from '../../../Channel/context/dux/actionTypes'; import { shouldPubSubPublishToThread } from '../../../internalInterfaces'; +import useThread from '../useThread'; interface DynamicProps { sdkInit: boolean; @@ -16,7 +15,6 @@ interface DynamicProps { interface StaticProps { logger: Logger; pubSub: SBUGlobalPubSub; - threadDispatcher: CustomUseReducerDispatcher; } export default function useHandleThreadPubsubEvents({ @@ -25,8 +23,18 @@ export default function useHandleThreadPubsubEvents({ parentMessage, }: DynamicProps, { pubSub, - threadDispatcher, }: StaticProps): void { + const { + actions: { + sendMessageStart, + sendMessageSuccess, + sendMessageFailure, + onFileInfoUpdated, + onMessageUpdated, + onMessageDeleted, + }, + } = useThread(); + useEffect(() => { const subscriber = new Map(); if (pubSub?.subscribe) { @@ -42,22 +50,14 @@ export default function useHandleThreadPubsubEvents({ url: URL.createObjectURL(fileInfo.file as File), })) ?? []; } - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_START, - payload: { - message: pendingMessage, - }, - }); + sendMessageStart(message); } scrollIntoLast?.(); })); subscriber.set(PUBSUB_TOPICS.ON_FILE_INFO_UPLOADED, pubSub.subscribe(PUBSUB_TOPICS.ON_FILE_INFO_UPLOADED, (props) => { const { response, publishingModules } = props; if (currentChannel?.url === response.channelUrl && shouldPubSubPublishToThread(publishingModules)) { - threadDispatcher({ - type: channelActions.ON_FILE_INFO_UPLOADED, - payload: response, - }); + onFileInfoUpdated(response); } })); subscriber.set(topics.SEND_USER_MESSAGE, pubSub.subscribe(topics.SEND_USER_MESSAGE, (props) => { @@ -68,29 +68,20 @@ export default function useHandleThreadPubsubEvents({ if (currentChannel?.url === channel?.url && message?.parentMessageId === parentMessage?.messageId ) { - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_SUCESS, - payload: { message }, - }); + sendMessageSuccess(message); } scrollIntoLast?.(); })); subscriber.set(topics.SEND_MESSAGE_FAILED, pubSub.subscribe(topics.SEND_MESSAGE_FAILED, (props) => { const { channel, message, publishingModules } = props; if (currentChannel?.url === channel?.url && message?.parentMessageId === parentMessage?.messageId && shouldPubSubPublishToThread(publishingModules)) { - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_FAILURE, - payload: { message }, - }); + sendMessageFailure(message); } })); subscriber.set(topics.SEND_FILE_MESSAGE, pubSub.subscribe(topics.SEND_FILE_MESSAGE, (props) => { const { channel, message, publishingModules } = props; if (currentChannel?.url === channel?.url && shouldPubSubPublishToThread(publishingModules)) { - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_SUCESS, - payload: { message }, - }); + sendMessageSuccess(message); } scrollIntoLast?.(); })); @@ -100,20 +91,12 @@ export default function useHandleThreadPubsubEvents({ message, } = props as { channel: GroupChannel, message: SendableMessageType }; if (currentChannel?.url === channel?.url) { - threadDispatcher({ - type: ThreadContextActionTypes.ON_MESSAGE_UPDATED, - payload: { channel, message }, - }); + onMessageUpdated(channel, message); } })); subscriber.set(topics.DELETE_MESSAGE, pubSub.subscribe(topics.DELETE_MESSAGE, (props) => { const { channel, messageId } = props as { channel: GroupChannel, messageId: number }; - if (currentChannel?.url === channel?.url) { - threadDispatcher({ - type: ThreadContextActionTypes.ON_MESSAGE_DELETED, - payload: { messageId }, - }); - } + onMessageDeleted(channel, messageId); })); } return () => { diff --git a/src/modules/Thread/context/hooks/useResendMessageCallback.ts b/src/modules/Thread/context/hooks/useResendMessageCallback.ts index aab62ec10..2e9d5e20c 100644 --- a/src/modules/Thread/context/hooks/useResendMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useResendMessageCallback.ts @@ -8,27 +8,30 @@ import { UserMessage, } from '@sendbird/chat/message'; import { useCallback } from 'react'; -import { CustomUseReducerDispatcher, Logger } from '../../../../lib/SendbirdState'; -import { ThreadContextActionTypes } from '../dux/actionTypes'; +import { Logger } from '../../../../lib/SendbirdState'; import topics, { SBUGlobalPubSub } from '../../../../lib/pubSub/topics'; import { SendableMessageType } from '../../../../utils'; import { PublishingModuleType } from '../../../internalInterfaces'; interface DynamicProps { currentChannel: GroupChannel | null; + resendMessageStart: (message: SendableMessageType) => void; + sendMessageSuccess: (message: SendableMessageType) => void; + sendMessageFailure: (message: SendableMessageType) => void; } interface StaticProps { logger: Logger; pubSub: SBUGlobalPubSub; - threadDispatcher: CustomUseReducerDispatcher; } export default function useResendMessageCallback({ currentChannel, + resendMessageStart, + sendMessageSuccess, + sendMessageFailure, }: DynamicProps, { logger, pubSub, - threadDispatcher, }: StaticProps): (failedMessage: SendableMessageType) => void { return useCallback((failedMessage: SendableMessageType) => { if ((failedMessage as SendableMessageType)?.isResendable) { @@ -38,17 +41,11 @@ export default function useResendMessageCallback({ currentChannel?.resendMessage(failedMessage as UserMessage) .onPending((message) => { logger.info('Thread | useResendMessageCallback: Resending user message started.', message); - threadDispatcher({ - type: ThreadContextActionTypes.RESEND_MESSAGE_START, - payload: { message }, - }); + resendMessageStart(message); }) .onSucceeded((message) => { logger.info('Thread | useResendMessageCallback: Resending user message succeeded.', message); - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_SUCESS, - payload: { message }, - }); + sendMessageSuccess(message); pubSub.publish(topics.SEND_USER_MESSAGE, { channel: currentChannel, message: message, @@ -58,35 +55,23 @@ export default function useResendMessageCallback({ .onFailed((error) => { logger.warning('Thread | useResendMessageCallback: Resending user message failed.', error); failedMessage.sendingStatus = SendingStatus.FAILED; - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_FAILURE, - payload: { message: failedMessage }, - }); + sendMessageFailure(failedMessage); }); } catch (err) { logger.warning('Thread | useResendMessageCallback: Resending user message failed.', err); failedMessage.sendingStatus = SendingStatus.FAILED; - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_FAILURE, - payload: { message: failedMessage }, - }); + sendMessageFailure(failedMessage); } } else if (failedMessage?.isFileMessage?.()) { try { currentChannel?.resendMessage?.(failedMessage as FileMessage) .onPending((message) => { logger.info('Thread | useResendMessageCallback: Resending file message started.', message); - threadDispatcher({ - type: ThreadContextActionTypes.RESEND_MESSAGE_START, - payload: { message }, - }); + resendMessageStart(message); }) .onSucceeded((message) => { logger.info('Thread | useResendMessageCallback: Resending file message succeeded.', message); - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_SUCESS, - payload: { message }, - }); + sendMessageSuccess(message); pubSub.publish(topics.SEND_FILE_MESSAGE, { channel: currentChannel, message: failedMessage, @@ -96,28 +81,19 @@ export default function useResendMessageCallback({ .onFailed((error) => { logger.warning('Thread | useResendMessageCallback: Resending file message failed.', error); failedMessage.sendingStatus = SendingStatus.FAILED; - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_FAILURE, - payload: { message: failedMessage }, - }); + sendMessageFailure(failedMessage); }); } catch (err) { logger.warning('Thread | useResendMessageCallback: Resending file message failed.', err); failedMessage.sendingStatus = SendingStatus.FAILED; - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_FAILURE, - payload: { message: failedMessage }, - }); + sendMessageFailure(failedMessage); } } else if (failedMessage?.isMultipleFilesMessage?.()) { try { currentChannel?.resendMessage?.(failedMessage as MultipleFilesMessage) .onPending((message) => { logger.info('Thread | useResendMessageCallback: Resending multiple files message started.', message); - threadDispatcher({ - type: ThreadContextActionTypes.RESEND_MESSAGE_START, - payload: { message }, - }); + resendMessageStart(message); }) .onFileUploaded((requestId, index, uploadableFileInfo: UploadableFileInfo, error) => { logger.info('Thread | useResendMessageCallback: onFileUploaded during resending multiple files message.', { @@ -139,10 +115,7 @@ export default function useResendMessageCallback({ }) .onSucceeded((message: MultipleFilesMessage) => { logger.info('Thread | useResendMessageCallback: Resending MFM succeeded.', message); - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_SUCESS, - payload: { message }, - }); + sendMessageSuccess(message); pubSub.publish(topics.SEND_FILE_MESSAGE, { channel: currentChannel, message, @@ -151,25 +124,16 @@ export default function useResendMessageCallback({ }) .onFailed((error, message) => { logger.warning('Thread | useResendMessageCallback: Resending MFM failed.', error); - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_FAILURE, - payload: { message }, - }); + sendMessageFailure(message); }); } catch (err) { logger.warning('Thread | useResendMessageCallback: Resending MFM failed.', err); - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_FAILURE, - payload: { message: failedMessage }, - }); + sendMessageFailure(failedMessage); } } else { logger.warning('Thread | useResendMessageCallback: Message is not resendable.', failedMessage); failedMessage.sendingStatus = SendingStatus.FAILED; - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_FAILURE, - payload: { message: failedMessage }, - }); + sendMessageFailure(failedMessage); } } }, [currentChannel]); diff --git a/src/modules/Thread/context/hooks/useSendFileMessage.ts b/src/modules/Thread/context/hooks/useSendFileMessage.ts index 1c9d07924..cf7babd54 100644 --- a/src/modules/Thread/context/hooks/useSendFileMessage.ts +++ b/src/modules/Thread/context/hooks/useSendFileMessage.ts @@ -1,9 +1,8 @@ import { useCallback } from 'react'; import { GroupChannel } from '@sendbird/chat/groupChannel'; -import { FileMessage, FileMessageCreateParams } from '@sendbird/chat/message'; +import { FileMessage, FileMessageCreateParams, SendingStatus } from '@sendbird/chat/message'; -import { CustomUseReducerDispatcher, Logger } from '../../../../lib/SendbirdState'; -import { ThreadContextActionTypes } from '../dux/actionTypes'; +import { Logger } from '../../../../lib/SendbirdState'; import topics, { SBUGlobalPubSub } from '../../../../lib/pubSub/topics'; import { scrollIntoLast } from '../utils'; import { SendableMessageType } from '../../../../utils'; @@ -13,11 +12,12 @@ import { SCROLL_BOTTOM_DELAY_FOR_SEND } from '../../../../utils/consts'; interface DynamicProps { currentChannel: GroupChannel | null; onBeforeSendFileMessage?: (file: File, quotedMessage?: SendableMessageType) => FileMessageCreateParams; + sendMessageStart: (message: SendableMessageType) => void; + sendMessageFailure: (message: SendableMessageType) => void; } interface StaticProps { logger: Logger; pubSub: SBUGlobalPubSub; - threadDispatcher: CustomUseReducerDispatcher; } interface LocalFileMessage extends FileMessage { @@ -30,10 +30,11 @@ export type SendFileMessageFunctionType = (file: File, quoteMessage?: SendableMe export default function useSendFileMessageCallback({ currentChannel, onBeforeSendFileMessage, + sendMessageStart, + sendMessageFailure, }: DynamicProps, { logger, pubSub, - threadDispatcher, }: StaticProps): SendFileMessageFunctionType { return useCallback((file, quoteMessage): Promise => { return new Promise((resolve, reject) => { @@ -51,23 +52,16 @@ export default function useSendFileMessageCallback({ currentChannel?.sendFileMessage(params) .onPending((pendingMessage) => { - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_START, - payload: { - /* pubSub is used instead of messagesDispatcher - to avoid redundantly calling `messageActionTypes.SEND_MESSAGE_START` */ - // TODO: remove data pollution - message: { - ...pendingMessage, - url: URL.createObjectURL(file), - // pending thumbnail message seems to be failed - requestState: 'pending', - isUserMessage: pendingMessage.isUserMessage, - isFileMessage: pendingMessage.isFileMessage, - isAdminMessage: pendingMessage.isAdminMessage, - isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, - }, - }, + // @ts-ignore + sendMessageStart({ + ...pendingMessage, + url: URL.createObjectURL(file), + // pending thumbnail message seems to be failed + sendingStatus: SendingStatus.PENDING, + isUserMessage: pendingMessage.isUserMessage, + isFileMessage: pendingMessage.isFileMessage, + isAdminMessage: pendingMessage.isAdminMessage, + isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, }); setTimeout(() => scrollIntoLast(), SCROLL_BOTTOM_DELAY_FOR_SEND); }) @@ -75,10 +69,7 @@ export default function useSendFileMessageCallback({ (message as LocalFileMessage).localUrl = URL.createObjectURL(file); (message as LocalFileMessage).file = file; logger.info('Thread | useSendFileMessageCallback: Sending file message failed.', { message, error }); - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_FAILURE, - payload: { message, error }, - }); + sendMessageFailure(message as SendableMessageType); reject(error); }) .onSucceeded((message) => { diff --git a/src/modules/Thread/context/hooks/useSendUserMessageCallback.ts b/src/modules/Thread/context/hooks/useSendUserMessageCallback.ts index ae14fa174..5ef9ff216 100644 --- a/src/modules/Thread/context/hooks/useSendUserMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useSendUserMessageCallback.ts @@ -3,8 +3,7 @@ import { GroupChannel } from '@sendbird/chat/groupChannel'; import { UserMessage, UserMessageCreateParams } from '@sendbird/chat/message'; import { User } from '@sendbird/chat'; -import { CustomUseReducerDispatcher, Logger } from '../../../../lib/SendbirdState'; -import { ThreadContextActionTypes } from '../dux/actionTypes'; +import { Logger } from '../../../../lib/SendbirdState'; import topics, { SBUGlobalPubSub } from '../../../../lib/pubSub/topics'; import { SendableMessageType } from '../../../../utils'; import { PublishingModuleType } from '../../../internalInterfaces'; @@ -14,11 +13,12 @@ interface DynamicProps { isMentionEnabled: boolean; currentChannel: GroupChannel | null; onBeforeSendUserMessage?: OnBeforeSendUserMessageType; + sendMessageStart: (message: SendableMessageType) => void; + sendMessageFailure: (message: SendableMessageType) => void; } interface StaticProps { logger: Logger; pubSub: SBUGlobalPubSub; - threadDispatcher: CustomUseReducerDispatcher; } export type SendMessageParams = { @@ -32,10 +32,11 @@ export default function useSendUserMessageCallback({ isMentionEnabled, currentChannel, onBeforeSendUserMessage, + sendMessageStart, + sendMessageFailure, }: DynamicProps, { logger, pubSub, - threadDispatcher, }: StaticProps): (props: SendMessageParams) => void { const sendMessage = useCallback((props: SendMessageParams) => { const { @@ -44,6 +45,7 @@ export default function useSendUserMessageCallback({ mentionTemplate, mentionedUsers, } = props; + const createDefaultParams = () => { const params = {} as UserMessageCreateParams; params.message = message; @@ -67,17 +69,11 @@ export default function useSendUserMessageCallback({ if (currentChannel?.sendUserMessage) { currentChannel?.sendUserMessage(params) .onPending((pendingMessage) => { - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_START, - payload: { message: pendingMessage }, - }); + sendMessageStart(pendingMessage as SendableMessageType); }) .onFailed((error, message) => { logger.info('Thread | useSendUserMessageCallback: Sending user message failed.', { message, error }); - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_FAILURE, - payload: { error, message }, - }); + sendMessageFailure(message as SendableMessageType); }) .onSucceeded((message) => { logger.info('Thread | useSendUserMessageCallback: Sending user message succeeded.', message); diff --git a/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts b/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts index 987d73e5e..c4632826f 100644 --- a/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts @@ -1,8 +1,7 @@ import { useCallback } from 'react'; import { GroupChannel } from '@sendbird/chat/groupChannel'; -import { FileMessage, FileMessageCreateParams, MessageMetaArray } from '@sendbird/chat/message'; -import { CustomUseReducerDispatcher, Logger } from '../../../../lib/SendbirdState'; -import { ThreadContextActionTypes } from '../dux/actionTypes'; +import { FileMessage, FileMessageCreateParams, MessageMetaArray, SendingStatus } from '@sendbird/chat/message'; +import { Logger } from '../../../../lib/SendbirdState'; import topics, { SBUGlobalPubSub } from '../../../../lib/pubSub/topics'; import { scrollIntoLast } from '../utils'; import { @@ -19,11 +18,12 @@ import { PublishingModuleType } from '../../../internalInterfaces'; interface DynamicParams { currentChannel: GroupChannel | null; onBeforeSendVoiceMessage?: (file: File, quoteMessage?: SendableMessageType) => FileMessageCreateParams; + sendMessageStart: (message: SendableMessageType) => void; + sendMessageFailure: (message: SendableMessageType) => void; } interface StaticParams { logger: Logger; pubSub: SBUGlobalPubSub; - threadDispatcher: CustomUseReducerDispatcher; } type FuncType = (file: File, duration: number, quoteMessage: SendableMessageType) => void; interface LocalFileMessage extends FileMessage { @@ -34,11 +34,12 @@ interface LocalFileMessage extends FileMessage { export const useSendVoiceMessageCallback = ({ currentChannel, onBeforeSendVoiceMessage, + sendMessageStart, + sendMessageFailure, }: DynamicParams, { logger, pubSub, - threadDispatcher, }: StaticParams): FuncType => { const sendMessage = useCallback((file: File, duration: number, quoteMessage: SendableMessageType) => { const messageParams: FileMessageCreateParams = ( @@ -68,23 +69,19 @@ export const useSendVoiceMessageCallback = ({ logger.info('Thread | useSendVoiceMessageCallback: Start sending voice message', messageParams); currentChannel?.sendFileMessage(messageParams) .onPending((pendingMessage) => { - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_START, - payload: { - /* pubSub is used instead of messagesDispatcher + // @ts-ignore + sendMessageStart({ + /* pubSub is used instead of messagesDispatcher to avoid redundantly calling `messageActionTypes.SEND_MESSAGE_START` */ - // TODO: remove data pollution - message: { - ...pendingMessage, - url: URL.createObjectURL(file), - // pending thumbnail message seems to be failed - requestState: 'pending', - isUserMessage: pendingMessage.isUserMessage, - isFileMessage: pendingMessage.isFileMessage, - isAdminMessage: pendingMessage.isAdminMessage, - isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, - }, - }, + // TODO: remove data pollution + ...pendingMessage, + url: URL.createObjectURL(file), + // pending thumbnail message seems to be failed + sendingStatus: SendingStatus.PENDING, + isUserMessage: pendingMessage.isUserMessage, + isFileMessage: pendingMessage.isFileMessage, + isAdminMessage: pendingMessage.isAdminMessage, + isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, }); setTimeout(() => scrollIntoLast(), SCROLL_BOTTOM_DELAY_FOR_SEND); }) @@ -92,10 +89,7 @@ export const useSendVoiceMessageCallback = ({ (message as LocalFileMessage).localUrl = URL.createObjectURL(file); (message as LocalFileMessage).file = file; logger.info('Thread | useSendVoiceMessageCallback: Sending voice message failed.', { message, error }); - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_FAILURE, - payload: { message, error }, - }); + sendMessageFailure(message as SendableMessageType); }) .onSucceeded((message) => { logger.info('Thread | useSendVoiceMessageCallback: Sending voice message succeeded.', message); diff --git a/src/modules/Thread/context/hooks/useSetCurrentUserId.ts b/src/modules/Thread/context/hooks/useSetCurrentUserId.ts new file mode 100644 index 000000000..fe3e02db8 --- /dev/null +++ b/src/modules/Thread/context/hooks/useSetCurrentUserId.ts @@ -0,0 +1,23 @@ +import useThread from '../useThread'; +import { useEffect } from 'react'; +import type { User } from '@sendbird/chat'; + +interface DynamicParams { + user: User | null; +} + +function useSetCurrentUserId( + { user }: DynamicParams, +) { + const { + actions: { + setCurrentUserId, + }, + } = useThread(); + + useEffect(() => { + setCurrentUserId(user?.userId); + }, [user?.userId]); +} + +export default useSetCurrentUserId; diff --git a/src/modules/Thread/context/hooks/useThreadFetchers.ts b/src/modules/Thread/context/hooks/useThreadFetchers.ts index 7947d276a..fa9e4d642 100644 --- a/src/modules/Thread/context/hooks/useThreadFetchers.ts +++ b/src/modules/Thread/context/hooks/useThreadFetchers.ts @@ -1,8 +1,6 @@ -import { ThreadContextActionTypes } from '../dux/actionTypes'; import { NEXT_THREADS_FETCH_SIZE, PREV_THREADS_FETCH_SIZE } from '../../consts'; import { BaseMessage, ThreadedMessageListParams } from '@sendbird/chat/message'; -import { SendableMessageType } from '../../../../utils'; -import { CustomUseReducerDispatcher } from '../../../../lib/SendbirdState'; +import { CoreMessageType, SendableMessageType } from '../../../../utils'; import { LoggerInterface } from '../../../../lib/Logger'; import { useCallback } from 'react'; import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext'; @@ -12,11 +10,19 @@ type Params = { anchorMessage?: SendableMessageType; parentMessage: SendableMessageType | null; isReactionEnabled?: boolean; - threadDispatcher: CustomUseReducerDispatcher; logger: LoggerInterface; threadListState: ThreadListStateTypes; oldestMessageTimeStamp: number; latestMessageTimeStamp: number; + initializeThreadListStart: () => void, + initializeThreadListSuccess: (parentMessage: BaseMessage, anchorMessage: SendableMessageType, threadedMessages: BaseMessage[]) => void, + initializeThreadListFailure: () => void, + getPrevMessagesStart: () => void, + getPrevMessagesSuccess: (threadedMessages: CoreMessageType[]) => void, + getPrevMessagesFailure: () => void, + getNextMessagesStart: () => void, + getNextMessagesSuccess: (threadedMessages: CoreMessageType[]) => void, + getNextMessagesFailure: () => void, }; function getThreadMessageListParams(params?: Partial): ThreadedMessageListParams { @@ -32,11 +38,19 @@ export const useThreadFetchers = ({ isReactionEnabled, anchorMessage, parentMessage: staleParentMessage, - threadDispatcher, logger, oldestMessageTimeStamp, latestMessageTimeStamp, threadListState, + initializeThreadListStart, + initializeThreadListSuccess, + initializeThreadListFailure, + getPrevMessagesStart, + getPrevMessagesSuccess, + getPrevMessagesFailure, + getNextMessagesStart, + getNextMessagesSuccess, + getNextMessagesFailure, }: Params) => { const { stores } = useSendbirdStateContext(); const timestamp = anchorMessage?.createdAt || 0; @@ -45,10 +59,7 @@ export const useThreadFetchers = ({ async (callback?: (messages: BaseMessage[]) => void) => { if (!stores.sdkStore.initialized || !staleParentMessage) return; - threadDispatcher({ - type: ThreadContextActionTypes.INITIALIZE_THREAD_LIST_START, - payload: null, - }); + initializeThreadListStart(); try { const params = getThreadMessageListParams({ includeReactions: isReactionEnabled }); @@ -56,17 +67,11 @@ export const useThreadFetchers = ({ const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(timestamp, params); logger.info('Thread | useGetThreadList: Initialize thread list succeeded.', { staleParentMessage, threadedMessages }); - threadDispatcher({ - type: ThreadContextActionTypes.INITIALIZE_THREAD_LIST_SUCCESS, - payload: { parentMessage, anchorMessage, threadedMessages }, - }); + initializeThreadListSuccess(parentMessage, anchorMessage, threadedMessages); setTimeout(() => callback?.(threadedMessages)); } catch (error) { logger.info('Thread | useGetThreadList: Initialize thread list failed.', error); - threadDispatcher({ - type: ThreadContextActionTypes.INITIALIZE_THREAD_LIST_FAILURE, - payload: error, - }); + initializeThreadListFailure(); } }, [stores.sdkStore.initialized, staleParentMessage, anchorMessage, isReactionEnabled], @@ -76,10 +81,7 @@ export const useThreadFetchers = ({ async (callback?: (messages: BaseMessage[]) => void) => { if (threadListState !== ThreadListStateTypes.INITIALIZED || oldestMessageTimeStamp === 0 || !staleParentMessage) return; - threadDispatcher({ - type: ThreadContextActionTypes.GET_PREV_MESSAGES_START, - payload: null, - }); + getPrevMessagesStart(); try { const params = getThreadMessageListParams({ nextResultSize: 0, includeReactions: isReactionEnabled }); @@ -87,17 +89,11 @@ export const useThreadFetchers = ({ const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(oldestMessageTimeStamp, params); logger.info('Thread | useGetPrevThreadsCallback: Fetch prev threads succeeded.', { parentMessage, threadedMessages }); - threadDispatcher({ - type: ThreadContextActionTypes.GET_PREV_MESSAGES_SUCESS, - payload: { parentMessage, threadedMessages }, - }); + getPrevMessagesSuccess(threadedMessages as CoreMessageType[]); setTimeout(() => callback?.(threadedMessages)); } catch (error) { logger.info('Thread | useGetPrevThreadsCallback: Fetch prev threads failed.', error); - threadDispatcher({ - type: ThreadContextActionTypes.GET_PREV_MESSAGES_FAILURE, - payload: error, - }); + getPrevMessagesFailure(); } }, [threadListState, oldestMessageTimeStamp, isReactionEnabled, staleParentMessage], @@ -107,35 +103,26 @@ export const useThreadFetchers = ({ async (callback?: (messages: BaseMessage[]) => void) => { if (threadListState !== ThreadListStateTypes.INITIALIZED || latestMessageTimeStamp === 0 || !staleParentMessage) return; - threadDispatcher({ - type: ThreadContextActionTypes.GET_NEXT_MESSAGES_START, - payload: null, - }); + getNextMessagesStart(); try { const params = getThreadMessageListParams({ prevResultSize: 0, includeReactions: isReactionEnabled }); const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(latestMessageTimeStamp, params); logger.info('Thread | useGetNextThreadsCallback: Fetch next threads succeeded.', { parentMessage, threadedMessages }); - threadDispatcher({ - type: ThreadContextActionTypes.GET_NEXT_MESSAGES_SUCESS, - payload: { parentMessage, threadedMessages }, - }); + getNextMessagesSuccess(threadedMessages as CoreMessageType[]); setTimeout(() => callback?.(threadedMessages)); } catch (error) { logger.info('Thread | useGetNextThreadsCallback: Fetch next threads failed.', error); - threadDispatcher({ - type: ThreadContextActionTypes.GET_NEXT_MESSAGES_FAILURE, - payload: error, - }); + getNextMessagesFailure(); } }, [threadListState, latestMessageTimeStamp, isReactionEnabled, staleParentMessage], ); return { - initialize, - loadPrevious, - loadNext, + initializeThreadFetcher: initialize, + fetchPrevThreads: loadPrevious, + fetchNextThreads: loadNext, }; }; diff --git a/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts b/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts index db58e4a2b..fb5a981e4 100644 --- a/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts @@ -3,20 +3,20 @@ import { User } from '@sendbird/chat'; import { GroupChannel } from '@sendbird/chat/groupChannel'; import { UserMessage, UserMessageUpdateParams } from '@sendbird/chat/message'; -import { CustomUseReducerDispatcher, Logger } from '../../../../lib/SendbirdState'; -import { ThreadContextActionTypes } from '../dux/actionTypes'; +import { Logger } from '../../../../lib/SendbirdState'; import topics, { SBUGlobalPubSub } from '../../../../lib/pubSub/topics'; import { PublishingModuleType } from '../../../internalInterfaces'; +import { SendableMessageType } from '../../../../utils'; interface DynamicProps { currentChannel: GroupChannel | null; isMentionEnabled?: boolean; + onMessageUpdated: (currentChannel: GroupChannel, message: SendableMessageType) => void; } interface StaticProps { logger: Logger; pubSub: SBUGlobalPubSub; - threadDispatcher: CustomUseReducerDispatcher; } type CallbackParams = { @@ -29,10 +29,10 @@ type CallbackParams = { export default function useUpdateMessageCallback({ currentChannel, isMentionEnabled, + onMessageUpdated, }: DynamicProps, { logger, pubSub, - threadDispatcher, }: StaticProps) { // TODO: add type return useCallback((props: CallbackParams) => { @@ -42,6 +42,7 @@ export default function useUpdateMessageCallback({ mentionedUsers, mentionTemplate, } = props; + const createParamsDefault = () => { const params = {} as UserMessageUpdateParams; params.message = message; @@ -62,13 +63,7 @@ export default function useUpdateMessageCallback({ currentChannel?.updateUserMessage?.(messageId, params) .then((message: UserMessage) => { logger.info('Thread | useUpdateMessageCallback: Message update succeeded.', message); - threadDispatcher({ - type: ThreadContextActionTypes.ON_MESSAGE_UPDATED, - payload: { - channel: currentChannel, - message: message, - }, - }); + onMessageUpdated(currentChannel, message); pubSub.publish( topics.UPDATE_USER_MESSAGE, { diff --git a/src/modules/Thread/context/useThread.ts b/src/modules/Thread/context/useThread.ts new file mode 100644 index 000000000..58e3f352c --- /dev/null +++ b/src/modules/Thread/context/useThread.ts @@ -0,0 +1,546 @@ +import { useSyncExternalStore } from 'use-sync-external-store/shim'; +import { useContext, useMemo } from 'react'; +import { ThreadContext, ThreadState } from './ThreadProvider'; +import { ChannelStateTypes, FileUploadInfoParams, ParentMessageStateTypes, ThreadListStateTypes } from '../types'; +import { GroupChannel, Member } from '@sendbird/chat/groupChannel'; +import { CoreMessageType, SendableMessageType } from '../../../utils'; +import { EmojiContainer, User } from '@sendbird/chat'; +import { compareIds } from './utils'; +import { + BaseMessage, + MultipleFilesMessage, + ReactionEvent, + UserMessage, +} from '@sendbird/chat/message'; +import { NEXT_THREADS_FETCH_SIZE, PREV_THREADS_FETCH_SIZE } from '../consts'; +import useToggleReactionCallback from './hooks/useToggleReactionsCallback'; +import useSendbirdStateContext from '../../../hooks/useSendbirdStateContext'; +import useSendUserMessageCallback from './hooks/useSendUserMessageCallback'; +import { PublishingModuleType } from '../../../lib/pubSub/topics'; + +import useSendFileMessageCallback from './hooks/useSendFileMessage'; +import useSendVoiceMessageCallback from './hooks/useSendVoiceMessageCallback'; +import { useSendMultipleFilesMessage } from '../../Channel/context/hooks/useSendMultipleFilesMessage'; +import useResendMessageCallback from './hooks/useResendMessageCallback'; +import useUpdateMessageCallback from './hooks/useUpdateMessageCallback'; +import useDeleteMessageCallback from './hooks/useDeleteMessageCallback'; +import { useThreadFetchers } from './hooks/useThreadFetchers'; + +function hasReqId( + message: T, +): message is T & { reqId: string } { + return 'reqId' in message; +} + +const useThread = () => { + const store = useContext(ThreadContext); + if (!store) throw new Error('useCreateChannel must be used within a CreateChannelProvider'); + + // SendbirdStateContext config + const { stores, config } = useSendbirdStateContext(); + const { logger, pubSub } = config; + const isMentionEnabled = config.groupChannel.enableMention; + const isReactionEnabled = config.groupChannel.enableReactions; + + const state: ThreadState = useSyncExternalStore(store.subscribe, store.getState); + const { + message, + parentMessage, + currentChannel, + threadListState, + allThreadMessages, + onBeforeSendUserMessage, + onBeforeSendFileMessage, + onBeforeSendVoiceMessage, + onBeforeSendMultipleFilesMessage, + } = state; + + const sendMessageStatusActions = { + sendMessageStart: (message: SendableMessageType) => store.setState(state => { + return { + ...state, + localThreadMessages: [ + ...state.localThreadMessages, + message, + ], + }; + }), + + sendMessageSuccess: (message: SendableMessageType) => store.setState(state => { + return { + ...state, + allThreadMessages: [ + ...state.allThreadMessages.filter((m) => ( + !compareIds((m as UserMessage)?.reqId, message?.reqId) + )), + message, + ], + localThreadMessages: state.localThreadMessages.filter((m) => ( + !compareIds((m as UserMessage)?.reqId, message?.reqId) + )), + }; + }), + + sendMessageFailure: (message: SendableMessageType) => store.setState(state => { + return { + ...state, + localThreadMessages: state.localThreadMessages.map((m) => ( + compareIds((m as UserMessage)?.reqId, message?.reqId) + ? message + : m + )), + }; + }), + + resendMessageStart: (message: SendableMessageType) => store.setState(state => { + return { + ...state, + localThreadMessages: state.localThreadMessages.map((m) => ( + compareIds((m as UserMessage)?.reqId, message?.reqId) + ? message + : m + )), + }; + }), + }; + + const toggleReaction = useToggleReactionCallback({ currentChannel }, { logger }); + + const sendMessageActions = { + sendMessage: useSendUserMessageCallback({ + isMentionEnabled, + currentChannel, + onBeforeSendUserMessage, + sendMessageStart: sendMessageStatusActions.sendMessageStart, + sendMessageFailure: sendMessageStatusActions.sendMessageFailure, + }, { + logger, + pubSub, + }), + + sendFileMessage: useSendFileMessageCallback({ + currentChannel, + onBeforeSendFileMessage, + sendMessageStart: sendMessageStatusActions.sendMessageStart, + sendMessageFailure: sendMessageStatusActions.sendMessageFailure, + }, { + logger, + pubSub, + }), + + sendVoiceMessage: useSendVoiceMessageCallback({ + currentChannel, + onBeforeSendVoiceMessage, + sendMessageStart: sendMessageStatusActions.sendMessageStart, + sendMessageFailure: sendMessageStatusActions.sendMessageFailure, + }, { + logger, + pubSub, + }), + + sendMultipleFilesMessage: useSendMultipleFilesMessage({ + currentChannel, + onBeforeSendMultipleFilesMessage, + publishingModules: [PublishingModuleType.THREAD], + }, { + logger, + pubSub, + })[0], + + resendMessage: useResendMessageCallback({ + resendMessageStart: sendMessageStatusActions.resendMessageStart, + sendMessageSuccess: sendMessageStatusActions.sendMessageSuccess, + sendMessageFailure: sendMessageStatusActions.sendMessageFailure, + currentChannel, + }, { logger, pubSub }), + }; + + const messageModifiedActions = { + onMessageUpdated: (channel: GroupChannel, message: SendableMessageType) => store.setState(state => { + if (state.currentChannel?.url !== channel?.url) { + return state; + } + + return { + ...state, + parentMessage: state.parentMessage?.messageId === message?.messageId + ? message + : state.parentMessage, + allThreadMessages: state.allThreadMessages?.map((msg) => ( + (msg?.messageId === message?.messageId) ? message : msg + )), + }; + }), + + onMessageDeleted: (channel: GroupChannel, messageId: number) => store.setState(state => { + if (state.currentChannel?.url !== channel?.url) { + return state; + } + if (state?.parentMessage?.messageId === messageId) { + return { + ...state, + parentMessage: null, + parentMessageState: ParentMessageStateTypes.NIL, + allThreadMessages: [], + }; + } + return { + ...state, + allThreadMessages: state.allThreadMessages?.filter((msg) => ( + msg?.messageId !== messageId + )), + localThreadMessages: state.localThreadMessages?.filter((msg) => ( + msg?.messageId !== messageId + )), + }; + }), + + onMessageDeletedByReqId: (reqId: string | number) => store.setState(state => { + return { + ...state, + localThreadMessages: state.localThreadMessages.filter((m) => ( + !compareIds((m as SendableMessageType).reqId, reqId) + )), + }; + }), + }; + + const modifyMessageActions = { + updateMessage: useUpdateMessageCallback({ + currentChannel, + isMentionEnabled, + onMessageUpdated: messageModifiedActions.onMessageUpdated, + }, { logger, pubSub }), + + deleteMessage: useDeleteMessageCallback({ + currentChannel, + onMessageDeleted: messageModifiedActions.onMessageDeleted, + onMessageDeletedByReqId: messageModifiedActions.onMessageDeletedByReqId, + }, { logger }), + }; + + const threadFetcherStatusActions = { + initializeThreadListStart: () => store.setState(state => { + return { + ...state, + threadListState: ThreadListStateTypes.LOADING, + allThreadMessages: [], + }; + }), + + initializeThreadListSuccess: (parentMessage: BaseMessage, anchorMessage: SendableMessageType, threadedMessages: BaseMessage[]) => store.setState(state => { + const anchorMessageCreatedAt = (!anchorMessage?.messageId) ? parentMessage?.createdAt : anchorMessage?.createdAt; + const anchorIndex = threadedMessages.findIndex((message) => message?.createdAt > anchorMessageCreatedAt); + const prevThreadMessages = anchorIndex > -1 ? threadedMessages.slice(0, anchorIndex) : threadedMessages; + const anchorThreadMessage = anchorMessage?.messageId ? [anchorMessage] : []; + const nextThreadMessages = anchorIndex > -1 ? threadedMessages.slice(anchorIndex) : []; + return { + ...state, + threadListState: ThreadListStateTypes.INITIALIZED, + hasMorePrev: anchorIndex === -1 || anchorIndex === PREV_THREADS_FETCH_SIZE, + hasMoreNext: threadedMessages.length - anchorIndex === NEXT_THREADS_FETCH_SIZE, + allThreadMessages: [prevThreadMessages, anchorThreadMessage, nextThreadMessages].flat() as CoreMessageType[], + }; + }), + + initializeThreadListFailure: () => store.setState(state => { + return { + ...state, + threadListState: ThreadListStateTypes.LOADING, + allThreadMessages: [], + }; + }), + + getPrevMessagesStart: () => store.setState(state => { + return { + ...state, + }; + }), + + getPrevMessagesSuccess: (threadedMessages: CoreMessageType[]) => store.setState(state => { + return { + ...state, + hasMorePrev: threadedMessages.length === PREV_THREADS_FETCH_SIZE, + allThreadMessages: [...threadedMessages, ...state.allThreadMessages], + }; + }), + + getPrevMessagesFailure: () => store.setState(state => { + return { + ...state, + hasMorePrev: false, + }; + }), + + getNextMessagesStart: () => store.setState(state => { + return { + ...state, + }; + }), + + getNextMessagesSuccess: (threadedMessages: CoreMessageType[]) => store.setState(state => { + return { + ...state, + hasMoreNext: threadedMessages.length === NEXT_THREADS_FETCH_SIZE, + allThreadMessages: [...state.allThreadMessages, ...threadedMessages], + }; + }), + + getNextMessagesFailure: () => store.setState(state => { + return { + ...state, + hasMoreNext: false, + }; + }), + }; + + const { initializeThreadFetcher, fetchPrevThreads, fetchNextThreads } = useThreadFetchers({ + parentMessage, + // anchorMessage should be null when parentMessage doesn't exist + anchorMessage: message?.messageId !== parentMessage?.messageId ? message || undefined : undefined, + logger, + isReactionEnabled, + threadListState, + oldestMessageTimeStamp: allThreadMessages[0]?.createdAt || 0, + latestMessageTimeStamp: allThreadMessages[allThreadMessages.length - 1]?.createdAt || 0, + initializeThreadListStart: threadFetcherStatusActions.initializeThreadListStart, + initializeThreadListSuccess: threadFetcherStatusActions.initializeThreadListSuccess, + initializeThreadListFailure: threadFetcherStatusActions.initializeThreadListFailure, + getPrevMessagesStart: threadFetcherStatusActions.getPrevMessagesStart, + getPrevMessagesSuccess: threadFetcherStatusActions.getPrevMessagesSuccess, + getPrevMessagesFailure: threadFetcherStatusActions.getPrevMessagesFailure, + getNextMessagesStart: threadFetcherStatusActions.getNextMessagesStart, + getNextMessagesSuccess: threadFetcherStatusActions.getNextMessagesSuccess, + getNextMessagesFailure: threadFetcherStatusActions.getNextMessagesFailure, + }); + + const actions = useMemo(() => ({ + setCurrentUserId: (currentUserId: string) => store.setState(state => ({ + ...state, + currentUserId: currentUserId, + })), + + getChannelStart: () => store.setState(state => ({ + ...state, + channelState: ChannelStateTypes.LOADING, + currentChannel: null, + })), + + getChannelSuccess: (groupChannel: GroupChannel) => store.setState(state => ({ + ...state, + channelState: ChannelStateTypes.INITIALIZED, + currentChannel: groupChannel, + // only support in normal group channel + isMuted: groupChannel?.members?.find((member) => member?.userId === state.currentUserId)?.isMuted || false, + isChannelFrozen: groupChannel?.isFrozen || false, + })), + + getChannelFailure: () => store.setState(state => ({ + ...state, + channelState: ChannelStateTypes.INVALID, + currentChannel: null, + })), + + getParentMessageStart: () => store.setState(state => ({ + ...state, + parentMessageState: ParentMessageStateTypes.LOADING, + parentMessage: null, + })), + + getParentMessageSuccess: (parentMessage: SendableMessageType) => store.setState(state => ({ + ...state, + parentMessageState: ParentMessageStateTypes.INITIALIZED, + parentMessage: parentMessage, + })), + + getParentMessageFailure: () => store.setState(state => ({ + ...state, + parentMessageState: ParentMessageStateTypes.INVALID, + parentMessage: null, + })), + + setEmojiContainer: (emojiContainer: EmojiContainer) => store.setState(state => ({ + ...state, + emojiContainer: emojiContainer, + })), + + onMessageReceived: (channel: GroupChannel, message: SendableMessageType) => store.setState(state => { + if ( + state.currentChannel?.url !== channel?.url + || state.hasMoreNext + || message?.parentMessage?.messageId !== state?.parentMessage?.messageId + ) { + return state; + } + + const isAlreadyReceived = state.allThreadMessages.findIndex((m) => ( + m.messageId === message.messageId + )) > -1; + + return { + ...state, + parentMessage: state.parentMessage?.messageId === message?.messageId ? message : state.parentMessage, + allThreadMessages: isAlreadyReceived + ? state.allThreadMessages.map((m) => ( + m.messageId === message.messageId ? message : m + )) + : [ + ...state.allThreadMessages.filter((m) => (m as SendableMessageType)?.reqId !== message?.reqId), + message, + ], + }; + }), + + onReactionUpdated: (reactionEvent: ReactionEvent) => store.setState(state => { + if (state?.parentMessage?.messageId === reactionEvent?.messageId) { + state.parentMessage?.applyReactionEvent?.(reactionEvent); + } + return { + ...state, + allThreadMessages: state.allThreadMessages.map((m) => { + if (reactionEvent?.messageId === m?.messageId) { + m?.applyReactionEvent?.(reactionEvent); + return m; + } + return m; + }), + }; + }), + + onUserMuted: (channel: GroupChannel, user: User) => store.setState(state => { + if (state.currentChannel?.url !== channel?.url || state.currentUserId !== user?.userId) { + return state; + } + return { + ...state, + isMuted: true, + }; + }), + + onUserUnmuted: (channel: GroupChannel, user: User) => store.setState(state => { + if (state.currentChannel?.url !== channel?.url || state.currentUserId !== user?.userId) { + return state; + } + return { + ...state, + isMuted: false, + }; + }), + + onUserBanned: () => store.setState(state => { + return { + ...state, + channelState: ChannelStateTypes.NIL, + threadListState: ThreadListStateTypes.NIL, + parentMessageState: ParentMessageStateTypes.NIL, + currentChannel: null, + parentMessage: null, + allThreadMessages: [], + hasMorePrev: false, + hasMoreNext: false, + }; + }), + + onUserUnbanned: () => store.setState(state => { + return { + ...state, + }; + }), + + onUserLeft: () => store.setState(state => { + return { + ...state, + channelState: ChannelStateTypes.NIL, + threadListState: ThreadListStateTypes.NIL, + parentMessageState: ParentMessageStateTypes.NIL, + currentChannel: null, + parentMessage: null, + allThreadMessages: [], + hasMorePrev: false, + hasMoreNext: false, + }; + }), + + onChannelFrozen: () => store.setState(state => { + return { + ...state, + isChannelFrozen: true, + }; + }), + + onChannelUnfrozen: () => store.setState(state => { + return { + ...state, + isChannelFrozen: false, + }; + }), + + onOperatorUpdated: (channel: GroupChannel) => store.setState(state => { + if (channel?.url === state.currentChannel?.url) { + return { + ...state, + currentChannel: channel, + }; + } + return state; + }), + + onTypingStatusUpdated: (channel: GroupChannel, typingMembers: Member[]) => store.setState(state => { + if (!compareIds(channel.url, state.currentChannel?.url)) { + return state; + } + return { + ...state, + typingMembers, + }; + }), + + onFileInfoUpdated: ({ + channelUrl, + requestId, + index, + uploadableFileInfo, + error, + }: FileUploadInfoParams) => store.setState(state => { + if (!compareIds(channelUrl, state.currentChannel?.url)) { + return state; + } + /** + * We don't have to do anything here because + * onFailed() will be called so handle error there instead. + */ + if (error) return state; + const { localThreadMessages } = state; + const messageToUpdate = localThreadMessages.find((message) => compareIds(hasReqId(message) && message.reqId, requestId), + ); + const fileInfoList = (messageToUpdate as MultipleFilesMessage) + .messageParams?.fileInfoList; + if (Array.isArray(fileInfoList)) { + fileInfoList[index] = uploadableFileInfo; + } + return { + ...state, + localThreadMessages, + }; + }), + + toggleReaction, + ...sendMessageStatusActions, + ...sendMessageActions, + ...messageModifiedActions, + ...modifyMessageActions, + ...threadFetcherStatusActions, + initializeThreadFetcher, + fetchPrevThreads, + fetchNextThreads, + }), [ + store, + currentChannel, + stores.sdkStore.initialized, + parentMessage, + ]); + + return { state, actions }; +}; + +export default useThread; diff --git a/src/modules/Thread/types.tsx b/src/modules/Thread/types.tsx index dacdbdab6..e4c8d2712 100644 --- a/src/modules/Thread/types.tsx +++ b/src/modules/Thread/types.tsx @@ -1,4 +1,6 @@ // Initializing status +import { UploadableFileInfo } from '@sendbird/chat/message'; + export enum ChannelStateTypes { NIL = 'NIL', LOADING = 'LOADING', @@ -17,3 +19,11 @@ export enum ThreadListStateTypes { INVALID = 'INVALID', INITIALIZED = 'INITIALIZED', } + +export interface FileUploadInfoParams { + channelUrl: string, + requestId: string, + index: number, + uploadableFileInfo: UploadableFileInfo, + error: Error, +}