MessageBubble
Một tin nhắn trong cuộc trò chuyện: bong bóng chứa nội dung, avatar và tên người gửi tùy chọn, trích dẫn tin được trả lời, các cảm xúc, phần chân với thời gian, đã sửa, đã ghim và trạng thái gửi, và một thanh thao tác khi hover. Đặt tin nhắn trong MessageList; dùng plain cho câu trả lời dài của trợ lý.
Xem trước
import { Avatar } from "@/components/ui/avatar"
import { MessageActions } from "@/components/ui/message-actions"
import { MessageBubble } from "@/components/ui/message-bubble"
const addReaction = (emoji: string) => { /* Thêm cảm xúc của người đọc vào tin nhắn. */ }
const toggleReaction = (emoji: string) => { /* Thêm hoặc bỏ cảm xúc của người đọc. */ }
const replyToMessage = () => { /* Đưa tin nhắn vào replyTo của Composer. */ }
const forwardMessage = () => { /* Mở hộp thoại chuyển tiếp. */ }
const setPinned = (pinned: boolean) => { /* Ghim hoặc bỏ ghim tin nhắn. */ }
const startSelection = () => { /* Bật chế độ chọn với tin nhắn này được chọn. */ }
const sampleReactions = [{ emoji: "👍", count: 3, reacted: true }, { emoji: "🎉", count: 1 }]
const messageActionsLabelsVi = { toolbar: "Thao tác tin nhắn", react: "Bày tỏ cảm xúc", reply: "Trả lời", forward: "Chuyển tiếp", pin: "Ghim", select: "Chọn", moreReactions: "Thêm cảm xúc", quickReactions: "Cảm xúc nhanh" }
<MessageBubble value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:42" reactions={sampleReactions} onReactionToggle={toggleReaction} toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} labels={messageActionsLabelsVi} />} labels={{ sending: "Đang gửi", sent: "Đã gửi", delivered: "Đã nhận", read: "Đã xem", failed: "Chưa gửi được", retry: "Thử lại", edited: "Đã sửa", pinned: "Đã ghim", forwarded: "Đã chuyển tiếp", replyTo: "Trả lời", select: "Chọn tin nhắn" }} className="max-w-md pt-xl">Bạn gửi bản brief trước buổi gọi được không?</MessageBubble>Cách dùng
Khi nào dùng
- Hiện một tin nhắn trong cuộc trò chuyện giữa người với người, hoặc với trợ lý, kèm người gửi, thời gian và trạng thái.
- Gom các tin nối tiếp một tin của cùng người gửi.
Khi nào không dùng
- Một bình luận trên trang hay một bài đánh giá. Dùng Card hoặc văn bản thường.
- Một dòng trạng thái ngắn trong giao diện. Dùng Alert hoặc Toast.
- Một gợi ý trên control. Dùng Tooltip.
Biến thể
side
start cho tin của người khác, end cho tin của chính người đọc.
import { Avatar } from "@/components/ui/avatar"
import { MessageActions } from "@/components/ui/message-actions"
import { MessageBubble } from "@/components/ui/message-bubble"
const addReaction = (emoji: string) => { /* Thêm cảm xúc của người đọc vào tin nhắn. */ }
const toggleReaction = (emoji: string) => { /* Thêm hoặc bỏ cảm xúc của người đọc. */ }
const replyToMessage = () => { /* Đưa tin nhắn vào replyTo của Composer. */ }
const forwardMessage = () => { /* Mở hộp thoại chuyển tiếp. */ }
const setPinned = (pinned: boolean) => { /* Ghim hoặc bỏ ghim tin nhắn. */ }
const startSelection = () => { /* Bật chế độ chọn với tin nhắn này được chọn. */ }
const sampleReactions = [{ emoji: "👍", count: 3, reacted: true }, { emoji: "🎉", count: 1 }]
const messageActionsLabelsVi = { toolbar: "Thao tác tin nhắn", react: "Bày tỏ cảm xúc", reply: "Trả lời", forward: "Chuyển tiếp", pin: "Ghim", select: "Chọn", moreReactions: "Thêm cảm xúc", quickReactions: "Cảm xúc nhanh" }
<MessageBubble value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:42" reactions={sampleReactions} onReactionToggle={toggleReaction} toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} labels={messageActionsLabelsVi} />} labels={{ sending: "Đang gửi", sent: "Đã gửi", delivered: "Đã nhận", read: "Đã xem", failed: "Chưa gửi được", retry: "Thử lại", edited: "Đã sửa", pinned: "Đã ghim", forwarded: "Đã chuyển tiếp", replyTo: "Trả lời", select: "Chọn tin nhắn" }} className="max-w-md pt-xl">Bạn gửi bản brief trước buổi gọi được không?</MessageBubble>
<MessageBubble side="end" value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:42" reactions={sampleReactions} onReactionToggle={toggleReaction} toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} labels={messageActionsLabelsVi} />} labels={{ sending: "Đang gửi", sent: "Đã gửi", delivered: "Đã nhận", read: "Đã xem", failed: "Chưa gửi được", retry: "Thử lại", edited: "Đã sửa", pinned: "Đã ghim", forwarded: "Đã chuyển tiếp", replyTo: "Trả lời", select: "Chọn tin nhắn" }} className="max-w-md pt-xl">Bạn gửi bản brief trước buổi gọi được không?</MessageBubble>variant
default: bong bóng trung tính có viền. accent: tin của chính người đọc trong cuộc trò chuyện giữa người với người. plain: không có nền, cho câu trả lời dài của trợ lý.
import { Avatar } from "@/components/ui/avatar"
import { MessageActions } from "@/components/ui/message-actions"
import { MessageBubble } from "@/components/ui/message-bubble"
const addReaction = (emoji: string) => { /* Thêm cảm xúc của người đọc vào tin nhắn. */ }
const toggleReaction = (emoji: string) => { /* Thêm hoặc bỏ cảm xúc của người đọc. */ }
const replyToMessage = () => { /* Đưa tin nhắn vào replyTo của Composer. */ }
const forwardMessage = () => { /* Mở hộp thoại chuyển tiếp. */ }
const setPinned = (pinned: boolean) => { /* Ghim hoặc bỏ ghim tin nhắn. */ }
const startSelection = () => { /* Bật chế độ chọn với tin nhắn này được chọn. */ }
const sampleReactions = [{ emoji: "👍", count: 3, reacted: true }, { emoji: "🎉", count: 1 }]
const messageActionsLabelsVi = { toolbar: "Thao tác tin nhắn", react: "Bày tỏ cảm xúc", reply: "Trả lời", forward: "Chuyển tiếp", pin: "Ghim", select: "Chọn", moreReactions: "Thêm cảm xúc", quickReactions: "Cảm xúc nhanh" }
<MessageBubble value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:42" reactions={sampleReactions} onReactionToggle={toggleReaction} toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} labels={messageActionsLabelsVi} />} labels={{ sending: "Đang gửi", sent: "Đã gửi", delivered: "Đã nhận", read: "Đã xem", failed: "Chưa gửi được", retry: "Thử lại", edited: "Đã sửa", pinned: "Đã ghim", forwarded: "Đã chuyển tiếp", replyTo: "Trả lời", select: "Chọn tin nhắn" }} className="max-w-md pt-xl">Bạn gửi bản brief trước buổi gọi được không?</MessageBubble>
<MessageBubble variant="accent" value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:42" reactions={sampleReactions} onReactionToggle={toggleReaction} toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} labels={messageActionsLabelsVi} />} labels={{ sending: "Đang gửi", sent: "Đã gửi", delivered: "Đã nhận", read: "Đã xem", failed: "Chưa gửi được", retry: "Thử lại", edited: "Đã sửa", pinned: "Đã ghim", forwarded: "Đã chuyển tiếp", replyTo: "Trả lời", select: "Chọn tin nhắn" }} className="max-w-md pt-xl">Bạn gửi bản brief trước buổi gọi được không?</MessageBubble>
<MessageBubble variant="plain" value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:42" reactions={sampleReactions} onReactionToggle={toggleReaction} toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} labels={messageActionsLabelsVi} />} labels={{ sending: "Đang gửi", sent: "Đã gửi", delivered: "Đã nhận", read: "Đã xem", failed: "Chưa gửi được", retry: "Thử lại", edited: "Đã sửa", pinned: "Đã ghim", forwarded: "Đã chuyển tiếp", replyTo: "Trả lời", select: "Chọn tin nhắn" }} className="max-w-md pt-xl">Bạn gửi bản brief trước buổi gọi được không?</MessageBubble>status
Trạng thái gửi của tin do chính người đọc gửi: sending (đang gửi), sent (máy chủ đã nhận), delivered (đã tới thiết bị người kia), read (đã xem) hoặc failed (lỗi).
import { Avatar } from "@/components/ui/avatar"
import { MessageActions } from "@/components/ui/message-actions"
import { MessageBubble } from "@/components/ui/message-bubble"
const retrySend = () => { /* Gửi lại tin nhắn. */ }
const addReaction = (emoji: string) => { /* Thêm cảm xúc của người đọc vào tin nhắn. */ }
const toggleReaction = (emoji: string) => { /* Thêm hoặc bỏ cảm xúc của người đọc. */ }
const replyToMessage = () => { /* Đưa tin nhắn vào replyTo của Composer. */ }
const forwardMessage = () => { /* Mở hộp thoại chuyển tiếp. */ }
const setPinned = (pinned: boolean) => { /* Ghim hoặc bỏ ghim tin nhắn. */ }
const startSelection = () => { /* Bật chế độ chọn với tin nhắn này được chọn. */ }
const sampleReactions = [{ emoji: "👍", count: 3, reacted: true }, { emoji: "🎉", count: 1 }]
const messageActionsLabelsVi = { toolbar: "Thao tác tin nhắn", react: "Bày tỏ cảm xúc", reply: "Trả lời", forward: "Chuyển tiếp", pin: "Ghim", select: "Chọn", moreReactions: "Thêm cảm xúc", quickReactions: "Cảm xúc nhanh" }
<MessageBubble value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:42" status="sending" reactions={sampleReactions} onReactionToggle={toggleReaction} toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} labels={messageActionsLabelsVi} />} labels={{ sending: "Đang gửi", sent: "Đã gửi", delivered: "Đã nhận", read: "Đã xem", failed: "Chưa gửi được", retry: "Thử lại", edited: "Đã sửa", pinned: "Đã ghim", forwarded: "Đã chuyển tiếp", replyTo: "Trả lời", select: "Chọn tin nhắn" }} className="max-w-md pt-xl">Bạn gửi bản brief trước buổi gọi được không?</MessageBubble>
<MessageBubble value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:42" status="sent" reactions={sampleReactions} onReactionToggle={toggleReaction} toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} labels={messageActionsLabelsVi} />} labels={{ sending: "Đang gửi", sent: "Đã gửi", delivered: "Đã nhận", read: "Đã xem", failed: "Chưa gửi được", retry: "Thử lại", edited: "Đã sửa", pinned: "Đã ghim", forwarded: "Đã chuyển tiếp", replyTo: "Trả lời", select: "Chọn tin nhắn" }} className="max-w-md pt-xl">Bạn gửi bản brief trước buổi gọi được không?</MessageBubble>
<MessageBubble value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:42" status="delivered" reactions={sampleReactions} onReactionToggle={toggleReaction} toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} labels={messageActionsLabelsVi} />} labels={{ sending: "Đang gửi", sent: "Đã gửi", delivered: "Đã nhận", read: "Đã xem", failed: "Chưa gửi được", retry: "Thử lại", edited: "Đã sửa", pinned: "Đã ghim", forwarded: "Đã chuyển tiếp", replyTo: "Trả lời", select: "Chọn tin nhắn" }} className="max-w-md pt-xl">Bạn gửi bản brief trước buổi gọi được không?</MessageBubble>
<MessageBubble value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:42" status="read" reactions={sampleReactions} onReactionToggle={toggleReaction} toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} labels={messageActionsLabelsVi} />} labels={{ sending: "Đang gửi", sent: "Đã gửi", delivered: "Đã nhận", read: "Đã xem", failed: "Chưa gửi được", retry: "Thử lại", edited: "Đã sửa", pinned: "Đã ghim", forwarded: "Đã chuyển tiếp", replyTo: "Trả lời", select: "Chọn tin nhắn" }} className="max-w-md pt-xl">Bạn gửi bản brief trước buổi gọi được không?</MessageBubble>
<MessageBubble value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:42" status="failed" onRetry={retrySend} reactions={sampleReactions} onReactionToggle={toggleReaction} toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} labels={messageActionsLabelsVi} />} labels={{ sending: "Đang gửi", sent: "Đã gửi", delivered: "Đã nhận", read: "Đã xem", failed: "Chưa gửi được", retry: "Thử lại", edited: "Đã sửa", pinned: "Đã ghim", forwarded: "Đã chuyển tiếp", replyTo: "Trả lời", select: "Chọn tin nhắn" }} className="max-w-md pt-xl">Bạn gửi bản brief trước buổi gọi được không?</MessageBubble>Ví dụ
Hai người
import { Avatar } from "@/components/ui/avatar"
import { MessageBubble } from "@/components/ui/message-bubble"
<div className="flex w-full max-w-md flex-col gap-md">
<MessageBubble author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:41">Can you send the brief before the call?</MessageBubble>
<MessageBubble side="end" variant="accent" time="10:42" status="read">Được, mình gửi ngay đây.</MessageBubble>
</div>Tin nối tiếp
Các tin nối tiếp một tin của cùng người gửi bỏ tên và avatar, và nằm gần nhau hơn.
import { Avatar } from "@/components/ui/avatar"
import { MessageBubble } from "@/components/ui/message-bubble"
<div className="flex w-full max-w-md flex-col gap-md">
<MessageBubble author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>}>The brief looks good.</MessageBubble>
<MessageBubble grouped author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:45">One question about the timeline, though.</MessageBubble>
</div>Trạng thái gửi
Tin của chính người đọc cho biết nó đang ở đâu: đang gửi, đã gửi, đã nhận, đã xem, hoặc chưa gửi được kèm cách thử lại.
import { MessageBubble } from "@/components/ui/message-bubble"
const retrySend = () => { /* Gửi lại tin nhắn. */ }
<div className="flex w-full max-w-md flex-col gap-md">
<MessageBubble side="end" variant="accent" time="10:46" status="sending">Mình đang tới.</MessageBubble>
<MessageBubble side="end" variant="accent" time="10:46" status="sent">Mình trễ năm phút.</MessageBubble>
<MessageBubble side="end" variant="accent" time="10:47" status="delivered">Gọi giúp mình một ly cà phê nhé?</MessageBubble>
<MessageBubble side="end" variant="accent" time="10:47" status="read">Cảm ơn nhé!</MessageBubble>
<MessageBubble side="end" variant="accent" time="10:48" status="failed" onRetry={retrySend}>Hẹn gặp lúc 3 giờ.</MessageBubble>
</div>Thao tác khi hover
Hover hoặc focus tin nhắn để hiện thanh thao tác: bày tỏ cảm xúc, trả lời, chuyển tiếp, ghim và chọn.
import { Avatar } from "@/components/ui/avatar"
import { MessageActions } from "@/components/ui/message-actions"
import { MessageBubble } from "@/components/ui/message-bubble"
const addReaction = (emoji: string) => { /* Thêm cảm xúc của người đọc vào tin nhắn. */ }
const replyToMessage = () => { /* Đưa tin nhắn vào replyTo của Composer. */ }
const forwardMessage = () => { /* Mở hộp thoại chuyển tiếp. */ }
const setPinned = (pinned: boolean) => { /* Ghim hoặc bỏ ghim tin nhắn. */ }
const startSelection = () => { /* Bật chế độ chọn với tin nhắn này được chọn. */ }
<MessageBubble value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:41" toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} />} className="max-w-md pt-xl">Can you send the brief before the call?</MessageBubble>Cảm xúc, trả lời và hơn nữa
Tin trả lời trích dẫn tin được trả lời; cảm xúc bật tắt được; đã sửa, đã ghim và đã chuyển tiếp hiện thành ghi chú.
import { Avatar } from "@/components/ui/avatar"
import { MessageBubble } from "@/components/ui/message-bubble"
const toggleReaction = (emoji: string) => { /* Thêm hoặc bỏ cảm xúc của người đọc. */ }
const sampleReactions = [{ emoji: "👍", count: 3, reacted: true }, { emoji: "🎉", count: 1 }]
const sampleReply = { author: "Linh Tran", text: "Bạn gửi bản brief trước buổi gọi được không?" }
<div className="flex w-full max-w-md flex-col gap-md">
<MessageBubble forwarded author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:41" reactions={sampleReactions} onReactionToggle={toggleReaction}>The launch moved to Friday.</MessageBubble>
<MessageBubble side="end" variant="accent" edited pinned time="10:43" status="read" replyTo={sampleReply}>Vậy mình gửi bản brief vào thứ Năm.</MessageBubble>
</div>Câu trả lời của trợ lý
Plain không có nền, nên câu trả lời dài đọc như văn bản trên trang, với các thao tác bên dưới.
import { IconCopy, IconSparkles } from "@tabler/icons-react"
import { Avatar } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { MessageBubble } from "@/components/ui/message-bubble"
<MessageBubble variant="plain" author="Trợ lý" avatar={<Avatar alt="">
<IconSparkles size={16} />
</Avatar>} actions={<Button size="sm" variant="ghost" iconOnly aria-label="Sao chép">
<IconCopy size={16} />
</Button>} className="max-w-md">The report covers three regions. Sales grew in two of them, and the third held steady.</MessageBubble>Nên và không nên
Nên
- Dùng side end với variant accent cho tin của chính người đọc trong cuộc trò chuyện giữa người với người, và side start với default cho mọi người khác.
- Dùng variant plain cho câu trả lời của trợ lý, thường dài và chứa danh sách hoặc code.
- Đặt grouped cho các tin nối tiếp một tin của cùng người gửi trong vài phút.
- Truyền một MessageActions làm toolbar chỉ với các thao tác cuộc trò chuyện hỗ trợ, và hiện cảm xúc dưới bong bóng.
- Giữ tin gửi lỗi tại chỗ với status failed và onRetry, thay vì xóa nó.
Không nên
- Đừng đặt Card hay một bong bóng khác trong bong bóng.
- Đừng hiện thời gian trên mọi tin nối tiếp; hiện nó ở tin cuối của nhóm.
- Đừng phân biệt hai phía chỉ bằng màu; giữ start và end.
- Đừng giấu thao tác chỉ sau hover: MessageActions cũng hiện khi focus và khi chạm.
Props
| Tên | Kiểu | Giá trị | Mặc định | Mô tả |
|---|---|---|---|---|
| value | string | - | - | Định danh tin nhắn để chọn trong MessageList. |
| side | enum | start | end | start | start cho tin của người khác, end cho tin của chính người đọc. |
| variant | enum | default | accent | plain | default | default: bong bóng trung tính có viền. accent: tin của chính người đọc trong cuộc trò chuyện giữa người với người. plain: không có nền, cho câu trả lời dài của trợ lý. |
| author | node | - | - | Tên người gửi phía trên bong bóng. Bị ẩn trên tin nối tiếp, nhưng vẫn được đọc. |
| avatar | node | - | - | Một Avatar cạnh bong bóng. Truyền alt="" khi author đã nêu tên người gửi. |
| time | node | - | - | Thời gian hiển thị, như 10:42. |
| dateTime | string | - | - | Thời gian dạng máy đọc cho phần tử time, như 2026-10-23T10:42. |
| status | enum | sending | sent | delivered | read | failed | - | Trạng thái gửi của tin do chính người đọc gửi: sending (đang gửi), sent (máy chủ đã nhận), delivered (đã tới thiết bị người kia), read (đã xem) hoặc failed (lỗi). |
| onRetry | () => void | - | - | Với status failed: hiện nút Thử lại gọi hàm này. |
| edited | boolean | - | false | Tin đã được sửa sau khi gửi: chữ Đã sửa hiện ở phần chân. |
| pinned | boolean | - | false | Tin được ghim trong cuộc trò chuyện: một icon ghim hiện ở phần chân. |
| forwarded | boolean | - | false | Tin được chuyển tiếp từ cuộc trò chuyện khác: chữ Đã chuyển tiếp hiện phía trên. |
| replyTo | { author: React.ReactNode; text: React.ReactNode; onClick?: () => void } | - | - | Tin nhắn mà tin này trả lời, được trích dẫn phía trên bong bóng. onClick biến trích dẫn thành nút cuộn tới tin đó. |
| reactions | { emoji: string; count: number; reacted?: boolean }[] | - | - | Các cảm xúc dưới bong bóng, dạng chip kèm số lượng. reacted đánh dấu cảm xúc của chính người đọc. |
| onReactionToggle | (emoji: string) => void | - | - | Biến mỗi chip cảm xúc thành nút bật tắt để thêm hoặc bỏ cảm xúc của người đọc. |
| toolbar | node | - | - | Một MessageActions hiện trên bong bóng khi hover, focus hoặc chạm. Bị ẩn trong chế độ chọn. |
| actions | node | - | - | Các control ở phần chân, như nút Sao chép. Dùng Button ghost cỡ sm. Cho thao tác khi hover, dùng toolbar. |
| grouped | boolean | - | false | Nối tiếp tin phía trên của cùng người gửi: không có tên hay avatar, và khoảng cách nhỏ hơn. |
| labels | { sending?: string; sent?: string; delivered?: string; read?: string; failed?: string; retry?: string; edited?: string; pinned?: string; forwarded?: string; replyTo?: string; select?: string; reaction?: (emoji: string, count: number) => string } | - | - | Mọi chữ tin nhắn hiện hoặc đọc ra. Mặc định: Sending, Sent, Delivered, Seen, Not sent, Retry, Edited, Pinned, Forwarded, Reply to, Select message. Tiếng Việt: Đang gửi, Đã gửi, Đã nhận, Đã xem, Chưa gửi được, Thử lại, Đã sửa, Đã ghim, Đã chuyển tiếp, Trả lời, Chọn tin nhắn. |
| children bắt buộc | node | - | - | Tin nhắn: văn bản, hoặc văn bản kèm liên kết và các ô AttachmentPreview. |
Cấu tạo
| Phần | Mô tả |
|---|---|
| root | Một hàng: checkbox chọn, cột avatar, rồi phần thân. Đảo chiều với side end. Trong chế độ chọn, hàng có padding, bo góc, và dùng nền accent dịu khi được chọn. |
| select | Checkbox của tin nhắn trong chế độ chọn, trong một hộp cao một dòng. |
| avatar | Cột tùy chọn cho Avatar ở mép ngoài. Giữ nguyên độ rộng trên tin nối tiếp, để các bong bóng thẳng hàng. |
| body | Tên người gửi, các ghi chú, bong bóng, cảm xúc và phần chân, xếp chồng. Tối đa 80% hàng, trừ plain. |
| author | Tên người gửi phía trên bong bóng. |
| forwarded | Ghi chú phía trên tin được chuyển tiếp: icon chuyển tiếp 12px và chữ Đã chuyển tiếp. |
| reply | Trích dẫn tin được trả lời: người gửi và một dòng của tin, sau một viền đầu đậm. Là nút khi có onClick. |
| bubble | Nội dung tin nhắn. Giữ các dấu xuống dòng người gửi đã gõ và ngắt các từ dài. |
| toolbar | MessageActions truyền vào toolbar, nổi trên góc trên phía ngoài của bong bóng. |
| reactions | Hàng chip cảm xúc dưới bong bóng. Nó xuống dòng khi hẹp. |
| reaction | Một chip: emoji và số lượng, cao 24px với vùng chạm 44px. Ở trạng thái nhấn, nền accent dịu, khi người đọc đã bày tỏ cảm xúc đó. |
| footer | Thời gian, đã sửa, ghim, trạng thái, Thử lại và các thao tác dưới bong bóng. |
| status | Trạng thái gửi: một icon 12px kèm chữ cho trình đọc màn hình, hoặc chữ hiển thị cho tin gửi lỗi. |
| icon | Icon trạng thái: đồng hồ khi đang gửi, một dấu check khi đã gửi, hai dấu check khi đã nhận, hai dấu check màu icon accent khi đã xem, vòng tròn cảnh báo khi gửi lỗi. |
| retry | Nút chữ gạch chân để gửi lại tin gửi lỗi. |
| actions | Các control tùy chọn ở phần chân, như nút Sao chép. |
Khả năng truy cập
Role: generic
| Phím | Hành động |
|---|---|
| Tab | Chuyển tới trích dẫn, thanh thao tác, các cảm xúc, Thử lại và các thao tác ở phần chân, theo thứ tự đó. Thanh thao tác là một điểm dừng tab. |
| Arrow Left / Arrow Right | Trong thanh thao tác, chuyển giữa các nút (MessageActions). |
| Space | Trong chế độ chọn, trên checkbox, chọn hoặc bỏ chọn tin nhắn. Shift kèm một cú nhấp chọn cả một khoảng. |
- Tên người gửi đứng trước nội dung, nên trình đọc màn hình đọc ai viết trước. Trên tin nối tiếp, tên bị ẩn nhưng vẫn được đọc.
- Truyền alt="" cho Avatar khi author đã nêu tên cùng người đó, để tên không bị đọc hai lần.
- Thời gian là phần tử time; truyền dateTime với giá trị dạng máy đọc.
- Icon trạng thái bị ẩn với trình đọc màn hình; trạng thái được đọc bằng chữ từ labels (EN `Sent`, `Delivered`, `Seen`, VI `Đã gửi`, `Đã nhận`, `Đã xem`). Tin gửi lỗi hiện chữ của nó, EN `Not sent`, bằng màu danger, kèm nút Thử lại khi có onRetry.
- Đã xem và đã nhận dùng chung icon hai dấu check; đã xem dùng màu icon accent, và chữ phân biệt chúng cho trình đọc màn hình.
- Trích dẫn tin được trả lời được đọc với EN `Reply to` trước người gửi. Khi có onClick, nó là nút cuộn tới tin đó.
- Mỗi chip cảm xúc là nút bật tắt có aria-pressed, tên EN `👍 2`; không có onReactionToggle thì các chip là văn bản.
- Thanh thao tác hiện khi hover, khi bất kỳ phần nào của tin có focus, khi popover emoji đang mở, và sau một lần chạm trên màn hình cảm ứng. Nó không bao giờ là cách duy nhất tới một thao tác: focus bằng bàn phím cũng làm nó hiện.
- Trong chế độ chọn, mỗi tin có một checkbox tên EN `Select message` và được mô tả bằng nội dung tin; nhấn bất kỳ đâu trên hàng để bật tắt, và hàng được chọn dùng nền accent dịu.
- Màu không bao giờ tự mình thể hiện phía: bong bóng còn nằm ở đầu hoặc cuối hàng.
Cài đặt
npx werewolf-ui add message-bubbleChạy npx werewolf-ui init một lần trong app trước khi thêm component. Init cài token, thêm CSS import và luật cho agent. Add cài component này cùng các dependency và giữ nguyên theme của bạn. Xem Cài đặt để biết yêu cầu và cách cài qua registry.