# Channel UI

An open channel is a screen of parts: a header, a message list and a composer. `Channel` gives one channel to every part inside it, moves them out of the keyboard's way, and hosts the attachment picker. The parts are yours to choose. This page covers `Channel` itself, keyboard handling, the configuration it shares with `Thread`, the typing indicator and the channel header. Channel details are on [Channel details UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/channel-details-ui/). The props are in the [Channel reference](https://getstream.io/chat/docs/sdk/react-native/component-reference/channel/channel/).

## The Channel component

`<Channel channel={channel}>` binds a channel to its children. It renders, from the outside in:

- `KeyboardCompatibleView`, which resizes the content when the keyboard opens. See [Keyboard and insets](#keyboard-and-insets).
- The contexts its children read: the channel, the user's capabilities in it, the message and composer configuration, the composer, and the channel's own audio players. [Providers and context hooks](https://getstream.io/chat/docs/sdk/react-native/ui/providers-and-context-hooks/) explains them.
- Your children.
- The attachment picker, a bottom sheet that opens from the composer. See [Message composer UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/message-composer-ui/#the-attachment-picker).

When it mounts, and again whenever you pass it a different channel, `Channel`:

1. Sets the page size of the channel's message list to 25 messages, unless you registered one in `client.config` (see [Message paginators](https://getstream.io/chat/docs/sdk/react-native/state/loading-data/message-paginators/)).
2. Watches the channel, unless the channel is already watched and loaded with its latest messages. A channel from a channel list opens without a request. With `initializeOnMount={false}` it doesn't watch, and you call `Channel.watch()` yourself. It never watches, queries or reloads a channel that has no id yet (a provisional channel, see [Provisional channels](https://getstream.io/chat/docs/sdk/react-native/state/state-and-entities/shared-entities/#provisional-channels)).
3. Loads the latest messages when the message list holds none, for example for a channel opened from a push notification.
4. Shows the newest messages when the message list was left on an older page, for example after a jump to a quoted, pinned or searched message. When the newest page is still loaded, this needs no request. It doesn't happen when a jump to a message already landed or is still loading, such as a deep link from the screen, or when `Channel` is about to jump to the first unread message: those decide where the list opens.
5. Takes the channel's unread snapshot from its current read state, which the unread separator and the unread notification render from. A channel the user opened before keeps its loaded messages, so this is what keeps them right when the user comes back to it.
6. With `initialScrollToFirstUnreadMessage`, jumps to the first unread message, unless something already jumped the list to a message.
7. Marks the channel read when it has unread messages, unless `markReadOnMount` is `false`. The unread snapshot stays, so the separator still shows where the new messages start.

With `initializeOnMount={false}`, `Channel` also leaves the list on the page it was left on. `useJumpToLatestOnOpen()`, exported by the SDK, does what step 4 does. It takes no arguments, and a component inside `Channel` or `Thread` calls it, so an app that sets `initializeOnMount={false}` and still wants the newest messages on open renders it there. `Thread` does the same for its replies; see [Thread UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/thread-ui/#the-thread-component).

While it's mounted, `Channel` also:

- activates the channel, so the client keeps it loaded and reloads it after a reconnect. [Entity lifecycle](https://getstream.io/chat/docs/sdk/react-native/state/state-and-entities/entity-lifecycle/) explains activation.
- marks the channel read after a reconnect, when the reloaded list shows the latest messages. It does so only while connection recovery is on, because with recovery off the messages sent during the drop were never loaded.
- sends a `typing.stop` event when the app goes to the background while the user is typing.
- renders the replacement when the channel you passed is superseded by a new object for the same conversation (see [Superseded channels](https://getstream.io/chat/docs/sdk/react-native/state/state-and-entities/entity-lifecycle/#superseded-channels)).

### When it renders something else

| Case                                                  | What `Channel` renders                                                                    | Override key            |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------- |
| The message list has no messages and its query failed | "Error loading messages for this channel...". Tapping it loads the latest messages again. | `LoadingErrorIndicator` |
| The `channel` has no `cid`                            | "Please select a channel first"                                                           | None                    |

The error state replaces everything inside `Channel`, including a header you render as a child. A screen that must keep its header in that state renders the header outside `Channel`.

## Keyboard and insets

`Channel` wraps its content in `KeyboardCompatibleView`, which adds padding at the bottom while the keyboard is open, so the composer stays above it. To place the composer right, it needs to know where `Channel` sits on the screen, and the attachment picker needs to know how far up it may open:

| Prop                     | Set it to                                                                                                                                                                                                 |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `keyboardVerticalOffset` | The distance from the top of the screen to the top of `Channel`: the navigation header's height, such as `useHeaderHeight()` from React Navigation, or `0` when `Channel` starts at the top of the screen |
| `topInset`               | The navigation header's height, when the screen has a native header, so the attachment picker stops below it. Leave it at `0` otherwise.                                                                  |
| `bottomInset`            | The height of anything below `Channel` on the screen, such as a tab bar. `0` by default.                                                                                                                  |

Set `keyboardVerticalOffset` on every screen, `0` included: when you leave it out, `Channel` falls back to a fixed 86.5 points on iOS. Use the same value everywhere the same layout renders.

Three more props change the keyboard handling. `keyboardBehavior` picks how the view makes room: `'padding'` (the default), `'height'` or `'position'`, as in React Native's `KeyboardAvoidingView`. `additionalKeyboardAvoidingViewProps` passes other props to the view. `disableKeyboardCompatibleView` turns it off, for an app that moves the content itself, for example with `react-native-keyboard-controller`.

[Keyboard and insets](https://getstream.io/chat/docs/sdk/react-native/ui/layout/keyboard-and-insets/) covers layouts with headers, tab bars and edge-to-edge Android, and [Chat in modals](https://getstream.io/chat/docs/sdk/react-native/ui/layout/chat-in-modals/) covers channels in a modal.

## Configuration shared with Thread

Besides the props of its own, `Channel` takes configuration for the parts below it. `Thread` takes the same configuration, and neither passes it to the other, so a channel screen and a thread screen each need it.

| Group             | Examples                                                                                                                                                                |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Message list      | `hideDateSeparators`, `hideStickyDateHeader`, `enableMessageGroupingByUser`, `maxTimeBetweenGroupedMessages`, `FlatList`                                                |
| Messages          | `messageActions`, `supportedReactions`, `reactionListPosition`, `messageContentOrder`, `onPressMessage`, `onLongPressMessage`, the `handle*` handlers, `myMessageTheme` |
| Composer          | `audioRecordingEnabled`, `hasCommands`, `hasCreatePoll`, `messageInputFloating`, `additionalTextInputProps`, `setInputRef`                                              |
| Attachment picker | `topInset`, `bottomInset`, `disableAttachmentPicker`, `hasCameraPicker`, `hasFilePicker`, `hasImagePicker`                                                              |
| Keyboard          | `keyboardVerticalOffset`, `keyboardBehavior`, `disableKeyboardCompatibleView`                                                                                           |
| Capabilities      | `overrideOwnCapabilities`                                                                                                                                               |

The props that only `Channel` takes are about loading the channel: `initializeOnMount`, `markReadOnMount` and `initialScrollToFirstUnreadMessage`. `Thread`'s own props are on [Thread UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/thread-ui/).

One way to keep the two in step is a hook that returns the shared props, spread on both:

```tsx title="src/ChatScreens.tsx"
import { useHeaderHeight } from "@react-navigation/elements";
import type {
  Channel as StreamChannel,
  Thread as StreamThread,
} from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageList,
  Thread,
} from "stream-chat-react-native";

// The configuration the channel screen and the thread screen share.
const useSharedChatProps = () => {
  const headerHeight = useHeaderHeight();

  return {
    audioRecordingEnabled: true,
    keyboardVerticalOffset: headerHeight,
    topInset: headerHeight,
  };
};

export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => {
  const sharedProps = useSharedChatProps();

  return (
    <Channel channel={channel} {...sharedProps}>
      <MessageList />
      <MessageComposer />
    </Channel>
  );
};

export const ThreadScreen = ({ thread }: { thread: StreamThread }) => {
  const sharedProps = useSharedChatProps();

  return (
    <Thread thread={thread} {...sharedProps}>
      <MessageList />
      <MessageComposer />
    </Thread>
  );
};
```

## Typing indicator

The channel keeps the latest typing event of each user in `channel.state.typing`. Three places show it:

- **`TypingIndicator`** renders at the newest end of the message list: the avatars of the people typing, up to three, and animated dots. It's part of the list's content, so it shows when the user scrolls to the latest message. `TypingIndicatorContainer` around it renders nothing while nobody is typing. `disableTypingIndicator` on `Channel` or `Thread` turns it off.
- **The channel list row** replaces its message line with who is typing, through `ChannelPreviewTypingIndicator`. See [Channel list UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/channel-list-ui/#channel-previews).
- **`useTypingString()`** returns the text "Cleo is typing", or "Cleo and 2 more are typing", for a header of your own, and an empty string while nobody is typing.

They leave out the current user, and they keep the channel's typing and the threads' typing apart. In a `Channel`, they count the events typed in the channel itself. Inside a `Thread`, they count only the events typed in that thread.

>
> **Info:** **Example**
>
> Bob is typing a reply in the thread of the user's message "Lunch?", and Cleo is typing a new message in the channel:
>
> 1. The channel's message list shows that Cleo is typing. Bob isn't shown there.
> 2. The thread's message list shows that Bob is typing.
> 3. A header with `useTypingString()` reads "Cleo is typing" in the channel, and "Bob is typing" inside the thread.
>

`useTypingUsers()` returns the users typing, by the same rule: the channel's, or the open thread's inside a `Thread`. `useChannelTypingUsers(channel, threadId?)` takes the channel as a parameter, so it works outside `Channel`, for example in a channel list row of your own. Both return the users without the current user.

`AITypingIndicatorView` shows a different state: an AI's reply being thought about or generated in the channel. The SDK doesn't place it; render it above the composer when your channel has an AI participant. See [SDK integration](https://getstream.io/chat/docs/sdk/react-native/features/ai/sdk-integration/).

[Typing indicators](https://getstream.io/chat/docs/sdk/react-native/features/messaging/typing-indicators/) covers the feature end to end: sending typing events and building an indicator of your own.

## The channel header

The SDK has no channel header component. A channel screen gets its header one of two ways:

- **From the navigator.** Set the native header's title, and its buttons, through the screen's options. Pass the header's height to `Channel` as `keyboardVerticalOffset` and `topInset`, as in [Quick start](https://getstream.io/chat/docs/sdk/react-native/getting-started/quick-start/#add-a-channel-list-with-navigation).
- **As the first child of `Channel`.** Hide the navigator's header and render your own above `MessageList`. `Channel` then starts at the top of the screen, so `keyboardVerticalOffset` is `0` and `topInset` stays unset. Inside `Channel`, the header reads the channel with `useChannel()` and can show who is typing.

The SDK's hooks give a header what it shows: `useChannelPreviewDisplayName(channel)` for the name, `ChannelAvatar` for the image, `useChannelMemberCount(channel)` and `useChannelOnlineMemberCount(channel)` for the counts, and `useTypingString()` for typing. They read the channel's stores, so the header updates as the channel changes. This header renders as the first child of `Channel`:

```tsx title="src/ChannelHeader.tsx"
import { Pressable, StyleSheet, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
  ChannelAvatar,
  ChevronLeft,
  PortalWhileClosingView,
  useChannel,
  useChannelMemberCount,
  useChannelOnlineMemberCount,
  useChannelPreviewDisplayName,
  useTypingString,
} from "stream-chat-react-native";

type ChannelHeaderProps = {
  onBack: () => void;
  onOpenDetails: () => void;
};

export const ChannelHeader = ({
  onBack,
  onOpenDetails,
}: ChannelHeaderProps) => {
  const channel = useChannel();
  const { top } = useSafeAreaInsets();
  const name = useChannelPreviewDisplayName(channel);
  const memberCount = useChannelMemberCount(channel);
  const onlineCount = useChannelOnlineMemberCount(channel);
  const typing = useTypingString();

  return (
    // Keeps the header above a message that animates back when the message menu closes.
    <PortalWhileClosingView
      portalHostName="channel-header"
      portalName="channel-header"
    >
      <View style={[styles.container, { paddingTop: top }]}>
        <Pressable
          accessibilityLabel="Back"
          accessibilityRole="button"
          onPress={onBack}
        >
          <ChevronLeft height={24} width={24} />
        </Pressable>
        <View style={styles.title}>
          <Text numberOfLines={1} style={styles.name}>
            {name}
          </Text>
          <Text numberOfLines={1}>
            {typing || `${memberCount} members, ${onlineCount} online`}
          </Text>
        </View>
        <Pressable
          accessibilityLabel="Channel details"
          accessibilityRole="button"
          onPress={onOpenDetails}
        >
          <ChannelAvatar channel={channel} size="lg" />
        </Pressable>
      </View>
    </PortalWhileClosingView>
  );
};

const styles = StyleSheet.create({
  container: {
    alignItems: "center",
    flexDirection: "row",
    gap: 8,
    paddingBottom: 8,
    paddingHorizontal: 12,
  },
  name: { fontSize: 16, fontWeight: "600" },
  title: { flex: 1 },
});
```

`PortalWhileClosingView` matters for a header inside the screen. When the message menu closes, the message animates back into the list, and the wrapper keeps the header drawn above it. The SDK wraps the composer the same way. [Overlays and bottom sheets](https://getstream.io/chat/docs/sdk/react-native/ui/components/overlays-and-bottom-sheets/) explains the message overlay.

The [Channel header UI cookbook](https://getstream.io/chat/docs/sdk/react-native/ui-cookbooks/headers/channel-header/) adds an unread badge on the back button and a connection status. The avatar button in this header is where an app opens channel details, which [Channel details UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/channel-details-ui/) explains.

## Replacing parts

`Channel` renders its children, so you replace a part of the channel screen by rendering a different component in its place: a header of your own, `MessageFlashList` instead of `MessageList`, or a composer of your own.

The parts on this page that render from an override key:

| Override key                                  | Replaces                                                                                                             |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `KeyboardCompatibleView`                      | The view that makes room for the keyboard. Gets `behavior`, `enabled`, `keyboardVerticalOffset` and the extra props. |
| `LoadingErrorIndicator`                       | The error state above, and the first-page error of the channel list                                                  |
| `TypingIndicator`, `TypingIndicatorContainer` | The typing indicator in the message list, and the container that hides it while nobody is typing                     |

The message list, the composer and the attachment picker have keys of their own, listed on [Message list UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/message-list-ui/#replacing-parts) and [Message composer UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/message-composer-ui/#replacing-parts). [Overriding components](https://getstream.io/chat/docs/sdk/react-native/ui/components/overriding-components/) explains how overrides apply.

## Used in

- [Typing indicators](https://getstream.io/chat/docs/sdk/react-native/features/messaging/typing-indicators/)
- [Keyboard and insets](https://getstream.io/chat/docs/sdk/react-native/ui/layout/keyboard-and-insets/)
- [Channel header](https://getstream.io/chat/docs/sdk/react-native/ui-cookbooks/headers/channel-header/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/ui/components/channel-ui/](https://getstream.io/chat/docs/sdk/react-native/ui/components/channel-ui/).