feat(web): add message search modal (chat/global)
Some checks failed
CI / test (push) Failing after 19s

- add Search action in chat header
- support search in active chat or globally when no chat selected
- show searchable message results and jump to target chat on click
This commit is contained in:
2026-03-08 02:14:26 +03:00
parent a9e4222062
commit 62390a1727

View File

@@ -3,6 +3,8 @@ import { ChatList } from "../components/ChatList";
import { ChatInfoPanel } from "../components/ChatInfoPanel";
import { MessageComposer } from "../components/MessageComposer";
import { MessageList } from "../components/MessageList";
import { searchMessages } from "../api/chats";
import type { Message } from "../chat/types";
import { useRealtime } from "../hooks/useRealtime";
import { useAuthStore } from "../store/authStore";
import { useChatStore } from "../store/chatStore";
@@ -19,6 +21,10 @@ export function ChatsPage() {
const activeChat = chats.find((chat) => chat.id === activeChatId);
const isReadOnlyChannel = Boolean(activeChat && activeChat.type === "channel" && activeChat.my_role === "member");
const [infoOpen, setInfoOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [searchLoading, setSearchLoading] = useState(false);
const [searchResults, setSearchResults] = useState<Message[]>([]);
useRealtime();
@@ -32,6 +38,39 @@ export function ChatsPage() {
}
}, [activeChatId, loadMessages]);
useEffect(() => {
if (!searchOpen) {
return;
}
const term = searchQuery.trim();
if (term.length < 2) {
setSearchResults([]);
setSearchLoading(false);
return;
}
let cancelled = false;
setSearchLoading(true);
void (async () => {
try {
const found = await searchMessages(term, activeChatId ?? undefined);
if (!cancelled) {
setSearchResults(found);
}
} catch {
if (!cancelled) {
setSearchResults([]);
}
} finally {
if (!cancelled) {
setSearchLoading(false);
}
}
})();
return () => {
cancelled = true;
};
}, [searchOpen, searchQuery, activeChatId]);
return (
<main className="h-screen w-full p-2 text-text md:p-4">
<div className="mx-auto flex h-full max-w-[1500px] gap-2 md:gap-4">
@@ -55,10 +94,15 @@ export function ChatsPage() {
<p className="truncate text-xs text-slate-300/80">{activeChat ? headerMetaLabel(activeChat) : "Select a chat"}</p>
</div>
</div>
<div className="flex items-center gap-2">
<button className="rounded-full bg-slate-700/70 px-3 py-1.5 text-xs font-semibold hover:bg-slate-600/80" onClick={() => setSearchOpen(true)}>
Search
</button>
<button className="rounded-full bg-slate-700/70 px-3 py-1.5 text-xs font-semibold hover:bg-slate-600/80" onClick={logout}>
Logout
</button>
</div>
</div>
<div className="min-h-0 flex-1">
<MessageList />
</div>
@@ -76,6 +120,41 @@ export function ChatsPage() {
</section>
</div>
<ChatInfoPanel chatId={activeChatId} open={infoOpen} onClose={() => setInfoOpen(false)} />
{searchOpen ? (
<div className="fixed inset-0 z-[130] flex items-start justify-center bg-slate-950/60 p-3 md:p-6" onClick={() => setSearchOpen(false)}>
<div className="w-full max-w-xl rounded-2xl border border-slate-700/80 bg-slate-900 p-3 shadow-2xl" onClick={(e) => e.stopPropagation()}>
<div className="mb-2 flex items-center justify-between">
<p className="text-sm font-semibold">Search messages</p>
<button className="rounded bg-slate-700 px-2 py-1 text-xs" onClick={() => setSearchOpen(false)}>Close</button>
</div>
<input
className="mb-2 w-full rounded-xl border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500"
placeholder={activeChatId ? "Search in current chat..." : "Global search..."}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{searchLoading ? <p className="px-2 py-1 text-xs text-slate-400">Searching...</p> : null}
{!searchLoading && searchQuery.trim().length >= 2 && searchResults.length === 0 ? (
<p className="px-2 py-1 text-xs text-slate-400">Nothing found</p>
) : null}
<div className="tg-scrollbar max-h-[50vh] space-y-1 overflow-auto">
{searchResults.map((message) => (
<button
className="block w-full rounded-lg bg-slate-800/80 px-3 py-2 text-left hover:bg-slate-700/80"
key={`search-msg-${message.id}`}
onClick={() => {
setActiveChatId(message.chat_id);
setSearchOpen(false);
}}
>
<p className="mb-1 text-[11px] text-slate-400">chat #{message.chat_id} · msg #{message.id}</p>
<p className="line-clamp-2 text-sm text-slate-100">{message.text || "[media]"}</p>
</button>
))}
</div>
</div>
</div>
) : null}
</main>
);
}