import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { Chat, OverlayProvider } from 'stream-chat-react-native';
const client = StreamChat.getInstance('api_key');
const Stack = createStackNavigator<{ home: undefined }>();
export const App = () =>
<NavigationContainer>
<OverlayProvider>
<Chat client={client}>
<Stack.Navigator>
<Stack.Screen component={() => {/** App components */})} name='home' />
</Stack.Navigator>
</Chat>
</OverlayProvider>
</NavigationContainer>;
Stream Chat with Navigation
Stream Chat for React Native provides many features out of the box that require positioning on the screen in a certain manor to result in the desired UI.
The AttachmentPicker
, ImageGallery
, and MessageOverlay
, all need to be rendered in front of other components to have the desired effect.
All of these elements are controlled by the OverlayProvider
, when used together with navigation considerations need to be taken in how these will appear.
The guidance provided makes the assumption you are using React Navigation in your app in conjunction with createStackNavigator
.
If you are using another navigation solution, or utilizing createNativeStackNavigator
, other considerations will need to be taken depending on your navigation arrangement.
createNativeStackNavigator
uses the native APIs UINavigationController
on iOS and
Fragment
on Android. The OverlayProvider
needs to exist in a view that can render content in front of the
chat screen. Therefore using a fullScreenModal
with createNativeStackNavigator
, which uses
UIModalPresentationFullScreen
on iOS and modal
on Android, to render your chat screen will leave the OverlayProvider
rendered
behind the chat. If you are having issues we suggest you get in touch with support and we can find a solution to your
specific navigation arrangement.
Navigation Container
The NavigationContainer
manages the apps state in React Navigation.
Nested navigators and screens all exist within the container.
To ensure the OverlayProvider
can render content above all of these screens, headers, tab-bars, etc. the OverlayProvider
must be rendered around them.
As noted in Hello Stream Chat the Chat
component can either surround the entire application or be rendered locally on a screen.
You can choose whatever suits your needs best, theming, connection handling, and translations are all handled out of the box in the Chat
component; and this may be a consideration in where in the app you want this component to be rendered.
Having it higher in the stack helps to ensure it is not unmounted at times when a connection is present.
If Chat
is unmounted with a connection present you may have to implement some connection handling functions yourself to ensure you reconnect when the app is, for instance, reopened from the background.
The WebSocket connection closes on it’s own approximately 15 seconds after the app is put into background.
Not handling the connection on appState
changes will also effect how Stream Chat handles Push Notifications.
import { StreamChat } from 'stream-chat';
import { Chat } from 'stream-chat-react-native';
const client = StreamChat.getInstance('api_key');
export const Screen = () => <Chat client={client}>{/** App components */}</Chat>;
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { OverlayProvider } from 'stream-chat-react-native';
import { Screen } from './Screen';
const Stack = createStackNavigator<{ home: undefined }>();
export const App = () => (
<NavigationContainer>
<OverlayProvider>
<Stack.Navigator>
<Stack.Screen component={Screen} name='home' />
</Stack.Navigator>
</OverlayProvider>
</NavigationContainer>
);
Keyboard
The Channel
component contains a KeyboardCompatibleView
that, like the standard React Native KeyboardAvoidingView
, needs a keyboardVerticalOffset
to account for distance between the top of the user screen and the react native view.
This height in most cases with React Navigation in conjunction with Stream Chat is the header height of React Navigation.
This can be accessed from React Navigation using the useHeaderHeight
hook from @react-navigation/stack
, and given as a prop to Channel
.
const headerHeight = useHeaderHeight();
<Channel keyboardVerticalOffset={headerHeight}>
Attachment Picker
The AttachmentPicker
is a keyboard-esk view that allows a user to attach photos and files.
Part of the construction of the AttachmentPicker
is a bottom-sheet provided by the OverlayProvider
.
This bottom-sheet provides a grid of images in a scroll-able list that can be lifted up to make selecting images easier.
The placement of the AttachmentPicker
is dependant on two values, the bottomInset
and the topInset
.
Top Inset
topInset
is used to determine how high the scroll-able bottom-sheet can go when opened.
topInset
defaults to 0 and covers the entire screen, or it can be set to the top safe area inset if desired.
The most common choice when using React Navigation is to get the header height using the useHeaderHeight
hook from @react-navigation/stack
and set the top inset to the given height for a nice visual result where the picker open to the header.
const headerHeight = useHeaderHeight();
const { setTopInset } = useAttachmentPickerContext();
useEffect(() => {
setTopInset(headerHeight);
}, [headerHeight]);
topInset
can be set via props on the OverlayProvider
, or set via the setTopInset
function provided by the useAttachmentPickerContext
hook.
Bottom Inset
bottomInset
is used to adjust the height of the AttachmentPicker
menu to align properly with the bottom-sheet when open.
This is the height between the bottom of the MessageInput
container and the bottom of the screen.
If you are displaying the chat screen without a tab-bar it is most likely the bottom inset is the bottom safe area inset.
If you are using a bottom tab-bar you can utilize the useBottomTabBarHeight
hook from @react-navigation/bottom-tabs
to get the appropriate height to use.
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Chat, OverlayProvider } from 'stream-chat-react-native';
const client = StreamChat.getInstance('api_key');
const Stack = createStackNavigator<{ home: undefined }>();
export const Nav = () => {
const { bottom } = useSafeAreaInsets();
return (
<NavigationContainer>
<OverlayProvider bottomInset={bottom}>
<Chat client={client}>
<Stack.Navigator>
<Stack.Screen component={() => {/** App components */})} name='home' />
</Stack.Navigator>
</Chat>
</OverlayProvider>
</NavigationContainer>
);
};
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Nav } from './Nav';
export const App = () => (
<SafeAreaProvider>
<Nav />
</SafeAreaProvider>
);
bottomInset
can be set via props on the OverlayProvider
, or set via the setBottomInset
function provided by the useAttachmentPickerContext
hook.
Resetting Selected Images
The selected images in the AttachmentPicker
are tightly coupled to the MessageInput
.
As there is only one AttachmentPicker
in the navigation stack, but multiple MessageInput
’s can exist, there are details around the setup that must be implemented correctly.
For the typical navigation setup clearing the selected images is handled out of the box.
For this to function properly your usage of the Channel
component must include the usage of thread
when appropriate.
Failing to do this will result in unintentional behavior, such as excess image uploads as the selected images will be uploaded twice when selected if the thread
state is not properly kept.
In more complex scenarios where more than one Channel
could potentially be rendered in multiple tabs a different approach would be necessary.
It is suggested that you architect an approach best for your specific scenario.
The setSelectedImages
function can be pulled off of the useAttachmentPickerContext
for granular control of the AttachmentPicker
images.
Image Gallery
The ImageGallery
is populated by the MessageList
component.
MessageList
utilizes information provided by both the ThreadContext
and threadList
prop to determine if the ImageGallery
should be updated.
If there is both a thread
provided by the ThreadContext
and the threadList
prop is true
on MessageList
, or both values are falsy, the ImageGallery
is updated appropriately.
In practice this means that if you implement a screen for the main Channel
, and another for Thread
that is navigated to onThreadSelect
, you need to indicate to the main Channel
it should not update the ImageGallery
while the Thread
screen is present.
To do this the main Channel
component should be given the appropriate thread
when the Thread
screen shown, then the thread
removed when navigating back to the main Channel
screen.
This can be done by keeping the current thread
in a context
and setting it onThreadSelect
, then removing it onThreadDismount
.
Alternatively if a user only has a single path to and from the Channel
screen to the Thread
screen and back you can accomplish the same result using a local state and the useFocusEffect
hook from React Navigation.
export const ThreadScreen = () => {
const { channel } = useAppChannel();
const { setThread, thread } = useAppThread();
return (
<Channel channel={channel} thread={thread}>
<Thread onThreadDismount={() => setThread(undefined)} />
</Channel>
);
};
export const ChannelScreen = () => {
const { channel } = useAppChannel();
const { setThread, thread } = useAppThread();
return (
<Channel channel={channel} thread={thread}>
<MessageList
onThreadSelect={selectedThread => {
setThread(selectedThread);
navigation.navigate('ThreadScreen');
}}
/>
<MessageInput />
</Channel>
);
};
export const ChannelScreen = () => {
const { channel } = useAppChannel();
const [selectedThread, setSelectedThread] = useState<MessageType>();
useFocusEffect(() => {
setSelectedThread(undefined);
});
return (
<Channel channel={channel} thread={selectedThread}>
<MessageList
onThreadSelect={thread => {
setSelectedThread(thread);
navigation.navigate('ThreadScreen', { thread });
}}
/>
<MessageInput />
</Channel>
);
};