Implement username search

This commit is contained in:
2025-10-17 00:19:47 +03:00
Unverified
parent 13c258ad95
commit dd946e7466
14 changed files with 582 additions and 44 deletions
@@ -9,28 +9,14 @@ export function DMUsersList() {
const { chat, switchToDM } = useAppState();
useEffect(() => {
if (chat.activeTab === "dms") {
if (chat.activeTab === "chats") {
loadUsers();
}
}, [chat.activeTab, loadUsers]);
if (isLoadingUsers) {
return (
<mdui-list>
<mdui-list-item headline="Загрузка..." description="Получение списка пользователей...">
<img src={defaultAvatar} alt="" slot="icon" />
</mdui-list-item>
</mdui-list>
);
}
if (dmUsers.length === 0) {
return (
<mdui-list>
<mdui-list-item headline="Нет пользователей" description="Пользователи не найдены">
<img src={defaultAvatar} alt="" slot="icon" />
</mdui-list-item>
</mdui-list>
<mdui-circular-progress />
);
}
@@ -5,6 +5,7 @@ import { useState, type FormEvent } from "react";
import { ProfileDialog } from "./profile/ProfileDialog";
import { SettingsDialog } from "./settings/SettingsDialog";
import { DMUsersList } from "./DMUsersList";
import { UsernameSearch } from "./UsernameSearch";
import type { Tabs } from "mdui";
import type { ChatTabs } from "@/pages/chat/state";
@@ -54,7 +55,6 @@ function ChatTabs() {
<mdui-tab value="chats">Чаты</mdui-tab>
<mdui-tab value="channels">Каналы</mdui-tab>
<mdui-tab value="contacts">Контакты</mdui-tab>
<mdui-tab value="dms">ЛС</mdui-tab>
<mdui-tab-panel slot="panel" value="chats">
<mdui-list>
@@ -76,13 +76,13 @@ function ChatTabs() {
>
<img src={defaultAvatar} alt="" slot="icon" />
</mdui-list-item>
{/* DM conversations will be loaded here */}
<DMUsersList />
</mdui-list>
</mdui-tab-panel>
<mdui-tab-panel slot="panel" value="channels">Скоро будет...</mdui-tab-panel>
<mdui-tab-panel slot="panel" value="contacts">Скоро будет...</mdui-tab-panel>
<mdui-tab-panel slot="panel" value="dms">
<DMUsersList />
</mdui-tab-panel>
</mdui-tabs>
</div>
);
@@ -109,6 +109,9 @@ export function LeftPanel() {
return (
<div className="chat-list" id="chat-list">
<ChatHeader />
<div className="search-container">
<UsernameSearch />
</div>
<ChatTabs />
<BottomAppBar />
</div>
@@ -0,0 +1,157 @@
import { useState, useEffect } from "react";
import { useAppState } from "@/pages/chat/state";
import { searchUsers, fetchUserPublicKey } from "@/core/api/dmApi";
import type { User } from "@/core/types";
import defaultAvatar from "@/images/default-avatar.png";
import SearchBar from "@/core/components/SearchBar";
interface SearchUser extends User {
publicKey?: string | null;
}
export function UsernameSearch() {
const { user, switchToDM } = useAppState();
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<SearchUser[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [debounceTimeout, setDebounceTimeout] = useState<NodeJS.Timeout | null>(null);
// Debounced search
useEffect(() => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
if (searchQuery.length > 1) {
setIsSearching(true);
const newTimeout = setTimeout(async () => {
if (user.authToken) {
try {
const users = await searchUsers(searchQuery, user.authToken);
setSearchResults(users);
} catch (error) {
console.error("Search failed:", error);
setSearchResults([]);
} finally {
setIsSearching(false);
}
}
}, 300);
setDebounceTimeout(newTimeout);
} else {
setSearchResults([]);
setIsSearching(false);
}
return () => {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
};
}, [searchQuery, user.authToken]);
async function handleUserClick(searchUser: SearchUser) {
if (!user.authToken) return;
try {
let publicKey = searchUser.publicKey;
if (!publicKey) {
const fetchedPublicKey = await fetchUserPublicKey(searchUser.id, user.authToken);
publicKey = fetchedPublicKey;
}
if (publicKey) {
switchToDM({
userId: searchUser.id,
username: searchUser.username,
publicKey: publicKey,
profilePicture: searchUser.profile_picture,
online: searchUser.online || false
});
// Collapse search
setIsExpanded(false);
setSearchQuery("");
setSearchResults([]);
}
} catch (error) {
console.error("Failed to start DM conversation:", error);
}
}
function handleQueryChange(query: string) {
setSearchQuery(query);
}
function handleToggleExpanded() {
if (isExpanded) {
// Collapsing
setSearchQuery("");
setSearchResults([]);
}
setIsExpanded(!isExpanded);
}
return (
<SearchBar
placeholder="Поиск"
searchQuery={searchQuery}
onQueryChange={handleQueryChange}
isExpanded={isExpanded}
onToggleExpanded={handleToggleExpanded}
leftIcon={isExpanded ? (
<mdui-button-icon
className="back-button"
onClick={(e) => {
e.stopPropagation();
handleToggleExpanded();
}}
type="button"
icon="arrow_back--outlined"
/>
) : "search--outlined"}
>
{isSearching && (
<div className="search-loading">
<mdui-circular-progress value={0}></mdui-circular-progress>
<span>Поиск...</span>
</div>
)}
{!isSearching && searchQuery.length >= 2 && searchResults.length === 0 && (
<div className="search-empty">
<span>Пользователи не найдены</span>
</div>
)}
{!isSearching && searchResults.length > 0 && (
<mdui-list>
{searchResults.map((searchUser) => (
<mdui-list-item
key={searchUser.id}
headline={searchUser.username}
description={searchUser.online ? "В сети" : "Не в сети"}
onClick={() => handleUserClick(searchUser)}
style={{ cursor: "pointer" }}
>
<img
src={searchUser.profile_picture || defaultAvatar}
alt={searchUser.username}
slot="icon"
onError={(e) => {
(e.target as HTMLImageElement).src = defaultAvatar;
}}
/>
</mdui-list-item>
))}
</mdui-list>
)}
{!isSearching && searchQuery.length < 2 && (
<div className="search-hint">
<span>Введите минимум 2 символа для поиска</span>
</div>
)}
</SearchBar>
);
}