Skip to content

Keep the attachments of a reply or comment while it sends #266

Description

@HMarzban

Problem

Sending a reply or a document comment with attachments deletes the uploads of those attachments while the message is being saved. Here an attachment is the composer entry, and its upload is the file in the media bucket.

The chain, on 14ab7f9c1. Paths are under apps/webapp/src/components/chatroom/.

  1. A send that fits one row clears the composer before dispatch. shouldClearComposerEarly is true when the chunker returns no chunks and there is no edit (utils/outboundMessagePipeline.ts:52-55, :125). An empty chunk list means the text fits one row (apps/webapp/src/utils/chunkHtmlContent.ts:61-66). A split message has at least two chunks, and attachments with it are refused (utils/outboundMessagePipeline.ts:92). So every reply or comment with attachments takes this path.
  2. cleanupAfterSubmit() runs at components/MessageComposer/hooks/useComposerSubmit.ts:160. It sets the reply and comment memory to null (:86-89).
  3. useComposerAttachmentLifecycle watches that memory. When it goes from set to unset, it calls clearAttachments({ deleteStorage: true }) (components/MessageComposer/hooks/useComposerAttachmentLifecycle.ts:79-86).
  4. clearAttachments calls deleteNonPersistedAttachmentStorage on the current list (components/MessageComposer/hooks/useComposerAttachments.ts:77-81). The attachments being sent are ready and not persisted, so each upload is removed from the bucket (stores/composerAttachmentsStore.ts:97-103).
  5. The composer clears the list with deleteStorage: false only after the send succeeds (useComposerSubmit.ts:182). By then the effect in step 3 has run.

The send and the delete race:

  • The send path marks the media as checked, so persistChatMessage skips its storage probe (utils/outboundMessagePipeline.ts:216, utils/sendChatMessage.ts:58).
  • The effect in step 3 runs before the INSERT is sent. A scratch harness with React 19.2.8 and zustand 5.0.15 showed the delete request leaving first. It did so with the feed at its newest message, and with it scrolled back. Which request the server finishes first is not known.

So there are two outcomes:

  • The message is saved, and its uploads are deleted a moment later. Every reader then sees an unavailable tile, such as "Image unavailable", in place of the media.
  • validate_message_medias() rejects the INSERT. A reply then shows a failed row, and Retry cannot succeed, because the uploads are gone. A comment has no optimistic row and no Retry. It shows only an error toast, and the early clear has already removed its text.

A plain send with attachments is not affected. It has no reply or comment memory, so step 3 does not fire.

Steps to reproduce

  1. Open a channel. Choose Reply on any message.
  2. Attach one image and type a short caption. Send.
  3. Reload the page, or open the channel in a second browser.
  4. See the reply with an "Image unavailable" tile. Or, before the reload, see it marked as failed.
  5. Check the media bucket. The upload is gone.

Acceptance criteria

  • A reply with attachments keeps every upload after it is sent.
  • A document comment with attachments keeps every upload after it is sent.
  • Cancelling a reply or a comment before sending still deletes the uploads of attachments added during that mode.
  • A failed reply keeps its uploads, so Retry on the failed row can succeed.

Agent Brief

Category: bug
Summary: Leaving reply or comment mode because of a send must not delete the uploads that the send carries.

Current behavior:
The early composer clear sets the reply or comment memory to null. A lifecycle effect reads that change as a cancel. It deletes the upload of every unsent attachment, including the attachments the send carries.

Desired behavior:
Attachments that belong to an outbound message are kept, not deleted. Only a real cancel deletes unsent uploads. A real cancel is the user leaving reply or comment mode without sending.

Key interfaces:

  • cleanupAfterSubmit() — runs before dispatch when shouldClearComposerEarly is true.
  • useComposerAttachmentLifecycle() — the set-to-unset effect on reply and comment memory.
  • clearAttachments({ deleteStorage }) and deleteNonPersistedAttachmentStorage() — the delete path.
  • ComposerAttachment.persisted — today it is true only for row-backed media in edit mode.

Out of scope

Notes

Coordinate with #263. It builds the send hand-off on the same early clear.

Coordinate with #290. After a failed plain send, it makes the failed row the only owner of the media. If this fix keeps a failed reply's attachments in the composer, the composer and the failed row share the same uploads.

Leaving reply or comment mode without sending also deletes attachments that were added before that mode began. #296 tracks that path.

Present since 7b6622687 (2026-06-24), which added chat media and this effect.

The chain is a code trace on 14ab7f9c1, and the request order comes from a scratch harness. It was not reproduced on a device.

Activity

  1. added
    bugSomething isn't working
    ChatRelated to chat features
    on Sep 14, 2026
  2. added a commit that references this issue on Sep 22, 2026
    875017d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ChatRelated to chat featuresbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions