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) } }