web: move contacts to dedicated burger-menu screen
Some checks failed
CI / test (push) Failing after 24s

This commit is contained in:
2026-03-08 11:57:01 +03:00
parent f6fecf57c7
commit 1546ae7381

View File

@@ -23,10 +23,12 @@ export function ChatList() {
const [messageResults, setMessageResults] = useState<Message[]>([]); const [messageResults, setMessageResults] = useState<Message[]>([]);
const [archivedChats, setArchivedChats] = useState<typeof chats>([]); const [archivedChats, setArchivedChats] = useState<typeof chats>([]);
const [searchLoading, setSearchLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false);
const [tab, setTab] = useState<"all" | "contacts" | "people" | "groups" | "channels" | "archived">("all"); const [tab, setTab] = useState<"all" | "people" | "groups" | "channels" | "archived">("all");
const [archivedLoading, setArchivedLoading] = useState(false); const [archivedLoading, setArchivedLoading] = useState(false);
const [contactsLoading, setContactsLoading] = useState(false); const [contactsLoading, setContactsLoading] = useState(false);
const [contacts, setContacts] = useState<UserSearchItem[]>([]); const [contacts, setContacts] = useState<UserSearchItem[]>([]);
const [contactsOpen, setContactsOpen] = useState(false);
const [contactsSearch, setContactsSearch] = useState("");
const [contactEmail, setContactEmail] = useState(""); const [contactEmail, setContactEmail] = useState("");
const [contactEmailError, setContactEmailError] = useState<string | null>(null); const [contactEmailError, setContactEmailError] = useState<string | null>(null);
const [ctxChatId, setCtxChatId] = useState<number | null>(null); const [ctxChatId, setCtxChatId] = useState<number | null>(null);
@@ -79,7 +81,7 @@ export function ChatList() {
}, [tab, chats.length]); }, [tab, chats.length]);
useEffect(() => { useEffect(() => {
if (tab !== "contacts") { if (!contactsOpen) {
return; return;
} }
let cancelled = false; let cancelled = false;
@@ -103,7 +105,7 @@ export function ChatList() {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [tab]); }, [contactsOpen]);
useEffect(() => { useEffect(() => {
const term = search.trim(); const term = search.trim();
@@ -273,7 +275,7 @@ export function ChatList() {
<button className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-slate-800" onClick={() => { void openSavedMessages(); setMenuOpen(false); }}> <button className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-slate-800" onClick={() => { void openSavedMessages(); setMenuOpen(false); }}>
Saved Messages Saved Messages
</button> </button>
<button className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-slate-800" onClick={() => { setTab("contacts"); setMenuOpen(false); }}> <button className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-slate-800" onClick={() => { setContactsOpen(true); setMenuOpen(false); }}>
Contacts Contacts
</button> </button>
<button className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-slate-800" onClick={() => { setSettingsOpen(true); setMenuOpen(false); }}> <button className="block w-full rounded px-3 py-2 text-left text-sm hover:bg-slate-800" onClick={() => { setSettingsOpen(true); setMenuOpen(false); }}>
@@ -338,7 +340,7 @@ export function ChatList() {
className="rounded bg-slate-700 px-2 py-1 text-[10px] hover:bg-slate-600" className="rounded bg-slate-700 px-2 py-1 text-[10px] hover:bg-slate-600"
onClick={async () => { onClick={async () => {
await addContact(user.id); await addContact(user.id);
if (tab === "contacts") { if (contactsOpen) {
setContacts(await listContacts()); setContacts(await listContacts());
} }
}} }}
@@ -433,10 +435,38 @@ export function ChatList() {
{tab === "archived" && !archivedLoading && archivedChats.length === 0 ? ( {tab === "archived" && !archivedLoading && archivedChats.length === 0 ? (
<p className="px-4 py-3 text-xs text-slate-400">Archive is empty</p> <p className="px-4 py-3 text-xs text-slate-400">Archive is empty</p>
) : null} ) : null}
{tab === "contacts" && contactsLoading ? ( {pinnedVisibleChats.length > 0 ? (
<p className="px-4 py-3 text-xs text-slate-400">Loading contacts...</p> <p className="px-4 py-2 text-[10px] uppercase tracking-wide text-slate-400">Pinned</p>
) : null} ) : null}
{tab === "contacts" ? ( {pinnedVisibleChats.map(renderChatRow)}
{regularVisibleChats.length > 0 ? (
<p className="px-4 py-2 text-[10px] uppercase tracking-wide text-slate-400">Chats</p>
) : null}
{regularVisibleChats.map(renderChatRow)}
</div>
{contactsOpen ? (
<div className="absolute inset-0 z-30 flex flex-col bg-slate-900">
<div className="border-b border-slate-700/50 px-4 py-3">
<div className="mb-3 flex items-center gap-2">
<button
className="rounded-lg bg-slate-700/70 px-2 py-1.5 text-xs"
onClick={() => {
setContactsOpen(false);
setContactsSearch("");
}}
type="button"
>
Back
</button>
<p className="text-sm font-semibold">Contacts</p>
</div>
<input
className="w-full rounded-full 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="Search contacts"
value={contactsSearch}
onChange={(e) => setContactsSearch(e.target.value)}
/>
</div>
<div className="border-b border-slate-800/60 px-4 py-3"> <div className="border-b border-slate-800/60 px-4 py-3">
<p className="mb-2 text-[10px] uppercase tracking-wide text-slate-400">Add contact (Email)</p> <p className="mb-2 text-[10px] uppercase tracking-wide text-slate-400">Add contact (Email)</p>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -472,48 +502,69 @@ export function ChatList() {
</div> </div>
{contactEmailError ? <p className="mt-1 text-[11px] text-red-400">{contactEmailError}</p> : null} {contactEmailError ? <p className="mt-1 text-[11px] text-red-400">{contactEmailError}</p> : null}
</div> </div>
) : null} <div className="tg-scrollbar flex-1 overflow-auto">
{tab === "contacts" && !contactsLoading && contacts.length === 0 ? ( {contactsLoading ? (
<p className="px-4 py-3 text-xs text-slate-400">No contacts yet</p> <p className="px-4 py-3 text-xs text-slate-400">Loading contacts...</p>
) : null} ) : null}
{tab === "contacts" {!contactsLoading &&
? contacts.map((user) => ( contacts.filter((user) => {
<div className="flex items-center gap-2 border-b border-slate-800/60 px-4 py-3" key={`contact-${user.id}`}> const term = contactsSearch.trim().toLowerCase();
<button if (!term) {
className="min-w-0 flex-1 text-left" return true;
onClick={async () => { }
const chat = await createPrivateChat(user.id); return (
const updatedChats = await getChats(); (user.name || "").toLowerCase().includes(term) ||
useChatStore.setState({ chats: updatedChats, activeChatId: chat.id }); (user.username || "").toLowerCase().includes(term) ||
}} (user.email || "").toLowerCase().includes(term)
type="button" );
> }).length === 0 ? (
<p className="truncate text-sm font-semibold">{user.name}</p> <p className="px-4 py-3 text-xs text-slate-400">No contacts yet</p>
<p className="truncate text-xs text-slate-400">{user.email || `@${user.username}`}</p> ) : null}
</button> {!contactsLoading
<button ? contacts
className="rounded bg-slate-700 px-2 py-1 text-[10px] text-red-200 hover:bg-slate-600" .filter((user) => {
onClick={async () => { const term = contactsSearch.trim().toLowerCase();
await removeContact(user.id); if (!term) {
setContacts((prev) => prev.filter((item) => item.id !== user.id)); return true;
}} }
type="button" return (
> (user.name || "").toLowerCase().includes(term) ||
Remove (user.username || "").toLowerCase().includes(term) ||
</button> (user.email || "").toLowerCase().includes(term)
</div> );
)) })
: null} .map((user) => (
<div className="flex items-center gap-2 border-b border-slate-800/60 px-4 py-3" key={`contact-${user.id}`}>
{tab !== "contacts" && pinnedVisibleChats.length > 0 ? ( <button
<p className="px-4 py-2 text-[10px] uppercase tracking-wide text-slate-400">Pinned</p> className="min-w-0 flex-1 text-left"
) : null} onClick={async () => {
{tab !== "contacts" ? pinnedVisibleChats.map(renderChatRow) : null} const chat = await createPrivateChat(user.id);
{tab !== "contacts" && regularVisibleChats.length > 0 ? ( const updatedChats = await getChats();
<p className="px-4 py-2 text-[10px] uppercase tracking-wide text-slate-400">Chats</p> useChatStore.setState({ chats: updatedChats, activeChatId: chat.id });
) : null} setContactsOpen(false);
{tab !== "contacts" ? regularVisibleChats.map(renderChatRow) : null} setContactsSearch("");
</div> }}
type="button"
>
<p className="truncate text-sm font-semibold">{user.name}</p>
<p className="truncate text-xs text-slate-400">{user.email || `@${user.username}`}</p>
</button>
<button
className="rounded bg-slate-700 px-2 py-1 text-[10px] text-red-200 hover:bg-slate-600"
onClick={async () => {
await removeContact(user.id);
setContacts((prev) => prev.filter((item) => item.id !== user.id));
}}
type="button"
>
Remove
</button>
</div>
))
: null}
</div>
</div>
) : null}
<NewChatPanel /> <NewChatPanel />
{ctxChatId && ctxPos {ctxChatId && ctxPos