diff --git a/mobile/src/session/use-mobile-native-chat-drafts-glued-pending.test.ts b/mobile/src/session/use-mobile-native-chat-drafts-glued-pending.test.ts index 2b89164ac06..bcea1f1054f 100644 --- a/mobile/src/session/use-mobile-native-chat-drafts-glued-pending.test.ts +++ b/mobile/src/session/use-mobile-native-chat-drafts-glued-pending.test.ts @@ -25,6 +25,16 @@ function assistantTurn(id: string, text: string, timestamp: number): NativeChatM } } +function imageTurn(id: string, path: string, timestamp: number): NativeChatMessage { + return { + id, + role: 'user', + blocks: [{ type: 'text', text: `[Image: source: ${path}]` }], + timestamp, + source: 'transcript' + } +} + /** Texts of the optimistic bubbles the chat list actually renders. */ function renderedPendingTexts( messages: NativeChatMessage[], @@ -299,6 +309,23 @@ describe('useMobileNativeChatDrafts glued pending sends', () => { expect(renderedPendingTexts(olderIdentical, state)).toEqual(['fix the', 'bug']) }) + // The preview pass runs before the rebase, so an entry with no boundary + // would bind the user's fresh photo to whatever image turn the read carries. + it('does not bind a photo sent before the read to an image turn already in it', async () => { + await mount([], false, false) + act(() => { + const origin = state?.captureSendOrigin('') + if (origin) { + state?.acceptSend(origin, '', ['file:///new.png']) + } + }) + + const olderImage = [imageTurn('m1', '/old/photo.png', 1000)] + await update(olderImage) + expect(state?.imagePreviewsByMessageId).toEqual({}) + expect(state?.pending.map((item) => item.images)).toEqual([['file:///new.png']]) + }) + it('still retires them once their own glued row lands', async () => { await mount([], false, false) rapidSend('fix the', 'bug') diff --git a/mobile/src/session/use-mobile-native-chat-drafts.ts b/mobile/src/session/use-mobile-native-chat-drafts.ts index e11df733987..2597a615cc5 100644 --- a/mobile/src/session/use-mobile-native-chat-drafts.ts +++ b/mobile/src/session/use-mobile-native-chat-drafts.ts @@ -291,7 +291,13 @@ export function useMobileNativeChatDrafts(args: { if (pending.length === 0) { return } - const landedImagePreviews = findLandedImagePreviewEchoes(messages, pending) + // This pass runs before the rebase below, so a send captured with no + // boundary would claim any image turn in the read — binding the user's fresh + // photo to an old one. It waits a tick and claims against a real tail. + const landedImagePreviews = findLandedImagePreviewEchoes( + messages, + pending.filter((item) => item.baselineResolved) + ) const landedImagePendingIds = new Set(landedImagePreviews.map((preview) => preview.pendingId)) if (landedImagePreviews.length > 0) { setImagePreviewsBySession((previous) =>