Skip to content

Stop the failing send for an edit with no text and no attachment left #291

Description

@HMarzban

Problem

Edit a message that has attachments and no text, then remove every attachment. The send control stays on screen. Pressing it always fails with the toast "Invalid content detected", and the message does not change.

On 14ab7f9c1. Paths are under apps/webapp/src/components/chatroom/.

  1. The send gate reads the saved message, not the attachment strip. editHadPersistedMedias is true while editMessageMemory.medias is not empty (utils/composerSendGate.ts:4-6). So composerSendGate stays true after every attachment is removed (utils/composerSendGate.ts:24-27). canSend uses this gate (components/MessageComposer/MessageComposer.tsx:229-235). The click-time re-check isSubmittable uses it too (components/MessageComposer/hooks/useComposerSubmit.ts:68-76). With canSend true, ComposerPrimaryAction shows the send control (components/MessageComposer/components/Actions/ActionButtons/ComposerPrimaryAction.tsx:62).
  2. The editor is empty. Edit loads no text when the message has none (components/MessageComposer/hooks/useComposerDraft.ts:80-81). The sanitizer also drops an empty <p></p> (apps/webapp/src/utils/sanitizeContent.ts:22, :27-28).
  3. prepareOutboundContent refuses the submit. With no ready attachment and no text, it returns Invalid content detected (utils/outboundMessagePipeline.ts:76, :82-84). The submit shows that toast and returns (components/MessageComposer/hooks/useComposerSubmit.ts:138-141).

So the clear patch of resolveEditMediasPatch (utils/outboundMessagePipeline.ts:65) is reached only when the edit keeps some text. Then it saves the text, removes the attachments, and deletes their uploads (:156-157, :262).

The gate's editHadPersistedMedias clause matters only when no text and no ready attachment remain. Saved attachments load into the attachment strip as ready (components/MessageComposer/hooks/useComposerAttachments.ts:140-155). Each ready attachment counts toward readyAttachmentCount (:191-195). So the clause only turns on a send that prepareOutboundContent always refuses.

No data is lost. The Remove control in the attachment strip (components/MessageComposer/components/Attachments/AttachmentStrip.tsx:145, :191) only queues the upload of a saved attachment for deletion (utils/chatMediaUploadRunner.ts:124-126). Ending the edit without a save empties that queue, so no upload is deleted (components/MessageComposer/hooks/useComposerAttachmentLifecycle.ts:74-77, components/MessageComposer/hooks/useComposerAttachments.ts:88-97). But the toast does not say what is wrong or what to do. Today, Delete is the only way to remove every attachment from a message with no text, unless the edit adds text (components/MessageCard/hooks/useMessageActionMenuItems.tsx:145-155).

The chatroom rules list "edit-with-media" as a canSend input (apps/webapp/src/components/chatroom/CLAUDE.md:73).

Steps to reproduce

  1. Open a channel. Start with an empty composer.
  2. Send one image with no text.
  3. Choose Edit on that message.
  4. In the attachment strip, press Remove on the image. The send control stays on screen.
  5. Press send. See the toast "Invalid content detected". The message keeps its image. The composer stays in edit mode, with an empty attachment strip.

Acceptance criteria

  • With an empty editor after an edit removes every attachment from a message with no text, send and Enter never show "Invalid content detected".
  • In that state, the composer either shows no send control, or its send opens the existing delete confirmation for the message. Say which on this issue before the change merges.
  • An edit that ends with an empty editor and an empty attachment strip never saves the message. The empty row that a long edit can save belongs to Keep a long chat edit whole, and never send an empty chunk #264.
  • An edit that keeps text and removes every attachment still saves the text. The attachments leave the message, and their uploads are deleted from storage.
  • An edit of a message with no text still saves when the attachment strip holds a ready attachment, saved or newly uploaded. This holds while no upload is running or failed.
  • Ending an edit without a save or a confirmed delete, after removing attachments, leaves the message and its uploads unchanged.
  • The chatroom rule that lists the canSend inputs matches the result.

Agent Brief

Category: bug
Summary: The send gate and prepareOutboundContent must agree when an edit removes every attachment from a message with no text.

Current behavior:
The send gate stays true for any edit of a message that had attachments, even after the user removes them all. With no text and no attachment, prepareOutboundContent refuses the submit. The user sees "Invalid content detected", and the message does not change.

Desired behavior:
An edit with no text and no attachment never offers a send that must fail. Either the composer shows no send control, or send opens the existing delete confirmation for the message. An edit never saves a message with no text and no attachment. Edits that keep text or keep an attachment work as today.

Key interfaces:

  • composerSendGate() and editHadPersistedMedias() — the send gate and its edit clause.
  • canSend in MessageComposerContext, and isSubmittable in useComposerSubmit() — both call the send gate.
  • prepareOutboundContent() — refuses a submit with no text and no attachment.
  • resolveEditMediasPatch() and the clear kind of EditMediasPatch — remove every attachment on an edit that keeps text.
  • ComposerPrimaryAction — shows the send control when canSend is true, and the voice note control when it is false.
  • DeleteMessageConfirmationDialog — the existing delete confirmation.

Out of scope

Notes

Coordinate with #264. It adds an edit guard to prepareOutboundContent, near the check that refuses this submit.

Coordinate with #277. It changes how an edit loads and releases its text and its attachments. If the user types text before Edit, that text stays in the editor (components/MessageComposer/hooks/useComposerDraft.ts:36-38, :80-81). The same steps then save that text as the message text. That defect belongs to #277.

Coordinate with #271. It edits the same rule line, apps/webapp/src/components/chatroom/CLAUDE.md:73.

Coordinate with #295. Until it lands, an Edit that starts from reply or comment mode empties the attachment strip with no user action. If the editor is also empty, this fix would then hide the send control or open the delete confirmation, though the user removed nothing.

If the fix shows no send control, ComposerPrimaryAction shows the voice note control in its place (components/MessageComposer/components/Actions/ActionButtons/ComposerPrimaryAction.tsx:86-102). An edit whose text is deleted shows the same control today.

apps/webapp/cypress/e2e/chatroom/attachments.cy.ts:706-723 already opens Edit on a message with an image. It can hold a regression case, if one is wanted.

The evidence is a code trace on 14ab7f9c1, confirmed by two verifiers and checked again for this draft. It was not reproduced in a browser.

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