Skip to content

Keep the unsent draft when reply, comment, or edit mode changes, or the chat reopens #296

Description

@HMarzban

Problem

Mode switches and a chat reopen lose work that belongs to the unsent draft. The composer shares one editor and one attachment list across send, reply, comment, and edit. 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/components/MessageComposer/.

Starting or leaving reply or comment mode deletes earlier attachments. When reply or comment memory goes from set to unset, the lifecycle effect calls clearAttachments({ deleteStorage: true }) (hooks/useComposerAttachmentLifecycle.ts:79-86). That deletes the upload of every ready attachment that is not persisted (apps/webapp/src/components/chatroom/stores/composerAttachmentsStore.ts:97-103). It includes attachments the user added before the mode began.

The first reply or comment in a channel after page load deletes them sooner, when the mode starts. setMemory writes null to all three memory slots (apps/webapp/src/stores/chat/workspaceSettingsStore.ts:93-103), and before that editMessageMemory is undefined. That change re-runs the edit-end effect (hooks/useComposerAttachmentLifecycle.ts:74-77). It calls cancelEditAttachments(), which deletes the same uploads, stops attachments that are still uploading, and empties the list (hooks/useComposerAttachments.ts:88-97). So that reply or comment is sent without them.

When the mode ends, the attachment hydrate can restore a tile from the saved draft after its upload is deleted (hooks/useComposerAttachmentDraft.ts:62-82).

Reopening a heading chat deletes the draft's uploads. The edit-end effect also runs on every composer mount, because edit memory is not set then (hooks/useComposerAttachmentLifecycle.ts:74-77). Closing a chat keeps its attachment list in the store. The mount disposes only the lists of other channels (hooks/useComposerAttachments.ts:99-110). So when the same heading chat opens again in the tab, cancelEditAttachments() deletes the uploads in that list (:94-97). The saved draft still lists them, so the tile can come back with no file behind it. What Send then does is described in #267.

Comment mode writes the comment into the saved draft. The draft hydrate skips comment mode, but the draft still counts as hydrated (hooks/useComposerDraft.ts:36-38). The text writer has no mode check (hooks/useTiptapEditor.ts:190-197). So the comment text enters the saved draft. Escape clears comment memory and calls clearContent(true) (hooks/useHandleEscKey.ts:51-54). Then the hydrate runs again and loads the saved draft back (hooks/useComposerDraft.ts:31-76). So the comment text returns as a normal message. When a comment opens a composer that was not mounted, the editor starts empty. The saved draft is not shown, and typing the comment replaces it.

Starting an edit stops attachments that are still uploading. Every edit calls loadExistingAttachments with the message media, even when the message has none (hooks/useComposerAttachments.ts:140-155). That calls the upload runner's reset(), which stops every attachment that is still uploading and clears the queue (apps/webapp/src/components/chatroom/utils/chatMediaUploadRunner.ts:91-102). An attachment that the unsent draft was still uploading is lost.

Steps to reproduce

  1. Reload a channel. Attach an image and wait until it finishes uploading. Choose Reply on any message.
  2. See the image tile gone at once. Check the media bucket: its upload is deleted.
  3. Choose Dismiss reply. Attach a second image and wait until it finishes uploading. Choose Reply again, then choose Dismiss reply. See the new tile go. It may come back from the saved draft, but its upload is deleted in both cases.
  4. Type draft text and wait one second. Choose Close chatroom. Select text under the same heading and choose Comment in the editor toolbar. See the composer open empty.
  5. Type my comment, wait one second, and press Escape. See my comment in the composer as a normal message. Reload. draft text is gone.
  6. Reload the channel. Attach an image and wait until it finishes uploading. Choose Close chatroom, then open the same heading chat again. Check the media bucket: the upload is deleted, though the tile may still show.

Acceptance criteria

  • Starting a reply or a comment keeps the unsent draft's attachments already in the composer, with their uploads. A reply or comment sent next carries them.
  • Cancelling reply or comment mode keeps the earlier attachments that are still in the composer, with their uploads.
  • Closing and reopening a heading chat in the same tab keeps the unsent draft's attachments, with their uploads.
  • Cancelling a reply or a comment deletes the uploads of attachments added during that mode.
  • Text typed in comment mode never enters the saved draft.
  • After Escape or the Dismiss comment button, the composer shows the saved draft from before comment mode began, and the saved draft still holds it.
  • Starting an edit does not stop an attachment of the unsent draft that is still uploading. When the edit ends, that attachment is back in the composer, with the unsent draft that Keep the unsent draft when the user edits a message #277 restores.

Agent Brief

Category: bug
Summary: A mode switch or a chat reopen must never delete, overwrite, or stop work that belongs to the unsent draft.

Current behavior:
Leaving reply or comment mode deletes the uploads of every unsent attachment, including earlier ones. The first reply or comment in a channel after page load deletes them when the mode starts. Reopening a heading chat in the same tab deletes the uploads of its unsent attachments. Text typed in comment mode enters the saved draft, so it returns as a normal message after Escape. A comment that opens an unmounted composer replaces the saved draft. Starting an edit stops attachments that are still uploading.

Desired behavior:
The unsent draft, text and attachments, survives every switch into and out of reply, comment, and edit mode, and a reopen of the chat. Only work that belongs to the mode is discarded when the mode is cancelled.

Key interfaces:

  • useComposerAttachmentLifecycle() — the set-to-unset effect on reply and comment memory, and the edit-end effect.
  • cancelEditAttachments() — the edit-end effect calls it on every composer mount, and when edit memory changes from undefined to null, not only when an edit ends.
  • clearAttachments({ deleteStorage }) and deleteNonPersistedAttachmentStorage().
  • useHandleEscKey() — the window Escape handler, which clears comment memory and the editor.
  • useComposerDraft() — its hydrate skips comment mode, then runs again when the mode ends.
  • The text writer in the composer onUpdate handler of useTiptapEditor(). It has no mode check.
  • loadExistingAttachments() and the upload runner reset. Removing the reset alone is not enough. ChatMediaUploadRunner writes each result by attachment id into the current list. During an edit that list holds only the message media, so the result is dropped.
  • ComposerAttachment — no field says whether an attachment was added before or during a mode. persisted marks only row-backed media.
  • The IndexedDB draft store. It keeps the earlier attachments, because the attachment writer skips reply and comment mode. The text writer does not skip them.

Out of scope

Notes

The fix for #277 sets the unsent draft aside during an edit. Reply and comment mode differ. The earlier text and attachments stay in the composer, and a reply or comment send carries them. So do not set them aside there. Record what the composer held when the mode began, and keep that on a cancel. Do not keep draft text in the chat store mode memory or in the composer context (apps/webapp/src/components/chatroom/CLAUDE.md:69, :73). Plan the two fixes together.

Skipping the mount call to cancelEditAttachments() leaves two things in the list after a close and reopen. One is a tile whose upload stopped when the composer unmounted. It stays uploading and blocks Send. The other is the media of an edit that the desktop Close chatroom button ended. Do not leave that tile stuck, or offer that media in a new message.

The evidence is a code trace on 14ab7f9c1, plus review harnesses that ran the real composer hooks in happy-dom. One harness closed and reopened the same heading chat. Each reopen sent one delete for the upload, and a control after a reload sent none. 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