Navigation

Stream Chat for React Native provides many features out of the box that require positioning the components on the screen in a certain manner to get the desired UI.

The AttachmentPicker and ImageGallery, 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, certain steps are needed to be taken to make these components appear fluently.

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.

The 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.

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.

The Chat component should ideally surround the entire application. 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 affect how Stream Chat handles Push Notifications.

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>;

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();

const App = () => {
  return (
    <Channel keyboardVerticalOffset={headerHeight}>
      {/* other components inside */}
    </Channel>
  );
};

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 dependent on two values, the bottomInset and the topInset.

Top Inset

The 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 opens to the header.

const headerHeight = useHeaderHeight();
const { setTopInset } = useAttachmentPickerContext();

useEffect(() => {
  setTopInset(headerHeight);
}, [headerHeight]);

The topInset can be set via props on the OverlayProvider, or set via the setTopInset function provided by the useAttachmentPickerContext hook.

Bottom Inset

The 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>
);

The 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.

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} threadList>
      <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>
  );
};
© Getstream.io, Inc. All Rights Reserved.