"use client" import * as React from "react" import { CheckCheckIcon, CheckIcon, FileIcon, MoreHorizontalIcon, PaperclipIcon, SearchIcon, SendIcon, SmileIcon, XIcon, } from "lucide-react" import { Avatar } from "@/components/display/avatar" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { cn } from "@/lib/utils" export type ChatMessageStatus = "sending" | "sent" | "delivered" | "read" | "failed" export type ChatAttachmentData = { key: string name: string size?: React.ReactNode type?: React.ReactNode href?: string preview?: string } export type ChatParticipant = { name: string avatar?: string fallback?: React.ReactNode status?: "online" | "offline" | "busy" | "away" } export type ChatShellProps = React.ComponentProps<"section"> & { sidebar?: React.ReactNode details?: React.ReactNode } function ChatShell({ sidebar, details, children, className, ...props }: ChatShellProps) { return (
{sidebar ? : null}
{children}
{details ? : null}
) } export type ChatHeaderProps = React.ComponentProps<"header"> & { participant: ChatParticipant description?: React.ReactNode actions?: React.ReactNode } function ChatHeader({ participant, description, actions, className, ...props }: ChatHeaderProps) { return (
{participant.name}
{description ?? (participant.status === "online" ? "Online" : participant.status)}
{actions}
) } export type ConversationItem = { key: string participant: ChatParticipant preview?: React.ReactNode time?: React.ReactNode unread?: number muted?: boolean pinned?: boolean } export type ConversationListProps = Omit, "onSelect"> & { items: ConversationItem[] selectedKey?: string defaultSelectedKey?: string onSelect?: (item: ConversationItem) => void searchable?: boolean searchPlaceholder?: string empty?: React.ReactNode } function ConversationList({ items, selectedKey, defaultSelectedKey, onSelect, searchable = true, searchPlaceholder = "Search conversations...", empty = "No conversations found.", className, ...props }: ConversationListProps) { const [internalKey, setInternalKey] = React.useState(defaultSelectedKey) const [query, setQuery] = React.useState("") const activeKey = selectedKey ?? internalKey const filtered = items.filter((item) => `${item.participant.name} ${String(item.preview ?? "")}`.toLowerCase().includes(query.trim().toLowerCase())) return (
{searchable ?
} clearable aria-label={searchPlaceholder} />
: }
{filtered.length ? filtered.map((item) => { const selected = item.key === activeKey return ( ) }) :
{empty}
}
) } export type ChatMessageListProps = React.ComponentProps<"div"> & { autoScroll?: boolean empty?: React.ReactNode } function ChatMessageList({ autoScroll = true, empty = "No messages yet.", children, className, ...props }: ChatMessageListProps) { const ref = React.useRef(null) React.useEffect(() => { const element = ref.current if (!autoScroll || !element) return if (typeof element.scrollTo === "function") element.scrollTo({ top: element.scrollHeight, behavior: "smooth" }) else element.scrollTop = element.scrollHeight }, [autoScroll, children]) return
{React.Children.count(children) ?
{children}
:
{empty}
}
} export type ChatAttachmentProps = React.ComponentProps<"div"> & Omit & { attachmentKey?: string onRemove?: () => void } function ChatAttachment({ name, size, type, href, preview, onRemove, attachmentKey: _attachmentKey, className, ...props }: ChatAttachmentProps) { const content = <>{preview ? : }{name}{[type, size].filter(Boolean).join(" ยท ")} return
{href ? {content} : content}{onRemove ? : null}
} export type ChatMessageProps = React.ComponentProps<"article"> & { participant?: ChatParticipant outgoing?: boolean time?: React.ReactNode status?: ChatMessageStatus replyTo?: React.ReactNode attachments?: ChatAttachmentData[] reactions?: Array<{ key: string; label: React.ReactNode; count?: number; active?: boolean }> onReaction?: (key: string) => void actions?: React.ReactNode } function ChatMessage({ participant, outgoing = false, time, status, replyTo, attachments, reactions, onReaction, actions, children, className, ...props }: ChatMessageProps) { const StatusIcon = status === "delivered" || status === "read" ? CheckCheckIcon : CheckIcon return (
{participant ? : null}
{participant && !outgoing ?
{participant.name}
: null}
{replyTo ?
{replyTo}
: null}
{children}
{attachments?.length ?
{attachments.map(({ key, ...attachment }) => )}
: null}
{time}{status && status !== "failed" && status !== "sending" ? : null}{status === "sending" ? Sending... : null}{status === "failed" ? Failed : null}
{reactions?.length ?
{reactions.map((reaction) => )}
: null}
{actions ?
{actions}
: null}
) } function ChatTypingIndicator({ participant, className, ...props }: React.ComponentProps<"div"> & { participant?: ChatParticipant }) { return
{participant ? : null}
} export type ChatComposerProps = Omit, "onSubmit"> & { value?: string defaultValue?: string onValueChange?: (value: string) => void onSend: (value: string) => void | Promise placeholder?: string disabled?: boolean sending?: boolean attachments?: ChatAttachmentData[] onRemoveAttachment?: (key: string) => void onAttachmentClick?: () => void onEmojiClick?: () => void maxLength?: number submitLabel?: string } function ChatComposer({ value, defaultValue = "", onValueChange, onSend, placeholder = "Write a message...", disabled = false, sending = false, attachments, onRemoveAttachment, onAttachmentClick, onEmojiClick, maxLength, submitLabel = "Send message", className, ...props }: ChatComposerProps) { const [internalValue, setInternalValue] = React.useState(defaultValue) const currentValue = value ?? internalValue const setValue = (next: string) => { if (value === undefined) setInternalValue(next); onValueChange?.(next) } const send = async () => { const next = currentValue.trim() if (!next || disabled || sending) return await onSend(next) setValue("") } return (
{ event.preventDefault(); void send() }} {...props}> {attachments?.length ?
{attachments.map(({ key, ...attachment }) => onRemoveAttachment(key) : undefined} />)}
: null}
{onAttachmentClick ? : null}