diff --git a/src/client/whatsapp.ts b/src/client/whatsapp.ts
index 422b7f4..dacdcd3 100644
--- a/src/client/whatsapp.ts
+++ b/src/client/whatsapp.ts
@@ -47,6 +47,12 @@ export class WhatsAppClient {
this.socket.addEventListener("message", (data) => {
const message = JSON.parse(data.data);
+
+ if (message.type === "event") {
+ this.emit(message.event, message.data);
+ return;
+ }
+
if (message.type !== "response") {
return;
}
diff --git a/ui/index.html b/ui/index.html
index 2a79158..a746b63 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -3,9 +3,12 @@
{chat.formattedTitle || chat.name}
- {/*
- {chat.timestamp}
- */}
+
+ {dayjs((chat.timestamp || 0) * 1000).fromNow()}
+
-
-
- {chat.previewMessage?.type === "chat"
- ? chat.previewMessage?.body?.substring(0, 50)
- : null}
-
- {/* {chat.unread && (
-
- {chat.unread}
-
- )} */}
+
+
+
+ {/*
+ {chat.unreadCount ? (
+
+ {chat.unreadCount}
+
+ ) : null} */}
@@ -105,3 +125,53 @@ export default function ChatsSection({ selected, onSelect }: Props) {
>
);
}
+
+function ChatPreviewMessage({
+ data,
+ from,
+}: {
+ data: ChatDTO["previewMessage"];
+ from?: ContactDTO | null;
+}) {
+ if (!data) {
+ return null;
+ }
+
+ let caption = useMemo(() => {
+ if (data.type === "image") {
+ return data.caption || "Picture";
+ }
+ if (data.type === "document") {
+ return data.caption || "Document";
+ }
+ if (data.type === "call_log") {
+ return "Call log";
+ }
+ if (data.type === "sticker") {
+ return "Sticker";
+ }
+
+ return data.type === "chat" || data.caption
+ ? data.caption || data.body
+ : null;
+ }, [data]);
+
+ return (
+ <>
+ {from ? (
+
+ {getFormattedName(from) + ": "}
+
+ ) : null}
+
+ {data.type === "image" &&
}
+ {data.type === "document" &&
}
+ {data.type === "call_log" &&
}
+ {data.type === "sticker" &&
}
+
+
+ {caption?.substring(0, 50)}
+
+ >
+ );
+}
diff --git a/ui/src/app/context.tsx b/ui/src/app/context.tsx
index 23910db..e6c32fd 100644
--- a/ui/src/app/context.tsx
+++ b/ui/src/app/context.tsx
@@ -7,6 +7,7 @@ export const WAContext = createContext<{
client: WhatsAppClient;
isConnected: boolean;
isReady: boolean;
+ addListener: (event: string, callback: (...args: any[]) => any) => () => void;
}>(null!);
export function useWAClient() {
@@ -15,8 +16,11 @@ export function useWAClient() {
return ctx;
}
+type ListenerMap = Record
any)[]>;
+
export function WAProvider({ children }: { children: React.ReactNode }) {
const isInitialized = useRef(false);
+ const eventListeners = useRef({});
const [isConnected, setConnected] = useState(false);
const { data: status } = useQuery({
@@ -25,6 +29,20 @@ export function WAProvider({ children }: { children: React.ReactNode }) {
enabled: isConnected,
});
+ function addListener(event: string, callback: (...args: any[]) => any) {
+ eventListeners.current[event] = eventListeners.current[event] || [];
+ eventListeners.current[event].push(callback);
+ return () => {
+ eventListeners.current[event] = eventListeners.current[event].filter(
+ (i) => i !== callback,
+ );
+ };
+ }
+
+ function emit(event: string, ...args: any[]) {
+ eventListeners.current[event]?.forEach((i) => i(...args));
+ }
+
useEffect(() => {
if (isInitialized.current) return;
@@ -32,6 +50,7 @@ export function WAProvider({ children }: { children: React.ReactNode }) {
isInitialized.current = true;
wa.on("status", setConnected);
+ wa.on("message", (data) => emit("message", data));
}, []);
return (
@@ -40,6 +59,7 @@ export function WAProvider({ children }: { children: React.ReactNode }) {
client: wa,
isConnected,
isReady: status?.authenticated && status?.ready ? true : false,
+ addListener,
}}
>
{children}
diff --git a/ui/src/app/messages.tsx b/ui/src/app/messages.tsx
index 000ff73..3b87198 100644
--- a/ui/src/app/messages.tsx
+++ b/ui/src/app/messages.tsx
@@ -5,6 +5,8 @@ import { useQuery } from "@tanstack/react-query";
import { cn } from "cn";
import {
ArrowLeftIcon,
+ CheckCheckIcon,
+ CheckIcon,
PaperclipIcon,
PhoneCallIcon,
SendIcon,
@@ -14,6 +16,8 @@ import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import MessageMedia from "@/components/message-media";
import Avatar from "@/components/ui/avatar";
+import { useContacts } from "@/hooks/useContact";
+import { useMessageEvent } from "@/hooks/useMessageEvent";
dayjs.extend(relativeTime);
@@ -32,11 +36,7 @@ export default function MessagesSection({
const [message, setMessage] = useState("");
const [isPending, setPending] = useState(false);
- const { data: contacts } = useQuery({
- queryKey: ["contacts"],
- queryFn: () => client.listContacts(),
- refetchOnWindowFocus: false,
- });
+ const { data: contacts } = useContacts();
const chatId = getId(chat.id);
const { data: messages, refetch } = useQuery({
@@ -46,17 +46,35 @@ export default function MessagesSection({
refetchInterval: 10_1000,
});
+ useMessageEvent((data) => {
+ if (data.chatId === chatId) refetch();
+ });
+
const messageList = useMemo(() => {
return messages
?.filter((i) => !["revoked"].includes(i.type as never))
- ?.map((message) => {
+ ?.map((i) => {
const from = contacts?.find(
- (c) => getId(c.id) === getId(message.author || message.from),
+ (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 {
- ...message,
- id: getId(message.id),
+ ...i,
+ id: getId(i.id),
from,
+ quotedMsg,
};
})
?.reverse();
@@ -135,6 +153,7 @@ export default function MessagesSection({
{messageList?.map((item, index) => (
@@ -151,23 +170,48 @@ export default function MessagesSection({
) : null}
- {item.quotedMsg && item.quotedMsg.type === "chat" ? (
-
+
+ {item.quotedMsg.filename ||
+ item.quotedMsg.caption ||
+ (item.quotedMsg.type === "chat"
+ ? item.quotedMsg.body
+ : "Quoted")}
+
+
) : null}
{item.type === "image" ||
item.type === "sticker" ||
- item.type === "document" ? (
-
+ item.type === "document" ||
+ item.type === "ptt" ? (
+
) : null}
{item.type === "chat" || item.caption ? (
@@ -187,12 +231,19 @@ export default function MessagesSection({
className={`mt-1 flex items-center justify-end gap-1 text-[10px] ${item.fromMe ? "text-primary" : "text-muted-foreground"}`}
>
{dayjs((item.timestamp ?? 0) * 1000).fromNow()}
- {/* {item.fromMe &&
- (item.read ? (
-
- ) : (
-
- ))} */}
+
+ {item.fromMe && (
+ <>
+ {item.ack && item.ack >= 2 ? (
+
+ ) : (
+
+ )}
+ >
+ )}
@@ -232,7 +283,7 @@ export default function MessagesSection({
}}
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"
+ className="max-h-24 min-h-10 flex-1 resize-none field-sizing-content bg-transparent py-2.5 text-sm outline-none placeholder:text-muted-foreground"
/>