Migrate message forwarding to backend REST endpoint
Both real mobile clients (iOS, Android) forward messages via POST
/v1/im/messages/forward, never a client-side {pub} packet -- confirmed
by reading their source directly. Switches web's single-message and
bulk forward paths to the same endpoint, which copies message content
verbatim regardless of shape, eliminating the client-side flat-content
detection this used to require. Comment-sending stays on the existing
sendText() path, unchanged, to preserve current ordering.
Removes send.ts's now-unused forwardMessage/flatForwardPayload.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e7cb4edf3a
commit
4cd2b33c53
@@ -17,7 +17,7 @@ import { VoiceRecorder } from '@/features/messages/VoiceRecorder';
|
||||
import { GroupAnnouncementModal } from '@/features/groups/GroupAnnouncementModal';
|
||||
import { JoinRequestsModal } from '@/features/groups/JoinRequestsModal';
|
||||
import { useBootstrap } from '@/lib/api/hooks';
|
||||
import { favoriteMessages, getTopicDraft, type MediaType, putTopicDraft, recallMessage } from '@/lib/api/messages';
|
||||
import { favoriteMessages, forwardMessages, getTopicDraft, type MediaType, putTopicDraft, recallMessage } from '@/lib/api/messages';
|
||||
import { getAllGroupMembers, getAllGroupMutes, getGroup, getGroupGovernance, getGroupSummary } from '@/lib/api/groups';
|
||||
import { getConversationContext, getUserProfile } from '@/lib/api/friends';
|
||||
import { getMe } from '@/lib/api/profile';
|
||||
@@ -31,7 +31,7 @@ import { groupSenderLabel } from '@/lib/messages/sender-name';
|
||||
import { sendFile, sendImage, sendVoice } from '@/lib/tinode/attachments';
|
||||
import { isTopicGone } from '@/lib/tinode/errors';
|
||||
import { useChatList, useTinode, useTopicMessages } from '@/lib/tinode/hooks';
|
||||
import { forwardMessage, sendPrebuiltContent, sendReply, sendText } from '@/lib/tinode/send';
|
||||
import { sendPrebuiltContent, sendReply, sendText } from '@/lib/tinode/send';
|
||||
import {
|
||||
closeConversation,
|
||||
ensureTopicAttached,
|
||||
@@ -818,25 +818,36 @@ export function ConversationView() {
|
||||
// order, landing #10 ahead of #3 in the target conversation and misrepresenting which one was
|
||||
// actually said first.
|
||||
const orderedSeqs = [...selectedSeqs].sort((a, b) => a - b);
|
||||
for (const targetTopic of targetTopics) {
|
||||
// A target that isn't the conversation currently open (i.e. almost any real forward target)
|
||||
// is not actually subscribed, and publishing on an unsubscribed topic is rejected outright by
|
||||
// the SDK -- see ensureTopicAttached's own comment for why this is the common case, not an
|
||||
// edge case.
|
||||
ensureTopicAttached(targetTopic)
|
||||
.then((target) => {
|
||||
for (const seq of orderedSeqs) {
|
||||
const msg = topic.findMessage(seq);
|
||||
// A message can be recalled by its sender AFTER it was already selected (the checkbox
|
||||
// itself refuses a NEW selection of a recalled message, but can't retroactively un-select
|
||||
// one recalled mid-session) -- forwarding null content would silently send an empty bubble
|
||||
// with a misleading "forwarded from" tag rather than the message the user actually picked.
|
||||
if (msg && !isRecalled(msg)) forwardMessage(topicId, msg, target).catch((err) => showError(err, '转发失败'));
|
||||
}
|
||||
if (comment) sendText(target, comment).catch((err) => showError(err, '附言发送失败'));
|
||||
// A message can be recalled by its sender AFTER it was already selected (the checkbox itself
|
||||
// refuses a NEW selection of a recalled message, but can't retroactively un-select one recalled
|
||||
// mid-session) -- forwarding null content would silently send an empty bubble with a misleading
|
||||
// "forwarded from" tag rather than the message the user actually picked.
|
||||
const sources = orderedSeqs
|
||||
.map((seq) => topic.findMessage(seq))
|
||||
.filter((msg): msg is StoredMessage => !!msg && !isRecalled(msg))
|
||||
.map((msg) => ({ topic: topicId, seq: msg.seq as number }));
|
||||
// One REST call now covers every (source, target) pair -- forwardMessages()'s own comment
|
||||
// explains why this replaced per-target `ensureTopicAttached()` + per-message client-side
|
||||
// `{pub}` publishes (was N sources x M targets separate WS sends, each independently racing
|
||||
// subscription/attachment; the server handles every pair in one request with real per-pair
|
||||
// success/failure reporting instead of each WS publish failing silently on its own).
|
||||
if (sources.length > 0) {
|
||||
forwardMessages(sources, targetTopics)
|
||||
.then((res) => {
|
||||
if (res.failed.length > 0) showError(undefined, `${res.failed.length} 条消息转发失败`);
|
||||
})
|
||||
.catch((err) => showError(err, '转发失败'));
|
||||
}
|
||||
// Comment stays on the OLD client-side path deliberately -- see forwardMessages()'s own comment
|
||||
// on why (preserves this app's existing "comment appears AFTER the forwarded messages" order,
|
||||
// which the endpoint's own built-in comment field does not).
|
||||
if (comment) {
|
||||
for (const targetTopic of targetTopics) {
|
||||
ensureTopicAttached(targetTopic)
|
||||
.then((target) => sendText(target, comment))
|
||||
.catch((err) => showError(err, '附言发送失败'));
|
||||
}
|
||||
}
|
||||
setBulkForwardOpen(false);
|
||||
exitSelectMode();
|
||||
};
|
||||
@@ -1218,14 +1229,23 @@ export function ConversationView() {
|
||||
<ForwardPickerModal
|
||||
onClose={() => setForwardMsg(null)}
|
||||
onPick={(targetTopics, comment) => {
|
||||
for (const targetTopic of targetTopics) {
|
||||
ensureTopicAttached(targetTopic)
|
||||
.then((target) => {
|
||||
forwardMessage(topicId, forwardMsg, target).catch((err) => showError(err, '转发失败'));
|
||||
if (comment) sendText(target, comment).catch((err) => showError(err, '附言发送失败'));
|
||||
if (typeof forwardMsg.seq === 'number') {
|
||||
forwardMessages([{ topic: topicId, seq: forwardMsg.seq }], targetTopics)
|
||||
.then((res) => {
|
||||
if (res.failed.length > 0) showError(undefined, `${res.failed.length} 个目标转发失败`);
|
||||
})
|
||||
.catch((err) => showError(err, '转发失败'));
|
||||
}
|
||||
// Comment stays on the OLD client-side path deliberately -- see forwardMessages()'s own
|
||||
// comment in lib/api/messages.ts on why (preserves the existing "comment appears AFTER
|
||||
// the forwarded message" order).
|
||||
if (comment) {
|
||||
for (const targetTopic of targetTopics) {
|
||||
ensureTopicAttached(targetTopic)
|
||||
.then((target) => sendText(target, comment))
|
||||
.catch((err) => showError(err, '附言发送失败'));
|
||||
}
|
||||
}
|
||||
setForwardMsg(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user