fix(realtime,ui): sync message deletes and channel delete/leave behavior
All checks were successful
CI / test (push) Successful in 23s

This commit is contained in:
2026-03-08 12:52:31 +03:00
parent 613edbecfe
commit 82322c4d42
4 changed files with 64 additions and 15 deletions

View File

@@ -13,6 +13,7 @@ from app.messages.schemas import (
MessageStatusUpdateRequest,
MessageUpdateRequest,
)
from app.messages.repository import get_message_by_id
from app.messages.service import (
create_chat_message,
delete_message,
@@ -87,7 +88,10 @@ async def remove_message(
current_user: User = Depends(get_current_user),
) -> None:
if for_all:
message = await get_message_by_id(db, message_id)
await delete_message_for_all(db, message_id=message_id, user_id=current_user.id)
if message:
await realtime_gateway.publish_chat_updated(chat_id=message.chat_id)
return
await delete_message(db, message_id=message_id, user_id=current_user.id)

View File

@@ -206,6 +206,11 @@ async def delete_message(db: AsyncSession, *, message_id: int, user_id: int) ->
membership = await chats_repository.get_chat_member(db, chat_id=message.chat_id, user_id=user_id)
if not membership:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You are not a member of this chat")
if chat.type == ChatType.CHANNEL and not chat.is_saved:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Channel messages can only be deleted for everyone",
)
# Telegram-like default: delete only for current user.
hidden = await repository.get_hidden_message(db, message_id=message.id, user_id=user_id)
if not hidden:

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { archiveChat, clearChat, createPrivateChat, deleteChat, getChats, getSavedMessagesChat, joinChat, pinChat, unarchiveChat, unpinChat } from "../api/chats";
import { archiveChat, clearChat, createPrivateChat, deleteChat, getChats, getSavedMessagesChat, joinChat, leaveChat, pinChat, unarchiveChat, unpinChat } from "../api/chats";
import { globalSearch } from "../api/search";
import type { DiscoverChat, Message, UserSearchItem } from "../chat/types";
import { addContact, addContactByEmail, listContacts, removeContact, updateMyProfile } from "../api/users";
@@ -50,7 +50,16 @@ export function ChatList() {
const canDeleteForEveryone = Boolean(
deleteModalChat &&
!deleteModalChat.is_saved &&
(deleteModalChat.type === "group" || deleteModalChat.type === "private")
(
deleteModalChat.type === "group" ||
deleteModalChat.type === "private" ||
(deleteModalChat.type === "channel" && (deleteModalChat.my_role === "owner" || deleteModalChat.my_role === "admin"))
)
);
const channelMemberLeaveOnly = Boolean(
deleteModalChat &&
deleteModalChat.type === "channel" &&
deleteModalChat.my_role === "member"
);
useEffect(() => {
@@ -598,7 +607,13 @@ export function ChatList() {
setDeleteForAll(false);
}}
>
{chats.find((c) => c.id === ctxChatId)?.is_saved ? "Clear chat" : "Delete chat"}
{(() => {
const chat = chats.find((c) => c.id === ctxChatId);
if (!chat) return "Delete chat";
if (chat.is_saved) return "Clear chat";
if (chat.type === "channel" && chat.my_role === "member") return "Leave channel";
return "Delete chat";
})()}
</button>
<button
className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800"
@@ -653,10 +668,19 @@ export function ChatList() {
<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">
{deleteModalChat?.is_saved ? "Clear chat" : "Delete chat"}: {deleteModalChat ? chatLabel(deleteModalChat) : "selected chat"}
{(deleteModalChat?.is_saved
? "Clear chat"
: deleteModalChat?.type === "channel" && deleteModalChat.my_role === "member"
? "Leave channel"
: "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>
<p className="mb-3 text-xs text-slate-400">
{channelMemberLeaveOnly
? "You will leave this channel."
: "Channel deletion removes it 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>
@@ -677,7 +701,11 @@ export function ChatList() {
setDeleteModalChatId(null);
return;
}
await deleteChat(deleteModalChatId, canDeleteForEveryone && deleteForAll);
if (channelMemberLeaveOnly) {
await leaveChat(deleteModalChatId);
} else {
await deleteChat(deleteModalChatId, canDeleteForEveryone && deleteForAll);
}
await loadChats();
if (activeChatId === deleteModalChatId) {
setActiveChatId(null);
@@ -685,7 +713,7 @@ export function ChatList() {
setDeleteModalChatId(null);
}}
>
{deleteModalChat?.is_saved ? "Clear" : "Delete"}
{deleteModalChat?.is_saved ? "Clear" : channelMemberLeaveOnly ? "Leave" : "Delete"}
</button>
<button className="flex-1 rounded bg-slate-700 px-3 py-2 text-sm" onClick={() => setDeleteModalChatId(null)}>
Cancel

View File

@@ -94,6 +94,7 @@ export function MessageList() {
const hasMore = Boolean(activeChatId && hasMoreByChat[activeChatId]);
const isLoadingMore = Boolean(activeChatId && loadingMoreByChat[activeChatId]);
const selectedMessages = useMemo(() => messages.filter((m) => selectedIds.has(m.id)), [messages, selectedIds]);
const channelOnlyDeleteForAll = activeChat?.type === "channel" && !activeChat?.is_saved;
const canDeleteAllForSelection = useMemo(
() => selectedMessages.length > 0 && selectedMessages.every((m) => canDeleteForEveryone(m, activeChat, me?.id)),
[selectedMessages, activeChat, me?.id]
@@ -329,9 +330,11 @@ export function MessageList() {
<div className="flex items-center justify-between border-b border-slate-700/50 bg-slate-900/80 px-3 py-2 text-xs">
<span className="font-semibold text-slate-200">{selectedIds.size} selected</span>
<div className="flex items-center gap-2">
<button className="rounded bg-red-500 px-2 py-1 font-semibold text-white" onClick={() => void deleteSelectedForMe()}>
Delete for me
</button>
{!channelOnlyDeleteForAll ? (
<button className="rounded bg-red-500 px-2 py-1 font-semibold text-white" onClick={() => void deleteSelectedForMe()}>
Delete for me
</button>
) : null}
{canDeleteAllForSelection ? (
<button className="rounded bg-red-600 px-2 py-1 font-semibold text-white" onClick={() => void deleteSelectedForEveryone()}>
Delete for everyone
@@ -747,11 +750,17 @@ export function MessageList() {
<div className="absolute inset-0 z-50 flex items-end justify-center bg-slate-950/55 p-3" onClick={() => setDeleteMessageId(null)}>
<div className="w-full rounded-xl border border-slate-700/80 bg-slate-900 p-3" onClick={(event) => event.stopPropagation()}>
<p className="mb-2 text-sm font-semibold">Delete message</p>
<p className="mb-3 text-xs text-slate-400">Choose how to delete this message.</p>
<p className="mb-3 text-xs text-slate-400">
{channelOnlyDeleteForAll
? "In channels, messages can only be deleted for everyone."
: "Choose how to delete this message."}
</p>
<div className="space-y-2">
<button className="w-full rounded bg-red-500 px-3 py-2 text-sm font-semibold text-white" onClick={() => void handleDelete(false)}>
Delete for me
</button>
{!channelOnlyDeleteForAll ? (
<button className="w-full rounded bg-red-500 px-3 py-2 text-sm font-semibold text-white" onClick={() => void handleDelete(false)}>
Delete for me
</button>
) : null}
{canDeleteForEveryone(messagesMap.get(deleteMessageId), activeChat, me?.id) ? (
<button className="w-full rounded bg-red-600 px-3 py-2 text-sm font-semibold text-white" onClick={() => void handleDelete(true)}>
Delete for everyone
@@ -904,12 +913,15 @@ function chatLabel(chat: { display_title?: string | null; title: string | null;
function canDeleteForEveryone(
message: { sender_id: number } | undefined,
chat: { type: "private" | "group" | "channel"; is_saved?: boolean } | undefined,
chat: { type: "private" | "group" | "channel"; is_saved?: boolean; my_role?: "owner" | "admin" | "member" | null } | undefined,
meId: number | undefined
): boolean {
if (!message || !chat || !meId) return false;
if (chat.is_saved) return false;
if (chat.type === "private") return true;
if (chat.type === "channel") {
return chat.my_role === "owner" || chat.my_role === "admin";
}
return message.sender_id === meId;
}