import React, { useEffect, useState } from 'react';
import { DevSettings, I18nManager, LogBox, Platform, useColorScheme } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import data from '@emoji-mart/data';
import notifee, { EventType } from '@notifee/react-native';
import Geolocation from '@react-native-community/geolocation';
import { getMessaging } from '@react-native-firebase/messaging';
import { createDrawerNavigator } from '@react-navigation/drawer';
import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { init, SearchIndex } from 'emoji-mart';
import type { LocalMessage, StreamChat, TextComposerMiddleware } from 'stream-chat';
import {
Chat,
createTextComposerEmojiMiddleware,
LiveLocationManagerProvider,
OverlayProvider,
setupCommandUIMiddlewares,
SqliteClient,
Streami18n,
ThemeProvider,
useOverlayContext,
WithComponents,
} from 'stream-chat-react-native';
import { MenuDrawer } from './src/components/MenuDrawer';
import { OfflineDbBoundary } from './src/components/OfflineDbBoundary';
import { useSampleAppComponentOverrides } from './src/components/SampleAppComponentOverrides';
import {
MessageInputFloatingConfigItem,
MessageOverlayBackdropConfigItem,
MessageListImplementationConfigItem,
MessageListModeConfigItem,
MessageListPruningConfigItem,
} from './src/components/SecretMenu.tsx';
import { AppContext } from './src/context/AppContext';
import { StreamChatProvider } from './src/context/StreamChatContext';
import { UserSearchProvider } from './src/context/UserSearchContext';
import { useChatClient } from './src/hooks/useChatClient';
import { useStreamChatTheme } from './src/hooks/useStreamChatTheme';
import { AdvancedUserSelectorScreen } from './src/screens/AdvancedUserSelectorScreen';
import { ChannelDetailsScreen } from './src/screens/ChannelDetailsScreen.tsx';
import { ChannelFilesScreen } from './src/screens/ChannelFilesScreen';
import { ChannelImagesScreen } from './src/screens/ChannelImagesScreen';
import { ChannelPinnedMessagesScreen } from './src/screens/ChannelPinnedMessagesScreen';
import { ChannelScreen } from './src/screens/ChannelScreen';
import { ChatScreen } from './src/screens/ChatScreen';
import { LoadingScreen } from './src/screens/LoadingScreen';
import { MapScreen } from './src/screens/MapScreen';
import { NewDirectMessagingScreen } from './src/screens/NewDirectMessagingScreen';
import { NewGroupChannelAddMemberScreen } from './src/screens/NewGroupChannelAddMemberScreen';
import { NewGroupChannelAssignNameScreen } from './src/screens/NewGroupChannelAssignNameScreen';
import { ThreadScreen } from './src/screens/ThreadScreen';
import { UserSelectorScreen } from './src/screens/UserSelectorScreen';
import type { StackNavigatorParamList, UserSelectorParamList } from './src/types';
import AsyncStore from './src/utils/AsyncStore.ts';
import { navigateToChannel, RootNavigationRef } from './src/utils/RootNavigation';
import { watchLocation } from './src/utils/watchLocation';
Geolocation.setRNConfiguration({
skipPermissionRequests: false,
authorizationLevel: 'always',
locationProvider: 'playServices',
});
init({ data });
if (__DEV__) {
DevSettings.addMenuItem('Reset local DB (offline storage)', () => {
SqliteClient.resetDB();
console.info('Local DB reset');
});
}
LogBox.ignoreLogs(['Non-serializable values were found in the navigation state']);
console.assert = () => null;
const initialChannelIdGlobalRef = { current: '' };
notifee.onBackgroundEvent(async ({ detail, type }) => {
if (type === EventType.PRESS) {
const channelId = detail.notification?.data?.channel_id as string;
if (channelId) {
navigateToChannel(channelId);
}
await Promise.resolve();
}
});
const Drawer = createDrawerNavigator();
const Stack = createNativeStackNavigator<StackNavigatorParamList>();
const UserSelectorStack = createNativeStackNavigator<UserSelectorParamList>();
const RTL_STORAGE_KEY = '@stream-rn-sampleapp-rtl-enabled';
const accessibilityConfig = { enabled: true };
const App = () => {
const { chatClient, isConnecting, loginUser, logout, switchUser } = useChatClient();
const [rtlEnabled, setRtlEnabled] = useState<boolean | undefined>(undefined);
const [messageListImplementation, setMessageListImplementation] = useState<
MessageListImplementationConfigItem['id'] | undefined
>(undefined);
const [messageListMode, setMessageListMode] = useState<
MessageListModeConfigItem['mode'] | undefined
>(undefined);
const [messageListPruning, setMessageListPruning] = useState<
MessageListPruningConfigItem['value'] | undefined
>(undefined);
const [messageInputFloating, setMessageInputFloating] = useState<
MessageInputFloatingConfigItem['value'] | undefined
>(undefined);
const [messageOverlayBackdrop, setMessageOverlayBackdrop] = useState<
MessageOverlayBackdropConfigItem['value'] | undefined
>(undefined);
const colorScheme = useColorScheme();
const streamChatTheme = useStreamChatTheme();
const streami18n = new Streami18n();
const componentOverrides = useSampleAppComponentOverrides(messageOverlayBackdrop);
const setRTLEnabled = React.useCallback(async (enabled: boolean) => {
await AsyncStore.setItem(RTL_STORAGE_KEY, enabled);
I18nManager.allowRTL(enabled);
I18nManager.forceRTL(enabled);
I18nManager.swapLeftAndRightInRTL(enabled);
setRtlEnabled(enabled);
DevSettings.reload();
}, []);
useEffect(() => {
const messaging = getMessaging();
const unsubscribeOnNotificationOpen = messaging.onNotificationOpenedApp((remoteMessage) => {
const channelId = remoteMessage.data?.channel_id as string;
if (channelId) {
navigateToChannel(channelId);
}
});
const unsubscribeForegroundEvent = notifee.onForegroundEvent(({ detail, type }) => {
if (type === EventType.PRESS) {
const channelId = detail.notification?.data?.channel_id as string;
if (channelId) {
navigateToChannel(channelId);
}
}
});
notifee.getInitialNotification().then((initialNotification) => {
if (initialNotification) {
const channelId = initialNotification.notification.data?.channel_id as string;
if (channelId) {
initialChannelIdGlobalRef.current = channelId;
}
}
});
messaging.getInitialNotification().then((remoteMessage) => {
if (remoteMessage) {
const channelId = remoteMessage.data?.channel_id as string;
if (channelId) {
initialChannelIdGlobalRef.current = channelId;
}
}
});
const getAppConfig = async () => {
const storedRTLEnabled = await AsyncStore.getItem<boolean>(RTL_STORAGE_KEY, false);
const nextRTLEnabled = !!storedRTLEnabled;
I18nManager.allowRTL(nextRTLEnabled);
I18nManager.forceRTL(nextRTLEnabled);
I18nManager.swapLeftAndRightInRTL(nextRTLEnabled);
if (I18nManager.isRTL !== nextRTLEnabled) {
DevSettings.reload();
return;
}
setRtlEnabled(nextRTLEnabled);
const messageListImplementationStoredValue = await AsyncStore.getItem(
'@stream-rn-sampleapp-messagelist-implementation',
{ id: 'flatlist' },
);
const messageListModeStoredValue = await AsyncStore.getItem(
'@stream-rn-sampleapp-messagelist-mode',
{ mode: 'default' },
);
const messageListPruningStoredValue = await AsyncStore.getItem(
'@stream-rn-sampleapp-messagelist-pruning',
{ value: undefined },
);
const messageInputFloatingStoredValue = await AsyncStore.getItem(
'@stream-rn-sampleapp-messageinput-floating',
{ value: false },
);
const messageOverlayBackdropStoredValue = await AsyncStore.getItem(
'@stream-rn-sampleapp-message-overlay-backdrop',
{ value: 'default' },
);
setMessageListImplementation(
messageListImplementationStoredValue?.id as MessageListImplementationConfigItem['id'],
);
setMessageListMode(messageListModeStoredValue?.mode as MessageListModeConfigItem['mode']);
setMessageListPruning(
messageListPruningStoredValue?.value as MessageListPruningConfigItem['value'],
);
setMessageInputFloating(
messageInputFloatingStoredValue?.value as MessageInputFloatingConfigItem['value'],
);
setMessageOverlayBackdrop(
messageOverlayBackdropStoredValue?.value as MessageOverlayBackdropConfigItem['value'],
);
};
getAppConfig();
return () => {
unsubscribeOnNotificationOpen();
unsubscribeForegroundEvent();
};
}, []);
useEffect(() => {
if (!chatClient) {
return;
}
chatClient.setMessageComposerSetupFunction(({ composer }) => {
composer.updateConfig({
drafts: {
enabled: true,
},
linkPreviews: {
enabled: true,
},
});
setupCommandUIMiddlewares(composer);
composer.textComposer.middlewareExecutor.insert({
middleware: [
createTextComposerEmojiMiddleware({
emojiSearchIndex: SearchIndex,
}) as TextComposerMiddleware,
],
position: { after: 'stream-io/text-composer/mentions-middleware' },
unique: true,
});
});
}, [chatClient]);
if (rtlEnabled === undefined || !messageListImplementation || !messageListMode) {
return;
}
return (
<SafeAreaProvider
style={{
backgroundColor: streamChatTheme.colors?.white_snow || '#FCFCFC',
}}
>
<GestureHandlerRootView style={{ flex: 1 }}>
<WithComponents overrides={componentOverrides}>
<OverlayProvider
accessibility={accessibilityConfig}
value={{ style: streamChatTheme }}
i18nInstance={streami18n}
>
<ThemeProvider style={streamChatTheme}>
<NavigationContainer
ref={RootNavigationRef}
theme={{
colors: {
...(colorScheme === 'dark' ? DarkTheme : DefaultTheme).colors,
background: streamChatTheme.colors?.white_snow || '#FCFCFC',
},
fonts: (colorScheme === 'dark' ? DarkTheme : DefaultTheme).fonts,
dark: colorScheme === 'dark',
}}
>
<AppContext.Provider
value={{
chatClient,
loginUser,
logout,
switchUser,
rtlEnabled,
setRTLEnabled,
messageListImplementation,
messageInputFloating: messageInputFloating ?? false,
messageListMode,
messageListPruning,
}}
>
{isConnecting && !chatClient ? (
<LoadingScreen />
) : chatClient ? (
<DrawerNavigatorWrapper chatClient={chatClient} i18nInstance={streami18n} />
) : (
<UserSelector />
)}
</AppContext.Provider>
</NavigationContainer>
</ThemeProvider>
</OverlayProvider>
</WithComponents>
</GestureHandlerRootView>
</SafeAreaProvider>
);
};
const DrawerNavigator: React.FC = () => (
<LiveLocationManagerProvider watchLocation={watchLocation}>
<Drawer.Navigator
drawerContent={MenuDrawer}
screenOptions={{
drawerStyle: {
width: 300,
},
}}
>
<Drawer.Screen component={HomeScreen} name='HomeScreen' options={{ headerShown: false }} />
</Drawer.Navigator>
</LiveLocationManagerProvider>
);
const isMessageAIGenerated = (message: LocalMessage) => !!message.ai_generated;
const DrawerNavigatorWrapper: React.FC<{
chatClient: StreamChat;
i18nInstance: Streami18n;
}> = ({ chatClient, i18nInstance }) => {
const [attempt, setAttempt] = useState(0);
const [offlineSupport, setOfflineSupport] = useState(true);
return (
<OfflineDbBoundary
key={`${attempt}-${offlineSupport}`}
onGiveUp={() => setOfflineSupport(false)}
onRetry={() => setAttempt((value) => value + 1)}
>
<Chat
client={chatClient}
enableOfflineSupport={offlineSupport}
i18nInstance={i18nInstance}
isMessageAIGenerated={isMessageAIGenerated}
useNativeMultipartUpload
>
<StreamChatProvider>
<UserSearchProvider>
<DrawerNavigator />
</UserSearchProvider>
</StreamChatProvider>
</Chat>
</OfflineDbBoundary>
);
};
const UserSelector = () => (
<UserSelectorStack.Navigator initialRouteName='UserSelectorScreen'>
<UserSelectorStack.Screen
component={AdvancedUserSelectorScreen}
name='AdvancedUserSelectorScreen'
options={{ gestureEnabled: false }}
/>
<UserSelectorStack.Screen
component={UserSelectorScreen}
name='UserSelectorScreen'
options={{ gestureEnabled: false, headerShown: false }}
/>
</UserSelectorStack.Navigator>
);
const HomeScreen = () => {
const { overlay } = useOverlayContext();
return (
<Stack.Navigator
initialRouteName={initialChannelIdGlobalRef.current ? 'ChannelScreen' : 'MessagingScreen'}
>
<Stack.Screen
component={ChatScreen}
name='MessagingScreen'
options={{ headerShown: false }}
/>
<Stack.Screen
component={ChannelScreen}
initialParams={
initialChannelIdGlobalRef.current
? { channelId: initialChannelIdGlobalRef.current }
: undefined
}
name='ChannelScreen'
options={{
gestureEnabled: Platform.OS === 'ios' && overlay === 'none',
headerShown: false,
}}
/>
<Stack.Screen
name='MapScreen'
component={MapScreen}
options={{ headerTitle: 'Location', headerBackTitle: 'Back' }}
/>
<Stack.Screen
component={NewDirectMessagingScreen}
name='NewDirectMessagingScreen'
options={{
headerShown: false,
}}
/>
<Stack.Screen
component={NewGroupChannelAddMemberScreen}
name='NewGroupChannelAddMemberScreen'
options={{ headerShown: false }}
/>
<Stack.Screen
component={NewGroupChannelAssignNameScreen}
name='NewGroupChannelAssignNameScreen'
options={{ headerShown: false }}
/>
<Stack.Screen
component={ChannelDetailsScreen}
name='ChannelDetailsScreen'
options={{ headerShown: false }}
/>
<Stack.Screen
component={ChannelImagesScreen}
name='ChannelImagesScreen'
options={{ headerShown: false }}
/>
<Stack.Screen
component={ChannelFilesScreen}
name='ChannelFilesScreen'
options={{ headerShown: false }}
/>
<Stack.Screen
component={ChannelPinnedMessagesScreen}
name='ChannelPinnedMessagesScreen'
options={{ headerShown: false }}
/>
<Stack.Screen
component={ThreadScreen}
name='ThreadScreen'
options={{
gestureEnabled: Platform.OS === 'ios' && overlay === 'none',
headerShown: false,
}}
/>
</Stack.Navigator>
);
};
export default App;