feat(core): clear saved chat and add message deletion scopes
Some checks failed
CI / test (push) Failing after 26s

backend:

- add message_hidden table for per-user message hiding

- support DELETE /messages/{id}?for_all=true|false

- implement delete-for-me vs delete-for-all logic by chat type/permissions

- add POST /chats/{chat_id}/clear and route saved chat deletion to clear

web:

- saved messages action changed from delete to clear

- message context menu now supports delete modal: for me / for everyone

- add local store helpers removeMessage/clearChatMessages

- include realtime stability improvements and app error boundary
This commit is contained in:
2026-03-08 01:13:20 +03:00
parent a42f97962b
commit 7f15edcb4e
15 changed files with 486 additions and 77 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { deleteChat } from "../api/chats";
import { clearChat, deleteChat } from "../api/chats";
import { updateMyProfile } from "../api/users";
import { useAuthStore } from "../store/authStore";
import { useChatStore } from "../store/chatStore";
@@ -27,7 +27,11 @@ export function ChatList() {
const [profileError, setProfileError] = useState<string | null>(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");
const canDeleteForEveryone = Boolean(
deleteModalChat &&
!deleteModalChat.is_saved &&
(deleteModalChat.type === "group" || deleteModalChat.type === "private")
);
useEffect(() => {
const timer = setTimeout(() => {
@@ -36,6 +40,20 @@ export function ChatList() {
return () => clearTimeout(timer);
}, [search, loadChats]);
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;
@@ -141,7 +159,7 @@ export function ChatList() {
setDeleteForAll(false);
}}
>
Delete chat
{chats.find((c) => c.id === ctxChatId)?.is_saved ? "Clear chat" : "Delete chat"}
</button>
</div>,
document.body
@@ -151,10 +169,15 @@ export function ChatList() {
{deleteModalChatId ? (
<div className="absolute inset-0 z-50 flex items-end justify-center bg-slate-950/55 p-3">
<div className="w-full rounded-xl border border-slate-700/80 bg-slate-900 p-3">
<p className="mb-2 text-sm font-semibold">Delete chat: {deleteModalChat ? chatLabel(deleteModalChat) : "selected chat"}</p>
<p className="mb-2 text-sm font-semibold">
{deleteModalChat?.is_saved ? "Clear chat" : "Delete chat"}: {deleteModalChat ? chatLabel(deleteModalChat) : "selected chat"}
</p>
{deleteModalChat?.type === "channel" ? (
<p className="mb-3 text-xs text-slate-400">Channels are removed for all subscribers.</p>
) : null}
{deleteModalChat?.is_saved ? (
<p className="mb-3 text-xs text-slate-400">This will clear all messages in Saved Messages.</p>
) : null}
{canDeleteForEveryone ? (
<label className="mb-3 flex items-center gap-2 text-sm">
<input checked={deleteForAll} onChange={(e) => setDeleteForAll(e.target.checked)} type="checkbox" />
@@ -165,6 +188,12 @@ export function ChatList() {
<button
className="flex-1 rounded bg-red-500 px-3 py-2 text-sm font-semibold text-white"
onClick={async () => {
if (deleteModalChat?.is_saved) {
await clearChat(deleteModalChatId);
useChatStore.getState().clearChatMessages(deleteModalChatId);
setDeleteModalChatId(null);
return;
}
await deleteChat(deleteModalChatId, canDeleteForEveryone && deleteForAll);
await loadChats(search.trim() ? search : undefined);
if (activeChatId === deleteModalChatId) {
@@ -173,7 +202,7 @@ export function ChatList() {
setDeleteModalChatId(null);
}}
>
Delete
{deleteModalChat?.is_saved ? "Clear" : "Delete"}
</button>
<button className="flex-1 rounded bg-slate-700 px-3 py-2 text-sm" onClick={() => setDeleteModalChatId(null)}>
Cancel