Skip to content

Keep a message's media when Edit starts from reply or comment mode #295

Description

@HMarzban

Problem

A user chooses Edit on a message with media while reply or comment mode is open. The composer attachment list then stays empty. Saving that edit removes the media from the message. Here an attachment is the composer entry, and its upload is the file in the media bucket.

On 14ab7f9c1. Paths are under apps/webapp/src/components/chatroom/ unless shown in full.

  1. Edit calls setEditMessageMemory (components/MessageCard/hooks/useEditMessageHandler.ts:12). Nothing gates Edit on the composer mode (components/MessageCard/hooks/useMessageActionMenuItems.tsx:138-144).
  2. setMemory clears reply, edit, and comment memory, then sets edit memory, in one store write (apps/webapp/src/stores/chat/workspaceSettingsStore.ts:56-58, :93-102). So reply memory and edit memory change in the same render.
  3. useComposerAttachmentLifecycle runs its effects in declaration order after that render. First the edit-load effect loads the message's media into the attachment list as persisted entries (components/MessageComposer/hooks/useComposerAttachmentLifecycle.ts:69-72, components/MessageComposer/hooks/useComposerAttachments.ts:140-155).
  4. Then the set-to-unset effect sees reply or comment mode go from set to unset. It calls clearAttachments({ deleteStorage: true }), which empties the attachment list (useComposerAttachmentLifecycle.ts:79-86, useComposerAttachments.ts:77-86). Nothing loads the media again, because edit memory does not change after that.
  5. The edit bar still shows the media thumb, because it reads edit memory, not the attachment list (components/MessageComposer/components/Context/EditContext.tsx:29-50). The attachment strip shows no tiles.
  6. On save, editHadPersistedMedias reads edit memory and returns true, but getReadyAttachments() returns nothing. So resolveEditMediasPatch returns clear (utils/composerSendGate.ts:4-6, utils/outboundMessagePipeline.ts:57-67). buildEditChunkPayload sends medias: null with type: 'text' (utils/outboundMessagePipeline.ts:156-157). updateMessage writes both to the message row (apps/webapp/src/api/messages/updateMessage.ts:23-28).

What the user sees depends on the message:

  • A message with a caption loses its media. The caption stays, and the message shows as edited. Every reader sees the change.
  • A media-only message with an empty editor cannot be saved. The send button stays enabled, because the send gate counts the media in edit memory (utils/composerSendGate.ts:24-27). Each try shows "Invalid content detected" (utils/outboundMessagePipeline.ts:82-84). The user can only cancel the edit.
  • A media-only message has no text to load, so the editor keeps any reply text (components/MessageComposer/hooks/useComposerDraft.ts:78-84). The save then writes the reply text as the message content and removes the media.

The mode switch does not delete the message's uploads. The entries are persisted, and deleteNonPersistedAttachmentStorage skips them (stores/composerAttachmentsStore.ts:97-103). But after the save, no live message lists those uploads. internal.cleanup_orphan_chat_media deletes such uploads once they are older than 24 hours (packages/supabase/scripts/10-3-func-message.sql:394-430). A pg_cron job runs it daily at 03:30 (packages/supabase/scripts/16-cron-jobs.sql:28-32). After that run, the media cannot come back.

The defect needs reply or comment mode to be open at the moment Edit is chosen. Edit from an idle composer loads the media. After Cancel edit, a second Edit also loads it.

Steps to reproduce

  1. Open a heading chat. Send a message with one image and a caption.
  2. Choose Reply on any message. Do not send.
  3. Open the menu on your image message and choose Edit.
  4. See the edit bar show the media thumb, but no attachment tile between the edit bar and the editor.
  5. Change the caption and save.
  6. See the message without its image. Reload, or open the chat in a second browser. The image is still gone.

Comment mode gives the same result. Start a comment on pad text under the heading from step 1. Its chat composer opens in comment mode. Then do steps 3 to 6.

Acceptance criteria

  • With reply mode open, Edit on a message with media shows one attachment tile per media item, as Edit from an idle composer does.
  • With comment mode open, Edit on a message with media shows the same tiles.
  • Saving that edit without touching the tiles keeps the message's media.
  • After that save, the message type is the same as after the same edit from an idle composer.
  • A media-only message edited from reply mode, with an empty editor, saves and keeps its media.
  • Removing a tile in that edit and saving still removes that media item from the message.
  • Cancelling reply or comment mode without starting an edit still removes the attachments added during that mode. The attachments added before that mode began belong to Keep the unsent draft when reply, comment, or edit mode changes, or the chat reopens #296.
  • Edit from an idle composer, and Edit on a second message while an edit is open, still load the media of the message being edited.

Agent Brief

Category: bug
Summary: Edit mode entered from reply or comment mode must load the edited message's media, and the save must keep it.

Current behavior:
Setting edit memory clears reply and comment memory in the same store write. After the next render, the lifecycle effects run in order. The edit-load effect loads the message's media into the attachment list. The set-to-unset effect then reads the mode change as a cancel and empties the attachment list. Edit memory still lists the media, but the attachment list is empty. On save, editHadPersistedMedias is true and getReadyAttachments() is empty, so resolveEditMediasPatch picks clear. updateMessage then writes medias: null and type: 'text'.

Desired behavior:
Edit mode always starts with the edited message's media in the attachment list, whatever mode was open before. Leaving reply or comment mode never empties the attachment list after the edit load. An edit save sends clear only when the user removed every attachment tile.

Key interfaces:

  • setMemory in the workspace settings store — clears reply, edit, and comment memory, then sets one of them.
  • useComposerAttachmentLifecycle() — the edit-load effect, and the set-to-unset effect on reply and comment memory.
  • loadExistingAttachments() and clearAttachments({ deleteStorage }) in useComposerAttachments().
  • ComposerAttachment.persisted — marks row-backed media, whose uploads a clear never deletes.
  • editHadPersistedMedias(), resolveEditMediasPatch(), and buildEditChunkPayload() — choose omit, set, or clear for an edit save.
  • updateMessage() — writes medias and type to the message row.

Out of scope

Notes

Coordinate with #266 and #296. Both change the same set-to-unset effect. Coordinate with #277. It changes what Edit does to the draft and to the attachment list. Coordinate with #291. It changes the send gate and the clear patch that Problem step 6 uses. #263 builds the send hand-off on the early clear. That clear fires the set-to-unset effect only on a reply or comment send, and #263 does not animate those.

The attachments spec already has "loads existing attachments when editing a media message" (apps/webapp/cypress/e2e/chatroom/attachments.cy.ts:706-723). A variant that opens a reply first would pin this regression. The webapp Cypress specs do not run in CI yet. See #276.

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

The claim came in as "likely". The evidence is a code trace on 14ab7f9c1 and a scratch harness, which confirm it. The harness mounted the real composer hooks in happy-dom with React 19.2.8, in the call order of MessageComposer. It then ran the real prepareOutboundContent and buildOutboundChunkPayload. It covered Edit from idle, reply then Edit, comment then Edit, and Cancel then Edit again. It also covered a media-only message, with and without reply text. It was not reproduced in a browser or on a device.

Activity

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