feat(conversations): discord-style composer + emoji picker
Redesign message composer to match Discord's compact input bar with + button, inline editor, and action icons. Add emoji-mart picker in popover that follows the active theme. Switch channel page to CSS grid layout to fix composer overflow issues.
This commit is contained in:
parent
ec095fa1db
commit
1523d576b3
8
bun.lock
8
bun.lock
@ -26,6 +26,8 @@
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@json-render/core": "^0.4.0",
|
||||
"@json-render/react": "^0.4.0",
|
||||
@ -372,6 +374,10 @@
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@emoji-mart/data": ["@emoji-mart/data@1.2.1", "", {}, "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="],
|
||||
|
||||
"@emoji-mart/react": ["@emoji-mart/react@1.1.1", "", { "peerDependencies": { "emoji-mart": "^5.2", "react": "^16.8 || ^17 || ^18" } }, "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g=="],
|
||||
|
||||
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
|
||||
|
||||
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
|
||||
@ -1554,6 +1560,8 @@
|
||||
|
||||
"embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="],
|
||||
|
||||
"emoji-mart": ["emoji-mart@5.6.0", "", {}, "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
@ -53,6 +53,8 @@
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@json-render/core": "^0.4.0",
|
||||
"@json-render/react": "^0.4.0",
|
||||
|
||||
@ -26,7 +26,10 @@ export default async function ChannelPage({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div
|
||||
className="grid h-full overflow-hidden"
|
||||
style={{ gridTemplateRows: "auto 1fr auto" }}
|
||||
>
|
||||
<ChannelHeader
|
||||
name={channel.name}
|
||||
description={channel.description ?? undefined}
|
||||
|
||||
@ -231,6 +231,23 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* constrain composer editor height */
|
||||
.composer-editor .ProseMirror {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@media (max-width: 639px) {
|
||||
.composer-editor .ProseMirror {
|
||||
max-height: 30vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* emoji-mart theme overrides */
|
||||
em-emoji-picker {
|
||||
--border-radius: 12px;
|
||||
max-height: 350px;
|
||||
}
|
||||
|
||||
/* mention pill styling in messages and editor */
|
||||
.mention {
|
||||
border-radius: calc(var(--radius) - 4px);
|
||||
|
||||
@ -20,7 +20,7 @@ export function MainContent({
|
||||
<div
|
||||
{...rest}
|
||||
className={cn(
|
||||
"flex flex-col overflow-x-hidden min-w-0",
|
||||
"flex flex-col overflow-x-hidden min-w-0 min-h-0",
|
||||
"transition-[flex,opacity] duration-300 ease-in-out",
|
||||
isCollapsed
|
||||
? "flex-[0_0_0%] opacity-0 overflow-hidden pointer-events-none"
|
||||
@ -31,7 +31,7 @@ export function MainContent({
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"@container/main flex flex-1 flex-col min-w-0",
|
||||
"@container/main flex flex-1 flex-col min-w-0 min-h-0",
|
||||
isConversations && "overflow-hidden"
|
||||
)}>
|
||||
{children}
|
||||
|
||||
@ -6,15 +6,79 @@ import StarterKit from "@tiptap/starter-kit"
|
||||
import Placeholder from "@tiptap/extension-placeholder"
|
||||
import Link from "@tiptap/extension-link"
|
||||
import Mention from "@tiptap/extension-mention"
|
||||
import { Bold, Italic, Code, Link as LinkIcon, List, ListOrdered, Send, Paperclip, Smile } from "lucide-react"
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Code,
|
||||
List,
|
||||
ListOrdered,
|
||||
Plus,
|
||||
Smile,
|
||||
Sticker,
|
||||
Gift,
|
||||
} from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import { sendMessage } from "@/app/actions/chat-messages"
|
||||
import { setTyping } from "@/app/actions/conversations-realtime"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useTheme } from "next-themes"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { createMentionSuggestion } from "./mention-suggestion"
|
||||
|
||||
// lazy-load emoji picker to keep initial bundle small
|
||||
const EmojiPicker = React.lazy(() =>
|
||||
import("@emoji-mart/react").then((mod) => ({ default: mod.default })),
|
||||
)
|
||||
|
||||
type EmojiData = {
|
||||
readonly native: string
|
||||
}
|
||||
|
||||
/** read a CSS custom property and resolve it to an "R, G, B" string */
|
||||
function cssVarToRgb(varName: string): string | null {
|
||||
if (typeof window === "undefined") return null
|
||||
const style = getComputedStyle(document.documentElement)
|
||||
const raw = style.getPropertyValue(varName).trim()
|
||||
if (!raw) return null
|
||||
|
||||
// create a temporary element to resolve the color
|
||||
const el = document.createElement("div")
|
||||
el.style.color = raw
|
||||
document.body.appendChild(el)
|
||||
const computed = getComputedStyle(el).color
|
||||
document.body.removeChild(el)
|
||||
|
||||
// computed is like "rgb(R, G, B)" or "rgba(R, G, B, A)"
|
||||
const match = computed.match(
|
||||
/rgba?\(\s*([\d.]+),?\s*([\d.]+),?\s*([\d.]+)/,
|
||||
)
|
||||
if (!match) return null
|
||||
return `${Math.round(Number(match[1]))}, ${Math.round(Number(match[2]))}, ${Math.round(Number(match[3]))}`
|
||||
}
|
||||
|
||||
function useEmojiThemeVars(): Record<string, string> {
|
||||
const [vars, setVars] = React.useState<Record<string, string>>({})
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
React.useEffect(() => {
|
||||
const bg = cssVarToRgb("--popover")
|
||||
const fg = cssVarToRgb("--popover-foreground")
|
||||
const input = cssVarToRgb("--muted")
|
||||
const next: Record<string, string> = {}
|
||||
if (bg) next["--em-rgb-background"] = bg
|
||||
if (fg) next["--em-rgb-color"] = fg
|
||||
if (input) next["--em-rgb-input"] = input
|
||||
setVars(next)
|
||||
}, [resolvedTheme])
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
type MessageComposerProps = {
|
||||
readonly channelId: string
|
||||
readonly channelName: string
|
||||
@ -29,7 +93,9 @@ type MentionInput = {
|
||||
targetId: string | null
|
||||
}
|
||||
|
||||
function extractMentions(json: Record<string, unknown>): Array<MentionInput> {
|
||||
function extractMentions(
|
||||
json: Record<string, unknown>,
|
||||
): Array<MentionInput> {
|
||||
const mentions: Array<MentionInput> = []
|
||||
|
||||
function walk(node: Record<string, unknown>) {
|
||||
@ -42,7 +108,10 @@ function extractMentions(json: Record<string, unknown>): Array<MentionInput> {
|
||||
} else if (id === "here") {
|
||||
mentions.push({ mentionType: "here", targetId: null })
|
||||
} else if (id === "compass-agent") {
|
||||
mentions.push({ mentionType: "agent", targetId: "compass-agent" })
|
||||
mentions.push({
|
||||
mentionType: "agent",
|
||||
targetId: "compass-agent",
|
||||
})
|
||||
} else {
|
||||
mentions.push({ mentionType: "user", targetId: id })
|
||||
}
|
||||
@ -68,10 +137,13 @@ export function MessageComposer({
|
||||
onSent,
|
||||
}: MessageComposerProps) {
|
||||
const router = useRouter()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const emojiThemeVars = useEmojiThemeVars()
|
||||
const [isSending, setIsSending] = React.useState(false)
|
||||
const [error, setError] = React.useState<string | null>(null)
|
||||
const [showToolbar, setShowToolbar] = React.useState(false)
|
||||
const [emojiOpen, setEmojiOpen] = React.useState(false)
|
||||
|
||||
// typing indicator - debounce to avoid spamming server
|
||||
const lastTypingSentRef = React.useRef<number>(0)
|
||||
const TYPING_DEBOUNCE_MS = 3000
|
||||
|
||||
@ -80,7 +152,10 @@ export function MessageComposer({
|
||||
if (now - lastTypingSentRef.current >= TYPING_DEBOUNCE_MS) {
|
||||
lastTypingSentRef.current = now
|
||||
setTyping(channelId).catch((err) => {
|
||||
console.error("[MessageComposer] typing indicator error:", err)
|
||||
console.error(
|
||||
"[MessageComposer] typing indicator error:",
|
||||
err,
|
||||
)
|
||||
})
|
||||
}
|
||||
}, [channelId])
|
||||
@ -94,7 +169,7 @@ export function MessageComposer({
|
||||
blockquote: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: placeholder ?? `Message #${channelName}...`,
|
||||
placeholder: placeholder ?? `Message #${channelName}`,
|
||||
}),
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
@ -112,7 +187,11 @@ export function MessageComposer({
|
||||
],
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "prose prose-sm max-w-none focus:outline-none min-h-[80px] p-3",
|
||||
class: cn(
|
||||
"prose prose-sm max-w-none focus:outline-none",
|
||||
"min-h-[22px] py-[11px] px-0",
|
||||
"text-sm leading-[22px]",
|
||||
),
|
||||
},
|
||||
},
|
||||
onUpdate: () => {
|
||||
@ -121,6 +200,15 @@ export function MessageComposer({
|
||||
},
|
||||
})
|
||||
|
||||
const handleEmojiSelect = React.useCallback(
|
||||
(emoji: EmojiData) => {
|
||||
if (!editor) return
|
||||
editor.chain().focus().insertContent(emoji.native).run()
|
||||
setEmojiOpen(false)
|
||||
},
|
||||
[editor],
|
||||
)
|
||||
|
||||
const handleSend = React.useCallback(async () => {
|
||||
if (!editor || isSending) return
|
||||
|
||||
@ -131,7 +219,9 @@ export function MessageComposer({
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const mentions = extractMentions(editor.getJSON() as Record<string, unknown>)
|
||||
const mentions = extractMentions(
|
||||
editor.getJSON() as Record<string, unknown>,
|
||||
)
|
||||
const contentHtml = editor.getHTML()
|
||||
|
||||
const result = await sendMessage({
|
||||
@ -150,7 +240,9 @@ export function MessageComposer({
|
||||
setError(result.error ?? "Failed to send message")
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to send message")
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Failed to send message",
|
||||
)
|
||||
} finally {
|
||||
setIsSending(false)
|
||||
}
|
||||
@ -175,102 +267,205 @@ export function MessageComposer({
|
||||
}, [editor, handleSend])
|
||||
|
||||
return (
|
||||
<div className="shrink-0 border-t bg-background p-4">
|
||||
<div className="rounded-lg border bg-background">
|
||||
<EditorContent editor={editor} className="max-h-[200px] overflow-y-auto" />
|
||||
|
||||
{editor && (
|
||||
<div className="flex items-center justify-between border-t p-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="min-h-[68px] px-2 pb-4 pt-2 sm:px-4">
|
||||
{/* formatting toolbar */}
|
||||
{editor && showToolbar && (
|
||||
<div className="mb-1.5 flex items-center gap-0.5 pl-10 sm:pl-12">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
disabled={!editor.can().chain().focus().toggleBold().run()}
|
||||
className="h-7 w-7 rounded-md text-muted-foreground hover:text-foreground"
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleBold().run()
|
||||
}
|
||||
disabled={
|
||||
!editor.can().chain().focus().toggleBold().run()
|
||||
}
|
||||
>
|
||||
<Bold className={cn(
|
||||
<Bold
|
||||
className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
editor.isActive("bold") && "text-primary"
|
||||
)} />
|
||||
editor.isActive("bold") && "text-primary",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
disabled={!editor.can().chain().focus().toggleItalic().run()}
|
||||
className="h-7 w-7 rounded-md text-muted-foreground hover:text-foreground"
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleItalic().run()
|
||||
}
|
||||
disabled={
|
||||
!editor.can().chain().focus().toggleItalic().run()
|
||||
}
|
||||
>
|
||||
<Italic className={cn(
|
||||
<Italic
|
||||
className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
editor.isActive("italic") && "text-primary"
|
||||
)} />
|
||||
editor.isActive("italic") && "text-primary",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
disabled={!editor.can().chain().focus().toggleCode().run()}
|
||||
className="h-7 w-7 rounded-md text-muted-foreground hover:text-foreground"
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleCode().run()
|
||||
}
|
||||
disabled={
|
||||
!editor.can().chain().focus().toggleCode().run()
|
||||
}
|
||||
>
|
||||
<Code className={cn(
|
||||
<Code
|
||||
className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
editor.isActive("code") && "text-primary"
|
||||
)} />
|
||||
editor.isActive("code") && "text-primary",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
className="h-7 w-7 rounded-md text-muted-foreground hover:text-foreground"
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleBulletList().run()
|
||||
}
|
||||
>
|
||||
<List className={cn(
|
||||
<List
|
||||
className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
editor.isActive("bulletList") && "text-primary"
|
||||
)} />
|
||||
editor.isActive("bulletList") && "text-primary",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
className="h-7 w-7 rounded-md text-muted-foreground hover:text-foreground"
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleOrderedList().run()
|
||||
}
|
||||
>
|
||||
<ListOrdered className={cn(
|
||||
<ListOrdered
|
||||
className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
editor.isActive("orderedList") && "text-primary"
|
||||
)} />
|
||||
</Button>
|
||||
|
||||
<Separator orientation="vertical" className="mx-1 h-6" />
|
||||
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" disabled>
|
||||
<Paperclip className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7">
|
||||
<Smile className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSend}
|
||||
disabled={isSending || !editor.getText().trim()}
|
||||
>
|
||||
<Send className="mr-1.5 h-3.5 w-3.5" />
|
||||
Send
|
||||
editor.isActive("orderedList") && "text-primary",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* main composer bar */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex items-end rounded-lg",
|
||||
"bg-muted/50 ring-1 ring-border",
|
||||
"focus-within:ring-2 focus-within:ring-ring",
|
||||
"transition-shadow",
|
||||
)}
|
||||
>
|
||||
{/* + button */}
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex h-[44px] w-[44px] shrink-0 items-center justify-center",
|
||||
"text-muted-foreground",
|
||||
"hover:text-foreground transition-colors",
|
||||
)}
|
||||
onClick={() => setShowToolbar((prev) => !prev)}
|
||||
aria-label="Toggle formatting"
|
||||
>
|
||||
<Plus
|
||||
className={cn(
|
||||
"h-5 w-5 transition-transform duration-200",
|
||||
showToolbar && "rotate-45",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* editor area */}
|
||||
<EditorContent
|
||||
editor={editor}
|
||||
className="composer-editor min-w-0 flex-1"
|
||||
/>
|
||||
|
||||
{/* right-side action icons */}
|
||||
<div className="flex h-[44px] shrink-0 items-center gap-0 pr-1 sm:pr-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"hidden sm:flex",
|
||||
"h-8 w-8 items-center justify-center rounded-md",
|
||||
"text-muted-foreground hover:text-foreground transition-colors",
|
||||
)}
|
||||
aria-label="Stickers"
|
||||
>
|
||||
<Sticker className="h-[18px] w-[18px]" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"hidden sm:flex",
|
||||
"h-8 w-8 items-center justify-center rounded-md",
|
||||
"text-muted-foreground hover:text-foreground transition-colors",
|
||||
)}
|
||||
aria-label="GIF"
|
||||
>
|
||||
<Gift className="h-[18px] w-[18px]" />
|
||||
</button>
|
||||
|
||||
{/* emoji picker */}
|
||||
<Popover open={emojiOpen} onOpenChange={setEmojiOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex h-8 w-8 items-center justify-center rounded-md",
|
||||
"text-muted-foreground hover:text-foreground transition-colors",
|
||||
emojiOpen && "text-foreground",
|
||||
)}
|
||||
aria-label="Emoji"
|
||||
>
|
||||
<Smile className="h-[18px] w-[18px]" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="top"
|
||||
align="end"
|
||||
sideOffset={8}
|
||||
className="w-auto border-none bg-transparent p-0 shadow-none"
|
||||
>
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<div className="flex h-[350px] w-[352px] items-center justify-center rounded-lg border bg-popover">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Loading...
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={emojiThemeVars as React.CSSProperties}>
|
||||
<EmojiPicker
|
||||
onEmojiSelect={handleEmojiSelect}
|
||||
theme={resolvedTheme === "dark" ? "dark" : "light"}
|
||||
set="native"
|
||||
skinTonePosition="search"
|
||||
previewPosition="none"
|
||||
maxFrequentRows={2}
|
||||
/>
|
||||
</div>
|
||||
</React.Suspense>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="mt-2 text-xs text-destructive">{error}</p>
|
||||
<p className="mt-1.5 text-xs text-destructive">{error}</p>
|
||||
)}
|
||||
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
<kbd className="rounded border bg-muted px-1.5 py-0.5 font-mono text-xs">Enter</kbd> to send,{" "}
|
||||
<kbd className="rounded border bg-muted px-1.5 py-0.5 font-mono text-xs">Shift+Enter</kbd> for new line
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -165,7 +165,7 @@ export function MessageList({ channelId, initialMessages }: MessageListProps) {
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center p-8">
|
||||
<div className="flex h-full min-h-0 items-center justify-center p-8 overflow-hidden">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No messages yet. Start the conversation!
|
||||
</p>
|
||||
@ -174,7 +174,7 @@ export function MessageList({ channelId, initialMessages }: MessageListProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="flex-1" ref={scrollRef}>
|
||||
<ScrollArea className="h-full min-h-0 flex-1" ref={scrollRef}>
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
{hasMore && (
|
||||
<div className="flex justify-center">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user