From 4d708055814115745c72d0537a1d7894d1e40b9d Mon Sep 17 00:00:00 2001 From: Khairul Hidayat Date: Fri, 18 Sep 2026 22:45:15 +0800 Subject: [PATCH] feat: add remove message feature --- ui/src/app/messages.tsx | 77 ++++++- ui/src/components/ui/dropdown-menu.tsx | 269 +++++++++++++++++++++++++ 2 files changed, 336 insertions(+), 10 deletions(-) create mode 100644 ui/src/components/ui/dropdown-menu.tsx diff --git a/ui/src/app/messages.tsx b/ui/src/app/messages.tsx index 4c56946..2457676 100644 --- a/ui/src/app/messages.tsx +++ b/ui/src/app/messages.tsx @@ -6,10 +6,12 @@ import { ArrowLeftIcon, CheckCheckIcon, CheckIcon, + ChevronDown, Loader2Icon, PaperclipIcon, PhoneCallIcon, SendIcon, + Trash2Icon, XIcon, } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -26,6 +28,13 @@ import Paragraph from "@/components/paragraph"; import { nanoid } from "nanoid"; import { Skeleton } from "@/components/ui/skeleton"; import SearchInput from "@/components/search-input"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; dayjs.extend(relativeTime); @@ -34,20 +43,17 @@ type Props = { maxCount?: number; }; -export default function MessagesSection({ chatId, maxCount = 50 }: Props) { +export default function MessagesSection({ chatId, maxCount = 20 }: Props) { const { client, isReady } = useWAClient(); const router = useHashRouter(); const msgContainerRef = useRef(null!); + const lastFetch = useRef(0); const queryClient = useQueryClient(); const { data: chats } = useChats(); const { data: contacts } = useContacts(); const [search, setSearch] = useState(""); - useEffect(() => { - if (chatId) msgContainerRef.current.scrollTop = 0; - }, [chatId]); - const { data: messages, isLoading, @@ -76,6 +82,10 @@ export default function MessagesSection({ chatId, maxCount = 50 }: Props) { select: (data) => data.pages.flat(), }); + useEffect(() => { + if (chatId) msgContainerRef.current.scrollTop = 0; + }, [chatId]); + useMessageEvent((data) => { if (data.chatId !== chatId) return; @@ -90,9 +100,15 @@ export default function MessagesSection({ chatId, maxCount = 50 }: Props) { }); const endScrollRef = useIntersection(() => { - if (hasNextPage && !isFetchingNextPage && !isLoading) { - fetchNextPage(); - } + if (!hasNextPage || isFetchingNextPage || isLoading) return; + + const t = Date.now() - lastFetch.current; + if (t < 1000) return; + + fetchNextPage().then(() => { + lastFetch.current = Date.now(); + msgContainerRef.current.scrollTop -= 10; + }); }); const chat = useMemo( @@ -147,6 +163,18 @@ export default function MessagesSection({ chatId, maxCount = 50 }: Props) { }); }, [messages, contacts, search]); + async function onDeleteMsg(id: string) { + if (!window.confirm("Delete this message?")) return; + try { + await client.exec( + `return WPP.chat.deleteMessage('${chatId}', '${id}', true, true);`, + ); + refetch(); + } catch (err) { + console.error(err); + } + } + function getMentionLabel(username: string) { const data = contacts?.find((c) => (c.id as any).user === username); return data?.notifyName || data?.formattedName || data?.name || username; @@ -232,7 +260,10 @@ export default function MessagesSection({ chatId, maxCount = 50 }: Props) {
+ + + } + > + + + + onDeleteMsg(item.id)}> + + Delete + + + + {item.isForwarded ? (

Forwarded @@ -522,6 +578,7 @@ function MessageInputSection({ value={message} onChange={(event) => setMessage(event.target.value)} onPaste={onPaste} + enterKeyHint="enter" onKeyDown={(event) => { if ( event.key === "Enter" && diff --git a/ui/src/components/ui/dropdown-menu.tsx b/ui/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..42fabc2 --- /dev/null +++ b/ui/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,269 @@ +import * as React from "react"; +import { Menu as MenuPrimitive } from "@base-ui/react/menu"; +import { cn } from "cn"; +import { ChevronRightIcon, CheckIcon } from "lucide-react"; + +function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { + return ; +} + +function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) { + return ; +} + +function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) { + return ; +} + +function DropdownMenuContent({ + align = "start", + alignOffset = 0, + side = "bottom", + sideOffset = 4, + className, + ...props +}: MenuPrimitive.Popup.Props & + Pick< + MenuPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" + >) { + return ( + + + + + + ); +} + +function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) { + return ; +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: MenuPrimitive.GroupLabel.Props & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: MenuPrimitive.Item.Props & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: MenuPrimitive.SubmenuTrigger.Props & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + align = "start", + alignOffset = -3, + side = "right", + sideOffset = 0, + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + inset, + ...props +}: MenuPrimitive.CheckboxItem.Props & { + inset?: boolean; +}) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + inset, + ...props +}: MenuPrimitive.RadioItem.Props & { + inset?: boolean; +}) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: MenuPrimitive.Separator.Props) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +};