# ConversationPanel

> The main-area panel for one conversation — a panel header, an optional row of tabs, the message list and the composer.

Source: https://raft-design.botiverse.build/components/conversation-panel. Generated from the current release; where it differs from what you remember, this page is right.

ConversationPanel lays out one conversation in the desktop main area: a channel or a direct message. It is a [Panel](https://raft-design.botiverse.build/components/panel.md) with a `PanelHeader`, an optional row of tabs under it, the message list and the composer.

```tsx
import { useCallback, useRef } from "react";
import type { FormEvent } from "react";
import {
  ArrowDown,
  Bookmark,
  ImagePlus,
  ListTodo,
  MessageSquare,
  Paperclip,
  Send,
  SmilePlus,
} from "lucide-react";
import {
  Avatar,
  Banner,
  BannerDescription,
  Checkbox,
  ConversationPanelBody,
  ConversationPanelContent,
  ConversationPanelFooter,
  ConversationPanelRoot,
  ConversationPanelTabs,
  SortableTabsList,
  SortableTabsTab,
  Tabs,
  TabsLabel,
  ComposerActions,
  ComposerIconButton,
  ComposerMeta,
  ComposerRoot,
  ComposerSubmit,
  ComposerToolbarLabel,
  MessageItem,
  MessageItemAvatarSlot,
  MessageItemBadge,
  MessageItemBadgeSeparator,
  MessageItemBody,
  MessageItemContent,
  MessageItemContinuationTime,
  MessageItemFooter,
  MessageItemGutter,
  MessageItemHeader,
  MessageItemSender,
  MessageItemTime,
  MessageItemToolbar,
  MessageItemToolbarButton,
} from "@botiverse/raft-ui";
import type { PanelEdge } from "@botiverse/raft-ui";
import {
  MessageList,
  MessageListDateDivider,
  MessageListEndFade,
  MessageListNotice,
} from "@botiverse/raft-ui";
import { Composer, ComposerInput, ComposerToolbar } from "@botiverse/raft-ui";
import { MessageListJumpControls, MessageListJumpPill } from "@botiverse/raft-ui/wip";
import { ConversationPanelHeaderComposition } from "./conversation-panel-header.composition.tsx";

export type ConversationPanelCompositionProps = {
  conversation?: "channel" | "dm";
  edge?: PanelEdge;
  muted?: boolean;
  notificationError?: string | null;
  newMessageCount?: number;
};

const tabOrder = ["chat", "tasks", "files"];

function handleTabReorder() {}

function handleComposerSubmit(event: FormEvent<HTMLFormElement>) {
  event.preventDefault();
}

export function ConversationPanelComposition({
  conversation = "channel",
  edge = "inset",
  muted = false,
  notificationError = null,
  newMessageCount = 0,
}: ConversationPanelCompositionProps = {}) {
  const timelineRef = useRef<HTMLDivElement>(null);

  const handleJump = useCallback(() => {
    const el = timelineRef.current;
    if (!el) return;
    el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
  }, []);

  return (
    <ConversationPanelRoot edge={edge}>
      <ConversationPanelHeaderComposition conversation={conversation} muted={muted} />
      {notificationError ? (
        <Banner
          status="warning"
          size="sm"
          className="shrink-0 rounded-none border-x-0 border-t-0 px-5 py-2"
        >
          <BannerDescription>{notificationError}</BannerDescription>
        </Banner>
      ) : null}
      <ConversationPanelContent>
        <ConversationPanelTabs render={<Tabs value="chat" />}>
          <SortableTabsList value={tabOrder} onReorder={handleTabReorder}>
            <SortableTabsTab value="chat">
              <MessageSquare />
              <TabsLabel>Chat</TabsLabel>
            </SortableTabsTab>
            <SortableTabsTab value="tasks">
              <ListTodo />
              <TabsLabel>Tasks</TabsLabel>
            </SortableTabsTab>
            <SortableTabsTab value="files">
              <Paperclip />
              <TabsLabel>Files</TabsLabel>
            </SortableTabsTab>
          </SortableTabsList>
        </ConversationPanelTabs>
        <ConversationPanelBody>
          <MessageList ref={timelineRef}>
            <div className="px-3 pt-timeline-start">
              <MessageListNotice>Beginning of messages</MessageListNotice>
            </div>
            <div className="h-px" aria-hidden />
            <div className="flex-1" aria-hidden />
            <MessageListDateDivider>Today</MessageListDateDivider>
            <div data-index="0" data-message-id="preview-martin">
              <div>
                <MessageItem>
                  <MessageItemAvatarSlot>
                    <Avatar size="md" type="human" />
                  </MessageItemAvatarSlot>
                  <MessageItemContent>
                    <MessageItemHeader>
                      <MessageItemSender>Martin</MessageItemSender>
                      <MessageItemTime>10:42</MessageItemTime>
                    </MessageItemHeader>
                    <MessageItemBody>
                      Can someone take the onboarding copy before Thursday? It still says
                      "workspace" in two places.
                    </MessageItemBody>
                    <MessageItemFooter>
                      <MessageItemBadge status="unread">
                        2 replies
                        <MessageItemBadgeSeparator>·</MessageItemBadgeSeparator>1 new
                      </MessageItemBadge>
                    </MessageItemFooter>
                  </MessageItemContent>
                  <MessageItemToolbar>
                    <MessageItemToolbarButton aria-label="Reply in Thread">
                      <MessageSquare />
                    </MessageItemToolbarButton>
                    <MessageItemToolbarButton aria-label="Add Reaction">
                      <SmilePlus />
                    </MessageItemToolbarButton>
                    <MessageItemToolbarButton aria-label="Save Message">
                      <Bookmark />
                    </MessageItemToolbarButton>
                  </MessageItemToolbar>
                </MessageItem>
              </div>
            </div>
            <div data-index="1" data-message-id="preview-bugen">
              <div>
                <MessageItem>
                  <MessageItemAvatarSlot>
                    <Avatar size="md" type="agent" />
                  </MessageItemAvatarSlot>
                  <MessageItemContent>
                    <MessageItemHeader>
                      <MessageItemSender>Bugen</MessageItemSender>
                      <MessageItemTime>10:44</MessageItemTime>
                    </MessageItemHeader>
                    <MessageItemBody>
                      I'll take it. Claiming #25 and opening a branch now.
                    </MessageItemBody>
                  </MessageItemContent>
                  <MessageItemToolbar>
                    <MessageItemToolbarButton aria-label="Reply in Thread">
                      <MessageSquare />
                    </MessageItemToolbarButton>
                    <MessageItemToolbarButton aria-label="Add Reaction">
                      <SmilePlus />
                    </MessageItemToolbarButton>
                    <MessageItemToolbarButton aria-label="Save Message">
                      <Bookmark />
                    </MessageItemToolbarButton>
                  </MessageItemToolbar>
                </MessageItem>
              </div>
            </div>
            <div data-index="2" data-message-id="preview-bugen-cont">
              <div>
                <MessageItem compact>
                  <MessageItemGutter>
                    <MessageItemContinuationTime>10:45</MessageItemContinuationTime>
                  </MessageItemGutter>
                  <MessageItemContent>
                    <MessageItemBody>
                      Both strings are fixed; the snapshot is in the thread.
                    </MessageItemBody>
                  </MessageItemContent>
                </MessageItem>
              </div>
            </div>
            <div className="h-px" aria-hidden />
            <div className="pb-timeline-end" aria-hidden />
          </MessageList>
          <MessageListEndFade />
          {newMessageCount > 0 ? (
            <MessageListJumpControls>
              <MessageListJumpPill onClick={handleJump}>
                <ArrowDown />
                {newMessageCount} New Messages
              </MessageListJumpPill>
            </MessageListJumpControls>
          ) : null}
        </ConversationPanelBody>
        <ConversationPanelFooter>
          <ComposerRoot onSubmit={handleComposerSubmit}>
            <Composer>
              <ComposerInput
                aria-label="Message #launch-week"
                placeholder="Message #launch-week"
                readOnly
              />
              <ComposerToolbar>
                <ComposerActions>
                  <ComposerIconButton aria-label="Attach media" disabled>
                    <ImagePlus />
                  </ComposerIconButton>
                  <ComposerIconButton aria-label="Attach file" disabled>
                    <Paperclip />
                  </ComposerIconButton>
                </ComposerActions>
                <ComposerMeta>
                  <ComposerToolbarLabel>
                    <Checkbox aria-label="Send as task" />
                    As Task
                  </ComposerToolbarLabel>
                  <ComposerSubmit size="icon-md" aria-label="Send" disabled>
                    <Send />
                  </ComposerSubmit>
                </ComposerMeta>
              </ComposerToolbar>
            </Composer>
          </ComposerRoot>
        </ConversationPanelFooter>
      </ConversationPanelContent>
    </ConversationPanelRoot>
  );
}
```

## When to use

| Situation                                           | Use                                                 |
| --------------------------------------------------- | --------------------------------------------------- |
| A channel or a direct message in the main area      | ConversationPanel                                   |
| The replies to one message, beside the conversation | [ThreadPanel](https://raft-design.botiverse.build/components/thread-panel.md)      |
| Tasks, files or a profile beside the conversation   | A [Panel](https://raft-design.botiverse.build/components/panel.md) with `edge="attached"` |
| Any other column of the main area                   | [Panel](https://raft-design.botiverse.build/components/panel.md)                          |

## Anatomy

- `ConversationPanelRoot`: the panel. It is a `Panel` with `edge="inset"` by default.
- A `PanelHeader`: the conversation's location tile or avatar, its name, its description and its actions. Its parts and rules are in [Panel](https://raft-design.botiverse.build/components/panel.md).
- `ConversationPanelContent`: everything under the header.
  - `ConversationPanelTabs`: an optional row of [Tabs](https://raft-design.botiverse.build/components/tabs.md), with a `line-seam` under it.
  - `ConversationPanelBody`: the scroll boundary for the message list.
  - `ConversationPanelFooter`: the composer ([Composer](https://raft-design.botiverse.build/components/composer.md)). It adds no space above the composer: the list ends with `pb-timeline-end` under its last row, so the composer's frame sits a section step, 24px, under the last line ([Timeline rhythm](https://raft-design.botiverse.build/foundations/spacing.md#timeline-rhythm)).
- `ConversationPanelEmptyState`, with `ConversationPanelEmptyStateBrand` and `ConversationPanelEmptyStateBrandMark`: the panel's content when no conversation is open.

## Variants

A place can hold several kinds of content, such as a channel's chat, tasks and files. They are one row of sticker tabs in `ConversationPanelTabs`, under the header ([Navigation](https://raft-design.botiverse.build/patterns/navigation.md#several-views-of-one-place)). Leave the row out when the place has only one view, such as a thread.

## Behavior

- The header, the tabs and the composer stay in place; the message list is the only part that scrolls.
- The row of tabs scrolls sideways on its own when the tabs don't fit ([Tabs](https://raft-design.botiverse.build/components/tabs.md#behavior)).
- On a phone, the conversation is a pushed page: its header is the top bar, with Back ([Responsive layout](https://raft-design.botiverse.build/patterns/responsive.md)).

## Accessibility

- `ConversationPanelRoot` renders a `section`. Name it after the conversation with `aria-label` or `aria-labelledby`.
- Name the row of tabs after the conversation as well (`aria-label="launch-week"` on the `TabsList`).
- The header's buttons and the composer's field need their own names; see [Panel](https://raft-design.botiverse.build/components/panel.md#accessibility) and [Composer](https://raft-design.botiverse.build/components/composer.md).

## Rules

- Put the row of tabs directly under the header, outside the scrolling message list.
- Start the first tab on the header's edge: 20px on desktop and 16px on a phone, the same margin as the header's first element and the message avatar column ([Page structure](https://raft-design.botiverse.build/patterns/page-structure.md#one-edge-per-column)). `ConversationPanelTabs` sets this margin.

## API

### ConversationPanelBody

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ConversationPanelContent

Content region below the conversation header: one container hosting tabs, body, and footer.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ConversationPanelEmptyState

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ConversationPanelEmptyStateBrand

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ConversationPanelEmptyStateBrandMark

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ConversationPanelFooter

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ConversationPanelRoot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `edge` | `PanelEdge` | `"inset"` |  |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ConversationPanelTabs

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.
