fix(realtime,ui): sync message deletes and channel delete/leave behavior
All checks were successful
CI / test (push) Successful in 23s
All checks were successful
CI / test (push) Successful in 23s
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user