import { useWAClient } from "./context"; import { getFilesAsBase64, getFormattedName, getId } from "@/lib/utils"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { cn } from "cn"; import { ArrowLeftIcon, CheckCheckIcon, CheckIcon, ChevronDown, CornerUpRightIcon, Loader2Icon, MousePointer2Icon, PaperclipIcon, PhoneCallIcon, SendIcon, Trash2Icon, XIcon, } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import MessageMedia from "@/components/message-media"; import { useContacts } from "@/hooks/useContact"; import { useMessageEvent } from "@/hooks/useMessageEvent"; import { useHashRouter } from "@/hooks/useHashRouter"; import { useChats } from "@/hooks/useChats"; import Avatar from "@/components/ui/avatar"; import { useIntersection } from "@/hooks/useIntersection"; 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, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { forwardDlg } from "./stores"; dayjs.extend(relativeTime); type Props = { chatId: string | null; maxCount?: number; }; 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(""); const [selectedIds, setSelectedIds] = useState | null>(null); const { data: messages, isLoading, refetch, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ["messages", chatId], queryFn: ({ pageParam }) => client .getMessages({ chatId: chatId!, count: maxCount, direction: "before", id: pageParam, }) .then((i) => i.reverse()), initialPageParam: undefined as string | undefined, enabled: Boolean(chatId && isReady), getNextPageParam: (lastPage) => { if (lastPage.length < maxCount) return undefined; return getId(lastPage[lastPage.length - 1]?.id); }, select: (data) => data.pages.flat(), }); useEffect(() => { if (chatId) { msgContainerRef.current.scrollTop = 0; setSelectedIds(null); } }, [chatId]); useMessageEvent((data) => { if (data.chatId !== chatId) return; queryClient.setQueryData(["messages", chatId], (old: any) => { if (!old) return old; return { ...old, pages: [[data, ...old.pages[0]], ...old.pages.slice(1)], }; }); }); const endScrollRef = useIntersection(() => { 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( () => chats?.find((c) => c.id === chatId), [chats, chatId], ); const messageList = useMemo(() => { const q = search.trim().toLowerCase(); return messages ?.filter((i) => { if (["revoked"].includes(i.type as never)) { return false; } if (q) { const body = i.caption || (i.type === "chat" ? i.body : "") || ""; const name = getFormattedName(i.from) || ""; return ( body.toLowerCase().includes(q) || name.toLowerCase().includes(q) || i.filename?.toLowerCase().includes(q) ); } return true; }) ?.map((i) => { const from = contacts?.find( (c) => getId(c.id) === getId(i.author || i.from), ); const quotedMsg = i.quotedMsg ? { ...i.quotedMsg, from: contacts?.find((c) => { return ( getId(c.id) === getId(i.quotedMsg?.author || i.quotedMsg?.from) ); }), } : null; return { ...i, id: getId(i.id), from, quotedMsg, }; }); }, [messages, contacts, search]); 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}', ${JSON.stringify(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; } function onMentionClick(username: string) { const data = contacts?.find((c) => (c.id as any).user === username); if (data) { router.push(`/?chatId=${getId(data.id)}`); } } return (
{selectedIds?.size ? (

{`${selectedIds.size} selected`}

) : (

{chat?.formattedTitle || chat?.name}

{/*

{selected.online ? "Online now" : "Active recently"}

*/}
)}
{/*
Today
*/} {messageList?.map((item, index) => (
{ if (e.target !== e.currentTarget) return; onSelectMsg(item.id); } : undefined } >
{ e.preventDefault(); router.push( `/?chatId=${getId(item.author || item.from?.id)}`, ); }} className="mb-1 text-xs text-muted-foreground" > {getFormattedName(item.from) || item.notifyName}
} > onSelectMsg(item.id)}> Select forwardDlg.onOpen({ messageIds: [item.id] }) } > Forward onDeleteMsg(item.id)}> Delete {item.isForwarded ? (

Forwarded

) : null} {/*
                  {JSON.stringify(item, null, 2)}
                
*/} {item.quotedMsg && ["chat", "image", "document"].includes( item.quotedMsg.type as string, ) ? ( ) : null} {item.type === "image" || item.type === "sticker" || item.type === "document" || item.type === "ptt" ? ( ) : null} {item.type === "chat" || item.caption ? ( ) : null} {item.type === "call_log" && (

Call log

)}
{dayjs((item.timestamp ?? 0) * 1000).fromNow()} {item.fromMe && ( <> {item.ack && item.ack >= 2 ? ( ) : ( )} )}
))} {messages == null || isLoading || isFetchingNextPage ? [...Array(10)].map((_, i) => (
)) : null} {messages != null &&
}
{isLoading || isFetchingNextPage ? ( ) : null} {isLoading || isFetchingNextPage ? "Loading..." : "End of messages"}
); } function MessageInputSection({ chatId, refetch, }: { chatId?: string | null; refetch: () => void; }) { const fileRef = useRef(null); const { client, isReady } = useWAClient(); const [message, setMessage] = useState(""); const [files, setFiles] = useState<{ id: string; file: File }[]>([]); const [isPending, setPending] = useState(false); useEffect(() => { function onDragOver(e: DragEvent) { e.preventDefault(); } function onDrop(e: DragEvent) { const fileList = e.dataTransfer?.files || []; if (fileList) { e.preventDefault(); onFileAttached([...fileList]); } } document.addEventListener("dragover", onDragOver); document.addEventListener("drop", onDrop); return () => { document.removeEventListener("dragover", onDragOver); document.removeEventListener("drop", onDrop); }; }, []); function onFileAttached(files: File[]) { setFiles((f) => [...f, ...files.map((file) => ({ id: nanoid(), file }))]); } function onPaste(e: React.ClipboardEvent) { const fileList = e.clipboardData?.files || []; if (fileList.length > 0) { e.preventDefault(); onFileAttached([...fileList]); } } async function sendMessage() { const text = message.trim(); if ((!text && !files.length) || !chatId || !isReady) return; try { setPending(true); const options = { waitForAck: true }; if (files.length > 0) { const fileList = await getFilesAsBase64(files.map((f) => f.file)); for (let i = 0; i < fileList.length; i++) { const file = fileList[i]; await client.sendFile({ chatId, data: file, options: { ...options, caption: i === 0 ? text : undefined, filename: files[i].file.name, mimetype: files[i].file.type, }, }); } } else { await client.sendText({ chatId, text, options }); } setTimeout(refetch, 1000); setFiles([]); setMessage(""); } catch (err) { alert((err as Error).message); } finally { setPending(false); } } function onRemoveAttachment(id: string) { setFiles((f) => f.filter((file) => file.id !== id)); } return (
{ if (e.target.files?.length) { onFileAttached([...e.target.files]); } e.target.value = ""; }} /> {files.length > 0 && (
{files.map((i) => ( onRemoveAttachment(i.id)} /> ))}
)}
{/* */}