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..353eb273c 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,185 +172,95 @@ export const ThreadProvider = (props: ThreadProviderProps) => { // // config const { logger, pubSub } = config; - const isMentionEnabled = config.groupChannel.enableMention; - const isReactionEnabled = config.groupChannel.enableReactions; - - // dux of Thread - const [threadStore, threadDispatcher] = useReducer( - threadReducer, - threadInitialState, - ) as [ThreadContextInitialState, CustomUseReducerDispatcher]; - const { - currentChannel, - allThreadMessages, - localThreadMessages, - parentMessage, - channelState, - threadListState, - parentMessageState, - hasMorePrev, - hasMoreNext, - emojiContainer, - isMuted, - isChannelFrozen, - currentUserId, - typingMembers, - }: ThreadContextInitialState = threadStore; - // Initialization - useEffect(() => { - threadDispatcher({ - type: ThreadContextActionTypes.INIT_USER_ID, - payload: user?.userId, - }); - }, [user]); + useSetCurrentUserId({ user }); useGetChannel({ channelUrl, sdkInit, message: propsMessage, - }, { sdk, logger, threadDispatcher }); + }, { sdk, logger }); useGetParentMessage({ channelUrl, sdkInit, parentMessage: propsParentMessage, - }, { sdk, logger, threadDispatcher }); - useGetAllEmoji({ sdk }, { logger, threadDispatcher }); + }, { sdk, logger }); + useGetAllEmoji({ sdk }, { logger }); // Handle channel events useHandleChannelEvents({ sdk, currentChannel, - }, { logger, threadDispatcher }); + }, { logger }); useHandleThreadPubsubEvents({ sdkInit, currentChannel, parentMessage, - }, { logger, pubSub, threadDispatcher }); - - const { initialize, loadPrevious, loadNext } = useThreadFetchers({ - parentMessage, - // anchorMessage should be null when parentMessage doesn't exist - anchorMessage: propsMessage?.messageId !== propsParentMessage?.messageId ? propsMessage || undefined : undefined, - logger, - isReactionEnabled, - threadDispatcher, - threadListState, - oldestMessageTimeStamp: allThreadMessages[0]?.createdAt || 0, - latestMessageTimeStamp: allThreadMessages[allThreadMessages.length - 1]?.createdAt || 0, - }); + }, { logger, pubSub }); useEffect(() => { if (stores.sdkStore.initialized && config.isOnline) { - initialize(); + initializeThreadFetcher(); } - }, [stores.sdkStore.initialized, config.isOnline, initialize]); + }, [stores.sdkStore.initialized, config.isOnline, initializeThreadFetcher]); - const toggleReaction = useToggleReactionCallback({ currentChannel }, { logger }); + // memo + const nicknamesMap: Map = useMemo(() => ( + (config.groupChannel.replyType !== 'none' && currentChannel) + ? getNicknamesMapFromMembers(currentChannel?.members) + : new Map() + ), [currentChannel?.members]); - // Send Message Hooks - const sendMessage = useSendUserMessageCallback({ - isMentionEnabled, - currentChannel, + useEffect(() => { + updateState({ + channelUrl, + message, + onHeaderActionClick, + onMoveToParentMessage, + onBeforeSendUserMessage, + onBeforeSendFileMessage, + onBeforeSendVoiceMessage, + onBeforeSendMultipleFilesMessage, + onBeforeDownloadFileMessage, + isMultipleFilesMessageEnabled, + filterEmojiCategoryIds, + nicknamesMap, + }); + }, [ + channelUrl, + message, + onHeaderActionClick, + onMoveToParentMessage, onBeforeSendUserMessage, - }, { - logger, - pubSub, - threadDispatcher, - }); - const sendFileMessage = useSendFileMessageCallback({ - currentChannel, onBeforeSendFileMessage, - }, { - logger, - pubSub, - threadDispatcher, - }); - const sendVoiceMessage = useSendVoiceMessageCallback({ - currentChannel, onBeforeSendVoiceMessage, - }, { - logger, - pubSub, - threadDispatcher, - }); - const [sendMultipleFilesMessage] = useSendMultipleFilesMessage({ - currentChannel, onBeforeSendMultipleFilesMessage, - publishingModules: [PublishingModuleType.THREAD], - }, { - logger, - pubSub, - }); + onBeforeDownloadFileMessage, + isMultipleFilesMessageEnabled, + filterEmojiCategoryIds, + nicknamesMap, + ]); - const resendMessage = useResendMessageCallback({ - currentChannel, - }, { logger, pubSub, threadDispatcher }); - const updateMessage = useUpdateMessageCallback({ - currentChannel, - isMentionEnabled, - }, { logger, pubSub, threadDispatcher }); - const deleteMessage = useDeleteMessageCallback( - { currentChannel, threadDispatcher }, - { logger }, - ); + return null; +}; - // memo - const nicknamesMap: Map = useMemo(() => ( - (config.groupChannel.replyType !== 'none' && currentChannel) - ? getNicknamesMapFromMembers(currentChannel?.members) - : new Map() - ), [currentChannel?.members]); +export const ThreadProvider = (props: ThreadProviderProps) => { + const { children } = props; return ( - - {/* UserProfileProvider */} - - {children} - - + + + {/* UserProfileProvider */} + + {children} + + ); }; export const useThreadContext = () => { - const context = React.useContext(ThreadContext); - if (!context) throw new Error('ThreadContext not found. Use within the Thread module'); - return context; + const { state, actions } = useThread(); + return { ...state, ...actions }; +}; + +const useThreadStore = () => { + return useStore(ThreadContext, state => state, initialState); }; diff --git a/src/modules/Thread/context/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..7a3962718 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,19 @@ export const useSendVoiceMessageCallback = ({ logger.info('Thread | useSendVoiceMessageCallback: Start sending voice message', messageParams); currentChannel?.sendFileMessage(messageParams) .onPending((pendingMessage) => { - threadDispatcher({ - type: ThreadContextActionTypes.SEND_MESSAGE_START, - payload: { - /* pubSub is used instead of messagesDispatcher + 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 + // @ts-ignore + requestState: 'pending', + isUserMessage: pendingMessage.isUserMessage, + isFileMessage: pendingMessage.isFileMessage, + isAdminMessage: pendingMessage.isAdminMessage, + isMultipleFilesMessage: pendingMessage.isMultipleFilesMessage, }); setTimeout(() => scrollIntoLast(), SCROLL_BOTTOM_DELAY_FOR_SEND); }) @@ -92,10 +93,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..36c1350ba 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,35 +99,26 @@ export const useThreadFetchers = ({ async (callback?: (messages: BaseMessage[]) => void) => { if (threadListState !== ThreadListStateTypes.INITIALIZED || latestMessageTimeStamp === 0 || !staleParentMessage) return; - threadDispatcher({ - type: ThreadContextActionTypes.GET_NEXT_MESSAGES_START, - payload: null, - }); + getNextMessagesStart(); try { const params = getThreadMessageListParams({ prevResultSize: 0, includeReactions: isReactionEnabled }); const { threadedMessages, parentMessage } = await staleParentMessage.getThreadedMessagesByTimestamp(latestMessageTimeStamp, params); logger.info('Thread | useGetNextThreadsCallback: Fetch next threads succeeded.', { parentMessage, threadedMessages }); - threadDispatcher({ - type: ThreadContextActionTypes.GET_NEXT_MESSAGES_SUCESS, - payload: { parentMessage, threadedMessages }, - }); + getNextMessagesSuccess(threadedMessages as CoreMessageType[]); setTimeout(() => callback?.(threadedMessages)); } catch (error) { logger.info('Thread | useGetNextThreadsCallback: Fetch next threads failed.', error); - threadDispatcher({ - type: ThreadContextActionTypes.GET_NEXT_MESSAGES_FAILURE, - payload: error, - }); + getNextMessagesFailure(); } }, [threadListState, latestMessageTimeStamp, isReactionEnabled, staleParentMessage], ); return { - initialize, - loadPrevious, - loadNext, + initializeThreadFetcher: initialize, + fetchPrevThreads: loadPrevious, + fetchNextThreads: loadNext, }; }; diff --git a/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts b/src/modules/Thread/context/hooks/useUpdateMessageCallback.ts index db58e4a2b..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..8e7983373 --- /dev/null +++ b/src/modules/Thread/context/useThread.ts @@ -0,0 +1,974 @@ +import { useSyncExternalStore } from 'use-sync-external-store/shim'; +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, 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 { 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, +): message is T & { reqId: string } { + 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 { logger, pubSub } = config; + const isMentionEnabled = config.groupChannel.enableMention; + const isReactionEnabled = config.groupChannel.enableReactions; + + const state: ThreadState = useSyncExternalStore(store.subscribe, store.getState); + const { + message, + parentMessage, + currentChannel, + threadListState, + allThreadMessages, + onBeforeSendUserMessage, + onBeforeSendFileMessage, + onBeforeSendVoiceMessage, + onBeforeSendMultipleFilesMessage, + } = state; + + const 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 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; + + 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], + ); + + 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], + ); + + const 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 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(); + } + + 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 actions = useMemo(() => ({ + setCurrentUserId: (currentUserId: string) => store.setState(state => ({ + ...state, + currentUserId: currentUserId, + })), + + getChannelStart: () => store.setState(state => ({ + ...state, + channelState: ChannelStateTypes.LOADING, + currentChannel: null, + })), + + getChannelSuccess: (groupChannel: GroupChannel) => store.setState(state => ({ + ...state, + channelState: ChannelStateTypes.INITIALIZED, + currentChannel: groupChannel, + // only support in normal group channel + isMuted: groupChannel?.members?.find((member) => member?.userId === state.currentUserId)?.isMuted || false, + isChannelFrozen: groupChannel?.isFrozen || false, + })), + + getChannelFailure: () => store.setState(state => ({ + ...state, + channelState: ChannelStateTypes.INVALID, + currentChannel: null, + })), + + getParentMessageStart: () => store.setState(state => ({ + ...state, + parentMessageState: ParentMessageStateTypes.LOADING, + parentMessage: null, + })), + + getParentMessageSuccess: (parentMessage: SendableMessageType) => store.setState(state => ({ + ...state, + parentMessageState: ParentMessageStateTypes.INITIALIZED, + parentMessage: parentMessage, + })), + + getParentMessageFailure: () => store.setState(state => ({ + ...state, + parentMessageState: ParentMessageStateTypes.INVALID, + parentMessage: null, + })), + + setEmojiContainer: (emojiContainer: EmojiContainer) => store.setState(state => ({ + ...state, + emojiContainer: emojiContainer, + })), + + onMessageReceived: (channel: GroupChannel, message: SendableMessageType) => store.setState(state => { + if ( + state.currentChannel?.url !== channel?.url + || state.hasMoreNext + || message?.parentMessage?.messageId !== state?.parentMessage?.messageId + ) { + return state; + } + + const isAlreadyReceived = state.allThreadMessages.findIndex((m) => ( + m.messageId === message.messageId + )) > -1; + + return { + ...state, + parentMessage: state.parentMessage?.messageId === message?.messageId ? message : state.parentMessage, + allThreadMessages: isAlreadyReceived + ? state.allThreadMessages.map((m) => ( + m.messageId === message.messageId ? message : m + )) + : [ + ...state.allThreadMessages.filter((m) => (m as SendableMessageType)?.reqId !== message?.reqId), + message, + ], + }; + }), + + 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, + }; + }), + + toggleReaction, + sendMessage, + sendFileMessage, + sendVoiceMessage, + sendMultipleFilesMessage, + resendMessage, + updateMessage, + deleteMessage, + initializeThreadFetcher, + fetchPrevThreads, + fetchNextThreads, + + }), [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, +}