Chat thread
A conversation with grouped bubbles, delivery states, retry and a keyboard-aware composer.
npx @eoria/cli add chat-threadThe command also adds Alert, Avatar, Button, IconButton, InputGroup, Keyboard and Text.
Usage
import { useState } from 'react'import { ChatThread, type ChatMessage } from '@/components/blocks/chat-thread'
export function ConversationRoute() { const [messages, setMessages] = useState<ChatMessage[]>([ { id: 'first', text: 'Can you look at the draft?', sentAt: '2026-09-22T09:41:00Z', from: 'them', }, ])
return ( <ChatThread participant={{ name: 'Grace Hopper', initials: 'GH', presence: 'online' }} messages={messages} onSend={async (text) => { const message = await sendMessage(text) setMessages((current) => [...current, message]) }} onRetry={async (id) => { await retryMessage(id) setMessages((current) => current.map((message) => message.id === id ? { ...message, status: 'sent' } : message, ), ) }} onBack={() => goBack()} /> )}sendMessage, retryMessage and goBack belong to your app. sendMessage returns a ChatMessage with a unique ID.
Props
| Prop | Type | Default |
|---|---|---|
messages |
ChatMessage[], oldest first |
required |
participant |
ChatParticipant |
required |
onSend |
(text: string) => void | Promise<void> |
required |
onRetry |
(id: string) => void | Promise<void> |
hides Retry |
onPressAttachment |
() => void | Promise<void> |
hides attachment button |
onPressHeader |
() => void | Promise<void> |
header is not pressable |
onBack |
() => void | Promise<void> |
hides back button |
header |
boolean |
true |
insetTop |
boolean |
true |
style |
StyleProp<ViewStyle> |
Data types
export type ChatMessage = { id: string text: string sentAt: Date | string from: 'me' | 'them' status?: 'sending' | 'sent' | 'read' | 'failed'}
export type ChatParticipant = { name: string avatarUrl?: string initials: string presence?: 'online' | 'offline' | string}Notes
- Pass messages in chronological order, oldest first. The block reverses a copy for its inverted
FlatList; it does not mutate the supplied array. Use stable, unique IDs. The parent owns sent messages and status updates. - Consecutive messages from the same sender share tighter spacing. The last bubble has a smaller bottom corner on the sender’s side. Groups break at local calendar-day boundaries, with a day label above the first message. Times use the device locale. Invalid timestamps display Unknown date and Unknown time.
- Each bubble is one screen-reader element containing sender, text, time and status. Retry remains a separate button. The header reads the participant’s name and presence together.
- The composer sends trimmed text and ignores whitespace-only messages. It stays disabled while an action is pending. A successful send clears the composer; a rejected send preserves the draft.
- All callback props accept promises. Buttons show loading while their actions run; a pending header action shows Opening profile. Rejections show
error.messagein an announced Alert, or generic text for other values. Repeated presses are guarded and state updates stop after unmount. - A failed message shows Retry when
onRetryis provided. Update that message throughmessagesafter retrying. The block does not add optimistic messages itself. KeyboardFooterkeeps the composer above the keyboard and pads the home indicator. Follow the keyboard setup. Setheader={false}andinsetTop={false}when your navigation header already supplies both.- The demo starts with a failed message to retry. Its first new send also fails, preserving the draft; the next succeeds with a toast.