Skip to content

Add draft message auto-save feature for conversations#433

Merged
torlando-tech merged 7 commits intomainfrom
claude/auto-save-message-drafts-bmzhe
Feb 15, 2026
Merged

Add draft message auto-save feature for conversations#433
torlando-tech merged 7 commits intomainfrom
claude/auto-save-message-drafts-bmzhe

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

Implements automatic draft message saving and restoration for conversations. Users' typed messages are now periodically saved to the database and restored when they re-open a conversation, with draft indicators shown in the conversation list.

Key Changes

Database Layer

  • Added DraftEntity and DraftDao for persistent draft storage
  • Created drafts table with foreign keys to conversations and local identities
  • Database migration from v35 to v36

Repository

  • Added draft operations to ConversationRepository:
    • saveDraft() - saves or deletes drafts based on content
    • getDraft() - retrieves saved draft for a conversation
    • clearDraft() - clears draft after message send
    • observeDrafts() - streams all drafts as a map for UI consumption

ViewModels

  • MessagingViewModel:

    • Tracks draft text state with _draftText StateFlow
    • Implements 500ms debounce on onDraftTextChanged() to avoid excessive database writes
    • Restores draft on conversation switch via LaunchedEffect
    • Clears draft after successful message send
    • Cancels pending draft saves on ViewModel cleanup
  • ChatsViewModel:

    • Observes all drafts via draftsMap StateFlow for conversation list display

UI

  • ChatsScreen: Displays "Draft: [preview]" in conversation list when draft exists

    • Draft text shown in italic with error color prefix
    • Replaces last message preview when draft is present
  • MessagingScreen:

    • Restores draft text when opening conversation
    • Tracks restoration state to prevent overwriting user input
    • Calls onDraftTextChanged() on every keystroke

Implementation Details

  • Debounce prevents excessive database writes while typing (500ms window)
  • Drafts are cleared immediately after successful message send
  • Unsaved text from the final <500ms window is acceptable per threading policy
  • Draft restoration uses LaunchedEffect to avoid overwriting user input on recomposition
  • Drafts are scoped per identity and conversation with proper foreign key constraints

https://claude.ai/code/session_01HqgRdtfTj6uJRRt2Y5NhhP

Implements periodic draft saving (500ms debounce) following Signal's
approach. Drafts persist to a dedicated Room table and are restored
when re-opening a conversation. The conversation list shows "Draft:"
prefix in italics for conversations with saved drafts.

- Add DraftEntity, DraftDao, and migration 35->36
- Add draft save/restore/clear methods to ConversationRepository
- Debounced auto-save in MessagingViewModel on text change
- Restore draft text when opening a conversation
- Clear draft on successful message send
- Show draft preview with "Draft:" prefix in ChatsScreen
- Cascading deletes clean up drafts when conversations are removed

Closes #238

https://claude.ai/code/session_01HqgRdtfTj6uJRRt2Y5NhhP
@torlando-tech torlando-tech linked an issue Feb 9, 2026 that may be closed by this pull request
@torlando-tech torlando-tech added this to the v0.9.0 milestone Feb 9, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 9, 2026

Greptile Summary

Implements auto-save draft functionality for message composition with proper race condition handling and database persistence.

Key Implementation Details:

  • Database migration (v35→v36) creates drafts table with composite PK (conversationHash, identityHash) and proper FK constraints
  • 500ms debounce on onDraftTextChanged prevents excessive database writes during typing
  • Race condition fixes (from commits 8a4e9ff and fce3f7a):
    • Re-checks active conversation after debounce delay before saving (line MessagingViewModel.kt:940)
    • Flushes pending draft immediately when switching conversations using lastDraftText tracking
    • Clears lastDraftText after message send to prevent re-saving sent text as draft
    • Keys draftRestored flag to destinationHash for proper restoration on conversation switch
  • Draft restoration uses LaunchedEffect keyed to both draftText and destinationHash to restore drafts when opening conversations
  • Conversation list displays "Draft:" prefix in italic error color when draft exists
  • Repository provides reactive observeDrafts() Flow that maps conversationHash → draft content

Previous review concerns addressed:

  • ✅ Draft restoration now works correctly when switching between conversations (draftRestored keyed to destinationHash)
  • ✅ Draft saves to correct conversation (re-checks _currentConversation.value after delay)

The implementation follows clean architecture with proper separation of concerns across database, repository, ViewModel, and UI layers.

Confidence Score: 5/5

  • This PR is safe to merge with no identified issues
  • All race conditions have been properly fixed with conversation re-checks and immediate flushing, database schema is well-designed with proper constraints, and the implementation follows best practices with debouncing and reactive patterns
  • No files require special attention

Important Files Changed

Filename Overview
data/src/main/java/com/lxmf/messenger/data/di/DatabaseModule.kt Migration 35→36 creates drafts table with proper FK constraints and index
data/src/main/java/com/lxmf/messenger/data/repository/ConversationRepository.kt Draft operations properly integrated with saveDraft/getDraft/clearDraft/observeDrafts methods
app/src/main/java/com/lxmf/messenger/viewmodel/MessagingViewModel.kt Draft logic with 500ms debounce, conversation re-check, flush on switch, and clear on send
app/src/main/java/com/lxmf/messenger/ui/screens/MessagingScreen.kt Draft restoration with draftRestored flag keyed to destinationHash, onDraftTextChanged on input

Flowchart

flowchart TD
    A[User Types in MessagingScreen] --> B[onDraftTextChanged called]
    B --> C[Save to lastDraftText]
    B --> D[Cancel previous draftSaveJob]
    B --> E[Start new debounced job 500ms]
    E --> F{Still same conversation?}
    F -->|Yes| G[saveDraft to repository]
    F -->|No| H[Discard save]
    G --> I[DraftDao.insertOrReplaceDraft]
    I --> J[Database UPDATE/INSERT]
    
    K[User Switches Conversation] --> L[Cancel draftSaveJob]
    L --> M[Flush lastDraftText immediately]
    M --> N[saveDraftNow for old conversation]
    N --> O[Load new conversation]
    O --> P[getDraft from repository]
    P --> Q[Update _draftText StateFlow]
    Q --> R[LaunchedEffect restores to messageText]
    
    S[User Sends Message] --> T[Clear draft on success]
    T --> U[Cancel draftSaveJob]
    T --> V[Clear lastDraftText]
    T --> W[clearDraft in repository]
    W --> X[DraftDao.deleteDraft]
    
    Y[ChatsViewModel] --> Z[observeDrafts Flow]
    Z --> AA[Map conversation hash to draft text]
    AA --> AB[ChatsScreen displays Draft: prefix]
Loading

Last reviewed commit: fce3f7a

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

- Key draftRestored to destinationHash so drafts restore correctly
  when navigating between conversations
- Re-check active conversation after debounce delay to prevent saving
  draft text to the wrong chat
- Flush pending draft immediately in loadMessages() before switching
  to a new conversation (uses saveDraftNow, fixes detekt unused warning)
- Track lastDraftText so the flush captures the most recent typed text

https://claude.ai/code/session_01HqgRdtfTj6uJRRt2Y5NhhP
@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Feb 10, 2026

After sending a message, lastDraftText was not being cleared. This meant
that if the user switched conversations after sending, the sent message
text could be re-saved as a draft for the previous conversation during
the flush in setConversation().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai review

torlando-tech and others added 4 commits February 15, 2026 14:55
Main added MIGRATION_35_36 for computedIdentityHash (COLUMBA-28 OOM fix).
Bumped draft table migration to MIGRATION_36_37 and database version to 37.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nstructors

Tests from main don't have the draftDao parameter that was added to
ConversationRepository in this branch. Add it to all three call sites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eenTest

Relaxed mockk returns Object for unmatched StateFlow properties, causing
ClassCastException when the UI collects draftsMap (Map) or draftText (String?).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Non-relaxed ConversationRepository mock throws MockKException when
ChatsViewModel calls observeDrafts() during initialization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
}

fun markAsRead(destinationHash: String) {
viewModelScope.launch {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: A race condition on conversation switch causes the previous conversation's draft to be displayed in the new conversation because draftRestored is reset prematurely.
Severity: CRITICAL

Suggested Fix

Synchronously clear the _draftText value when the conversation changes. Alternatively, update the draftRestored logic to be keyed on both the draft's content and the conversation's destinationHash to prevent stale data from being applied.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: app/src/main/java/com/lxmf/messenger/viewmodel/MessagingViewModel.kt#L912

Potential issue: When a user switches conversations, a race condition occurs. The
`draftRestored` flag is reset to `false` upon conversation change, keyed by
`destinationHash`. Before the new draft is loaded asynchronously, a `LaunchedEffect`
incorrectly applies the stale draft from the previous conversation, which is still
present in `viewModel.draftText`. It then sets `draftRestored` to `true`. When the
correct draft for the new conversation arrives, it is ignored because `draftRestored` is
already `true`, causing the wrong draft to be displayed and preventing the correct one
from appearing.

@torlando-tech torlando-tech merged commit e1ef28b into main Feb 15, 2026
9 of 10 checks passed
@torlando-tech torlando-tech deleted the claude/auto-save-message-drafts-bmzhe branch February 15, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-save typed messages as drafts

2 participants