feat(messages): support forwarding to multiple chats
This commit is contained in:
@@ -5,6 +5,7 @@ from app.auth.service import get_current_user
|
|||||||
from app.database.session import get_db
|
from app.database.session import get_db
|
||||||
from app.messages.schemas import (
|
from app.messages.schemas import (
|
||||||
MessageCreateRequest,
|
MessageCreateRequest,
|
||||||
|
MessageForwardBulkRequest,
|
||||||
MessageForwardRequest,
|
MessageForwardRequest,
|
||||||
MessageReactionRead,
|
MessageReactionRead,
|
||||||
MessageReactionToggleRequest,
|
MessageReactionToggleRequest,
|
||||||
@@ -17,6 +18,7 @@ from app.messages.service import (
|
|||||||
delete_message,
|
delete_message,
|
||||||
delete_message_for_all,
|
delete_message_for_all,
|
||||||
forward_message,
|
forward_message,
|
||||||
|
forward_message_bulk,
|
||||||
get_messages,
|
get_messages,
|
||||||
list_message_reactions,
|
list_message_reactions,
|
||||||
search_messages,
|
search_messages,
|
||||||
@@ -116,6 +118,19 @@ async def forward_message_endpoint(
|
|||||||
return message
|
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])
|
@router.get("/{message_id}/reactions", response_model=list[MessageReactionRead])
|
||||||
async def list_reactions_endpoint(
|
async def list_reactions_endpoint(
|
||||||
message_id: int,
|
message_id: int,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from app.messages.models import Message
|
|||||||
from app.messages.spam_guard import enforce_message_spam_policy
|
from app.messages.spam_guard import enforce_message_spam_policy
|
||||||
from app.messages.schemas import (
|
from app.messages.schemas import (
|
||||||
MessageCreateRequest,
|
MessageCreateRequest,
|
||||||
|
MessageForwardBulkRequest,
|
||||||
MessageForwardRequest,
|
MessageForwardRequest,
|
||||||
MessageReactionRead,
|
MessageReactionRead,
|
||||||
MessageReactionToggleRequest,
|
MessageReactionToggleRequest,
|
||||||
@@ -276,6 +277,49 @@ async def forward_message(
|
|||||||
return forwarded
|
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(
|
async def list_message_reactions(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -163,6 +163,13 @@ export async function forwardMessage(messageId: number, targetChatId: number): P
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function forwardMessageBulk(messageId: number, targetChatIds: number[]): Promise<Message[]> {
|
||||||
|
const { data } = await http.post<Message[]>(`/messages/${messageId}/forward-bulk`, {
|
||||||
|
target_chat_ids: targetChatIds
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function listMessageReactions(messageId: number): Promise<MessageReaction[]> {
|
export async function listMessageReactions(messageId: number): Promise<MessageReaction[]> {
|
||||||
const { data } = await http.get<MessageReaction[]>(`/messages/${messageId}/reactions`);
|
const { data } = await http.get<MessageReaction[]>(`/messages/${messageId}/reactions`);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
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 type { Message, MessageReaction } from "../chat/types";
|
||||||
import { useAuthStore } from "../store/authStore";
|
import { useAuthStore } from "../store/authStore";
|
||||||
import { useChatStore } from "../store/chatStore";
|
import { useChatStore } from "../store/chatStore";
|
||||||
@@ -40,6 +40,7 @@ export function MessageList() {
|
|||||||
const [forwardQuery, setForwardQuery] = useState("");
|
const [forwardQuery, setForwardQuery] = useState("");
|
||||||
const [forwardError, setForwardError] = useState<string | null>(null);
|
const [forwardError, setForwardError] = useState<string | null>(null);
|
||||||
const [isForwarding, setIsForwarding] = useState(false);
|
const [isForwarding, setIsForwarding] = useState(false);
|
||||||
|
const [forwardSelectedChatIds, setForwardSelectedChatIds] = useState<Set<number>>(new Set());
|
||||||
const [deleteMessageId, setDeleteMessageId] = useState<number | null>(null);
|
const [deleteMessageId, setDeleteMessageId] = useState<number | null>(null);
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
||||||
@@ -86,6 +87,7 @@ export function MessageList() {
|
|||||||
}
|
}
|
||||||
setCtx(null);
|
setCtx(null);
|
||||||
setForwardMessageId(null);
|
setForwardMessageId(null);
|
||||||
|
setForwardSelectedChatIds(new Set());
|
||||||
setDeleteMessageId(null);
|
setDeleteMessageId(null);
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
};
|
};
|
||||||
@@ -98,6 +100,7 @@ export function MessageList() {
|
|||||||
setCtx(null);
|
setCtx(null);
|
||||||
setDeleteMessageId(null);
|
setDeleteMessageId(null);
|
||||||
setForwardMessageId(null);
|
setForwardMessageId(null);
|
||||||
|
setForwardSelectedChatIds(new Set());
|
||||||
setReactionsByMessage({});
|
setReactionsByMessage({});
|
||||||
}, [activeChatId]);
|
}, [activeChatId]);
|
||||||
|
|
||||||
@@ -127,13 +130,19 @@ export function MessageList() {
|
|||||||
}
|
}
|
||||||
const chatId = activeChatId;
|
const chatId = activeChatId;
|
||||||
|
|
||||||
async function handleForward(targetChatId: number) {
|
async function handleForwardSubmit() {
|
||||||
if (!forwardMessageId) return;
|
if (!forwardMessageId) return;
|
||||||
|
const targetChatIds = [...forwardSelectedChatIds];
|
||||||
|
if (!targetChatIds.length) {
|
||||||
|
setForwardError("Select at least one chat");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsForwarding(true);
|
setIsForwarding(true);
|
||||||
setForwardError(null);
|
setForwardError(null);
|
||||||
try {
|
try {
|
||||||
await forwardMessage(forwardMessageId, targetChatId);
|
await forwardMessageBulk(forwardMessageId, targetChatIds);
|
||||||
setForwardMessageId(null);
|
setForwardMessageId(null);
|
||||||
|
setForwardSelectedChatIds(new Set());
|
||||||
setForwardQuery("");
|
setForwardQuery("");
|
||||||
} catch {
|
} catch {
|
||||||
setForwardError("Failed to forward message");
|
setForwardError("Failed to forward message");
|
||||||
@@ -414,6 +423,7 @@ export function MessageList() {
|
|||||||
setForwardMessageId(ctx.messageId);
|
setForwardMessageId(ctx.messageId);
|
||||||
setForwardQuery("");
|
setForwardQuery("");
|
||||||
setForwardError(null);
|
setForwardError(null);
|
||||||
|
setForwardSelectedChatIds(new Set());
|
||||||
setCtx(null);
|
setCtx(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -459,10 +469,20 @@ export function MessageList() {
|
|||||||
<div className="tg-scrollbar max-h-56 space-y-1 overflow-auto">
|
<div className="tg-scrollbar max-h-56 space-y-1 overflow-auto">
|
||||||
{forwardTargets.map((chat) => (
|
{forwardTargets.map((chat) => (
|
||||||
<button
|
<button
|
||||||
className="block w-full rounded-lg bg-slate-800 px-3 py-2 text-left text-sm hover:bg-slate-700 disabled:opacity-60"
|
className={`block w-full rounded-lg px-3 py-2 text-left text-sm disabled:opacity-60 ${forwardSelectedChatIds.has(chat.id) ? "bg-sky-500/30" : "bg-slate-800 hover:bg-slate-700"}`}
|
||||||
disabled={isForwarding}
|
disabled={isForwarding}
|
||||||
key={chat.id}
|
key={chat.id}
|
||||||
onClick={() => void handleForward(chat.id)}
|
onClick={() => {
|
||||||
|
setForwardSelectedChatIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(chat.id)) {
|
||||||
|
next.delete(chat.id);
|
||||||
|
} else {
|
||||||
|
next.add(chat.id);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<p className="truncate font-semibold">{chatLabel(chat)}</p>
|
<p className="truncate font-semibold">{chatLabel(chat)}</p>
|
||||||
<p className="truncate text-xs text-slate-400">{chat.handle ? `@${chat.handle}` : chat.type}</p>
|
<p className="truncate text-xs text-slate-400">{chat.handle ? `@${chat.handle}` : chat.type}</p>
|
||||||
@@ -471,9 +491,14 @@ export function MessageList() {
|
|||||||
{forwardTargets.length === 0 ? <p className="px-1 py-2 text-xs text-slate-400">No chats found</p> : null}
|
{forwardTargets.length === 0 ? <p className="px-1 py-2 text-xs text-slate-400">No chats found</p> : null}
|
||||||
</div>
|
</div>
|
||||||
{forwardError ? <p className="mt-2 text-xs text-red-400">{forwardError}</p> : null}
|
{forwardError ? <p className="mt-2 text-xs text-red-400">{forwardError}</p> : null}
|
||||||
<button className="mt-3 w-full rounded bg-slate-700 px-3 py-2 text-sm" onClick={() => setForwardMessageId(null)}>
|
<div className="mt-3 flex gap-2">
|
||||||
Cancel
|
<button className="w-full rounded bg-sky-500 px-3 py-2 text-sm font-semibold text-slate-950" onClick={() => void handleForwardSubmit()}>
|
||||||
</button>
|
Forward ({forwardSelectedChatIds.size})
|
||||||
|
</button>
|
||||||
|
<button className="w-full rounded bg-slate-700 px-3 py-2 text-sm" onClick={() => setForwardMessageId(null)}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user