feat(chat): forward messages with multi-select (#1)
- Message action pill gains a Forward button; tapping it enters selection mode (tap bubbles to multi-select, footer bar shows count + Forward/Cancel, Esc exits). - Forward picker lists EXISTING conversations only (DMs + groups from the sidebar), searchable, multi-target. POST /api/messages/forward copies body+attachment into each target (authorized as participant/member), live-pushed like a normal send. - /files auth now accepts ANY message carrying an attachment (allByAttachment), so forwarded images stay viewable for the new recipients. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1341,6 +1341,43 @@ route('POST', '/api/messages', async (req, res) => {
|
||||
json(res, 200, dto);
|
||||
});
|
||||
|
||||
// Forward one or more of my visible messages to existing conversations (DMs I'm in / groups I'm a
|
||||
// member of). Copies body + attachment (attachment stays viewable via the any-carrier /files auth).
|
||||
route('POST', '/api/messages/forward', async (req, res) => {
|
||||
const u = currentUser(req);
|
||||
if (!u) return json(res, 401, { error: 'unauthorized' });
|
||||
const { messageIds, targets } = await readBody(req);
|
||||
if (!Array.isArray(messageIds) || !messageIds.length || !Array.isArray(targets) || !targets.length) return json(res, 400, { error: 'messageIds and targets required' });
|
||||
// Gather source messages the user is allowed to see, oldest-first (preserve order).
|
||||
const srcs = [];
|
||||
for (const mid of messageIds.slice(0, 30)) {
|
||||
const m = R.messages.byId(mid);
|
||||
if (!m || m.team_id !== u.team_id || m.deleted || m.poll_id) continue;
|
||||
const ok = m.conversation_id ? R.conversations.isMember(m.conversation_id, u.id) : (m.sender_id === u.id || m.recipient_id === u.id);
|
||||
if (ok && (m.body || m.attachment_id)) srcs.push(m);
|
||||
}
|
||||
if (!srcs.length) return json(res, 400, { error: 'nothing to forward' });
|
||||
srcs.sort((a, b) => a.created_at - b.created_at);
|
||||
const names = namesFor(u.team_id);
|
||||
let sent = 0;
|
||||
for (const t of (targets || []).slice(0, 20)) {
|
||||
let isGroup = t.kind === 'group', tid = t.id;
|
||||
if (isGroup) { if (!R.conversations.isMember(tid, u.id)) continue; }
|
||||
else { tid = R.users.resolve(tid); if (!tid || !R.users.inTenant(tid, u.team_id)) continue; }
|
||||
for (const m of srcs) {
|
||||
const nid = A.token(16);
|
||||
if (isGroup) R.messages.send({ id: nid, teamId: u.team_id, senderId: u.id, recipientId: '', body: m.body, attachmentId: m.attachment_id, conversationId: tid });
|
||||
else R.messages.send({ id: nid, teamId: u.team_id, senderId: u.id, recipientId: tid, body: m.body, attachmentId: m.attachment_id });
|
||||
const dto = buildMsgDTO(R.messages.byId(nid), names, u.id);
|
||||
const push = { type: 'chat-message', message: { ...dto, fromName: u.name || u.email } };
|
||||
if (isGroup) { for (const mid of R.conversations.members(tid)) { try { CHAT.pushToUser(mid, push); } catch (_) {} } }
|
||||
else { try { CHAT.pushToUser(tid, push); } catch (_) {} if (tid !== u.id) try { CHAT.pushToUser(u.id, push); } catch (_) {} }
|
||||
sent++;
|
||||
}
|
||||
}
|
||||
json(res, 200, { ok: true, sent });
|
||||
});
|
||||
|
||||
// Delete one of YOUR OWN messages for everyone (clears content, keeps the row as a placeholder).
|
||||
route('POST', '/api/messages/delete', async (req, res) => {
|
||||
const u = currentUser(req);
|
||||
|
||||
Reference in New Issue
Block a user