پیامهای اپ
پیشنمایش تمامصفحهپیامرسانِ اپ: فهرستِ گفتوگوها با جستوجو، عکس، آنلاین، آخرین پیام و شمارِ خواندهنشده؛ صفحهٔ گفتوگو با بازگشت و ورودیِ پیامِ چسبان.
۱. نصب کامپوننتها
این بلاک از ۱۰ آیتم دیگ استفاده میکند: app-baravatarbadgebuttonchat-messagecontrol-sizeiconspersianprompt-inputsearch-field
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chat-message.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/prompt-input.json https://docs.digdesign.ir/r/search-field.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chat-message.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/prompt-input.json https://docs.digdesign.ir/r/search-field.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chat-message.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/prompt-input.json https://docs.digdesign.ir/r/search-field.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chat-message.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/prompt-input.json https://docs.digdesign.ir/r/search-field.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
mobile-messages.tsx
"use client"
import * as React from "react"
import { ChevronRightIcon } from "@/components/icons"
import { AppBar, AppBarLeading, AppBarTitle } from "@/components/ui/app-bar"
import { Avatar } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ChatMessage, ChatMessageAvatar, ChatMessageBubble, ChatMessageContent } from "@/components/ui/chat-message"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { PromptInput, PromptInputSubmitButton, PromptInputTextarea, PromptInputToolbar, PromptInputTools, PromptInputAttachButton } from "@/components/ui/prompt-input"
import { SearchField } from "@/components/ui/search-field"
import { toFaDigits, toSearchKey } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"
type Thread = { id: string; name: string; photo: StockImageName; last: string; time: string; unread: number; online?: boolean }
const THREADS: Thread[] = [
{ id: "1", name: "سارا محمدی", photo: "personWoman2", last: "فایلِ فاکتور را فرستادم 👌", time: "۱۰:۴۲", unread: 2, online: true },
{ id: "2", name: "امیر رضایی", photo: "personMan1", last: "جلسه را بگذاریم فردا؟", time: "۰۹:۱۵", unread: 0, online: true },
{ id: "3", name: "نگار احمدی", photo: "personWoman3", last: "ممنون، رسید.", time: "دیروز", unread: 0 },
{ id: "4", name: "علی کاظمی", photo: "personMan2", last: "قیمتها بهروز شد.", time: "دوشنبه", unread: 5 },
]
/**
* پیامها در اپ: فهرستِ گفتوگوها با جستوجو، عکس، وضعیتِ آنلاین، آخرین پیام و شمارِ
* خواندهنشده؛ لمسِ هر ردیف صفحهٔ گفتوگو را با نوارِ بازگشت و ورودی پیامِ چسبان باز میکند.
*/
export default function MobileMessagesBlock() {
const [query, setQuery] = React.useState("")
const [open, setOpen] = React.useState<Thread | null>(null)
const [messages, setMessages] = React.useState([
{ id: 1, role: "assistant" as const, text: "سلام! فاکتورِ مهر آماده شد؟" },
{ id: 2, role: "user" as const, text: "بله، الان میفرستم." },
{ id: 3, role: "assistant" as const, text: "فایلِ فاکتور را فرستادم 👌" },
])
if (open) {
return (
<ControlSizeProvider size="xl" asChild>
<div className="mx-auto flex h-svh w-full max-w-md flex-col bg-background">
<AppBar>
<AppBarLeading>
<Button variant="ghost" size="icon" aria-label="بازگشت به پیامها" onClick={() => setOpen(null)}>
<ChevronRightIcon className="ltr:rotate-180" />
</Button>
</AppBarLeading>
<AppBarTitle className="flex items-center gap-3">
<Avatar size="sm" alt="" src={stockImages[open.photo].src} />
<span className="flex flex-col items-start">
{open.name}
<span className="text-xs font-normal text-muted-foreground">{open.online ? "آنلاین" : "اخیراً"}</span>
</span>
</AppBarTitle>
</AppBar>
<main className="flex flex-1 flex-col gap-4 overflow-y-auto bg-subtle p-4" aria-live="polite">
{messages.map((message) => (
<ChatMessage key={message.id} role={message.role}>
{message.role === "assistant" && <ChatMessageAvatar alt={open.name} src={stockImages[open.photo].src} />}
<ChatMessageContent>
<ChatMessageBubble>{message.text}</ChatMessageBubble>
</ChatMessageContent>
</ChatMessage>
))}
</main>
<footer className="border-t border-border p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
<PromptInput onSubmit={(text) => text.trim() && setMessages((list) => [...list, { id: list.length + 1, role: "user", text }])}>
<PromptInputTextarea placeholder="پیام…" aria-label="پیام" />
<PromptInputToolbar>
<PromptInputTools>
<PromptInputAttachButton />
</PromptInputTools>
<PromptInputSubmitButton />
</PromptInputToolbar>
</PromptInput>
</footer>
</div>
</ControlSizeProvider>
)
}
const visible = THREADS.filter((thread) => toSearchKey(thread.name + thread.last).includes(toSearchKey(query)))
return (
<ControlSizeProvider size="xl" asChild>
<div className="mx-auto flex min-h-svh w-full max-w-md flex-col bg-background">
<AppBar sticky>
<AppBarTitle>پیامها</AppBarTitle>
</AppBar>
<main className="flex flex-1 flex-col gap-4 p-4">
<SearchField aria-label="جستوجو در گفتوگوها" placeholder="جستوجو" value={query} onValueChange={setQuery} />
<ul className="flex flex-col">
{visible.map((thread) => (
<li key={thread.id}>
<Button variant="ghost" onClick={() => setOpen(thread)} className="h-auto min-h-18 w-full justify-start gap-3 px-2 py-3 text-start font-normal">
<span className="relative shrink-0">
<Avatar size="lg" alt="" src={stockImages[thread.photo].src} />
{thread.online && <span aria-label="آنلاین" className="absolute -end-0.5 -bottom-0.5 size-3.5 rounded-full bg-success ring-2 ring-background" />}
</span>
<span className="flex min-w-0 flex-1 flex-col gap-1">
<span className="flex items-center justify-between gap-2">
<span className={thread.unread ? "font-bold" : "font-medium"}>{thread.name}</span>
<span className="text-xs text-muted-foreground">{thread.time}</span>
</span>
<span className="flex items-center justify-between gap-2">
<span className={`truncate text-sm ${thread.unread ? "text-foreground" : "text-muted-foreground"}`}>{thread.last}</span>
{thread.unread > 0 && <Badge color="primary">{toFaDigits(thread.unread)}</Badge>}
</span>
</span>
</Button>
</li>
))}
</ul>
</main>
</div>
</ControlSizeProvider>
)
}