mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
refactor: build the draft note from the tag inside DraftTagState
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016aj4ajZ4uQ58Bqts7riJ5i
This commit is contained in:
@@ -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<out Event>,
|
||||
broadcast: Set<Event> = emptySet(),
|
||||
): AddressableNote? =
|
||||
) {
|
||||
try {
|
||||
createAndSendDraftInner(draftTag, template, broadcast)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createAndSendDraftInner(
|
||||
draftTag: String,
|
||||
template: EventTemplate<out Event>,
|
||||
broadcast: Set<Event> = 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
|
||||
|
||||
+16
-12
@@ -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() {
|
||||
|
||||
+2
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -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)
|
||||
}
|
||||
|
||||
+2
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user