diff --git a/ui/index.html b/ui/index.html
index 6330bc3..2a79158 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -5,7 +5,7 @@
-
ui
+ WhatsApp Bridge UI
diff --git a/ui/package.json b/ui/package.json
index 8493269..fa1ac14 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -16,6 +16,7 @@
"@tanstack/react-query": "^5.102.8",
"class-variance-authority": "^0.7.1",
"cn": "^0.3.0",
+ "dayjs": "^1.11.23",
"lucide-react": "^1.46.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 650b638..d33fdca 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -26,6 +26,9 @@ importers:
cn:
specifier: ^0.3.0
version: 0.3.0
+ dayjs:
+ specifier: ^1.11.23
+ version: 1.11.23
lucide-react:
specifier: ^1.46.0
version: 1.46.0(react@19.3.0)
@@ -833,6 +836,9 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+ dayjs@1.11.23:
+ resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==}
+
debounce-fn@4.0.0:
resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==}
engines: {node: '>=10'}
@@ -2674,6 +2680,8 @@ snapshots:
csstype@3.2.3: {}
+ dayjs@1.11.23: {}
+
debounce-fn@4.0.0:
dependencies:
mimic-fn: 3.1.0
diff --git a/ui/src/app/chats.tsx b/ui/src/app/chats.tsx
new file mode 100644
index 0000000..1f55969
--- /dev/null
+++ b/ui/src/app/chats.tsx
@@ -0,0 +1,127 @@
+import { useWAClient } from "@/lib/context";
+import { cn, getId, getInitial } from "@/lib/utils";
+import type { ChatDTO } from "@shared/dto";
+import { useQuery } from "@tanstack/react-query";
+import { SearchIcon, XIcon } from "lucide-react";
+import { useMemo, useState } from "react";
+
+type Props = {
+ selected: ChatDTO | null;
+ onSelect: (chat: ChatDTO) => void;
+};
+export default function ChatsSection({ selected, onSelect }: Props) {
+ const [query, setQuery] = useState("");
+ const [showSearch, setShowSearch] = useState(false);
+
+ const { client, isReady } = useWAClient();
+ const { data: chats } = useQuery({
+ queryKey: ["chats"],
+ queryFn: () =>
+ client
+ .listChats({ limit: 500 })
+ .then((res) => res.map((i) => ({ ...i, id: getId(i.id) }))),
+ enabled: isReady,
+ refetchInterval: 10_000,
+ });
+
+ const filteredChats = useMemo(() => {
+ if (!query.trim()) {
+ return chats?.slice(0, 100);
+ }
+
+ return chats
+ ?.filter(
+ (chat) =>
+ chat.formattedTitle?.toLowerCase().includes(query.toLowerCase()) ||
+ chat.lastMessage?.body?.toLowerCase().includes(query.toLowerCase()),
+ )
+ ?.slice(0, 100);
+ }, [chats, query]);
+
+ return (
+ <>
+
+ {showSearch ? (
+
+
+ setQuery(event.target.value)}
+ placeholder="Search conversations"
+ className="min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
+ />
+
+
+ ) : (
+
+ )}
+
+
+ {/*
+ All
+ ·
+ Unread
+ ·
+ Groups
+
*/}
+
+
+ {filteredChats?.map((chat) => (
+
+ ))}
+
+ >
+ );
+}
diff --git a/ui/src/app/messages.tsx b/ui/src/app/messages.tsx
index 100d57a..39ed8b2 100644
--- a/ui/src/app/messages.tsx
+++ b/ui/src/app/messages.tsx
@@ -3,13 +3,12 @@ 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 { ArrowLeftIcon, PaperclipIcon, SendIcon } from "lucide-react";
import React, { useMemo, useState } from "react";
+import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+
+dayjs.extend(relativeTime);
type Props = {
chat: ChatDTO;
@@ -200,7 +199,7 @@ export default function MessagesSection({
- {item.timestamp}
+ {dayjs((item.timestamp ?? 0) * 1000).fromNow()}
{/* {item.fromMe &&
(item.read ? (
@@ -258,9 +257,9 @@ export default function MessagesSection({
-
+ {/*
Press Enter to send
-
+
*/}
);
diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx
index 47ba6eb..2ebf68d 100644
--- a/ui/src/app/page.tsx
+++ b/ui/src/app/page.tsx
@@ -1,57 +1,19 @@
-import { useMemo, useState } from "react";
-import {
- ChevronDown,
- Edit3,
- EllipsisVertical,
- Search,
- Sparkles,
- X,
-} from "lucide-react";
-import { useWAClient } from "@/lib/context";
-import { useQuery } from "@tanstack/react-query";
-import { cn, getId, getInitial } from "@/lib/utils";
+import { useState } from "react";
+import ChatsSection from "./chats";
+import type { ChatDTO } from "@shared/dto";
import MessagesSection from "./messages";
export default function Page() {
- const [selectedId, setSelectedId] = useState();
- const [query, setQuery] = useState("");
- const [showSearch, setShowSearch] = useState(false);
+ const [chat, setChat] = useState(null);
const [mobileChat, setMobileChat] = useState(false);
- const { client, isReady } = useWAClient();
- const { data: chats } = useQuery({
- queryKey: ["chats"],
- queryFn: () =>
- client
- .listChats({ limit: 500 })
- .then((res) => res.map((i) => ({ ...i, id: getId(i.id) }))),
- enabled: isReady,
- });
-
- const filteredChats = useMemo(
- () =>
- chats?.filter(
- (chat) =>
- chat.formattedTitle?.toLowerCase().includes(query.toLowerCase()) ||
- chat.lastMessage?.body?.toLowerCase().includes(query.toLowerCase()),
- ),
- [chats, query],
- );
-
- function openChat(id: string) {
- setSelectedId(id);
- setMobileChat(true);
- }
-
- const selectedChat = chats?.find((chat) => chat.id === selectedId);
-
return (