ScrollToBottomButton

ScrollToBottomButton is the round button that floats over the bottom of the message list while the user isn't at the latest message. In a channel, its badge shows the user's unread count. MessageList and MessageFlashList render it through the ScrollToBottomButton override key and pass it what a press does. Floating controls explains when it shows.

Best practices

  • Replace it with the ScrollToBottomButton override key, and call the onPress the list passes. The list decides what a press does: scroll down, or jump to the latest messages when they aren't loaded, then mark the channel read.
  • Return null when showNotification is false. MessageFlashList keeps the button mounted and passes false while it should be hidden.
  • Read the unread count from the channel's read state, as the default does, rather than counting messages yourself.

Basic usage

This replacement shows a text button with the unread count:

src/JumpToLatestButton.tsx
import { Pressable, StyleSheet, Text } from "react-native";
import type { ReadState } from "stream-chat";
import {
  type ScrollToBottomButtonProps,
  useChannel,
  useChatContext,
  useStateStore,
  useThreadContext,
  WithComponents,
} from "stream-chat-react-native";

const selectRead = (state: ReadState) => ({ read: state.read });

const JumpToLatestButton = ({
  onPress,
  showNotification = true,
}: ScrollToBottomButtonProps) => {
  const { client } = useChatContext();
  const channel = useChannel();
  const { thread } = useThreadContext();
  const { read } = useStateStore(channel.state, selectRead);
  const unreadCount = client.userId
    ? (read[client.userId]?.unread_messages ?? 0)
    : 0;

  if (!showNotification) {
    return null;
  }

  return (
    <Pressable
      accessibilityRole="button"
      onPress={onPress}
      style={styles.button}
    >
      <Text style={styles.label}>
        {!thread && unreadCount ? `${unreadCount} new` : "Latest"}
      </Text>
    </Pressable>
  );
};

const overrides = { ScrollToBottomButton: JumpToLatestButton };

export const ListWithJumpButton = ({
  children,
}: {
  children: React.ReactNode;
}) => <WithComponents overrides={overrides}>{children}</WithComponents>;

const styles = StyleSheet.create({
  button: {
    backgroundColor: "#005fff",
    borderRadius: 16,
    paddingHorizontal: 12,
    paddingVertical: 6,
  },
  label: { color: "white", fontWeight: "600" },
});

The list positions the button absolutely, 16 points from the right, and above the composer when the composer floats. Change the position through theme.messageList.scrollToBottomButtonContainer.

What the default renders

  • An outlined icon button with the Down icon from the icons override map.
  • In a channel, a badge with the user's unread count, from channel.state's read state, while the count is above zero. In a thread, no badge.
  • Its accessibility label is messageList.scrollToBottom.accessibilityLabel, or messageList.scrollToBottom.withCount.accessibilityLabel with the count. The keys are exported as SCROLL_TO_BOTTOM_ACCESSIBILITY_LABEL_KEY and SCROLL_TO_BOTTOM_WITH_COUNT_ACCESSIBILITY_LABEL_KEY.

With accessibility turned on, the list also gets a scroll-to-bottom accessibility action while the button shows, so screen reader users reach the same jump without finding the button.

Props

Prop Type Default Description
onPress () => void required Called when the user presses the button. The list passes a function that scrolls to the newest message, or jumps to the latest messages when they aren't loaded, hides the button, and in a channel marks the channel read with updateChannelUnreadState: false, so the unread separator stays where it was.
showNotification boolean true Whether the button renders. MessageList mounts the button only while it should show; MessageFlashList mounts it always and passes the visibility here.
  • useChannel() and useThreadContext() tell the button whether it's in a thread.
  • useOwnUnreadCount(channel) returns the current user's unread count in a channel, kept current as reads arrive, or undefined without a channel. The default button's badge uses it.
  • useStateStore(channel.state, selector) reads the channel's read state, as in the sample above.
  • useMessagePaginator() returns the paginator the list renders. Its hasMoreHead is true while the latest messages aren't loaded, and MessagePaginator.jumpToTheLatestMessage() loads them. See Message paginator.