feat: add forward msg

This commit is contained in:
2026-09-20 09:29:07 +08:00
parent c2af4f9f6b
commit c9c55f56c8
10 changed files with 990 additions and 61 deletions
+117 -54
View File
@@ -7,7 +7,9 @@ import {
CheckCheckIcon,
CheckIcon,
ChevronDown,
CornerUpRightIcon,
Loader2Icon,
MousePointer2Icon,
PaperclipIcon,
PhoneCallIcon,
SendIcon,
@@ -32,9 +34,11 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { forwardDlg } from "./stores";
dayjs.extend(relativeTime);
@@ -53,6 +57,7 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
const { data: chats } = useChats();
const { data: contacts } = useContacts();
const [search, setSearch] = useState("");
const [selectedIds, setSelectedIds] = useState<Set<string> | null>(null);
const {
data: messages,
@@ -83,7 +88,10 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
});
useEffect(() => {
if (chatId) msgContainerRef.current.scrollTop = 0;
if (chatId) {
msgContainerRef.current.scrollTop = 0;
setSelectedIds(null);
}
}, [chatId]);
useMessageEvent((data) => {
@@ -163,11 +171,23 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
});
}, [messages, contacts, search]);
async function onDeleteMsg(id: string) {
function onSelectMsg(id: string) {
setSelectedIds((s) => {
const set = new Set(s || []);
if (!set.has(id)) {
set.add(id);
} else {
set.delete(id);
}
return !set.size ? null : set;
});
}
async function onDeleteMsg(id: string | string[]) {
if (!window.confirm("Delete this message?")) return;
try {
await client.exec(
`return WPP.chat.deleteMessage('${chatId}', '${id}', true, true);`,
`return WPP.chat.deleteMessage('${chatId}', ${JSON.stringify(id)}, true, true);`,
);
refetch();
} catch (err) {
@@ -192,63 +212,82 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
className={`flex min-w-0 h-full flex-1 flex-col bg-muted ${chatId ? "flex" : "hidden md:flex"}`}
>
<header className="flex items-center justify-between border-b border-border bg-card px-4 py-3 sm:px-6">
<div className="flex min-w-0 items-center gap-3 w-full">
<button
aria-label="Back to conversations"
onClick={() => router.goBack()}
className="grid size-9 place-items-center rounded-full text-muted-foreground hover:bg-muted md:hidden cursor-pointer -ml-2"
>
<ArrowLeftIcon size={19} />
</button>
{selectedIds?.size ? (
<div className="flex items-center gap-3 w-full">
<button
aria-label="Cancel selection"
onClick={() => setSelectedIds(null)}
className="grid size-10 place-items-center rounded-full text-muted-foreground hover:bg-muted cursor-pointer -ml-2"
>
<XIcon size={19} />
</button>
<p className="text-sm font-medium truncate">{`${selectedIds.size} selected`}</p>
<div className="flex-1" />
<Avatar
name={chat?.formattedTitle || chat?.name}
className="size-10"
/>
<Button
variant="ghost"
size="icon-lg"
className="rounded-full"
title="Forward"
onClick={() => {
forwardDlg.onOpen({ messageIds: [...selectedIds] });
setSelectedIds(null);
}}
>
<CornerUpRightIcon />
</Button>
<Button
variant="ghost"
size="icon-lg"
className="rounded-full"
title="Delete"
onClick={() => {
onDeleteMsg([...selectedIds]);
setSelectedIds(null);
}}
>
<Trash2Icon />
</Button>
</div>
) : (
<div className="flex min-w-0 items-center gap-3 w-full">
<button
aria-label="Back to conversations"
onClick={() => router.goBack()}
className="grid size-9 place-items-center rounded-full text-muted-foreground hover:bg-muted md:hidden cursor-pointer -ml-2"
>
<ArrowLeftIcon size={19} />
</button>
<div className="min-w-0 flex-1 truncate">
<h2 className="truncate text-sm font-semibold">
{chat?.formattedTitle || chat?.name}
</h2>
<Avatar
name={chat?.formattedTitle || chat?.name}
className="size-10 -ml-2"
/>
{/* <p className="text-xs text-[#1ea66a]">
<div className="min-w-0 flex-1 truncate">
<h2 className="truncate text-sm font-semibold">
{chat?.formattedTitle || chat?.name}
</h2>
{/* <p className="text-xs text-[#1ea66a]">
{selected.online ? "Online now" : "Active recently"}
</p> */}
</div>
</div>
<SearchInput
className="w-full max-w-40 md:max-w-sm"
inputClassName="text-xs placeholder:text-xs"
placeholder="Search..."
defaultValue={search}
onValueChange={setSearch}
/>
</div>
{/* <div className="flex items-center gap-1 text-muted-foreground">
<button
aria-label="Start video call"
className="grid size-9 place-items-center rounded-full hover:bg-muted"
>
<VideoIcon size={18} />
</button>
<button
aria-label="Start voice call"
className="grid size-9 place-items-center rounded-full hover:bg-muted"
>
<Phone size={18} />
</button>
<button
aria-label="Chat details"
className="grid size-9 place-items-center rounded-full hover:bg-muted"
>
<EllipsisVertical size={18} />
</button>
</div> */}
<SearchInput
className="w-full max-w-40 md:max-w-sm"
inputClassName="text-xs placeholder:text-xs"
placeholder="Search..."
defaultValue={search}
onValueChange={setSearch}
/>
</div>
)}
</header>
<div
ref={msgContainerRef}
className="message-pattern flex-1 px-4 py-6 sm:px-8 flex flex-col-reverse overflow-y-auto"
className="message-pattern flex-1 py-6 flex flex-col-reverse overflow-y-auto"
>
{/* <div className="mb-6 flex items-center justify-center gap-2 text-[11px] text-muted-foreground">
<span className="h-px flex-1 bg-border" />
@@ -261,9 +300,20 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
id={item.id.replace(/^(?:false_|true_)/, "")}
key={`${item.timestamp}-${index}`}
className={cn(
"mb-3 flex",
"flex px-4 sm:px-8 py-1.5",
item.fromMe ? "justify-end" : "justify-start",
selectedIds != null && "cursor-pointer hover:bg-[#26855c]/10",
selectedIds?.has(item.id) && "bg-[#26855c]/30!",
)}
role={selectedIds != null ? "button" : undefined}
onClick={
selectedIds != null
? (e) => {
if (e.target !== e.currentTarget) return;
onSelectMsg(item.id);
}
: undefined
}
>
<div className="max-w-[82%] sm:max-w-[65%]">
<a
@@ -280,7 +330,7 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
</a>
<div
className={cn(
"rounded-2xl px-4 py-2.5 text-sm shadow-sm relative group",
"rounded-2xl px-4 py-2.5 text-sm shadow-sm relative group rounded-tr-md",
item.fromMe
? "rounded-br-md bg-[#d8f5e8] dark:bg-[#26855c] text-foreground"
: "rounded-bl-md bg-card text-card-foreground",
@@ -290,15 +340,28 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
<DropdownMenuTrigger
render={
<Button
variant="default"
variant="ghost"
size="icon-sm"
className="absolute top-1 right-1 z-1 rounded-full opacity-0 group-hover:opacity-100"
className="absolute top-0 right-0 z-1 md:opacity-0 group-hover:opacity-100"
/>
}
>
<ChevronDown />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onSelectMsg(item.id)}>
<MousePointer2Icon />
Select
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
forwardDlg.onOpen({ messageIds: [item.id] })
}
>
<CornerUpRightIcon />
Forward
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onDeleteMsg(item.id)}>
<Trash2Icon />
Delete