Thread
Thread gives one thread to the components inside it. It renders its children with the contexts a message list and a composer need, the same ones Channel provides, but bound to the thread's replies and composer. Render it next to Channel, on a screen of its own, not inside it. Thread UI explains what it does, and Replies and threads covers the feature.
Best practices
- Get the thread from the client, never from
new Thread().onThreadSelectonMessageList,MessageFlashListandThreadListgives you the thread the user picked, andclient.threads.ensure({ channel, parentMessage })returns the thread for a message, creating it when needed. The client keeps one instance per thread and sends it its events. - Compose the screen from children.
Threadrenders no message list or composer of its own: renderMessageListorMessageFlashList, andMessageComposer, inside it. - Pass
Threadthe same configuration you passChannel, such askeyboardVerticalOffset,topInset,audioRecordingEnabledandmessageActions. A thread doesn't inherit it from a channel. - Clear your app's record of the open thread in
onThreadDismount, for example when the user navigates back. - A
Threadneeds noChannelaround it. Inside it,useChannel()returns the thread's channel.
Basic usage
The channel screen opens a thread when the user taps a message's replies, and the thread screen renders it:
import type {
Channel as StreamChannel,
Thread as StreamThread,
} from "stream-chat";
import {
Channel,
MessageComposer,
MessageList,
Thread,
} from "stream-chat-react-native";
export const ChannelScreen = ({
channel,
openThread,
}: {
channel: StreamChannel;
openThread: (thread: StreamThread) => void;
}) => (
<Channel channel={channel} keyboardVerticalOffset={0}>
<MessageList onThreadSelect={openThread} />
<MessageComposer />
</Channel>
);
export const ThreadScreen = ({
onClose,
thread,
}: {
onClose: () => void;
thread: StreamThread;
}) => (
<Thread keyboardVerticalOffset={0} onThreadDismount={onClose} thread={thread}>
<MessageList />
<MessageComposer />
</Thread>
);MessageList renders the parent message above the replies, and MessageComposer writes into thread.messageComposer. Moving between channels, threads and messages passes the thread from one screen to the other, and opens a thread at a reply.
What it does
While it's mounted, Thread:
- loads the first page of replies with
paginator.toTail()onthread.messagePaginator, unless the paginator already holds some. A thread from the thread list arrives with its latest replies; - reloads the thread with
Thread.reload()while its state is stale; - watches the thread's channel once when nobody has loaded it, so the user's capabilities in the channel reach the message menu, the reactions and the composer;
- activates the thread with
Thread.activate(), so it receives its events even when the thread list doesn't hold it, and marks new replies read as they arrive. Opening a thread doesn't mark its channel read; - routes its notifications to its own notification host.
When it unmounts, it releases the activation and calls onThreadDismount. When the first page of replies fails to load, it renders LoadingErrorIndicator, "Error loading replies...", in place of its children, and tapping it loads the replies again.
UI customization
Thread has no override key. Change the screen by changing its children. Inside it, the message list and the composer take the same override keys as in a channel. To give the thread overrides of its own, wrap its children in WithComponents:
import type { Thread as StreamThread } from "stream-chat";
import {
MessageComposer,
MessageList,
Thread,
WithComponents,
} from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";
const NoTimestamp = () => null;
const threadOverrides: ComponentOverrides = { MessageTimestamp: NoTimestamp };
export const CompactThread = ({ thread }: { thread: StreamThread }) => (
<Thread thread={thread}>
<WithComponents overrides={threadOverrides}>
<MessageList />
<MessageComposer />
</WithComponents>
</Thread>
);The parent message isn't a child you place. The message list renders it at the older end of the list as ThreadFooterComponent: the parent message, the reply count, and InlineLoadingMoreThreadIndicator while older replies load. To change it, pass your own component as FooterComponent on MessageList or HeaderComponent on MessageFlashList; Replacing the parent message area shows one.
ThreadFooterComponent props
ThreadFooterComponent reads the thread from ThreadContext and the parent message and reply count from thread.state, so it updates when they change. It renders nothing until the thread has its parent message.
| Prop | Type | Default | Description |
|---|---|---|---|
parentMessagePreventPress |
boolean |
true |
Whether tapping and long-pressing the parent message do nothing. false makes it pressable like any message. |
InlineLoadingMoreThreadIndicator takes no props. It shows a spinner while thread.messagePaginator loads.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
thread |
Thread |
required | The thread to render, a Thread from stream-chat. Get it from onThreadSelect, client.threads.ensure() or client.threads.get(). |
children |
ReactNode |
none | The screen's content: usually MessageList or MessageFlashList, and MessageComposer. |
allowThreadMessagesInChannel |
boolean |
true |
Shows the "Also send to channel" checkbox in the composer. false hides it. |
onThreadDismount |
() => void |
none | Called when Thread unmounts. Only the function passed on the first render is called. |
notificationHostId |
string |
the thread's own host | The notification host the thread's notifications are routed to. By default, a host derived from the thread's id, which getThreadNotificationHostId(thread.id) returns. |
Configuration shared with Channel
Thread also takes the configuration props of Channel, with the same types and defaults. They configure the parts inside it, and only those: a thread screen doesn't inherit the props of a channel screen. Their descriptions are on Channel. The props that only Channel takes, channel, initializeOnMount, markReadOnMount and initialScrollToFirstUnreadMessage, are about loading a channel; Thread loads its replies itself. Channel also takes client and t, which Thread doesn't.
Messages
These reach MessagesContext, which the messages read.
| Prop | Type | Default |
|---|---|---|
additionalPressableProps |
Omit<PressableProps, 'style'> |
none |
customMessageSwipeAction |
({ channel, message, }: { channel: Channel; message: LocalMessage; }) => void |
none |
disableTypingIndicator |
boolean |
false |
dismissKeyboardOnMessageTouch |
boolean |
true |
enableSwipeToReply |
boolean |
true |
FlatList |
typeof NativeHandlers.FlatList | undefined |
the registered FlatList |
forceAlignMessages |
Alignment | boolean |
none |
getMessageGroupStyle |
(params: MessageGroupStylesParams) => GroupStyle[] |
the SDK's grouping |
giphyVersion |
keyof NonNullable<Attachment['giphy']> |
"fixed_height" |
handleBan |
(message: LocalMessage) => Promise<void> |
none |
handleBlockUser |
(user: LocalMessage['user']) => Promise<void> |
none |
handleCopy |
(message: LocalMessage) => Promise<void> |
none |
handleDelete |
(message: LocalMessage) => Promise<void> |
none |
handleDeleteForMe |
(message: LocalMessage) => Promise<void> |
none |
handleEdit |
(message: LocalMessage) => void |
none |
handleFlag |
(message: LocalMessage) => Promise<void> |
none |
handleMarkUnread |
(message: LocalMessage) => Promise<void> |
none |
handleMute |
(message: LocalMessage) => Promise<void> |
none |
handlePinMessage |
((message: LocalMessage) => MessageActionType) | null |
none |
handleQuotedReply |
(message: LocalMessage) => Promise<void> |
none |
handleReaction |
(message: LocalMessage, reactionType: string) => Promise<void> |
none |
handleRetry |
(message: LocalMessage) => Promise<void> |
none |
handleThreadReply |
(message: LocalMessage) => Promise<void> |
none |
hasCreatePoll |
boolean |
whether polls are on for the channel |
isAttachmentEqual |
(prevAttachment: Attachment, nextAttachment: Attachment) => boolean |
none |
markdownRules |
MarkdownRules |
none |
messageActions |
(param: MessageActionsParams) => MessageActionType[] |
the SDK's actions |
messageContentOrder |
MessageContentType[] |
quoted reply, gallery, files, poll, AI text, attachments, location, text |
messageOverlayTargetId |
string |
"@stream-io/message-root" |
messageSwipeToReplyHitSlop |
ViewProps['hitSlop'] |
{ left: width, right: width }, with the window's width |
messageTextNumberOfLines |
number |
none |
myMessageTheme |
DeepPartial<Theme> |
none |
onAlsoSentToChannelHeaderPress |
(payload: AlsoSentToChannelHeaderPressPayload) => void |
none |
onLongPressMessage |
(payload: MessagePressableHandlerPayload) => void |
none |
onPressInMessage |
(payload: MessagePressableHandlerPayload) => void |
none |
onPressMessage |
(payload: MessagePressableHandlerPayload) => void |
none |
reactionListPosition |
'top' | 'bottom' |
"top" |
reactionListType |
'clustered' | 'segmented' |
"clustered" |
selectReaction |
(message: LocalMessage) => (reactionType: string) => Promise<void> |
none |
shouldShowUnreadUnderlay |
boolean |
true |
supportedReactions |
ReactionData[] |
the SDK's reactions |
urlPreviewType |
'compact' | 'full' |
"full" |
Composer
These reach MessageInputContext. See MessageInputContext.
| Prop | Type | Default |
|---|---|---|
additionalTextInputProps |
TextInputProps |
none |
asyncMessagesLockDistance |
number |
50 |
asyncMessagesMinimumPressDuration |
number |
500 |
asyncMessagesSlideToCancelDistance |
number |
75 |
attachmentPickerBottomSheetHeight |
number |
333, or 72 with disableAttachmentPicker |
attachmentSelectionBarHeight |
number |
72 |
audioRecordingEnabled |
boolean |
false |
audioRecordingSendOnComplete |
boolean |
false |
compressImageQuality |
number |
none |
createPollOptionGap |
number |
8 |
focusInputOnPickerClose |
boolean |
true |
handleAttachButtonPress |
() => void |
none |
hasCameraPicker |
boolean |
whether an image picker is installed |
hasCommands |
boolean |
whether the channel type has commands |
hasFilePicker |
boolean |
whether a document picker is installed |
hasImagePicker |
boolean |
whether an image picker or media library is installed |
messageInputFloating |
boolean |
false |
openPollCreationDialog |
({ sendMessage }: Pick<LocalMessageInputContext, 'sendMessage'>) => void |
none |
setInputRef |
(ref: TextInputRef | null) => void |
none |
hasCreatePoll has no effect in a thread: the attachment picker offers no poll button there.
Attachment picker
These reach AttachmentPickerContext. See Attachment picker.
| Prop | Type | Default |
|---|---|---|
bottomInset |
number |
0 |
disableAttachmentPicker |
boolean |
true without a media library |
numberOfAttachmentImagesToLoadPerCall |
number |
25 |
numberOfAttachmentPickerImageColumns |
number |
3 |
shouldRenderAttachmentPicker |
boolean |
true |
topInset |
number |
0 |
Message list
These reach ChannelContext, which the message list reads.
| Prop | Type | Default |
|---|---|---|
allowDateSeparatorForSystemMessages |
boolean |
false |
enableMessageGroupingByUser |
boolean |
true |
enforceUniqueReaction |
boolean |
false |
hideDateSeparators |
boolean |
false |
hideStickyDateHeader |
boolean |
false |
maxTimeBetweenGroupedMessages |
number |
no limit |
Keyboard, audio and capabilities
| Prop | Type | Default |
|---|---|---|
additionalKeyboardAvoidingViewProps |
Partial<KeyboardCompatibleViewProps> |
none |
allowConcurrentAudioPlayback |
boolean |
false |
disableKeyboardCompatibleView |
boolean |
false |
keyboardBehavior |
KeyboardCompatibleViewProps['behavior'] |
"padding" |
keyboardVerticalOffset |
number |
86.5 on iOS, -300 on Android |
overrideOwnCapabilities |
Partial<OwnCapabilitiesContextValue> |
none |
Related hooks
| Hook | Returns |
|---|---|
useThreadContext() |
{ thread, allowThreadMessagesInChannel }; thread is undefined outside a Thread |
useChannel() |
inside a thread, the thread's channel |
useMessagePaginator() |
inside a thread, thread.messagePaginator |
useMessageComposer() |
inside a thread, thread.messageComposer |
useTypingString() |
who is typing in the thread |
See ThreadContext, useThreadContext and, for the thread's state and methods, Thread and thread manager.