Overview
The ChatPanel component renders the active conversation interface. It includes the chat header, scrollable message list, message composer, and a media preview dialog.
Usage
import { ChatPanel } from "@/components/chat/chat-panel"
function ChatView() {
return (
<ChatPanel
chat={activeChat}
contact={selectedContact}
messages={messageList}
profileId={currentUser.id}
draft={currentDraft}
typing={isContactTyping}
onBack={() => setActiveChat(undefined)}
onChangeDraft={handleDraftChange}
onClearDraft={handleClearDraft}
onSendMessage={handleSendMessage}
/>
)
}
Props
The chat conversation object containing metadata and message IDs
The contact information for the person in this conversation
Array of message objects to display in chronological order
The current user’s ID, used to distinguish outgoing vs incoming messages
The saved draft for this conversation (text and attachments)
Whether the contact is currently typing
Callback when the back button is pressed (mobile only)
onChangeDraft
(chatId: string, draft: { text?: string; attachments?: MediaAttachment[] }) => void
required
Callback when draft content changes
onClearDraft
(chatId: string) => void
required
Callback to clear the draft after sending
onSendMessage
(chatId: string, payload: { text?: string; media?: MediaAttachment }) => void
required
Callback when a message is sent
Type definitions
interface ChatPanelProps {
chat: Chat
contact: Contact
messages: Message[]
profileId: string
draft?: DraftMessage
typing?: boolean
onBack?: () => void
onChangeDraft: (chatId: string, draft: { text?: string; attachments?: MediaAttachment[] }) => void
onClearDraft: (chatId: string) => void
onSendMessage: (chatId: string, payload: { text?: string; media?: MediaAttachment }) => void
}
interface Chat {
id: string
contactId: string
messageIds: string[]
unreadCount: number
archived?: boolean
muted?: boolean
lastActivityAt: string
lastMessagePreview?: string
}
interface Contact {
id: string
name: string
phoneNumber: string
about: string
avatarUrl: string
isOnline: boolean
lastSeenAt: string
favorite?: boolean
pinned?: boolean
}
interface DraftMessage {
chatId: string
text: string
attachments: MediaAttachment[]
}
Layout structure
The ChatPanel consists of three main sections:
- Header - Shows contact info and online status
- Message list - Scrollable area with messages and typing indicator
- Composer - Text input and attachment controls
<section className="flex h-full flex-1 flex-col bg-gradient-to-b from-background/90 to-background">
<ChatHeader contact={contact} isOnline={contact.isOnline} onBack={onBack} />
<div className="relative flex flex-1 flex-col overflow-hidden">
<MessageList
chatId={chat.id}
messages={messages}
contactInitials={contactInitials}
profileId={profileId}
isTyping={typing}
typingLabel={`${contact.name} is typing`}
onMediaPreview={(media) => setPreview(media)}
/>
</div>
<MessageComposer
chatId={chat.id}
draftText={draft?.text}
draftAttachments={draft?.attachments}
onChangeDraft={onChangeDraft}
onClearDraft={onClearDraft}
onSend={onSendMessage}
/>
</section>
The component includes a dialog for previewing media attachments:
<Dialog open={!!preview} onOpenChange={(open) => !open && setPreview(null)}>
<DialogContent className="max-w-2xl border-none bg-background/95 p-0">
{preview ? (
<figure className="overflow-hidden rounded-2xl">
<Image
src={preview.url}
alt={preview.caption ?? "Shared image"}
width={preview.width ?? 1200}
height={preview.height ?? 900}
className="h-auto w-full object-cover"
priority
/>
{preview.caption ? (
<figcaption className="px-4 py-3 text-sm text-muted-foreground">
{preview.caption}
</figcaption>
) : null}
</figure>
) : null}
</DialogContent>
</Dialog>
The media preview opens when a user clicks on an image in the message list. The onMediaPreview callback is passed to the MessageList component.
The component computes contact initials for avatar fallbacks:
function initials(name: string) {
return name
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("")
}
Visual effects
The component includes a subtle radial gradient overlay:
<div className="pointer-events-none absolute inset-0 select-none bg-[radial-gradient(circle_at_top,rgba(67,160,71,0.1),transparent_60%)]" />