بلاک‌ها · موبایل

پیام‌رسانِ اپ: فهرستِ گفت‌وگوها با جست‌وجو، عکس، آنلاین، آخرین پیام و شمارِ خوانده‌نشده؛ صفحهٔ گفت‌وگو با بازگشت و ورودیِ پیامِ چسبان.

۱. نصب کامپوننت‌ها

این بلاک از ۱۰ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  )
}