diff --git a/extensions/imessage/src/send.test.ts b/extensions/imessage/src/send.test.ts index 7a163a2ac33..b2ef938ae59 100644 --- a/extensions/imessage/src/send.test.ts +++ b/extensions/imessage/src/send.test.ts @@ -200,6 +200,42 @@ describe("sendMessageIMessage receipts", () => { expect(client["request"]).not.toHaveBeenCalled(); }); + it("preserves audioAsVoice media when replying to an iMessage thread", async () => { + const client = createClient({ message_id: 12345 }); + const runCliJson = vi.fn().mockResolvedValueOnce({ messageId: "p:0/threaded-voice-guid" }); + + const result = await sendMessageIMessage("chat_guid:chat-1", "", { + config: IMESSAGE_TEST_CFG, + client, + mediaUrl: "/tmp/voice.caf", + audioAsVoice: true, + replyToId: "p:0/reply-guid", + resolveAttachmentImpl: async () => ({ path: "/tmp/voice.caf", contentType: "audio/x-caf" }), + runCliJson, + }); + + expect(result.messageId).toBe("p:0/threaded-voice-guid"); + expect(runCliJson.mock.calls).toEqual([ + [ + [ + "send-attachment", + "--chat", + "chat-1", + "--file", + "/tmp/voice.caf", + "--audio", + "--reply-to", + "p:0/reply-guid", + "--transport", + "auto", + ], + ], + ]); + expect(result.receipt.replyToId).toBe("p:0/reply-guid"); + expect(result.receipt.parts.map((part) => part.kind)).toEqual(["voice"]); + expect(client["request"]).not.toHaveBeenCalled(); + }); + it("resolves chat_id media-only payloads before using send-attachment", async () => { const client = createClient({ message_id: 12345 }); const runCliJson = vi diff --git a/extensions/imessage/src/send.ts b/extensions/imessage/src/send.ts index 95b73c91d0c..83f6d974b64 100644 --- a/extensions/imessage/src/send.ts +++ b/extensions/imessage/src/send.ts @@ -731,6 +731,7 @@ async function trySendAttachmentForTarget(params: { service?: IMessageService; filePath: string; audioAsVoice?: boolean; + replyToId?: string; echoText?: string; runCliJson: (args: readonly string[]) => Promise>; resolveMessageGuidImpl?: IMessageSendOpts["resolveMessageGuidImpl"]; @@ -761,6 +762,7 @@ async function trySendAttachmentForTarget(params: { "--file", params.filePath, ...(params.audioAsVoice ? ["--audio"] : []), + ...(params.replyToId ? ["--reply-to", params.replyToId] : []), "--transport", "auto", ]); @@ -826,6 +828,7 @@ async function trySendAttachmentForTarget(params: { messageId, target: params.target, kind: params.audioAsVoice ? "voice" : "media", + ...(params.replyToId ? { replyToId: params.replyToId } : {}), }), }; } @@ -908,7 +911,7 @@ export async function sendMessageIMessage( opts.runCliJson ?? ((args: readonly string[]) => runIMessageCliJson(cliPath, dbPath, args, timeoutMs)); - if (filePath && !resolvedReplyToId) { + if (filePath && (!resolvedReplyToId || opts.audioAsVoice)) { const attachmentEchoText = message.trim() ? resolveOutboundEchoText("", mediaContentType) : echoText; @@ -919,6 +922,7 @@ export async function sendMessageIMessage( service, filePath, audioAsVoice: opts.audioAsVoice, + ...(resolvedReplyToId ? { replyToId: resolvedReplyToId } : {}), echoText: attachmentEchoText, runCliJson, resolveMessageGuidImpl: opts.resolveMessageGuidImpl,