From fa12576a8c4eb0ed1574e9f538a40feec9e70979 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 12:29:37 +0000 Subject: [PATCH 1/7] fix: respect "Automatically create drafts" setting in comment composer CommentPostViewModel.sendDraftSync() unconditionally signed and published a draft event whenever the user typed in a comment, ignoring the "Automatically create drafts" setting. Every other composer ViewModel guards this path with accountViewModel.settings.automaticallyCreateDrafts(); add the same check here so disabling the setting stops generic draft events from being signed. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i --- .../amethyst/ui/note/nip22Comments/CommentPostViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 7df304cae5..bb8554386e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -507,7 +507,7 @@ open class CommentPostViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) - } else { + } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { attachments.add(it.first) From df5361d2200b82526a9951fb4a4bf91639305401 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 13:38:20 +0000 Subject: [PATCH 2/7] fix: don't sign a draft deletion when no draft exists deleteDraftInner() unconditionally signed a deleted-draft wrap event plus a deletion event, even when no draft for that tag existed. With an external NIP-55/NIP-46 signer, clearing a composer to blank could then prompt for a signature even when "Automatically create drafts" is off, since the blank-text branch of sendDraftSync() always calls delete. Skip both signatures when the draft does not exist in the cache. The existence lookup reuses the addressable note already fetched for relay hints, so it adds no extra work, and real drafts are still deleted. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i --- .../main/java/com/vitorpamplona/amethyst/model/Account.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 0c14a14089..8d596c1c42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2092,7 +2092,12 @@ class Account( suspend fun deleteDraftInner(draftTag: String) { if (!isWriteable()) return - val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList() + // Nothing to delete means nothing to sign. Avoids prompting the signer to delete a + // draft that was never created (e.g. when "Automatically create drafts" is off). + val existingDraft = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag)) + if (existingDraft?.event == null) return + + val extraRelays = existingDraft.relays val deletedDraft = DraftWrapEvent.createDeletedEvent(draftTag, signer) val deletionEvent = signer.sign(DeletionEvent.build(listOf(deletedDraft))) From 50dbbeeecc3810ba0007885fee9061d369166c6c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 14:59:44 +0000 Subject: [PATCH 3/7] fix: hold a strong reference to the draft note for reliable deletion LocalCache.addressables keeps AddressableNotes via WeakReference, so a saved draft could be garbage-collected between creation and deletion. The previous existence check (look the draft up by tag before signing) would then find nothing locally and skip the deletion, leaving an orphan draft on the relays. Each composer ViewModel now holds a strong reference to its draft note: createAndSendDraftIgnoreErrors returns the consumed AddressableNote, and load()/editFromDraft captures the note when editing an existing draft. deleteDraftInner takes that held note directly (sourcing the dTag and relays from it) instead of a tag lookup, so it can always reach the draft it needs to delete and still signs nothing when there is no draft. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i --- .../vitorpamplona/amethyst/model/Account.kt | 33 ++++++++++++------- .../nip22Comments/CommentPostViewModel.kt | 15 ++++++--- .../privateDM/send/ChatNewMessageViewModel.kt | 15 ++++++--- .../send/ChannelNewMessageViewModel.kt | 15 ++++++--- .../nip23LongForm/LongFormPostViewModel.kt | 15 ++++++--- .../nip99Classifieds/NewProductViewModel.kt | 15 ++++++--- .../loggedIn/home/ShortNotePostViewModel.kt | 19 +++++++---- .../room/chat/NestNewMessageViewModel.kt | 14 +++++--- .../NewPublicMessageViewModel.kt | 15 ++++++--- 9 files changed, 110 insertions(+), 46 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 8d596c1c42..c938742658 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2045,24 +2045,30 @@ class Account( extraNotesToBroadcast.forEach { client.publish(it, relays) } } + /** + * Returns the local [AddressableNote] that now holds the draft so the caller (the + * composer ViewModel) can keep a strong reference to it. [LocalCache.addressables] only + * keeps weak references, so without an owner holding the note it can be garbage collected + * and a later deletion would not find it locally (leaving an orphan on the relays). + */ suspend fun createAndSendDraftIgnoreErrors( draftTag: String, template: EventTemplate, broadcast: Set = emptySet(), - ) { + ): AddressableNote? = try { createAndSendDraftInner(draftTag, template, broadcast) } catch (e: Exception) { if (e is CancellationException) throw e + null } - } suspend fun createAndSendDraftInner( draftTag: String, template: EventTemplate, broadcast: Set = emptySet(), - ) { - if (!isWriteable()) return + ): AddressableNote? { + if (!isWriteable()) return null val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList() @@ -2079,25 +2085,28 @@ class Account( client.publish(it, relayList.toSet()) } } + + return cache.getOrCreateAddressableNote(draftEvent.address()) } - suspend fun deleteDraftIgnoreErrors(draftTag: String) { + suspend fun deleteDraftIgnoreErrors(draftNote: AddressableNote?) { try { - deleteDraftInner(draftTag) + deleteDraftInner(draftNote) } catch (e: Exception) { if (e is CancellationException) throw e } } - suspend fun deleteDraftInner(draftTag: String) { + suspend fun deleteDraftInner(draftNote: AddressableNote?) { if (!isWriteable()) return - // Nothing to delete means nothing to sign. Avoids prompting the signer to delete a - // draft that was never created (e.g. when "Automatically create drafts" is off). - val existingDraft = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag)) - if (existingDraft?.event == null) return + // Nothing to delete means nothing to sign. The caller passes the note it has been + // holding (so it can't be garbage collected before we get here); a null note or one + // without an event means no draft was ever created, so we avoid prompting the signer. + if (draftNote?.event == null) return - val extraRelays = existingDraft.relays + val draftTag = draftNote.dTag() + val extraRelays = draftNote.relays val deletedDraft = DraftWrapEvent.createDeletedEvent(draftTag, signer) val deletionEvent = signer.sign(DeletionEvent.build(listOf(deletedDraft))) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index bb8554386e..f19c84c134 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -136,6 +137,10 @@ open class CommentPostViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the saved draft so LocalCache's weak reference can't collect it + // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). + var draftNote: AddressableNote? = null + init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { @@ -256,6 +261,7 @@ open class CommentPostViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { + draftNote = draft as? AddressableNote viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -489,7 +495,7 @@ open class CommentPostViewModel : } } - val version = draftTag.current + val draftToDelete = draftNote val anonymous = wantsAnonymousPost cancel() @@ -500,13 +506,13 @@ open class CommentPostViewModel : } accountViewModel.viewModelScope.launch(Dispatchers.IO) { - accountViewModel.account.deleteDraftIgnoreErrors(version) + accountViewModel.account.deleteDraftIgnoreErrors(draftToDelete) } } suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { @@ -515,7 +521,7 @@ open class CommentPostViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) + draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } @@ -737,6 +743,7 @@ open class CommentPostViewModel : open fun cancel() { draftTag.rotate() + draftNote = null message.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 8bbc1f2fe6..1c7a459fad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -126,6 +127,10 @@ class ChatNewMessageViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the saved draft so LocalCache's weak reference can't collect it + // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). + var draftNote: AddressableNote? = null + lateinit var accountViewModel: AccountViewModel lateinit var account: Account @@ -322,6 +327,7 @@ class ChatNewMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { + draftNote = draft as? AddressableNote viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -418,17 +424,17 @@ class ChatNewMessageViewModel : } suspend fun sendPostSync() { - val version = draftTag.current + val draftToDelete = draftNote innerSendPost(null) cancel() accountViewModel.viewModelScope.launch(Dispatchers.IO) { - accountViewModel.account.deleteDraftIgnoreErrors(version) + accountViewModel.account.deleteDraftIgnoreErrors(draftToDelete) } } suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - account.deleteDraftIgnoreErrors(draftTag.current) + account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { innerSendPost(draftTag.current) } @@ -617,7 +623,7 @@ class ChatNewMessageViewModel : } if (draftTag != null) { - accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template) + draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template) } else { accountViewModel.account.sendNip17PrivateMessage(template) } @@ -639,6 +645,7 @@ class ChatNewMessageViewModel : fun cancel() { draftTag.rotate() + draftNote = null message.setTextAndPlaceCursorAtEnd("") subject.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 510b42a501..f237d9b9df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -112,6 +113,10 @@ open class ChannelNewMessageViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the saved draft so LocalCache's weak reference can't collect it + // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). + var draftNote: AddressableNote? = null + init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { @@ -224,6 +229,7 @@ open class ChannelNewMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { + draftNote = draft as? AddressableNote viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -301,20 +307,20 @@ open class ChannelNewMessageViewModel : val template = createTemplate() ?: return val channelRelays = channel?.relays() ?: emptySet() - val version = draftTag.current + val draftToDelete = draftNote cancel() accountViewModel.account.signAndSendPrivatelyOrBroadcast(template) { channelRelays.toList() } accountViewModel.viewModelScope.launch(Dispatchers.IO) { - accountViewModel.account.deleteDraftIgnoreErrors(version) + accountViewModel.account.deleteDraftIgnoreErrors(draftToDelete) } } suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - account.deleteDraftIgnoreErrors(draftTag.current) + account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { @@ -323,7 +329,7 @@ open class ChannelNewMessageViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) + draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } @@ -553,6 +559,7 @@ open class ChannelNewMessageViewModel : open fun cancel() { draftTag.rotate() + draftNote = null message.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index b986a8be8e..8bcfe39f13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -128,6 +129,10 @@ class LongFormPostViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the saved draft so LocalCache's weak reference can't collect it + // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). + var draftNote: AddressableNote? = null + lateinit var accountViewModel: AccountViewModel lateinit var account: Account @@ -232,6 +237,7 @@ class LongFormPostViewModel : val noteAuthor = draft?.author if (draft != null && noteEvent is DraftWrapEvent && noteAuthor != null) { + draftNote = draft as? AddressableNote viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -324,7 +330,7 @@ class LongFormPostViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val version = draftTag.current + val draftToDelete = draftNote cancel() if (accountViewModel.settings.useTrackedBroadcasts()) { @@ -342,16 +348,16 @@ class LongFormPostViewModel : } accountViewModel.launchSigner { - accountViewModel.account.deleteDraftIgnoreErrors(version) + accountViewModel.account.deleteDraftIgnoreErrors(draftToDelete) } } suspend fun sendDraftSync() { if (message.text.toString().isBlank() && title.text.isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, emptySet()) + draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, emptySet()) } } @@ -577,6 +583,7 @@ class LongFormPostViewModel : fun cancel() { draftTag.rotate() + draftNote = null title = TextFieldValue("") summary = TextFieldValue("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 5574c773b8..f5f47d5e10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -114,6 +115,10 @@ open class NewProductViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the saved draft so LocalCache's weak reference can't collect it + // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). + var draftNote: AddressableNote? = null + lateinit var accountViewModel: AccountViewModel lateinit var account: Account @@ -213,6 +218,7 @@ open class NewProductViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { + draftNote = draft as? AddressableNote viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -315,21 +321,21 @@ open class NewProductViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val version = draftTag.current + val draftToDelete = draftNote cancel() accountViewModel.account.signAndSendPrivatelyOrBroadcast(template, relayList = { relayList }) accountViewModel.viewModelScope.launch(Dispatchers.IO) { - accountViewModel.account.deleteDraftIgnoreErrors(version) + accountViewModel.account.deleteDraftIgnoreErrors(draftToDelete) } } suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template) + draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template) } } @@ -468,6 +474,7 @@ open class NewProductViewModel : open fun cancel() { draftTag.rotate() + draftNote = null message.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 96590b55e1..cf3564503e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -183,6 +184,10 @@ open class ShortNotePostViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the saved draft so LocalCache's weak reference can't collect it + // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). + var draftNote: AddressableNote? = null + lateinit var accountViewModel: AccountViewModel lateinit var account: Account @@ -478,6 +483,7 @@ open class ShortNotePostViewModel : val noteAuthor = draft?.author if (draft != null && noteEvent is DraftWrapEvent && noteAuthor != null) { + draftNote = draft as? AddressableNote viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -889,7 +895,7 @@ open class ShortNotePostViewModel : } } - val version = draftTag.current + val draftToDelete = draftNote val anonymous = wantsAnonymousPost val scheduledFor = scheduledForSec val privately = wantsPrivateNote @@ -903,7 +909,7 @@ open class ShortNotePostViewModel : @Suppress("UNCHECKED_CAST") accountViewModel.account.sendPrivateNote(template as EventTemplate) accountViewModel.launchSigner { - accountViewModel.account.deleteDraftIgnoreErrors(version) + accountViewModel.account.deleteDraftIgnoreErrors(draftToDelete) } return } @@ -935,7 +941,7 @@ open class ShortNotePostViewModel : ), ) accountViewModel.launchSigner { - accountViewModel.account.deleteDraftIgnoreErrors(version) + accountViewModel.account.deleteDraftIgnoreErrors(draftToDelete) } return } @@ -961,13 +967,13 @@ open class ShortNotePostViewModel : } accountViewModel.launchSigner { - accountViewModel.account.deleteDraftIgnoreErrors(version) + accountViewModel.account.deleteDraftIgnoreErrors(draftToDelete) } } suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { @@ -976,7 +982,7 @@ open class ShortNotePostViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) + draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } @@ -1312,6 +1318,7 @@ open class ShortNotePostViewModel : open fun cancel() { draftTag.rotate() + draftNote = null message.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index a9e7993a97..3132759d23 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -118,6 +118,10 @@ open class NestNewMessageViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the saved draft so LocalCache's weak reference can't collect it + // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). + var draftNote: AddressableNote? = null + init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { @@ -230,6 +234,7 @@ open class NestNewMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { + draftNote = draft as? AddressableNote viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -303,7 +308,7 @@ open class NestNewMessageViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val version = draftTag.current + val draftToDelete = draftNote cancel() // Broadcast to the user's default relays — the nest has no @@ -312,13 +317,13 @@ open class NestNewMessageViewModel : // `account.signAndComputeBroadcast(...)`. accountViewModel.account.signAndComputeBroadcast(template) accountViewModel.viewModelScope.launch(Dispatchers.IO) { - accountViewModel.account.deleteDraftIgnoreErrors(version) + accountViewModel.account.deleteDraftIgnoreErrors(draftToDelete) } } suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - account.deleteDraftIgnoreErrors(draftTag.current) + account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { @@ -327,7 +332,7 @@ open class NestNewMessageViewModel : } val template = createTemplate() ?: return - accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) + draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } @@ -472,6 +477,7 @@ open class NestNewMessageViewModel : open fun cancel() { draftTag.rotate() + draftNote = null message.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index dac12b57dc..a94e84c94a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -129,6 +130,10 @@ class NewPublicMessageViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the saved draft so LocalCache's weak reference can't collect it + // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). + var draftNote: AddressableNote? = null + lateinit var accountViewModel: AccountViewModel lateinit var account: Account @@ -257,6 +262,7 @@ class NewPublicMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { + draftNote = draft as? AddressableNote viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -340,18 +346,18 @@ class NewPublicMessageViewModel : } } - val version = draftTag.current + val draftToDelete = draftNote cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) accountViewModel.viewModelScope.launch(Dispatchers.IO) { - accountViewModel.account.deleteDraftIgnoreErrors(version) + accountViewModel.account.deleteDraftIgnoreErrors(draftToDelete) } } suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) + accountViewModel.account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val broadcast = mutableSetOf() nip95attachments.forEach { @@ -360,7 +366,7 @@ class NewPublicMessageViewModel : } val template = createTemplate() - accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast) + draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast) } } @@ -519,6 +525,7 @@ class NewPublicMessageViewModel : fun cancel() { draftTag.rotate() + draftNote = null toUsers.setTextAndPlaceCursorAtEnd("") message.setTextAndPlaceCursorAtEnd("") From a242411df6fafc90cb4f6bda27925dcac452f1d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 15:15:42 +0000 Subject: [PATCH 4/7] refactor: own the draft note reference inside DraftTagState MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The strong reference that keeps a saved draft alive (so LocalCache's weak reference can't collect it before deletion) was duplicated as a draftNote field across all eight composer ViewModels, each re-clearing it in cancel(). The note's lifecycle is 1:1 with the draft tag, so DraftTagState is its natural owner: it now holds the AddressableNote, exposes held() to set it, and drops it in rotate() — which every cancel() already calls. ViewModels now reference draftTag.note / draftTag.held(...) and no longer carry their own field or reset logic. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i --- .../ui/note/creators/draftTags/DraftTagState.kt | 17 +++++++++++++++++ .../note/nip22Comments/CommentPostViewModel.kt | 13 ++++--------- .../privateDM/send/ChatNewMessageViewModel.kt | 13 ++++--------- .../send/ChannelNewMessageViewModel.kt | 13 ++++--------- .../nip23LongForm/LongFormPostViewModel.kt | 13 ++++--------- .../nip99Classifieds/NewProductViewModel.kt | 13 ++++--------- .../loggedIn/home/ShortNotePostViewModel.kt | 13 ++++--------- .../nests/room/chat/NestNewMessageViewModel.kt | 13 ++++--------- .../publicMessages/NewPublicMessageViewModel.kt | 13 ++++--------- 9 files changed, 49 insertions(+), 72 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt index a014a1627e..1a387abd9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import com.vitorpamplona.amethyst.model.AddressableNote import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce @@ -36,6 +37,16 @@ class DraftTagState { var current: String by mutableStateOf(newTag()) var usedDraftTags by mutableStateOf(setOf(current)) + /** + * Strong reference to the AddressableNote backing the current draft tag. LocalCache only + * keeps weak references to addressables, so without an owner holding the note it can be + * garbage-collected and a later deletion would not find it locally, orphaning the draft on + * the relays. Its lifecycle is tied to the tag: [held] when a draft is saved or an existing + * draft is loaded, and dropped by [rotate] when we move on to a fresh draft. + */ + var note: AddressableNote? = null + private set + private val _versions = MutableStateFlow(0) @OptIn(FlowPreview::class) @@ -46,6 +57,7 @@ class DraftTagState { fun rotate() { set(newTag()) + note = null _versions.update { 0 } } @@ -54,6 +66,11 @@ class DraftTagState { usedDraftTags += existingTag } + /** Keeps a strong reference to the note that backs the current draft. */ + fun held(note: AddressableNote?) { + this.note = note + } + fun newVersion() { _versions.update { it + 1 } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index f19c84c134..2202628679 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -137,10 +137,6 @@ open class CommentPostViewModel : IExpiration { val draftTag = DraftTagState() - // Strong reference to the saved draft so LocalCache's weak reference can't collect it - // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). - var draftNote: AddressableNote? = null - init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { @@ -261,7 +257,7 @@ open class CommentPostViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftNote = draft as? AddressableNote + draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -495,7 +491,7 @@ open class CommentPostViewModel : } } - val draftToDelete = draftNote + val draftToDelete = draftTag.note val anonymous = wantsAnonymousPost cancel() @@ -512,7 +508,7 @@ open class CommentPostViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftNote) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { @@ -521,7 +517,7 @@ open class CommentPostViewModel : } val template = createTemplate() ?: return - draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) + draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments)) } } @@ -743,7 +739,6 @@ open class CommentPostViewModel : open fun cancel() { draftTag.rotate() - draftNote = null message.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 1c7a459fad..247b79dfd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -127,10 +127,6 @@ class ChatNewMessageViewModel : IExpiration { val draftTag = DraftTagState() - // Strong reference to the saved draft so LocalCache's weak reference can't collect it - // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). - var draftNote: AddressableNote? = null - lateinit var accountViewModel: AccountViewModel lateinit var account: Account @@ -327,7 +323,7 @@ class ChatNewMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftNote = draft as? AddressableNote + draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -424,7 +420,7 @@ class ChatNewMessageViewModel : } suspend fun sendPostSync() { - val draftToDelete = draftNote + val draftToDelete = draftTag.note innerSendPost(null) cancel() accountViewModel.viewModelScope.launch(Dispatchers.IO) { @@ -434,7 +430,7 @@ class ChatNewMessageViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - account.deleteDraftIgnoreErrors(draftNote) + account.deleteDraftIgnoreErrors(draftTag.note) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { innerSendPost(draftTag.current) } @@ -623,7 +619,7 @@ class ChatNewMessageViewModel : } if (draftTag != null) { - draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template) + this.draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template)) } else { accountViewModel.account.sendNip17PrivateMessage(template) } @@ -645,7 +641,6 @@ class ChatNewMessageViewModel : fun cancel() { draftTag.rotate() - draftNote = null message.setTextAndPlaceCursorAtEnd("") subject.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index f237d9b9df..6989a6411d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -113,10 +113,6 @@ open class ChannelNewMessageViewModel : IExpiration { val draftTag = DraftTagState() - // Strong reference to the saved draft so LocalCache's weak reference can't collect it - // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). - var draftNote: AddressableNote? = null - init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { @@ -229,7 +225,7 @@ open class ChannelNewMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftNote = draft as? AddressableNote + draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -307,7 +303,7 @@ open class ChannelNewMessageViewModel : val template = createTemplate() ?: return val channelRelays = channel?.relays() ?: emptySet() - val draftToDelete = draftNote + val draftToDelete = draftTag.note cancel() accountViewModel.account.signAndSendPrivatelyOrBroadcast(template) { @@ -320,7 +316,7 @@ open class ChannelNewMessageViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - account.deleteDraftIgnoreErrors(draftNote) + account.deleteDraftIgnoreErrors(draftTag.note) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { @@ -329,7 +325,7 @@ open class ChannelNewMessageViewModel : } val template = createTemplate() ?: return - draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) + draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments)) } } @@ -559,7 +555,6 @@ open class ChannelNewMessageViewModel : open fun cancel() { draftTag.rotate() - draftNote = null message.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index 8bcfe39f13..683cf29ae2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -129,10 +129,6 @@ class LongFormPostViewModel : IExpiration { val draftTag = DraftTagState() - // Strong reference to the saved draft so LocalCache's weak reference can't collect it - // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). - var draftNote: AddressableNote? = null - lateinit var accountViewModel: AccountViewModel lateinit var account: Account @@ -237,7 +233,7 @@ class LongFormPostViewModel : val noteAuthor = draft?.author if (draft != null && noteEvent is DraftWrapEvent && noteAuthor != null) { - draftNote = draft as? AddressableNote + draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -330,7 +326,7 @@ class LongFormPostViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val draftToDelete = draftNote + val draftToDelete = draftTag.note cancel() if (accountViewModel.settings.useTrackedBroadcasts()) { @@ -354,10 +350,10 @@ class LongFormPostViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank() && title.text.isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftNote) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val template = createTemplate() ?: return - draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, emptySet()) + draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, emptySet())) } } @@ -583,7 +579,6 @@ class LongFormPostViewModel : fun cancel() { draftTag.rotate() - draftNote = null title = TextFieldValue("") summary = TextFieldValue("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index f5f47d5e10..c5875d2c61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -115,10 +115,6 @@ open class NewProductViewModel : IExpiration { val draftTag = DraftTagState() - // Strong reference to the saved draft so LocalCache's weak reference can't collect it - // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). - var draftNote: AddressableNote? = null - lateinit var accountViewModel: AccountViewModel lateinit var account: Account @@ -218,7 +214,7 @@ open class NewProductViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftNote = draft as? AddressableNote + draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -321,7 +317,7 @@ open class NewProductViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val draftToDelete = draftNote + val draftToDelete = draftTag.note cancel() accountViewModel.account.signAndSendPrivatelyOrBroadcast(template, relayList = { relayList }) @@ -332,10 +328,10 @@ open class NewProductViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftNote) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val template = createTemplate() ?: return - draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template) + draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template)) } } @@ -474,7 +470,6 @@ open class NewProductViewModel : open fun cancel() { draftTag.rotate() - draftNote = null message.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index cf3564503e..ad297038fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -184,10 +184,6 @@ open class ShortNotePostViewModel : IExpiration { val draftTag = DraftTagState() - // Strong reference to the saved draft so LocalCache's weak reference can't collect it - // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). - var draftNote: AddressableNote? = null - lateinit var accountViewModel: AccountViewModel lateinit var account: Account @@ -483,7 +479,7 @@ open class ShortNotePostViewModel : val noteAuthor = draft?.author if (draft != null && noteEvent is DraftWrapEvent && noteAuthor != null) { - draftNote = draft as? AddressableNote + draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -895,7 +891,7 @@ open class ShortNotePostViewModel : } } - val draftToDelete = draftNote + val draftToDelete = draftTag.note val anonymous = wantsAnonymousPost val scheduledFor = scheduledForSec val privately = wantsPrivateNote @@ -973,7 +969,7 @@ open class ShortNotePostViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftNote) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { @@ -982,7 +978,7 @@ open class ShortNotePostViewModel : } val template = createTemplate() ?: return - draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) + draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments)) } } @@ -1318,7 +1314,6 @@ open class ShortNotePostViewModel : open fun cancel() { draftTag.rotate() - draftNote = null message.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index 3132759d23..df825041dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -118,10 +118,6 @@ open class NestNewMessageViewModel : IExpiration { val draftTag = DraftTagState() - // Strong reference to the saved draft so LocalCache's weak reference can't collect it - // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). - var draftNote: AddressableNote? = null - init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { @@ -234,7 +230,7 @@ open class NestNewMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftNote = draft as? AddressableNote + draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -308,7 +304,7 @@ open class NestNewMessageViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val draftToDelete = draftNote + val draftToDelete = draftTag.note cancel() // Broadcast to the user's default relays — the nest has no @@ -323,7 +319,7 @@ open class NestNewMessageViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - account.deleteDraftIgnoreErrors(draftNote) + account.deleteDraftIgnoreErrors(draftTag.note) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { @@ -332,7 +328,7 @@ open class NestNewMessageViewModel : } val template = createTemplate() ?: return - draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) + draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments)) } } @@ -477,7 +473,6 @@ open class NestNewMessageViewModel : open fun cancel() { draftTag.rotate() - draftNote = null message.setTextAndPlaceCursorAtEnd("") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index a94e84c94a..bf1703bd5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -130,10 +130,6 @@ class NewPublicMessageViewModel : IExpiration { val draftTag = DraftTagState() - // Strong reference to the saved draft so LocalCache's weak reference can't collect it - // before we get a chance to delete it (see Account.createAndSendDraftIgnoreErrors). - var draftNote: AddressableNote? = null - lateinit var accountViewModel: AccountViewModel lateinit var account: Account @@ -262,7 +258,7 @@ class NewPublicMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftNote = draft as? AddressableNote + draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -346,7 +342,7 @@ class NewPublicMessageViewModel : } } - val draftToDelete = draftNote + val draftToDelete = draftTag.note cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) @@ -357,7 +353,7 @@ class NewPublicMessageViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftNote) + accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val broadcast = mutableSetOf() nip95attachments.forEach { @@ -366,7 +362,7 @@ class NewPublicMessageViewModel : } val template = createTemplate() - draftNote = accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast) + draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast)) } } @@ -525,7 +521,6 @@ class NewPublicMessageViewModel : fun cancel() { draftTag.rotate() - draftNote = null toUsers.setTextAndPlaceCursorAtEnd("") message.setTextAndPlaceCursorAtEnd("") From b5706564d92f368446dbf1ed066e4d5b286ddd33 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 15:38:55 +0000 Subject: [PATCH 5/7] refactor: build the draft note from the tag inside DraftTagState MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of capturing the AddressableNote returned by each save (and on load) and re-assigning it, DraftTagState now builds the note from the current tag via an injected (tag -> AddressableNote) builder and rebuilds it whenever the tag changes. Because that note is the live cached object for the address, its event tracks the draft automatically as it is saved or removed, so: - the note is never null inside the state (lateinit, wired by start()); - createAndSendDraftIgnoreErrors no longer needs to return the note, and load no longer needs to capture it — set(oldTag) rebuilds it; - the writer's existence check becomes "is there a real, non-deleted draft event in the note" (DraftWrapEvent.isDeleted()), which also stops a second blank-delete from re-signing an already-emptied draft. ViewModels just wire draftTag.start(account::getOrCreateDraftNote) in init() and reference draftTag.note; the per-VM field and held() are gone. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i --- .../vitorpamplona/amethyst/model/Account.kt | 34 ++++++++++--------- .../note/creators/draftTags/DraftTagState.kt | 28 ++++++++------- .../nip22Comments/CommentPostViewModel.kt | 5 ++- .../privateDM/send/ChatNewMessageViewModel.kt | 5 ++- .../send/ChannelNewMessageViewModel.kt | 5 ++- .../nip23LongForm/LongFormPostViewModel.kt | 5 ++- .../nip99Classifieds/NewProductViewModel.kt | 5 ++- .../loggedIn/home/ShortNotePostViewModel.kt | 5 ++- .../room/chat/NestNewMessageViewModel.kt | 4 +-- .../NewPublicMessageViewModel.kt | 5 ++- 10 files changed, 50 insertions(+), 51 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index c938742658..88a70560bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2046,29 +2046,31 @@ class Account( } /** - * Returns the local [AddressableNote] that now holds the draft so the caller (the - * composer ViewModel) can keep a strong reference to it. [LocalCache.addressables] only - * keeps weak references, so without an owner holding the note it can be garbage collected - * and a later deletion would not find it locally (leaving an orphan on the relays). + * The live [AddressableNote] backing a draft tag for this account. It is the same cached + * note that draft events are consumed into, so its `event` tracks the draft over time. The + * composer holds onto it (via DraftTagState) so [LocalCache]'s weak reference can't collect + * it before a deletion needs it, which would otherwise orphan the draft on the relays. */ + fun getOrCreateDraftNote(draftTag: String): AddressableNote = cache.getOrCreateAddressableNote(DraftWrapEvent.createAddress(signer.pubKey, draftTag)) + suspend fun createAndSendDraftIgnoreErrors( draftTag: String, template: EventTemplate, broadcast: Set = emptySet(), - ): AddressableNote? = + ) { try { createAndSendDraftInner(draftTag, template, broadcast) } catch (e: Exception) { if (e is CancellationException) throw e - null } + } suspend fun createAndSendDraftInner( draftTag: String, template: EventTemplate, broadcast: Set = emptySet(), - ): AddressableNote? { - if (!isWriteable()) return null + ) { + if (!isWriteable()) return val extraRelays = cache.getAddressableNoteIfExists(DraftWrapEvent.createAddressTag(signer.pubKey, draftTag))?.relays ?: emptyList() @@ -2085,11 +2087,9 @@ class Account( client.publish(it, relayList.toSet()) } } - - return cache.getOrCreateAddressableNote(draftEvent.address()) } - suspend fun deleteDraftIgnoreErrors(draftNote: AddressableNote?) { + suspend fun deleteDraftIgnoreErrors(draftNote: AddressableNote) { try { deleteDraftInner(draftNote) } catch (e: Exception) { @@ -2097,13 +2097,15 @@ class Account( } } - suspend fun deleteDraftInner(draftNote: AddressableNote?) { + suspend fun deleteDraftInner(draftNote: AddressableNote) { if (!isWriteable()) return - // Nothing to delete means nothing to sign. The caller passes the note it has been - // holding (so it can't be garbage collected before we get here); a null note or one - // without an event means no draft was ever created, so we avoid prompting the signer. - if (draftNote?.event == null) return + // Only a real, still-present draft needs a deletion signed. The note is always non-null + // (it's the live cache note for the tag), but its event is null when no draft was ever + // saved (e.g. auto-drafts disabled) and already empty once it has been deleted — in both + // cases there is nothing to delete, so we avoid prompting the signer. + val draftEvent = draftNote.event as? DraftWrapEvent + if (draftEvent == null || draftEvent.isDeleted()) return val draftTag = draftNote.dTag() val extraRelays = draftNote.relays diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt index 1a387abd9f..f62f78e444 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt @@ -37,14 +37,17 @@ class DraftTagState { var current: String by mutableStateOf(newTag()) var usedDraftTags by mutableStateOf(setOf(current)) + private var noteBuilder: ((tag: String) -> AddressableNote)? = null + /** - * Strong reference to the AddressableNote backing the current draft tag. LocalCache only - * keeps weak references to addressables, so without an owner holding the note it can be - * garbage-collected and a later deletion would not find it locally, orphaning the draft on - * the relays. Its lifecycle is tied to the tag: [held] when a draft is saved or an existing - * draft is loaded, and dropped by [rotate] when we move on to a fresh draft. + * Strong reference to the AddressableNote backing the [current] draft tag, kept alive so + * LocalCache's weak reference can't garbage-collect it before a deletion needs it (which + * would orphan the draft on the relays). It is the live cached note for the tag, so its + * `event` reflects the draft automatically as it is saved or removed — no need to re-assign + * it after each save. Rebuilt whenever the tag changes ([set]/[rotate]); valid once [start] + * wires the builder, which happens when the composer is initialized. */ - var note: AddressableNote? = null + lateinit var note: AddressableNote private set private val _versions = MutableStateFlow(0) @@ -55,20 +58,21 @@ class DraftTagState { @OptIn(ExperimentalUuidApi::class) fun newTag() = Uuid.random().toString() + /** Wires the tag -> note builder and builds the note for the current tag. */ + fun start(builder: (tag: String) -> AddressableNote) { + noteBuilder = builder + note = builder(current) + } + fun rotate() { set(newTag()) - note = null _versions.update { 0 } } fun set(existingTag: String) { current = existingTag usedDraftTags += existingTag - } - - /** Keeps a strong reference to the note that backs the current draft. */ - fun held(note: AddressableNote?) { - this.note = note + noteBuilder?.let { note = it(existingTag) } } fun newVersion() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 2202628679..a7f01eb8e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -39,7 +39,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -238,6 +237,7 @@ open class CommentPostViewModel : open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account + draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -257,7 +257,6 @@ open class CommentPostViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -517,7 +516,7 @@ open class CommentPostViewModel : } val template = createTemplate() ?: return - draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments)) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 247b79dfd7..e7a989b769 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -37,7 +37,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -258,6 +257,7 @@ class ChatNewMessageViewModel : fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account + draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -323,7 +323,6 @@ class ChatNewMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -619,7 +618,7 @@ class ChatNewMessageViewModel : } if (draftTag != null) { - this.draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template)) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag, template) } else { accountViewModel.account.sendNip17PrivateMessage(template) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 6989a6411d..5ba50c7a11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -43,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -185,6 +184,7 @@ open class ChannelNewMessageViewModel : open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account + draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -225,7 +225,6 @@ open class ChannelNewMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -325,7 +324,7 @@ open class ChannelNewMessageViewModel : } val template = createTemplate() ?: return - draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments)) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index 683cf29ae2..e5e6c7fc7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -39,7 +39,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -215,6 +214,7 @@ class LongFormPostViewModel : fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account + draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -233,7 +233,6 @@ class LongFormPostViewModel : val noteAuthor = draft?.author if (draft != null && noteEvent is DraftWrapEvent && noteAuthor != null) { - draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -353,7 +352,7 @@ class LongFormPostViewModel : accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val template = createTemplate() ?: return - draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, emptySet())) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, emptySet()) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index c5875d2c61..d1dce35892 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -199,6 +198,7 @@ open class NewProductViewModel : open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account + draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -214,7 +214,6 @@ open class NewProductViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -331,7 +330,7 @@ open class NewProductViewModel : accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val template = createTemplate() ?: return - draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template)) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index ad297038fa..f8f2a34214 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -41,7 +41,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -458,6 +457,7 @@ open class ShortNotePostViewModel : open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account + draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -479,7 +479,6 @@ open class ShortNotePostViewModel : val noteAuthor = draft?.author if (draft != null && noteEvent is DraftWrapEvent && noteAuthor != null) { - draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -978,7 +977,7 @@ open class ShortNotePostViewModel : } val template = createTemplate() ?: return - draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments)) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index df825041dc..a1218de29b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -190,6 +190,7 @@ open class NestNewMessageViewModel : open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account + draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -230,7 +231,6 @@ open class NestNewMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -328,7 +328,7 @@ open class NestNewMessageViewModel : } val template = createTemplate() ?: return - draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments)) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index bf1703bd5a..d112b93f0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -209,6 +208,7 @@ class NewPublicMessageViewModel : fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account + draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -258,7 +258,6 @@ class NewPublicMessageViewModel : val noteAuthor = draft.author if (noteEvent is DraftWrapEvent && noteAuthor != null) { - draftTag.held(draft as? AddressableNote) viewModelScope.launch(Dispatchers.IO) { accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> val oldTag = (draft.event as? AddressableEvent)?.dTag() @@ -362,7 +361,7 @@ class NewPublicMessageViewModel : } val template = createTemplate() - draftTag.held(accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast)) + accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast) } } From 94b99e0973387a7e3bb701e2334284787ab98beb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 18:16:42 +0000 Subject: [PATCH 6/7] refactor: derive the draft note from the versions flow, decouple DraftTagState MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DraftTagState goes back to pure tag/version state — it no longer knows about AddressableNote or needs an account-aware builder. Instead each composer derives draftNote from the debounced versions collector it already runs: on each emission it maps the current tag to its live cache note via account.getOrCreateDraftNote(current). The ViewModel field holds the strong reference that keeps LocalCache's weak entry alive until a deletion needs it. load() refreshes draftNote after set(oldTag) because set() doesn't bump versions, covering the open-a-draft-then-send-without-editing case. deleteDraftInner takes the nullable note again and still only signs when the note holds a real, non-deleted DraftWrapEvent. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i --- .../vitorpamplona/amethyst/model/Account.kt | 13 ++++++------ .../note/creators/draftTags/DraftTagState.kt | 21 ------------------- .../nip22Comments/CommentPostViewModel.kt | 13 +++++++++--- .../privateDM/send/ChatNewMessageViewModel.kt | 13 +++++++++--- .../send/ChannelNewMessageViewModel.kt | 13 +++++++++--- .../nip23LongForm/LongFormPostViewModel.kt | 13 +++++++++--- .../nip99Classifieds/NewProductViewModel.kt | 13 +++++++++--- .../loggedIn/home/ShortNotePostViewModel.kt | 13 +++++++++--- .../room/chat/NestNewMessageViewModel.kt | 12 ++++++++--- .../NewPublicMessageViewModel.kt | 13 +++++++++--- 10 files changed, 85 insertions(+), 52 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 88a70560bc..9f214f91b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2089,7 +2089,7 @@ class Account( } } - suspend fun deleteDraftIgnoreErrors(draftNote: AddressableNote) { + suspend fun deleteDraftIgnoreErrors(draftNote: AddressableNote?) { try { deleteDraftInner(draftNote) } catch (e: Exception) { @@ -2097,14 +2097,13 @@ class Account( } } - suspend fun deleteDraftInner(draftNote: AddressableNote) { + suspend fun deleteDraftInner(draftNote: AddressableNote?) { if (!isWriteable()) return - // Only a real, still-present draft needs a deletion signed. The note is always non-null - // (it's the live cache note for the tag), but its event is null when no draft was ever - // saved (e.g. auto-drafts disabled) and already empty once it has been deleted — in both - // cases there is nothing to delete, so we avoid prompting the signer. - val draftEvent = draftNote.event as? DraftWrapEvent + // Only a real, still-present draft needs a deletion signed. The note's event is null when + // no draft was ever saved (e.g. auto-drafts disabled) and already empty once it has been + // deleted — in both cases there is nothing to delete, so we avoid prompting the signer. + val draftEvent = draftNote?.event as? DraftWrapEvent if (draftEvent == null || draftEvent.isDeleted()) return val draftTag = draftNote.dTag() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt index f62f78e444..a014a1627e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import com.vitorpamplona.amethyst.model.AddressableNote import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce @@ -37,19 +36,6 @@ class DraftTagState { var current: String by mutableStateOf(newTag()) var usedDraftTags by mutableStateOf(setOf(current)) - private var noteBuilder: ((tag: String) -> AddressableNote)? = null - - /** - * Strong reference to the AddressableNote backing the [current] draft tag, kept alive so - * LocalCache's weak reference can't garbage-collect it before a deletion needs it (which - * would orphan the draft on the relays). It is the live cached note for the tag, so its - * `event` reflects the draft automatically as it is saved or removed — no need to re-assign - * it after each save. Rebuilt whenever the tag changes ([set]/[rotate]); valid once [start] - * wires the builder, which happens when the composer is initialized. - */ - lateinit var note: AddressableNote - private set - private val _versions = MutableStateFlow(0) @OptIn(FlowPreview::class) @@ -58,12 +44,6 @@ class DraftTagState { @OptIn(ExperimentalUuidApi::class) fun newTag() = Uuid.random().toString() - /** Wires the tag -> note builder and builds the note for the current tag. */ - fun start(builder: (tag: String) -> AddressableNote) { - noteBuilder = builder - note = builder(current) - } - fun rotate() { set(newTag()) _versions.update { 0 } @@ -72,7 +52,6 @@ class DraftTagState { fun set(existingTag: String) { current = existingTag usedDraftTags += existingTag - noteBuilder?.let { note = it(existingTag) } } fun newVersion() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index a7f01eb8e9..7816fe7439 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -136,9 +137,15 @@ open class CommentPostViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the live cache note for the current draft tag (derived from the + // versions flow below), so LocalCache cannot weakly collect it before a deletion needs it. + var draftNote: AddressableNote? = null + private set + init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { + draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { sendDraftSync() @@ -237,7 +244,6 @@ open class CommentPostViewModel : open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account - draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -262,6 +268,7 @@ open class CommentPostViewModel : val oldTag = (draft.event as? AddressableEvent)?.dTag() if (oldTag != null) { draftTag.set(oldTag) + draftNote = account.getOrCreateDraftNote(oldTag) } loadFromDraft(innerNote) } @@ -490,7 +497,7 @@ open class CommentPostViewModel : } } - val draftToDelete = draftTag.note + val draftToDelete = draftNote val anonymous = wantsAnonymousPost cancel() @@ -507,7 +514,7 @@ open class CommentPostViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) + accountViewModel.account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index e7a989b769..802cd7915e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -126,12 +127,18 @@ class ChatNewMessageViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the live cache note for the current draft tag (derived from the + // versions flow below), so LocalCache cannot weakly collect it before a deletion needs it. + var draftNote: AddressableNote? = null + private set + lateinit var accountViewModel: AccountViewModel lateinit var account: Account init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { + draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { accountViewModel.launchSigner { @@ -257,7 +264,6 @@ class ChatNewMessageViewModel : fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account - draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -328,6 +334,7 @@ class ChatNewMessageViewModel : val oldTag = (draft.event as? AddressableEvent)?.dTag() if (oldTag != null) { draftTag.set(oldTag) + draftNote = account.getOrCreateDraftNote(oldTag) } loadFromDraft(innerNote) } @@ -419,7 +426,7 @@ class ChatNewMessageViewModel : } suspend fun sendPostSync() { - val draftToDelete = draftTag.note + val draftToDelete = draftNote innerSendPost(null) cancel() accountViewModel.viewModelScope.launch(Dispatchers.IO) { @@ -429,7 +436,7 @@ class ChatNewMessageViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - account.deleteDraftIgnoreErrors(draftTag.note) + account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { innerSendPost(draftTag.current) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 5ba50c7a11..62fcfe08b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -112,9 +113,15 @@ open class ChannelNewMessageViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the live cache note for the current draft tag (derived from the + // versions flow below), so LocalCache cannot weakly collect it before a deletion needs it. + var draftNote: AddressableNote? = null + private set + init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { + draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { accountViewModel.launchSigner { @@ -184,7 +191,6 @@ open class ChannelNewMessageViewModel : open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account - draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -230,6 +236,7 @@ open class ChannelNewMessageViewModel : val oldTag = (draft.event as? AddressableEvent)?.dTag() if (oldTag != null) { draftTag.set(oldTag) + draftNote = account.getOrCreateDraftNote(oldTag) } loadFromDraft(innerNote) } @@ -302,7 +309,7 @@ open class ChannelNewMessageViewModel : val template = createTemplate() ?: return val channelRelays = channel?.relays() ?: emptySet() - val draftToDelete = draftTag.note + val draftToDelete = draftNote cancel() accountViewModel.account.signAndSendPrivatelyOrBroadcast(template) { @@ -315,7 +322,7 @@ open class ChannelNewMessageViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - account.deleteDraftIgnoreErrors(draftTag.note) + account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index e5e6c7fc7d..ef029422c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -128,12 +129,18 @@ class LongFormPostViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the live cache note for the current draft tag (derived from the + // versions flow below), so LocalCache cannot weakly collect it before a deletion needs it. + var draftNote: AddressableNote? = null + private set + lateinit var accountViewModel: AccountViewModel lateinit var account: Account init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { + draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { accountViewModel.launchSigner { @@ -214,7 +221,6 @@ class LongFormPostViewModel : fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account - draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -238,6 +244,7 @@ class LongFormPostViewModel : val oldTag = (draft.event as? AddressableEvent)?.dTag() if (oldTag != null) { draftTag.set(oldTag) + draftNote = account.getOrCreateDraftNote(oldTag) } loadFromDraft(innerNote) } @@ -325,7 +332,7 @@ class LongFormPostViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val draftToDelete = draftTag.note + val draftToDelete = draftNote cancel() if (accountViewModel.settings.useTrackedBroadcasts()) { @@ -349,7 +356,7 @@ class LongFormPostViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank() && title.text.isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) + accountViewModel.account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val template = createTemplate() ?: return accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, emptySet()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index d1dce35892..19055e35ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -114,12 +115,18 @@ open class NewProductViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the live cache note for the current draft tag (derived from the + // versions flow below), so LocalCache cannot weakly collect it before a deletion needs it. + var draftNote: AddressableNote? = null + private set + lateinit var accountViewModel: AccountViewModel lateinit var account: Account init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { + draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { accountViewModel.launchSigner { @@ -198,7 +205,6 @@ open class NewProductViewModel : open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account - draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -219,6 +225,7 @@ open class NewProductViewModel : val oldTag = (draft.event as? AddressableEvent)?.dTag() if (oldTag != null) { draftTag.set(oldTag) + draftNote = account.getOrCreateDraftNote(oldTag) } loadFromDraft(innerNote) } @@ -316,7 +323,7 @@ open class NewProductViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val draftToDelete = draftTag.note + val draftToDelete = draftNote cancel() accountViewModel.account.signAndSendPrivatelyOrBroadcast(template, relayList = { relayList }) @@ -327,7 +334,7 @@ open class NewProductViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) + accountViewModel.account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val template = createTemplate() ?: return accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index f8f2a34214..9452082e39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -183,12 +184,18 @@ open class ShortNotePostViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the live cache note for the current draft tag (derived from the + // versions flow below), so LocalCache cannot weakly collect it before a deletion needs it. + var draftNote: AddressableNote? = null + private set + lateinit var accountViewModel: AccountViewModel lateinit var account: Account init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { + draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { accountViewModel.launchSigner { @@ -457,7 +464,6 @@ open class ShortNotePostViewModel : open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account - draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -484,6 +490,7 @@ open class ShortNotePostViewModel : val oldTag = (draft.event as? AddressableEvent)?.dTag() if (oldTag != null) { draftTag.set(oldTag) + draftNote = account.getOrCreateDraftNote(oldTag) } loadFromDraft(innerNote) } @@ -890,7 +897,7 @@ open class ShortNotePostViewModel : } } - val draftToDelete = draftTag.note + val draftToDelete = draftNote val anonymous = wantsAnonymousPost val scheduledFor = scheduledForSec val privately = wantsPrivateNote @@ -968,7 +975,7 @@ open class ShortNotePostViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) + accountViewModel.account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index a1218de29b..7090f6df59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -118,9 +118,15 @@ open class NestNewMessageViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the live cache note for the current draft tag (derived from the + // versions flow below), so LocalCache cannot weakly collect it before a deletion needs it. + var draftNote: AddressableNote? = null + private set + init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { + draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { accountViewModel.launchSigner { @@ -190,7 +196,6 @@ open class NestNewMessageViewModel : open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account - draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -236,6 +241,7 @@ open class NestNewMessageViewModel : val oldTag = (draft.event as? AddressableEvent)?.dTag() if (oldTag != null) { draftTag.set(oldTag) + draftNote = account.getOrCreateDraftNote(oldTag) } loadFromDraft(innerNote) } @@ -304,7 +310,7 @@ open class NestNewMessageViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val draftToDelete = draftTag.note + val draftToDelete = draftNote cancel() // Broadcast to the user's default relays — the nest has no @@ -319,7 +325,7 @@ open class NestNewMessageViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - account.deleteDraftIgnoreErrors(draftTag.note) + account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index d112b93f0b..c84c2b1d05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -129,12 +130,18 @@ class NewPublicMessageViewModel : IExpiration { val draftTag = DraftTagState() + // Strong reference to the live cache note for the current draft tag (derived from the + // versions flow below), so LocalCache cannot weakly collect it before a deletion needs it. + var draftNote: AddressableNote? = null + private set + lateinit var accountViewModel: AccountViewModel lateinit var account: Account init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { + draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { accountViewModel.launchSigner { @@ -208,7 +215,6 @@ class NewPublicMessageViewModel : fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM this.account = accountVM.account - draftTag.start(account::getOrCreateDraftNote) this.canAddInvoice = hasLnAddress() this.canAddZapRaiser = hasLnAddress() @@ -263,6 +269,7 @@ class NewPublicMessageViewModel : val oldTag = (draft.event as? AddressableEvent)?.dTag() if (oldTag != null) { draftTag.set(oldTag) + draftNote = account.getOrCreateDraftNote(oldTag) } loadFromDraft(innerNote) } @@ -341,7 +348,7 @@ class NewPublicMessageViewModel : } } - val draftToDelete = draftTag.note + val draftToDelete = draftNote cancel() accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) @@ -352,7 +359,7 @@ class NewPublicMessageViewModel : suspend fun sendDraftSync() { if (message.text.toString().isBlank()) { - accountViewModel.account.deleteDraftIgnoreErrors(draftTag.note) + accountViewModel.account.deleteDraftIgnoreErrors(draftNote) } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val broadcast = mutableSetOf() nip95attachments.forEach { From 119517bd3add16bba80780d8e99e2ca65d86ed32 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 18:41:23 +0000 Subject: [PATCH 7/7] fix: only resolve the draft note when there is a version to save The versions collector resolved draftNote = account.getOrCreateDraftNote(...) on every emission, including the content-less initial tick (~1s after the composer opens). That touched the lateinit account before any user input; if it were ever unset at that moment the throw would kill the collectLatest coroutine and silently stop all draft saves for that composer. Move the resolve inside the `if (it > 0)` guard, co-located with the save, so account is only read once a real edit exists. The open-a-draft-then-send- without-editing case is still covered by the explicit refresh in load(). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i --- .../amethyst/ui/note/nip22Comments/CommentPostViewModel.kt | 2 +- .../loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt | 2 +- .../chats/publicChannels/send/ChannelNewMessageViewModel.kt | 2 +- .../loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt | 2 +- .../loggedIn/discover/nip99Classifieds/NewProductViewModel.kt | 2 +- .../amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt | 2 +- .../screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt | 2 +- .../notifications/publicMessages/NewPublicMessageViewModel.kt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 7816fe7439..3c3973b09e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -145,9 +145,9 @@ open class CommentPostViewModel : init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { - draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { + draftNote = account.getOrCreateDraftNote(draftTag.current) sendDraftSync() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 802cd7915e..c5c94e64c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -138,9 +138,9 @@ class ChatNewMessageViewModel : init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { - draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { + draftNote = account.getOrCreateDraftNote(draftTag.current) accountViewModel.launchSigner { sendDraftSync() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 62fcfe08b4..142c025d54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -121,9 +121,9 @@ open class ChannelNewMessageViewModel : init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { - draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { + draftNote = account.getOrCreateDraftNote(draftTag.current) accountViewModel.launchSigner { sendDraftSync() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index ef029422c6..cac2107755 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -140,9 +140,9 @@ class LongFormPostViewModel : init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { - draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { + draftNote = account.getOrCreateDraftNote(draftTag.current) accountViewModel.launchSigner { sendDraftSync() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 19055e35ba..0cbfe7a650 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -126,9 +126,9 @@ open class NewProductViewModel : init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { - draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { + draftNote = account.getOrCreateDraftNote(draftTag.current) accountViewModel.launchSigner { sendDraftSync() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 9452082e39..a447dbcdca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -195,9 +195,9 @@ open class ShortNotePostViewModel : init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { - draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { + draftNote = account.getOrCreateDraftNote(draftTag.current) accountViewModel.launchSigner { sendDraftSync() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index 7090f6df59..44bfb87c27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -126,9 +126,9 @@ open class NestNewMessageViewModel : init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { - draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { + draftNote = account.getOrCreateDraftNote(draftTag.current) accountViewModel.launchSigner { sendDraftSync() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index c84c2b1d05..a97e2068da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -141,9 +141,9 @@ class NewPublicMessageViewModel : init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { - draftNote = account.getOrCreateDraftNote(draftTag.current) // don't save the first if (it > 0) { + draftNote = account.getOrCreateDraftNote(draftTag.current) accountViewModel.launchSigner { sendDraftSync() }