The messaging system provides real-time communication with message status tracking, typing indicators, and support for multiple content types.
Message lifecycle
Messages go through multiple status stages from creation to delivery. The system automatically tracks and updates the status of each message.
Message status flow
Messages transition through these statuses:
Queued
Message is created and queued for sending
Sending
Message is being transmitted to the server
Sent
Message successfully sent to server
Delivered
Message delivered to recipient’s device
Read
Message opened and read by recipient
Message interface
The Message type defines the structure of all messages in the application:
export type MessageStatus = "queued" | "sending" | "sent" | "delivered" | "read" | "error"
export type MessageContentType = "text" | "image" | "audio"
export interface Message {
id: string
chatId: string
authorId: string
contentType: MessageContentType
text?: string
media?: MediaAttachment
status: MessageStatus
createdAt: string
updatedAt?: string
replyToId?: string
isForwarded?: boolean
reactions?: Reaction[]
}
Sending messages
Use the sendComposerPayload method from the chat store to send messages:
const sendMessage = useChatStore((state) => state.sendComposerPayload)
// Send text message
const messageId = sendMessage(chatId, {
text: "Hello, how are you?"
})
// Send message with media
sendMessage(chatId, {
text: "Check out this photo!",
media: mediaAttachment
})
Status lifecycle implementation
The system automatically schedules status updates after sending a message:
function scheduleLifecycle(
messageId: string,
get: () => ChatStore,
set: (updater: (state: ChatStore) => ChatStore | Partial<ChatStore>) => void,
initialDelay = 350
) {
const steps: MessageStatus[] = ["queued", "sending", "sent", "delivered", "read"]
steps.forEach((status, idx) => {
const delay = initialDelay + idx * 450
const timer = setTimeout(() => {
set((state) => {
const existing = state.messages[messageId]
if (!existing || existing.status === "error") {
return state
}
const nextMessage: Message = {
...existing,
status,
updatedAt: new Date().toISOString(),
}
return {
...state,
messages: { ...state.messages, [messageId]: nextMessage },
}
})
}, delay)
})
}
Status updates occur at ~450ms intervals, creating a realistic progression from “queued” to “read”.
Delivery status indicators
The MessageBubble component displays visual indicators for message status:
components/chat/message-bubble.tsx
const statusIcon = {
queued: ClockCountdown,
sending: Clock,
sent: Check,
delivered: Checks,
read: Checks,
error: WarningCircle,
} as const
const statusColor: Record<Message["status"], string> = {
queued: "text-muted-foreground",
sending: "text-muted-foreground",
sent: "text-muted-foreground",
delivered: "text-status-delivered",
read: "text-status-read",
error: "text-destructive",
}
Status indicator rendering
For outgoing messages, the status icon appears next to the timestamp:
components/chat/message-bubble.tsx
<footer className="mt-1 flex items-center gap-1 text-[11px] text-muted-foreground">
<span>{timeLabel}</span>
{isOutgoing ? (
<StatusIcon
className={cn(
"h-3.5 w-3.5",
message.status === "read" ? "text-status-read" : statusColor[message.status]
)}
weight={message.status === "read" || message.status === "delivered" ? "fill" : "regular"}
aria-label={`Message ${message.status}`}
/>
) : null}
</footer>
Single check mark indicates “sent”, double check marks indicate “delivered” or “read” with color differentiation.
Typing indicators
The typing indicator shows when a contact is composing a message.
Managing typing state
Update typing status using the setTypingIndicator method:
setTypingIndicator: (chatId, authorId, isTyping) => {
set((state) => {
const exists = state.typingIndicators.find(
(indicator) => indicator.chatId === chatId && indicator.authorId === authorId
)
if (isTyping && !exists) {
const indicator: TypingIndicator = {
chatId,
authorId,
startedAt: new Date().toISOString(),
}
return { ...state, typingIndicators: [...state.typingIndicators, indicator] }
}
if (!isTyping && exists) {
return {
...state,
typingIndicators: state.typingIndicators.filter(
(indicator) => !(indicator.chatId === chatId && indicator.authorId === authorId)
),
}
}
return state
})
}
Typing indicator component
The TypingIndicator component displays an animated “typing” state:
components/chat/typing-indicator.tsx
const TypingIndicatorComponent = ({ label = "typing" }: TypingIndicatorProps) => {
const dots = [0, 1, 2]
return (
<div
className="inline-flex items-center gap-2 rounded-full bg-black/5 px-3 py-1 text-xs font-medium text-muted-foreground"
aria-live="polite"
aria-label={`${label} indicator`}
>
<span className="sr-only">{label}</span>
<div className="flex items-center gap-1">
{dots.map((dot) => (
<span
key={dot}
className="block h-1.5 w-1.5 animate-bounce rounded-full bg-accent"
style={{ animationDelay: `${dot * 0.12}s` }}
/>
))}
</div>
</div>
)
}
The typing indicator uses staggered animations on three dots to create a wave effect.
Receiving messages
Incoming messages are added to the chat using the receiveMessage method:
receiveMessage: (chatId, message) => {
set((state) => {
const nextMessages = { ...state.messages, [message.id]: message }
const chat = state.chats[chatId]
if (!chat) return state
const nextChat: Chat = {
...chat,
messageIds: [...chat.messageIds, message.id],
unreadCount:
state.activeChatId === chatId ? 0 : Math.min(chat.unreadCount + 1, 99),
lastActivityAt: message.createdAt,
lastMessagePreview: derivePreview(message),
}
return {
...state,
messages: nextMessages,
chats: { ...state.chats, [chatId]: nextChat },
}
})
}
Unread count is capped at 99 to prevent excessive badge numbers.
Message reactions
Messages can include emoji reactions from participants:
export interface Reaction {
emoji: string
authorId: string
createdAt: string
}
Reactions are displayed inline with the message bubble:
components/chat/message-bubble.tsx
{message.reactions?.length ? (
<div className="mt-2 inline-flex items-center gap-1 rounded-full bg-black/10 px-2 py-0.5 text-xs text-foreground/80">
{message.reactions.map((reaction) => (
<span key={reaction.authorId + reaction.emoji}>{reaction.emoji}</span>
))}
</div>
) : null}
Helper utilities
Generate message IDs
Create unique identifiers for new messages:
export function generateMessageId(prefix = "msg") {
try {
const uuid = typeof globalThis !== "undefined" && "crypto" in globalThis
? globalThis.crypto?.randomUUID?.()
: undefined
if (uuid) return `${prefix}-${uuid}`
throw new Error("no-crypto")
} catch {
return `${prefix}-${Math.random().toString(36).slice(2, 9)}`
}
}
Derive message preview
Generate preview text for chat list:
export function derivePreview(message: Message) {
if (message.contentType === "image") {
return message.media?.caption ?? "📷 Photo"
}
if (message.contentType === "audio") {
return "🎧 Audio message"
}
return message.text ?? "New message"
}