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 (
- {selectedChat ? ( + {chat ? (