From 6513dc39b6c29b77601e493e9bcdeb3ad4b589f9 Mon Sep 17 00:00:00 2001 From: Khairul Hidayat Date: Mon, 21 Sep 2026 09:21:18 +0800 Subject: [PATCH] feat: add reply msg --- ui/src/app/messages.tsx | 155 ++++++++++++++++++++++++++++++---------- ui/src/lib/utils.ts | 10 +++ 2 files changed, 129 insertions(+), 36 deletions(-) diff --git a/ui/src/app/messages.tsx b/ui/src/app/messages.tsx index ac0e40a..a95dac1 100644 --- a/ui/src/app/messages.tsx +++ b/ui/src/app/messages.tsx @@ -1,5 +1,10 @@ import { useWAClient } from "./context"; -import { getFilesAsBase64, getFormattedName, getId } from "@/lib/utils"; +import { + copyToClipboard, + getFilesAsBase64, + getFormattedName, + getId, +} from "@/lib/utils"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { cn } from "cn"; import { @@ -7,11 +12,13 @@ import { CheckCheckIcon, CheckIcon, ChevronDown, + CopyIcon, CornerUpRightIcon, Loader2Icon, MousePointer2Icon, PaperclipIcon, PhoneCallIcon, + ReplyIcon, SendIcon, Trash2Icon, XIcon, @@ -39,6 +46,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { forwardDlg } from "./stores"; +import type { ContactDTO, MessageDTO } from "@shared/dto"; dayjs.extend(relativeTime); @@ -47,6 +55,11 @@ type Props = { maxCount?: number; }; +type MessageDTOWithStrID = Omit & { + id: string; + from?: ContactDTO; +}; + export default function MessagesSection({ chatId, maxCount = 20 }: Props) { const { client, isReady } = useWAClient(); const router = useHashRouter(); @@ -58,6 +71,7 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) { const { data: contacts } = useContacts(); const [search, setSearch] = useState(""); const [selectedIds, setSelectedIds] = useState | null>(null); + const [replyTo, setReplyTo] = useState(null); const { data: messages, @@ -153,6 +167,7 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) { const quotedMsg = i.quotedMsg ? { ...i.quotedMsg, + fromMe: i.fromMe, from: contacts?.find((c) => { return ( getId(c.id) === @@ -349,10 +364,31 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) { + setReplyTo(item as never)}> + + Reply + + + + onSelectMsg(item.id)}> Select + + + copyToClipboard( + item.caption || + (item.type === "chat" ? item.body : null) || + "", + ) + } + > + + Copy + + forwardDlg.onOpen({ messageIds: [item.id] }) @@ -361,6 +397,7 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) { Forward + onDeleteMsg(item.id)}> @@ -383,39 +420,11 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) { ["chat", "image", "document"].includes( item.quotedMsg.type as string, ) ? ( - + ) : null} {item.type === "image" || @@ -501,17 +510,72 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) { - + ); } +function QuotedMsg({ + data, + getMentionLabel, + onMentionClick, + className, +}: { + data: MessageDTOWithStrID; + getMentionLabel?: (username: string) => string; + onMentionClick?: (username: string) => void; + className?: string; +}) { + return ( + + ); +} + function MessageInputSection({ chatId, refetch, + replyTo, + setReplyTo, + getMentionLabel, }: { chatId?: string | null; refetch: () => void; + replyTo?: MessageDTOWithStrID | null; + setReplyTo?: (id: MessageDTOWithStrID | null) => void; + getMentionLabel?: (username: string) => string; }) { const fileRef = useRef(null); const { client, isReady } = useWAClient(); @@ -559,7 +623,7 @@ function MessageInputSection({ try { setPending(true); - const options = { waitForAck: true }; + const options = { waitForAck: true, quotedMsg: replyTo?.id || undefined }; if (files.length > 0) { const fileList = await getFilesAsBase64(files.map((f) => f.file)); @@ -583,6 +647,7 @@ function MessageInputSection({ setTimeout(refetch, 1000); setFiles([]); setMessage(""); + setReplyTo?.(null); } catch (err) { alert((err as Error).message); } finally { @@ -596,6 +661,24 @@ function MessageInputSection({ return (
+ {replyTo != null && ( +
+ + + +
+ )} + (data?: T | null) { useStore: () => useStore(store), }); } + +export async function copyToClipboard(text: string) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch (err) { + console.error("Failed to copy text: ", err); + return false; + } +}