From bf8db28a7cdd1e83c09ad1aa0f501b8ebb257178 Mon Sep 17 00:00:00 2001 From: gosangam Date: Sun, 30 Aug 2026 10:57:28 +0530 Subject: [PATCH] feat: chat search --- app/(app)/chats/page.tsx | 99 ++++++++++++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/app/(app)/chats/page.tsx b/app/(app)/chats/page.tsx index 680a164e..9ae01246 100644 --- a/app/(app)/chats/page.tsx +++ b/app/(app)/chats/page.tsx @@ -16,14 +16,19 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; -import { MessageCircle, Users } from "lucide-react"; +import { MessageCircle, Search, Users, X } from "lucide-react"; import { useAuth } from "@/components/auth/auth-provider"; import { useAuthGate } from "@/components/auth/auth-gate"; import { useNotifications } from "@/components/notifications/notifications-provider"; import { KautilyaIcon } from "@/components/selection-toolbar/shared"; import { ChatListItem } from "@/components/chat/chat-list-item"; import { NewGroupDialog } from "@/components/chat/new-group-dialog"; -import { chat, type InboxItem, type InboxTab } from "@/lib/chat"; +import { + chat, + conversationTitle, + type InboxItem, + type InboxTab, +} from "@/lib/chat"; import { cn } from "@/lib/utils"; /** How long a row stays highlighted after a message lands. */ @@ -36,6 +41,15 @@ export default function ChatsPage() { const router = useRouter(); const [tab, setTab] = useState("chats"); + const [searching, setSearching] = useState(false); + const [query, setQuery] = useState(""); + + // Closing clears the term too: leaving a filter applied behind a collapsed + // field is how a chat goes missing and looks like a bug. + const closeSearch = useCallback(() => { + setSearching(false); + setQuery(""); + }, []); const [items, setItems] = useState([]); const [state, setState] = useState<"loading" | "ready" | "error">("loading"); const [highlighted, setHighlighted] = useState>(new Set()); @@ -120,22 +134,69 @@ export default function ChatsPage() { ); } + // Filtered here rather than on the server: the inbox is already loaded in + // full, so this is instant and keeps working offline. Matches the thread's + // name and, for a DM, the peer's handle — the two things you actually + // remember someone by. + const term = query.trim().toLowerCase(); + const visible = term + ? items.filter((item) => { + const title = conversationTitle(item).toLowerCase(); + const handle = item.peer?.handle?.toLowerCase() ?? ""; + return title.includes(term) || handle.includes(term); + }) + : items; + return (
-
-

- Chats -

- {tab === "chats" && ( + {searching ? ( + // The search field REPLACES the header rather than sitting under it: + // the title and New group are not useful while filtering, and stacking + // a second row would push the list down every time it opens. +
+ + setQuery(e.target.value)} + placeholder="Search chats" + className="flex-1 bg-transparent text-sm outline-none pointer-coarse:text-base placeholder:text-muted-foreground" + /> - )} -
+
+ ) : ( +
+

+ Chats +

+
+ + {tab === "chats" && ( + + )} +
+
+ )}
setTab("chats")}> @@ -155,8 +216,16 @@ export default function ChatsPage() {

) : state === "loading" ? (

Loading…

- ) : items.length === 0 ? ( - tab === "kautilya" ? ( + ) : visible.length === 0 ? ( + // A search that matches nothing is NOT an empty inbox: saying "no + // chats yet" there reads as data loss rather than a narrow filter. + term ? ( + } + title="No matches" + text={`Nothing in ${tab === "kautilya" ? "Kautilya" : "your chats"} matches “${query.trim()}”.`} + /> + ) : tab === "kautilya" ? ( } title="Kautilya is coming soon" @@ -171,7 +240,7 @@ export default function ChatsPage() { ) ) : (
    - {items.map((item) => ( + {visible.map((item) => (