Skip to content

Date Separators

Our MessageList/MessageFlashList components render an inline date separator above the first message of each day, so everything below a separator belongs to the day it names.

System messages — "X was added to the channel", channel updates, or any message your backend writes with type: 'system' — are excluded from this by default. That matters when a system message is the first message of a day: it takes the day boundary and renders nothing, and the regular message below it shares the same day, so that day ends up with no separator at all.

If your channels get system messages at day boundaries, enable allowDateSeparatorForSystemMessages on Channel:

<Channel channel={channel} allowDateSeparatorForSystemMessages>
  <MessageList />
  <MessageComposer />
</Channel>

With it on, the separator is rendered above the system message that opens the day, and the regular messages that follow correctly get none — the day is still dated exactly once.

Hiding separators on selected days

Some applications write a system message to mark something other than a membership change — the start of a support session, for example — and would rather not date a day that holds nothing but those markers.

The separator is a component slot, so returning null from it suppresses it for that day. Combine it with allowDateSeparatorForSystemMessages: without the flag there is no separator on those days to suppress in the first place.

import {
  Channel,
  InlineDateSeparator,
  InlineDateSeparatorProps,
  MessageList,
  usePaginatedMessageListContext,
  WithComponents,
} from "stream-chat-react-native";

const DaysWithMessagesOnly = ({ date }: InlineDateSeparatorProps) => {
  const { hasMore, messages } = usePaginatedMessageListContext();

  if (!date) return null;

  const day = date.toDateString();
  const hasRegularMessage = messages.some(
    (message) =>
      message.type !== "system" && message.created_at.toDateString() === day,
  );

  // The oldest loaded day may still gain messages from the next page, so leave it dated until
  // the history behind it is loaded. Otherwise the separator appears mid-scroll after `loadMore`.
  const dayIsPartiallyLoaded =
    day === messages[0]?.created_at.toDateString() && hasMore;

  if (!hasRegularMessage && !dayIsPartiallyLoaded) return null;

  return <InlineDateSeparator date={date} />;
};

export const Chat = () => (
  <WithComponents overrides={{ InlineDateSeparator: DaysWithMessagesOnly }}>
    <Channel channel={channel} allowDateSeparatorForSystemMessages>
      <MessageList />
    </Channel>
  </WithComponents>
);

hasMore and messages both come from PaginatedMessageListContext; the same values are available on the channel as channel.state.messagePagination.hasPrev and channel.state.messages if that suits your structure better.

:::note MessageWrapper wraps whatever the slot renders in a View carrying messageList.inlineDateSeparatorContainer, whose default includes vertical padding. Returning null leaves that padding behind as a gap, so set inlineDateSeparatorContainer: { paddingVertical: 0 } in your theme and apply the spacing inside your own component instead. :::

Performance

The rule above scans the loaded messages on every separator render. On channels that load a lot of history, cache the result per message list instead — the client hands out a new array whenever the list changes, so a WeakMap keyed on it computes once and every separator after that is a lookup:

const dayCache = new WeakMap<readonly LocalMessage[], Set<number>>();

const dayKey = (date: Date) =>
  date.getFullYear() * 10000 + date.getMonth() * 100 + date.getDate();

const daysWithRegularMessages = (messages: readonly LocalMessage[]) => {
  let days = dayCache.get(messages);

  if (!days) {
    days = new Set<number>();
    for (const message of messages) {
      if (message.type !== "system") days.add(dayKey(message.created_at));
    }
    dayCache.set(messages, days);
  }

  return days;
};

Comparing integer day keys rather than toDateString() values is also considerably cheaper, and it matters here because the comparison runs once per loaded message.

Finally, set maximumMessageLimit on Channel to bound how much history stays loaded. It has no default, so a long-lived channel keeps growing and every list update walks all of it.

Turning separators off entirely

To remove inline date separators altogether, use hideDateSeparators on Channel. It takes precedence over allowDateSeparatorForSystemMessages, and hideStickyDateHeader does the same for the floating date header at the top of the list.