web: move contacts to dedicated burger-menu screen
Some checks failed
CI / test (push) Failing after 24s
Some checks failed
CI / test (push) Failing after 24s
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user