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

Pinned messages

Pinning keeps an important message easy to find. This page covers how a message gets pinned, how the channel keeps its list of pinned messages, and the components that show them.

What users get

The user opens the menu on a message and picks Pin to Conversation. A line with a pin icon above the message now reads "Pinned by" and the user's name, or "Pinned by You" on their own screen. Picking Unpin from Conversation removes it.

ChannelDetails has a Pinned messages row. It opens a full-screen list of the channel's pinned messages with their author and date, and a search field filters them by text. When there are none, the list says "Long-press a message to pin it to the chat".

A pinned message with a line reading Pinned by You above it and a highlighted row on iOS
A pinned message with a line reading Pinned by You above it and a highlighted row on Android

How it works across the layers

A pin is part of the message. A pinned message has pinned: true, pinned_at, pinned_by and, when the pin expires, pin_expires.

  • State. client.pinMessage and client.unpinMessage change the message on the server. The message.updated event then brings the change to every client watching the channel, and each channel keeps its pinned messages in channel.pinnedMessagesPaginator.
  • UI. MessageHeader renders MessagePinnedHeader above any message with message.pinned. The Pin and Unpin actions in the message menu call the client. PinnedMessageList is the list behind the Pinned messages row of ChannelDetails.
  • Navigation. Not involved. The items of PinnedMessageList don't open the message.

The pinned list

channel.pinnedMessagesPaginator holds the channel's pinned messages, oldest pin first. It's a paginator like the others in the state layer: its state store has items, isLoading, hasMoreTail, hasMoreHead and lastQueryError. How pagination works explains these fields.

Info:

Example

Take a channel with 25 pinned messages:

  1. When the channel is queried or watched, the response carries a page of its pinned messages. They become the first page of the list.
  2. PinnedMessagePaginator.toTail() loads the next page of earlier pins from the pinned messages endpoint.
  3. Bob pins another message. The message.updated event arrives, the message now has pinned: true, and the list adds it. When he unpins it, the next message.updated removes it.
  4. A pinned message that is edited, reacted to or deleted changes in the list too, because the list and the message list share each message.

Thread replies stay out of the list, unless a reply was also sent to the channel. A pinned reply still shows "Pinned by" in its thread. Truncating the channel also clears the pins from before the cutoff.

Pinning and unpinning

Pass the message, or { id }. The second argument sets when the pin expires, as a Date or an RFC 3339 string, and pinned_at, the pin time that orders the pinned list, which defaults to now:

// Pinned until someone unpins it
await client.pinMessage(message);

// Expires after an hour
await client.pinMessage(message, {
  pin_expires: new Date(Date.now() + 60 * 60 * 1000),
});

// Expires at an exact time
await client.pinMessage(message, { pin_expires: "2026-12-31T23:00:00Z" });

await client.unpinMessage(message);

Neither call changes local state. The message and the pinned list update when message.updated arrives. The Pin action does the same, and shows a "Message pinned" notification once the request succeeds, or an error notification when it fails.

Turning it on or off

Pinning is always available. The server decides who may pin through the pin-message capability, which comes from the channel type's permissions. See Permissions reference.

The message menu shows Pin to Conversation and Unpin from Conversation only to users with pin-message, on channel messages and thread replies alike. To hide them from users who have it, pass overrideOwnCapabilities={{ pinMessage: false }} to Channel and Thread, or remove the pinMessage and unpinMessage actions, as Message actions shows.

Customizing

The pinned header

Replace MessagePinnedHeader through WithComponents. It reads the message from useMessageContext():

import { StyleSheet, Text, View } from "react-native";
import {
  Pin,
  useMessageContext,
  WithComponents,
} from "stream-chat-react-native";

const PinnedLabel = () => {
  const { message } = useMessageContext();

  if (!message?.pinned) return null;

  return (
    <View style={styles.row}>
      <Pin height={12} width={12} />
      <Text style={styles.text}>
        {message.pinned_by?.name ?? "Someone"} pinned this
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  row: { alignItems: "center", flexDirection: "row", gap: 4 },
  text: { fontSize: 12 },
});

export const App = () => (
  <WithComponents overrides={{ MessagePinnedHeader: PinnedLabel }}>
    {/* the rest of the app */}
  </WithComponents>
);

MessageHeader renders the pinned header together with the reminder header and the "Also sent in channel" header. Replace MessageHeader to change how they're laid out. The Pin indicator UI cookbook restyles the pinned header and replaces it.

Your own pinned list

To show pins outside channel details, for example in a bar above the message list, read channel.pinnedMessagesPaginator.state. Place this component inside Channel:

import { Pressable, Text, View } from "react-native";
import type { LocalMessage, PaginatorState } from "stream-chat";
import { useChannel, useStateStore } from "stream-chat-react-native";

const pinnedListSelector = ({
  hasMoreTail,
  isLoading,
  items,
}: PaginatorState<LocalMessage>) => ({ hasMoreTail, isLoading, items });

export const PinnedBar = () => {
  const channel = useChannel();
  const {
    hasMoreTail,
    isLoading,
    items = [],
  } = useStateStore(channel.pinnedMessagesPaginator.state, pinnedListSelector);

  if (!items.length) return null;

  return (
    <View>
      {items.map((message) => (
        <Pressable
          key={message.id}
          onPress={() =>
            void channel.messagePaginator.jumpToMessage(message.id)
          }
        >
          <Text numberOfLines={1}>
            {message.user?.name}: {message.text}
          </Text>
        </Pressable>
      ))}
      {hasMoreTail ? (
        <Pressable
          disabled={isLoading}
          onPress={() => void channel.pinnedMessagesPaginator.toTail()}
        >
          <Text>Earlier pins</Text>
        </Pressable>
      ) : null}
    </View>
  );
};

The list keeps itself current, so the bar re-renders when someone pins or unpins a message. Reading state in components explains the selector rules this sample follows. Message list UI covers what the list shows after a jump.

The Pinned messages list

PinnedMessageList loads its list through a MessageSearchSource, 25 messages per page, filtered to the channel's pinned messages and searched by text. It takes a searchSource prop to use a search source of your own, and searchInputProps and additionalFlatListProps for its parts. PinnedMessageList and PinnedMessageItem are override keys. To change the rows of channel details, see Channel details and members.

Reference