Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

Moving between channels, threads and messages

The SDK's components take users from one place to another in many ways: a row in a list, the replies button, a search result, a quoted message. This page lists each one, with what opens and where it lands.

The components open channels and threads through Navigation, and ChatView decides where they land. The examples use the app from Add threads in the Quick start: a channels view with a channel slot and a thread slot, and a threads view with a thread slot.

What the user clicks What opens Where it lands
Channels or Threads in the view selector The other view The whole layout
A row in the channel list The channel Channels view, in place of the open channel
A row in the thread list The thread Threads view, in place of the open thread
The replies button, or a message's reply action The message's thread The view shown, beside the channel
View on "Also sent in channel", in a thread The reply in the channel Channels view, in the channel's message list
View on "Replied to a thread", in a channel The reply in its thread The view shown, beside the channel
A channel in the search results The channel Channels view, in place of the open channel
A message in the search results The message, in its channel Channels view, in place of the open channel
A user in the search results A direct message channel with the user Channels view, in place of the open channel
Send direct message on a member's profile A direct message channel with the member Channels view, in place of the open channel
A pinned message in channel details The message, in its channel Channels view, in the channel's message list
A quoted message The quoted message The same message list
The unread messages notification The first unread message The same message list
The scroll to latest button The latest message The same message list
The back button in a header What the panel was opened on top of The same panel
The close button in a header Nothing: the panel closes
A section in channel details That section Channel details

Switching between the channel list and the thread list

ChatView has two views. In the Quick start app, the channels view shows the channel list and the open channel, and the threads view shows the user's threads and the open thread. ChatView.Selector renders a button for each, and the Threads button shows how many threads have unread replies.

Each view keeps what's open in its slots. A user who switches to the threads view and back finds the same channel open.

Two kinds of navigation also switch views:

  • Opening a channel switches to the channels view first. For example, View on a reply in the threads view opens its channel in the channels view.
  • A thread opens in the view that's shown. From the replies button in a channel, it opens in the channels view, beside the channel. From the thread list, it opens in the threads view.

To switch views from your own component, call setActiveView from useChatViewContext():

import { useChatViewContext } from "stream-chat-react/slot-layout";

export const AllThreadsButton = () => {
  const { activeView, setActiveView } = useChatViewContext();

  if (activeView === "threads") return null;

  return (
    <button onClick={() => setActiveView("threads")} type="button">
      All threads
    </button>
  );
};

The two buttons are also exported, as ChatViewChannelsSelectorButton and ChatViewThreadsSelectorButton, for a rail of your own. See View selector.

Opening a thread

Three things open a thread:

  • The replies button under a message that has replies, such as "3 replies".
  • The reply actions on a message: the quick reply button next to it, and Thread Reply in its menu. Replies inside a thread don't offer them.
  • A row in the thread list. With ⌘ or ctrl held, it opens the thread beside the one that's open, when the view has a second thread slot.

Each one calls openThread. The replies button and the actions pass the channel and the parent message, and get the thread for that message. In the Quick start app, the thread opens in the thread slot, beside the channel.

The actions and the replies button use handleOpenThread from the message context, which Message builds with useOpenThreadHandler, so Message's openThread prop replaces what all of them do. A custom message UI opens the thread the same way:

import { useMessageContext } from "stream-chat-react";

export const ReplyInThreadButton = () => {
  const { handleOpenThread } = useMessageContext();

  return (
    <button onClick={handleOpenThread} type="button">
      Reply in thread
    </button>
  );
};

The replies button and handleOpenThread don't pass the click to openThread, so a navigation override can't read ⌘ or ctrl from them.

Reply also sent in channel

A reply sent from a thread with Also send in channel checked shows in both message lists. In a channel of two members, the checkbox reads Also send as a direct message. In the thread, it's marked "Also sent in channel". In the channel, it's marked "Replied to a thread". View, next to either label, goes to the other copy:

  • From the thread to the channel. View jumps to the reply in the channel's message list. If the channel isn't on screen, for example in the threads view, it opens the channel first.
  • From the channel to the thread. View opens the reply's thread and jumps to the reply in it. If the thread isn't loaded yet, it fetches it first. If the thread can't be found, the user sees a "Thread has not been found" notification.

useMessageAlsoSentInChannelNavigation() holds this navigation. It returns viewReference, which picks the direction from where it's rendered, viewReplyInChannel and viewReplyInThread for each direction, and isInThread and isShownInChannel. To add something around the jump, pass your own onView to the SDK's indicator and replace it through WithComponents:

import type { PropsWithChildren } from "react";
import {
  MessageAlsoSentInChannelIndicator,
  useMessageAlsoSentInChannelNavigation,
  WithComponents,
} from "stream-chat-react";

const LoggedAlsoSentIndicator = () => {
  const { isInThread, viewReference } = useMessageAlsoSentInChannelNavigation();

  return (
    <MessageAlsoSentInChannelIndicator
      onView={async () => {
        console.log(isInThread ? "To the channel" : "To the thread");
        await viewReference();
      }}
    />
  );
};

export const AppComponents = ({ children }: PropsWithChildren) => (
  <WithComponents
    overrides={{ MessageAlsoSentInChannelIndicator: LoggedAlsoSentIndicator }}
  >
    {children}
  </WithComponents>
);

On a narrow screen, the thread can cover the channel it jumps to. Responsive layouts uses this override to close the thread first.

Opening a channel from elsewhere

Besides a row in the channel list, four things open a channel:

  • A channel in the search results. It opens the channel, watches it, and adds it to the channel lists whose filters it matches, so it shows there without reloading them.
  • A message in the search results. It opens the message's channel and jumps to the message.
  • A user in the search results. It opens a direct message channel with that user, creating it if it doesn't exist. The channel type is Search's directMessagingChannelType, messaging by default.
  • Channel details. Send direct message on a member's profile opens a direct message channel with the member, in the channel's type. Selecting a pinned message opens its channel and jumps to the message. Both close channel details.

A plain click on a channel replaces the open channel, and closes the thread that was open beside it. The channel list and the search results pass the click to openChannel, so an override can open the channel beside instead: see ⌘/ctrl-click opens beside. Each row and result also takes an onSelect handler, which replaces what a click does.

The two actions in channel details call openChannel too, without the click, so an override passed as deriveWorkspaceNavigation, or a navigation of your own, receives them with no event.

Jumping to a message

A jump moves a message list to one message. When that message isn't loaded, the list loads the messages around it first. The message is then scrolled into view and highlighted. The SDK jumps to a message from:

  • A quoted message. Clicking the quote jumps to the original, in the list the quote is in: the thread's list inside a thread.
  • A message in the search results, after opening its channel.
  • A pinned message in channel details, after opening its channel.
  • A reply also sent in channel, in either direction.
  • The unread messages notification in a channel's message list, which jumps to the first unread message.
  • The scroll to latest button, which jumps to the latest message when it isn't loaded, and otherwise scrolls down to it.

The jump itself is the message list's, in stream-chat: see Message paginators. A component of your own jumps the same way, in the list on screen: useMessagePaginator() returns the thread's list inside a thread, and the channel's otherwise.

import { useMessagePaginator } from "stream-chat-react";

export const ShowMessageButton = ({ messageId }: { messageId: string }) => {
  const messagePaginator = useMessagePaginator();

  return (
    <button
      onClick={() => void messagePaginator.jumpToMessage(messageId)}
      type="button"
    >
      Show message
    </button>
  );
};

To jump in a channel that isn't open, open it first with openChannel, then jump, as the message search results do.

Back and close

ChannelHeader and ThreadHeader have a back button at the start and a close button at the end. They are WorkspaceNavigationBackButton and WorkspaceNavigationCloseButton, the defaults of each header's StartContent and EndContent. Each one shows only when it can do something:

  • Back shows on a panel that was opened on top of something else. For example, a channel opened beside the open one while a thread is there covers that thread. Back reveals the thread again. If it was the only layer, the thread mounts again and loses its scroll position. See Layers.
  • Close on a thread shows beside a channel, and on a second thread in the threads view. The threads view's main thread has none: the user switches views instead.
  • Close on a channel shows only on a channel opened beside another one.

Closing a panel beside the main one closes it together with everything opened on top of it. In the main panel, close removes what's on top, one layer at a time. With nothing stacked on it, close empties the main panel, for example a thread that replaced the channel in a one-slot layout.

useCloseThread() returns the function the thread's close button calls. It closes the thread in context, in the panel it renders in. A custom thread header uses it like this:

import {
  ThreadHeader,
  useCloseThread,
  useThreadContext,
  useWorkspaceNavigation,
  useWorkspacePanel,
} from "stream-chat-react";

const DoneButton = () => {
  const thread = useThreadContext();
  const panel = useWorkspacePanel();
  const { isThreadDismissable } = useWorkspaceNavigation();
  const closeThread = useCloseThread();

  if (!isThreadDismissable(thread?.id, panel)) return null;

  return (
    <button onClick={(event) => closeThread({ event })} type="button">
      Done
    </button>
  );
};

export const MyThreadHeader = () => <ThreadHeader EndContent={DoneButton} />;

A component passed as EndContent or StartContent replaces the SDK's button, so it checks isThreadDismissable itself.

Inside channel details

AvatarWithChannelDetail, from stream-chat-react/channel-detail, makes the channel header's avatar open channel details:

import { ChannelHeader } from "stream-chat-react";
import { AvatarWithChannelDetail } from "stream-chat-react/channel-detail";

export const DetailedChannelHeader = () => (
  <ChannelHeader Avatar={AvatarWithChannelDetail} />
);

Channel details has five sections: Channel info, Members, Pinned messages, Photos & videos and Files. Its section navigator moves between them, in one of two layouts:

  • Wide, when channel details are at least 640px wide: the sections are listed beside the one shown, and picking one shows it in its place.
  • Narrow: one column. The section's header has a menu button that opens the list of sections as a drawer. Escape or a click outside closes it.

Some sections open a screen of their own with a back button: a member's profile from Members, adding members, and editing the channel info. Back returns to the screen it was opened from. A header with a back button shows no menu button.

Selecting a pinned message and Send direct message leave channel details, as described in Opening a channel from elsewhere. The sections and the navigator's props are in Section Navigator.

Used in