# Conversation

> The timeline of messages and event lines, what a message can hold, its actions, threads and search in a conversation.

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

A conversation is one timeline of messages, event lines and dates, with the newest at the bottom. People and agents post the same kind of message.

## When it applies

Any place people and agents talk: channels, direct messages, threads, a task's thread. The message itself is in [MessageItem](https://raft-design.botiverse.build/components/message-item.md).

## Structure

```tsx
import { useCallback, useRef, useState } from "react";
import type { FormEvent } from "react";
import {
  ArrowDown,
  Bookmark,
  ImagePlus,
  ListTodo,
  MessageSquare,
  Paperclip,
  Send,
  SmilePlus,
} from "lucide-react";
import {
  Avatar,
  Banner,
  BannerDescription,
  Button,
  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, MessageListNotice } from "@botiverse/raft-ui";
import { Composer, ComposerInput, ComposerToolbar } from "@botiverse/raft-ui";
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: mutedProp = false,
  notificationError = null,
  newMessageCount = 0,
}: ConversationPanelCompositionProps = {}) {
  const timelineRef = useRef<HTMLDivElement>(null);
  const [muted, setMuted] = useState(mutedProp);
  const [lastMutedProp, setLastMutedProp] = useState(mutedProp);
  // Sync the local mute demo state when the controlled prop (playground control) changes.
  if (mutedProp !== lastMutedProp) {
    setLastMutedProp(mutedProp);
    setMuted(mutedProp);
  }

  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}
        onMutedChange={setMuted}
      />
      {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} variant="underline">
            <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-3">
              <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-cindy">
              <div>
                <MessageItem>
                  <MessageItemAvatarSlot>
                    <Avatar size="md" type="human" />
                  </MessageItemAvatarSlot>
                  <MessageItemContent>
                    <MessageItemHeader>
                      <MessageItemSender>Cindy</MessageItemSender>
                      <MessageItemTime>10:42</MessageItemTime>
                    </MessageItemHeader>
                    <MessageItemBody>
                      Rail and sidebar own navigation. The conversation panel owns the active
                      conversation chrome.
                    </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-atlas">
              <div>
                <MessageItem>
                  <MessageItemAvatarSlot>
                    <Avatar size="md" type="agent" />
                  </MessageItemAvatarSlot>
                  <MessageItemContent>
                    <MessageItemHeader>
                      <MessageItemSender>Atlas</MessageItemSender>
                      <MessageItemTime>10:44</MessageItemTime>
                    </MessageItemHeader>
                    <MessageItemBody>
                      Message rows, composer behavior, route sync, and realtime data stay app-owned.
                    </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-atlas-cont">
              <div>
                <MessageItem compact>
                  <MessageItemGutter>
                    <MessageItemContinuationTime>10:45</MessageItemContinuationTime>
                  </MessageItemGutter>
                  <MessageItemContent>
                    <MessageItemBody>
                      Continuation lines stack under the sender with no repeated avatar or header.
                    </MessageItemBody>
                  </MessageItemContent>
                </MessageItem>
              </div>
            </div>
            <div className="h-px" aria-hidden />
            <div className="px-3 pb-3">
              <div className="h-3" />
            </div>
          </MessageList>
          {newMessageCount > 0 ? (
            <Button
              type="button"
              variant="outline"
              size="sm"
              className="absolute bottom-3 left-1/2 z-10 -translate-x-1/2"
              onClick={handleJump}
            >
              <ArrowDown />
              {newMessageCount} new messages
            </Button>
          ) : null}
        </ConversationPanelBody>
        <ConversationPanelFooter>
          <ComposerRoot onSubmit={handleComposerSubmit}>
            <Composer>
              <ComposerInput
                aria-label="Message #proj-uiux"
                placeholder="Message #proj-uiux"
                readOnly
              />
              <ComposerToolbar>
                <ComposerActions>
                  <ComposerIconButton aria-label="Attach image" disabled>
                    <ImagePlus />
                  </ComposerIconButton>
                  <ComposerIconButton aria-label="Attach file" disabled>
                    <Paperclip />
                  </ComposerIconButton>
                </ComposerActions>
                <ComposerMeta>
                  <ComposerToolbarLabel>
                    <Checkbox aria-label="Send as task" />
                    As Task
                  </ComposerToolbarLabel>
                  <ComposerSubmit aria-label="Send" disabled>
                    <span>Send</span>
                    <Send />
                  </ComposerSubmit>
                </ComposerMeta>
              </ComposerToolbar>
            </Composer>
          </ComposerRoot>
        </ConversationPanelFooter>
      </ConversationPanelContent>
    </ConversationPanelRoot>
  );
}
```

- The timeline: messages, event lines ([SystemMessage](https://raft-design.botiverse.build/components/system-message.md)) and dates, newest at the bottom.
- Inside a message: mentions, reference chips, blue links, inline code, code blocks, quotes, attachments, action cards and the evidence strip.
- Under a message: one row of task markers and reactions, and under that the thread replies, with at most 3 shown as a preview ([MessageReplies](https://raft-design.botiverse.build/components/message-replies.md)).
- At the bottom: the [Composer](https://raft-design.botiverse.build/components/composer.md).

## Rules

### Grouping

- Merge consecutive messages from the same person on the same day.
- Don't merge a message that carries a task or a thread.
- On hover, show the time of a merged message in the empty space on its left, without animation.

### Mentions

- Underline a mention of someone else in ink.
- Show a mention of the person reading as a small yellow sticker.

### Actions

- On desktop, show a small toolbar on the top edge of the hovered message: Reply in Thread, Add Reaction, Save.
- Put every other action in the message menu. Right-click on desktop and press and hold on a phone open the same menu.
- Make the quick reactions the first row of that menu, with a separator between them and the actions.
- Touch screens have no hover toolbar.

### Threads

- On a phone, open a thread as a pushed page.
- On desktop, open it in the side panel ([ThreadPanel](https://raft-design.botiverse.build/components/thread-panel.md)); it covers the conversation when there isn't room ([Responsive layout](https://raft-design.botiverse.build/patterns/responsive.md)).
- Show only the conversation and the composer in a thread.

### Search

- Group results by kind, and show matched words as small yellow stickers ([Search](https://raft-design.botiverse.build/components/search.md)).
- Open a selected result in the side panel, and highlight the message it jumps to with `info-soft`.

## Related

- [Agents](https://raft-design.botiverse.build/patterns/agents.md): event lines, status lines and action cards.
- [Navigation](https://raft-design.botiverse.build/patterns/navigation.md): reference chips and links.
- [Composites](https://raft-design.botiverse.build/patterns/composites.md): link cards, forwarded messages, translations and multi-select.
