From 7c4a5f990d703bbe05c0ceb328ca2296ba7ed992 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 09:55:39 +0300 Subject: [PATCH] feat(messages): support forwarding to multiple chats --- app/messages/router.py | 15 ++++++++++ app/messages/service.py | 44 ++++++++++++++++++++++++++++++ web/src/api/chats.ts | 7 +++++ web/src/components/MessageList.tsx | 41 ++++++++++++++++++++++------ 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/app/messages/router.py b/app/messages/router.py index 705f104..009cbc2 100644 --- a/app/messages/router.py +++ b/app/messages/router.py @@ -5,6 +5,7 @@ from app.auth.service import get_current_user from app.database.session import get_db from app.messages.schemas import ( MessageCreateRequest, + MessageForwardBulkRequest, MessageForwardRequest, MessageReactionRead, MessageReactionToggleRequest, @@ -17,6 +18,7 @@ from app.messages.service import ( delete_message, delete_message_for_all, forward_message, + forward_message_bulk, get_messages, list_message_reactions, search_messages, @@ -116,6 +118,19 @@ async def forward_message_endpoint( return message +@router.post("/{message_id}/forward-bulk", response_model=list[MessageRead], status_code=status.HTTP_201_CREATED) +async def forward_message_bulk_endpoint( + message_id: int, + payload: MessageForwardBulkRequest, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +) -> list[MessageRead]: + messages = await forward_message_bulk(db, source_message_id=message_id, sender_id=current_user.id, payload=payload) + for message in messages: + await realtime_gateway.publish_message_created(message=message, sender_id=current_user.id) + return messages + + @router.get("/{message_id}/reactions", response_model=list[MessageReactionRead]) async def list_reactions_endpoint( message_id: int, diff --git a/app/messages/service.py b/app/messages/service.py index 9d329b5..1aaa59f 100644 --- a/app/messages/service.py +++ b/app/messages/service.py @@ -10,6 +10,7 @@ from app.messages.models import Message from app.messages.spam_guard import enforce_message_spam_policy from app.messages.schemas import ( MessageCreateRequest, + MessageForwardBulkRequest, MessageForwardRequest, MessageReactionRead, MessageReactionToggleRequest, @@ -276,6 +277,49 @@ async def forward_message( return forwarded +async def forward_message_bulk( + db: AsyncSession, + *, + source_message_id: int, + sender_id: int, + payload: MessageForwardBulkRequest, +) -> list[Message]: + source = await repository.get_message_by_id(db, source_message_id) + if not source: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Source message not found") + await ensure_chat_membership(db, chat_id=source.chat_id, user_id=sender_id) + + target_chat_ids = list(dict.fromkeys(payload.target_chat_ids)) + if not target_chat_ids: + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="No target chats") + + forwarded_messages: list[Message] = [] + for target_chat_id in target_chat_ids: + await ensure_chat_membership(db, chat_id=target_chat_id, user_id=sender_id) + target_chat = await chats_repository.get_chat_by_id(db, target_chat_id) + if not target_chat: + continue + target_membership = await chats_repository.get_chat_member(db, chat_id=target_chat_id, user_id=sender_id) + if not target_membership: + continue + if target_chat.type == ChatType.CHANNEL and target_membership.role == ChatMemberRole.MEMBER: + continue + forwarded = await repository.create_message( + db, + chat_id=target_chat_id, + sender_id=sender_id, + reply_to_message_id=None, + forwarded_from_message_id=source.id, + message_type=source.type, + text=source.text, + ) + forwarded_messages.append(forwarded) + await db.commit() + for message in forwarded_messages: + await db.refresh(message) + return forwarded_messages + + async def list_message_reactions( db: AsyncSession, *, diff --git a/web/src/api/chats.ts b/web/src/api/chats.ts index 05d26f4..1d4268c 100644 --- a/web/src/api/chats.ts +++ b/web/src/api/chats.ts @@ -163,6 +163,13 @@ export async function forwardMessage(messageId: number, targetChatId: number): P return data; } +export async function forwardMessageBulk(messageId: number, targetChatIds: number[]): Promise { + const { data } = await http.post(`/messages/${messageId}/forward-bulk`, { + target_chat_ids: targetChatIds + }); + return data; +} + export async function listMessageReactions(messageId: number): Promise { const { data } = await http.get(`/messages/${messageId}/reactions`); return data; diff --git a/web/src/components/MessageList.tsx b/web/src/components/MessageList.tsx index 83c5db0..b781c98 100644 --- a/web/src/components/MessageList.tsx +++ b/web/src/components/MessageList.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; -import { deleteMessage, forwardMessage, listMessageReactions, pinMessage, toggleMessageReaction } from "../api/chats"; +import { deleteMessage, forwardMessageBulk, listMessageReactions, pinMessage, toggleMessageReaction } from "../api/chats"; import type { Message, MessageReaction } from "../chat/types"; import { useAuthStore } from "../store/authStore"; import { useChatStore } from "../store/chatStore"; @@ -40,6 +40,7 @@ export function MessageList() { const [forwardQuery, setForwardQuery] = useState(""); const [forwardError, setForwardError] = useState(null); const [isForwarding, setIsForwarding] = useState(false); + const [forwardSelectedChatIds, setForwardSelectedChatIds] = useState>(new Set()); const [deleteMessageId, setDeleteMessageId] = useState(null); const [deleteError, setDeleteError] = useState(null); const [selectedIds, setSelectedIds] = useState>(new Set()); @@ -86,6 +87,7 @@ export function MessageList() { } setCtx(null); setForwardMessageId(null); + setForwardSelectedChatIds(new Set()); setDeleteMessageId(null); setSelectedIds(new Set()); }; @@ -98,6 +100,7 @@ export function MessageList() { setCtx(null); setDeleteMessageId(null); setForwardMessageId(null); + setForwardSelectedChatIds(new Set()); setReactionsByMessage({}); }, [activeChatId]); @@ -127,13 +130,19 @@ export function MessageList() { } const chatId = activeChatId; - async function handleForward(targetChatId: number) { + async function handleForwardSubmit() { if (!forwardMessageId) return; + const targetChatIds = [...forwardSelectedChatIds]; + if (!targetChatIds.length) { + setForwardError("Select at least one chat"); + return; + } setIsForwarding(true); setForwardError(null); try { - await forwardMessage(forwardMessageId, targetChatId); + await forwardMessageBulk(forwardMessageId, targetChatIds); setForwardMessageId(null); + setForwardSelectedChatIds(new Set()); setForwardQuery(""); } catch { setForwardError("Failed to forward message"); @@ -414,6 +423,7 @@ export function MessageList() { setForwardMessageId(ctx.messageId); setForwardQuery(""); setForwardError(null); + setForwardSelectedChatIds(new Set()); setCtx(null); }} > @@ -459,10 +469,20 @@ export function MessageList() {
{forwardTargets.map((chat) => (
{forwardError ?

{forwardError}

: null} - +
+ + +
) : null}