Custom Message UI

Overview

Channel accepts the following props for overriding message components:

Best Practices

  • Prefer theming or subcomponent overrides before replacing MessageSimple.
  • Keep custom message components memoized to protect FlatList performance.
  • Use useMessageContext for per-message data and handlers instead of prop drilling.
  • Override only the pieces you need (e.g., MessageText, MessageAvatar) to reduce surface area.
  • Test with reactions, attachments, and threads to confirm layout consistency.

Message

This higher-order component wraps the UI component (MessageSimple) and provides handlers.

You usually don't need to customize this unless you want custom handlers for actions/gestures. Use the Message component as a reference, and memoize for FlatList performance.

<OverlayProvider bottomInset={bottom} i18nInstance={streami18n}>
  <Chat client={chatClient} i18nInstance={streami18n}>
    <Channel
      channel={channel}
      keyboardVerticalOffset={headerHeight}
      Message={CustomMessageComponent}
    >
      <View style={{ flex: 1 }}>
        <MessageList />
        <MessageInput />
      </View>
    </Channel>
  </Chat>
</OverlayProvider>

MessageSimple

This is the actual message bubble UI. You can access handlers from the Message HOC via useMessageContext.

import {
  OverlayProvider,
  Chat,
  Channel,
  MessageList,
  MessageInput,
  useChannelContext,
  useMessagesContext,
  useMessageContext,
} from "stream-chat-react-native";

const CustomMessageUIComponent = () => {
  const { channel, members, watchers, error, loading } = useChannelContext();
  const { handleDeleteMessage, handleResendMessage, message, reactions } =
    useMessageContext();

  /** Custom implementation */
};

<OverlayProvider bottomInset={bottom} i18nInstance={streami18n}>
  <Chat client={chatClient} i18nInstance={streami18n}>
    <Channel
      channel={channel}
      keyboardVerticalOffset={headerHeight}
      MessageSimple={CustomMessageUIComponent}
    >
      <View style={{ flex: 1 }}>
        <MessageList />
        <MessageInput />
      </View>
    </Channel>
  </Chat>
</OverlayProvider>;

To customize specific parts of MessageSimple, pass these props on Channel:

<Channel
  channel={channel}
  keyboardVerticalOffset={headerHeight}
  MessageAvatar={CustomAvatarComponent}
  MessageText={CustomTextComponent}
>

Message bubble with custom text styles & fonts

We use react-native-simple-markdown in Message to render markdown, so styling differs from a plain Text component.

Theme styles for markdown live under messageSimple -> content -> markdown. You can override individual markdown sub-component styles.

const themeStyle = {
  messageSimple: {
    content: {
      markdown: {
        heading1: {
          color: "pink",
        },
        inlineCode: {
          fontSize: 10,
        },
      },
    },
  },
};

<Chat style={themeStyle}>...</Chat>;

Markdown keys available for styling:

export type MarkdownStyle = Partial<{
  autolink: TextStyle;
  blockQuoteBar: ViewStyle;
  blockQuoteSection: ViewStyle;
  blockQuoteSectionBar: ViewStyle;
  blockQuoteText: TextStyle | ViewStyle;
  br: TextStyle;
  codeBlock: TextStyle;
  del: TextStyle;
  em: TextStyle;
  heading: TextStyle;
  heading1: TextStyle;
  heading2: TextStyle;
  heading3: TextStyle;
  heading4: TextStyle;
  heading5: TextStyle;
  heading6: TextStyle;
  hr: ViewStyle;
  image: ImageStyle;
  inlineCode: TextStyle;
  list: ViewStyle;
  listItem: ViewStyle;
  listItemBullet: TextStyle;
  listItemNumber: TextStyle;
  listItemText: TextStyle;
  listRow: ViewStyle;
  mailTo: TextStyle;
  mentions: TextStyle;
  newline: TextStyle;
  noMargin: TextStyle;
  paragraph: TextStyle;
  paragraphCenter: TextStyle;
  paragraphWithImage: ViewStyle;
  strong: TextStyle;
  sublist: ViewStyle;
  table: ViewStyle;
  tableHeader: ViewStyle;
  tableHeaderCell: TextStyle;
  tableRow: ViewStyle;
  tableRowCell: ViewStyle;
  tableRowLast: ViewStyle;
  text: TextStyle;
  u: TextStyle;
  view: ViewStyle;
}>;

Message bubble with full width

Because the default bubble is complex (reactions, attachments, etc.), full-width bubbles are not supported out of the box. You can theme them at the app level:

import { vw } from "stream-chat-react-native";

const maxWidth = vw(100) - 72;
const themeStyle = {
  messageSimple: {
    card: {
      container: {
        width: maxWidth,
      },
    },
    content: {
      container: {
        width: maxWidth,
      },
      textContainer: {
        width: maxWidth,
        maxWidth: maxWidth,
      },
      wrapper: { width: maxWidth },
    },
    gallery: {
      // -2 because of the 2px border
      width: maxWidth - 2,
    },
    giphy: {
      container: {
        width: maxWidth,
      },
    },
  },
  // Override reply so the reuse of message style does not overflow text in the message input
  reply: {
    textContainer: {
      maxWidth: undefined,
      width: undefined,
    },
  },
};

<Chat style={themeStyle}>...</Chat>;

Message bubble without border

Simple style customization using theme object, does the trick:

const themeStyle = {
  messageSimple: {
    content: {
      container: {
        borderWidth: 0,
      },
      containerInner: {
        borderWidth: 0,
      },
      deletedContainerInner: {
        borderWidth: 0,
      },
      textContainer: {
        borderWidth: 0,
      },
    },
  },
};

<Chat style={themeStyle}>...</Chat>;

Instagram style double-tap reaction

Double tap handler can be easily attached to a message component using react-native-gesture-handler. Please check the following example for implementation.

  • v3 of React Native Chat SDK used to support a prop onDoubleTapMessage, but it has been dropped since v4 of SDK for performance reasons.
  • Gesture handlers and animations inside a dynamic list may result in increased memory consumption.
const CustomMessage = (props: MessageSimpleProps) => {
  const doubleTapRef = useRef(null);
  const { message, handleToggleReaction } = useMessageContext();

  const _onDoubleTap = useAnimatedGestureHandler<TapGestureHandlerGestureEvent>(
    {
      onActive: () => {
        console.log('double tapped')
        runOnJS(handleToggleReaction)('love')
      },
    },
    [message],
  );

  return (
    <TapGestureHandler
      waitFor={doubleTapRef}>
      <Animated.View>
        <TapGestureHandler
          ref={doubleTapRef}
          numberOfTaps={2}
          onHandlerStateChange={_onDoubleTap}>
          <Animated.View>
            <MessageSimple {...props} />
          </Animated.View>
        </TapGestureHandler>
      </Animated.View>
    </TapGestureHandler>
  );
}

<Channel
  channel={channel}
  MessageSimple={CustomMessage}
>

Slack style messages all on the left side

By default, received messages are shown on left side of the MessageList and sent messages are shown on right side of the MessageList.

You can change this at the Message level via the prop forceAlignMessages or set the alignment for the entire Channel using the same forceAlignMessages prop.

<Channel
  channel={channel}
  forceAlignMessages='left'
  keyboardVerticalOffset={headerHeight}
  thread={thread}
>

Message bubble with name of sender

In group messaging it's important to show the name of the sender associated message bubble - similar to Slack or WhatsApp. By default this is done in the MessageFooter component. This component is fully replaceable via props on Channel and is provided a set of props itself, MessageFooterProps, that can be used for rendering. Any additional data for rendering a custom footer can be pulled from contexts such as the MessageContext via the useMessageContext hook.

If you wanted to move the information about the sender to the top of the message you can provide a MessageHeader component to Channel which is provided the same props, MessageHeaderProps, as the footer, MessageFooterProps, and again can utilize the contexts as needed.

<Channel
  channel={channel}
  keyboardVerticalOffset={headerHeight}
  MessageHeader={(props) =>
    props.message?.user?.id !== chatClient.userID ? (
      <View
        style={{ flexDirection: 'row' }}
      >
        {Object.keys(props.members).length > 2 &&
          props.message.user?.name ? (
            <Text style={[{ color: grey, marginRight: 8 }]}>
              {props.message.user.name}
            </Text>
          ) : null}
        <Text style={[{ color: grey, textAlign: props.alignment }]}>
          {props.formattedDate}
        </Text>
      </View>
    ) : null
  }
  MessageFooter={() => null}
  thread={thread}
>

StandardFooter

NoFooter

HeaderAdded

Standard FooterNo FooterHeader Added

Swipe message left to delete and right to reply

We support a default swipe right to reply gesture on the message component. This can be disabled by passing false to the enableSwipeToReply prop on the Channel component.

The default content on the left side when you swipe right can be customized by passing a custom component to the MessageSwipeContent prop on the Channel component.

const CustomMessageSwipeContent = () => {
  return (
    <View>
      <Text>Reply</Text>
    </View>
  );
};

<Channel
  channel={channel}
  MessageSwipeContent={CustomMessageSwipeContent}
>

MessageSwipeContent

To add more advanced swipe controls to your messages, it is suggested that you create a custom Message component to replace the default one. An easy solution is to wrap the standard exported message component from stream-chat-react-native in a Swipeable from react-native-gesture-handler/Swipeable. You can then use the functions provided by Swipeable to fine tune to functionality to your liking.

You can add reply functionality by calling setQuotedMessageState, available from the useMessagesContext hook. Or you can delete the message using a combination of client.deleteMessage and updateMessage, the latter of which is also available from the useMessagesContext hook. You can find the internal implementation of these functions in the Message component; or you can add any other functionality you like. It is suggested to add custom logic when implementing swipe-able messages to ensure you only can swipe appropriate messages, i.e. you can only swipe to delete messages you have the ability to delete and have not yet been deleted. Using Message props and contexts this is easily achievable.

const SwipeableMessage = (props: MessageProps) => {
  return (
    <Swipeable
      onSwipeableLeftOpen={reply(props.message)}
      onSwipeableRightOpen={delete props.message}
      overshootLeft={false}
      overshootRight={false}
      renderLeftActions={(progress) => (
        <Animated.View
          style={{
            backgroundColor: "blue",
            transform: [
              {
                translateX: progress.interpolate({
                  inputRange: [0, 1],
                  outputRange: [-100, 0],
                }),
              },
            ],
            width: 100,
          }}
        />
      )}
      renderRightActions={(progress) => (
        <Animated.View
          style={{
            justifyContent: "center",
            opacity: progress.interpolate({
              inputRange: [0, 1],
              outputRange: [0, 1],
            }),
          }}
        >
          <StreamReaction />
        </Animated.View>
      )}
    >
      <Message {...props} />
    </Swipeable>
  );
};

SwipingOpacity

SwipingOpen

SwipingTranslateX

Swiping partially open (opacity partial)Swiping all the way openSwiping using transform -> translateX

Hiding deleted messages from the channel

The deletedMessagesVisibilityType prop in the Channel component enables changing the visibility of the deleted messages to both sender and receiver, none of them or either of them.

<Channel
  ...
  deletedMessagesVisibilityType="sender"
>