Skip to main content

Guides

Chat List​

Most apps are a list of chats plus the chats themselves. ChatList is the list: map your backend's conversations to ChatListItem and wire the callbacks, and you get a messenger's main screen:

  • a header with search, and a selection bar (pin, mute, archive, delete) after a long-press
  • an "Archived Chats" folder, and swipe to archive
  • pinned chats first, which you drag into a new order (with reorder handles while selecting)
  • unread counters, typing and drafts, a loading placeholder and a new-chat button
import { Chat, ChatList, ChatListItem } from '@kesha-antonov/react-native-chat'

const chats: ChatListItem[] = conversations.map(c => ({
id: c.id,
title: c.name,
avatar: c.photoUrl,
kind: c.isGroup ? 'group' : 'private',
lastMessage: c.lastMessage, // an IMessage: preview, time and your sent/read checks
unreadCount: c.unread,
isPinned: c.pinned, // pinned chats show first, in the order you pass them
isMuted: c.muted,
isArchived: c.archived,
isOnline: c.online, // green dot on the avatar
isVerified: c.verified, // check badge after the name
typingUsers: c.typing, // User[]
typingAction: c.action, // 'typing' | 'recordingVoice' | 'sendingPhoto' | ...
draft: drafts[c.id],
}))

<ChatList
chats={chats}
user={me}
isLoading={!loaded}
onPressChat={chat => navigation.navigate('Chat', { id: chat.id })}
onPressNewChat={() => navigation.navigate('NewChat')}
onPinChats={(selected, isPinned) => api.pin(selected.map(c => c.id), isPinned)}
onMuteChats={(selected, isMuted) => api.mute(selected.map(c => c.id), isMuted)}
onArchiveChats={(selected, isArchived) => api.archive(selected.map(c => c.id), isArchived)}
onDeleteChats={selected => api.delete(selected.map(c => c.id))}
onReorderPinnedChats={ids => api.setPinnedOrder(ids)}
/>

Each action appears only when its callback is passed. The list is controlled: update chats from your backend and it follows. For a prototype, useChatList keeps the chats in local state and returns the handlers:

const list = useChatList(initialChats)
<ChatList chats={list.chats} user={me} {...list.handlers} onPressChat={openChat} />
// later: list.receiveMessage(chatId, message), list.markRead(chatId), list.upsertChat(chat)
PropWhat it does
chatsEvery chat, archived ones included
userYou: "You:" in groups and the checks on your own last messages
onPressChat / onLongPressChatA tap; a long-press (default: select)
onPinChats / onMuteChats / onArchiveChats / onDeleteChatsSelection actions; archive also by swiping a row
onReorderPinnedChatsTurns on dragging pinned chats; receives the new order of ids
onPressNewChatShows the new-chat button
isLoadingThe loading placeholder
isSortEnabledPinned first, then by newest message (default true)
isDeleteConfirmed / isSwipeEnabledAsk before deleting; swipe to archive (both default true)
isHeaderVisible / title / renderHeaderRight / isTopInsetAppliedThe built-in header (hide it to use your navigator's)
headerRightPositionrenderHeaderRight before the search button (default) or 'afterSearch', e.g. for a close button
timeFormat / dateFormatdayjs formats for a row's time today ('HH:mm' for a 24-hour clock) and its date before this week ('DD.MM.YY')
isSearchEnabled / filterChat / onSearchChangeSearch; a custom match; a server-side search
isArchiveOpen / onArchiveOpenChangeControl which list shows
renderChat / renderEmpty / listProps / styleA custom row (ChatListCell is the built-in one), the empty text, FlatList props (onEndReached, refreshControl...)
theme / darkTheme / colorScheme / locale / labels / iconsAs on Chat; the list's colors are theme.chatList

ChatListCell, ChatListAvatar, ChatListShimmer, sortChats, getChatPreview and formatChatListDate are exported for building your own row or screen.

ChatHeader is the chat screen's bar to match: back arrow, a 42 avatar, the name with a status line under it ("online" and "typing" in the accent color) and your buttons at the end, in theme.chatList's header colors.

<ChatHeader
title='Anna Petrova'
subtitle={isOnline ? 'online' : 'last seen recently'}
isSubtitleActive={isOnline}
typingUsers={typingUsers}
avatar={{ id: chat.id, uri: chat.avatar }}
onBack={navigation.goBack}
onPressTitle={openProfile}
renderRight={() => <CallButton />}
/>

Reply to Messages​

Swipe-to-reply, reply preview and threading

Swipe on a message to reply to it, showing a reply preview in the input toolbar and the replied message above the new bubble. Uses ReanimatedSwipeable from react-native-gesture-handler plus react-native-reanimated.

Basic Usage​

<Chat
messages={messages}
onSend={onSend}
user={{ _id: 1 }}
reply={{
swipe: {
isEnabled: true,
direction: 'left', // swipe left to reply
},
}}
/>

Replying works out of the box: long-press a message and pick Reply, or swipe it left. The swipe takes over from the list after 0.4cm (about 25pt) of mostly sideways travel, the row follows the finger up to 80pt, a reply arrow fades in from 20pt and fills by 50pt, where a light haptic ticks; letting go past 50pt starts the reply, and the row slides back over 180ms. Turn the swipe off with reply={{ swipe: { isEnabled: false } }}, or all replying with reply={{ isEnabled: false }}.

Tapping a quote inside a bubble scrolls to the quoted message and highlights its row for a second, then fades it out over 300ms. The scroll-to-bottom button then comes up, and its first tap takes you back to the message you tapped the quote in. Pass reply.onPress(replyMessage, message) to do something else.

Reply Props (Grouped)​

interface ReplyProps<TMessage> {
isEnabled?: boolean // The menu's Reply and swipe-to-reply; default true
onReply?: (message: TMessage) => void // A reply started (menu or swipe); use with a controlled `message`

// Swipe gesture configuration
swipe?: {
isEnabled?: boolean // Swipe-to-reply; default true (false to turn it off)
direction?: 'left' | 'right' // Swipe direction; default 'left'
onSwipe?: (message: TMessage) => void // Callback when swiped
renderAction?: ( // Custom swipe action component
progress: SharedValue<number>,
translation: SharedValue<number>,
position: 'left' | 'right'
) => React.ReactNode
actionContainerStyle?: StyleProp<ViewStyle>
}

// Reply preview styling (above input toolbar)
previewStyle?: {
containerStyle?: StyleProp<ViewStyle>
textStyle?: StyleProp<TextStyle>
imageStyle?: StyleProp<ImageStyle>
}

// In-bubble reply styling
messageStyle?: {
containerStyle?: StyleProp<ViewStyle>
containerStyleLeft?: StyleProp<ViewStyle>
containerStyleRight?: StyleProp<ViewStyle>
textStyle?: StyleProp<TextStyle>
textStyleLeft?: StyleProp<TextStyle>
textStyleRight?: StyleProp<TextStyle>
imageStyle?: StyleProp<ImageStyle>
}

// Callbacks and state
message?: ReplyMessage // Controlled reply state
onClear?: () => void // Called when reply cleared
onPress?: (message: ReplyMessage) => void // Quote tapped; default jumps to the quoted message and flashes it

// Custom renderers
renderPreview?: (props: ReplyPreviewProps) => React.ReactNode
renderMessageReply?: (props: MessageReplyProps) => React.ReactNode
}

ReplyMessage Structure​

interface ReplyMessage {
_id: string | number
text: string
user: User
image?: string
audio?: string
}

Advanced Example with External State​

const [replyMessage, setReplyMessage] = useState<ReplyMessage | null>(null)

<Chat
messages={messages}
onSend={messages => {
const newMessages = messages.map(msg => ({
...msg,
replyMessage: replyMessage || undefined,
}))
setMessages(prev => Chat.append(prev, newMessages))
setReplyMessage(null)
}}
user={{ _id: 1 }}
reply={{
swipe: {
isEnabled: true,
direction: 'right',
onSwipe: setReplyMessage,
},
message: replyMessage,
onClear: () => setReplyMessage(null),
onPress: (msg) => scrollToMessage(msg._id),
}}
/>

Smooth Animations​

The reply preview animates automatically - it expands from zero height with a fade-in when it appears, collapses with a fade-out when cleared, and transitions smoothly when you reply to a different message. These use react-native-reanimated for 60fps performance.

Maintaining Scroll Position (AI Chatbots)​

Keep the reading position while long answers stream in

The list follows new content only while it is at the newest message. Scrolled up, the reading position stays put while a long answer streams in or new messages arrive, and the scroll-to-bottom button counts them. Both are Legend List behaviours (maintainScrollAtEnd, maintainVisibleContentPosition) that the chat turns on; override them through listProps:

// Never follow new content, even at the newest message
<Chat listProps={{ maintainScrollAtEnd: false }} />

Streaming (AI) Messages​

Render AI assistant replies token-by-token. Incoming chunks are batched with requestAnimationFrame (one render per frame, only the streaming bubble re-renders) and a blinking caret shows while a message is still streaming.

AI reply streaming token-by-token and rendering as markdown, with a blinking caret and a Stop button in the composer

useStreamingMessages, stop control and a full example

The reply above streams in token-by-token (note the caret ▋) and renders as markdown - bold, italics, lists, inline and fenced code - while the app swaps the send button for a Stop control mid-stream (with renderSend, below). See Markdown rendering for AI replies to enable markdown.

  • IMessage.streaming - flag a message as streaming (shows the caret)
  • useStreamingMessages(...) - owns the message list, rAF-batches push(), and supports stop via AbortController. It returns { messages, setMessages, append, startStream, isStreaming, stop }; setMessages is there for anything the hook does not cover, so you can patch a message with a plain map.
import { useCallback } from 'react'
import { Chat, IMessage, Send, SendProps, useStreamingMessages } from '@kesha-antonov/react-native-chat'

function Bot () {
const { messages, append, startStream, isStreaming, stop } = useStreamingMessages<IMessage>()

const onSend = useCallback((newMessages: IMessage[] = []) => {
append(newMessages[0]) // show the user's message
const stream = startStream({ user: { _id: 2, name: 'Assistant' } }) // empty streaming bubble

runMyModel(newMessages[0].text, {
signal: stream.signal, // aborts when stop() is called
onToken: token => stream.push(token), // batched, one render per frame
onDone: () => stream.done(), // clears the streaming flag
})
}, [append, startStream])

// While a reply streams, the send button becomes a Stop button.
const renderSend = useCallback(
(sendProps: SendProps<IMessage>) =>
isStreaming ? <MyStopButton onPress={stop} /> : <Send {...sendProps} />,
[isStreaming, stop]
)

return <Chat messages={messages} onSend={onSend} user={{ _id: 1 }} renderSend={renderSend} />
}

See docs/STREAMING.md for the full hook API and a real Claude streaming adapter (via a backend proxy). A runnable demo lives in example/components/chat-examples/AIBotExample.tsx.

Markdown rendering for AI replies​

Built-in renderer, or react-native-streamdown

AI/LLM replies are usually markdown (bold, lists, fenced code). Enable markdown with messageTextProps={{ markdown: true }} (streamed messages auto-render as markdown unless you pass markdown={false}):

// Force markdown for every message:
<Chat messageTextProps={{ markdown: true }} {...props} />

// Streamed messages auto-render as markdown; disable with markdown: false.

There are two renderers and you get the best available one automatically:

  • Built-in, zero-dependency renderer (default). Covers headings, bullet/ordered lists, blockquotes, fenced + inline code, bold/italic/strikethrough, and links. It handles streaming-incomplete markdown gracefully (a half-written **bold or an unclosed code fence renders as plain text until complete), so it's safe to feed token-by-token. Nothing to install. Exposed as BasicMarkdown if you want to use it directly.

  • react-native-streamdown (optional upgrade). When installed it's used instead, for richer streaming-safe markdown (tables, partial-table handling, etc.). It is a native module with its own peers - install the full set:

    npx expo install react-native-streamdown react-native-enriched-markdown remend katex

    It also requires react-native-worklets >= 0.8.3 (i.e. react-native-reanimated >= 4.3), and react-native-enriched-markdown is a native module, so a dev build / prebuild is required (it does not work in Expo Go). 0.3 processes the markdown on a worklet runtime, which needs react-native-worklets' Bundle Mode: pass bundleMode: true and importForwarding: { moduleNames: ['remend'] } to react-native-worklets/plugin in babel.config.js, and wrap metro.config.js in getBundleModeMetroConfig from react-native-worklets/bundleMode (the example app does both). Until Bundle Mode is on, the chat keeps the built-in renderer and logs a one-time dev hint, since 0.3 throws on its first render without it. Version 0.3+ (StreamdownText) and the older Streamdown export are both recognised. The bubble's text color, size and link color are mapped onto its markdownStyle, so replies match the chat theme in light and dark. Anything in markdownProps is forwarded to StreamdownText; a markdownStyle there is merged per element over the derived one:

    <Chat
    messageTextProps={{
    markdown: true,
    markdownProps: { markdownStyle: { codeBlock: { backgroundColor: '#0d1117' } } },
    }}
    {...props}
    />

Emoji Reactions​

Long-press a message to open a quick emoji picker; selected reactions render as pills in the bubble and toggle on tap. The bar's chevron opens the built-in full emoji grid (search, tabs, recents) in place; both ship in the core with no extra dependencies, and renderExpandedPicker / renderReactionPicker replace them.

Long-press menu with the quick reactions bar    A reaction pill under the message    The expanded reactions grid with search and tabs

Wiring up the toggle, and the full prop list

Store reactions on each message as a reactions array and pass reactions.onReactionPress - that alone turns reactions on. Chat.toggleReaction(messages, messageId, emoji, userId) is the reducer, so the handler is one line. Reaction state is owned by you, so it works with any backend:

interface IChatMessage extends IMessage {
reactions?: MessageReaction[] // { emoji: string, userIds: (string | number)[] }[]
}

const CURRENT_USER_ID = 1

const handleReactionPress = useCallback((message: IChatMessage, emoji: string) => {
setMessages(prev => Chat.toggleReaction(prev, message._id, emoji, CURRENT_USER_ID))
}, [])

<Chat
messages={messages}
onSend={onSend}
user={{ _id: CURRENT_USER_ID }}
reactions={{
onReactionPress: handleReactionPress,
// Optional: provide a richer picker (e.g. a full emoji browser).
// See example/components/chat-examples/ReactionsExample.tsx
// renderReactionPicker: props => <MyEmojiPicker {...props} />,
}}
/>

Chat.toggleReaction (also exported as toggleReaction) returns a new array and leaves other messages untouched. A user can leave up to three different reactions on a message by default, as in Telegram - a fourth replaces their oldest; pass { maxPerUser: 1 } as a fifth argument for one reaction each, or any other limit (Infinity for none).

For a prototype, useChatMessages({ initialMessages, user }) keeps the messages in local state and returns chatProps wired to sending, reactions, editing and deleting: <Chat {...chatProps} />.

Reactions Props (Grouped)​

  • isEnabled (Bool) - Enable emoji reactions; default is on whenever onReactionPress is passed
  • emojis (String[]) - Emojis shown in the quick picker (default ['👍', '❤️', '😂', '😮', '😢', '👎'])
  • onReactionPress (Function) - (message, emoji) => void called when an emoji is selected or a pill is tapped. Apply it with Chat.toggleReaction
  • getUser (Function) - (userId) => User | undefined, who reacted. As in Telegram, a pill shows up to three reactors' avatars instead of a count in a one-to-one chat, and in a group while the message has three reactions or fewer, when every reactor is known. Defaults to the senders of the loaded messages (and you)
  • renderReactions (Function) - Override the reactions-display component rendered below the bubble
  • renderReactionPicker (Function) - Override the picker shown on long-press (use for a full emoji browser). It receives ReactionPickerProps: visible, message, emojis, onSelect, onDismiss, position, the bubble's on-screen box (pageX, pageY, bubbleWidth, bubbleHeight), the two picker styles and:
    • messagePreview (ReactNode, optional) - A copy of the pressed bubble to draw lifted above your backdrop at the bubble's box. While it is mounted the original bubble is hidden, so the message seems to rise out of the list; if you never render it, the original simply stays put. The easiest way to get the built-in look is to render the exported ContextMenu with items={[]} (see Message actions for its props)
  • renderExpandedPicker (Function({ onSelect }) | null, optional) - The expand button. The reactions bar (in the quick picker and above the context menu) ends in a chevron that grows the bar in place into a panel holding what this returns. Defaults to the built-in emoji grid (8 columns, search, tabs, recents); pass your own, or null to drop the button. Call onSelect(emoji) with the pick; the overlay closes and onReactionPress is called. Also passed to a custom renderReactionPicker
  • containerStyle, reactionStyle, reactionActiveStyle, reactionTextStyle, reactionCountStyle - Styles for the reaction pills
  • pickerContainerStyle, pickerEmojiStyle - Styles for the quick picker

Smart Link Parsing​

URLs, emails, phones, hashtags and mentions

Message text is automatically scanned for URLs, emails, and phone numbers; hashtags and mentions are opt-in. Configure it via messageTextProps:

<Chat
messageTextProps={{
url: true, // default true
email: true, // default true
phone: true, // default true
hashtag: true, // default false
mention: true, // default false
hashtagUrl: 'https://example.com/hashtag',
mentionUrl: 'https://example.com',
linkStyle: { left: { color: '#1d9bf0' }, right: { color: '#fff' } },
onPress: (message, url, type) => {
// type: 'url' | 'email' | 'phone' | 'mention' | 'hashtag'
Linking.openURL(url)
},
}}
/>

For full control, pass custom matchers ({ type, pattern, getLinkUrl?, getLinkText?, renderLink?, onPress? }[]) to add or override patterns. See the Links example in the example app.

Message actions (long-press context menu)​

Floating menu

Long-press a message to open a context menu beside your finger. It opens by default with these actions: Reply, Copy (with expo-clipboard or @react-native-clipboard/clipboard installed), Forward, Pin / Unpin, Edit (your own text messages), Select and Delete - Forward, Pin, Edit and Delete appear once you pass onForwardMessage, onPinMessage, onEditMessage and onDeleteMessage. Select starts multi-select: check circles beside the messages and a Copy / Forward / Delete bar in the composer's place (see onSelectionChange). When reactions are enabled, a reactions row is shown on top of the menu automatically.

<Chat
messages={messages}
onSend={onSend}
user={user}
onEditMessage={(message, text) => setMessages(prev => prev.map(m => m._id === message._id ? { ...m, text } : m))}
onDeleteMessage={message => setMessages(prev => prev.filter(m => m._id !== message._id))}
/>

To change the set, pass messageActions. A function receives the built-in actions for the message (each with its key), so you can add, drop or reorder; an array replaces them. defaultMessageActions picks the built-ins by key, or turns them off with false.

<Chat
messageActions={(message, defaults) => [
...defaults.filter(action => action.key !== 'forward'),
{ label: 'Report', icon: ({ color, size }) => <Flag color={color} size={size} />, onPress: () => report(message) },
]}
/>

After 0.4s the menu opens with a haptic (if expo-haptics is installed). The message neither shrinks nor lifts - the chat dims under a black 0.2 scrim and the pressed message stays lit where it is. The menu - a 200-wide card of 48pt rows with icons, in theme.contextMenu colors - opens with its right edge just left of your finger and its top at it, unrolling from the top while each row drops into place. The reactions bar above it pops out from its right end with a slight overshoot, with a "thought bubble" tail; reactions you already picked sit smaller in a circle, and the chevron at its end grows the bar into the emoji panel. A picked reaction grows and flies into the message's reaction pill; an action runs once the menu has closed; a tap on the backdrop closes it all. Every animation runs on Reanimated and follows the system Reduce Motion setting. The reaction-only picker (reactions without messageActions) is the same overlay without actions.

A fully custom menu - each item is { key?, label, icon?, onPress, destructive? }:

import { setStringAsync } from 'expo-clipboard'
import { Copy, Trash2 } from 'lucide-react-native' // optional icons

<Chat
messageActions={message => [
{ label: 'Copy', icon: ({ color, size }) => <Copy color={color} size={size} />, onPress: () => setStringAsync(message.text) },
{ label: 'Delete', icon: ({ color, size }) => <Trash2 color={color} size={size} />, destructive: true, onPress: () => deleteMessage(message) },
]}
/>

ContextMenu is exported for custom pickers and menus. Beyond visible, items, onDismiss (called once its exit has played), position and the bubble's box (pageX, pageY, bubbleWidth, bubbleHeight), it takes these optional props:

  • reactions (Object) - { emojis, onSelect, selected?, containerStyle?, emojiStyle?, renderAccessory?, renderExpandedPicker? }. selected lists the reactions the user already picked (drawn smaller, in a circle); renderAccessory adds content at the end of the bar; renderExpandedPicker adds the expand button and the panel it opens (see reactions.renderExpandedPicker above)
  • touchX, touchY (Number) - Where the finger was, in window coordinates; the menu opens beside it (beside the bubble's middle without them)
  • messagePreview (ReactNode) - The copy of the message drawn above the scrim in place; pass the one a custom picker receives
  • children (ReactNode) - Drawn above everything else in the overlay, e.g. a full emoji sheet

Note: This library no longer depends on @expo/react-native-action-sheet. Prefer messageActions above. If you specifically want a native action sheet, install it yourself, wrap your tree in ActionSheetProvider, and either call useActionSheet() in your own onLongPressMessage or pass an actionSheet prop - the actionSheet prop / context.actionSheet() escape hatch still works when you provide an implementation. The composer "+" actions use the built-in themed AttachmentSheet and need no setup.

Interactive content inside bubbles (video players, maps)​

Keeping native controls tappable

When reactions or messageActions are enabled, the long-press surface spans the whole message row - the bubble and the empty space beside it. (A tap gesture is added on top only when onPressMessage is set.) Those recognizers do not cancel touches on native subviews, so a react-native-video / expo-video player rendered through renderMessageVideo keeps its native controls interactive.

If a message must own every touch that lands on it, set isMessageGestureEnabled to false for it. The gesture surface then drops behind the bubble: the bubble's content takes its touches, and long-pressing the row next to the bubble still opens the picker - so reactions are never lost for that message.

<Chat
reactions={{ isEnabled: true, onReactionPress }}
renderMessageVideo={props => <Video source={{ uri: props.currentMessage.video }} controls style={styles.video} />}
// the video owns its controls; long-press beside the bubble still reacts
isMessageGestureEnabled={message => !message.video}
/>

Theming & Dark Mode​

Token groups, runtime switching and themed components

The default look is a modern messenger style: a classic day theme (pale green outgoing bubbles over a green swirl wallpaper) and a dark blue night theme (a purple-to-blue outgoing gradient fixed to the screen). Every value is a token. Override any subset via theme (light) and darkTheme (dark); your overrides are deep-merged over defaultLightTheme / defaultDarkTheme, and the resolved theme switches at runtime with the color scheme (system, or forced via colorScheme). Explicit per-component style props still win over the theme.

<Chat
theme={{
colors: { accent: '#3390EC', outgoingBubble: '#3390EC', outgoingText: '#FFFFFF' },
radii: { bubble: 18 },
wallpaper: { colors: ['#7FA381', '#FFF5C5', '#336F55', '#FBE37D'] },
}}
darkTheme={{ bubble: { outgoingGradient: ['#5B3FA8', '#2F6FD6'] } }}
// colorScheme="dark" // optional: force a scheme instead of following the system
{...props}
/>

Token groups: colors, radii, spacing, typography, avatar, sendButton, composer, voice, plus:

  • bubble - hasTail (a curled tail on the last bubble of a run; drawn with react-native-svg when installed), isTimeInline (the time on the last line of text when it fits; false gives it a row), outgoingGradient (stops, top to bottom, or null for the solid colors.outgoingBubble), outgoingGradientMode ('screen': one gradient across the list, each bubble shows its slice as it scrolls; 'bubble': each bubble carries the whole gradient), maxWidthInset (the text column is min(width, height) - maxWidthInset, 80 by default), bigEmojiMaxCount (a message of up to this many emoji and nothing else drops its bubble and draws them large, with the time in a service pill; 20 by default, 0 turns it off), bigEmojiSizes (font size by emoji count, 40.8, 40.8, 33.6, 26.4, 22.8).
  • wallpaper - isEnabled, colors (three or four, for the swirl gradient), isAnimated (one step per outgoing message), pattern (an image tiled over the gradient; no pattern is bundled), patternOpacity, patternColor. Off, the chat shows colors.background. Replace it entirely with the renderWallpaper prop, or draw it elsewhere with the exported ChatWallpaper component.
  • nameColors - the seven sender-name colors, picked per user id. Empty uses colors.senderName.
  • avatar.palette / avatar.paletteEnd - initials avatar gradient stops, picked per user id. An empty paletteEnd draws flat colors.
  • textScale - the text size the user picked, a multiplier of every typography size (1 by default): messages, times, names, day and service pills, quotes and the composer grow together, e.g. theme={{ textScale: 1.2 }}.
  • chatList.headerSubtitle / chatList.headerSubtitleActive - ChatHeader's status line, grey and in the accent color.

A few behaviours follow from the tokens: setting colors.outgoingBubble turns the gradient off, setting colors.senderName empties nameColors, and setting avatar.palette without paletteEnd draws flat avatars - so an app that picks its own colors keeps seeing them. Bubbles in a run (same sender, at most five minutes apart, utils.isSameGroup) share one tail and one name, 2pt apart (spacing.withinGroup), with 6pt between runs (spacing.betweenGroups).

Build your own theme-aware components with the exported hooks:

import { useTheme, useThemedStyles } from '@kesha-antonov/react-native-chat'
import { StyleSheet } from 'react-native'

const MyBadge = () => {
const theme = useTheme()
const styles = useThemedStyles(t => StyleSheet.create({
badge: { backgroundColor: t.colors.accent, borderRadius: t.radii.bubble },
}))
return <View style={styles.badge} />
}

Also exported: defaultLightTheme, defaultDarkTheme, ChatWallpaper, and the ChatTheme / PartialChatTheme types.

The built-in panels have their own token groups: emojiKeyboard, contextMenu, attachments and chatList. The full set of tokens and their default values is the ChatTheme type and defaultLightTheme / defaultDarkTheme.

Localization (i18n)​

Built-in translations and label overrides

All built-in UI strings (composer placeholder, send/cancel, load earlier, today, voice/video/location labels, slide-to-cancel, reply/edit banner, camera-permission text) route through a label table. Built-in translations ship for es, fr, de, ru, zh, ar, pt, ja, ko, it, tr, hi, nl, pl and id, with English as the default, selected by the existing locale prop. A regional tag falls back to its base language, so pt-BR resolves to pt. Override any individual string with labels:

<Chat
locale="fr" // pick a built-in translation
labels={{ placeholder: 'Votre message...' }} // override any string
{...props}
/>

Right-to-left layout follows the locale (Arabic, Hebrew, Persian, Urdu and a few others). forceRTL overrides the guess: true for an RTL language not in that set, false to keep a left-to-right layout.

Exported helpers: ChatLabels (type), defaultLabels, translations, resolveLabels, and the useLabels hook for reading the resolved labels in custom components.

Message Status​

Tick indicators for sent / delivered / read

Set sent, received, pending or isFailed on a message to show its delivery status. The status is yours to set: add a message with pending: true in onSend, then set sent and received as your backend confirms. Every outgoing message shows it, voice notes, round notes, files and photos included. They render beside the time: one check once sent, two once received (read), a spinning clock while pending, a red badge when sending failed. isEdited or editedAt prefix the time with "edited":

const message: IMessage = {
_id: 1,
text: 'Delivered!',
createdAt: new Date(),
user: { _id: 1 },
sent: true,
received: true,
}

Customize the indicators with renderTicks (full override) or tickStyle (style only):

<Chat
renderTicks={message => (message.received ? <MyReadIcon /> : null)}
tickStyle={{ color: '#1d9bf0' }}
/>

TypeScript​

Generic over your own message type

Chat ships complete type definitions and is generic over your message type. Extend IMessage to add custom fields and everything stays typed end to end:

import { Chat, IMessage } from '@kesha-antonov/react-native-chat'

interface MyMessage extends IMessage {
reactions?: { emoji: string, userIds: (string | number)[] }[]
}

<Chat<MyMessage>
messages={messages}
onSend={msgs => {/* msgs is typed as MyMessage[] */}}
user={{ _id: 1 }}
/>

Building blocks​

Lower-level components and helpers

Chat is assembled from exported parts, so you can reuse them in your own screens or render props:

  • Rows and bubbles: Message, Bubble, MessageText, MessageImage, MessageVideo, MessageAudio, MessageFile, MessageLocation, MessageReply, SystemMessage, Day, Time, Avatar, ChatAvatar, UnreadSeparator, ChatEmpty, LinkPreviewCard, MediaCard
  • Composer: InputToolbar, Composer, Send, Actions, ReplyPreview, EmojiGrid, AttachmentMenu, AttachmentSheet
  • Voice and video: VoiceMessageInput, WaveformPlayer, VideoRecordButton, VideoNoteRecorder, with isVoiceRecordingAvailable, isWaveformAvailable, isVideoRecordingAvailable, isVisionCameraAvailable
  • Menus and status: ContextMenu, TypingStatus (getTypingText), StreamingCursor, MarkdownMessageText (isMarkdownAvailable), BasicMarkdown
  • Lists: MessagesContainer, ChatList, ChatListCell, ChatListAvatar, ChatListShimmer
  • Hooks: useTheme, useThemedStyles, useColorScheme, useIcons, useLabels, useIsRTL, useChatContext, useStreamingMessages, useChatMessages, useChatList
  • Helpers: toggleReaction, getChatCapabilities, buildAttachmentMessages, getAttachmentTabs, formatFileSize, findFirstUrl, countEmojiOnly, isRTLLocale, mirrorPosition, formatChatListDate, getChatPreview, sortChats, matchesChatQuery, DEFAULT_MESSAGE_ACTION_KEYS, EMOJI_CATEGORY_KEYS, DEFAULT_RECENT_EMOJIS

Components rendered outside Chat need a GestureHandlerRootView above them.