import type { AsyncStorageStatic } from '@react-native-async-storage/async-storage'; import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Platform } from 'react-native'; import type { MMKV } from 'react-native-mmkv'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import SendbirdChat, { DeviceOsPlatform, SendbirdChatParams, SendbirdPlatform, SendbirdProduct } from '@sendbird/chat'; import { GroupChannelModule } from '@sendbird/chat/groupChannel'; import { OpenChannelModule } from '@sendbird/chat/openChannel'; import type { HeaderStyleContextType, UIKitTheme } from '@sendbird/uikit-react-native-foundation'; import { DialogProvider, Header, HeaderStyleProvider, LightUIKitTheme, ToastProvider, UIKitThemeProvider, } from '@sendbird/uikit-react-native-foundation'; import { SBUConfig, UIKitConfigProvider } from '@sendbird/uikit-tools'; import { Logger, NOOP, PartialDeep, SendbirdChatSDK, SendbirdGroupChannel, SendbirdGroupChannelCreateParams, SendbirdMember, SendbirdReactedUserInfo, SendbirdUser, useIsFirstMount, } from '@sendbird/uikit-utils'; import { LocalizationContext, LocalizationProvider } from '../contexts/LocalizationCtx'; import { PlatformServiceProvider } from '../contexts/PlatformServiceCtx'; import { ReactionProvider } from '../contexts/ReactionCtx'; import { type SBUHandlers, SBUHandlersProvider } from '../contexts/SBUHandlersCtx'; import type { ChatRelatedFeaturesInUIKit } from '../contexts/SendbirdChatCtx'; import { SendbirdChatProvider } from '../contexts/SendbirdChatCtx'; import { UserProfileProvider } from '../contexts/UserProfileCtx'; import EmojiManager from '../libs/EmojiManager'; import type { ImageCompressionConfigInterface } from '../libs/ImageCompressionConfig'; import ImageCompressionConfig from '../libs/ImageCompressionConfig'; import InternalLocalCacheStorage from '../libs/InternalLocalCacheStorage'; import MentionConfig, { MentionConfigInterface } from '../libs/MentionConfig'; import MentionManager from '../libs/MentionManager'; import SBUUtils from '../libs/SBUUtils'; import VoiceMessageConfig, { VoiceMessageConfigInterface } from '../libs/VoiceMessageConfig'; import VoiceMessageStatusManager from '../libs/VoiceMessageStatusManager'; import StringSetEn from '../localization/StringSet.en'; import type { StringSet } from '../localization/StringSet.type'; import SBUDynamicModule from '../platform/dynamicModule'; import type { ClipboardServiceInterface, FileServiceInterface, MediaServiceInterface, NotificationServiceInterface, PlayerServiceInterface, RecorderServiceInterface, } from '../platform/types'; import { ErrorBoundaryProps, LocalCacheStorage } from '../types'; import VERSION from '../version'; import InternalErrorBoundaryContainer from './InternalErrorBoundaryContainer'; const NetInfo = SBUDynamicModule.get('@react-native-community/netinfo', 'warn'); export const SendbirdUIKit = Object.freeze({ VERSION, PLATFORM: Platform.OS.toLowerCase(), DEFAULT: { AUTO_PUSH_TOKEN_REGISTRATION: true, USE_USER_ID_FOR_NICKNAME: false, IMAGE_COMPRESSION: true, }, }); export type ChatOmittedInitParams = SendbirdChatParams<[GroupChannelModule, OpenChannelModule]>; export type SendbirdUIKitContainerProps = React.PropsWithChildren<{ appId: string; platformServices: { file: FileServiceInterface; notification: NotificationServiceInterface; clipboard: ClipboardServiceInterface; media: MediaServiceInterface; player: PlayerServiceInterface; recorder: RecorderServiceInterface; }; chatOptions: Partial & Partial & { onInitialized?: (sdkInstance: SendbirdChatSDK) => SendbirdChatSDK; localCacheStorage: LocalCacheStorage; }; uikitOptions?: PartialDeep<{ common: SBUConfig['common']; groupChannel: Omit & { /** * @deprecated Currently, this feature is turned off by default. If you wish to use this feature, contact us: {@link https://dashboard.sendbird.com/settings/contact_us?category=feedback_and_feature_requests&product=UIKit} */ enableReactionsSupergroup: never; }; groupChannelList: SBUConfig['groupChannel']['channelList']; groupChannelSettings: SBUConfig['groupChannel']['setting']; openChannel: SBUConfig['openChannel']['channel']; }>; localization?: { stringSet?: StringSet; }; styles?: { theme?: UIKitTheme; statusBarTranslucent?: boolean; defaultHeaderTitleAlign?: 'left' | 'center'; defaultHeaderHeight?: number; HeaderComponent?: HeaderStyleContextType['HeaderComponent']; }; errorBoundary?: { disabled?: boolean; onError?: (props: ErrorBoundaryProps) => void; ErrorInfoComponent?: (props: ErrorBoundaryProps) => React.ReactNode; }; handlers?: Partial; toast?: { dismissTimeout?: number; }; userProfile?: { onCreateChannel: (channel: SendbirdGroupChannel) => void; onBeforeCreateChannel?: ( channelParams: SendbirdGroupChannelCreateParams, users: SendbirdUser[] | SendbirdMember[] | SendbirdReactedUserInfo[], ) => SendbirdGroupChannelCreateParams | Promise; }; reaction?: { onPressUserProfile?: (user: SendbirdReactedUserInfo) => void; }; userMention?: Pick, 'mentionLimit' | 'suggestionLimit' | 'debounceMills'>; imageCompression?: Partial; voiceMessage?: PartialDeep; }>; const SendbirdUIKitContainer = (props: SendbirdUIKitContainerProps) => { const { children, appId, chatOptions, uikitOptions, platformServices, localization, styles, errorBoundary, handlers, toast, userProfile, reaction, } = props; if (!chatOptions.localCacheStorage) { throw new Error('SendbirdUIKitContainer: chatOptions.localCacheStorage is required'); } else if ('getItem' in chatOptions.localCacheStorage) { Logger.warn( 'SendbirdUIKitContainer: localCacheStorage for `AsyncStorage` is deprecated. Please use `MMKV` instead.', ); } const defaultStringSet = localization?.stringSet ?? StringSetEn; const isFirstMount = useIsFirstMount(); const unsubscribes = useRef void>>([]); const [internalStorage] = useState(() => new InternalLocalCacheStorage(chatOptions.localCacheStorage)); const [sdkInstance, setSdkInstance] = useState(() => { const sendbird = initializeSendbird(appId, chatOptions); unsubscribes.current = sendbird.unsubscribes; return sendbird.chatSDK; }); const { imageCompressionConfig, voiceMessageConfig, mentionConfig } = useConfigInstance(props); const emojiManager = useMemo(() => new EmojiManager(internalStorage), [internalStorage]); const mentionManager = useMemo(() => new MentionManager(mentionConfig), [mentionConfig]); const voiceMessageStatusManager = useMemo(() => new VoiceMessageStatusManager(), []); useLayoutEffect(() => { if (!isFirstMount) { const sendbird = initializeSendbird(appId, chatOptions); setSdkInstance(sendbird.chatSDK); unsubscribes.current = sendbird.unsubscribes; } return () => { unsubscribes.current.forEach((u) => { try { u(); } catch {} }); }; }, [appId, internalStorage]); const renderChildren = () => { if (errorBoundary?.disabled) { return children; } else { return {children}; } }; const sbuHandlers: SBUHandlers = { onOpenURL: SBUUtils.openURL, ...handlers, }; return ( {(value) => { const STRINGS = value?.STRINGS || defaultStringSet; return ( {renderChildren()} ); }} ); }; type InitOptions = Partial & { localCacheStorage: LocalCacheStorage; onInitialized?: (sdk: SendbirdChatSDK) => SendbirdChatSDK; }; const initializeSendbird = (appId: string, options: InitOptions) => { let chatSDK: SendbirdChatSDK; const unsubscribes: Array<() => void> = []; const { localCacheStorage, onInitialized, ...chatInitParams } = options; const isMMKVStorage = 'getString' in localCacheStorage; chatSDK = SendbirdChat.init({ appId, newInstance: true, modules: [new GroupChannelModule(), new OpenChannelModule()], localCacheEnabled: true, useMMKVStorageStore: isMMKVStorage ? (localCacheStorage as MMKV) : undefined, useAsyncStorageStore: !isMMKVStorage ? (localCacheStorage as AsyncStorageStatic) : undefined, ...chatInitParams, }); if (onInitialized) { chatSDK = onInitialized(chatSDK); } const platform = getDeviceOSPlatform(); if (SendbirdUIKit.VERSION && platform) { const deviceOSInfo = { platform, version: String(Platform.Version) }; const customData = { platform_version: getReactNativeVersion() }; const uikitExtension = { product: SendbirdProduct.UIKIT_CHAT, version: SendbirdUIKit.VERSION, platform: SendbirdPlatform.REACT_NATIVE, }; chatSDK.addSendbirdExtensions([uikitExtension], deviceOSInfo, customData); chatSDK.addExtension('sb_uikit', SendbirdUIKit.VERSION); } if (SendbirdUIKit.PLATFORM) { chatSDK.addExtension('device-os-platform', SendbirdUIKit.PLATFORM); } if (NetInfo?.addEventListener) { try { // NOTE: For removing buggy behavior of NetInfo.addEventListener // When you first add an event listener, it is assumed that the initialization of the internal event detector is done simultaneously. // In other words, when you call the first event listener two events are triggered immediately // - the one that is called when adding the event listener // - and the internal initialization event NetInfo.addEventListener(NOOP)(); } catch {} const listener = (callback: () => void, callbackType: 'online' | 'offline') => { let callCount = 0; const unsubscribe = NetInfo.addEventListener((state) => { const online = Boolean(state.isConnected) || Boolean(state.isInternetReachable); // NOTE: When NetInfo.addEventListener is called // the event is immediately triggered regardless of whether the event actually occurred. // This is why it filters the first event. if (callCount === 0) { callCount++; return; } if (online && callbackType === 'online') callback(); if (!online && callbackType === 'offline') callback(); }); unsubscribes.push(unsubscribe); return unsubscribe; }; chatSDK.setOnlineListener?.((onOnline) => listener(onOnline, 'online')); chatSDK.setOfflineListener?.((onOffline) => listener(onOffline, 'offline')); } return { chatSDK, unsubscribes }; }; function getDeviceOSPlatform() { switch (Platform.OS) { case 'android': return DeviceOsPlatform.ANDROID; case 'ios': return DeviceOsPlatform.IOS; case 'web': return DeviceOsPlatform.WEB; case 'windows': return DeviceOsPlatform.WINDOWS; default: return undefined; } } function getReactNativeVersion() { const { major, minor, patch } = Platform.constants.reactNativeVersion; return `${major}.${minor}.${patch}`; } const useConfigInstance = ({ imageCompression, userMention, voiceMessage }: SendbirdUIKitContainerProps) => { const mentionConfig = useMemo(() => { return new MentionConfig({ mentionLimit: userMention?.mentionLimit || MentionConfig.DEFAULT.MENTION_LIMIT, suggestionLimit: userMention?.suggestionLimit || MentionConfig.DEFAULT.SUGGESTION_LIMIT, debounceMills: userMention?.debounceMills ?? MentionConfig.DEFAULT.DEBOUNCE_MILLS, delimiter: MentionConfig.DEFAULT.DELIMITER, trigger: MentionConfig.DEFAULT.TRIGGER, }); }, [userMention?.mentionLimit, userMention?.suggestionLimit, userMention?.debounceMills]); const imageCompressionConfig = useMemo(() => { return new ImageCompressionConfig({ compressionRate: imageCompression?.compressionRate || ImageCompressionConfig.DEFAULT.COMPRESSION_RATE, width: imageCompression?.width, height: imageCompression?.height, }); }, [imageCompression?.compressionRate, imageCompression?.width, imageCompression?.height]); const voiceMessageConfig = useMemo(() => { return new VoiceMessageConfig({ recorder: { minDuration: voiceMessage?.recorder?.minDuration ?? VoiceMessageConfig.DEFAULT.RECORDER.MIN_DURATION, maxDuration: voiceMessage?.recorder?.maxDuration ?? VoiceMessageConfig.DEFAULT.RECORDER.MAX_DURATION, }, }); }, [voiceMessage?.recorder?.minDuration, voiceMessage?.recorder?.maxDuration]); return { mentionConfig, imageCompressionConfig, voiceMessageConfig, }; }; export default SendbirdUIKitContainer;