From 73299c82adfcd02b4c43f044a192d06e283cea49 Mon Sep 17 00:00:00 2001 From: "junyoung.lim" Date: Tue, 12 Nov 2024 11:20:34 +0900 Subject: [PATCH 01/10] Migrate ThreadProvider with the new convention --- .../ParentMessageInfoItem.tsx | 18 +- .../components/ParentMessageInfo/index.tsx | 34 +- .../Thread/components/RemoveMessageModal.tsx | 8 +- .../components/SuggestedMentionList.tsx | 8 +- .../components/ThreadList/ThreadListItem.tsx | 32 +- .../ThreadList/ThreadListItemContent.tsx | 12 +- .../Thread/components/ThreadList/index.tsx | 12 +- .../components/ThreadMessageInput/index.tsx | 28 +- .../Thread/components/ThreadUI/index.tsx | 30 +- src/modules/Thread/context/ThreadProvider.tsx | 312 ++++++----- .../context/hooks/useDeleteMessageCallback.ts | 22 +- .../Thread/context/hooks/useGetAllEmoji.ts | 17 +- .../Thread/context/hooks/useGetChannel.ts | 27 +- .../context/hooks/useGetParentMessage.ts | 30 +- .../context/hooks/useHandleChannelEvents.ts | 93 ++-- .../hooks/useHandleThreadPubsubEvents.ts | 57 +- .../context/hooks/useResendMessageCallback.ts | 78 +-- .../context/hooks/useSendFileMessage.ts | 45 +- .../hooks/useSendUserMessageCallback.ts | 24 +- .../hooks/useSendVoiceMessageCallback.ts | 45 +- .../context/hooks/useSetCurrentUserId.ts | 23 + .../Thread/context/hooks/useThreadFetchers.ts | 65 +-- .../context/hooks/useUpdateMessageCallback.ts | 21 +- src/modules/Thread/context/useThread.ts | 495 ++++++++++++++++++ src/modules/Thread/types.tsx | 10 + 25 files changed, 996 insertions(+), 550 deletions(-) create mode 100644 src/modules/Thread/context/hooks/useSetCurrentUserId.ts create mode 100644 src/modules/Thread/context/useThread.ts diff --git a/src/modules/Thread/components/ParentMessageInfo/ParentMessageInfoItem.tsx b/src/modules/Thread/components/ParentMessageInfo/ParentMessageInfoItem.tsx index 97bb7fb06..b52ee9a74 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/GroupChannelProvider'; 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 a72a0d33f..ea11fa077 100644 --- a/src/modules/Thread/context/ThreadProvider.tsx +++ b/src/modules/Thread/context/ThreadProvider.tsx @@ -1,6 +1,6 @@ -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, @@ -10,12 +10,9 @@ import type { 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 { ThreadContextInitialState } from './dux/initialState'; import type { OnBeforeDownloadFileMessageType } from '../../GroupChannel/context/GroupChannelProvider'; import useGetChannel from './hooks/useGetChannel'; @@ -23,16 +20,16 @@ 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 { SendMessageParams } from './hooks/useSendUserMessageCallback'; 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,6 +46,8 @@ export interface ThreadProviderProps extends isMultipleFilesMessageEnabled?: boolean; filterEmojiCategoryIds?: (message: SendableMessageType) => EmojiCategory['id'][]; } + +// actions export interface ThreadProviderInterface extends ThreadProviderProps, ThreadContextInitialState { // hooks for fetching threads fetchPrevThreads: (callback?: (messages?: Array) => void) => void; @@ -63,11 +62,80 @@ export interface ThreadProviderInterface extends ThreadProviderProps, ThreadCont deleteMessage: (message: SendableMessageType) => Promise; nicknamesMap: Map; } -const ThreadContext = React.createContext(null); -export const ThreadProvider = (props: ThreadProviderProps) => { +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 initialState = { + channelUrl: '', + message: null, + onHeaderActionClick: null, + onMoveToParentMessage: null, + onBeforeSendUserMessage: null, + onBeforeSendFileMessage: null, + onBeforeSendVoiceMessage: null, + onBeforeSendMultipleFilesMessage: null, + onBeforeDownloadFileMessage: null, + isMultipleFilesMessageEnabled: null, + filterEmojiCategoryIds: null, + 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,6 +147,18 @@ export const ThreadProvider = (props: ThreadProviderProps) => { isMultipleFilesMessageEnabled, filterEmojiCategoryIds, } = props; + + const { + state: { + currentChannel, + parentMessage, + }, + actions: { + initializeThreadFetcher, + }, + } = useThread(); + const { updateState } = useThreadStore(); + const propsMessage = props?.message; const propsParentMessage = getParentMessageFrom(propsMessage); // Context from SendbirdProvider @@ -92,180 +172,88 @@ 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 }); + }, { 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} + + + ); }; @@ -274,3 +262,7 @@ export const useThreadContext = () => { if (!context) throw new Error('ThreadContext not found. Use within the Thread module'); return context; }; + +const useThreadStore = () => { + return useStore(ThreadContext, state => state, initialState); +}; diff --git a/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts b/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts index 30c113931..8921e945c 100644 --- a/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts @@ -1,12 +1,11 @@ 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'; +import useThread from '../useThread'; interface DynamicProps { currentChannel: GroupChannel | null; - threadDispatcher: CustomUseReducerDispatcher; } interface StaticProps { logger: Logger; @@ -14,10 +13,15 @@ interface StaticProps { export default function useDeleteMessageCallback({ currentChannel, - threadDispatcher, }: DynamicProps, { logger, }: StaticProps): (message: SendableMessageType) => Promise { + const { + actions: { + onMessageDeletedByReqId, + onMessageDeleted, + }, + } = useThread(); return useCallback((message: SendableMessageType): Promise => { logger.info('Thread | useDeleteMessageCallback: Deleting message.', message); const { sendingStatus } = message; @@ -26,10 +30,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 +38,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..428f28c61 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,27 @@ 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, - }); + 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..fbda14458 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,24 @@ 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 +48,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..98c3c47a2 100644 --- a/src/modules/Thread/context/hooks/useResendMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useResendMessageCallback.ts @@ -8,11 +8,11 @@ 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'; +import useThread from '../useThread'; interface DynamicProps { currentChannel: GroupChannel | null; @@ -20,7 +20,6 @@ interface DynamicProps { interface StaticProps { logger: Logger; pubSub: SBUGlobalPubSub; - threadDispatcher: CustomUseReducerDispatcher; } export default function useResendMessageCallback({ @@ -28,8 +27,14 @@ export default function useResendMessageCallback({ }: DynamicProps, { logger, pubSub, - threadDispatcher, }: StaticProps): (failedMessage: SendableMessageType) => void { + const { + actions: { + resendMessageStart, + sendMessageSuccess, + sendMessageFailure, + }, + } = useThread(); return useCallback((failedMessage: SendableMessageType) => { if ((failedMessage as SendableMessageType)?.isResendable) { logger.info('Thread | useResendMessageCallback: Resending failedMessage start.', failedMessage); @@ -38,17 +43,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 +57,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 +83,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 +117,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 +126,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..a7fb40a3c 100644 --- a/src/modules/Thread/context/hooks/useSendFileMessage.ts +++ b/src/modules/Thread/context/hooks/useSendFileMessage.ts @@ -2,13 +2,13 @@ import { useCallback } from 'react'; import { GroupChannel } from '@sendbird/chat/groupChannel'; import { FileMessage, FileMessageCreateParams } 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'; import { PublishingModuleType } from './useSendMultipleFilesMessage'; import { SCROLL_BOTTOM_DELAY_FOR_SEND } from '../../../../utils/consts'; +import useThread from '../useThread'; interface DynamicProps { currentChannel: GroupChannel | null; @@ -17,7 +17,6 @@ interface DynamicProps { interface StaticProps { logger: Logger; pubSub: SBUGlobalPubSub; - threadDispatcher: CustomUseReducerDispatcher; } interface LocalFileMessage extends FileMessage { @@ -33,8 +32,14 @@ export default function useSendFileMessageCallback({ }: DynamicProps, { logger, pubSub, - threadDispatcher, }: StaticProps): SendFileMessageFunctionType { + const { + actions: { + sendMessageStart, + sendMessageFailure, + }, + } = useThread(); + return useCallback((file, quoteMessage): Promise => { return new Promise((resolve, reject) => { const createParamsDefault = () => { @@ -51,23 +56,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, - }, - }, + sendMessageStart({ + ...pendingMessage, + url: URL.createObjectURL(file), + // pending thumbnail message seems to be failed + // @ts-ignore + requestState: 'pending', + isUserMessage: pendingMessage.isUserMessage, + isFileMessage: pendingMessage.isFileMessage, + isAdminMessage: pendingMessage.isAdminMessage, + isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, }); setTimeout(() => scrollIntoLast(), SCROLL_BOTTOM_DELAY_FOR_SEND); }) @@ -75,10 +73,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..c44983e10 100644 --- a/src/modules/Thread/context/hooks/useSendUserMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useSendUserMessageCallback.ts @@ -3,11 +3,11 @@ 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'; +import useThread from '../useThread'; export type OnBeforeSendUserMessageType = (message: string, quoteMessage?: SendableMessageType) => UserMessageCreateParams; interface DynamicProps { @@ -18,7 +18,6 @@ interface DynamicProps { interface StaticProps { logger: Logger; pubSub: SBUGlobalPubSub; - threadDispatcher: CustomUseReducerDispatcher; } export type SendMessageParams = { @@ -35,7 +34,6 @@ export default function useSendUserMessageCallback({ }: DynamicProps, { logger, pubSub, - threadDispatcher, }: StaticProps): (props: SendMessageParams) => void { const sendMessage = useCallback((props: SendMessageParams) => { const { @@ -44,6 +42,14 @@ export default function useSendUserMessageCallback({ mentionTemplate, mentionedUsers, } = props; + + const { + actions: { + sendMessageStart, + sendMessageFailure, + }, + } = useThread(); + const createDefaultParams = () => { const params = {} as UserMessageCreateParams; params.message = message; @@ -67,17 +73,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..cc4c8dcbc 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 { Logger } from '../../../../lib/SendbirdState'; import topics, { SBUGlobalPubSub } from '../../../../lib/pubSub/topics'; import { scrollIntoLast } from '../utils'; import { @@ -15,6 +14,7 @@ import { } from '../../../../utils/consts'; import { SendableMessageType } from '../../../../utils'; import { PublishingModuleType } from '../../../internalInterfaces'; +import useThread from '../useThread'; interface DynamicParams { currentChannel: GroupChannel | null; @@ -23,7 +23,6 @@ interface DynamicParams { interface StaticParams { logger: Logger; pubSub: SBUGlobalPubSub; - threadDispatcher: CustomUseReducerDispatcher; } type FuncType = (file: File, duration: number, quoteMessage: SendableMessageType) => void; interface LocalFileMessage extends FileMessage { @@ -38,8 +37,14 @@ export const useSendVoiceMessageCallback = ({ { logger, pubSub, - threadDispatcher, }: StaticParams): FuncType => { + const { + actions: { + sendMessageStart, + sendMessageFailure, + }, + } = useThread(); + const sendMessage = useCallback((file: File, duration: number, quoteMessage: SendableMessageType) => { const messageParams: FileMessageCreateParams = ( onBeforeSendVoiceMessage @@ -68,23 +73,18 @@ 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 + 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 + requestState: 'pending', + isUserMessage: pendingMessage.isUserMessage, + isFileMessage: pendingMessage.isFileMessage, + isAdminMessage: pendingMessage.isAdminMessage, + isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, }); setTimeout(() => scrollIntoLast(), SCROLL_BOTTOM_DELAY_FOR_SEND); }) @@ -92,10 +92,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..880a2897c --- /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]); +} + +export default useSetCurrentUserId; diff --git a/src/modules/Thread/context/hooks/useThreadFetchers.ts b/src/modules/Thread/context/hooks/useThreadFetchers.ts index 7947d276a..f2ef4635f 100644 --- a/src/modules/Thread/context/hooks/useThreadFetchers.ts +++ b/src/modules/Thread/context/hooks/useThreadFetchers.ts @@ -1,18 +1,16 @@ -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'; import { ThreadListStateTypes } from '../../types'; +import useThread from '../useThread'; type Params = { anchorMessage?: SendableMessageType; parentMessage: SendableMessageType | null; isReactionEnabled?: boolean; - threadDispatcher: CustomUseReducerDispatcher; logger: LoggerInterface; threadListState: ThreadListStateTypes; oldestMessageTimeStamp: number; @@ -32,12 +30,24 @@ export const useThreadFetchers = ({ isReactionEnabled, anchorMessage, parentMessage: staleParentMessage, - threadDispatcher, logger, oldestMessageTimeStamp, latestMessageTimeStamp, threadListState, }: Params) => { + const { + actions: { + initializeThreadListStart, + initializeThreadListSuccess, + initializeThreadListFailure, + getPrevMessagesStart, + getPrevMessagesSuccess, + getPrevMessagesFailure, + getNextMessagesStart, + getNextMessagesSuccess, + getNextMessagesFailure, + }, + } = useThread(); const { stores } = useSendbirdStateContext(); const timestamp = anchorMessage?.createdAt || 0; @@ -45,10 +55,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 +63,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 +77,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 +85,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,27 +99,18 @@ 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], diff --git a/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts b/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts index db58e4a2b..85c2fab86 100644 --- a/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts @@ -3,11 +3,11 @@ 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 useThread from '../useThread'; interface DynamicProps { currentChannel: GroupChannel | null; @@ -16,7 +16,6 @@ interface DynamicProps { interface StaticProps { logger: Logger; pubSub: SBUGlobalPubSub; - threadDispatcher: CustomUseReducerDispatcher; } type CallbackParams = { @@ -32,7 +31,6 @@ export default function useUpdateMessageCallback({ }: DynamicProps, { logger, pubSub, - threadDispatcher, }: StaticProps) { // TODO: add type return useCallback((props: CallbackParams) => { @@ -42,6 +40,13 @@ export default function useUpdateMessageCallback({ mentionedUsers, mentionTemplate, } = props; + + const { + actions: { + onMessageUpdated, + }, + } = useThread(); + const createParamsDefault = () => { const params = {} as UserMessageUpdateParams; params.message = message; @@ -62,13 +67,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..41c85eda9 --- /dev/null +++ b/src/modules/Thread/context/useThread.ts @@ -0,0 +1,495 @@ +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 useSendFileMessageCallback from './hooks/useSendFileMessage'; +import useSendVoiceMessageCallback from './hooks/useSendVoiceMessageCallback'; +import { useSendMultipleFilesMessage } from '../../Channel/context/hooks/useSendMultipleFilesMessage'; +import { PublishingModuleType } from '../../internalInterfaces'; +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 { 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 { + initialize: initializeThreadFetcher, + loadPrevious: fetchPrevThreads, + loadNext: fetchNextThreads, + } = useThreadFetchers({ + parentMessage: state.parentMessage, + // anchorMessage should be null when parentMessage doesn't exist + anchorMessage: state.message?.messageId !== state.parentMessage?.messageId ? state.message || undefined : undefined, + logger, + isReactionEnabled, + threadListState: state.threadListState, + oldestMessageTimeStamp: state.allThreadMessages[0]?.createdAt || 0, + latestMessageTimeStamp: state.allThreadMessages[state.allThreadMessages.length - 1]?.createdAt || 0, + }); + + const actions = useMemo(() => ({ + setCurrentUserId: (currentUserId: string) => store.setState(state => ({ + ...state, + currentUserId: currentUserId, + })), + + toggleReaction: useToggleReactionCallback({ currentChannel: state.currentChannel }, { logger }), + + sendMessage: useSendUserMessageCallback({ + isMentionEnabled: isMentionEnabled, + currentChannel: state.currentChannel, + onBeforeSendUserMessage: state.onBeforeSendUserMessage, + }, { + logger, + pubSub, + }), + + sendFileMessage: useSendFileMessageCallback({ + currentChannel: state.currentChannel, + onBeforeSendFileMessage: state.onBeforeSendFileMessage, + }, { + logger, + pubSub, + }), + + sendVoiceMessage: useSendVoiceMessageCallback({ + currentChannel: state.currentChannel, + onBeforeSendVoiceMessage: state.onBeforeSendVoiceMessage, + }, { + logger, + pubSub, + }), + + sendMultipleFilesMessage: useSendMultipleFilesMessage({ + currentChannel: state.currentChannel, + onBeforeSendMultipleFilesMessage: state.onBeforeSendMultipleFilesMessage, + publishingModules: [PublishingModuleType.THREAD], + }, { + logger, + pubSub, + })[0], + + resendMessage: useResendMessageCallback({ + currentChannel: state.currentChannel, + }, { logger, pubSub }), + + updateMessage: useUpdateMessageCallback({ + currentChannel: state.currentChannel, + isMentionEnabled, + }, { logger, pubSub }), + + deleteMessage: useDeleteMessageCallback( + { currentChannel: state.currentChannel }, + { logger }, + ), + + initializeThreadFetcher: initializeThreadFetcher, + + fetchPrevThreads: fetchPrevThreads, + + fetchNextThreads: fetchNextThreads, + + 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, + ], + }; + }), + + 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) + )), + }; + }), + + 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, + }; + }), + + 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 + )), + }; + }), + + 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, + }; + }), + + 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, + }; + }), + + }), [store]); + + 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, +} From aae70ba421ac1fac475517006a1d363607722207 Mon Sep 17 00:00:00 2001 From: "junyoung.lim" Date: Tue, 12 Nov 2024 14:02:22 +0900 Subject: [PATCH 02/10] Move hook functions into the useThread() --- src/modules/Thread/context/ThreadProvider.tsx | 8 +- .../Thread/context/hooks/useThreadFetchers.ts | 6 +- src/modules/Thread/context/useThread.ts | 633 +++++++++++++++--- 3 files changed, 562 insertions(+), 85 deletions(-) diff --git a/src/modules/Thread/context/ThreadProvider.tsx b/src/modules/Thread/context/ThreadProvider.tsx index ea11fa077..353eb273c 100644 --- a/src/modules/Thread/context/ThreadProvider.tsx +++ b/src/modules/Thread/context/ThreadProvider.tsx @@ -247,20 +247,18 @@ export const ThreadProvider = (props: ThreadProviderProps) => { return ( - + {/* 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 = () => { diff --git a/src/modules/Thread/context/hooks/useThreadFetchers.ts b/src/modules/Thread/context/hooks/useThreadFetchers.ts index f2ef4635f..36c1350ba 100644 --- a/src/modules/Thread/context/hooks/useThreadFetchers.ts +++ b/src/modules/Thread/context/hooks/useThreadFetchers.ts @@ -117,8 +117,8 @@ export const useThreadFetchers = ({ ); return { - initialize, - loadPrevious, - loadNext, + initializeThreadFetcher: initialize, + fetchPrevThreads: loadPrevious, + fetchNextThreads: loadNext, }; }; diff --git a/src/modules/Thread/context/useThread.ts b/src/modules/Thread/context/useThread.ts index 41c85eda9..8e7983373 100644 --- a/src/modules/Thread/context/useThread.ts +++ b/src/modules/Thread/context/useThread.ts @@ -1,24 +1,31 @@ import { useSyncExternalStore } from 'use-sync-external-store/shim'; -import { useContext, useMemo } from 'react'; +import { useCallback, 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 { compareIds, scrollIntoLast as scrollIntoLastForThread, scrollIntoLast } from './utils'; +import { + BaseMessage, FileMessage, FileMessageCreateParams, MessageMetaArray, MessageType, + MultipleFilesMessage, type MultipleFilesMessageCreateParams, + ReactionEvent, SendingStatus, ThreadedMessageListParams, type UploadableFileInfo, + UserMessage, + UserMessageCreateParams, UserMessageUpdateParams, +} 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 useSendFileMessageCallback from './hooks/useSendFileMessage'; -import useSendVoiceMessageCallback from './hooks/useSendVoiceMessageCallback'; -import { useSendMultipleFilesMessage } from '../../Channel/context/hooks/useSendMultipleFilesMessage'; -import { PublishingModuleType } from '../../internalInterfaces'; -import useResendMessageCallback from './hooks/useResendMessageCallback'; -import useUpdateMessageCallback from './hooks/useUpdateMessageCallback'; -import useDeleteMessageCallback from './hooks/useDeleteMessageCallback'; -import { useThreadFetchers } from './hooks/useThreadFetchers'; +import { SendMessageParams } from './hooks/useSendUserMessageCallback'; +import topics, { PUBSUB_TOPICS, PublishingModuleType } from '../../../lib/pubSub/topics'; +import { + META_ARRAY_MESSAGE_TYPE_KEY, META_ARRAY_MESSAGE_TYPE_VALUE__VOICE, + META_ARRAY_VOICE_DURATION_KEY, + SCROLL_BOTTOM_DELAY_FOR_SEND, + VOICE_MESSAGE_FILE_NAME, + VOICE_MESSAGE_MIME_TYPE, +} from '../../../utils/consts'; +import { shouldPubSubPublishToThread } from '../../internalInterfaces'; function hasReqId( message: T, @@ -26,94 +33,554 @@ function hasReqId( return 'reqId' in message; } +interface LocalFileMessage extends FileMessage { + localUrl: string; + file: File; +} + +function getThreadMessageListParams(params?: Partial): ThreadedMessageListParams { + return { + prevResultSize: PREV_THREADS_FETCH_SIZE, + nextResultSize: NEXT_THREADS_FETCH_SIZE, + includeMetaArray: true, + ...params, + }; +} + const useThread = () => { const store = useContext(ThreadContext); if (!store) throw new Error('useCreateChannel must be used within a CreateChannelProvider'); // SendbirdStateContext config - const { config } = useSendbirdStateContext(); + 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 { - initialize: initializeThreadFetcher, - loadPrevious: fetchPrevThreads, - loadNext: fetchNextThreads, - } = useThreadFetchers({ - parentMessage: state.parentMessage, - // anchorMessage should be null when parentMessage doesn't exist - anchorMessage: state.message?.messageId !== state.parentMessage?.messageId ? state.message || undefined : undefined, - logger, - isReactionEnabled, - threadListState: state.threadListState, - oldestMessageTimeStamp: state.allThreadMessages[0]?.createdAt || 0, - latestMessageTimeStamp: state.allThreadMessages[state.allThreadMessages.length - 1]?.createdAt || 0, - }); + message, + parentMessage, + currentChannel, + threadListState, + allThreadMessages, + onBeforeSendUserMessage, + onBeforeSendFileMessage, + onBeforeSendVoiceMessage, + onBeforeSendMultipleFilesMessage, + } = state; + + const toggleReaction = useToggleReactionCallback({ currentChannel }, { logger }); + + const sendMessage = useCallback((props: SendMessageParams) => { + const { + message, + quoteMessage, + mentionTemplate, + mentionedUsers, + } = props; + + const createDefaultParams = () => { + const params = {} as UserMessageCreateParams; + params.message = message; + const mentionedUsersLength = mentionedUsers?.length || 0; + if (isMentionEnabled && mentionedUsersLength) { + params.mentionedUsers = mentionedUsers; + } + if (isMentionEnabled && mentionTemplate && mentionedUsersLength) { + params.mentionedMessageTemplate = mentionTemplate; + } + if (quoteMessage) { + params.isReplyToChannel = true; + params.parentMessageId = quoteMessage.messageId; + } + return params; + }; + + const params = onBeforeSendUserMessage?.(message, quoteMessage) ?? createDefaultParams(); + logger.info('Thread | useSendUserMessageCallback: Sending user message start.', params); + + if (currentChannel?.sendUserMessage) { + currentChannel?.sendUserMessage(params) + .onPending((pendingMessage) => { + actions.sendMessageStart(pendingMessage as SendableMessageType); + }) + .onFailed((error, message) => { + logger.info('Thread | useSendUserMessageCallback: Sending user message failed.', { message, error }); + actions.sendMessageFailure(message as SendableMessageType); + }) + .onSucceeded((message) => { + logger.info('Thread | useSendUserMessageCallback: Sending user message succeeded.', message); + // because Thread doesn't subscribe SEND_USER_MESSAGE + pubSub.publish(topics.SEND_USER_MESSAGE, { + channel: currentChannel, + message: message as UserMessage, + publishingModules: [PublishingModuleType.THREAD], + }); + }); + } + }, [isMentionEnabled, currentChannel]); + + const sendFileMessage = useCallback((file, quoteMessage): Promise => { + return new Promise((resolve, reject) => { + const createParamsDefault = () => { + const params = {} as FileMessageCreateParams; + params.file = file; + if (quoteMessage) { + params.isReplyToChannel = true; + params.parentMessageId = quoteMessage.messageId; + } + return params; + }; + const params = onBeforeSendFileMessage?.(file, quoteMessage) ?? createParamsDefault(); + logger.info('Thread | useSendFileMessageCallback: Sending file message start.', params); + + currentChannel?.sendFileMessage(params) + .onPending((pendingMessage) => { + actions.sendMessageStart({ + ...pendingMessage, + url: URL.createObjectURL(file), + // pending thumbnail message seems to be failed + // @ts-ignore + requestState: 'pending', + isUserMessage: pendingMessage.isUserMessage, + isFileMessage: pendingMessage.isFileMessage, + isAdminMessage: pendingMessage.isAdminMessage, + isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, + }); + setTimeout(() => scrollIntoLast(), SCROLL_BOTTOM_DELAY_FOR_SEND); + }) + .onFailed((error, message) => { + (message as LocalFileMessage).localUrl = URL.createObjectURL(file); + (message as LocalFileMessage).file = file; + logger.info('Thread | useSendFileMessageCallback: Sending file message failed.', { message, error }); + actions.sendMessageFailure(message as SendableMessageType); + reject(error); + }) + .onSucceeded((message) => { + logger.info('Thread | useSendFileMessageCallback: Sending file message succeeded.', message); + pubSub.publish(topics.SEND_FILE_MESSAGE, { + channel: currentChannel, + message: message as FileMessage, + publishingModules: [PublishingModuleType.THREAD], + }); + resolve(message as FileMessage); + }); + }); + }, [currentChannel]); + + const sendVoiceMessage = useCallback((file: File, duration: number, quoteMessage: SendableMessageType) => { + const messageParams: FileMessageCreateParams = ( + onBeforeSendVoiceMessage + && typeof onBeforeSendVoiceMessage === 'function' + ) + ? onBeforeSendVoiceMessage(file, quoteMessage) + : { + file, + fileName: VOICE_MESSAGE_FILE_NAME, + mimeType: VOICE_MESSAGE_MIME_TYPE, + metaArrays: [ + new MessageMetaArray({ + key: META_ARRAY_VOICE_DURATION_KEY, + value: [`${duration}`], + }), + new MessageMetaArray({ + key: META_ARRAY_MESSAGE_TYPE_KEY, + value: [META_ARRAY_MESSAGE_TYPE_VALUE__VOICE], + }), + ], + }; + if (quoteMessage) { + messageParams.isReplyToChannel = true; + messageParams.parentMessageId = quoteMessage.messageId; + } + logger.info('Thread | useSendVoiceMessageCallback: Start sending voice message', messageParams); + currentChannel?.sendFileMessage(messageParams) + .onPending((pendingMessage) => { + actions.sendMessageStart({ + /* pubSub is used instead of messagesDispatcher + to avoid redundantly calling `messageActionTypes.SEND_MESSAGE_START` */ + // TODO: remove data pollution + ...pendingMessage, + url: URL.createObjectURL(file), + // pending thumbnail message seems to be failed + // @ts-ignore + requestState: 'pending', + isUserMessage: pendingMessage.isUserMessage, + isFileMessage: pendingMessage.isFileMessage, + isAdminMessage: pendingMessage.isAdminMessage, + isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, + }); + setTimeout(() => scrollIntoLast(), SCROLL_BOTTOM_DELAY_FOR_SEND); + }) + .onFailed((error, message) => { + (message as LocalFileMessage).localUrl = URL.createObjectURL(file); + (message as LocalFileMessage).file = file; + logger.info('Thread | useSendVoiceMessageCallback: Sending voice message failed.', { message, error }); + actions.sendMessageFailure(message as SendableMessageType); + }) + .onSucceeded((message) => { + logger.info('Thread | useSendVoiceMessageCallback: Sending voice message succeeded.', message); + pubSub.publish(topics.SEND_FILE_MESSAGE, { + channel: currentChannel, + message: message as FileMessage, + publishingModules: [PublishingModuleType.THREAD], + }); + }); + }, [ + currentChannel, + onBeforeSendVoiceMessage, + ]); + + const sendMultipleFilesMessage = useCallback(( + files: Array, + quoteMessage?: SendableMessageType, + ): Promise => { + return new Promise((resolve, reject) => { + if (!currentChannel) { + logger.warning('Channel: Sending MFm failed, because currentChannel is null.', { currentChannel }); + reject(); + } + if (files.length <= 1) { + logger.warning('Channel: Sending MFM failed, because there are no multiple files.', { files }); + reject(); + } + let messageParams: MultipleFilesMessageCreateParams = { + fileInfoList: files.map((file: File): UploadableFileInfo => ({ + file, + fileName: file.name, + fileSize: file.size, + mimeType: file.type, + })), + }; + if (quoteMessage) { + messageParams.isReplyToChannel = true; + messageParams.parentMessageId = quoteMessage.messageId; + } + if (typeof onBeforeSendMultipleFilesMessage === 'function') { + messageParams = onBeforeSendMultipleFilesMessage(files, quoteMessage); + } + logger.info('Channel: Start sending MFM', { messageParams }); + try { + currentChannel?.sendMultipleFilesMessage(messageParams) + .onFileUploaded((requestId, index, uploadableFileInfo: UploadableFileInfo, error) => { + logger.info('Channel: onFileUploaded during sending MFM', { + requestId, + index, + error, + uploadableFileInfo, + }); + pubSub.publish(PUBSUB_TOPICS.ON_FILE_INFO_UPLOADED, { + response: { + channelUrl: currentChannel.url, + requestId, + index, + uploadableFileInfo, + error, + }, + publishingModules: [PublishingModuleType.THREAD], + }); + }) + .onPending((pendingMessage: MultipleFilesMessage) => { + logger.info('Channel: in progress of sending MFM', { pendingMessage, fileInfoList: messageParams.fileInfoList }); + pubSub.publish(PUBSUB_TOPICS.SEND_MESSAGE_START, { + message: pendingMessage, + channel: currentChannel, + publishingModules: [PublishingModuleType.THREAD], + }); + setTimeout(() => { + if (shouldPubSubPublishToThread([PublishingModuleType.THREAD])) { + scrollIntoLastForThread(0); + } + }, SCROLL_BOTTOM_DELAY_FOR_SEND); + }) + .onFailed((error, failedMessage: MultipleFilesMessage) => { + logger.error('Channel: Sending MFM failed.', { error, failedMessage }); + pubSub.publish(PUBSUB_TOPICS.SEND_MESSAGE_FAILED, { + channel: currentChannel, + message: failedMessage, + publishingModules: [PublishingModuleType.THREAD], + error, + }); + reject(error); + }) + .onSucceeded((succeededMessage: MultipleFilesMessage) => { + logger.info('Channel: Sending voice message success!', { succeededMessage }); + pubSub.publish(PUBSUB_TOPICS.SEND_FILE_MESSAGE, { + channel: currentChannel, + message: succeededMessage, + publishingModules: [PublishingModuleType.THREAD], + }); + resolve(succeededMessage); + }); + } catch (error) { + logger.error('Channel: Sending MFM failed.', { error }); + reject(error); + } + }); + }, [ + currentChannel, + onBeforeSendMultipleFilesMessage, + ]); + + const resendMessage = useCallback((failedMessage: SendableMessageType) => { + if ((failedMessage as SendableMessageType)?.isResendable) { + logger.info('Thread | useResendMessageCallback: Resending failedMessage start.', failedMessage); + if (failedMessage?.isUserMessage?.() || failedMessage?.messageType === MessageType.USER) { + try { + currentChannel?.resendMessage(failedMessage as UserMessage) + .onPending((message) => { + logger.info('Thread | useResendMessageCallback: Resending user message started.', message); + actions.resendMessageStart(message); + }) + .onSucceeded((message) => { + logger.info('Thread | useResendMessageCallback: Resending user message succeeded.', message); + actions.sendMessageSuccess(message); + pubSub.publish(topics.SEND_USER_MESSAGE, { + channel: currentChannel, + message: message, + publishingModules: [PublishingModuleType.THREAD], + }); + }) + .onFailed((error) => { + logger.warning('Thread | useResendMessageCallback: Resending user message failed.', error); + failedMessage.sendingStatus = SendingStatus.FAILED; + actions.sendMessageFailure(failedMessage); + }); + } catch (err) { + logger.warning('Thread | useResendMessageCallback: Resending user message failed.', err); + failedMessage.sendingStatus = SendingStatus.FAILED; + actions.sendMessageFailure(failedMessage); + } + } else if (failedMessage?.isFileMessage?.()) { + try { + currentChannel?.resendMessage?.(failedMessage as FileMessage) + .onPending((message) => { + logger.info('Thread | useResendMessageCallback: Resending file message started.', message); + actions.resendMessageStart(message); + }) + .onSucceeded((message) => { + logger.info('Thread | useResendMessageCallback: Resending file message succeeded.', message); + actions.sendMessageSuccess(message); + pubSub.publish(topics.SEND_FILE_MESSAGE, { + channel: currentChannel, + message: failedMessage, + publishingModules: [PublishingModuleType.THREAD], + }); + }) + .onFailed((error) => { + logger.warning('Thread | useResendMessageCallback: Resending file message failed.', error); + failedMessage.sendingStatus = SendingStatus.FAILED; + actions.sendMessageFailure(failedMessage); + }); + } catch (err) { + logger.warning('Thread | useResendMessageCallback: Resending file message failed.', err); + failedMessage.sendingStatus = SendingStatus.FAILED; + actions.sendMessageFailure(failedMessage); + } + } else if (failedMessage?.isMultipleFilesMessage?.()) { + try { + currentChannel?.resendMessage?.(failedMessage as MultipleFilesMessage) + .onPending((message) => { + logger.info('Thread | useResendMessageCallback: Resending multiple files message started.', message); + actions.resendMessageStart(message); + }) + .onFileUploaded((requestId, index, uploadableFileInfo: UploadableFileInfo, error) => { + logger.info('Thread | useResendMessageCallback: onFileUploaded during resending multiple files message.', { + requestId, + index, + error, + uploadableFileInfo, + }); + pubSub.publish(topics.ON_FILE_INFO_UPLOADED, { + response: { + channelUrl: currentChannel.url, + requestId, + index, + uploadableFileInfo, + error, + }, + publishingModules: [PublishingModuleType.THREAD], + }); + }) + .onSucceeded((message: MultipleFilesMessage) => { + logger.info('Thread | useResendMessageCallback: Resending MFM succeeded.', message); + actions.sendMessageSuccess(message); + pubSub.publish(topics.SEND_FILE_MESSAGE, { + channel: currentChannel, + message, + publishingModules: [PublishingModuleType.THREAD], + }); + }) + .onFailed((error, message) => { + logger.warning('Thread | useResendMessageCallback: Resending MFM failed.', error); + actions.sendMessageFailure(message); + }); + } catch (err) { + logger.warning('Thread | useResendMessageCallback: Resending MFM failed.', err); + actions.sendMessageFailure(failedMessage); + } + } else { + logger.warning('Thread | useResendMessageCallback: Message is not resendable.', failedMessage); + failedMessage.sendingStatus = SendingStatus.FAILED; + actions.sendMessageFailure(failedMessage); + } + } + }, [currentChannel]); - const actions = useMemo(() => ({ - setCurrentUserId: (currentUserId: string) => store.setState(state => ({ - ...state, - currentUserId: currentUserId, - })), + const anchorMessage = message?.messageId !== parentMessage?.messageId ? message || undefined : undefined; + const timestamp = anchorMessage?.createdAt || 0; + const oldestMessageTimeStamp = allThreadMessages[0]?.createdAt || 0; + const latestMessageTimeStamp = allThreadMessages[allThreadMessages.length - 1]?.createdAt || 0; - toggleReaction: useToggleReactionCallback({ currentChannel: state.currentChannel }, { logger }), + const initializeThreadFetcher = useCallback( + async (callback?: (messages: BaseMessage[]) => void) => { + const staleParentMessage = parentMessage; - sendMessage: useSendUserMessageCallback({ - isMentionEnabled: isMentionEnabled, - currentChannel: state.currentChannel, - onBeforeSendUserMessage: state.onBeforeSendUserMessage, - }, { - logger, - pubSub, - }), + if (!stores.sdkStore.initialized || !staleParentMessage) return; - sendFileMessage: useSendFileMessageCallback({ - currentChannel: state.currentChannel, - onBeforeSendFileMessage: state.onBeforeSendFileMessage, - }, { - logger, - pubSub, - }), + actions.initializeThreadListStart(); - sendVoiceMessage: useSendVoiceMessageCallback({ - currentChannel: state.currentChannel, - onBeforeSendVoiceMessage: state.onBeforeSendVoiceMessage, - }, { - logger, - pubSub, - }), + try { + const params = getThreadMessageListParams({ includeReactions: isReactionEnabled }); + logger.info('Thread | useGetThreadList: Initialize thread list start.', { timestamp, params }); + + const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(timestamp, params); + logger.info('Thread | useGetThreadList: Initialize thread list succeeded.', { staleParentMessage, threadedMessages }); + actions.initializeThreadListSuccess(parentMessage, anchorMessage, threadedMessages); + setTimeout(() => callback?.(threadedMessages)); + } catch (error) { + logger.info('Thread | useGetThreadList: Initialize thread list failed.', error); + actions.initializeThreadListFailure(); + } + }, + [stores.sdkStore.initialized, parentMessage, anchorMessage, isReactionEnabled], + ); + + const fetchPrevThreads = useCallback( + async (callback?: (messages: BaseMessage[]) => void) => { + const staleParentMessage = parentMessage; + + if (threadListState !== ThreadListStateTypes.INITIALIZED || oldestMessageTimeStamp === 0 || !staleParentMessage) return; + + actions.getPrevMessagesStart(); + + try { + const params = getThreadMessageListParams({ nextResultSize: 0, includeReactions: isReactionEnabled }); + + const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(oldestMessageTimeStamp, params); + + logger.info('Thread | useGetPrevThreadsCallback: Fetch prev threads succeeded.', { parentMessage, threadedMessages }); + actions.getPrevMessagesSuccess(threadedMessages as CoreMessageType[]); + setTimeout(() => callback?.(threadedMessages)); + } catch (error) { + logger.info('Thread | useGetPrevThreadsCallback: Fetch prev threads failed.', error); + actions.getPrevMessagesFailure(); + } + }, + [threadListState, oldestMessageTimeStamp, isReactionEnabled, parentMessage], + ); - sendMultipleFilesMessage: useSendMultipleFilesMessage({ - currentChannel: state.currentChannel, - onBeforeSendMultipleFilesMessage: state.onBeforeSendMultipleFilesMessage, - publishingModules: [PublishingModuleType.THREAD], - }, { - logger, - pubSub, - })[0], + const fetchNextThreads = useCallback( + async (callback?: (messages: BaseMessage[]) => void) => { + const staleParentMessage = parentMessage; - resendMessage: useResendMessageCallback({ - currentChannel: state.currentChannel, - }, { logger, pubSub }), + if (threadListState !== ThreadListStateTypes.INITIALIZED || latestMessageTimeStamp === 0 || !staleParentMessage) return; - updateMessage: useUpdateMessageCallback({ - currentChannel: state.currentChannel, - isMentionEnabled, - }, { logger, pubSub }), + actions.getNextMessagesStart(); - deleteMessage: useDeleteMessageCallback( - { currentChannel: state.currentChannel }, - { logger }, - ), + try { + const params = getThreadMessageListParams({ prevResultSize: 0, includeReactions: isReactionEnabled }); - initializeThreadFetcher: initializeThreadFetcher, + const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(latestMessageTimeStamp, params); + logger.info('Thread | useGetNextThreadsCallback: Fetch next threads succeeded.', { parentMessage, threadedMessages }); + actions.getNextMessagesSuccess(threadedMessages as CoreMessageType[]); + setTimeout(() => callback?.(threadedMessages)); + } catch (error) { + logger.info('Thread | useGetNextThreadsCallback: Fetch next threads failed.', error); + actions.getNextMessagesFailure(); + } + }, + [threadListState, latestMessageTimeStamp, isReactionEnabled, parentMessage], + ); + + const updateMessage = useCallback((props: { + messageId: number; + message: string; + mentionedUsers?: User[]; + mentionTemplate?: string; + }) => { + const { + messageId, + message, + mentionedUsers, + mentionTemplate, + } = props; + + const createParamsDefault = () => { + const params = {} as UserMessageUpdateParams; + params.message = message; + if (isMentionEnabled && mentionedUsers && mentionedUsers?.length > 0) { + params.mentionedUsers = mentionedUsers; + } + if (isMentionEnabled && mentionTemplate) { + params.mentionedMessageTemplate = mentionTemplate; + } else { + params.mentionedMessageTemplate = message; + } + return params; + }; + + const params = createParamsDefault(); + logger.info('Thread | useUpdateMessageCallback: Message update start.', params); + + currentChannel?.updateUserMessage?.(messageId, params) + .then((message: UserMessage) => { + logger.info('Thread | useUpdateMessageCallback: Message update succeeded.', message); + actions.onMessageUpdated(currentChannel, message); + pubSub.publish( + topics.UPDATE_USER_MESSAGE, + { + fromSelector: true, + channel: currentChannel, + message: message, + publishingModules: [PublishingModuleType.THREAD], + }, + ); + }); + }, [currentChannel, isMentionEnabled]); + + const deleteMessage = useCallback((message: SendableMessageType): Promise => { + logger.info('Thread | useDeleteMessageCallback: Deleting message.', message); + const { sendingStatus } = message; + return new Promise((resolve, reject) => { + logger.info('Thread | useDeleteMessageCallback: Deleting message requestState:', sendingStatus); + // Message is only on local + if (sendingStatus === 'failed' || sendingStatus === 'pending') { + logger.info('Thread | useDeleteMessageCallback: Deleted message from local:', message); + actions.onMessageDeletedByReqId(message.reqId); + resolve(); + } - fetchPrevThreads: fetchPrevThreads, + logger.info('Thread | useDeleteMessageCallback: Deleting message from remote:', sendingStatus); + currentChannel?.deleteMessage?.(message) + .then(() => { + logger.info('Thread | useDeleteMessageCallback: Deleting message success!', message); + actions.onMessageDeleted(currentChannel, message.messageId); + resolve(); + }) + .catch((err) => { + logger.warning('Thread | useDeleteMessageCallback: Deleting message failed!', err); + reject(err); + }); + }); + }, [currentChannel]); - fetchNextThreads: fetchNextThreads, + const actions = useMemo(() => ({ + setCurrentUserId: (currentUserId: string) => store.setState(state => ({ + ...state, + currentUserId: currentUserId, + })), getChannelStart: () => store.setState(state => ({ ...state, @@ -487,6 +954,18 @@ const useThread = () => { }; }), + toggleReaction, + sendMessage, + sendFileMessage, + sendVoiceMessage, + sendMultipleFilesMessage, + resendMessage, + updateMessage, + deleteMessage, + initializeThreadFetcher, + fetchPrevThreads, + fetchNextThreads, + }), [store]); return { state, actions }; From b11d66c5fec0e327466e9abbc9e035c47fb58779 Mon Sep 17 00:00:00 2001 From: "junyoung.lim" Date: Tue, 12 Nov 2024 14:07:52 +0900 Subject: [PATCH 03/10] Add ts-ignore --- src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts b/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts index cc4c8dcbc..7a3962718 100644 --- a/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts @@ -80,6 +80,7 @@ export const useSendVoiceMessageCallback = ({ ...pendingMessage, url: URL.createObjectURL(file), // pending thumbnail message seems to be failed + // @ts-ignore requestState: 'pending', isUserMessage: pendingMessage.isUserMessage, isFileMessage: pendingMessage.isFileMessage, From 493aaba181ec6877413a460bb5b70a3ce186b5df Mon Sep 17 00:00:00 2001 From: "junyoung.lim" Date: Wed, 27 Nov 2024 13:08:44 +0900 Subject: [PATCH 04/10] Apply the feedback and write basic unit test --- .../components/ThreadList/ThreadListItem.tsx | 3 +- .../context/__test__/ThreadProvider.spec.tsx | 109 +++ .../Thread/context/hooks/useGetChannel.ts | 1 + .../context/hooks/useHandleChannelEvents.ts | 1 + .../context/hooks/useSetCurrentUserId.ts | 2 +- src/modules/Thread/context/useThread.ts | 876 +++++++++--------- 6 files changed, 557 insertions(+), 435 deletions(-) create mode 100644 src/modules/Thread/context/__test__/ThreadProvider.spec.tsx diff --git a/src/modules/Thread/components/ThreadList/ThreadListItem.tsx b/src/modules/Thread/components/ThreadList/ThreadListItem.tsx index c227254ff..873e04cce 100644 --- a/src/modules/Thread/components/ThreadList/ThreadListItem.tsx +++ b/src/modules/Thread/components/ThreadList/ThreadListItem.tsx @@ -47,7 +47,6 @@ export default function ThreadListItem(props: ThreadListItemProps): React.ReactE const { isOnline, userMention, logger, groupChannel } = config; const userId = stores?.userStore?.user?.userId; const { dateLocale, stringSet } = useLocalization(); - const threadContext = useThread?.(); const { state: { message: openingMessage, @@ -65,7 +64,7 @@ export default function ThreadListItem(props: ThreadListItemProps): React.ReactE resendMessage, deleteMessage, }, - } = threadContext; + } = useThread(); const [showEdit, setShowEdit] = useState(false); const [showRemove, setShowRemove] = useState(false); 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..f95baf91e --- /dev/null +++ b/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { act, waitFor } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { ThreadProvider, ThreadState } from '../ThreadProvider'; +import { ChannelStateTypes, ParentMessageStateTypes, ThreadListStateTypes } from '../../types'; +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 initialState: ThreadState = { + channelUrl: '', + message: null, + currentChannel: null, + allThreadMessages: [], + localThreadMessages: [], + parentMessage: null, + channelState: ChannelStateTypes.NIL, + parentMessageState: ParentMessageStateTypes.NIL, + threadListState: ThreadListStateTypes.NIL, + hasMorePrev: false, + hasMoreNext: false, + emojiContainer: {} as any, + isMuted: false, + isChannelFrozen: false, + currentUserId: '', + typingMembers: [], + nicknamesMap: new Map(), + }; + + 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('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'); + }); + }); + }); +}); diff --git a/src/modules/Thread/context/hooks/useGetChannel.ts b/src/modules/Thread/context/hooks/useGetChannel.ts index 428f28c61..7319ba6a8 100644 --- a/src/modules/Thread/context/hooks/useGetChannel.ts +++ b/src/modules/Thread/context/hooks/useGetChannel.ts @@ -35,6 +35,7 @@ export default function useGetChannel({ useEffect(() => { // validation check if (sdkInit && channelUrl && sdk?.groupChannel) { + logger.info('Thread | useInitialize: Get channel started'); getChannelStart(); sdk.groupChannel.getChannel?.(channelUrl) .then((groupChannel) => { diff --git a/src/modules/Thread/context/hooks/useHandleChannelEvents.ts b/src/modules/Thread/context/hooks/useHandleChannelEvents.ts index fbda14458..2cd11c4cb 100644 --- a/src/modules/Thread/context/hooks/useHandleChannelEvents.ts +++ b/src/modules/Thread/context/hooks/useHandleChannelEvents.ts @@ -38,6 +38,7 @@ export default function useHandleChannelEvents({ onTypingStatusUpdated, }, } = useThread(); + useEffect(() => { const handlerId = uuidv4(); // validation check diff --git a/src/modules/Thread/context/hooks/useSetCurrentUserId.ts b/src/modules/Thread/context/hooks/useSetCurrentUserId.ts index 880a2897c..fe3e02db8 100644 --- a/src/modules/Thread/context/hooks/useSetCurrentUserId.ts +++ b/src/modules/Thread/context/hooks/useSetCurrentUserId.ts @@ -17,7 +17,7 @@ function useSetCurrentUserId( useEffect(() => { setCurrentUserId(user?.userId); - }, [user]); + }, [user?.userId]); } export default useSetCurrentUserId; diff --git a/src/modules/Thread/context/useThread.ts b/src/modules/Thread/context/useThread.ts index 8e7983373..edca38318 100644 --- a/src/modules/Thread/context/useThread.ts +++ b/src/modules/Thread/context/useThread.ts @@ -72,72 +72,137 @@ const useThread = () => { const toggleReaction = useToggleReactionCallback({ currentChannel }, { logger }); - const sendMessage = useCallback((props: SendMessageParams) => { - const { - message, - quoteMessage, - mentionTemplate, - mentionedUsers, - } = props; - - const createDefaultParams = () => { - const params = {} as UserMessageCreateParams; - params.message = message; - const mentionedUsersLength = mentionedUsers?.length || 0; - if (isMentionEnabled && mentionedUsersLength) { - params.mentionedUsers = mentionedUsers; - } - if (isMentionEnabled && mentionTemplate && mentionedUsersLength) { - params.mentionedMessageTemplate = mentionTemplate; - } - if (quoteMessage) { - params.isReplyToChannel = true; - params.parentMessageId = quoteMessage.messageId; - } - return params; - }; - - const params = onBeforeSendUserMessage?.(message, quoteMessage) ?? createDefaultParams(); - logger.info('Thread | useSendUserMessageCallback: Sending user message start.', params); - - if (currentChannel?.sendUserMessage) { - currentChannel?.sendUserMessage(params) - .onPending((pendingMessage) => { - actions.sendMessageStart(pendingMessage as SendableMessageType); - }) - .onFailed((error, message) => { - logger.info('Thread | useSendUserMessageCallback: Sending user message failed.', { message, error }); - actions.sendMessageFailure(message as SendableMessageType); - }) - .onSucceeded((message) => { - logger.info('Thread | useSendUserMessageCallback: Sending user message succeeded.', message); - // because Thread doesn't subscribe SEND_USER_MESSAGE - pubSub.publish(topics.SEND_USER_MESSAGE, { - channel: currentChannel, - message: message as UserMessage, - publishingModules: [PublishingModuleType.THREAD], - }); - }); - } - }, [isMentionEnabled, currentChannel]); - - const sendFileMessage = useCallback((file, quoteMessage): Promise => { - return new Promise((resolve, reject) => { - const createParamsDefault = () => { - const params = {} as FileMessageCreateParams; - params.file = file; + const sendMessageActions = { + sendMessage: useCallback((props: SendMessageParams) => { + const { + message, + quoteMessage, + mentionTemplate, + mentionedUsers, + } = props; + + const createDefaultParams = () => { + const params = {} as UserMessageCreateParams; + params.message = message; + const mentionedUsersLength = mentionedUsers?.length || 0; + if (isMentionEnabled && mentionedUsersLength) { + params.mentionedUsers = mentionedUsers; + } + if (isMentionEnabled && mentionTemplate && mentionedUsersLength) { + params.mentionedMessageTemplate = mentionTemplate; + } if (quoteMessage) { params.isReplyToChannel = true; params.parentMessageId = quoteMessage.messageId; } return params; }; - const params = onBeforeSendFileMessage?.(file, quoteMessage) ?? createParamsDefault(); - logger.info('Thread | useSendFileMessageCallback: Sending file message start.', params); - currentChannel?.sendFileMessage(params) + const params = onBeforeSendUserMessage?.(message, quoteMessage) ?? createDefaultParams(); + logger.info('Thread | useSendUserMessageCallback: Sending user message start.', params); + + if (currentChannel?.sendUserMessage) { + currentChannel?.sendUserMessage(params) + .onPending((pendingMessage) => { + actions.sendMessageStart(pendingMessage as SendableMessageType); + }) + .onFailed((error, message) => { + logger.info('Thread | useSendUserMessageCallback: Sending user message failed.', { message, error }); + actions.sendMessageFailure(message as SendableMessageType); + }) + .onSucceeded((message) => { + logger.info('Thread | useSendUserMessageCallback: Sending user message succeeded.', message); + actions.sendMessageSuccess(message as SendableMessageType); + // because Thread doesn't subscribe SEND_USER_MESSAGE + pubSub.publish(topics.SEND_USER_MESSAGE, { + channel: currentChannel, + message: message as UserMessage, + publishingModules: [PublishingModuleType.THREAD], + }); + }); + } + }, [isMentionEnabled, currentChannel]), + + sendFileMessage: useCallback((file, quoteMessage): Promise => { + return new Promise((resolve, reject) => { + const createParamsDefault = () => { + const params = {} as FileMessageCreateParams; + params.file = file; + if (quoteMessage) { + params.isReplyToChannel = true; + params.parentMessageId = quoteMessage.messageId; + } + return params; + }; + const params = onBeforeSendFileMessage?.(file, quoteMessage) ?? createParamsDefault(); + logger.info('Thread | useSendFileMessageCallback: Sending file message start.', params); + + currentChannel?.sendFileMessage(params) + .onPending((pendingMessage) => { + actions.sendMessageStart({ + ...pendingMessage, + url: URL.createObjectURL(file), + // pending thumbnail message seems to be failed + // @ts-ignore + requestState: 'pending', + isUserMessage: pendingMessage.isUserMessage, + isFileMessage: pendingMessage.isFileMessage, + isAdminMessage: pendingMessage.isAdminMessage, + isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, + }); + setTimeout(() => scrollIntoLast(), SCROLL_BOTTOM_DELAY_FOR_SEND); + }) + .onFailed((error, message) => { + (message as LocalFileMessage).localUrl = URL.createObjectURL(file); + (message as LocalFileMessage).file = file; + logger.info('Thread | useSendFileMessageCallback: Sending file message failed.', { message, error }); + actions.sendMessageFailure(message as SendableMessageType); + reject(error); + }) + .onSucceeded((message) => { + logger.info('Thread | useSendFileMessageCallback: Sending file message succeeded.', message); + pubSub.publish(topics.SEND_FILE_MESSAGE, { + channel: currentChannel, + message: message as FileMessage, + publishingModules: [PublishingModuleType.THREAD], + }); + resolve(message as FileMessage); + }); + }); + }, [currentChannel]), + + sendVoiceMessage: useCallback((file: File, duration: number, quoteMessage: SendableMessageType) => { + const messageParams: FileMessageCreateParams = ( + onBeforeSendVoiceMessage + && typeof onBeforeSendVoiceMessage === 'function' + ) + ? onBeforeSendVoiceMessage(file, quoteMessage) + : { + file, + fileName: VOICE_MESSAGE_FILE_NAME, + mimeType: VOICE_MESSAGE_MIME_TYPE, + metaArrays: [ + new MessageMetaArray({ + key: META_ARRAY_VOICE_DURATION_KEY, + value: [`${duration}`], + }), + new MessageMetaArray({ + key: META_ARRAY_MESSAGE_TYPE_KEY, + value: [META_ARRAY_MESSAGE_TYPE_VALUE__VOICE], + }), + ], + }; + if (quoteMessage) { + messageParams.isReplyToChannel = true; + messageParams.parentMessageId = quoteMessage.messageId; + } + logger.info('Thread | useSendVoiceMessageCallback: Start sending voice message', messageParams); + currentChannel?.sendFileMessage(messageParams) .onPending((pendingMessage) => { actions.sendMessageStart({ + /* pubSub is used instead of messagesDispatcher + to avoid redundantly calling `messageActionTypes.SEND_MESSAGE_START` */ + // TODO: remove data pollution ...pendingMessage, url: URL.createObjectURL(file), // pending thumbnail message seems to be failed @@ -153,246 +218,61 @@ const useThread = () => { .onFailed((error, message) => { (message as LocalFileMessage).localUrl = URL.createObjectURL(file); (message as LocalFileMessage).file = file; - logger.info('Thread | useSendFileMessageCallback: Sending file message failed.', { message, error }); + logger.info('Thread | useSendVoiceMessageCallback: Sending voice message failed.', { message, error }); actions.sendMessageFailure(message as SendableMessageType); - reject(error); }) .onSucceeded((message) => { - logger.info('Thread | useSendFileMessageCallback: Sending file message succeeded.', message); + logger.info('Thread | useSendVoiceMessageCallback: Sending voice message succeeded.', message); pubSub.publish(topics.SEND_FILE_MESSAGE, { channel: currentChannel, message: message as FileMessage, publishingModules: [PublishingModuleType.THREAD], }); - resolve(message as FileMessage); }); - }); - }, [currentChannel]); - - const sendVoiceMessage = useCallback((file: File, duration: number, quoteMessage: SendableMessageType) => { - const messageParams: FileMessageCreateParams = ( - onBeforeSendVoiceMessage - && typeof onBeforeSendVoiceMessage === 'function' - ) - ? onBeforeSendVoiceMessage(file, quoteMessage) - : { - file, - fileName: VOICE_MESSAGE_FILE_NAME, - mimeType: VOICE_MESSAGE_MIME_TYPE, - metaArrays: [ - new MessageMetaArray({ - key: META_ARRAY_VOICE_DURATION_KEY, - value: [`${duration}`], - }), - new MessageMetaArray({ - key: META_ARRAY_MESSAGE_TYPE_KEY, - value: [META_ARRAY_MESSAGE_TYPE_VALUE__VOICE], - }), - ], - }; - if (quoteMessage) { - messageParams.isReplyToChannel = true; - messageParams.parentMessageId = quoteMessage.messageId; - } - logger.info('Thread | useSendVoiceMessageCallback: Start sending voice message', messageParams); - currentChannel?.sendFileMessage(messageParams) - .onPending((pendingMessage) => { - actions.sendMessageStart({ - /* pubSub is used instead of messagesDispatcher - to avoid redundantly calling `messageActionTypes.SEND_MESSAGE_START` */ - // TODO: remove data pollution - ...pendingMessage, - url: URL.createObjectURL(file), - // pending thumbnail message seems to be failed - // @ts-ignore - requestState: 'pending', - isUserMessage: pendingMessage.isUserMessage, - isFileMessage: pendingMessage.isFileMessage, - isAdminMessage: pendingMessage.isAdminMessage, - isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, - }); - setTimeout(() => scrollIntoLast(), SCROLL_BOTTOM_DELAY_FOR_SEND); - }) - .onFailed((error, message) => { - (message as LocalFileMessage).localUrl = URL.createObjectURL(file); - (message as LocalFileMessage).file = file; - logger.info('Thread | useSendVoiceMessageCallback: Sending voice message failed.', { message, error }); - actions.sendMessageFailure(message as SendableMessageType); - }) - .onSucceeded((message) => { - logger.info('Thread | useSendVoiceMessageCallback: Sending voice message succeeded.', message); - pubSub.publish(topics.SEND_FILE_MESSAGE, { - channel: currentChannel, - message: message as FileMessage, - publishingModules: [PublishingModuleType.THREAD], - }); - }); - }, [ - currentChannel, - onBeforeSendVoiceMessage, - ]); - - const sendMultipleFilesMessage = useCallback(( - files: Array, - quoteMessage?: SendableMessageType, - ): Promise => { - return new Promise((resolve, reject) => { - if (!currentChannel) { - logger.warning('Channel: Sending MFm failed, because currentChannel is null.', { currentChannel }); - reject(); - } - if (files.length <= 1) { - logger.warning('Channel: Sending MFM failed, because there are no multiple files.', { files }); - reject(); - } - let messageParams: MultipleFilesMessageCreateParams = { - fileInfoList: files.map((file: File): UploadableFileInfo => ({ - file, - fileName: file.name, - fileSize: file.size, - mimeType: file.type, - })), - }; - if (quoteMessage) { - messageParams.isReplyToChannel = true; - messageParams.parentMessageId = quoteMessage.messageId; - } - if (typeof onBeforeSendMultipleFilesMessage === 'function') { - messageParams = onBeforeSendMultipleFilesMessage(files, quoteMessage); - } - logger.info('Channel: Start sending MFM', { messageParams }); - try { - currentChannel?.sendMultipleFilesMessage(messageParams) - .onFileUploaded((requestId, index, uploadableFileInfo: UploadableFileInfo, error) => { - logger.info('Channel: onFileUploaded during sending MFM', { - requestId, - index, - error, - uploadableFileInfo, - }); - pubSub.publish(PUBSUB_TOPICS.ON_FILE_INFO_UPLOADED, { - response: { - channelUrl: currentChannel.url, - requestId, - index, - uploadableFileInfo, - error, - }, - publishingModules: [PublishingModuleType.THREAD], - }); - }) - .onPending((pendingMessage: MultipleFilesMessage) => { - logger.info('Channel: in progress of sending MFM', { pendingMessage, fileInfoList: messageParams.fileInfoList }); - pubSub.publish(PUBSUB_TOPICS.SEND_MESSAGE_START, { - message: pendingMessage, - channel: currentChannel, - publishingModules: [PublishingModuleType.THREAD], - }); - setTimeout(() => { - if (shouldPubSubPublishToThread([PublishingModuleType.THREAD])) { - scrollIntoLastForThread(0); - } - }, SCROLL_BOTTOM_DELAY_FOR_SEND); - }) - .onFailed((error, failedMessage: MultipleFilesMessage) => { - logger.error('Channel: Sending MFM failed.', { error, failedMessage }); - pubSub.publish(PUBSUB_TOPICS.SEND_MESSAGE_FAILED, { - channel: currentChannel, - message: failedMessage, - publishingModules: [PublishingModuleType.THREAD], - error, - }); - reject(error); - }) - .onSucceeded((succeededMessage: MultipleFilesMessage) => { - logger.info('Channel: Sending voice message success!', { succeededMessage }); - pubSub.publish(PUBSUB_TOPICS.SEND_FILE_MESSAGE, { - channel: currentChannel, - message: succeededMessage, - publishingModules: [PublishingModuleType.THREAD], - }); - resolve(succeededMessage); - }); - } catch (error) { - logger.error('Channel: Sending MFM failed.', { error }); - reject(error); - } - }); - }, [ - currentChannel, - onBeforeSendMultipleFilesMessage, - ]); - - const resendMessage = useCallback((failedMessage: SendableMessageType) => { - if ((failedMessage as SendableMessageType)?.isResendable) { - logger.info('Thread | useResendMessageCallback: Resending failedMessage start.', failedMessage); - if (failedMessage?.isUserMessage?.() || failedMessage?.messageType === MessageType.USER) { - try { - currentChannel?.resendMessage(failedMessage as UserMessage) - .onPending((message) => { - logger.info('Thread | useResendMessageCallback: Resending user message started.', message); - actions.resendMessageStart(message); - }) - .onSucceeded((message) => { - logger.info('Thread | useResendMessageCallback: Resending user message succeeded.', message); - actions.sendMessageSuccess(message); - pubSub.publish(topics.SEND_USER_MESSAGE, { - channel: currentChannel, - message: message, - publishingModules: [PublishingModuleType.THREAD], - }); - }) - .onFailed((error) => { - logger.warning('Thread | useResendMessageCallback: Resending user message failed.', error); - failedMessage.sendingStatus = SendingStatus.FAILED; - actions.sendMessageFailure(failedMessage); - }); - } catch (err) { - logger.warning('Thread | useResendMessageCallback: Resending user message failed.', err); - failedMessage.sendingStatus = SendingStatus.FAILED; - actions.sendMessageFailure(failedMessage); + }, [ + currentChannel, + onBeforeSendVoiceMessage, + ]), + + sendMultipleFilesMessage: useCallback(( + files: Array, + quoteMessage?: SendableMessageType, + ): Promise => { + return new Promise((resolve, reject) => { + if (!currentChannel) { + logger.warning('Channel: Sending MFm failed, because currentChannel is null.', { currentChannel }); + reject(); } - } else if (failedMessage?.isFileMessage?.()) { - try { - currentChannel?.resendMessage?.(failedMessage as FileMessage) - .onPending((message) => { - logger.info('Thread | useResendMessageCallback: Resending file message started.', message); - actions.resendMessageStart(message); - }) - .onSucceeded((message) => { - logger.info('Thread | useResendMessageCallback: Resending file message succeeded.', message); - actions.sendMessageSuccess(message); - pubSub.publish(topics.SEND_FILE_MESSAGE, { - channel: currentChannel, - message: failedMessage, - publishingModules: [PublishingModuleType.THREAD], - }); - }) - .onFailed((error) => { - logger.warning('Thread | useResendMessageCallback: Resending file message failed.', error); - failedMessage.sendingStatus = SendingStatus.FAILED; - actions.sendMessageFailure(failedMessage); - }); - } catch (err) { - logger.warning('Thread | useResendMessageCallback: Resending file message failed.', err); - failedMessage.sendingStatus = SendingStatus.FAILED; - actions.sendMessageFailure(failedMessage); + if (files.length <= 1) { + logger.warning('Channel: Sending MFM failed, because there are no multiple files.', { files }); + reject(); + } + let messageParams: MultipleFilesMessageCreateParams = { + fileInfoList: files.map((file: File): UploadableFileInfo => ({ + file, + fileName: file.name, + fileSize: file.size, + mimeType: file.type, + })), + }; + if (quoteMessage) { + messageParams.isReplyToChannel = true; + messageParams.parentMessageId = quoteMessage.messageId; + } + if (typeof onBeforeSendMultipleFilesMessage === 'function') { + messageParams = onBeforeSendMultipleFilesMessage(files, quoteMessage); } - } else if (failedMessage?.isMultipleFilesMessage?.()) { + logger.info('Channel: Start sending MFM', { messageParams }); try { - currentChannel?.resendMessage?.(failedMessage as MultipleFilesMessage) - .onPending((message) => { - logger.info('Thread | useResendMessageCallback: Resending multiple files message started.', message); - actions.resendMessageStart(message); - }) + currentChannel?.sendMultipleFilesMessage(messageParams) .onFileUploaded((requestId, index, uploadableFileInfo: UploadableFileInfo, error) => { - logger.info('Thread | useResendMessageCallback: onFileUploaded during resending multiple files message.', { + logger.info('Channel: onFileUploaded during sending MFM', { requestId, index, error, uploadableFileInfo, }); - pubSub.publish(topics.ON_FILE_INFO_UPLOADED, { + pubSub.publish(PUBSUB_TOPICS.ON_FILE_INFO_UPLOADED, { response: { channelUrl: currentChannel.url, requestId, @@ -403,178 +283,318 @@ const useThread = () => { publishingModules: [PublishingModuleType.THREAD], }); }) - .onSucceeded((message: MultipleFilesMessage) => { - logger.info('Thread | useResendMessageCallback: Resending MFM succeeded.', message); - actions.sendMessageSuccess(message); - pubSub.publish(topics.SEND_FILE_MESSAGE, { + .onPending((pendingMessage: MultipleFilesMessage) => { + logger.info('Channel: in progress of sending MFM', { pendingMessage, fileInfoList: messageParams.fileInfoList }); + pubSub.publish(PUBSUB_TOPICS.SEND_MESSAGE_START, { + message: pendingMessage, channel: currentChannel, - message, publishingModules: [PublishingModuleType.THREAD], }); + setTimeout(() => { + if (shouldPubSubPublishToThread([PublishingModuleType.THREAD])) { + scrollIntoLastForThread(0); + } + }, SCROLL_BOTTOM_DELAY_FOR_SEND); }) - .onFailed((error, message) => { - logger.warning('Thread | useResendMessageCallback: Resending MFM failed.', error); - actions.sendMessageFailure(message); + .onFailed((error, failedMessage: MultipleFilesMessage) => { + logger.error('Channel: Sending MFM failed.', { error, failedMessage }); + pubSub.publish(PUBSUB_TOPICS.SEND_MESSAGE_FAILED, { + channel: currentChannel, + message: failedMessage, + publishingModules: [PublishingModuleType.THREAD], + error, + }); + reject(error); + }) + .onSucceeded((succeededMessage: MultipleFilesMessage) => { + logger.info('Channel: Sending voice message success!', { succeededMessage }); + pubSub.publish(PUBSUB_TOPICS.SEND_FILE_MESSAGE, { + channel: currentChannel, + message: succeededMessage, + publishingModules: [PublishingModuleType.THREAD], + }); + resolve(succeededMessage); }); - } catch (err) { - logger.warning('Thread | useResendMessageCallback: Resending MFM failed.', err); + } catch (error) { + logger.error('Channel: Sending MFM failed.', { error }); + reject(error); + } + }); + }, [ + currentChannel, + onBeforeSendMultipleFilesMessage, + ]), + + resendMessage: useCallback((failedMessage: SendableMessageType) => { + if ((failedMessage as SendableMessageType)?.isResendable) { + logger.info('Thread | useResendMessageCallback: Resending failedMessage start.', failedMessage); + if (failedMessage?.isUserMessage?.() || failedMessage?.messageType === MessageType.USER) { + try { + currentChannel?.resendMessage(failedMessage as UserMessage) + .onPending((message) => { + logger.info('Thread | useResendMessageCallback: Resending user message started.', message); + actions.resendMessageStart(message); + }) + .onSucceeded((message) => { + logger.info('Thread | useResendMessageCallback: Resending user message succeeded.', message); + actions.sendMessageSuccess(message); + pubSub.publish(topics.SEND_USER_MESSAGE, { + channel: currentChannel, + message: message, + publishingModules: [PublishingModuleType.THREAD], + }); + }) + .onFailed((error) => { + logger.warning('Thread | useResendMessageCallback: Resending user message failed.', error); + failedMessage.sendingStatus = SendingStatus.FAILED; + actions.sendMessageFailure(failedMessage); + }); + } catch (err) { + logger.warning('Thread | useResendMessageCallback: Resending user message failed.', err); + failedMessage.sendingStatus = SendingStatus.FAILED; + actions.sendMessageFailure(failedMessage); + } + } else if (failedMessage?.isFileMessage?.()) { + try { + currentChannel?.resendMessage?.(failedMessage as FileMessage) + .onPending((message) => { + logger.info('Thread | useResendMessageCallback: Resending file message started.', message); + actions.resendMessageStart(message); + }) + .onSucceeded((message) => { + logger.info('Thread | useResendMessageCallback: Resending file message succeeded.', message); + actions.sendMessageSuccess(message); + pubSub.publish(topics.SEND_FILE_MESSAGE, { + channel: currentChannel, + message: failedMessage, + publishingModules: [PublishingModuleType.THREAD], + }); + }) + .onFailed((error) => { + logger.warning('Thread | useResendMessageCallback: Resending file message failed.', error); + failedMessage.sendingStatus = SendingStatus.FAILED; + actions.sendMessageFailure(failedMessage); + }); + } catch (err) { + logger.warning('Thread | useResendMessageCallback: Resending file message failed.', err); + failedMessage.sendingStatus = SendingStatus.FAILED; + actions.sendMessageFailure(failedMessage); + } + } else if (failedMessage?.isMultipleFilesMessage?.()) { + try { + currentChannel?.resendMessage?.(failedMessage as MultipleFilesMessage) + .onPending((message) => { + logger.info('Thread | useResendMessageCallback: Resending multiple files message started.', message); + actions.resendMessageStart(message); + }) + .onFileUploaded((requestId, index, uploadableFileInfo: UploadableFileInfo, error) => { + logger.info('Thread | useResendMessageCallback: onFileUploaded during resending multiple files message.', { + requestId, + index, + error, + uploadableFileInfo, + }); + pubSub.publish(topics.ON_FILE_INFO_UPLOADED, { + response: { + channelUrl: currentChannel.url, + requestId, + index, + uploadableFileInfo, + error, + }, + publishingModules: [PublishingModuleType.THREAD], + }); + }) + .onSucceeded((message: MultipleFilesMessage) => { + logger.info('Thread | useResendMessageCallback: Resending MFM succeeded.', message); + actions.sendMessageSuccess(message); + pubSub.publish(topics.SEND_FILE_MESSAGE, { + channel: currentChannel, + message, + publishingModules: [PublishingModuleType.THREAD], + }); + }) + .onFailed((error, message) => { + logger.warning('Thread | useResendMessageCallback: Resending MFM failed.', error); + actions.sendMessageFailure(message); + }); + } catch (err) { + logger.warning('Thread | useResendMessageCallback: Resending MFM failed.', err); + actions.sendMessageFailure(failedMessage); + } + } else { + logger.warning('Thread | useResendMessageCallback: Message is not resendable.', failedMessage); + failedMessage.sendingStatus = SendingStatus.FAILED; actions.sendMessageFailure(failedMessage); } - } else { - logger.warning('Thread | useResendMessageCallback: Message is not resendable.', failedMessage); - failedMessage.sendingStatus = SendingStatus.FAILED; - actions.sendMessageFailure(failedMessage); } - } - }, [currentChannel]); + }, [currentChannel]), + }; const anchorMessage = message?.messageId !== parentMessage?.messageId ? message || undefined : undefined; const timestamp = anchorMessage?.createdAt || 0; const oldestMessageTimeStamp = allThreadMessages[0]?.createdAt || 0; const latestMessageTimeStamp = allThreadMessages[allThreadMessages.length - 1]?.createdAt || 0; - const initializeThreadFetcher = useCallback( - async (callback?: (messages: BaseMessage[]) => void) => { - const staleParentMessage = parentMessage; + const threadFetcherActions = { + initializeThreadFetcher: useCallback( + async (callback?: (messages: BaseMessage[]) => void) => { + const staleParentMessage = parentMessage; - if (!stores.sdkStore.initialized || !staleParentMessage) return; + if (!stores.sdkStore.initialized || !staleParentMessage) return; - actions.initializeThreadListStart(); + actions.initializeThreadListStart(); - try { - const params = getThreadMessageListParams({ includeReactions: isReactionEnabled }); - logger.info('Thread | useGetThreadList: Initialize thread list start.', { timestamp, params }); + try { + const params = getThreadMessageListParams({ includeReactions: isReactionEnabled }); + logger.info('Thread | useGetThreadList: Initialize thread list start.', { timestamp, params }); + + const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(timestamp, params); + logger.info('Thread | useGetThreadList: Initialize thread list succeeded.', { staleParentMessage, threadedMessages }); + actions.initializeThreadListSuccess(parentMessage, anchorMessage, threadedMessages); + setTimeout(() => callback?.(threadedMessages)); + } catch (error) { + logger.info('Thread | useGetThreadList: Initialize thread list failed.', error); + actions.initializeThreadListFailure(); + } + }, + [stores.sdkStore.initialized, parentMessage, anchorMessage, isReactionEnabled], + ), - const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(timestamp, params); - logger.info('Thread | useGetThreadList: Initialize thread list succeeded.', { staleParentMessage, threadedMessages }); - actions.initializeThreadListSuccess(parentMessage, anchorMessage, threadedMessages); - setTimeout(() => callback?.(threadedMessages)); - } catch (error) { - logger.info('Thread | useGetThreadList: Initialize thread list failed.', error); - actions.initializeThreadListFailure(); - } - }, - [stores.sdkStore.initialized, parentMessage, anchorMessage, isReactionEnabled], - ); + fetchPrevThreads: useCallback( + async (callback?: (messages: BaseMessage[]) => void) => { + const staleParentMessage = parentMessage; - const fetchPrevThreads = useCallback( - async (callback?: (messages: BaseMessage[]) => void) => { - const staleParentMessage = parentMessage; + if (threadListState !== ThreadListStateTypes.INITIALIZED || oldestMessageTimeStamp === 0 || !staleParentMessage) return; - if (threadListState !== ThreadListStateTypes.INITIALIZED || oldestMessageTimeStamp === 0 || !staleParentMessage) return; + actions.getPrevMessagesStart(); - actions.getPrevMessagesStart(); + try { + const params = getThreadMessageListParams({ nextResultSize: 0, includeReactions: isReactionEnabled }); - try { - const params = getThreadMessageListParams({ nextResultSize: 0, includeReactions: isReactionEnabled }); + const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(oldestMessageTimeStamp, params); - const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(oldestMessageTimeStamp, params); + logger.info('Thread | useGetPrevThreadsCallback: Fetch prev threads succeeded.', { parentMessage, threadedMessages }); + actions.getPrevMessagesSuccess(threadedMessages as CoreMessageType[]); + setTimeout(() => callback?.(threadedMessages)); + } catch (error) { + logger.info('Thread | useGetPrevThreadsCallback: Fetch prev threads failed.', error); + actions.getPrevMessagesFailure(); + } + }, + [threadListState, oldestMessageTimeStamp, isReactionEnabled, parentMessage], + ), - logger.info('Thread | useGetPrevThreadsCallback: Fetch prev threads succeeded.', { parentMessage, threadedMessages }); - actions.getPrevMessagesSuccess(threadedMessages as CoreMessageType[]); - setTimeout(() => callback?.(threadedMessages)); - } catch (error) { - logger.info('Thread | useGetPrevThreadsCallback: Fetch prev threads failed.', error); - actions.getPrevMessagesFailure(); - } - }, - [threadListState, oldestMessageTimeStamp, isReactionEnabled, parentMessage], - ); + fetchNextThreads: useCallback( + async (callback?: (messages: BaseMessage[]) => void) => { + const staleParentMessage = parentMessage; - const fetchNextThreads = useCallback( - async (callback?: (messages: BaseMessage[]) => void) => { - const staleParentMessage = parentMessage; + if (threadListState !== ThreadListStateTypes.INITIALIZED || latestMessageTimeStamp === 0 || !staleParentMessage) return; - if (threadListState !== ThreadListStateTypes.INITIALIZED || latestMessageTimeStamp === 0 || !staleParentMessage) return; + actions.getNextMessagesStart(); - actions.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 }); + actions.getNextMessagesSuccess(threadedMessages as CoreMessageType[]); + setTimeout(() => callback?.(threadedMessages)); + } catch (error) { + logger.info('Thread | useGetNextThreadsCallback: Fetch next threads failed.', error); + actions.getNextMessagesFailure(); + } + }, + [threadListState, latestMessageTimeStamp, isReactionEnabled, parentMessage], + ), + }; - try { - const params = getThreadMessageListParams({ prevResultSize: 0, includeReactions: isReactionEnabled }); + const modifyMessageActions = { + updateMessage: useCallback((props: { + messageId: number; + message: string; + mentionedUsers?: User[]; + mentionTemplate?: string; + }) => { + const { + messageId, + message, + mentionedUsers, + mentionTemplate, + } = props; - const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(latestMessageTimeStamp, params); - logger.info('Thread | useGetNextThreadsCallback: Fetch next threads succeeded.', { parentMessage, threadedMessages }); - actions.getNextMessagesSuccess(threadedMessages as CoreMessageType[]); - setTimeout(() => callback?.(threadedMessages)); - } catch (error) { - logger.info('Thread | useGetNextThreadsCallback: Fetch next threads failed.', error); - actions.getNextMessagesFailure(); - } - }, - [threadListState, latestMessageTimeStamp, isReactionEnabled, parentMessage], - ); - - const updateMessage = useCallback((props: { - messageId: number; - message: string; - mentionedUsers?: User[]; - mentionTemplate?: string; - }) => { - const { - messageId, - message, - mentionedUsers, - mentionTemplate, - } = props; - - const createParamsDefault = () => { - const params = {} as UserMessageUpdateParams; - params.message = message; - if (isMentionEnabled && mentionedUsers && mentionedUsers?.length > 0) { - params.mentionedUsers = mentionedUsers; - } - if (isMentionEnabled && mentionTemplate) { - params.mentionedMessageTemplate = mentionTemplate; - } else { - params.mentionedMessageTemplate = message; - } - return params; - }; - - const params = createParamsDefault(); - logger.info('Thread | useUpdateMessageCallback: Message update start.', params); - - currentChannel?.updateUserMessage?.(messageId, params) - .then((message: UserMessage) => { - logger.info('Thread | useUpdateMessageCallback: Message update succeeded.', message); - actions.onMessageUpdated(currentChannel, message); - pubSub.publish( - topics.UPDATE_USER_MESSAGE, - { - fromSelector: true, - channel: currentChannel, - message: message, - publishingModules: [PublishingModuleType.THREAD], - }, - ); - }); - }, [currentChannel, isMentionEnabled]); - - const deleteMessage = useCallback((message: SendableMessageType): Promise => { - logger.info('Thread | useDeleteMessageCallback: Deleting message.', message); - const { sendingStatus } = message; - return new Promise((resolve, reject) => { - logger.info('Thread | useDeleteMessageCallback: Deleting message requestState:', sendingStatus); - // Message is only on local - if (sendingStatus === 'failed' || sendingStatus === 'pending') { - logger.info('Thread | useDeleteMessageCallback: Deleted message from local:', message); - actions.onMessageDeletedByReqId(message.reqId); - resolve(); - } + const createParamsDefault = () => { + const params = {} as UserMessageUpdateParams; + params.message = message; + if (isMentionEnabled && mentionedUsers && mentionedUsers?.length > 0) { + params.mentionedUsers = mentionedUsers; + } + if (isMentionEnabled && mentionTemplate) { + params.mentionedMessageTemplate = mentionTemplate; + } else { + params.mentionedMessageTemplate = message; + } + return params; + }; - logger.info('Thread | useDeleteMessageCallback: Deleting message from remote:', sendingStatus); - currentChannel?.deleteMessage?.(message) - .then(() => { - logger.info('Thread | useDeleteMessageCallback: Deleting message success!', message); - actions.onMessageDeleted(currentChannel, message.messageId); - resolve(); - }) - .catch((err) => { - logger.warning('Thread | useDeleteMessageCallback: Deleting message failed!', err); - reject(err); + const params = createParamsDefault(); + logger.info('Thread | useUpdateMessageCallback: Message update start.', params); + + currentChannel?.updateUserMessage?.(messageId, params) + .then((message: UserMessage) => { + logger.info('Thread | useUpdateMessageCallback: Message update succeeded.', message); + actions.onMessageUpdated(currentChannel, message); + pubSub.publish( + topics.UPDATE_USER_MESSAGE, + { + fromSelector: true, + channel: currentChannel, + message: message, + publishingModules: [PublishingModuleType.THREAD], + }, + ); }); - }); - }, [currentChannel]); + }, [currentChannel, isMentionEnabled]), + + deleteMessage: useCallback((message: SendableMessageType): Promise => { + logger.info('Thread | useDeleteMessageCallback: Deleting message.', message); + const { sendingStatus } = message; + return new Promise((resolve, reject) => { + logger.info('Thread | useDeleteMessageCallback: Deleting message requestState:', sendingStatus); + // Message is only on local + if (sendingStatus === 'failed' || sendingStatus === 'pending') { + logger.info('Thread | useDeleteMessageCallback: Deleted message from local:', message); + actions.onMessageDeletedByReqId(message.reqId); + resolve(); + } + + logger.info('Thread | useDeleteMessageCallback: Deleting message from remote:', sendingStatus); + currentChannel?.deleteMessage?.(message) + .then(() => { + logger.info('Thread | useDeleteMessageCallback: Deleting message success!', message); + actions.onMessageDeleted(currentChannel, message.messageId); + resolve(); + }) + .catch((err) => { + logger.warning('Thread | useDeleteMessageCallback: Deleting message failed!', err); + reject(err); + }); + }); + }, [currentChannel]), + }; + + // const getChannelSuccess = (groupChannel: GroupChannel) => { + // console.log('getChannelSuccess'); + // console.log(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, + // })); + // }; const actions = useMemo(() => ({ setCurrentUserId: (currentUserId: string) => store.setState(state => ({ @@ -955,18 +975,10 @@ const useThread = () => { }), toggleReaction, - sendMessage, - sendFileMessage, - sendVoiceMessage, - sendMultipleFilesMessage, - resendMessage, - updateMessage, - deleteMessage, - initializeThreadFetcher, - fetchPrevThreads, - fetchNextThreads, - - }), [store]); + ...sendMessageActions, + ...modifyMessageActions, + ...threadFetcherActions, + }), [store, currentChannel]); return { state, actions }; }; From c945c8183e04d2cd141e7bebfa484069e8f7ef62 Mon Sep 17 00:00:00 2001 From: "junyoung.lim" Date: Thu, 28 Nov 2024 15:51:38 +0900 Subject: [PATCH 05/10] Change custom hook convention --- .../context/__test__/ThreadProvider.spec.tsx | 23 +- .../context/hooks/useDeleteMessageCallback.ts | 11 +- .../context/hooks/useResendMessageCallback.ts | 14 +- .../context/hooks/useSendFileMessage.ts | 12 +- .../hooks/useSendUserMessageCallback.ts | 12 +- .../hooks/useSendVoiceMessageCallback.ts | 12 +- .../Thread/context/hooks/useThreadFetchers.ts | 32 +- .../context/hooks/useUpdateMessageCallback.ts | 10 +- src/modules/Thread/context/useThread.ts | 971 +++++------------- 9 files changed, 307 insertions(+), 790 deletions(-) diff --git a/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx b/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx index f95baf91e..005801b78 100644 --- a/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx +++ b/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { act, waitFor } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; -import { ThreadProvider, ThreadState } from '../ThreadProvider'; -import { ChannelStateTypes, ParentMessageStateTypes, ThreadListStateTypes } from '../../types'; +import { ThreadProvider } from '../ThreadProvider'; import useThread from '../useThread'; import { SendableMessageType } from '../../../../utils'; @@ -47,26 +46,6 @@ jest.mock('../../../../hooks/useSendbirdStateContext', () => ({ })); describe('ThreadProvider', () => { - const initialState: ThreadState = { - channelUrl: '', - message: null, - currentChannel: null, - allThreadMessages: [], - localThreadMessages: [], - parentMessage: null, - channelState: ChannelStateTypes.NIL, - parentMessageState: ParentMessageStateTypes.NIL, - threadListState: ThreadListStateTypes.NIL, - hasMorePrev: false, - hasMoreNext: false, - emojiContainer: {} as any, - isMuted: false, - isChannelFrozen: false, - currentUserId: '', - typingMembers: [], - nicknamesMap: new Map(), - }; - const initialMockMessage = { messageId: 1, } as SendableMessageType; diff --git a/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts b/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts index 8921e945c..021ca7d27 100644 --- a/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useDeleteMessageCallback.ts @@ -2,10 +2,11 @@ import { GroupChannel } from '@sendbird/chat/groupChannel'; import { useCallback } from 'react'; import { Logger } from '../../../../lib/SendbirdState'; import { SendableMessageType } from '../../../../utils'; -import useThread from '../useThread'; interface DynamicProps { currentChannel: GroupChannel | null; + onMessageDeletedByReqId: (reqId: string | number) => void, + onMessageDeleted: (channel: GroupChannel, messageId: number) => void, } interface StaticProps { logger: Logger; @@ -13,15 +14,11 @@ interface StaticProps { export default function useDeleteMessageCallback({ currentChannel, + onMessageDeletedByReqId, + onMessageDeleted, }: DynamicProps, { logger, }: StaticProps): (message: SendableMessageType) => Promise { - const { - actions: { - onMessageDeletedByReqId, - onMessageDeleted, - }, - } = useThread(); return useCallback((message: SendableMessageType): Promise => { logger.info('Thread | useDeleteMessageCallback: Deleting message.', message); const { sendingStatus } = message; diff --git a/src/modules/Thread/context/hooks/useResendMessageCallback.ts b/src/modules/Thread/context/hooks/useResendMessageCallback.ts index 98c3c47a2..2e9d5e20c 100644 --- a/src/modules/Thread/context/hooks/useResendMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useResendMessageCallback.ts @@ -12,10 +12,12 @@ import { Logger } from '../../../../lib/SendbirdState'; import topics, { SBUGlobalPubSub } from '../../../../lib/pubSub/topics'; import { SendableMessageType } from '../../../../utils'; import { PublishingModuleType } from '../../../internalInterfaces'; -import useThread from '../useThread'; interface DynamicProps { currentChannel: GroupChannel | null; + resendMessageStart: (message: SendableMessageType) => void; + sendMessageSuccess: (message: SendableMessageType) => void; + sendMessageFailure: (message: SendableMessageType) => void; } interface StaticProps { logger: Logger; @@ -24,17 +26,13 @@ interface StaticProps { export default function useResendMessageCallback({ currentChannel, + resendMessageStart, + sendMessageSuccess, + sendMessageFailure, }: DynamicProps, { logger, pubSub, }: StaticProps): (failedMessage: SendableMessageType) => void { - const { - actions: { - resendMessageStart, - sendMessageSuccess, - sendMessageFailure, - }, - } = useThread(); return useCallback((failedMessage: SendableMessageType) => { if ((failedMessage as SendableMessageType)?.isResendable) { logger.info('Thread | useResendMessageCallback: Resending failedMessage start.', failedMessage); diff --git a/src/modules/Thread/context/hooks/useSendFileMessage.ts b/src/modules/Thread/context/hooks/useSendFileMessage.ts index a7fb40a3c..ddf65215e 100644 --- a/src/modules/Thread/context/hooks/useSendFileMessage.ts +++ b/src/modules/Thread/context/hooks/useSendFileMessage.ts @@ -8,11 +8,12 @@ import { scrollIntoLast } from '../utils'; import { SendableMessageType } from '../../../../utils'; import { PublishingModuleType } from './useSendMultipleFilesMessage'; import { SCROLL_BOTTOM_DELAY_FOR_SEND } from '../../../../utils/consts'; -import useThread from '../useThread'; interface DynamicProps { currentChannel: GroupChannel | null; onBeforeSendFileMessage?: (file: File, quotedMessage?: SendableMessageType) => FileMessageCreateParams; + sendMessageStart: (message: SendableMessageType) => void; + sendMessageFailure: (message: SendableMessageType) => void; } interface StaticProps { logger: Logger; @@ -29,17 +30,12 @@ export type SendFileMessageFunctionType = (file: File, quoteMessage?: SendableMe export default function useSendFileMessageCallback({ currentChannel, onBeforeSendFileMessage, + sendMessageStart, + sendMessageFailure, }: DynamicProps, { logger, pubSub, }: StaticProps): SendFileMessageFunctionType { - const { - actions: { - sendMessageStart, - sendMessageFailure, - }, - } = useThread(); - return useCallback((file, quoteMessage): Promise => { return new Promise((resolve, reject) => { const createParamsDefault = () => { diff --git a/src/modules/Thread/context/hooks/useSendUserMessageCallback.ts b/src/modules/Thread/context/hooks/useSendUserMessageCallback.ts index c44983e10..5ef9ff216 100644 --- a/src/modules/Thread/context/hooks/useSendUserMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useSendUserMessageCallback.ts @@ -7,13 +7,14 @@ import { Logger } from '../../../../lib/SendbirdState'; import topics, { SBUGlobalPubSub } from '../../../../lib/pubSub/topics'; import { SendableMessageType } from '../../../../utils'; import { PublishingModuleType } from '../../../internalInterfaces'; -import useThread from '../useThread'; export type OnBeforeSendUserMessageType = (message: string, quoteMessage?: SendableMessageType) => UserMessageCreateParams; interface DynamicProps { isMentionEnabled: boolean; currentChannel: GroupChannel | null; onBeforeSendUserMessage?: OnBeforeSendUserMessageType; + sendMessageStart: (message: SendableMessageType) => void; + sendMessageFailure: (message: SendableMessageType) => void; } interface StaticProps { logger: Logger; @@ -31,6 +32,8 @@ export default function useSendUserMessageCallback({ isMentionEnabled, currentChannel, onBeforeSendUserMessage, + sendMessageStart, + sendMessageFailure, }: DynamicProps, { logger, pubSub, @@ -43,13 +46,6 @@ export default function useSendUserMessageCallback({ mentionedUsers, } = props; - const { - actions: { - sendMessageStart, - sendMessageFailure, - }, - } = useThread(); - const createDefaultParams = () => { const params = {} as UserMessageCreateParams; params.message = message; diff --git a/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts b/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts index 7a3962718..78674b14e 100644 --- a/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts @@ -14,11 +14,12 @@ import { } from '../../../../utils/consts'; import { SendableMessageType } from '../../../../utils'; import { PublishingModuleType } from '../../../internalInterfaces'; -import useThread from '../useThread'; interface DynamicParams { currentChannel: GroupChannel | null; onBeforeSendVoiceMessage?: (file: File, quoteMessage?: SendableMessageType) => FileMessageCreateParams; + sendMessageStart: (message: SendableMessageType) => void; + sendMessageFailure: (message: SendableMessageType) => void; } interface StaticParams { logger: Logger; @@ -33,18 +34,13 @@ interface LocalFileMessage extends FileMessage { export const useSendVoiceMessageCallback = ({ currentChannel, onBeforeSendVoiceMessage, + sendMessageStart, + sendMessageFailure, }: DynamicParams, { logger, pubSub, }: StaticParams): FuncType => { - const { - actions: { - sendMessageStart, - sendMessageFailure, - }, - } = useThread(); - const sendMessage = useCallback((file: File, duration: number, quoteMessage: SendableMessageType) => { const messageParams: FileMessageCreateParams = ( onBeforeSendVoiceMessage diff --git a/src/modules/Thread/context/hooks/useThreadFetchers.ts b/src/modules/Thread/context/hooks/useThreadFetchers.ts index 36c1350ba..fa9e4d642 100644 --- a/src/modules/Thread/context/hooks/useThreadFetchers.ts +++ b/src/modules/Thread/context/hooks/useThreadFetchers.ts @@ -5,7 +5,6 @@ import { LoggerInterface } from '../../../../lib/Logger'; import { useCallback } from 'react'; import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext'; import { ThreadListStateTypes } from '../../types'; -import useThread from '../useThread'; type Params = { anchorMessage?: SendableMessageType; @@ -15,6 +14,15 @@ type Params = { 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 { @@ -34,20 +42,16 @@ export const useThreadFetchers = ({ oldestMessageTimeStamp, latestMessageTimeStamp, threadListState, + initializeThreadListStart, + initializeThreadListSuccess, + initializeThreadListFailure, + getPrevMessagesStart, + getPrevMessagesSuccess, + getPrevMessagesFailure, + getNextMessagesStart, + getNextMessagesSuccess, + getNextMessagesFailure, }: Params) => { - const { - actions: { - initializeThreadListStart, - initializeThreadListSuccess, - initializeThreadListFailure, - getPrevMessagesStart, - getPrevMessagesSuccess, - getPrevMessagesFailure, - getNextMessagesStart, - getNextMessagesSuccess, - getNextMessagesFailure, - }, - } = useThread(); const { stores } = useSendbirdStateContext(); const timestamp = anchorMessage?.createdAt || 0; diff --git a/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts b/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts index 85c2fab86..fb5a981e4 100644 --- a/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts @@ -7,11 +7,12 @@ import { Logger } from '../../../../lib/SendbirdState'; import topics, { SBUGlobalPubSub } from '../../../../lib/pubSub/topics'; import { PublishingModuleType } from '../../../internalInterfaces'; -import useThread from '../useThread'; +import { SendableMessageType } from '../../../../utils'; interface DynamicProps { currentChannel: GroupChannel | null; isMentionEnabled?: boolean; + onMessageUpdated: (currentChannel: GroupChannel, message: SendableMessageType) => void; } interface StaticProps { logger: Logger; @@ -28,6 +29,7 @@ type CallbackParams = { export default function useUpdateMessageCallback({ currentChannel, isMentionEnabled, + onMessageUpdated, }: DynamicProps, { logger, pubSub, @@ -41,12 +43,6 @@ export default function useUpdateMessageCallback({ mentionTemplate, } = props; - const { - actions: { - onMessageUpdated, - }, - } = useThread(); - const createParamsDefault = () => { const params = {} as UserMessageUpdateParams; params.message = message; diff --git a/src/modules/Thread/context/useThread.ts b/src/modules/Thread/context/useThread.ts index edca38318..7b43ccb7b 100644 --- a/src/modules/Thread/context/useThread.ts +++ b/src/modules/Thread/context/useThread.ts @@ -1,31 +1,30 @@ import { useSyncExternalStore } from 'use-sync-external-store/shim'; -import { useCallback, useContext, useMemo } from 'react'; +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, scrollIntoLast as scrollIntoLastForThread, scrollIntoLast } from './utils'; +import { compareIds } from './utils'; import { - BaseMessage, FileMessage, FileMessageCreateParams, MessageMetaArray, MessageType, - MultipleFilesMessage, type MultipleFilesMessageCreateParams, - ReactionEvent, SendingStatus, ThreadedMessageListParams, type UploadableFileInfo, + BaseMessage, + MultipleFilesMessage, + ReactionEvent, UserMessage, - UserMessageCreateParams, UserMessageUpdateParams, } 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 { SendMessageParams } from './hooks/useSendUserMessageCallback'; -import topics, { PUBSUB_TOPICS, PublishingModuleType } from '../../../lib/pubSub/topics'; -import { - META_ARRAY_MESSAGE_TYPE_KEY, META_ARRAY_MESSAGE_TYPE_VALUE__VOICE, - META_ARRAY_VOICE_DURATION_KEY, - SCROLL_BOTTOM_DELAY_FOR_SEND, - VOICE_MESSAGE_FILE_NAME, - VOICE_MESSAGE_MIME_TYPE, -} from '../../../utils/consts'; -import { shouldPubSubPublishToThread } from '../../internalInterfaces'; +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, @@ -33,26 +32,12 @@ function hasReqId( return 'reqId' in message; } -interface LocalFileMessage extends FileMessage { - localUrl: string; - file: File; -} - -function getThreadMessageListParams(params?: Partial): ThreadedMessageListParams { - return { - prevResultSize: PREV_THREADS_FETCH_SIZE, - nextResultSize: NEXT_THREADS_FETCH_SIZE, - includeMetaArray: true, - ...params, - }; -} - 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 { config } = useSendbirdStateContext(); const { logger, pubSub } = config; const isMentionEnabled = config.groupChannel.enableMention; const isReactionEnabled = config.groupChannel.enableReactions; @@ -70,531 +55,264 @@ const useThread = () => { 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: useCallback((props: SendMessageParams) => { - const { - message, - quoteMessage, - mentionTemplate, - mentionedUsers, - } = props; - - const createDefaultParams = () => { - const params = {} as UserMessageCreateParams; - params.message = message; - const mentionedUsersLength = mentionedUsers?.length || 0; - if (isMentionEnabled && mentionedUsersLength) { - params.mentionedUsers = mentionedUsers; - } - if (isMentionEnabled && mentionTemplate && mentionedUsersLength) { - params.mentionedMessageTemplate = mentionTemplate; - } - if (quoteMessage) { - params.isReplyToChannel = true; - params.parentMessageId = quoteMessage.messageId; - } - return params; - }; + sendMessage: useSendUserMessageCallback({ + isMentionEnabled, + currentChannel, + onBeforeSendUserMessage, + sendMessageStart: sendMessageStatusActions.sendMessageStart, + sendMessageFailure: sendMessageStatusActions.sendMessageFailure, + }, { + logger, + pubSub, + }), - const params = onBeforeSendUserMessage?.(message, quoteMessage) ?? createDefaultParams(); - logger.info('Thread | useSendUserMessageCallback: Sending user message start.', params); - - if (currentChannel?.sendUserMessage) { - currentChannel?.sendUserMessage(params) - .onPending((pendingMessage) => { - actions.sendMessageStart(pendingMessage as SendableMessageType); - }) - .onFailed((error, message) => { - logger.info('Thread | useSendUserMessageCallback: Sending user message failed.', { message, error }); - actions.sendMessageFailure(message as SendableMessageType); - }) - .onSucceeded((message) => { - logger.info('Thread | useSendUserMessageCallback: Sending user message succeeded.', message); - actions.sendMessageSuccess(message as SendableMessageType); - // because Thread doesn't subscribe SEND_USER_MESSAGE - pubSub.publish(topics.SEND_USER_MESSAGE, { - channel: currentChannel, - message: message as UserMessage, - publishingModules: [PublishingModuleType.THREAD], - }); - }); - } - }, [isMentionEnabled, currentChannel]), - - sendFileMessage: useCallback((file, quoteMessage): Promise => { - return new Promise((resolve, reject) => { - const createParamsDefault = () => { - const params = {} as FileMessageCreateParams; - params.file = file; - if (quoteMessage) { - params.isReplyToChannel = true; - params.parentMessageId = quoteMessage.messageId; - } - return params; - }; - const params = onBeforeSendFileMessage?.(file, quoteMessage) ?? createParamsDefault(); - logger.info('Thread | useSendFileMessageCallback: Sending file message start.', params); - - currentChannel?.sendFileMessage(params) - .onPending((pendingMessage) => { - actions.sendMessageStart({ - ...pendingMessage, - url: URL.createObjectURL(file), - // pending thumbnail message seems to be failed - // @ts-ignore - requestState: 'pending', - isUserMessage: pendingMessage.isUserMessage, - isFileMessage: pendingMessage.isFileMessage, - isAdminMessage: pendingMessage.isAdminMessage, - isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, - }); - setTimeout(() => scrollIntoLast(), SCROLL_BOTTOM_DELAY_FOR_SEND); - }) - .onFailed((error, message) => { - (message as LocalFileMessage).localUrl = URL.createObjectURL(file); - (message as LocalFileMessage).file = file; - logger.info('Thread | useSendFileMessageCallback: Sending file message failed.', { message, error }); - actions.sendMessageFailure(message as SendableMessageType); - reject(error); - }) - .onSucceeded((message) => { - logger.info('Thread | useSendFileMessageCallback: Sending file message succeeded.', message); - pubSub.publish(topics.SEND_FILE_MESSAGE, { - channel: currentChannel, - message: message as FileMessage, - publishingModules: [PublishingModuleType.THREAD], - }); - resolve(message as FileMessage); - }); - }); - }, [currentChannel]), - - sendVoiceMessage: useCallback((file: File, duration: number, quoteMessage: SendableMessageType) => { - const messageParams: FileMessageCreateParams = ( - onBeforeSendVoiceMessage - && typeof onBeforeSendVoiceMessage === 'function' - ) - ? onBeforeSendVoiceMessage(file, quoteMessage) - : { - file, - fileName: VOICE_MESSAGE_FILE_NAME, - mimeType: VOICE_MESSAGE_MIME_TYPE, - metaArrays: [ - new MessageMetaArray({ - key: META_ARRAY_VOICE_DURATION_KEY, - value: [`${duration}`], - }), - new MessageMetaArray({ - key: META_ARRAY_MESSAGE_TYPE_KEY, - value: [META_ARRAY_MESSAGE_TYPE_VALUE__VOICE], - }), - ], - }; - if (quoteMessage) { - messageParams.isReplyToChannel = true; - messageParams.parentMessageId = quoteMessage.messageId; - } - logger.info('Thread | useSendVoiceMessageCallback: Start sending voice message', messageParams); - currentChannel?.sendFileMessage(messageParams) - .onPending((pendingMessage) => { - actions.sendMessageStart({ - /* pubSub is used instead of messagesDispatcher - to avoid redundantly calling `messageActionTypes.SEND_MESSAGE_START` */ - // TODO: remove data pollution - ...pendingMessage, - url: URL.createObjectURL(file), - // pending thumbnail message seems to be failed - // @ts-ignore - requestState: 'pending', - isUserMessage: pendingMessage.isUserMessage, - isFileMessage: pendingMessage.isFileMessage, - isAdminMessage: pendingMessage.isAdminMessage, - isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, - }); - setTimeout(() => scrollIntoLast(), SCROLL_BOTTOM_DELAY_FOR_SEND); - }) - .onFailed((error, message) => { - (message as LocalFileMessage).localUrl = URL.createObjectURL(file); - (message as LocalFileMessage).file = file; - logger.info('Thread | useSendVoiceMessageCallback: Sending voice message failed.', { message, error }); - actions.sendMessageFailure(message as SendableMessageType); - }) - .onSucceeded((message) => { - logger.info('Thread | useSendVoiceMessageCallback: Sending voice message succeeded.', message); - pubSub.publish(topics.SEND_FILE_MESSAGE, { - channel: currentChannel, - message: message as FileMessage, - publishingModules: [PublishingModuleType.THREAD], - }); - }); - }, [ + sendFileMessage: useSendFileMessageCallback({ + currentChannel, + onBeforeSendFileMessage, + sendMessageStart: sendMessageStatusActions.sendMessageStart, + sendMessageFailure: sendMessageStatusActions.sendMessageFailure, + }, { + logger, + pubSub, + }), + + sendVoiceMessage: useSendVoiceMessageCallback({ currentChannel, onBeforeSendVoiceMessage, - ]), - - sendMultipleFilesMessage: useCallback(( - files: Array, - quoteMessage?: SendableMessageType, - ): Promise => { - return new Promise((resolve, reject) => { - if (!currentChannel) { - logger.warning('Channel: Sending MFm failed, because currentChannel is null.', { currentChannel }); - reject(); - } - if (files.length <= 1) { - logger.warning('Channel: Sending MFM failed, because there are no multiple files.', { files }); - reject(); - } - let messageParams: MultipleFilesMessageCreateParams = { - fileInfoList: files.map((file: File): UploadableFileInfo => ({ - file, - fileName: file.name, - fileSize: file.size, - mimeType: file.type, - })), - }; - if (quoteMessage) { - messageParams.isReplyToChannel = true; - messageParams.parentMessageId = quoteMessage.messageId; - } - if (typeof onBeforeSendMultipleFilesMessage === 'function') { - messageParams = onBeforeSendMultipleFilesMessage(files, quoteMessage); - } - logger.info('Channel: Start sending MFM', { messageParams }); - try { - currentChannel?.sendMultipleFilesMessage(messageParams) - .onFileUploaded((requestId, index, uploadableFileInfo: UploadableFileInfo, error) => { - logger.info('Channel: onFileUploaded during sending MFM', { - requestId, - index, - error, - uploadableFileInfo, - }); - pubSub.publish(PUBSUB_TOPICS.ON_FILE_INFO_UPLOADED, { - response: { - channelUrl: currentChannel.url, - requestId, - index, - uploadableFileInfo, - error, - }, - publishingModules: [PublishingModuleType.THREAD], - }); - }) - .onPending((pendingMessage: MultipleFilesMessage) => { - logger.info('Channel: in progress of sending MFM', { pendingMessage, fileInfoList: messageParams.fileInfoList }); - pubSub.publish(PUBSUB_TOPICS.SEND_MESSAGE_START, { - message: pendingMessage, - channel: currentChannel, - publishingModules: [PublishingModuleType.THREAD], - }); - setTimeout(() => { - if (shouldPubSubPublishToThread([PublishingModuleType.THREAD])) { - scrollIntoLastForThread(0); - } - }, SCROLL_BOTTOM_DELAY_FOR_SEND); - }) - .onFailed((error, failedMessage: MultipleFilesMessage) => { - logger.error('Channel: Sending MFM failed.', { error, failedMessage }); - pubSub.publish(PUBSUB_TOPICS.SEND_MESSAGE_FAILED, { - channel: currentChannel, - message: failedMessage, - publishingModules: [PublishingModuleType.THREAD], - error, - }); - reject(error); - }) - .onSucceeded((succeededMessage: MultipleFilesMessage) => { - logger.info('Channel: Sending voice message success!', { succeededMessage }); - pubSub.publish(PUBSUB_TOPICS.SEND_FILE_MESSAGE, { - channel: currentChannel, - message: succeededMessage, - publishingModules: [PublishingModuleType.THREAD], - }); - resolve(succeededMessage); - }); - } catch (error) { - logger.error('Channel: Sending MFM failed.', { error }); - reject(error); - } - }); - }, [ + sendMessageStart: sendMessageStatusActions.sendMessageStart, + sendMessageFailure: sendMessageStatusActions.sendMessageFailure, + }, { + logger, + pubSub, + }), + + sendMultipleFilesMessage: useSendMultipleFilesMessage({ currentChannel, onBeforeSendMultipleFilesMessage, - ]), - - resendMessage: useCallback((failedMessage: SendableMessageType) => { - if ((failedMessage as SendableMessageType)?.isResendable) { - logger.info('Thread | useResendMessageCallback: Resending failedMessage start.', failedMessage); - if (failedMessage?.isUserMessage?.() || failedMessage?.messageType === MessageType.USER) { - try { - currentChannel?.resendMessage(failedMessage as UserMessage) - .onPending((message) => { - logger.info('Thread | useResendMessageCallback: Resending user message started.', message); - actions.resendMessageStart(message); - }) - .onSucceeded((message) => { - logger.info('Thread | useResendMessageCallback: Resending user message succeeded.', message); - actions.sendMessageSuccess(message); - pubSub.publish(topics.SEND_USER_MESSAGE, { - channel: currentChannel, - message: message, - publishingModules: [PublishingModuleType.THREAD], - }); - }) - .onFailed((error) => { - logger.warning('Thread | useResendMessageCallback: Resending user message failed.', error); - failedMessage.sendingStatus = SendingStatus.FAILED; - actions.sendMessageFailure(failedMessage); - }); - } catch (err) { - logger.warning('Thread | useResendMessageCallback: Resending user message failed.', err); - failedMessage.sendingStatus = SendingStatus.FAILED; - actions.sendMessageFailure(failedMessage); - } - } else if (failedMessage?.isFileMessage?.()) { - try { - currentChannel?.resendMessage?.(failedMessage as FileMessage) - .onPending((message) => { - logger.info('Thread | useResendMessageCallback: Resending file message started.', message); - actions.resendMessageStart(message); - }) - .onSucceeded((message) => { - logger.info('Thread | useResendMessageCallback: Resending file message succeeded.', message); - actions.sendMessageSuccess(message); - pubSub.publish(topics.SEND_FILE_MESSAGE, { - channel: currentChannel, - message: failedMessage, - publishingModules: [PublishingModuleType.THREAD], - }); - }) - .onFailed((error) => { - logger.warning('Thread | useResendMessageCallback: Resending file message failed.', error); - failedMessage.sendingStatus = SendingStatus.FAILED; - actions.sendMessageFailure(failedMessage); - }); - } catch (err) { - logger.warning('Thread | useResendMessageCallback: Resending file message failed.', err); - failedMessage.sendingStatus = SendingStatus.FAILED; - actions.sendMessageFailure(failedMessage); - } - } else if (failedMessage?.isMultipleFilesMessage?.()) { - try { - currentChannel?.resendMessage?.(failedMessage as MultipleFilesMessage) - .onPending((message) => { - logger.info('Thread | useResendMessageCallback: Resending multiple files message started.', message); - actions.resendMessageStart(message); - }) - .onFileUploaded((requestId, index, uploadableFileInfo: UploadableFileInfo, error) => { - logger.info('Thread | useResendMessageCallback: onFileUploaded during resending multiple files message.', { - requestId, - index, - error, - uploadableFileInfo, - }); - pubSub.publish(topics.ON_FILE_INFO_UPLOADED, { - response: { - channelUrl: currentChannel.url, - requestId, - index, - uploadableFileInfo, - error, - }, - publishingModules: [PublishingModuleType.THREAD], - }); - }) - .onSucceeded((message: MultipleFilesMessage) => { - logger.info('Thread | useResendMessageCallback: Resending MFM succeeded.', message); - actions.sendMessageSuccess(message); - pubSub.publish(topics.SEND_FILE_MESSAGE, { - channel: currentChannel, - message, - publishingModules: [PublishingModuleType.THREAD], - }); - }) - .onFailed((error, message) => { - logger.warning('Thread | useResendMessageCallback: Resending MFM failed.', error); - actions.sendMessageFailure(message); - }); - } catch (err) { - logger.warning('Thread | useResendMessageCallback: Resending MFM failed.', err); - actions.sendMessageFailure(failedMessage); - } - } else { - logger.warning('Thread | useResendMessageCallback: Message is not resendable.', failedMessage); - failedMessage.sendingStatus = SendingStatus.FAILED; - actions.sendMessageFailure(failedMessage); - } - } - }, [currentChannel]), + publishingModules: [PublishingModuleType.THREAD], + }, { + logger, + pubSub, + }), + + resendMessage: useResendMessageCallback({ + resendMessageStart: sendMessageStatusActions.resendMessageStart, + sendMessageSuccess: sendMessageStatusActions.sendMessageSuccess, + sendMessageFailure: sendMessageStatusActions.sendMessageFailure, + currentChannel, + }, { logger, pubSub }), }; - const anchorMessage = message?.messageId !== parentMessage?.messageId ? message || undefined : undefined; - const timestamp = anchorMessage?.createdAt || 0; - const oldestMessageTimeStamp = allThreadMessages[0]?.createdAt || 0; - const latestMessageTimeStamp = allThreadMessages[allThreadMessages.length - 1]?.createdAt || 0; - - const threadFetcherActions = { - initializeThreadFetcher: useCallback( - async (callback?: (messages: BaseMessage[]) => void) => { - const staleParentMessage = parentMessage; - - if (!stores.sdkStore.initialized || !staleParentMessage) return; - - actions.initializeThreadListStart(); - - try { - const params = getThreadMessageListParams({ includeReactions: isReactionEnabled }); - logger.info('Thread | useGetThreadList: Initialize thread list start.', { timestamp, params }); - - const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(timestamp, params); - logger.info('Thread | useGetThreadList: Initialize thread list succeeded.', { staleParentMessage, threadedMessages }); - actions.initializeThreadListSuccess(parentMessage, anchorMessage, threadedMessages); - setTimeout(() => callback?.(threadedMessages)); - } catch (error) { - logger.info('Thread | useGetThreadList: Initialize thread list failed.', error); - actions.initializeThreadListFailure(); - } - }, - [stores.sdkStore.initialized, parentMessage, anchorMessage, isReactionEnabled], - ), - - fetchPrevThreads: useCallback( - async (callback?: (messages: BaseMessage[]) => void) => { - const staleParentMessage = parentMessage; - - if (threadListState !== ThreadListStateTypes.INITIALIZED || oldestMessageTimeStamp === 0 || !staleParentMessage) return; - - actions.getPrevMessagesStart(); - - try { - const params = getThreadMessageListParams({ nextResultSize: 0, includeReactions: isReactionEnabled }); - - const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(oldestMessageTimeStamp, params); - - logger.info('Thread | useGetPrevThreadsCallback: Fetch prev threads succeeded.', { parentMessage, threadedMessages }); - actions.getPrevMessagesSuccess(threadedMessages as CoreMessageType[]); - setTimeout(() => callback?.(threadedMessages)); - } catch (error) { - logger.info('Thread | useGetPrevThreadsCallback: Fetch prev threads failed.', error); - actions.getPrevMessagesFailure(); - } - }, - [threadListState, oldestMessageTimeStamp, isReactionEnabled, parentMessage], - ), - - fetchNextThreads: useCallback( - async (callback?: (messages: BaseMessage[]) => void) => { - const staleParentMessage = parentMessage; - - if (threadListState !== ThreadListStateTypes.INITIALIZED || latestMessageTimeStamp === 0 || !staleParentMessage) return; - - actions.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 }); - actions.getNextMessagesSuccess(threadedMessages as CoreMessageType[]); - setTimeout(() => callback?.(threadedMessages)); - } catch (error) { - logger.info('Thread | useGetNextThreadsCallback: Fetch next threads failed.', error); - actions.getNextMessagesFailure(); - } - }, - [threadListState, latestMessageTimeStamp, isReactionEnabled, parentMessage], - ), + 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: useCallback((props: { - messageId: number; - message: string; - mentionedUsers?: User[]; - mentionTemplate?: string; - }) => { - const { - messageId, - message, - mentionedUsers, - mentionTemplate, - } = props; - - const createParamsDefault = () => { - const params = {} as UserMessageUpdateParams; - params.message = message; - if (isMentionEnabled && mentionedUsers && mentionedUsers?.length > 0) { - params.mentionedUsers = mentionedUsers; - } - if (isMentionEnabled && mentionTemplate) { - params.mentionedMessageTemplate = mentionTemplate; - } else { - params.mentionedMessageTemplate = message; - } - return params; + 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, + }; + }), - const params = createParamsDefault(); - logger.info('Thread | useUpdateMessageCallback: Message update start.', params); - - currentChannel?.updateUserMessage?.(messageId, params) - .then((message: UserMessage) => { - logger.info('Thread | useUpdateMessageCallback: Message update succeeded.', message); - actions.onMessageUpdated(currentChannel, message); - pubSub.publish( - topics.UPDATE_USER_MESSAGE, - { - fromSelector: true, - channel: currentChannel, - message: message, - publishingModules: [PublishingModuleType.THREAD], - }, - ); - }); - }, [currentChannel, isMentionEnabled]), - - deleteMessage: useCallback((message: SendableMessageType): Promise => { - logger.info('Thread | useDeleteMessageCallback: Deleting message.', message); - const { sendingStatus } = message; - return new Promise((resolve, reject) => { - logger.info('Thread | useDeleteMessageCallback: Deleting message requestState:', sendingStatus); - // Message is only on local - if (sendingStatus === 'failed' || sendingStatus === 'pending') { - logger.info('Thread | useDeleteMessageCallback: Deleted message from local:', message); - actions.onMessageDeletedByReqId(message.reqId); - resolve(); - } - - logger.info('Thread | useDeleteMessageCallback: Deleting message from remote:', sendingStatus); - currentChannel?.deleteMessage?.(message) - .then(() => { - logger.info('Thread | useDeleteMessageCallback: Deleting message success!', message); - actions.onMessageDeleted(currentChannel, message.messageId); - resolve(); - }) - .catch((err) => { - logger.warning('Thread | useDeleteMessageCallback: Deleting message failed!', err); - reject(err); - }); - }); - }, [currentChannel]), + 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 getChannelSuccess = (groupChannel: GroupChannel) => { - // console.log('getChannelSuccess'); - // console.log(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, - // })); - // }; + 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 => ({ @@ -673,54 +391,6 @@ const useThread = () => { }; }), - 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) - )), - }; - }), - onReactionUpdated: (reactionEvent: ReactionEvent) => store.setState(state => { if (state?.parentMessage?.messageId === reactionEvent?.messageId) { state.parentMessage?.applyReactionEvent?.(reactionEvent); @@ -825,53 +495,6 @@ const useThread = () => { }; }), - 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 - )), - }; - }), - onFileInfoUpdated: ({ channelUrl, requestId, @@ -901,83 +524,15 @@ const useThread = () => { }; }), - 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, - }; - }), - toggleReaction, + ...sendMessageStatusActions, ...sendMessageActions, + ...messageModifiedActions, ...modifyMessageActions, - ...threadFetcherActions, + ...threadFetcherStatusActions, + initializeThreadFetcher, + fetchPrevThreads, + fetchNextThreads, }), [store, currentChannel]); return { state, actions }; From 050d886cde5baa8be7dbffe7638db5c7a66f2b01 Mon Sep 17 00:00:00 2001 From: "junyoung.lim" Date: Thu, 28 Nov 2024 15:54:59 +0900 Subject: [PATCH 06/10] Fix --- src/modules/Thread/context/hooks/useSendFileMessage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/Thread/context/hooks/useSendFileMessage.ts b/src/modules/Thread/context/hooks/useSendFileMessage.ts index ddf65215e..e10b313c6 100644 --- a/src/modules/Thread/context/hooks/useSendFileMessage.ts +++ b/src/modules/Thread/context/hooks/useSendFileMessage.ts @@ -1,6 +1,6 @@ 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 { Logger } from '../../../../lib/SendbirdState'; import topics, { SBUGlobalPubSub } from '../../../../lib/pubSub/topics'; @@ -58,6 +58,7 @@ export default function useSendFileMessageCallback({ // pending thumbnail message seems to be failed // @ts-ignore requestState: 'pending', + sendingStatus: SendingStatus.PENDING, isUserMessage: pendingMessage.isUserMessage, isFileMessage: pendingMessage.isFileMessage, isAdminMessage: pendingMessage.isAdminMessage, From bba9a908cc5bb7c5269acb0eb84ebc4cae720465 Mon Sep 17 00:00:00 2001 From: "junyoung.lim" Date: Thu, 28 Nov 2024 16:09:07 +0900 Subject: [PATCH 07/10] Fix manual test bugs --- .../context/hooks/useFileUploadCallback.tsx | 3 +- .../ThreadList/ThreadListItemContent.tsx | 2 +- .../context/__test__/ThreadProvider.spec.tsx | 60 +++++++++++++++++++ .../hooks/useSendVoiceMessageCallback.ts | 3 +- src/modules/Thread/context/useThread.ts | 9 ++- 5 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/modules/OpenChannel/context/hooks/useFileUploadCallback.tsx b/src/modules/OpenChannel/context/hooks/useFileUploadCallback.tsx index e29024ac2..b4c795171 100644 --- a/src/modules/OpenChannel/context/hooks/useFileUploadCallback.tsx +++ b/src/modules/OpenChannel/context/hooks/useFileUploadCallback.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import type { OpenChannel } from '@sendbird/chat/openChannel'; -import type { FileMessageCreateParams } from '@sendbird/chat/message'; +import { FileMessageCreateParams, SendingStatus } from '@sendbird/chat/message'; import type { Logger } from '../../../../lib/SendbirdState'; import type { ImageCompressionOptions } from '../../../../lib/Sendbird'; @@ -107,6 +107,7 @@ function useFileUploadCallback({ url: URL.createObjectURL(file), // pending thumbnail message seems to be failed requestState: 'pending', + sendingStatus: SendingStatus.PENDING, isUserMessage: pendingMessage.isUserMessage, isFileMessage: pendingMessage.isFileMessage, isAdminMessage: pendingMessage.isAdminMessage, diff --git a/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx b/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx index a41780292..7e05bb74c 100644 --- a/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx +++ b/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx @@ -128,7 +128,7 @@ export default function ThreadListItemContent(props: ThreadListItemContentProps) ); const supposedHoverClassName = isMenuMounted ? 'sendbird-mouse-hover' : ''; const isReactionEnabledInChannel = isReactionEnabled && !channel?.isEphemeral; - const isOgMessageEnabledInGroupChannel = channel.isGroupChannel() && config.groupChannel.enableOgtag; + const isOgMessageEnabledInGroupChannel = channel?.isGroupChannel() && config.groupChannel.enableOgtag; // Mobile const mobileMenuRef = useRef(null); diff --git a/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx b/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx index 005801b78..28a7c7cd9 100644 --- a/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx +++ b/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx @@ -64,6 +64,66 @@ describe('ThreadProvider', () => { }); + 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 }) => ( {}}> diff --git a/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts b/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts index 78674b14e..bdc5b3ba4 100644 --- a/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { GroupChannel } from '@sendbird/chat/groupChannel'; -import { FileMessage, FileMessageCreateParams, MessageMetaArray } from '@sendbird/chat/message'; +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'; @@ -78,6 +78,7 @@ export const useSendVoiceMessageCallback = ({ // pending thumbnail message seems to be failed // @ts-ignore requestState: 'pending', + sendingStatus: SendingStatus.PENDING, isUserMessage: pendingMessage.isUserMessage, isFileMessage: pendingMessage.isFileMessage, isAdminMessage: pendingMessage.isAdminMessage, diff --git a/src/modules/Thread/context/useThread.ts b/src/modules/Thread/context/useThread.ts index 7b43ccb7b..e52c5caf3 100644 --- a/src/modules/Thread/context/useThread.ts +++ b/src/modules/Thread/context/useThread.ts @@ -37,7 +37,7 @@ const useThread = () => { if (!store) throw new Error('useCreateChannel must be used within a CreateChannelProvider'); // SendbirdStateContext config - const { config } = useSendbirdStateContext(); + const { stores, config } = useSendbirdStateContext(); const { logger, pubSub } = config; const isMentionEnabled = config.groupChannel.enableMention; const isReactionEnabled = config.groupChannel.enableReactions; @@ -533,7 +533,12 @@ const useThread = () => { initializeThreadFetcher, fetchPrevThreads, fetchNextThreads, - }), [store, currentChannel]); + }), [ + store, + currentChannel, + stores.sdkStore.initialized, + parentMessage, + ]); return { state, actions }; }; From 3c10a0566d313e0096e6cea8b17574c670bd2365 Mon Sep 17 00:00:00 2001 From: "junyoung.lim" Date: Thu, 28 Nov 2024 16:38:59 +0900 Subject: [PATCH 08/10] Apply feedback and fix build error --- .../context/hooks/useFileUploadCallback.tsx | 3 +- .../context/__test__/ThreadProvider.spec.tsx | 67 +++++++++++++++++++ .../context/hooks/useSendFileMessage.ts | 3 +- .../hooks/useSendVoiceMessageCallback.ts | 3 +- src/modules/Thread/context/useThread.ts | 2 +- 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/modules/OpenChannel/context/hooks/useFileUploadCallback.tsx b/src/modules/OpenChannel/context/hooks/useFileUploadCallback.tsx index b4c795171..e29024ac2 100644 --- a/src/modules/OpenChannel/context/hooks/useFileUploadCallback.tsx +++ b/src/modules/OpenChannel/context/hooks/useFileUploadCallback.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import type { OpenChannel } from '@sendbird/chat/openChannel'; -import { FileMessageCreateParams, SendingStatus } from '@sendbird/chat/message'; +import type { FileMessageCreateParams } from '@sendbird/chat/message'; import type { Logger } from '../../../../lib/SendbirdState'; import type { ImageCompressionOptions } from '../../../../lib/Sendbird'; @@ -107,7 +107,6 @@ function useFileUploadCallback({ url: URL.createObjectURL(file), // pending thumbnail message seems to be failed requestState: 'pending', - sendingStatus: SendingStatus.PENDING, isUserMessage: pendingMessage.isUserMessage, isFileMessage: pendingMessage.isFileMessage, isAdminMessage: pendingMessage.isAdminMessage, diff --git a/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx b/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx index 28a7c7cd9..01b5e866c 100644 --- a/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx +++ b/src/modules/Thread/context/__test__/ThreadProvider.spec.tsx @@ -145,4 +145,71 @@ describe('ThreadProvider', () => { }); }); }); + + // 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/hooks/useSendFileMessage.ts b/src/modules/Thread/context/hooks/useSendFileMessage.ts index e10b313c6..cf7babd54 100644 --- a/src/modules/Thread/context/hooks/useSendFileMessage.ts +++ b/src/modules/Thread/context/hooks/useSendFileMessage.ts @@ -52,12 +52,11 @@ export default function useSendFileMessageCallback({ currentChannel?.sendFileMessage(params) .onPending((pendingMessage) => { + // @ts-ignore sendMessageStart({ ...pendingMessage, url: URL.createObjectURL(file), // pending thumbnail message seems to be failed - // @ts-ignore - requestState: 'pending', sendingStatus: SendingStatus.PENDING, isUserMessage: pendingMessage.isUserMessage, isFileMessage: pendingMessage.isFileMessage, diff --git a/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts b/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts index bdc5b3ba4..c4632826f 100644 --- a/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts +++ b/src/modules/Thread/context/hooks/useSendVoiceMessageCallback.ts @@ -69,6 +69,7 @@ export const useSendVoiceMessageCallback = ({ logger.info('Thread | useSendVoiceMessageCallback: Start sending voice message', messageParams); currentChannel?.sendFileMessage(messageParams) .onPending((pendingMessage) => { + // @ts-ignore sendMessageStart({ /* pubSub is used instead of messagesDispatcher to avoid redundantly calling `messageActionTypes.SEND_MESSAGE_START` */ @@ -76,8 +77,6 @@ export const useSendVoiceMessageCallback = ({ ...pendingMessage, url: URL.createObjectURL(file), // pending thumbnail message seems to be failed - // @ts-ignore - requestState: 'pending', sendingStatus: SendingStatus.PENDING, isUserMessage: pendingMessage.isUserMessage, isFileMessage: pendingMessage.isFileMessage, diff --git a/src/modules/Thread/context/useThread.ts b/src/modules/Thread/context/useThread.ts index e52c5caf3..58e3f352c 100644 --- a/src/modules/Thread/context/useThread.ts +++ b/src/modules/Thread/context/useThread.ts @@ -145,7 +145,7 @@ const useThread = () => { }, { logger, pubSub, - }), + })[0], resendMessage: useResendMessageCallback({ resendMessageStart: sendMessageStatusActions.resendMessageStart, From 83ac4eaea9d5c86a85ed89253af0b99fb2fc9ed9 Mon Sep 17 00:00:00 2001 From: "junyoung.lim" Date: Thu, 28 Nov 2024 16:49:53 +0900 Subject: [PATCH 09/10] Remove dux entirely --- src/modules/Thread/context/ThreadProvider.tsx | 25 +- src/modules/Thread/context/dux/actionTypes.ts | 48 --- .../Thread/context/dux/initialState.ts | 44 -- src/modules/Thread/context/dux/reducer.ts | 403 ------------------ 4 files changed, 1 insertion(+), 519 deletions(-) delete mode 100644 src/modules/Thread/context/dux/actionTypes.ts delete mode 100644 src/modules/Thread/context/dux/initialState.ts delete mode 100644 src/modules/Thread/context/dux/reducer.ts diff --git a/src/modules/Thread/context/ThreadProvider.tsx b/src/modules/Thread/context/ThreadProvider.tsx index 029ad3bf5..a599a5ea6 100644 --- a/src/modules/Thread/context/ThreadProvider.tsx +++ b/src/modules/Thread/context/ThreadProvider.tsx @@ -2,8 +2,7 @@ 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'; @@ -12,18 +11,12 @@ import { getNicknamesMapFromMembers, getParentMessageFrom } from './utils'; import { UserProfileProvider, UserProfileProviderProps } from '../../../lib/UserProfileContext'; import useSendbirdStateContext from '../../../hooks/useSendbirdStateContext'; -import { 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 useUpdateMessageCallback from './hooks/useUpdateMessageCallback'; -import useToggleReactionCallback from './hooks/useToggleReactionsCallback'; -import { SendMessageParams } from './hooks/useSendUserMessageCallback'; -import useSendVoiceMessageCallback from './hooks/useSendVoiceMessageCallback'; import { CoreMessageType, SendableMessageType } from '../../../utils'; import { createStore } from '../../../utils/storeManager'; import { ChannelStateTypes, ParentMessageStateTypes, ThreadListStateTypes } from '../types'; @@ -47,22 +40,6 @@ export interface ThreadProviderProps extends filterEmojiCategoryIds?: (message: SendableMessageType) => EmojiCategory['id'][]; } -// actions -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; - nicknamesMap: Map; -} - export interface ThreadState { channelUrl: string; message: SendableMessageType | null; 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; -} From 63bc58a2e1b9466b8924b8378798fdcddfcab13d Mon Sep 17 00:00:00 2001 From: "junyoung.lim" Date: Thu, 28 Nov 2024 17:52:35 +0900 Subject: [PATCH 10/10] Fix initial state --- src/modules/Thread/context/ThreadProvider.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/modules/Thread/context/ThreadProvider.tsx b/src/modules/Thread/context/ThreadProvider.tsx index a599a5ea6..bb4da9a93 100644 --- a/src/modules/Thread/context/ThreadProvider.tsx +++ b/src/modules/Thread/context/ThreadProvider.tsx @@ -72,15 +72,15 @@ export interface ThreadState { const initialState = { channelUrl: '', message: null, - onHeaderActionClick: null, - onMoveToParentMessage: null, - onBeforeSendUserMessage: null, - onBeforeSendFileMessage: null, - onBeforeSendVoiceMessage: null, - onBeforeSendMultipleFilesMessage: null, - onBeforeDownloadFileMessage: null, - isMultipleFilesMessageEnabled: null, - filterEmojiCategoryIds: null, + onHeaderActionClick: undefined, + onMoveToParentMessage: undefined, + onBeforeSendUserMessage: undefined, + onBeforeSendFileMessage: undefined, + onBeforeSendVoiceMessage: undefined, + onBeforeSendMultipleFilesMessage: undefined, + onBeforeDownloadFileMessage: undefined, + isMultipleFilesMessageEnabled: undefined, + filterEmojiCategoryIds: undefined, currentChannel: null, allThreadMessages: [], localThreadMessages: [], @@ -136,8 +136,7 @@ export const ThreadManager: React.FC