Skip to main content
Version: v5

Custom Thread List

In this cookbook we'll go over how we can create and customize a screen containing the ThreadList component. The goal will be to display a list of threads as well as have a banner that displays the number of unread threads within the list.

Prerequisites

This cookbook assumes that you've already set up a separate screen that is able to display a single Thread along with all of its replies. If this is indeed the case, there are no changes that you will need to do in order to have it working for the ThreadList screen.

It also assumes that you have a functioning chatClient that is used elsewhere.

For illustration purposes, we are going to be using the React Navigation library for navigation (however, any navigation library can be used).

Creating the screen

Here is how you add the ThreadList component in a new screen example:

import { OverlayProvider, Chat, ThreadList } from 'stream-chat-react-native';

const ThreadListScreen = () => {
return (
<OverlayProvider>
<Chat client={client}>
<ThreadList />
</Chat>
</OverlayProvider>
);
};

This alone should be enough to already display all of the threads the user is involved in using the standard UI.

Step 1 Preview

As a next step, let's make sure that the ThreadList updates internally only when the screen is in focus. For this purpose, we can use the isFocused property like so:

import { OverlayProvider, Chat, ThreadList } from 'stream-chat-react-native';
// any navigation library hook/method can be used for this
import { useIsFocused } from '@react-navigation/native';

const ThreadListScreen = () => {
const isFocused = useIsFocused();
return (
<OverlayProvider>
<Chat client={client}>
<ThreadList isFocused={isFocused} />
</Chat>
</OverlayProvider>
);
};

This can be particularly useful if we decide to put the list of threads as a separate tab at the bottom of our app (and the screens don't get unmounted when switching tabs).

The ThreadList should display as intended now and refresh only when we're looking at it. However, we may notice that clicking on an item within the ThreadList does not do anything. Since we would like to see the item that we clicked on in more detail, let's navigate to the thread that it describes:

import { OverlayProvider, Chat, ThreadList } from 'stream-chat-react-native';
// any navigation library hook/method can be used for this
import { useNavigation, useIsFocused } from '@react-navigation/native';

const ThreadListScreen = () => {
const isFocused = useIsFocused();
const navigation = useNavigation();
return (
<OverlayProvider>
<Chat client={client}>
<ThreadList
isFocused={isFocused}
// here we can reuse the same method as we would in the ChannelList component
onThreadSelect={(thread, channel) => {
navigation.navigate('ThreadScreen', {
thread,
channel,
});
}}
/>
</Chat>
</OverlayProvider>
);
};

Now we should be able to navigate to any thread displayed in the list. For the next step, let's assume that we do not need all of the information displayed in each item within the ThreadList - but instead are really only interested in displaying the ID of each thread. We also want these to be separated by exactly 10 pixels apart.

For this, we can override how the ThreadListItem is rendered within the list.

import { TouchableOpacity, Text } from 'react-native';
import {
OverlayProvider,
Chat,
ThreadList,
useThreadsContext,
useThreadListItemContext,
MessageType,
} from 'stream-chat-react-native';

// any navigation library hook/method can be used for this
import { useNavigation, useIsFocused } from '@react-navigation/native';

const ThreadListItem = () => {
// we grab the definition of the navigation function from the ThreadsContext
const { onThreadSelect } = useThreadsContext();
// we grab the actual thread, channel and its parent message from the ThreadListItemContext
const { channel, thread, parentMessage } = useThreadListItemContext();
return (
<TouchableOpacity
style={{ backgroundColor: 'red', padding: 5 }}
onPress={() => {
if (onThreadSelect) {
// since we are overriding the behaviour of the item it is mandatory to pass the parameters in the
// below to onThreadSelect()
onThreadSelect({ thread: parentMessage as MessageType, threadInstance: thread }, channel);
}
}}
>
<Text>{thread?.id}</Text>
</TouchableOpacity>
)
}

const ThreadListScreen = () => {
const isFocused = useIsFocused();
const navigation = useNavigation();
return (
<OverlayProvider>
<Chat client={client}>
<ThreadList
isFocused={isFocused}
{/* here we can reuse the same method as we would in the ChannelList component */}
onThreadSelect={(thread, channel) => {
navigation.navigate('ThreadScreen', {
thread,
channel,
});
}}
ThreadListItem={ThreadListItem}
/>
</Chat>
</OverlayProvider>
);
};

Now we should have successfully rendered the thread list items.

Step 2 Preview

As a second to last step, we would like to display a banner right on top of the list telling us how many unread threads we have in total just so that we can keep track.

For that, we can rely on using the state store our SDK exposes and the useStateStore hook:

import { TouchableOpacity, Text, View } from 'react-native';
import {
OverlayProvider,
Chat,
ThreadList,
useThreadsContext,
useThreadListItemContext,
MessageType,
useStateStore,
} from 'stream-chat-react-native';
import { ThreadManagerState } from 'stream-chat';
// any navigation library hook/method can be used for this
import { useNavigation, useIsFocused } from '@react-navigation/native';

// ...

// create a selector for unreadThreadCount
const selector = (nextValue: ThreadManagerState) => [nextValue.unreadThreadCount];

const CustomBanner = () => {
// use our utility hook to access the store
const [unreadCount] = useStateStore(client?.threads?.state, selector);

// display the banner
return (
<View style={{ paddingVertical: 15, paddingHorizontal: 5 }}>
<Text>You have {unreadCount} unread threads !</Text>
</View>
);
};

const ThreadListScreen = () => {
const isFocused = useIsFocused();
const navigation = useNavigation();
return (
<OverlayProvider>
<Chat client={client}>
{/* it's important that the banner is also a child of <Chat /> */}
<CustomBanner />
<ThreadList
isFocused={isFocused}
{/* here we can reuse the same method as we would in the ChannelList component */}
onThreadSelect={(thread, channel) => {
navigation.navigate('ThreadScreen', {
thread,
channel,
});
}}
ThreadListItem={ThreadListItem}
/>
</Chat>
</OverlayProvider>
);
};

Step 3 Preview

And as a final step, we would like each item in the ThreadList to be separated by 10 pixels. For this, we can use the FlatList properties that can be accessed through the additionalFlatListProps prop.

Finally, our screen would look like this:

// ...

const ItemSeparatorComponent = () => <View style={{ paddingVertical: 5 }} />

const ThreadListScreen = () => {
const isFocused = useIsFocused();
const navigation = useNavigation();
return (
<OverlayProvider>
<Chat client={client}>
{/* it's important that the banner is also a child of <Chat /> */}
<CustomBanner />
<ThreadList
isFocused={isFocused}
{/* here we can reuse the same method as we would in the ChannelList component */}
onThreadSelect={(thread, channel) => {
navigation.navigate('ThreadScreen', {
thread,
channel,
});
}}
ThreadListItem={ThreadListItem}
additionalFlatListProps={{
ItemSeparatorComponent,
}}
/>
</Chat>
</OverlayProvider>
);
};

Step 4 Preview

We now have an entirely custom ThreadList that we can further modify to our liking.

Did you find this page helpful?