import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { archiveChat, clearChat, createPrivateChat, deleteChat, getChats, joinChat, unarchiveChat } 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"; import { NewChatPanel } from "./NewChatPanel"; export function ChatList() { const chats = useChatStore((s) => s.chats); 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([]); const [discoverResults, setDiscoverResults] = useState([]); const [messageResults, setMessageResults] = useState([]); const [archivedChats, setArchivedChats] = useState([]); const [searchLoading, setSearchLoading] = useState(false); const [tab, setTab] = useState<"all" | "people" | "groups" | "channels" | "archived">("all"); const [ctxChatId, setCtxChatId] = useState(null); const [ctxPos, setCtxPos] = useState<{ x: number; y: number } | null>(null); const [deleteModalChatId, setDeleteModalChatId] = useState(null); const [deleteForAll, setDeleteForAll] = useState(false); const [profileOpen, setProfileOpen] = useState(false); const [profileName, setProfileName] = useState(""); const [profileUsername, setProfileUsername] = useState(""); const [profileBio, setProfileBio] = useState(""); const [profileAvatarUrl, setProfileAvatarUrl] = useState(""); const [profileAllowPrivateMessages, setProfileAllowPrivateMessages] = useState(true); const [profileError, setProfileError] = useState(null); const [profileSaving, setProfileSaving] = useState(false); const deleteModalChat = chats.find((chat) => chat.id === deleteModalChatId) ?? null; const canDeleteForEveryone = Boolean( deleteModalChat && !deleteModalChat.is_saved && (deleteModalChat.type === "group" || deleteModalChat.type === "private") ); useEffect(() => { void loadChats(); }, [loadChats]); useEffect(() => { if (tab !== "archived") { return; } let cancelled = false; void (async () => { try { const rows = await getChats(undefined, true); if (!cancelled) { setArchivedChats(rows); } } catch { if (!cancelled) { setArchivedChats([]); } } })(); return () => { cancelled = true; }; }, [tab]); useEffect(() => { const term = search.trim(); if (term.replace("@", "").length < 2) { setUserResults([]); setDiscoverResults([]); setMessageResults([]); setSearchLoading(false); return; } let cancelled = false; setSearchLoading(true); void (async () => { try { const result = await globalSearch(term); if (cancelled) { return; } setUserResults(result.users); setDiscoverResults(result.chats); setMessageResults(result.messages); } catch { if (!cancelled) { setUserResults([]); setDiscoverResults([]); setMessageResults([]); } } finally { if (!cancelled) { setSearchLoading(false); } } })(); return () => { cancelled = true; }; }, [search]); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.key !== "Escape") { return; } setCtxChatId(null); setCtxPos(null); setDeleteModalChatId(null); setProfileOpen(false); }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, []); useEffect(() => { if (!me) { return; } setProfileName(me.name || ""); setProfileUsername(me.username || ""); setProfileBio(me.bio || ""); setProfileAvatarUrl(me.avatar_url || ""); setProfileAllowPrivateMessages(me.allow_private_messages ?? true); }, [me]); const filteredChats = chats.filter((chat) => { if (chat.archived) { return false; } if (tab === "people") { return chat.type === "private"; } if (tab === "groups") { return chat.type === "group"; } if (tab === "channels") { return chat.type === "channel"; } return true; }); const tabs: Array<{ id: "all" | "people" | "groups" | "channels" | "archived"; label: string }> = [ { id: "all", label: "All" }, { id: "people", label: "Люди" }, { id: "groups", label: "Groups" }, { id: "channels", label: "Каналы" }, { id: "archived", label: "Архив" } ]; const visibleChats = tab === "archived" ? archivedChats : filteredChats; return ( ); } function getSafeContextPosition(x: number, y: number, menuWidth: number, menuHeight: number): { x: number; y: number } { const pad = 8; const cursorOffset = 4; const wantedX = x + cursorOffset; const wantedY = y + cursorOffset; const safeX = Math.min(Math.max(pad, wantedX), window.innerWidth - menuWidth - pad); const safeY = Math.min(Math.max(pad, wantedY), window.innerHeight - menuHeight - pad); return { x: safeX, y: safeY }; } function chatLabel(chat: { display_title?: string | null; title: string | null; type: "private" | "group" | "channel"; is_saved?: boolean }): string { if (chat.display_title?.trim()) return chat.display_title; if (chat.title?.trim()) return chat.title; if (chat.is_saved) return "Saved Messages"; if (chat.type === "private") return "Direct chat"; if (chat.type === "group") return "Group"; return "Channel"; } function chatMetaLabel(chat: { type: "private" | "group" | "channel"; is_saved?: boolean; counterpart_is_online?: boolean | null; counterpart_last_seen_at?: string | null; members_count?: number | null; online_count?: number | null; subscribers_count?: number | null; }): string { if (chat.is_saved) { return "Personal cloud chat"; } if (chat.type === "private") { if (chat.counterpart_is_online) { return "online"; } if (chat.counterpart_last_seen_at) { return `last seen ${formatLastSeen(chat.counterpart_last_seen_at)}`; } return "offline"; } if (chat.type === "group") { const members = chat.members_count ?? 0; const online = chat.online_count ?? 0; return `${members} members, ${online} online`; } const subscribers = chat.subscribers_count ?? chat.members_count ?? 0; return `${subscribers} subscribers`; } function formatLastSeen(value: string): string { const date = new Date(value); if (Number.isNaN(date.getTime())) { return "recently"; } return date.toLocaleString(undefined, { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }); }