Add draft message auto-save feature for conversations#433
Add draft message auto-save feature for conversations#433torlando-tech merged 7 commits intomainfrom
Conversation
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
Greptile SummaryImplements auto-save draft functionality for message composition with proper race condition handling and database persistence. Key Implementation Details:
Previous review concerns addressed:
The implementation follows clean architecture with proper separation of concerns across database, repository, ViewModel, and UI layers. Confidence Score: 5/5
Important Files Changed
Flowchartflowchart 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]
Last reviewed commit: fce3f7a |
app/src/main/java/com/lxmf/messenger/viewmodel/MessagingViewModel.kt
Outdated
Show resolved
Hide resolved
- 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
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
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>
|
@greptileai review |
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 { |
There was a problem hiding this comment.
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.
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
DraftEntityandDraftDaofor persistent draft storagedraftstable with foreign keys to conversations and local identitiesRepository
ConversationRepository:saveDraft()- saves or deletes drafts based on contentgetDraft()- retrieves saved draft for a conversationclearDraft()- clears draft after message sendobserveDrafts()- streams all drafts as a map for UI consumptionViewModels
MessagingViewModel:_draftTextStateFlowonDraftTextChanged()to avoid excessive database writesLaunchedEffectChatsViewModel:draftsMapStateFlow for conversation list displayUI
ChatsScreen: Displays "Draft: [preview]" in conversation list when draft existsMessagingScreen:onDraftTextChanged()on every keystrokeImplementation Details
LaunchedEffectto avoid overwriting user input on recompositionhttps://claude.ai/code/session_01HqgRdtfTj6uJRRt2Y5NhhP