feat(chats): add per-user chat archive support

This commit is contained in:
2026-03-08 09:53:28 +03:00
parent 76f008d635
commit fdf973eeab
10 changed files with 248 additions and 16 deletions

View File

@@ -8,9 +8,12 @@ export interface ChatNotificationSettings {
muted: boolean;
}
export async function getChats(query?: string): Promise<Chat[]> {
export async function getChats(query?: string, archived = false): Promise<Chat[]> {
const { data } = await http.get<Chat[]>("/chats", {
params: query?.trim() ? { query: query.trim() } : undefined
params: {
...(query?.trim() ? { query: query.trim() } : {}),
...(archived ? { archived: true } : {})
}
});
return data;
}
@@ -185,6 +188,16 @@ export async function clearChat(chatId: number): Promise<void> {
await http.post(`/chats/${chatId}/clear`);
}
export async function archiveChat(chatId: number): Promise<Chat> {
const { data } = await http.post<Chat>(`/chats/${chatId}/archive`);
return data;
}
export async function unarchiveChat(chatId: number): Promise<Chat> {
const { data } = await http.post<Chat>(`/chats/${chatId}/unarchive`);
return data;
}
export async function deleteMessage(messageId: number, forAll = false): Promise<void> {
await http.delete(`/messages/${messageId}`, { params: { for_all: forAll } });
}

View File

@@ -12,6 +12,7 @@ export interface Chat {
description?: string | null;
is_public?: boolean;
is_saved?: boolean;
archived?: boolean;
unread_count?: number;
pinned_message_id?: number | null;
members_count?: number | null;

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { clearChat, createPrivateChat, deleteChat, getChats, joinChat } from "../api/chats";
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";
@@ -20,8 +20,9 @@ export function ChatList() {
const [userResults, setUserResults] = useState<UserSearchItem[]>([]);
const [discoverResults, setDiscoverResults] = useState<DiscoverChat[]>([]);
const [messageResults, setMessageResults] = useState<Message[]>([]);
const [archivedChats, setArchivedChats] = useState<typeof chats>([]);
const [searchLoading, setSearchLoading] = useState(false);
const [tab, setTab] = useState<"all" | "people" | "groups" | "channels">("all");
const [tab, setTab] = useState<"all" | "people" | "groups" | "channels" | "archived">("all");
const [ctxChatId, setCtxChatId] = useState<number | null>(null);
const [ctxPos, setCtxPos] = useState<{ x: number; y: number } | null>(null);
const [deleteModalChatId, setDeleteModalChatId] = useState<number | null>(null);
@@ -45,6 +46,28 @@ export function ChatList() {
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) {
@@ -108,6 +131,9 @@ export function ChatList() {
}, [me]);
const filteredChats = chats.filter((chat) => {
if (chat.archived) {
return false;
}
if (tab === "people") {
return chat.type === "private";
}
@@ -120,13 +146,16 @@ export function ChatList() {
return true;
});
const tabs: Array<{ id: "all" | "people" | "groups" | "channels"; label: string }> = [
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: "channels", label: "Каналы" },
{ id: "archived", label: "Архив" }
];
const visibleChats = tab === "archived" ? archivedChats : filteredChats;
return (
<aside className="relative flex h-full w-full max-w-xs flex-col bg-slate-900/60" onClick={() => { setCtxChatId(null); setCtxPos(null); }}>
<div className="border-b border-slate-700/50 px-3 py-3">
@@ -238,7 +267,7 @@ export function ChatList() {
) : null}
</div>
<div className="tg-scrollbar flex-1 overflow-auto">
{filteredChats.map((chat) => (
{visibleChats.map((chat) => (
<button
className={`block w-full border-b border-slate-800/60 px-4 py-3 text-left transition ${
activeChatId === chat.id ? "bg-sky-500/20" : "hover:bg-slate-800/65"
@@ -291,6 +320,27 @@ export function ChatList() {
>
{chats.find((c) => c.id === ctxChatId)?.is_saved ? "Clear chat" : "Delete chat"}
</button>
<button
className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800"
onClick={async () => {
const target = chats.find((c) => c.id === ctxChatId) ?? archivedChats.find((c) => c.id === ctxChatId);
if (!target) {
return;
}
if (target.archived) {
await unarchiveChat(target.id);
} else {
await archiveChat(target.id);
}
await loadChats();
const nextArchived = await getChats(undefined, true);
setArchivedChats(nextArchived);
setCtxChatId(null);
setCtxPos(null);
}}
>
{(chats.find((c) => c.id === ctxChatId) ?? archivedChats.find((c) => c.id === ctxChatId))?.archived ? "Unarchive" : "Archive"}
</button>
</div>,
document.body
)