From 8b2b43d3c44084d4a83878f85df34466bb9840c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 01:19:16 +0000 Subject: [PATCH 01/14] feat: redesign always-on service notification icon as a circular badge The persistent relay-service notification previously reused a hollow gem outline, which looked unpolished. Replace it with a full, solid disc that has the brand gem punched out of its centre as negative space (slightly smaller, with a comfortable surrounding ring). The enclosing circle gives the always-running service its own distinct, continuous "badge" feel while keeping the recognisable Amethyst gem shape. Built from the real gem path so it stays on-brand; a single evenOdd path turns the gem into a hole and restores its facet dot as a solid island. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi --- .../main/res/drawable/amethyst_service.xml | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/amethyst/src/main/res/drawable/amethyst_service.xml b/amethyst/src/main/res/drawable/amethyst_service.xml index 2413c3894e..96de785da2 100644 --- a/amethyst/src/main/res/drawable/amethyst_service.xml +++ b/amethyst/src/main/res/drawable/amethyst_service.xml @@ -3,31 +3,25 @@ ~ ~ Notification small icons are rendered by the system as flat, alpha-only ~ silhouettes, so the regular `amethyst` gem becomes an identical solid gem - ~ for both real notifications and this persistent service. To stop the - ~ service from reading as a new notification, this variant draws the gem as - ~ a hollow outline — same brand shape, clearly different at a glance. + ~ for both real notifications and this persistent service. To set the + ~ always-on service apart, this variant draws a full, solid disc with the + ~ brand gem punched out of its centre (slightly smaller, as negative space). + ~ The enclosing circle reads as a continuous, always-running badge — clearly + ~ different at a glance while keeping the recognisable gem shape. + ~ + ~ The single path combines the disc (outer subpath) with the gem body and its + ~ facet dot (inner subpaths); `fillType="evenOdd"` turns the gem into a hole, + ~ and the small facet dot back into a solid island within that hole, echoing + ~ the highlight on the regular gem. The gem is centred on the disc and scaled + ~ to ~52% of its diameter so a comfortable ring surrounds it on every side. --> - - - - + From 27191c3a36b309af2871b18d9fa93f14113a8146 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 02:06:49 +0000 Subject: [PATCH 02/14] fix: enlarge gem in service notification icon by 10% The punched-out gem now spans ~57% of the disc diameter (was ~52%), giving it slightly more presence within the circular badge. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi --- amethyst/src/main/res/drawable/amethyst_service.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/res/drawable/amethyst_service.xml b/amethyst/src/main/res/drawable/amethyst_service.xml index 96de785da2..1840d24a7e 100644 --- a/amethyst/src/main/res/drawable/amethyst_service.xml +++ b/amethyst/src/main/res/drawable/amethyst_service.xml @@ -13,7 +13,7 @@ ~ facet dot (inner subpaths); `fillType="evenOdd"` turns the gem into a hole, ~ and the small facet dot back into a solid island within that hole, echoing ~ the highlight on the regular gem. The gem is centred on the disc and scaled - ~ to ~52% of its diameter so a comfortable ring surrounds it on every side. + ~ to ~57% of its diameter so a comfortable ring surrounds it on every side. --> + android:pathData="M256,16A240,240 0,1,1 256,496A240,240 0,1,1 256,16ZM381.672,328.981C381.376,328.082,312.47,164.009,293.518,118.5c-23.075,0,-71.802,0,-106.174,0c-10.087,24.948,-36.127,88.536,-57.016,139.761c11.448,27.311,21.729,51.844,29.657,70.768l43.232,0a2.955,2.955 0,0,0,2.761,-3.472c-0.371,-8.73,-24.135,-40.46,-17.046,-74.784c1.889,-7.385,3.337,-14.888,5.404,-22.246a259.628,259.628 135,0,1,19.85,-51.402c1.313,-2.497,1.922,-3.617,4.946,-1.728c6.454,4.015,13.661,3.784,20.841,3.262c6.19,-1.346,9.204,-4.311,20.545,-1.297c2.244,0,4.43,0,6.723,0.205c5.452,0.431,10.56,1.728,13.838,6.663c3.574,5.393,4.15,11.492,3.574,17.724c-0.872,9.322,-0.253,18.004,7.638,24.576a84.962,84.962 0,0,0,8.865,5.91c3.913,2.514,8.639,3.606,11.992,7.089c1.981,2.051,3.149,4.241,2.288,7.164c-0.856,2.923,-3.149,4.301,-6.158,4.624c-7.622,0.829,-15.065,-0.721,-22.552,-1.625c-0.974,-0.118,-1.932,-0.194,-2.955,-0.253a16.944,16.944 0,0,1,-5.63,0.194l-1.76,0a39.437,39.437 0,0,0,-8.714,2.083c-7.062,2.573,-13.488,5.404,-21.815,5.259a6.384,6.384 0,0,0,-3.03,1.313c-6.529,5.802,-10.458,13.133,-7.191,23.839c8.951,25.007,31.918,85.333,41.789,111.374c-0.28,-17.638,-0.355,-49.217,-0.414,-64.546c39.41,0.059,99.834,0.027,104.651,0.027ZM248.823,202.886a8.332,8.477 90,1,0,16.955,0a8.332,8.477 90,1,0,-16.955,0Z" /> From fa12576a8c4eb0ed1574e9f538a40feec9e70979 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 12:29:37 +0000 Subject: [PATCH 03/14] 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 04/14] 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 bacb8a57ad8cedd26762d41ed12c2d3c5241c5ef Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 14:57:22 +0000 Subject: [PATCH 05/14] feat: add circular-badge service icon as amethyst_service2 option Keep the original hollow-outline amethyst_service icon and add the new circular badge (solid disc with the gem punched out of its centre) as a separate amethyst_service2 drawable, so the always-on notification icon can be switched between the two without losing either design. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi --- .../main/res/drawable/amethyst_service.xml | 36 +++++++++++-------- .../main/res/drawable/amethyst_service2.xml | 27 ++++++++++++++ 2 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 amethyst/src/main/res/drawable/amethyst_service2.xml diff --git a/amethyst/src/main/res/drawable/amethyst_service.xml b/amethyst/src/main/res/drawable/amethyst_service.xml index 1840d24a7e..2413c3894e 100644 --- a/amethyst/src/main/res/drawable/amethyst_service.xml +++ b/amethyst/src/main/res/drawable/amethyst_service.xml @@ -3,25 +3,31 @@ ~ ~ Notification small icons are rendered by the system as flat, alpha-only ~ silhouettes, so the regular `amethyst` gem becomes an identical solid gem - ~ for both real notifications and this persistent service. To set the - ~ always-on service apart, this variant draws a full, solid disc with the - ~ brand gem punched out of its centre (slightly smaller, as negative space). - ~ The enclosing circle reads as a continuous, always-running badge — clearly - ~ different at a glance while keeping the recognisable gem shape. - ~ - ~ The single path combines the disc (outer subpath) with the gem body and its - ~ facet dot (inner subpaths); `fillType="evenOdd"` turns the gem into a hole, - ~ and the small facet dot back into a solid island within that hole, echoing - ~ the highlight on the regular gem. The gem is centred on the disc and scaled - ~ to ~57% of its diameter so a comfortable ring surrounds it on every side. + ~ for both real notifications and this persistent service. To stop the + ~ service from reading as a new notification, this variant draws the gem as + ~ a hollow outline — same brand shape, clearly different at a glance. --> - + + + + diff --git a/amethyst/src/main/res/drawable/amethyst_service2.xml b/amethyst/src/main/res/drawable/amethyst_service2.xml new file mode 100644 index 0000000000..1840d24a7e --- /dev/null +++ b/amethyst/src/main/res/drawable/amethyst_service2.xml @@ -0,0 +1,27 @@ + + + + From 50dbbeeecc3810ba0007885fee9061d369166c6c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 14:59:44 +0000 Subject: [PATCH 06/14] 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 07/14] 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 e1c152ada87f6be25427e1a83d90fda7fc527942 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 15:33:15 +0000 Subject: [PATCH 08/14] feat: add four more service-icon options + on-device preview harness Add four additional always-on service notification icon candidates, each keeping the centred brand gem with a "background service / connected to servers" motif around it: - amethyst_service3: orbit ring with three server nodes - amethyst_service4: two looping sync arrows (running) - amethyst_service5: hub & spoke to five nodes (connected to relays) - amethyst_service6: concentric broadcast waves To compare all candidates on a real device, add a DEBUG-only helper (ServiceIconPreviewNotifications) that posts one always-on-style ongoing notification per icon (same channel style, ongoing/silent/low priority), triggered from MainActivity.onCreate under BuildConfig.DEBUG. NOTE: the preview harness (ServiceIconPreviewNotifications + the MainActivity hook) is temporary scaffolding for the icon bake-off and is meant to be removed once a design is chosen. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi --- .../ServiceIconPreviewNotifications.kt | 108 ++++++++++++++++++ .../vitorpamplona/amethyst/ui/MainActivity.kt | 9 ++ .../main/res/drawable/amethyst_service3.xml | 30 +++++ .../main/res/drawable/amethyst_service4.xml | 35 ++++++ .../main/res/drawable/amethyst_service5.xml | 65 +++++++++++ .../main/res/drawable/amethyst_service6.xml | 57 +++++++++ 6 files changed, 304 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/ServiceIconPreviewNotifications.kt create mode 100644 amethyst/src/main/res/drawable/amethyst_service3.xml create mode 100644 amethyst/src/main/res/drawable/amethyst_service4.xml create mode 100644 amethyst/src/main/res/drawable/amethyst_service5.xml create mode 100644 amethyst/src/main/res/drawable/amethyst_service6.xml diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/ServiceIconPreviewNotifications.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/ServiceIconPreviewNotifications.kt new file mode 100644 index 0000000000..a1398ce5a0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/ServiceIconPreviewNotifications.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.notifications + +// ===================================================================================== +// TEMPORARY / DEBUG-ONLY — REMOVE BEFORE MERGE. +// +// Posts one always-on-style notification per candidate small-icon so the different +// "service icon" designs can be compared side by side in the status bar / shade on a +// real device. These mimic the real NotificationRelayService notification (same +// channel, ongoing, silent, low priority) but each uses a different drawable and a +// label naming the design. +// +// To remove the whole experiment later: delete this file, the four extra drawables +// (amethyst_service2..6), and the single ServiceIconPreviewNotifications.postAll(...) +// call in MainActivity.onCreate. +// ===================================================================================== + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import androidx.annotation.DrawableRes +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.vitorpamplona.amethyst.R + +object ServiceIconPreviewNotifications { + private const val CHANNEL_ID = "service_icon_preview" + private const val BASE_ID = 920_000 + + private data class Candidate( + @DrawableRes val icon: Int, + val label: String, + ) + + private val candidates = + listOf( + Candidate(R.drawable.amethyst_service, "1 · Hollow gem outline (current)"), + Candidate(R.drawable.amethyst_service2, "2 · Solid circle, gem cut-out"), + Candidate(R.drawable.amethyst_service3, "3 · Orbit + server nodes"), + Candidate(R.drawable.amethyst_service4, "4 · Sync arrows (running)"), + Candidate(R.drawable.amethyst_service5, "5 · Hub & spoke (connected)"), + Candidate(R.drawable.amethyst_service6, "6 · Broadcast waves"), + ) + + fun postAll(context: Context) { + val manager = NotificationManagerCompat.from(context) + if (!manager.areNotificationsEnabled()) return + + ensureChannel(context) + + candidates.forEachIndexed { index, candidate -> + val notification = + NotificationCompat + .Builder(context, CHANNEL_ID) + .setContentTitle("Service icon ${index + 1}") + .setContentText(candidate.label) + .setSmallIcon(candidate.icon) + .setOngoing(true) + .setSilent(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .build() + + manager.notify(BASE_ID + index, notification) + } + } + + fun clearAll(context: Context) { + val manager = NotificationManagerCompat.from(context) + candidates.indices.forEach { manager.cancel(BASE_ID + it) } + } + + private fun ensureChannel(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + val channel = + NotificationChannel( + CHANNEL_ID, + "Service icon preview (debug)", + NotificationManager.IMPORTANCE_LOW, + ).apply { + description = "Temporary side-by-side preview of always-on service icons" + setShowBadge(false) + } + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index edf9d17fbf..9a594cfa42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -27,11 +27,13 @@ import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.notifications.NotificationRelayService +import com.vitorpamplona.amethyst.service.notifications.ServiceIconPreviewNotifications import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia import com.vitorpamplona.amethyst.ui.navigation.findParameterValue @@ -78,6 +80,13 @@ class MainActivity : AppCompatActivity() { Log.d("ActivityLifecycle") { "MainActivity.onCreate $this" } + // TEMPORARY / DEBUG-ONLY — REMOVE BEFORE MERGE. + // Posts side-by-side preview notifications for the candidate always-on + // service icons so they can be compared on a real device. + if (BuildConfig.DEBUG) { + ServiceIconPreviewNotifications.postAll(this) + } + setContent { StringResSetup() AmethystTheme { diff --git a/amethyst/src/main/res/drawable/amethyst_service3.xml b/amethyst/src/main/res/drawable/amethyst_service3.xml new file mode 100644 index 0000000000..4eeb6cf4fa --- /dev/null +++ b/amethyst/src/main/res/drawable/amethyst_service3.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/amethyst/src/main/res/drawable/amethyst_service4.xml b/amethyst/src/main/res/drawable/amethyst_service4.xml new file mode 100644 index 0000000000..6ebb841d09 --- /dev/null +++ b/amethyst/src/main/res/drawable/amethyst_service4.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/amethyst/src/main/res/drawable/amethyst_service5.xml b/amethyst/src/main/res/drawable/amethyst_service5.xml new file mode 100644 index 0000000000..54bde2c604 --- /dev/null +++ b/amethyst/src/main/res/drawable/amethyst_service5.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + diff --git a/amethyst/src/main/res/drawable/amethyst_service6.xml b/amethyst/src/main/res/drawable/amethyst_service6.xml new file mode 100644 index 0000000000..be905d5e97 --- /dev/null +++ b/amethyst/src/main/res/drawable/amethyst_service6.xml @@ -0,0 +1,57 @@ + + + + + + + + + + From b5706564d92f368446dbf1ed066e4d5b286ddd33 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 15:38:55 +0000 Subject: [PATCH 09/14] 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 002c8b6d30b76ff2e04a7ad9b71ad9ee4cc85582 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 15:53:11 +0000 Subject: [PATCH 10/14] style: enlarge service-icon options to fill the icon frame Scale up the orbit/sync/hub/waves motifs and their centred gems so each drawing occupies as much of the 512 viewport as possible (rings and nodes pushed near the edge, larger central gem), keeping a small margin so strokes don't clip. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi --- .../main/res/drawable/amethyst_service3.xml | 15 ++++---- .../main/res/drawable/amethyst_service4.xml | 17 ++++----- .../main/res/drawable/amethyst_service5.xml | 35 ++++++++++--------- .../main/res/drawable/amethyst_service6.xml | 17 ++++----- 4 files changed, 44 insertions(+), 40 deletions(-) diff --git a/amethyst/src/main/res/drawable/amethyst_service3.xml b/amethyst/src/main/res/drawable/amethyst_service3.xml index 4eeb6cf4fa..2c28c2e25a 100644 --- a/amethyst/src/main/res/drawable/amethyst_service3.xml +++ b/amethyst/src/main/res/drawable/amethyst_service3.xml @@ -1,7 +1,8 @@ + android:pathData="M351.968,311.731C351.742,311.044,299.123,185.752,284.65,151c-17.621,0,-54.831,0,-81.078,0c-7.703,19.051,-27.588,67.609,-43.54,106.726c8.742,20.855,16.593,39.59,22.647,54.041l33.013,0a2.257,2.257 0,0,0,2.109,-2.651c-0.284,-6.667,-18.43,-30.897,-13.017,-57.108c1.443,-5.639,2.548,-11.369,4.127,-16.988a198.261,198.261 135,0,1,15.159,-39.253c1.003,-1.907,1.467,-2.762,3.777,-1.319c4.928,3.066,10.432,2.889,15.915,2.491c4.727,-1.028,7.028,-3.292,15.689,-0.991c1.714,0,3.383,0,5.134,0.156c4.164,0.329,8.064,1.319,10.567,5.088c2.729,4.118,3.169,8.775,2.729,13.535c-0.666,7.119,-0.193,13.749,5.832,18.767a64.88,64.88 0,0,0,6.77,4.513c2.988,1.919,6.597,2.754,9.158,5.413c1.513,1.566,2.404,3.239,1.747,5.471c-0.654,2.232,-2.404,3.284,-4.702,3.531c-5.82,0.633,-11.505,-0.551,-17.222,-1.241c-0.744,-0.09,-1.476,-0.148,-2.257,-0.193a12.939,12.939 0,0,1,-4.299,0.148l-1.344,0a30.116,30.116 0,0,0,-6.654,1.591c-5.393,1.965,-10.3,4.127,-16.659,4.016a4.875,4.875 0,0,0,-2.314,1.003c-4.986,4.431,-7.986,10.029,-5.491,18.204c6.835,19.096,24.374,65.164,31.912,85.049c-0.214,-13.469,-0.271,-37.584,-0.316,-49.29c30.095,0.045,76.237,0.021,79.915,0.021ZM250.519,215.44a6.363,6.474 90,1,0,12.947,0a6.363,6.474 90,1,0,-12.947,0Z" /> + android:pathData="M256,46A210,210 0,1,1 256,466A210,210 0,1,1 256,46Z" /> + android:pathData="M256,10A36,36 0,1,1 256,82A36,36 0,1,1 256,10Z" /> + android:pathData="M74.13,325A36,36 0,1,1 74.13,397A36,36 0,1,1 74.13,325Z" /> + android:pathData="M437.87,325A36,36 0,1,1 437.87,397A36,36 0,1,1 437.87,325Z" /> diff --git a/amethyst/src/main/res/drawable/amethyst_service4.xml b/amethyst/src/main/res/drawable/amethyst_service4.xml index 6ebb841d09..f61d10eb86 100644 --- a/amethyst/src/main/res/drawable/amethyst_service4.xml +++ b/amethyst/src/main/res/drawable/amethyst_service4.xml @@ -1,7 +1,8 @@ + android:pathData="M351.968,311.731C351.742,311.044,299.123,185.752,284.65,151c-17.621,0,-54.831,0,-81.078,0c-7.703,19.051,-27.588,67.609,-43.54,106.726c8.742,20.855,16.593,39.59,22.647,54.041l33.013,0a2.257,2.257 0,0,0,2.109,-2.651c-0.284,-6.667,-18.43,-30.897,-13.017,-57.108c1.443,-5.639,2.548,-11.369,4.127,-16.988a198.261,198.261 135,0,1,15.159,-39.253c1.003,-1.907,1.467,-2.762,3.777,-1.319c4.928,3.066,10.432,2.889,15.915,2.491c4.727,-1.028,7.028,-3.292,15.689,-0.991c1.714,0,3.383,0,5.134,0.156c4.164,0.329,8.064,1.319,10.567,5.088c2.729,4.118,3.169,8.775,2.729,13.535c-0.666,7.119,-0.193,13.749,5.832,18.767a64.88,64.88 0,0,0,6.77,4.513c2.988,1.919,6.597,2.754,9.158,5.413c1.513,1.566,2.404,3.239,1.747,5.471c-0.654,2.232,-2.404,3.284,-4.702,3.531c-5.82,0.633,-11.505,-0.551,-17.222,-1.241c-0.744,-0.09,-1.476,-0.148,-2.257,-0.193a12.939,12.939 0,0,1,-4.299,0.148l-1.344,0a30.116,30.116 0,0,0,-6.654,1.591c-5.393,1.965,-10.3,4.127,-16.659,4.016a4.875,4.875 0,0,0,-2.314,1.003c-4.986,4.431,-7.986,10.029,-5.491,18.204c6.835,19.096,24.374,65.164,31.912,85.049c-0.214,-13.469,-0.271,-37.584,-0.316,-49.29c30.095,0.045,76.237,0.021,79.915,0.021ZM250.519,215.44a6.363,6.474 90,1,0,12.947,0a6.363,6.474 90,1,0,-12.947,0Z" /> + android:pathData="M301.74,40.81A220,220 0,0,1,301.74,471.19" /> + android:pathData="M210.26,471.19A220,220 0,0,1,210.26,40.81" /> + android:pathData="M271.96,474.85L322.2,449.25L326.9,487.53Z" /> + android:pathData="M240.04,37.15L189.8,62.75L185.1,24.47Z" /> diff --git a/amethyst/src/main/res/drawable/amethyst_service5.xml b/amethyst/src/main/res/drawable/amethyst_service5.xml index 54bde2c604..d8785cf09a 100644 --- a/amethyst/src/main/res/drawable/amethyst_service5.xml +++ b/amethyst/src/main/res/drawable/amethyst_service5.xml @@ -1,7 +1,8 @@ + android:pathData="M341.914,305.892C341.712,305.278,294.605,193.111,281.649,162c-15.775,0,-49.086,0,-72.585,0c-6.896,17.055,-24.698,60.526,-38.978,95.545c7.827,18.67,14.855,35.442,20.275,48.38l29.555,0a2.02,2.02 0,0,0,1.888,-2.373c-0.254,-5.968,-16.499,-27.66,-11.653,-51.125c1.292,-5.048,2.281,-10.178,3.694,-15.208a177.491,177.491 135,0,1,13.571,-35.141c0.898,-1.707,1.314,-2.473,3.382,-1.181c4.412,2.745,9.339,2.587,14.248,2.23c4.232,-0.92,6.292,-2.947,14.045,-0.887c1.534,0,3.028,0,4.596,0.14c3.727,0.294,7.219,1.181,9.46,4.555c2.443,3.687,2.837,7.856,2.443,12.117c-0.596,6.373,-0.173,12.308,5.221,16.801a58.083,58.083 0,0,0,6.06,4.04c2.675,1.718,5.906,2.465,8.198,4.846c1.354,1.402,2.153,2.9,1.564,4.898c-0.585,1.998,-2.153,2.94,-4.21,3.161c-5.21,0.567,-10.299,-0.493,-15.418,-1.111c-0.666,-0.081,-1.321,-0.132,-2.02,-0.173a11.583,11.583 0,0,1,-3.849,0.132l-1.203,0a26.961,26.961 0,0,0,-5.957,1.424c-4.828,1.759,-9.221,3.694,-14.914,3.595a4.364,4.364 0,0,0,-2.072,0.898c-4.463,3.967,-7.15,8.978,-4.916,16.297c6.119,17.096,21.82,58.337,28.569,76.139c-0.191,-12.058,-0.243,-33.647,-0.283,-44.126c26.942,0.04,68.25,0.018,71.543,0.018ZM251.093,219.689a5.696,5.795 90,1,0,11.591,0a5.696,5.795 90,1,0,-11.591,0Z" /> + android:pathData="M256,130L256,34" /> + android:pathData="M375.83,217.06L467.13,187.4" /> + android:pathData="M330.06,357.94L386.49,435.6" /> + android:pathData="M181.94,357.94L125.51,435.6" /> + android:pathData="M136.17,217.06L44.87,187.4" /> + android:pathData="M256,6A28,28 0,1,1 256,62A28,28 0,1,1 256,6Z" /> + android:pathData="M467.13,159.4A28,28 0,1,1 467.13,215.4A28,28 0,1,1 467.13,159.4Z" /> + android:pathData="M386.49,407.6A28,28 0,1,1 386.49,463.6A28,28 0,1,1 386.49,407.6Z" /> + android:pathData="M125.51,407.6A28,28 0,1,1 125.51,463.6A28,28 0,1,1 125.51,407.6Z" /> + android:pathData="M44.87,159.4A28,28 0,1,1 44.87,215.4A28,28 0,1,1 44.87,159.4Z" /> diff --git a/amethyst/src/main/res/drawable/amethyst_service6.xml b/amethyst/src/main/res/drawable/amethyst_service6.xml index be905d5e97..ae5b988830 100644 --- a/amethyst/src/main/res/drawable/amethyst_service6.xml +++ b/amethyst/src/main/res/drawable/amethyst_service6.xml @@ -1,7 +1,8 @@ + android:pathData="M341.914,305.892C341.712,305.278,294.605,193.111,281.649,162c-15.775,0,-49.086,0,-72.585,0c-6.896,17.055,-24.698,60.526,-38.978,95.545c7.827,18.67,14.855,35.442,20.275,48.38l29.555,0a2.02,2.02 0,0,0,1.888,-2.373c-0.254,-5.968,-16.499,-27.66,-11.653,-51.125c1.292,-5.048,2.281,-10.178,3.694,-15.208a177.491,177.491 135,0,1,13.571,-35.141c0.898,-1.707,1.314,-2.473,3.382,-1.181c4.412,2.745,9.339,2.587,14.248,2.23c4.232,-0.92,6.292,-2.947,14.045,-0.887c1.534,0,3.028,0,4.596,0.14c3.727,0.294,7.219,1.181,9.46,4.555c2.443,3.687,2.837,7.856,2.443,12.117c-0.596,6.373,-0.173,12.308,5.221,16.801a58.083,58.083 0,0,0,6.06,4.04c2.675,1.718,5.906,2.465,8.198,4.846c1.354,1.402,2.153,2.9,1.564,4.898c-0.585,1.998,-2.153,2.94,-4.21,3.161c-5.21,0.567,-10.299,-0.493,-15.418,-1.111c-0.666,-0.081,-1.321,-0.132,-2.02,-0.173a11.583,11.583 0,0,1,-3.849,0.132l-1.203,0a26.961,26.961 0,0,0,-5.957,1.424c-4.828,1.759,-9.221,3.694,-14.914,3.595a4.364,4.364 0,0,0,-2.072,0.898c-4.463,3.967,-7.15,8.978,-4.916,16.297c6.119,17.096,21.82,58.337,28.569,76.139c-0.191,-12.058,-0.243,-33.647,-0.283,-44.126c26.942,0.04,68.25,0.018,71.543,0.018ZM251.093,219.689a5.696,5.795 90,1,0,11.591,0a5.696,5.795 90,1,0,-11.591,0Z" /> + android:pathData="M123.56,185.58A150,150 0,0,1,388.44,185.58" /> + android:pathData="M388.44,326.42A150,150 0,0,1,123.56,326.42" /> + android:pathData="M82.94,163.98A196,196 0,0,1,429.06,163.98" /> + android:pathData="M429.06,348.02A196,196 0,0,1,82.94,348.02" /> + android:pathData="M42.33,142.39A242,242 0,0,1,469.67,142.39" /> + android:pathData="M469.67,369.61A242,242 0,0,1,42.33,369.61" /> From 6b6ee26a88bd21bf459c6f9bbe82b08e81ffc678 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 17:57:31 +0000 Subject: [PATCH 11/14] style: maximize centred gem in the service-icon options Grow the central Amethyst gem in each motif (orbit/sync/hub/waves) to the largest size that still clears the surrounding elements. For the hub icon the spokes now start farther from the centre to give the gem room. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi --- .../src/main/res/drawable/amethyst_service3.xml | 6 +++--- .../src/main/res/drawable/amethyst_service4.xml | 6 +++--- .../src/main/res/drawable/amethyst_service5.xml | 16 ++++++++-------- .../src/main/res/drawable/amethyst_service6.xml | 6 +++--- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/amethyst/src/main/res/drawable/amethyst_service3.xml b/amethyst/src/main/res/drawable/amethyst_service3.xml index 2c28c2e25a..0937d18a76 100644 --- a/amethyst/src/main/res/drawable/amethyst_service3.xml +++ b/amethyst/src/main/res/drawable/amethyst_service3.xml @@ -1,8 +1,8 @@ + android:pathData="M384.871,330.838C384.567,329.917,313.907,161.667,294.473,115c-23.662,0,-73.63,0,-108.877,0c-10.343,25.583,-37.047,90.79,-58.468,143.318c11.74,28.006,22.282,53.163,30.412,72.57l44.332,0a3.03,3.03 0,0,0,2.831,-3.56c-0.381,-8.953,-24.749,-41.49,-17.48,-76.687c1.937,-7.573,3.422,-15.267,5.542,-22.812a266.236,266.236 135,0,1,20.356,-52.711c1.347,-2.561,1.97,-3.709,5.072,-1.772c6.618,4.118,14.008,3.88,21.371,3.345c6.347,-1.38,9.438,-4.421,21.068,-1.33c2.302,0,4.543,0,6.894,0.21c5.591,0.442,10.829,1.772,14.191,6.833c3.665,5.53,4.255,11.784,3.665,18.176c-0.894,9.56,-0.259,18.463,7.832,25.202a87.125,87.125 0,0,0,9.091,6.06c4.013,2.578,8.859,3.698,12.297,7.269c2.031,2.103,3.229,4.349,2.346,7.346c-0.878,2.997,-3.229,4.41,-6.314,4.741c-7.816,0.85,-15.449,-0.74,-23.127,-1.667c-0.999,-0.121,-1.981,-0.199,-3.03,-0.259a17.375,17.375 0,0,1,-5.773,0.199l-1.805,0a40.441,40.441 0,0,0,-8.936,2.136c-7.242,2.638,-13.832,5.542,-22.37,5.393a6.546,6.546 0,0,0,-3.107,1.347c-6.695,5.95,-10.724,13.467,-7.374,24.446c9.179,25.643,32.73,87.505,42.853,114.209c-0.287,-18.087,-0.364,-50.47,-0.425,-66.189c40.413,0.061,102.375,0.028,107.315,0.028ZM248.64,201.534a8.544,8.693 90,1,0,17.386,0a8.544,8.693 90,1,0,-17.386,0Z" /> + android:pathData="M393.097,335.615C392.774,334.635,317.604,155.646,296.929,106c-25.172,0,-78.329,0,-115.826,0c-11.004,27.216,-39.411,96.585,-62.2,152.466c12.489,29.793,23.704,56.557,32.353,77.202l47.162,0a3.224,3.224 0,0,0,3.012,-3.787c-0.405,-9.524,-26.329,-44.138,-18.596,-81.582c2.061,-8.056,3.64,-16.241,5.895,-24.268a283.23,283.23 135,0,1,21.655,-56.075c1.433,-2.724,2.096,-3.946,5.396,-1.885c7.04,4.38,14.903,4.128,22.735,3.558c6.753,-1.468,10.041,-4.703,22.413,-1.415c2.449,0,4.832,0,7.334,0.223c5.948,0.47,11.52,1.885,15.096,7.269c3.899,5.884,4.527,12.536,3.899,19.336c-0.951,10.17,-0.276,19.641,8.332,26.81a92.686,92.686 0,0,0,9.671,6.447c4.269,2.742,9.424,3.934,13.082,7.733c2.161,2.237,3.435,4.627,2.495,7.815c-0.934,3.188,-3.435,4.692,-6.717,5.044c-8.314,0.904,-16.435,-0.787,-24.603,-1.773c-1.063,-0.129,-2.108,-0.211,-3.224,-0.276a18.484,18.484 0,0,1,-6.142,0.211l-1.92,0a43.022,43.022 0,0,0,-9.506,2.272c-7.704,2.807,-14.715,5.895,-23.798,5.737a6.964,6.964 0,0,0,-3.306,1.433c-7.122,6.33,-11.409,14.327,-7.845,26.006c9.765,27.28,34.82,93.091,45.588,121.498c-0.305,-19.242,-0.388,-53.691,-0.452,-70.414c42.993,0.065,108.909,0.029,114.165,0.029ZM248.17,198.057a9.089,9.248 90,1,0,18.496,0a9.089,9.248 90,1,0,-18.496,0Z" /> + android:pathData="M363.849,318.631C363.595,317.859,304.461,177.055,288.198,138c-19.802,0,-61.619,0,-91.117,0c-8.656,21.41,-31.004,75.98,-48.93,119.94c9.825,23.437,18.647,44.491,25.451,60.732l37.101,0a2.536,2.536 0,0,0,2.37,-2.979c-0.319,-7.492,-20.712,-34.722,-14.629,-64.178c1.621,-6.337,2.864,-12.776,4.638,-19.091a222.808,222.808 135,0,1,17.035,-44.113c1.127,-2.143,1.649,-3.104,4.245,-1.483c5.538,3.446,11.723,3.247,17.885,2.799c5.312,-1.155,7.899,-3.7,17.631,-1.113c1.926,0,3.802,0,5.769,0.176c4.679,0.37,9.063,1.483,11.876,5.718c3.067,4.628,3.561,9.862,3.067,15.211c-0.748,8,-0.217,15.451,6.555,21.091a72.913,72.913 0,0,0,7.608,5.072c3.358,2.157,7.414,3.095,10.291,6.083c1.7,1.76,2.702,3.64,1.963,6.148c-0.734,2.508,-2.702,3.691,-5.284,3.968c-6.541,0.711,-12.929,-0.619,-19.354,-1.395c-0.836,-0.102,-1.658,-0.166,-2.536,-0.217a14.541,14.541 0,0,1,-4.832,0.166l-1.51,0a33.844,33.844 0,0,0,-7.478,1.788c-6.06,2.208,-11.576,4.638,-18.721,4.513a5.478,5.478 0,0,0,-2.601,1.127c-5.603,4.979,-8.975,11.271,-6.171,20.458c7.682,21.46,27.391,73.232,35.863,95.579c-0.24,-15.137,-0.305,-42.237,-0.356,-55.392c33.821,0.051,85.675,0.023,89.81,0.023ZM249.84,210.419a7.15,7.275 90,1,0,14.55,0a7.15,7.275 90,1,0,-14.55,0Z" /> + android:pathData="M256,100L256,34" /> + android:pathData="M404.36,207.79L467.13,187.4" /> + android:pathData="M347.69,382.21L386.49,435.6" /> + android:pathData="M164.31,382.21L125.51,435.6" /> + android:pathData="M107.64,207.79L44.87,187.4" /> diff --git a/amethyst/src/main/res/drawable/amethyst_service6.xml b/amethyst/src/main/res/drawable/amethyst_service6.xml index ae5b988830..7018689d70 100644 --- a/amethyst/src/main/res/drawable/amethyst_service6.xml +++ b/amethyst/src/main/res/drawable/amethyst_service6.xml @@ -1,8 +1,8 @@ + android:pathData="M366.591,320.223C366.331,319.432,305.694,175.048,289.016,135c-20.306,0,-63.186,0,-93.433,0c-8.876,21.954,-31.792,77.912,-50.174,122.989c10.075,24.033,19.121,45.622,26.098,62.276l38.044,0a2.6,2.6 0,0,0,2.43,-3.055c-0.327,-7.683,-21.239,-35.605,-15.001,-65.81c1.663,-6.499,2.937,-13.101,4.755,-19.576a228.472,228.472 135,0,1,17.468,-45.234c1.156,-2.198,1.691,-3.183,4.353,-1.52c5.679,3.533,12.021,3.33,18.34,2.87c5.447,-1.184,8.1,-3.794,18.079,-1.142c1.975,0,3.898,0,5.916,0.18c4.798,0.379,9.293,1.52,12.178,5.864c3.145,4.746,3.652,10.113,3.145,15.597c-0.767,8.204,-0.223,15.844,6.721,21.627a74.766,74.766 0,0,0,7.801,5.201c3.443,2.212,7.602,3.173,10.553,6.238c1.743,1.805,2.771,3.732,2.013,6.304c-0.753,2.572,-2.771,3.785,-5.419,4.069c-6.707,0.729,-13.258,-0.635,-19.846,-1.43c-0.857,-0.104,-1.7,-0.171,-2.6,-0.223a14.911,14.911 0,0,1,-4.954,0.171l-1.549,0a34.705,34.705 0,0,0,-7.668,1.833c-6.214,2.264,-11.87,4.755,-19.197,4.628a5.618,5.618 0,0,0,-2.667,1.156c-5.745,5.106,-9.203,11.557,-6.328,20.978c7.877,22.006,28.088,75.093,36.775,98.009c-0.246,-15.522,-0.313,-43.311,-0.365,-56.801c34.681,0.052,87.854,0.024,92.093,0.024ZM249.684,209.26a7.332,7.46 90,1,0,14.92,0a7.332,7.46 90,1,0,-14.92,0Z" /> Date: Fri, 26 Jun 2026 18:04:42 +0000 Subject: [PATCH 12/14] feat: adopt orbit-ring service notification icon; drop preview scaffolding Make the orbit design (centred gem ringed by three "server" nodes) the always-on relay-service notification icon by writing it into the existing amethyst_service drawable, so NotificationRelayService picks it up unchanged. Remove the icon bake-off scaffolding now that a design is chosen: - delete the alternative drawables (amethyst_service2..6) - delete the DEBUG-only ServiceIconPreviewNotifications helper - drop its trigger and now-unused imports from MainActivity Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Kvjst6qVHuQ8rKD3LtvTzi --- .../ServiceIconPreviewNotifications.kt | 108 ------------------ .../vitorpamplona/amethyst/ui/MainActivity.kt | 9 -- .../main/res/drawable/amethyst_service.xml | 45 ++++---- .../main/res/drawable/amethyst_service2.xml | 27 ----- .../main/res/drawable/amethyst_service3.xml | 31 ----- .../main/res/drawable/amethyst_service4.xml | 36 ------ .../main/res/drawable/amethyst_service5.xml | 66 ----------- .../main/res/drawable/amethyst_service6.xml | 58 ---------- 8 files changed, 24 insertions(+), 356 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/ServiceIconPreviewNotifications.kt delete mode 100644 amethyst/src/main/res/drawable/amethyst_service2.xml delete mode 100644 amethyst/src/main/res/drawable/amethyst_service3.xml delete mode 100644 amethyst/src/main/res/drawable/amethyst_service4.xml delete mode 100644 amethyst/src/main/res/drawable/amethyst_service5.xml delete mode 100644 amethyst/src/main/res/drawable/amethyst_service6.xml diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/ServiceIconPreviewNotifications.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/ServiceIconPreviewNotifications.kt deleted file mode 100644 index a1398ce5a0..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/ServiceIconPreviewNotifications.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.service.notifications - -// ===================================================================================== -// TEMPORARY / DEBUG-ONLY — REMOVE BEFORE MERGE. -// -// Posts one always-on-style notification per candidate small-icon so the different -// "service icon" designs can be compared side by side in the status bar / shade on a -// real device. These mimic the real NotificationRelayService notification (same -// channel, ongoing, silent, low priority) but each uses a different drawable and a -// label naming the design. -// -// To remove the whole experiment later: delete this file, the four extra drawables -// (amethyst_service2..6), and the single ServiceIconPreviewNotifications.postAll(...) -// call in MainActivity.onCreate. -// ===================================================================================== - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.os.Build -import androidx.annotation.DrawableRes -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import com.vitorpamplona.amethyst.R - -object ServiceIconPreviewNotifications { - private const val CHANNEL_ID = "service_icon_preview" - private const val BASE_ID = 920_000 - - private data class Candidate( - @DrawableRes val icon: Int, - val label: String, - ) - - private val candidates = - listOf( - Candidate(R.drawable.amethyst_service, "1 · Hollow gem outline (current)"), - Candidate(R.drawable.amethyst_service2, "2 · Solid circle, gem cut-out"), - Candidate(R.drawable.amethyst_service3, "3 · Orbit + server nodes"), - Candidate(R.drawable.amethyst_service4, "4 · Sync arrows (running)"), - Candidate(R.drawable.amethyst_service5, "5 · Hub & spoke (connected)"), - Candidate(R.drawable.amethyst_service6, "6 · Broadcast waves"), - ) - - fun postAll(context: Context) { - val manager = NotificationManagerCompat.from(context) - if (!manager.areNotificationsEnabled()) return - - ensureChannel(context) - - candidates.forEachIndexed { index, candidate -> - val notification = - NotificationCompat - .Builder(context, CHANNEL_ID) - .setContentTitle("Service icon ${index + 1}") - .setContentText(candidate.label) - .setSmallIcon(candidate.icon) - .setOngoing(true) - .setSilent(true) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .build() - - manager.notify(BASE_ID + index, notification) - } - } - - fun clearAll(context: Context) { - val manager = NotificationManagerCompat.from(context) - candidates.indices.forEach { manager.cancel(BASE_ID + it) } - } - - private fun ensureChannel(context: Context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - val channel = - NotificationChannel( - CHANNEL_ID, - "Service icon preview (debug)", - NotificationManager.IMPORTANCE_LOW, - ).apply { - description = "Temporary side-by-side preview of always-on service icons" - setShowBadge(false) - } - val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 9a594cfa42..edf9d17fbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -27,13 +27,11 @@ import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.notifications.NotificationRelayService -import com.vitorpamplona.amethyst.service.notifications.ServiceIconPreviewNotifications import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia import com.vitorpamplona.amethyst.ui.navigation.findParameterValue @@ -80,13 +78,6 @@ class MainActivity : AppCompatActivity() { Log.d("ActivityLifecycle") { "MainActivity.onCreate $this" } - // TEMPORARY / DEBUG-ONLY — REMOVE BEFORE MERGE. - // Posts side-by-side preview notifications for the candidate always-on - // service icons so they can be compared on a real device. - if (BuildConfig.DEBUG) { - ServiceIconPreviewNotifications.postAll(this) - } - setContent { StringResSetup() AmethystTheme { diff --git a/amethyst/src/main/res/drawable/amethyst_service.xml b/amethyst/src/main/res/drawable/amethyst_service.xml index 2413c3894e..4bddc5b470 100644 --- a/amethyst/src/main/res/drawable/amethyst_service.xml +++ b/amethyst/src/main/res/drawable/amethyst_service.xml @@ -3,31 +3,34 @@ ~ ~ Notification small icons are rendered by the system as flat, alpha-only ~ silhouettes, so the regular `amethyst` gem becomes an identical solid gem - ~ for both real notifications and this persistent service. To stop the - ~ service from reading as a new notification, this variant draws the gem as - ~ a hollow outline — same brand shape, clearly different at a glance. + ~ for both real notifications and this persistent service. To set the + ~ always-on service apart, this variant centres the brand gem inside a ring + ~ of three "server" nodes — signalling a background service quietly connected + ~ to a network of relays, clearly different from a one-off notification at a + ~ glance. The gem is grown to the largest size that still clears the nodes. --> - - - - + + + + + diff --git a/amethyst/src/main/res/drawable/amethyst_service2.xml b/amethyst/src/main/res/drawable/amethyst_service2.xml deleted file mode 100644 index 1840d24a7e..0000000000 --- a/amethyst/src/main/res/drawable/amethyst_service2.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - diff --git a/amethyst/src/main/res/drawable/amethyst_service3.xml b/amethyst/src/main/res/drawable/amethyst_service3.xml deleted file mode 100644 index 0937d18a76..0000000000 --- a/amethyst/src/main/res/drawable/amethyst_service3.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - diff --git a/amethyst/src/main/res/drawable/amethyst_service4.xml b/amethyst/src/main/res/drawable/amethyst_service4.xml deleted file mode 100644 index 66f38611c7..0000000000 --- a/amethyst/src/main/res/drawable/amethyst_service4.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - diff --git a/amethyst/src/main/res/drawable/amethyst_service5.xml b/amethyst/src/main/res/drawable/amethyst_service5.xml deleted file mode 100644 index f8228fbe56..0000000000 --- a/amethyst/src/main/res/drawable/amethyst_service5.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - diff --git a/amethyst/src/main/res/drawable/amethyst_service6.xml b/amethyst/src/main/res/drawable/amethyst_service6.xml deleted file mode 100644 index 7018689d70..0000000000 --- a/amethyst/src/main/res/drawable/amethyst_service6.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - From 94b99e0973387a7e3bb701e2334284787ab98beb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 18:16:42 +0000 Subject: [PATCH 13/14] 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 14/14] 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() }