feat(search): add unified global search for users/chats/messages
Some checks failed
CI / test (push) Failing after 24s

This commit is contained in:
2026-03-08 09:41:20 +03:00
parent 76ab9c72f5
commit bc483afd78
8 changed files with 185 additions and 15 deletions

16
web/src/api/search.ts Normal file
View File

@@ -0,0 +1,16 @@
import { http } from "./http";
import type { DiscoverChat, Message, UserSearchItem } from "../chat/types";
export interface GlobalSearchResponse {
users: UserSearchItem[];
chats: DiscoverChat[];
messages: Message[];
}
export async function globalSearch(query: string): Promise<GlobalSearchResponse> {
const { data } = await http.get<GlobalSearchResponse>("/search", {
params: { query, users_limit: 10, chats_limit: 10, messages_limit: 10 }
});
return data;
}

View File

@@ -1,8 +1,8 @@
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { clearChat, createPrivateChat, discoverChats, deleteChat, getChats, joinChat } from "../api/chats";
import { searchUsers } from "../api/users";
import type { DiscoverChat, UserSearchItem } from "../chat/types";
import { clearChat, createPrivateChat, deleteChat, getChats, joinChat } from "../api/chats";
import { globalSearch } from "../api/search";
import type { DiscoverChat, Message, UserSearchItem } from "../chat/types";
import { updateMyProfile } from "../api/users";
import { useAuthStore } from "../store/authStore";
import { useChatStore } from "../store/chatStore";
@@ -13,11 +13,13 @@ export function ChatList() {
const messagesByChat = useChatStore((s) => s.messagesByChat);
const activeChatId = useChatStore((s) => s.activeChatId);
const setActiveChatId = useChatStore((s) => s.setActiveChatId);
const setFocusedMessage = useChatStore((s) => s.setFocusedMessage);
const loadChats = useChatStore((s) => s.loadChats);
const me = useAuthStore((s) => s.me);
const [search, setSearch] = useState("");
const [userResults, setUserResults] = useState<UserSearchItem[]>([]);
const [discoverResults, setDiscoverResults] = useState<DiscoverChat[]>([]);
const [messageResults, setMessageResults] = useState<Message[]>([]);
const [searchLoading, setSearchLoading] = useState(false);
const [tab, setTab] = useState<"all" | "people" | "groups" | "channels">("all");
const [ctxChatId, setCtxChatId] = useState<number | null>(null);
@@ -40,17 +42,15 @@ export function ChatList() {
);
useEffect(() => {
const timer = setTimeout(() => {
void loadChats(search.trim() ? search : undefined);
}, 250);
return () => clearTimeout(timer);
}, [search, loadChats]);
void loadChats();
}, [loadChats]);
useEffect(() => {
const term = search.trim();
if (term.replace("@", "").length < 2) {
setUserResults([]);
setDiscoverResults([]);
setMessageResults([]);
setSearchLoading(false);
return;
}
@@ -58,16 +58,18 @@ export function ChatList() {
setSearchLoading(true);
void (async () => {
try {
const [users, publicChats] = await Promise.all([searchUsers(term), discoverChats(term)]);
const result = await globalSearch(term);
if (cancelled) {
return;
}
setUserResults(users);
setDiscoverResults(publicChats);
setUserResults(result.users);
setDiscoverResults(result.chats);
setMessageResults(result.messages);
} catch {
if (!cancelled) {
setUserResults([]);
setDiscoverResults([]);
setMessageResults([]);
}
} finally {
if (!cancelled) {
@@ -156,7 +158,7 @@ export function ChatList() {
{search.trim().replace("@", "").length >= 2 ? (
<div className="mt-3 rounded-xl border border-slate-700/70 bg-slate-900/70 p-2">
{searchLoading ? <p className="px-2 py-1 text-xs text-slate-400">Searching...</p> : null}
{!searchLoading && userResults.length === 0 && discoverResults.length === 0 ? (
{!searchLoading && userResults.length === 0 && discoverResults.length === 0 && messageResults.length === 0 ? (
<p className="px-2 py-1 text-xs text-slate-400">Nothing found</p>
) : null}
{userResults.length > 0 ? (
@@ -173,6 +175,7 @@ export function ChatList() {
setSearch("");
setUserResults([]);
setDiscoverResults([]);
setMessageResults([]);
}}
>
<p className="truncate text-xs font-semibold">{user.name}</p>
@@ -197,6 +200,7 @@ export function ChatList() {
setSearch("");
setUserResults([]);
setDiscoverResults([]);
setMessageResults([]);
}}
>
<div className="min-w-0">
@@ -208,6 +212,28 @@ export function ChatList() {
))}
</div>
) : null}
{messageResults.length > 0 ? (
<div>
<p className="px-2 py-1 text-[10px] uppercase tracking-wide text-slate-400">Messages</p>
{messageResults.slice(0, 5).map((message) => (
<button
className="block w-full rounded-lg px-2 py-1.5 text-left hover:bg-slate-800"
key={`message-${message.id}`}
onClick={async () => {
setActiveChatId(message.chat_id);
setFocusedMessage(message.chat_id, message.id);
setSearch("");
setUserResults([]);
setDiscoverResults([]);
setMessageResults([]);
}}
>
<p className="truncate text-[11px] text-slate-400">chat #{message.chat_id}</p>
<p className="truncate text-xs font-semibold">{message.text || "[media]"}</p>
</button>
))}
</div>
) : null}
</div>
) : null}
</div>
@@ -299,7 +325,7 @@ export function ChatList() {
return;
}
await deleteChat(deleteModalChatId, canDeleteForEveryone && deleteForAll);
await loadChats(search.trim() ? search : undefined);
await loadChats();
if (activeChatId === deleteModalChatId) {
setActiveChatId(null);
}
@@ -351,7 +377,7 @@ export function ChatList() {
allow_private_messages: profileAllowPrivateMessages
});
useAuthStore.setState({ me: updated });
await loadChats(search.trim() ? search : undefined);
await loadChats();
setProfileOpen(false);
} catch {
setProfileError("Failed to update profile");

View File

@@ -1 +1 @@
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/chats.ts","./src/api/http.ts","./src/api/notifications.ts","./src/api/users.ts","./src/app/app.tsx","./src/chat/types.ts","./src/components/apperrorboundary.tsx","./src/components/authpanel.tsx","./src/components/chatinfopanel.tsx","./src/components/chatlist.tsx","./src/components/messagecomposer.tsx","./src/components/messagelist.tsx","./src/components/newchatpanel.tsx","./src/hooks/userealtime.ts","./src/pages/authpage.tsx","./src/pages/chatspage.tsx","./src/store/authstore.ts","./src/store/chatstore.ts","./src/utils/format.ts","./src/utils/ws.ts"],"version":"5.9.2"}
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/chats.ts","./src/api/http.ts","./src/api/notifications.ts","./src/api/search.ts","./src/api/users.ts","./src/app/app.tsx","./src/chat/types.ts","./src/components/apperrorboundary.tsx","./src/components/authpanel.tsx","./src/components/chatinfopanel.tsx","./src/components/chatlist.tsx","./src/components/messagecomposer.tsx","./src/components/messagelist.tsx","./src/components/newchatpanel.tsx","./src/hooks/userealtime.ts","./src/pages/authpage.tsx","./src/pages/chatspage.tsx","./src/store/authstore.ts","./src/store/chatstore.ts","./src/utils/format.ts","./src/utils/ws.ts"],"version":"5.9.2"}