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. Use this page to know what each one opens and where it lands, and how to do the same from your own components.

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(). Here is an AllThreadsButton component that switches to the threads view:

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. Here is a ReplyInThreadButton component, for a custom message UI, that 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 register it with <WithComponents/>. Here is a LoggedAlsoSentIndicator component that logs which way the user went:

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 that channel.
  • 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. The channel type is the directMessagingChannelType prop of <Search/>, 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.

Opening a result explains what opening a search result changes in state, such as watching the channel and adding it to the channel lists.

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. What the list does on screen during a jump is in Jumps on screen. 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. Here is a ShowMessageButton component that jumps to one message:

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, in a layout that renders its slots with <Slot/>, a channel opened beside the open one while a thread is there covers that thread. Back reveals the thread again; Layers explains what that does to the thread.
  • Close on a thread shows in the channels view, wherever the thread is, 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. Here is a MyThreadHeader component whose close button reads Done:

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

Channel details opens from the channel header's avatar, and moves between its sections itself: Channel details UI explains both. Two of its actions leave it: selecting a pinned message, and Send direct message on a member's profile. They open a channel, as described in Opening a channel from elsewhere.

Used in