Skip to main content

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

chat
Chat
required
The chat conversation object containing metadata and message IDs
contact
Contact
required
The contact information for the person in this conversation
messages
Message[]
required
Array of message objects to display in chronological order
profileId
string
required
The current user’s ID, used to distinguish outgoing vs incoming messages
draft
DraftMessage
The saved draft for this conversation (text and attachments)
typing
boolean
Whether the contact is currently typing
onBack
() => void
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:
  1. Header - Shows contact info and online status
  2. Message list - Scrollable area with messages and typing indicator
  3. 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>

Media preview

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.

Contact initials

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%)]" />

Build docs developers (and LLMs) love