feat: frontend draft

This commit is contained in:
2026-09-15 17:55:48 +08:00
parent a13e4d2d99
commit 5329bb2ed9
28 changed files with 4865 additions and 67 deletions
+241
View File
@@ -0,0 +1,241 @@
import { useWAClient } from "@/lib/context";
import { getFormattedName, getId, getInitial } from "@/lib/utils";
import type { ChatDTO } from "@shared/dto";
import { useQuery } from "@tanstack/react-query";
import { cn } from "cn";
import {
ArrowLeftIcon,
CircleHelpIcon,
PaperclipIcon,
SendIcon,
} from "lucide-react";
import React, { useMemo, useState } from "react";
type Props = {
chat: ChatDTO;
mobileChat: boolean;
setMobileChat: React.Dispatch<React.SetStateAction<boolean>>;
};
export default function MessagesSection({
chat,
mobileChat,
setMobileChat,
}: Props) {
const { client, isReady } = useWAClient();
const [message, setMessage] = useState("");
const { data: contacts } = useQuery({
queryKey: ["contacts"],
queryFn: () => client.listContacts(),
refetchOnWindowFocus: false,
});
const chatId = getId(chat.id);
const { data: messages } = useQuery({
queryKey: ["messages", chatId],
queryFn: () =>
client.getMessages({ chatId, count: 50 }).then((res) =>
res.map((i) => ({
...i,
id: getId(i.id),
fromMe: i.id.fromMe || i.fromMe,
})),
),
enabled: Boolean(chatId && isReady),
});
const messageList = useMemo(() => {
return messages
?.filter((i) => !["call_log", "revoked"].includes(i.type as never))
.map((message) => {
const from = contacts?.find(
(c) => getId(c.id) === getId(message.author || message.from),
);
return {
...message,
from,
};
})
?.reverse();
}, [messages, contacts]);
function sendMessage() {
const text = message.trim();
if (!text) return;
setMessage("");
}
return (
<section
className={`flex min-w-0 flex-1 flex-col bg-muted ${mobileChat ? "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">
<button
aria-label="Back to conversations"
onClick={() => setMobileChat(false)}
className="grid size-9 place-items-center rounded-full text-muted-foreground hover:bg-muted md:hidden"
>
<ArrowLeftIcon size={19} />
</button>
<div
className={cn(
"grid size-10 shrink-0 place-items-center rounded-full text-sm font-semibold",
)}
>
{getInitial(chat.formattedTitle)}
</div>
<div className="min-w-0">
<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>
{/* <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> */}
</header>
<div className="message-pattern flex-1 px-4 py-6 sm:px-8 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" />
<span>Today</span>
<span className="h-px flex-1 bg-border" />
</div> */}
{messageList?.map((item, index) => (
<div
key={`${item.timestamp}-${index}`}
className={`mb-3 flex ${item.fromMe ? "justify-end" : "justify-start"}`}
>
<div className="max-w-[82%] sm:max-w-[65%]">
<p className="mb-1 text-xs text-muted-foreground">
{getFormattedName(item.from) || item.notifyName}
</p>
<div
className={`rounded-2xl px-4 py-2.5 text-sm shadow-sm ${item.fromMe ? "rounded-br-md bg-[#d8f5e8] dark:bg-[#26855c] text-foreground" : "rounded-bl-md bg-card text-card-foreground"}`}
>
{item.isForwarded ? (
<p className="text-xs italic mb-1 text-primary/80">
Forwarded
</p>
) : null}
{item.quotedMsg && item.quotedMsg.type === "chat" ? (
<div
className={cn(
"bg-card/30 px-2 py-1.5 rounded-lg mb-2 mt-1",
!item.fromMe && "bg-[#d8f5e8]/30 dark:bg-[#26855c]/30",
)}
>
<p className="whitespace-pre-wrap">{item.quotedMsg.body}</p>
</div>
) : null}
{item.type === "image" && (
<img
src={"data:image/jpeg;base64," + item.body}
alt=""
className="w-full"
/>
)}
{item.type === "chat" || item.caption ? (
<p className="whitespace-pre-wrap">
{item.caption || item.body}
</p>
) : null}
{item.type === "sticker" && <p className="italic">Sticker</p>}
<div
className={`mt-1 flex items-center justify-end gap-1 text-[10px] ${item.fromMe ? "text-primary" : "text-muted-foreground"}`}
>
<span>{item.timestamp}</span>
{/* {item.fromMe &&
(item.read ? (
<CheckCheck size={14} />
) : (
<Check size={13} />
))} */}
</div>
</div>
</div>
</div>
))}
</div>
<div className="border-t border-border bg-card px-3 py-3 sm:px-6 sm:py-4">
<div className="mx-auto flex max-w-2xl items-end gap-2">
<button
aria-label="Add attachment"
className="grid size-10 shrink-0 place-items-center rounded-full text-muted-foreground hover:bg-muted hover:text-muted-foreground"
>
<PaperclipIcon size={19} />
</button>
<div className="flex min-h-10 flex-1 items-center gap-2 rounded-2xl bg-muted px-3">
{/* <button
aria-label="Add emoji"
className="shrink-0 text-muted-foreground hover:text-muted-foreground"
>
<SmileIcon size={19} />
</button> */}
<textarea
value={message}
onChange={(event) => setMessage(event.target.value)}
onKeyDown={(event) => {
if (
event.key === "Enter" &&
!event.shiftKey &&
!event.nativeEvent.isComposing &&
event.keyCode !== 229
) {
event.preventDefault();
sendMessage();
}
}}
placeholder="Write a message..."
rows={1}
className="max-h-24 min-h-10 flex-1 resize-none bg-transparent py-2.5 text-sm outline-none placeholder:text-muted-foreground"
/>
</div>
<button
aria-label="Send message"
onClick={sendMessage}
className="grid size-10 shrink-0 place-items-center rounded-full bg-primary text-primary-foreground shadow-sm transition hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
disabled={!message.trim()}
>
<SendIcon size={17} />
</button>
</div>
<div className="mx-auto mt-2 hidden max-w-2xl items-center justify-center gap-1 text-[10px] text-muted-foreground sm:flex">
<CircleHelpIcon size={12} /> Press Enter to send
</div>
</div>
</section>
);
}