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,8 +1,8 @@
from sqlalchemy import select
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.chats.models import ChatMember
from app.messages.models import Message, MessageIdempotencyKey, MessageReceipt, MessageType
from app.messages.models import Message, MessageHidden, MessageIdempotencyKey, MessageReceipt, MessageType
async def create_message(
@@ -76,10 +76,18 @@ async def list_chat_messages(
db: AsyncSession,
chat_id: int,
*,
user_id: int,
limit: int = 50,
before_id: int | None = None,
) -> list[Message]:
query = select(Message).where(Message.chat_id == chat_id)
query = (
select(Message)
.outerjoin(
MessageHidden,
(MessageHidden.message_id == Message.id) & (MessageHidden.user_id == user_id),
)
.where(Message.chat_id == chat_id, MessageHidden.id.is_(None))
)
if before_id is not None:
query = query.where(Message.id < before_id)
result = await db.execute(query.order_by(Message.id.desc()).limit(limit))
@@ -97,10 +105,15 @@ async def search_messages(
stmt = (
select(Message)
.join(ChatMember, ChatMember.chat_id == Message.chat_id)
.outerjoin(
MessageHidden,
(MessageHidden.message_id == Message.id) & (MessageHidden.user_id == user_id),
)
.where(
ChatMember.user_id == user_id,
Message.text.is_not(None),
Message.text.ilike(f"%{query.strip()}%"),
MessageHidden.id.is_(None),
)
.order_by(Message.id.desc())
.limit(limit)
@@ -115,6 +128,29 @@ async def delete_message(db: AsyncSession, message: Message) -> None:
await db.delete(message)
async def hide_message_for_user(db: AsyncSession, *, message_id: int, user_id: int) -> MessageHidden:
hidden = MessageHidden(message_id=message_id, user_id=user_id)
db.add(hidden)
await db.flush()
return hidden
async def get_hidden_message(db: AsyncSession, *, message_id: int, user_id: int) -> MessageHidden | None:
result = await db.execute(
select(MessageHidden).where(MessageHidden.message_id == message_id, MessageHidden.user_id == user_id).limit(1)
)
return result.scalar_one_or_none()
async def list_chat_message_ids(db: AsyncSession, *, chat_id: int) -> list[int]:
result = await db.execute(select(Message.id).where(Message.chat_id == chat_id))
return list(result.scalars().all())
async def delete_messages_in_chat(db: AsyncSession, *, chat_id: int) -> None:
await db.execute(delete(Message).where(Message.chat_id == chat_id))
async def get_message_receipt(db: AsyncSession, *, chat_id: int, user_id: int) -> MessageReceipt | None:
result = await db.execute(
select(MessageReceipt).where(