mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement username search
This commit is contained in:
@@ -96,6 +96,7 @@
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
min-height: 0; // allow children to manage their own scrolling
|
||||
position: relative; // provide positioning context for absolute children
|
||||
|
||||
.chat-header-left {
|
||||
display: flex;
|
||||
@@ -106,7 +107,6 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.product-name {
|
||||
flex-grow: 1;
|
||||
@@ -192,6 +192,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Search container
|
||||
.search-container {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
height: 48px + 8px;
|
||||
}
|
||||
|
||||
// ChatHeader component styles
|
||||
.chat-header-left {
|
||||
.product-name {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useAppState } from "@/pages/chat/state";
|
||||
import {
|
||||
fetchUsers,
|
||||
fetchUserPublicKey,
|
||||
fetchDMHistory,
|
||||
decryptDm,
|
||||
sendDMViaWebSocket
|
||||
sendDMViaWebSocket,
|
||||
fetchDMConversations
|
||||
} from "@/core/api/dmApi";
|
||||
import type { User, Message, DmEncryptedJSON } from "@/core/types";
|
||||
import { websocket } from "@/core/websocket";
|
||||
@@ -72,32 +72,28 @@ export function useDM() {
|
||||
}
|
||||
}, [user.authToken]);
|
||||
|
||||
// Load users when DM tab is active
|
||||
// Load DM conversations when chats tab is active
|
||||
const loadUsers = useCallback(async () => {
|
||||
if (!user.authToken || isLoadingUsers || usersLoadedRef.current) return;
|
||||
|
||||
usersLoadedRef.current = true;
|
||||
setIsLoadingUsers(true);
|
||||
try {
|
||||
const users = await fetchUsers(user.authToken);
|
||||
console.log("Fetched users:", users);
|
||||
const dmUsersWithState: DMUser[] = users.map(user => ({
|
||||
...user,
|
||||
unreadCount: 0,
|
||||
lastMessage: undefined,
|
||||
const conversations = await fetchDMConversations(user.authToken);
|
||||
console.log("Fetched conversations:", conversations);
|
||||
|
||||
const dmUsersWithState: DMUser[] = conversations.map((conv: any) => ({
|
||||
...conv.user,
|
||||
unreadCount: conv.unreadCount,
|
||||
lastMessage: conv.lastMessage ? "Последнее сообщение" : undefined,
|
||||
publicKey: null
|
||||
}));
|
||||
|
||||
setDmUsersState(dmUsersWithState);
|
||||
setDmUsers(users);
|
||||
setDmUsers(conversations.map((conv: any) => conv.user));
|
||||
|
||||
// Load last messages and unread counts for visible users
|
||||
// Call loadUserLastMessage directly without dependency
|
||||
for (const dmUser of dmUsersWithState) {
|
||||
await loadUserLastMessage(dmUser);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load DM users:", error);
|
||||
console.error("Failed to load DM conversations:", error);
|
||||
} finally {
|
||||
setIsLoadingUsers(false);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { API_BASE_URL } from "@/core/config";
|
||||
import { initialize, subscribe, startElectronReceiver, isSupported } from "@/core/push-notifications/push-notifications";
|
||||
import { isElectron } from "@/core/electron/electron";
|
||||
|
||||
export type ChatTabs = "chats" | "channels" | "contacts" | "dms";
|
||||
export type ChatTabs = "chats" | "channels" | "contacts";
|
||||
|
||||
export type CallStatus = "calling" | "connecting" | "active" | "ended";
|
||||
|
||||
@@ -403,7 +403,7 @@ export const useAppState = create<AppState>((set, get) => ({
|
||||
username: dmData.username,
|
||||
publicKey: dmData.publicKey
|
||||
},
|
||||
activeTab: "dms"
|
||||
activeTab: "chats"
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user