diff --git a/src/lib/Sendbird/context/SendbirdContext.tsx b/src/lib/Sendbird/context/SendbirdContext.tsx index 5f495f841..8556326ea 100644 --- a/src/lib/Sendbird/context/SendbirdContext.tsx +++ b/src/lib/Sendbird/context/SendbirdContext.tsx @@ -3,6 +3,7 @@ import { createStore } from '../../../utils/storeManager'; import { initialState } from './initialState'; import { SendbirdState } from '../types'; import { useStore } from '../../../hooks/useStore'; +import { TwoDepthPartial } from '../../../utils/typeHelpers/partialDeep'; /** * SendbirdContext @@ -12,7 +13,25 @@ export const SendbirdContext = React.createContext createStore(initialState); +export const createSendbirdContextStore = (props?: TwoDepthPartial) => createStore({ + config: { + ...initialState.config, + ...props?.config, + }, + stores: { + ...initialState.stores, + ...props?.stores, + }, + eventHandlers: { + ...initialState.eventHandlers, + ...props?.eventHandlers, + }, + emojiManager: initialState.emojiManager, + utils: { + ...initialState.utils, + ...props?.utils, + }, +}); /** * A specialized hook for Ssendbird state management diff --git a/src/lib/Sendbird/context/SendbirdProvider.tsx b/src/lib/Sendbird/context/SendbirdProvider.tsx index 71c03d957..6b13c576e 100644 --- a/src/lib/Sendbird/context/SendbirdProvider.tsx +++ b/src/lib/Sendbird/context/SendbirdProvider.tsx @@ -3,7 +3,13 @@ import React, { ReactElement, useEffect, useMemo, useRef, useState } from 'react import { useUIKitConfig } from '@sendbird/uikit-tools'; /* Types */ -import type { ImageCompressionOptions, SendbirdProviderProps, SendbirdStateConfig } from '../types'; +import { + ImageCompressionOptions, + Logger, + SendbirdProviderProps, + SendbirdState, + SendbirdStateConfig, +} from '../types'; /* Providers */ import VoiceMessageProvider from '../../VoiceMessageProvider'; @@ -33,10 +39,10 @@ import { DEFAULT_MULTIPLE_FILES_MESSAGE_LIMIT, DEFAULT_UPLOAD_SIZE_LIMIT, VOICE_ import { EmojiReactionListRoot, MenuRoot } from '../../../ui/ContextMenu'; import useSendbird from './hooks/useSendbird'; -import { SendbirdContext, useSendbirdStore } from './SendbirdContext'; -import { createStore } from '../../../utils/storeManager'; -import { initialState } from './initialState'; +import { createSendbirdContextStore, SendbirdContext, useSendbirdStore } from './SendbirdContext'; import useDeepCompareEffect from '../../../hooks/useDeepCompareEffect'; +import { deleteNullish } from '../../../utils/utils'; +import { TwoDepthPartial } from '../../../utils/typeHelpers/partialDeep'; /** * SendbirdContext - Manager @@ -49,6 +55,7 @@ const SendbirdContextManager = ({ customWebSocketHost, configureSession, theme = 'light', + logger, config = {}, nickname = '', colorSet, @@ -68,11 +75,10 @@ const SendbirdContextManager = ({ eventHandlers, htmlTextDirection = 'ltr', forceLeftToRightMessageLayout = false, -}: SendbirdProviderProps): ReactElement => { +}: SendbirdProviderProps & { logger: Logger }): ReactElement => { const onStartDirectMessage = _onStartDirectMessage ?? _onUserProfileMessage; - const { logLevel = '', userMention = {}, isREMUnitEnabled = false, pubSub: customPubSub } = config; + const { userMention = {}, isREMUnitEnabled = false, pubSub: customPubSub } = config; const { isMobile } = useMediaQueryContext(); - const [logger, setLogger] = useState(LoggerFactory(logLevel as LogLevel)); const [pubSub] = useState(customPubSub ?? pubSubFactory()); const { state, updateState } = useSendbirdStore(); @@ -121,11 +127,6 @@ const SendbirdContextManager = ({ actions.disconnect({ logger }); }); - // to create a pubsub to communicate between parent and child - useEffect(() => { - setLogger(LoggerFactory(logLevel as LogLevel)); - }, [logLevel]); - // should move to reducer const [currentTheme, setCurrentTheme] = useState(theme); useEffect(() => { @@ -365,8 +366,49 @@ const SendbirdContextManager = ({ return null; }; -const InternalSendbirdProvider = ({ children, stringSet, breakpoint, dateLocale }) => { - const storeRef = useRef(createStore(initialState)); +const InternalSendbirdProvider = (props: SendbirdProviderProps & { logger: Logger }) => { + const { + children, + stringSet, + breakpoint, + dateLocale, + } = props; + + const defaultProps: TwoDepthPartial = deleteNullish({ + config: { + renderUserProfile: props?.renderUserProfile, + onStartDirectMessage: props?.onStartDirectMessage, + allowProfileEdit: props?.allowProfileEdit, + appId: props?.appId, + userId: props?.userId, + accessToken: props?.accessToken, + theme: props?.theme, + htmlTextDirection: props?.htmlTextDirection, + forceLeftToRightMessageLayout: props?.forceLeftToRightMessageLayout, + pubSub: props?.config?.pubSub, + logger: props?.logger, + userListQuery: props?.userListQuery, + voiceRecord: { + maxRecordingTime: props?.voiceRecord?.maxRecordingTime ?? VOICE_RECORDER_DEFAULT_MAX, + minRecordingTime: props?.voiceRecord?.minRecordingTime ?? VOICE_RECORDER_DEFAULT_MIN, + }, + userMention: { + maxMentionCount: props?.config?.userMention?.maxMentionCount || 10, + maxSuggestionCount: props?.config?.userMention?.maxSuggestionCount || 15, + }, + imageCompression: { + compressionRate: 0.7, + outputFormat: 'preserve', + ...props?.imageCompression, + }, + disableMarkAsDelivered: props?.disableMarkAsDelivered, + isMultipleFilesMessageEnabled: props?.isMultipleFilesMessageEnabled, + }, + eventHandlers: props?.eventHandlers, + }); + + const storeRef = useRef(createSendbirdContextStore(defaultProps)); + const localeStringSet = useMemo(() => { return { ...getStringSet('en'), ...stringSet }; }, [stringSet]); @@ -391,11 +433,19 @@ const InternalSendbirdProvider = ({ children, stringSet, breakpoint, dateLocale }; export const SendbirdContextProvider = (props: SendbirdProviderProps) => { - const { children } = props; + const { children, config } = props; + const logLevel = config?.logLevel; + + const [logger, setLogger] = useState(LoggerFactory(logLevel as LogLevel)); + + // to create a pubsub to communicate between parent and child + useEffect(() => { + setLogger(LoggerFactory(logLevel as LogLevel)); + }, [logLevel]); return ( - - + + {children} ); diff --git a/src/modules/ChannelSettings/context/ChannelSettingsProvider.tsx b/src/modules/ChannelSettings/context/ChannelSettingsProvider.tsx index f6a783d5a..56d06d7de 100644 --- a/src/modules/ChannelSettings/context/ChannelSettingsProvider.tsx +++ b/src/modules/ChannelSettings/context/ChannelSettingsProvider.tsx @@ -7,7 +7,7 @@ import { useStore } from '../../../hooks/useStore'; import { useChannelHandler } from './hooks/useChannelHandler'; import uuidv4 from '../../../utils/uuid'; -import { classnames } from '../../../utils/utils'; +import { classnames, deleteNullish } from '../../../utils/utils'; import { createStore } from '../../../utils/storeManager'; import { UserProfileProvider } from '../../../lib/UserProfileContext'; import useSendbird from '../../../lib/Sendbird/context/hooks/useSendbird'; @@ -103,9 +103,27 @@ const ChannelSettingsManager = ({ return null; }; -const createChannelSettingsStore = () => createStore(initialState); -const InternalChannelSettingsProvider = ({ children }) => { - const storeRef = useRef(createChannelSettingsStore()); +const createChannelSettingsStore = (props?: Partial) => createStore({ + ...initialState, + ...props, +}); + +const InternalChannelSettingsProvider = (props: ChannelSettingsContextProps) => { + const { children } = props; + + const defaultProps: Partial = deleteNullish({ + channelUrl: props?.channelUrl, + onCloseClick: props?.onCloseClick, + onLeaveChannel: props?.onLeaveChannel, + onChannelModified: props?.onChannelModified, + onBeforeUpdateChannel: props?.onBeforeUpdateChannel, + renderUserListItem: props?.renderUserListItem, + queries: props?.queries, + overrideInviteUser: props?.overrideInviteUser, + }); + + const storeRef = useRef(createChannelSettingsStore(defaultProps)); + return ( {children} @@ -116,7 +134,7 @@ const InternalChannelSettingsProvider = ({ children }) => { const ChannelSettingsProvider = (props: ChannelSettingsContextProps) => { const { children, className } = props; return ( - +
diff --git a/src/modules/ChannelSettings/context/types.ts b/src/modules/ChannelSettings/context/types.ts index 685c0e586..8c9585d54 100644 --- a/src/modules/ChannelSettings/context/types.ts +++ b/src/modules/ChannelSettings/context/types.ts @@ -42,6 +42,6 @@ export interface ChannelSettingsState extends CommonChannelSettingsProps { export interface ChannelSettingsContextProps extends CommonChannelSettingsProps, Pick { - children?: React.ReactElement; + children?: ReactNode; className?: string; } diff --git a/src/modules/CreateChannel/context/CreateChannelProvider.tsx b/src/modules/CreateChannel/context/CreateChannelProvider.tsx index a7fbfced1..983cf53b5 100644 --- a/src/modules/CreateChannel/context/CreateChannelProvider.tsx +++ b/src/modules/CreateChannel/context/CreateChannelProvider.tsx @@ -11,6 +11,7 @@ import { createStore } from '../../../utils/storeManager'; import { useStore } from '../../../hooks/useStore'; import useCreateChannel from './useCreateChannel'; import useSendbird from '../../../lib/Sendbird/context/hooks/useSendbird'; +import { deleteNullish } from '../../../utils/utils'; const CreateChannelContext = React.createContext> | null>(null); @@ -139,16 +140,31 @@ const CreateChannelProvider: React.FC = (props: Crea const { children } = props; return ( - + {children} ); }; -const createCreateChannelStore = () => createStore(initialState); -const InternalCreateChannelProvider: React.FC> = ({ children }) => { - const storeRef = useRef(createCreateChannelStore()); +const createCreateChannelStore = (props?: Partial) => createStore({ + ...initialState, + ...props, +}); + +const InternalCreateChannelProvider: React.FC> = (props: CreateChannelProviderProps) => { + const { children } = props; + + const defaultProps: Partial = deleteNullish({ + userListQuery: props?.userListQuery, + onCreateChannelClick: props?.onCreateChannelClick, + onChannelCreated: props?.onChannelCreated, + onBeforeCreateChannel: props?.onBeforeCreateChannel, + onCreateChannel: props?.onCreateChannel, + overrideInviteUser: props?.overrideInviteUser, + }); + + const storeRef = useRef(createCreateChannelStore(defaultProps)); return ( diff --git a/src/modules/GroupChannel/context/GroupChannelProvider.tsx b/src/modules/GroupChannel/context/GroupChannelProvider.tsx index bd76b0d8a..117a1dbdc 100644 --- a/src/modules/GroupChannel/context/GroupChannelProvider.tsx +++ b/src/modules/GroupChannel/context/GroupChannelProvider.tsx @@ -31,6 +31,7 @@ import type { } from './types'; import useSendbird from '../../../lib/Sendbird/context/hooks/useSendbird'; import useDeepCompareEffect from '../../../hooks/useDeepCompareEffect'; +import { deleteNullish } from '../../../utils/utils'; const initialState = { currentChannel: null, @@ -60,8 +61,48 @@ const initialState = { export const GroupChannelContext = createContext> | null>(null); -export const InternalGroupChannelProvider: React.FC> = ({ children }) => { - const storeRef = useRef(createStore(initialState)); +const createGroupChannelStore = (props?: Partial) => createStore({ + ...initialState, + ...props, +}); + +export const InternalGroupChannelProvider = (props: GroupChannelProviderProps) => { + const { children } = props; + + const defaultProps: Partial = deleteNullish({ + channelUrl: props?.channelUrl, + renderUserProfile: props?.renderUserProfile, + disableUserProfile: props?.disableUserProfile, + onUserProfileMessage: props?.onUserProfileMessage, + onStartDirectMessage: props?.onStartDirectMessage, + isReactionEnabled: props?.isReactionEnabled, + isMessageGroupingEnabled: props?.isMessageGroupingEnabled, + isMultipleFilesMessageEnabled: props?.isMultipleFilesMessageEnabled, + showSearchIcon: props?.showSearchIcon, + threadReplySelectType: props?.threadReplySelectType, + disableMarkAsRead: props?.disableMarkAsRead, + scrollBehavior: props?.scrollBehavior, + forceLeftToRightMessageLayout: props?.forceLeftToRightMessageLayout, + startingPoint: props?.startingPoint, + animatedMessageId: props?.animatedMessageId, + onMessageAnimated: props?.onMessageAnimated, + messageListQueryParams: props?.messageListQueryParams, + filterEmojiCategoryIds: props?.filterEmojiCategoryIds, + onBeforeSendUserMessage: props?.onBeforeSendUserMessage, + onBeforeSendFileMessage: props?.onBeforeSendFileMessage, + onBeforeSendVoiceMessage: props?.onBeforeSendVoiceMessage, + onBeforeSendMultipleFilesMessage: props?.onBeforeSendMultipleFilesMessage, + onBeforeUpdateUserMessage: props?.onBeforeUpdateUserMessage, + onBeforeDownloadFileMessage: props?.onBeforeDownloadFileMessage, + onBackClick: props?.onBackClick, + onChatHeaderActionClick: props?.onChatHeaderActionClick, + onReplyInThreadClick: props?.onReplyInThreadClick, + onSearchClick: props?.onSearchClick, + onQuoteMessageClick: props?.onQuoteMessageClick, + renderUserMentionItem: props?.renderUserMentionItem, + }); + + const storeRef = useRef(createGroupChannelStore(defaultProps)); return ( @@ -319,7 +360,7 @@ const GroupChannelManager :React.FC = (props) => { return ( - + {props.children} diff --git a/src/modules/GroupChannel/context/__tests__/useGroupChannel.spec.tsx b/src/modules/GroupChannel/context/__tests__/useGroupChannel.spec.tsx index ef336312d..9753768b7 100644 --- a/src/modules/GroupChannel/context/__tests__/useGroupChannel.spec.tsx +++ b/src/modules/GroupChannel/context/__tests__/useGroupChannel.spec.tsx @@ -113,14 +113,14 @@ describe('useGroupChannel', () => { it('provides initial state', () => { const { result } = renderHook(() => useGroupChannel(), { wrapper }); - expect(result.current.state).toEqual(expect.objectContaining({ + expect(result.current.state).toMatchObject({ currentChannel: null, channelUrl: mockChannel.url, fetchChannelError: null, quoteMessage: null, animatedMessageId: null, isScrollBottomReached: true, - })); + }); }); it('updates channel state', () => { diff --git a/src/modules/GroupChannelList/context/GroupChannelListProvider.tsx b/src/modules/GroupChannelList/context/GroupChannelListProvider.tsx index cd1d41e14..f2980c9ba 100644 --- a/src/modules/GroupChannelList/context/GroupChannelListProvider.tsx +++ b/src/modules/GroupChannelList/context/GroupChannelListProvider.tsx @@ -14,7 +14,7 @@ import type { SdkStore } from '../../../lib/Sendbird/types'; import { PartialRequired } from '../../../utils/typeHelpers/partialRequired'; import { createStore } from '../../../utils/storeManager'; import { useStore } from '../../../hooks/useStore'; -import { noop } from '../../../utils/utils'; +import { deleteNullish, noop } from '../../../utils/utils'; import useSendbird from '../../../lib/Sendbird/context/hooks/useSendbird'; import useGroupChannelList from './useGroupChannelList'; import useDeepCompareEffect from '../../../hooks/useDeepCompareEffect'; @@ -224,9 +224,32 @@ export const GroupChannelListManager: React.FC = return null; }; -const createGroupChannelListStore = () => createStore(initialState); -const InternalGroupChannelListProvider = ({ children }) => { - const storeRef = useRef(createGroupChannelListStore()); +const createGroupChannelListStore = (props?: Partial) => createStore({ + ...initialState, + ...props, +}); + +const InternalGroupChannelListProvider = (props: GroupChannelListProviderProps) => { + const { children } = props; + + const defaultProps: Partial = deleteNullish({ + onChannelSelect: props?.onChannelSelect, + onChannelCreated: props?.onChannelCreated, + className: props?.className, + selectedChannelUrl: props?.selectedChannelUrl, + allowProfileEdit: props?.allowProfileEdit, + disableAutoSelect: props?.disableAutoSelect, + isTypingIndicatorEnabled: props?.isTypingIndicatorEnabled, + isMessageReceiptStatusEnabled: props?.isMessageReceiptStatusEnabled, + channelListQueryParams: props?.channelListQueryParams, + onThemeChange: props?.onThemeChange, + onCreateChannelClick: props?.onCreateChannelClick, + onBeforeCreateChannel: props?.onBeforeCreateChannel, + onUserProfileUpdated: props?.onUserProfileUpdated, + }); + + const storeRef = useRef(createGroupChannelListStore(defaultProps)); + return ( {children} @@ -238,7 +261,7 @@ export const GroupChannelListProvider = (props: GroupChannelListProviderProps) = const { children, className } = props; return ( - +
{children}
diff --git a/src/modules/MessageSearch/context/MessageSearchProvider.tsx b/src/modules/MessageSearch/context/MessageSearchProvider.tsx index 330582028..216179380 100644 --- a/src/modules/MessageSearch/context/MessageSearchProvider.tsx +++ b/src/modules/MessageSearch/context/MessageSearchProvider.tsx @@ -14,6 +14,7 @@ import { createStore } from '../../../utils/storeManager'; import { useStore } from '../../../hooks/useStore'; import useMessageSearch from './hooks/useMessageSearch'; import useSendbird from '../../../lib/Sendbird/context/hooks/useSendbird'; +import { deleteNullish } from '../../../utils/utils'; export interface MessageSearchProviderProps { channelUrl: string; @@ -134,9 +135,23 @@ const MessageSearchManager: React.FC = ({ return null; }; -const createMessageSearchStore = () => createStore(initialState); -const InternalMessageSearchProvider: React.FC> = ({ children }) => { - const storeRef = useRef(createMessageSearchStore()); +const createMessageSearchStore = (props?: Partial) => createStore({ + ...initialState, + ...props, +}); + +const InternalMessageSearchProvider: React.FC> = (props: MessageSearchProviderProps) => { + const { children } = props; + + const defaultProps: Partial = deleteNullish({ + channelUrl: props?.channelUrl, + messageSearchQuery: props?.messageSearchQuery, + searchString: props?.searchString, + onResultLoaded: props?.onResultLoaded, + onResultClick: props?.onResultClick, + }); + + const storeRef = useRef(createMessageSearchStore(defaultProps)); return ( @@ -145,17 +160,18 @@ const InternalMessageSearchProvider: React.FC> ); }; -const MessageSearchProvider: React.FC = ({ - children, - channelUrl, - searchString, - messageSearchQuery, - onResultLoaded, - onResultClick, -}) => { +const MessageSearchProvider: React.FC = (props: MessageSearchProviderProps) => { + const { + children, + channelUrl, + searchString, + messageSearchQuery, + onResultLoaded, + onResultClick, + } = props; return ( - + > | null>(null); -export const InternalThreadProvider: React.FC> = ({ children }) => { - const storeRef = useRef(createStore(initialState)); +const createThreadStore = (props?: Partial) => createStore({ + ...initialState, + ...props, +}); + +export const InternalThreadProvider: React.FC> = (props: ThreadProviderProps) => { + const { children } = props; + + const defaultProps: Partial = { + channelUrl: props?.channelUrl, + message: props?.message, + onHeaderActionClick: props?.onHeaderActionClick, + onMoveToParentMessage: props?.onMoveToParentMessage, + onBeforeSendUserMessage: props?.onBeforeSendUserMessage, + onBeforeSendFileMessage: props?.onBeforeSendFileMessage, + onBeforeSendVoiceMessage: props?.onBeforeSendVoiceMessage, + onBeforeSendMultipleFilesMessage: props?.onBeforeSendMultipleFilesMessage, + onBeforeDownloadFileMessage: props?.onBeforeDownloadFileMessage, + isMultipleFilesMessageEnabled: props?.isMultipleFilesMessageEnabled, + filterEmojiCategoryIds: props?.filterEmojiCategoryIds, + }; + + const storeRef = useRef(createThreadStore(defaultProps)); return ( @@ -211,7 +232,7 @@ export const ThreadProvider = (props: ThreadProviderProps) => { const { children } = props; return ( - + {/* UserProfileProvider */} diff --git a/src/utils/typeHelpers/partialDeep.ts b/src/utils/typeHelpers/partialDeep.ts index 29368ddfb..0b8502bd3 100644 --- a/src/utils/typeHelpers/partialDeep.ts +++ b/src/utils/typeHelpers/partialDeep.ts @@ -18,3 +18,13 @@ export type PartialDeep = T extends object [P in keyof T]?: PartialDeep; } : T; + +export type TwoDepthPartial = T extends object + ? T extends Set // Set, Map, Function, etc. are also treated as an object so we need this to skip recursion for them. + ? T + : T extends (...args: any[]) => any + ? T + : { + [P in keyof T]?: Partial; + } + : T;