From 47a70b4fa3268f24478708a4389c7c3c7b0fcdf2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 20:15:28 +0000 Subject: [PATCH 01/21] refactor: move OwnedEmojiPack to commons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure data class (only quartz + @Stable deps) — extract to commons/model/nip30CustomEmojis so Desktop/CLI/iOS can reuse it. Moves the unit test to commons commonTest and re-points callers. https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod --- .../amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt | 1 + .../ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt | 2 +- .../screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt | 2 +- .../emojipacks/list/metadata/EmojiPackMetadataViewModel.kt | 2 +- .../amethyst/commons}/model/nip30CustomEmojis/OwnedEmojiPack.kt | 2 +- .../commons}/model/nip30CustomEmojis/OwnedEmojiPackTest.kt | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip30CustomEmojis/OwnedEmojiPack.kt (96%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst => commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons}/model/nip30CustomEmojis/OwnedEmojiPackTest.kt (97%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt index 720e05e7c5..fa08d93418 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip30CustomEmojis import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt index b8db4721b3..77aaacf039 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt @@ -59,7 +59,7 @@ import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.OwnedEmojiPack +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack import com.vitorpamplona.amethyst.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.ui.components.M3ActionRow import com.vitorpamplona.amethyst.ui.components.M3ActionSection diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt index ad60f4c2b1..f6d07e8b84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt @@ -56,7 +56,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.NoteState -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.OwnedEmojiPack +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.M3ActionDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt index cb504c5a9d..4f139befb9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt @@ -31,8 +31,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.OwnedEmojiPack import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPack.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/OwnedEmojiPack.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPack.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/OwnedEmojiPack.kt index dbfc499337..d047be3754 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPack.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/OwnedEmojiPack.kt @@ -18,7 +18,7 @@ * 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.model.nip30CustomEmojis +package com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPackTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/OwnedEmojiPackTest.kt similarity index 97% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPackTest.kt rename to commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/OwnedEmojiPackTest.kt index 4edfae52e8..037fb9f70c 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPackTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/OwnedEmojiPackTest.kt @@ -18,7 +18,7 @@ * 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.model.nip30CustomEmojis +package com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import org.junit.Assert.assertEquals From 19d6c9baaa4392c3db785c96c2918f71df930512 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 20:15:36 +0000 Subject: [PATCH 02/21] refactor: move TrustProviderListDecryptionCache to commons, rename pkg to nip85TrustedAssertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the pure (quartz-only) decryption cache into commons. Converge the commons trustedAssertions package onto the quartz NIP slug 'nip85TrustedAssertions' (per migration plan §10.2), moving the existing TrustProviderListState interface + UserCardsCache with it and re-pointing all callers. https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod --- .../src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 2 +- .../amethyst/model/trustedAssertions/TrustProviderListState.kt | 3 ++- .../kotlin/com/vitorpamplona/amethyst/commons/model/User.kt | 2 +- .../TrustProviderListDecryptionCache.kt | 2 +- .../TrustProviderListState.kt | 2 +- .../UserCardsCache.kt | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions}/TrustProviderListDecryptionCache.kt (96%) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/{trustedAssertions => nip85TrustedAssertions}/TrustProviderListState.kt (95%) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/{trustedAssertions => nip85TrustedAssertions}/UserCardsCache.kt (98%) 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 af22c00ff3..70c38b208b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListS import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction +import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendStage import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSender @@ -111,7 +112,6 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState -import com.vitorpamplona.amethyst.model.trustedAssertions.TrustProviderListDecryptionCache import com.vitorpamplona.amethyst.model.trustedAssertions.TrustProviderListState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt index 324ad09ae0..2494036a77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model.trustedAssertions +import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -41,7 +42,7 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import com.vitorpamplona.amethyst.commons.model.trustedAssertions.TrustProviderListState as ITrustProviderListState +import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListState as ITrustProviderListState class TrustProviderListState( val signer: NostrSigner, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt index df04dbf8a4..fb465ea3ae 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.nip01Core.UserRelaysCache import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.UserNip05Cache import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusCache import com.vitorpamplona.amethyst.commons.model.nip56Reports.UserReportCache -import com.vitorpamplona.amethyst.commons.model.trustedAssertions.UserCardsCache +import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.UserCardsCache import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/TrustProviderListDecryptionCache.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/TrustProviderListDecryptionCache.kt index 27f21e1479..ea0c96ec3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/TrustProviderListDecryptionCache.kt @@ -18,7 +18,7 @@ * 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.model.trustedAssertions +package com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/TrustProviderListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/TrustProviderListState.kt similarity index 95% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/TrustProviderListState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/TrustProviderListState.kt index 53b197aa3b..943e01b888 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/TrustProviderListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/TrustProviderListState.kt @@ -18,7 +18,7 @@ * 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.commons.model.trustedAssertions +package com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ServiceProviderTag import kotlinx.coroutines.flow.StateFlow diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserCardsCache.kt similarity index 98% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserCardsCache.kt index 295f945397..5077850035 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/trustedAssertions/UserCardsCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserCardsCache.kt @@ -18,7 +18,7 @@ * 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.commons.model.trustedAssertions +package com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.User From d57f8c18c673663104604e718973a0116a8ea86b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 20:32:43 +0000 Subject: [PATCH 03/21] feat: first-class NIP-32 hashtag labels on posts and in the hashtag feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let users tag any post with a hashtag via a NIP-32 kind 1985 label event (using the `#t` tag-association namespace), and surface follow-labeled posts in the hashtag feed. quartz: - LabelEvent.buildHashtagLabel() + HASHTAG_NAMESPACE ("#t") and hashtagAssociations() to build/extract hashtag-association labels. commons: - Note now carries a `labels` reverse-reference map (hashtag -> labeler notes) with addLabel/removeLabel and a NoteFlowSet.labels flow, mirroring reactions/reports. amethyst: - LocalCache consumes LabelEvent, attaching hashtag labels to their target notes and re-notifying feed observers for already-cached targets. - Account.createLabelHashtagEvent/labelHashtag/consumeLabelEvent and AccountViewModel.labelWithHashtag (tracked + direct broadcast). - Overflow "⋯" menu gains an "Add hashtag" action backed by a new AddHashtagLabelDialog. - HashtagFeedFilter also accepts posts a followed user labeled with the hashtag; a new label sub-assembler subscribes to kind 1985 by `#l` and fetches missing label targets. - Hashtag feed shows an attribution banner ("#tag added by @user") above follow-labeled posts via a custom RefresheableFeedView onLoaded. https://claude.ai/code/session_019gc3FipVBcndF9fmqCCfVX --- .../vitorpamplona/amethyst/model/Account.kt | 49 ++++++ .../amethyst/model/LocalCache.kt | 42 +++++ .../ui/note/elements/AddHashtagLabelDialog.kt | 103 +++++++++++ .../amethyst/ui/note/elements/DropDownMenu.kt | 15 ++ .../amethyst/ui/screen/FeedView.kt | 14 +- .../ui/screen/loggedIn/AccountViewModel.kt | 10 ++ .../loggedIn/hashtag/HashtagFeedLoaded.kt | 160 ++++++++++++++++++ .../screen/loggedIn/hashtag/HashtagScreen.kt | 3 + .../loggedIn/hashtag/dal/HashtagFeedFilter.kt | 31 +++- .../hashtag/datasource/FilterHashtagLabels.kt | 96 +++++++++++ .../datasource/HashtagFilterAssembler.kt | 3 + .../HashtagFilterAssemblerSubscription.kt | 6 +- .../datasource/HashtagLabelSubAssembler.kt | 46 +++++ amethyst/src/main/res/values/strings.xml | 7 + .../amethyst/commons/model/Note.kt | 45 ++++- .../quartz/nip32Labeling/LabelEvent.kt | 36 ++++ .../quartz/nip32Labeling/LabelEventTest.kt | 45 +++++ 17 files changed, 705 insertions(+), 6 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddHashtagLabelDialog.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagFeedLoaded.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagLabelSubAssembler.kt 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 af22c00ff3..2fffcdc791 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -192,6 +192,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec +import com.vitorpamplona.quartz.nip32Labeling.LabelEvent import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent @@ -741,6 +742,54 @@ class Account( cache.justConsumeMyOwnEvent(event) } + /** + * NIP-32: tags [note] with [hashtag] by publishing a kind 1985 label event using the + * `#t` tag-association namespace. Fire-and-forget; signs and broadcasts immediately. + */ + suspend fun labelHashtag( + note: Note, + hashtag: String, + ) { + createLabelHashtagEvent(note, hashtag)?.let { (event, relays) -> + cache.justConsumeMyOwnEvent(event) + client.publish(event, relays) + } + } + + /** + * Builds and signs a NIP-32 hashtag label event for [note] without sending it. + * Returns the signed event and target relays for tracked broadcasting, or null if + * the account can't write or the note has no underlying event. + */ + suspend fun createLabelHashtagEvent( + note: Note, + hashtag: String, + ): Pair>? { + if (!signer.isWriteable()) return null + + val eventHint = note.toEventHint() ?: return null + + val template = + LabelEvent.buildHashtagLabel( + labeledEventId = eventHint.event.id, + labeledEventRelay = eventHint.relay?.url, + labeledEventAuthor = eventHint.event.pubKey, + hashtag = hashtag, + ) + + val event = signer.sign(template) + val relays = computeRelayListToBroadcast(event) + + return event to relays + } + + /** + * Consumes a label event into local cache. Called when tracked broadcasting succeeds. + */ + fun consumeLabelEvent(event: Event) { + cache.justConsumeMyOwnEvent(event) + } + suspend fun createZapRequestFor( event: Event, pollOption: Int?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 47ec75f681..f7847f225a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -143,6 +143,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip32Labeling.LabelEvent import com.vitorpamplona.quartz.nip34Git.grasp.UserGraspListEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent @@ -1525,6 +1526,43 @@ object LocalCache : ILocalCache, ICacheProvider { return false } + fun consume( + event: LabelEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val note = getOrCreateNote(event.id) + + // Already processed this event. + if (note.event != null) return true + + if (wasVerified || justVerify(event)) { + val author = getOrCreateUser(event.pubKey) + val repliesTo = computeReplyTo(event) + + note.loadEvent(event, author, repliesTo) + + // Attach NIP-32 hashtag labels (namespace #t) to the labeled events so the + // hashtag feed can surface posts tagged by a follow and attribute the labeler. + val hashtags = event.hashtagAssociations() + if (hashtags.isNotEmpty()) { + event.labeledEvents().mapNotNull { checkGetOrCreateNote(it) }.forEach { target -> + hashtags.forEach { hashtag -> target.addLabel(hashtag, note) } + + // If the labeled post is already in cache, re-notify feed observers so the + // hashtag feed can pick it up now that a (possibly followed) user labeled it. + if (target.event != null) refreshNewNoteObservers(target) + } + } + + refreshNewNoteObservers(note) + + return true + } + + return false + } + fun consume( event: ReportEvent, relay: NormalizedRelayUrl?, @@ -3730,6 +3768,10 @@ object LocalCache : ILocalCache, ICacheProvider { consume(event, relay, wasVerified) } + is LabelEvent -> { + consume(event, relay, wasVerified) + } + is ContactCardEvent -> { consume(event, relay, wasVerified) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddHashtagLabelDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddHashtagLabelDialog.kt new file mode 100644 index 0000000000..cfde82f681 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddHashtagLabelDialog.kt @@ -0,0 +1,103 @@ +/* + * 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.ui.note.elements + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes + +/** + * NIP-32: lets the user tag any post with a hashtag by publishing a kind 1985 label + * event (under the `#t` tag-association namespace). The label is public so that the + * author's and the labeler's followers can discover the post in the hashtag feed. + */ +@Composable +fun AddHashtagLabelDialog( + note: Note, + accountViewModel: AccountViewModel, + onDismiss: () -> Unit, +) { + var hashtag by remember { mutableStateOf("") } + + // Strip the leading '#', drop whitespace and lowercase so the stored label matches the + // hashtag-feed convention. A blank result disables the confirm button. + val sanitized = + hashtag + .trim() + .removePrefix("#") + .filterNot { it.isWhitespace() } + .lowercase() + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringRes(R.string.add_hashtag_label_title)) }, + text = { + Column { + Text(stringRes(R.string.add_hashtag_label_explainer)) + OutlinedTextField( + value = hashtag, + onValueChange = { hashtag = it }, + singleLine = true, + label = { Text(stringRes(R.string.add_hashtag_label_field)) }, + prefix = { Text("#") }, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + }, + confirmButton = { + TextButton( + enabled = sanitized.isNotEmpty(), + onClick = { + accountViewModel.labelWithHashtag(note, sanitized) + onDismiss() + }, + ) { + Text(stringRes(R.string.add_hashtag_label_confirm)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringRes(R.string.cancel)) + } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index cea9f92a61..fd4d2b5695 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -115,6 +115,7 @@ fun NoteDropDownMenu( nav: INav, ) { var reportDialogShowing by remember { mutableStateOf(false) } + var addLabelDialogShowing by remember { mutableStateOf(false) } val state by observeBookmarksFollowsAndAccount(note, accountViewModel).collectAsStateWithLifecycle( DropDownParams( @@ -286,6 +287,9 @@ fun NoteDropDownMenu( } } } + M3ActionRow(icon = MaterialSymbols.Tag, text = stringRes(R.string.add_hashtag_label)) { + addLabelDialogShowing = true + } // Pick exactly one curation flow per kind: music tracks go to playlists, emoji // packs go to the emoji list, everything else gets the standard bookmark rows. // Showing both at once is noisy and makes "bookmark" feel like the catch-all when @@ -387,6 +391,17 @@ fun NoteDropDownMenu( onDismiss() } } + + if (addLabelDialogShowing) { + AddHashtagLabelDialog( + note = note, + accountViewModel = accountViewModel, + onDismiss = { + addLabelDialogShowing = false + onDismiss() + }, + ) + } } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index 7e878a2bce..3089f9ad8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -51,10 +51,22 @@ fun RefresheableFeedView( scrollStateKey: String? = null, accountViewModel: AccountViewModel, nav: INav, + onLoaded: (@Composable (FeedState.Loaded, LazyListState) -> Unit)? = null, ) { RefresheableBox(viewModel, enablePullRefresh) { SaveableFeedState(viewModel.feedState, scrollStateKey) { listState -> - RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead) + if (onLoaded != null) { + RenderFeedState( + viewModel, + accountViewModel, + listState, + nav, + routeForLastRead, + onLoaded = { onLoaded(it, listState) }, + ) + } else { + RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 1f4d7ba6c7..b50e91abd9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1068,6 +1068,16 @@ class AccountViewModel( direct = { account.removeBookmark(note, false) }, ) + /** NIP-32: tags [note] with [hashtag] by publishing a kind 1985 label event. */ + fun labelWithHashtag( + note: Note, + hashtag: String, + ) = launchTrackedOrDirect( + createTracked = { account.createLabelHashtagEvent(note, hashtag) }, + consumeTracked = account::consumeLabelEvent, + direct = { account.labelHashtag(note, hashtag) }, + ) + fun removeDeletedBookmarks( deletedEventIds: Set, deletedAddresses: Set
, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagFeedLoaded.kt new file mode 100644 index 0000000000..62aeae683b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagFeedLoaded.kt @@ -0,0 +1,160 @@ +/* + * 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.ui.screen.loggedIn.hashtag + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding +import com.vitorpamplona.amethyst.ui.theme.Size16Modifier +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash + +/** + * Hashtag feed renderer that adds an attribution banner above any post that a followed user + * surfaced through a NIP-32 hashtag label (rather than the author tagging it directly). + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun HashtagFeedLoaded( + tag: String, + loaded: FeedState.Loaded, + listState: LazyListState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val items by loaded.feed.collectAsStateWithLifecycle() + + LazyColumn( + contentPadding = rememberFeedContentPadding(FeedPadding), + state = listState, + ) { + itemsIndexed( + items.list, + key = { _, item -> item.idHex }, + contentType = { _, item -> item.event?.kind ?: -1 }, + ) { _, item -> + Column(Modifier.fillMaxWidth().animateItem()) { + HashtagLabelAttribution(item, tag, accountViewModel) + + NoteCompose( + item, + modifier = Modifier.fillMaxWidth(), + routeForLastRead = null, + isBoostedNote = false, + isHiddenFeed = items.showHidden, + quotesLeft = 3, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + HorizontalDivider( + thickness = DividerThickness, + ) + } + } +} + +/** + * Shows "🏷 #tag added by " when a followed user labeled this post with the hashtag + * and the author didn't include it themselves. Renders nothing otherwise. + */ +@Composable +fun HashtagLabelAttribution( + note: Note, + tag: String, + accountViewModel: AccountViewModel, +) { + val labelState by note + .flow() + .labels.stateFlow + .collectAsStateWithLifecycle() + + // The author already used the hashtag — nothing to attribute. + if (note.event?.isTaggedHash(tag) == true) return + + val follows = accountViewModel.account.followingKeySet() + val labeler = + labelState.note.labels[tag.lowercase()] + ?.firstOrNull { it.author?.pubkeyHex in follows } + ?.author ?: return + + Row( + modifier = Modifier.fillMaxWidth().then(HalfStartPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + symbol = MaterialSymbols.Tag, + contentDescription = null, + modifier = Size16Modifier, + tint = MaterialTheme.colorScheme.placeholderText, + ) + Spacer(StdHorzSpacer) + Text( + "#$tag", + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + ) + Spacer(StdHorzSpacer) + Text( + stringRes(R.string.hashtag_label_added_by), + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + ) + Spacer(StdHorzSpacer) + UsernameDisplay( + labeler, + fontWeight = FontWeight.Bold, + textColor = MaterialTheme.colorScheme.placeholderText, + accountViewModel = accountViewModel, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt index 9124536352..10900d7bc6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt @@ -113,6 +113,9 @@ fun HashtagScreen( null, accountViewModel = accountViewModel, nav = nav, + onLoaded = { state, listState -> + HashtagFeedLoaded(tag.hashtag, state, listState, accountViewModel, nav) + }, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt index a3f47452e9..b5eb204f25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent @@ -53,9 +54,10 @@ class HashtagFeedFilter( override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + tag override fun feed(): List { + val follows = account.followingKeySet() val notes = cache.notes.filterIntoSet { _, it -> - acceptableEvent(it, tag) + acceptableEvent(it, tag, follows) } return sort(notes) @@ -63,16 +65,39 @@ class HashtagFeedFilter( override fun applyFilter(newItems: Set): Set = innerApplyFilter(newItems) - private fun innerApplyFilter(collection: Collection): Set = collection.filterTo(HashSet()) { acceptableEvent(it, tag) } + private fun innerApplyFilter(collection: Collection): Set { + val follows = account.followingKeySet() + return collection.filterTo(HashSet()) { acceptableEvent(it, tag, follows) } + } fun acceptableEvent( it: Note, hashTag: String, + follows: Set, ): Boolean = - (acceptableViaHashtag(it.event, hashTag) || acceptableViaScope(it.event, hashTag)) && + ( + acceptableViaHashtag(it.event, hashTag) || + acceptableViaScope(it.event, hashTag) || + acceptableViaFollowLabel(it, hashTag, follows) + ) && !it.isHiddenFor(account.hiddenUsers.flow.value) && account.isAcceptable(it) + /** + * NIP-32: accept a post that a followed user has tagged with this hashtag via a kind + * 1985 label event (namespace `#t`), even if the post's author never used the hashtag. + * Requires the target to actually have an event so it can render. + */ + fun acceptableViaFollowLabel( + note: Note, + hashTag: String, + follows: Set, + ): Boolean { + if (note.event == null) return false + val labelers = note.labels[hashTag.lowercase()] ?: return false + return labelers.any { it.author?.pubkeyHex in follows } + } + fun acceptableViaHashtag( event: Event?, hashTag: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt new file mode 100644 index 0000000000..2d4bf16af0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt @@ -0,0 +1,96 @@ +/* + * 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.ui.screen.loggedIn.hashtag.datasource + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip32Labeling.LabelEvent +import com.vitorpamplona.quartz.utils.mapOfSet + +/** + * Builds the relay filters that power the NIP-32 side of the hashtag feed: + * + * 1. A subscription for kind 1985 label events that tag posts with this hashtag + * (the `l` tag under the `#t` namespace). We don't restrict authors at the relay + * — the feed restricts display to the user's follows locally — but we do bound it + * with a limit. + * 2. Requests for the underlying posts a *followed* user has labeled but that aren't in + * cache yet, so they can actually render in the feed. This rotates as new label + * events arrive (the sub-assembler re-runs after each EOSE). + */ +fun filterHashtagLabels( + account: Account, + hashtag: String, + relays: Set, + since: SincePerRelayMap?, +): List { + val labelValues = hashtagAlts(hashtag).sorted() + + val labelFilters = + relays.map { relay -> + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = listOf(LabelEvent.KIND), + tags = mapOf("l" to labelValues), + limit = 200, + since = since?.get(relay)?.time, + ), + ) + } + + val follows = account.followingKeySet() + + // Collect the posts that a followed user labeled with this hashtag but that we don't + // have the full event for yet, and ask their likely relays for them. + val labelNotes = + LocalCache.notes.filterIntoSet { _, note -> + val noteEvent = note.event + noteEvent is LabelEvent && + noteEvent.pubKey in follows && + hashtag in noteEvent.hashtagAssociations() + } + + val missingTargets = + mapOfSet { + labelNotes.forEach { labelNote -> + (labelNote.event as LabelEvent).labeledEvents().forEach { targetId -> + val target = LocalCache.getNoteIfExists(targetId) + if (target?.event == null) { + val targetNote = LocalCache.getOrCreateNote(targetId) + potentialRelaysToFindEvent(targetNote).ifEmpty { relays }.forEach { relayUrl -> + add(relayUrl, targetId) + } + } + } + } + } + + return labelFilters + filterMissingEvents(missingTargets) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt index 55a481ed2e..d394b588d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -28,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class HashtagQueryState( val hashtag: String, val relays: Set, + val account: Account, ) { val lowercaseHashtag = hashtag.lowercase() } @@ -38,6 +40,7 @@ class HashtagFilterAssembler( val group = listOf( HashtagFeedFilterSubAssembler(client, ::allKeys), + HashtagLabelSubAssembler(client, ::allKeys), ) override fun invalidateKeys() = invalidateFilters() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt index 0075f5d49c..54d0e69a0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt @@ -35,7 +35,11 @@ fun HashtagFilterAssemblerSubscription( // even if they are tracking the same tag. val state = remember(tag) { - HashtagQueryState(tag.hashtag, accountViewModel.account.followOutboxesOrProxy.flow.value) + HashtagQueryState( + tag.hashtag, + accountViewModel.account.followOutboxesOrProxy.flow.value, + accountViewModel.account, + ) } LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().hashtags) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagLabelSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagLabelSubAssembler.kt new file mode 100644 index 0000000000..c84cf69895 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagLabelSubAssembler.kt @@ -0,0 +1,46 @@ +/* + * 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.ui.screen.loggedIn.hashtag.datasource + +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter + +/** + * NIP-32: streams kind 1985 label events that tag posts with the screen's hashtag and + * pulls in the posts a followed user labeled. Rotates after each EOSE (`invalidateAfterEose`) + * so newly-arrived label events get their target posts fetched on the next pass. + */ +class HashtagLabelSubAssembler( + client: INostrClient, + allKeys: () -> Set, +) : PerUniqueIdEoseManager(client, allKeys, invalidateAfterEose = true) { + override fun updateFilter( + key: HashtagQueryState, + since: SincePerRelayMap?, + ): List = filterHashtagLabels(key.account, key.lowercaseHashtag, key.relays, since) + + /** + * Only one key per hashtag. Prefixed so it doesn't collide with the main feed sub-assembler. + */ + override fun id(key: HashtagQueryState) = "label-" + key.lowercaseHashtag +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 01d3b1c9ba..cf1d12b9e4 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -806,6 +806,13 @@ Remove from Private Bookmarks Remove from Public Bookmarks + Add hashtag + Add a hashtag + Publicly tag this post with a hashtag (NIP-32 label). People who follow you will see it in that hashtag\'s feed. + Hashtag + Add + added by + Pinned Notes Your pinned notes Pin to Profile diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt index 9179858e50..ebe9e27d02 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt @@ -145,6 +145,7 @@ open class Note( removeZap(note) removeZapPayment(note) removeReport(note) + removeLabel(note) } var poll: PollResponsesCache? = null @@ -166,6 +167,16 @@ open class Note( var reports = mapOf>() private set + /** + * NIP-32 hashtag labels (kind 1985) targeting this note. + * Key: hashtag value, lowercased (the `l` tag value under the `#t` namespace). + * Value: the list of LabelEvent notes that applied that hashtag to this note, + * so `source.author` identifies who labeled it. Used by the hashtag feed to + * surface posts a follow has tagged and to attribute the label in the UI. + */ + var labels = mapOf>() + private set + var zaps = mapOf() private set @@ -366,12 +377,14 @@ open class Note( val zapsChanged = zaps.isNotEmpty() || zapPayments.isNotEmpty() || onchainZaps.isNotEmpty() || nutzaps.isNotEmpty() val boostsChanged = boosts.isNotEmpty() val reportsChanged = reports.isNotEmpty() + val labelsChanged = labels.isNotEmpty() val toBeRemoved = replies + reactions.values.flatten() + boosts + reports.values.flatten() + + labels.values.flatten() + zaps.keys + zaps.values.filterNotNull() + zapPayments.keys + @@ -382,6 +395,7 @@ open class Note( reactions = mapOf() boosts = listOf() reports = mapOf() + labels = mapOf() zaps = mapOf() onchainZaps = mapOf() onchainZapResolved = false @@ -394,6 +408,7 @@ open class Note( if (reactionsChanged) flowSet?.reactions?.invalidateData() if (boostsChanged) flowSet?.boosts?.invalidateData() if (reportsChanged) flowSet?.reports?.invalidateData() + if (labelsChanged) flowSet?.labels?.invalidateData() if (zapsChanged) flowSet?.zaps?.invalidateData() return toBeRemoved @@ -668,6 +683,32 @@ open class Note( } } + /** Attach a NIP-32 LabelEvent note as having tagged this note with [hashtag] (lowercased). */ + fun addLabel( + hashtag: String, + note: Note, + ) { + val listOfLabelers = labels[hashtag] + if (listOfLabelers == null) { + labels = labels + Pair(hashtag, listOf(note)) + flowSet?.labels?.invalidateData() + } else if (!listOfLabelers.contains(note)) { + labels = labels + Pair(hashtag, listOfLabelers + note) + flowSet?.labels?.invalidateData() + } + } + + /** Detach a LabelEvent note (e.g. deleted) from every hashtag bucket it was in. */ + fun removeLabel(note: Note) { + if (labels.none { it.value.contains(note) }) return + + labels = + labels + .mapValues { it.value - note } + .filterValues { it.isNotEmpty() } + flowSet?.labels?.invalidateData() + } + fun addRelaySync(relay: NormalizedRelayUrl) = syncLock.withLock { if (relay !in relays) { @@ -1239,6 +1280,7 @@ class NoteFlowSet( val zaps = NoteBundledRefresherFlow(u) val ots = NoteBundledRefresherFlow(u) val edits = NoteBundledRefresherFlow(u) + val labels = NoteBundledRefresherFlow(u) @OptIn(ExperimentalCoroutinesApi::class) fun author() = @@ -1257,7 +1299,8 @@ class NoteFlowSet( replies.hasObservers() || zaps.hasObservers() || ots.hasObservers() || - edits.hasObservers() + edits.hasObservers() || + labels.hasObservers() } @Stable diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEvent.kt index 9615773a04..853d82dac9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEvent.kt @@ -82,6 +82,16 @@ class LabelEvent( /** Labels filtered by a specific namespace. */ fun labelsByNamespace(namespace: String) = labels().filter { it.namespace == namespace } + /** + * Hashtags this event associates with its targets, via the NIP-32 tag-association + * namespace `#t` (`["L", "#t"]` + `["l", "", "#t"]`). Values are returned + * lowercased so callers can compare without worrying about case. + */ + fun hashtagAssociations() = + labelsByNamespace(HASHTAG_NAMESPACE) + .map { it.label.lowercase() } + .distinct() + /** Referenced event IDs (label targets). */ fun labeledEvents() = tags.mapNotNull(ETag::parseId) @@ -104,6 +114,32 @@ class LabelEvent( const val KIND = 1985 const val ALT = "Label event" + /** + * NIP-32 tag-association namespace for hashtags. A label of the form + * `["l", "", "#t"]` (with `["L", "#t"]`) associates the target + * with the hashtag `` under the standard `t` tag. + */ + const val HASHTAG_NAMESPACE = "#t" + + /** + * Build a label event that tags an existing event with a hashtag, using the + * NIP-32 tag-association namespace `#t`. The hashtag is stored lowercased so + * follow-graph hashtag feeds match regardless of input case. + */ + fun buildHashtagLabel( + labeledEventId: HexKey, + labeledEventRelay: String? = null, + labeledEventAuthor: HexKey? = null, + hashtag: String, + createdAt: Long = TimeUtils.now(), + ) = buildEventLabel( + labeledEventId = labeledEventId, + labeledEventRelay = labeledEventRelay, + labeledEventAuthor = labeledEventAuthor, + labels = listOf(LabelTag(hashtag.removePrefix("#").lowercase(), HASHTAG_NAMESPACE)), + createdAt = createdAt, + ) + /** * Build a label event for labeling events. */ diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEventTest.kt index 7ed37345ff..7365ae4157 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEventTest.kt @@ -160,6 +160,51 @@ class LabelEventTest { assertEquals("en", languageLabels[0].label) } + @Test + fun testBuildHashtagLabel() { + val template = + LabelEvent.buildHashtagLabel( + labeledEventId = testEventId, + labeledEventAuthor = testPubKey, + hashtag = "#PhotoGraphy", + ) + + assertEquals(1985, template.kind) + + val hasEventRef = template.tags.any { it[0] == "e" && it[1] == testEventId } + assertTrue(hasEventRef, "Should have e tag targeting labeled event") + + val hasNamespace = template.tags.any { it[0] == "L" && it[1] == "#t" } + assertTrue(hasNamespace, "Should have the #t tag-association namespace") + + // The leading '#' is stripped and the value is lowercased. + val hasLabel = template.tags.any { it[0] == "l" && it[1] == "photography" && it[2] == "#t" } + assertTrue(hasLabel, "Should have a lowercased l label tag under #t") + } + + @Test + fun testHashtagAssociations() { + val event = + EventFactory.create( + id = testEventId, + pubKey = testPubKey, + createdAt = 1234567890L, + kind = 1985, + tags = + arrayOf( + arrayOf("L", "#t"), + arrayOf("l", "Bitcoin", "#t"), + arrayOf("l", "nostr", "#t"), + arrayOf("l", "MIT", "license"), + arrayOf("e", testEventId), + ), + content = "", + sig = "c".repeat(128), + ) + + assertEquals(listOf("bitcoin", "nostr"), event.hashtagAssociations()) + } + @Test fun testLabeledTargets() { val event = From b0f9b1621a28c6e70ced13eb53ae0502f814d89e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 20:37:04 +0000 Subject: [PATCH 04/21] refactor: move CashuToken to commons/model/nip60Cashu CashuToken and Proof are pure data classes (only @Immutable + kotlinx.serialization). Extract to commons so Desktop/CLI/iOS can reuse them, and re-point all callers. https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod --- .../androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt | 2 +- .../vitorpamplona/amethyst/service/cashu/CachedCashuParser.kt | 1 + .../com/vitorpamplona/amethyst/service/cashu/CashuParser.kt | 1 + .../amethyst/service/cashu/melt/MeltProcessor.kt | 2 +- .../vitorpamplona/amethyst/service/cashu/melt/MeltResult.kt | 2 +- .../com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt | 2 +- .../com/vitorpamplona/amethyst/service/cashu/v3/V3Token.kt | 2 +- .../com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt | 4 ++-- .../com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt | 2 +- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 2 +- .../amethyst/commons/model/nip60Cashu}/CashuToken.kt | 2 +- 11 files changed, 12 insertions(+), 10 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip60Cashu}/CashuToken.kt (96%) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt index 22854e5cc8..3c2d30ed82 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken import com.vitorpamplona.amethyst.service.cashu.CashuParser -import com.vitorpamplona.amethyst.service.cashu.CashuToken import com.vitorpamplona.amethyst.ui.components.GenericLoadable import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.runBlocking diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CachedCashuParser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CachedCashuParser.kt index 000e1b5f2c..887e7f9c6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CachedCashuParser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CachedCashuParser.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.cashu import android.util.LruCache +import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken import com.vitorpamplona.amethyst.ui.components.GenericLoadable import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt index ad5c379d6d..cb76b27bcb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.cashu +import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken import com.vitorpamplona.amethyst.service.cashu.v3.V3Parser import com.vitorpamplona.amethyst.service.cashu.v4.V4Parser import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltProcessor.kt index 6ea4f28bff..88a0d471c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltProcessor.kt @@ -24,7 +24,7 @@ import android.content.Context import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.service.cashu.CashuToken +import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.utils.asTextOrNull diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltResult.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltResult.kt index 821f4f23ba..d700a6dd1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltResult.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltResult.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.cashu.melt -import com.vitorpamplona.amethyst.service.cashu.CashuToken +import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken class MeltResult( val token: CashuToken, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt index 88e2f4c57b..1e7c4b98f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.cashu.v3 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import com.vitorpamplona.amethyst.service.cashu.CashuToken +import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken import com.vitorpamplona.amethyst.ui.components.GenericLoadable import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Token.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Token.kt index e9118e6c88..07a49a98a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Token.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Token.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.cashu.v3 -import com.vitorpamplona.amethyst.service.cashu.Proof +import com.vitorpamplona.amethyst.commons.model.nip60Cashu.Proof import kotlinx.serialization.Serializable @Serializable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt index 2fb2fb60d2..c5b29f2683 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.cashu.v4 -import com.vitorpamplona.amethyst.service.cashu.CashuToken -import com.vitorpamplona.amethyst.service.cashu.Proof +import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken +import com.vitorpamplona.amethyst.commons.model.nip60Cashu.Proof import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.quartz.nip01Core.core.toHexKey import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt index 36f29b00c5..d97c2b38e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt @@ -55,8 +55,8 @@ import androidx.core.net.toUri import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.Cashu import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons +import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken import com.vitorpamplona.amethyst.service.cashu.CachedCashuParser -import com.vitorpamplona.amethyst.service.cashu.CashuToken import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.util.setText import com.vitorpamplona.amethyst.ui.note.CopyIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 1f4d7ba6c7..d9bf66998c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuToken import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.commons.nipACWebRtcCalls.CallManager import com.vitorpamplona.amethyst.commons.tor.TorType @@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilde import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.broadcast.BroadcastTracker -import com.vitorpamplona.amethyst.service.cashu.CashuToken import com.vitorpamplona.amethyst.service.cashu.melt.MeltProcessor import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuToken.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip60Cashu/CashuToken.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuToken.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip60Cashu/CashuToken.kt index dc921de6d9..b8fe8e3f96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuToken.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip60Cashu/CashuToken.kt @@ -18,7 +18,7 @@ * 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.cashu +package com.vitorpamplona.amethyst.commons.model.nip60Cashu import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable From 5d629faa05c3412c6c1aedf6bc2b8ce05c6a60d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 20:37:12 +0000 Subject: [PATCH 05/21] refactor: dedup EmojiUtils against commons util amethyst/service/EmojiUtils duplicates commons/util/EmojiUtils (identical logic; commons uses the KMP codepoint helpers instead of JVM Character calls). Delete the duplicate and re-point callers. https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod --- .../amethyst/service/EmojiUtils.kt | 135 ------------------ .../ui/note/UpdateReactionTypeDialog.kt | 2 +- .../loggedIn/notifications/CardFeedState.kt | 2 +- .../com/vitorpamplona/amethyst/CharsetTest.kt | 2 +- 4 files changed, 3 insertions(+), 138 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt deleted file mode 100644 index 010b382ada..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/EmojiUtils.kt +++ /dev/null @@ -1,135 +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 - -import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists - -fun String.isUTF16Char(pos: Int): Boolean = Character.charCount(this.codePointAt(pos)) == 2 - -fun String.firstFullCharOld(): String = - when (this.length) { - 0, 1 -> { - this - } - - 2, 3 -> { - if (isUTF16Char(0)) this.take(2) else this.take(1) - } - - else -> { - val first = isUTF16Char(0) - val second = isUTF16Char(2) - if (first && second) { - this.take(4) - } else if (first) { - this.take(2) - } else { - this.take(1) - } - } - } - -fun String.firstFullChar(): String { - var isInJoin = false - var hasHadSecondChance = false - var start = 0 - var previousCharLength = 0 - var next: Int - var codePoint: Int - - var i = 0 - - while (i < this.length) { - codePoint = codePointAt(i) - - // Skips if it starts with the join char 0x200D - if (codePoint == 0x200D && previousCharLength == 0) { - next = offsetByCodePoints(i, 1) - start = next - } else { - // If join, searches for the next char - if (codePoint == 0xFE0F) { - } else if (codePoint == 0x200D) { - isInJoin = true - } else { - // stops when two chars are not joined together - if (previousCharLength > 0 && !isInJoin) { - if (Character.charCount(codePoint) == 1 || hasHadSecondChance) { - break - } else { - hasHadSecondChance = true - } - } else { - hasHadSecondChance = false - } - - isInJoin = false - } - - // next char to evaluate - next = offsetByCodePoints(i, 1) - previousCharLength += (next - i) - } - - i = next - } - - // if ends in join, then seachers backwards until a char is found. - if (isInJoin) { - i = previousCharLength - 1 - while (i > 0) { - if (this[i].code == 0x200D) { - previousCharLength -= 1 - } else { - break - } - - i -= 1 - } - } - - return substring(start, start + previousCharLength) -} - -fun String.firstFullCharOrEmoji(tags: ImmutableListOfLists): String { - if (length <= 2) { - return firstFullChar() - } - - if (this[0] == ':') { - // makes sure an emoji exists - val emojiParts = this.split(":", limit = 3) - if (emojiParts.size >= 2) { - val emojiName = emojiParts[1] - val emojiUrl = tags.lists.firstOrNull { it.size > 1 && it[1] == emojiName }?.getOrNull(2) - if (emojiUrl != null) { - return ":$emojiName:$emojiUrl" - } - } - } - - if (EmojiCoder.isCoded(this)) { - return EmojiCoder.cropToFirstMessage(this) - } - - return firstFullChar() -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index d51dd4a7a9..fd71504495 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -73,9 +73,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder +import com.vitorpamplona.amethyst.commons.util.firstFullChar import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.service.firstFullChar import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt index 3223aab655..99125a720f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.commons.ui.notifications.Card +import com.vitorpamplona.amethyst.commons.util.firstFullCharOrEmoji import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/CharsetTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/CharsetTest.kt index fb2b8ca31c..86ee1985f7 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/CharsetTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/CharsetTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst -import com.vitorpamplona.amethyst.service.firstFullChar +import com.vitorpamplona.amethyst.commons.util.firstFullChar import org.junit.Assert import org.junit.Test From 6eee10001070ca99d96ed0cba6a8dc03d2a43f81 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 20:37:20 +0000 Subject: [PATCH 06/21] refactor: dedup CountFormatter against commons util amethyst/service/CountFormatter.countToHumanReadable(counter, str) duplicates the identical 2-arg overloads already in commons/util/NumberFormatters. Delete the duplicate and re-point the two relay-row callers. https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod --- .../amethyst/service/CountFormatter.kt | 43 ------------------- .../relays/common/RelayEventCountRow.kt | 2 +- .../loggedIn/relays/common/RelayStatusRow.kt | 2 +- 3 files changed, 2 insertions(+), 45 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/CountFormatter.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CountFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CountFormatter.kt deleted file mode 100644 index ea92e3f00e..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CountFormatter.kt +++ /dev/null @@ -1,43 +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 - -import kotlin.math.roundToInt - -fun countToHumanReadable( - counter: Int, - str: String, -) = when { - counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()}G $str" - counter >= 1000000 -> "${(counter / 1000000f).roundToInt()}M $str" - counter >= 1000 -> "${(counter / 1000f).roundToInt()}K $str" - else -> "$counter $str" -} - -fun countToHumanReadable( - counter: Long, - str: String, -) = when { - counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()}G $str" - counter >= 1000000 -> "${(counter / 1000000f).roundToInt()}M $str" - counter >= 1000 -> "${(counter / 1000f).roundToInt()}K $str" - else -> "$counter $str" -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt index 1ffa1a1fe5..151796cd82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.service.countToHumanReadable +import com.vitorpamplona.amethyst.commons.util.countToHumanReadable import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font10SP import com.vitorpamplona.amethyst.ui.theme.Size10Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt index 3443bc9b80..fdca99ef1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.service.countToHumanReadable +import com.vitorpamplona.amethyst.commons.util.countToHumanReadable import com.vitorpamplona.amethyst.service.countToHumanReadableBytes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel From 789dd4f02bfbd4c1005bc6c29f9724a112ea09c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 20:52:12 +0000 Subject: [PATCH 07/21] refactor: dedup ByteFormatter against commons util amethyst/service/ByteFormatter duplicates commons/util/countToHumanReadableBytes. Adopt the commons version as canonical (it appends a " B" unit suffix for sub-1000 byte counts; the amethyst copy returned a bare number) and re-point the three callers. https://claude.ai/code/session_01H66WwvUYm5KtAWBLgUMcod --- .../amethyst/service/ByteFormatter.kt | 39 ------------------- .../amethyst/ui/note/types/Torrent.kt | 2 +- .../amethyst/ui/note/types/TorrentComment.kt | 2 +- .../loggedIn/relays/common/RelayStatusRow.kt | 2 +- 4 files changed, 3 insertions(+), 42 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/ByteFormatter.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ByteFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ByteFormatter.kt deleted file mode 100644 index d5e097440d..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ByteFormatter.kt +++ /dev/null @@ -1,39 +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 - -import kotlin.math.roundToInt - -fun countToHumanReadableBytes(counter: Int) = - when { - counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()} GB" - counter >= 1000000 -> "${(counter / 1000000f).roundToInt()} MB" - counter >= 1000 -> "${(counter / 1000f).roundToInt()} KB" - else -> "$counter" - } - -fun countToHumanReadableBytes(counter: Long) = - when { - counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()} GB" - counter >= 1000000 -> "${(counter / 1000000f).roundToInt()} MB" - counter >= 1000 -> "${(counter / 1000f).roundToInt()} KB" - else -> "$counter" - } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt index de4f5c3ed2..69deaf4b22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt @@ -49,9 +49,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.countToHumanReadableBytes import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt index e86374d0b6..55bcc1968e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt @@ -45,9 +45,9 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.countToHumanReadableBytes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.LoadNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt index fdca99ef1b..b444354b05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.util.countToHumanReadable -import com.vitorpamplona.amethyst.service.countToHumanReadableBytes +import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes From 8e7d169198c40f7ca6954e992e504206c581c560 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 20:59:19 +0000 Subject: [PATCH 08/21] feat: route follow hashtag-labels through each follow's outbox relays The kind-1985 label subscription for the hashtag feed now follows the NIP-65 outbox model: instead of querying the flat hashtag relay set for all labels and filtering to follows locally, it queries each follow's outbox relays for that follow's own label events (authors restricted per relay via account.followsPerRelay), tagged with the hashtag. This ensures a follow's labels are picked up even when they never reach the hashtag's own relays. https://claude.ai/code/session_019gc3FipVBcndF9fmqCCfVX --- .../hashtag/datasource/FilterHashtagLabels.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt index 2d4bf16af0..cd0a703978 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterHashtagLabels.kt @@ -31,14 +31,16 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip32Labeling.LabelEvent import com.vitorpamplona.quartz.utils.mapOfSet +import kotlin.math.min /** * Builds the relay filters that power the NIP-32 side of the hashtag feed: * - * 1. A subscription for kind 1985 label events that tag posts with this hashtag - * (the `l` tag under the `#t` namespace). We don't restrict authors at the relay - * — the feed restricts display to the user's follows locally — but we do bound it - * with a limit. + * 1. A subscription for kind 1985 label events that tag posts with this hashtag (the `l` + * tag under the `#t` namespace), routed through the NIP-65 outbox model: each follow's + * labels are requested from THAT follow's outbox relays, with the relay's filter + * restricted to the follows that actually write there. This is what guarantees we pick + * up a follow's labels even if they never reach the hashtag's own relays. * 2. Requests for the underlying posts a *followed* user has labeled but that aren't in * cache yet, so they can actually render in the feed. This rotates as new label * events arrive (the sub-assembler re-runs after each EOSE). @@ -51,15 +53,20 @@ fun filterHashtagLabels( ): List { val labelValues = hashtagAlts(hashtag).sorted() + // Outbox routing: relay -> the follows who publish to it. + val followsPerRelay = account.followsPerRelay.value val labelFilters = - relays.map { relay -> + followsPerRelay.mapNotNull { (relay, authors) -> + if (authors.isEmpty()) return@mapNotNull null + val authorList = authors.sorted() RelayBasedFilter( relay = relay, filter = Filter( kinds = listOf(LabelEvent.KIND), + authors = authorList, tags = mapOf("l" to labelValues), - limit = 200, + limit = min(authorList.size * 5, 500), since = since?.get(relay)?.time, ), ) From 3f4066c4b8daab7a4389691cb90d09aa24dd0e97 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 21:23:56 +0000 Subject: [PATCH 09/21] refactor: buildHashtagLabel takes an EventHintBundle Replace the labeledEventId/relay/author trio with a single EventHintBundle, building the e-tag via the standard EventHintBundle.toETag() idiom. The Account caller now passes the note's event hint directly. https://claude.ai/code/session_019gc3FipVBcndF9fmqCCfVX --- .../vitorpamplona/amethyst/model/Account.kt | 8 +------- .../quartz/nip32Labeling/LabelEvent.kt | 20 +++++++++---------- .../quartz/nip32Labeling/LabelEventTest.kt | 15 ++++++++++++-- 3 files changed, 24 insertions(+), 19 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 2fffcdc791..649a22afe0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -769,13 +769,7 @@ class Account( val eventHint = note.toEventHint() ?: return null - val template = - LabelEvent.buildHashtagLabel( - labeledEventId = eventHint.event.id, - labeledEventRelay = eventHint.relay?.url, - labeledEventAuthor = eventHint.event.pubKey, - hashtag = hashtag, - ) + val template = LabelEvent.buildHashtagLabel(eventHint, hashtag) val event = signer.sign(template) val relays = computeRelayListToBroadcast(event) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEvent.kt index 853d82dac9..974bfec5a0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEvent.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint @@ -34,6 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTag +import com.vitorpamplona.quartz.nip01Core.tags.events.toETag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag @@ -127,18 +129,16 @@ class LabelEvent( * follow-graph hashtag feeds match regardless of input case. */ fun buildHashtagLabel( - labeledEventId: HexKey, - labeledEventRelay: String? = null, - labeledEventAuthor: HexKey? = null, + labeledEvent: EventHintBundle, hashtag: String, createdAt: Long = TimeUtils.now(), - ) = buildEventLabel( - labeledEventId = labeledEventId, - labeledEventRelay = labeledEventRelay, - labeledEventAuthor = labeledEventAuthor, - labels = listOf(LabelTag(hashtag.removePrefix("#").lowercase(), HASHTAG_NAMESPACE)), - createdAt = createdAt, - ) + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT) + eTag(labeledEvent.toETag()) + val tag = LabelTag(hashtag.removePrefix("#").lowercase(), HASHTAG_NAMESPACE) + labelNamespace(tag.namespace) + label(tag) + } /** * Build a label event for labeling events. diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEventTest.kt index 7365ae4157..7dc80aebe1 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip32Labeling/LabelEventTest.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip32Labeling import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip32Labeling.tags.LabelTag import com.vitorpamplona.quartz.utils.EventFactory @@ -162,10 +163,20 @@ class LabelEventTest { @Test fun testBuildHashtagLabel() { + val labeledEvent = + EventFactory.create( + id = testEventId, + pubKey = testPubKey, + createdAt = 1234567890L, + kind = 1, + tags = arrayOf(), + content = "hello", + sig = "c".repeat(128), + ) + val template = LabelEvent.buildHashtagLabel( - labeledEventId = testEventId, - labeledEventAuthor = testPubKey, + labeledEvent = EventHintBundle(labeledEvent), hashtag = "#PhotoGraphy", ) From c619d71890471a3148571e726bd47da51900d2f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 21:40:54 +0000 Subject: [PATCH 10/21] refactor: move NwcWalletEntry to commons/model/nip47WalletConnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure data class (only quartz + a UUID id generator) — extract to commons so Desktop/CLI/iOS can reuse the NWC wallet entry model. Swaps java.util.UUID for the multiplatform kotlin.uuid.Uuid (matching the quartz convention) and re-points the three callers (LocalPreferences, AccountSettings, WalletViewModel). https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb --- .../java/com/vitorpamplona/amethyst/LocalPreferences.kt | 4 ++-- .../com/vitorpamplona/amethyst/model/AccountSettings.kt | 2 +- .../amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt | 2 +- .../commons}/model/nip47WalletConnect/NwcWalletEntry.kt | 8 +++++--- 4 files changed, 9 insertions(+), 7 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip47WalletConnect/NwcWalletEntry.kt (88%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 3645439400..3bf5f7eeca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -25,11 +25,11 @@ import android.content.Context import android.content.SharedPreferences import androidx.compose.runtime.Immutable import androidx.core.content.edit +import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntry +import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntryNorm import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.TopFilter import com.vitorpamplona.amethyst.model.UiSettings -import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcWalletEntry -import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcWalletEntryNorm import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index caba2542f7..b113c5ca78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatRepository import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListRepository -import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcWalletEntryNorm +import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntryNorm import com.vitorpamplona.amethyst.model.nip60Cashu.CashuPreferences import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt index 04b4a75fd6..c17037f630 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntryNorm import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcWalletEntryNorm import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcWalletEntry.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip47WalletConnect/NwcWalletEntry.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcWalletEntry.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip47WalletConnect/NwcWalletEntry.kt index 3a3e24d99d..94a44fa94c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcWalletEntry.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip47WalletConnect/NwcWalletEntry.kt @@ -18,15 +18,17 @@ * 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.model.nip47WalletConnect +package com.vitorpamplona.amethyst.commons.model.nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import kotlinx.serialization.Serializable -import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid +@OptIn(ExperimentalUuidApi::class) @Serializable data class NwcWalletEntry( - val id: String = UUID.randomUUID().toString(), + val id: String = Uuid.random().toString(), val name: String, val uri: Nip47WalletConnect.Nip47URI, ) { From 466086c475f3995b207767550cef79c7b2f2bdc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 21:42:51 +0000 Subject: [PATCH 11/21] refactor: move FavoriteAlgoFeedsListDecryptionCache to commons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure quartz-only decryption cache — extract to commons/model/nip51Lists/favoriteAlgoFeedsLists so Desktop/CLI/iOS can reuse it (mirrors the TrustProviderListDecryptionCache move). Re-points Account and the sibling FavoriteAlgoFeedsListState. https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb --- .../src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 2 +- .../favoriteAlgoFeedsLists/FavoriteAlgoFeedsListState.kt | 1 + .../FavoriteAlgoFeedsListDecryptionCache.kt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListDecryptionCache.kt (95%) 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 70c38b208b..8c6f84a465 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListD import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction +import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult @@ -73,7 +74,6 @@ import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayLis import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState -import com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListState.kt index b39d78e80b..dfb41f8db2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListState.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists +import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListDecryptionCache.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListDecryptionCache.kt index 03b3f85367..10382fa2d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/favoriteAlgoFeedsLists/FavoriteAlgoFeedsListDecryptionCache.kt @@ -18,7 +18,7 @@ * 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.model.nip51Lists.favoriteAlgoFeedsLists +package com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache From ab72427445e66f3431008550a077cd17e11301c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 21:43:55 +0000 Subject: [PATCH 12/21] refactor: move HashtagListDecryptionCache to commons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure quartz-only decryption cache — extract to commons/model/nip51Lists/hashtagLists. Re-points Account, FeedDecryptionCaches, and the sibling HashtagListState. https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb --- .../src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 2 +- .../amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt | 1 + .../amethyst/model/topNavFeeds/FeedDecryptionCaches.kt | 2 +- .../model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt (96%) 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 8c6f84a465..0a9bd3b694 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListS import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult @@ -77,7 +78,6 @@ import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRela import com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState -import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt index b43e1d319e..0724d6a7bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists +import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt index 09d218d6c4..bcc30e4c19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache -import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt index b89fc6f039..6aa9da1171 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt @@ -18,7 +18,7 @@ * 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.model.nip51Lists.hashtagLists +package com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache From 593c320004458b516ead8dfea756ac39751da139 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 21:46:13 +0000 Subject: [PATCH 13/21] refactor: move Mute/People/Community decryption caches to commons Extract the three remaining keystone-free quartz-only decryption caches (MuteListDecryptionCache, PeopleListDecryptionCache, CommunityListDecryptionCache) into commons/model so Desktop/CLI/iOS can reuse them. Re-points Account, FeedDecryptionCaches, and the sibling state holders (MuteListState, PeopleListsState, BlockPeopleListState, CommunityListState). The remaining relay-list decryption caches depend on GenericRelayListCache -> amethyst.model.Note (the keystone), so they stay until Phase A extracts Note/LocalCache. https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb --- .../main/java/com/vitorpamplona/amethyst/model/Account.kt | 6 +++--- .../nip51Lists/blockPeopleList/BlockPeopleListState.kt | 2 +- .../amethyst/model/nip51Lists/muteList/MuteListState.kt | 1 + .../model/nip51Lists/peopleList/PeopleListsState.kt | 1 + .../amethyst/model/nip72Communities/CommunityListState.kt | 1 + .../amethyst/model/topNavFeeds/FeedDecryptionCaches.kt | 6 +++--- .../model/nip51Lists/muteList/MuteListDecryptionCache.kt | 2 +- .../nip51Lists/peopleList/PeopleListDecryptionCache.kt | 2 +- .../model/nip72Communities/CommunityListDecryptionCache.kt | 2 +- 9 files changed, 13 insertions(+), 10 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip51Lists/muteList/MuteListDecryptionCache.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip72Communities/CommunityListDecryptionCache.kt (97%) 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 0a9bd3b694..1ffcb8b9a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -37,7 +37,10 @@ import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction +import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendStage @@ -83,10 +86,8 @@ import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayLis import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSetsState import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkListsState -import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.FollowListsState -import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListsState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState @@ -98,7 +99,6 @@ import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayLis import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip62Vanish.VanishRequestsState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState -import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState import com.vitorpamplona.amethyst.model.nipA3PaymentTargets.NipA3PaymentTargetsState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt index 7fb539c879..89d594a626 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt index 670645211a..ae90a7c09b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip51Lists.muteList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt index d279ec20e5..d3e224650d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.peopleList import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events +import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt index 97a5c2dfb3..ebd3d1d99a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model.nip72Communities +import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt index bcc30e4c19..be73ba0244 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.amethyst.model.topNavFeeds import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache -import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache -import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListDecryptionCache -import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache class FeedDecryptionCaches( val peopleListCache: PeopleListDecryptionCache, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/muteList/MuteListDecryptionCache.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/muteList/MuteListDecryptionCache.kt index 947c93ec8e..53e1e203eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/muteList/MuteListDecryptionCache.kt @@ -18,7 +18,7 @@ * 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.model.nip51Lists.muteList +package com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt index bffcfee91f..e8f1af7638 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt @@ -18,7 +18,7 @@ * 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.model.nip51Lists.peopleList +package com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip72Communities/CommunityListDecryptionCache.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip72Communities/CommunityListDecryptionCache.kt index ed7d129bc1..22aae56162 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip72Communities/CommunityListDecryptionCache.kt @@ -18,7 +18,7 @@ * 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.model.nip72Communities +package com.vitorpamplona.amethyst.commons.model.nip72Communities import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache From a38388fce53083da485fe1a1ffe34e9b23bad527 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 21:52:06 +0000 Subject: [PATCH 14/21] refactor: move LabeledBookmarkList to commons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure data class (only @Stable + quartz bookmark tags) — extract to commons/model/nip51Lists/labeledBookmarkLists so Desktop/CLI/iOS can reuse the bookmark-group model. Re-points the six bookmark-group UI files and the sibling LabeledBookmarkListsState. https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb --- .../labeledBookmarkLists/LabeledBookmarkListsState.kt | 1 + .../ui/screen/loggedIn/bookmarkgroups/list/BookmarkGroupItem.kt | 2 +- .../bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt | 2 +- .../loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt | 2 +- .../list/metadata/BookmarkGroupMetadataViewModel.kt | 2 +- .../membershipManagement/ArticleBookmarkListManagementScreen.kt | 2 +- .../membershipManagement/PostBookmarkListManagementScreen.kt | 2 +- .../nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt | 2 +- 8 files changed, 8 insertions(+), 7 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt (96%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt index 6c1a5aa944..2fd8cb2895 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events +import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/BookmarkGroupItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/BookmarkGroupItem.kt index d533e558e3..32151f4a1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/BookmarkGroupItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/BookmarkGroupItem.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.ui.components.M3ActionRow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt index 594e067c7f..af5c4bcf59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsFeedView.kt @@ -42,10 +42,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.OldBookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.PinListState -import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt index 469a490eb1..2ac0c207cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt @@ -35,10 +35,10 @@ import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.OldBookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.PinListState -import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.bottombars.fabBottomBarPadding import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt index 993a1c171c..835f5fc517 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt @@ -31,8 +31,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt index a7780754c4..d7558061b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt @@ -36,8 +36,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt index e7f1b2d61b..32fa78f8d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt @@ -36,8 +36,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt index 8a16f79bdf..a3e8e03d70 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt @@ -18,7 +18,7 @@ * 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.model.nip51Lists.labeledBookmarkLists +package com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark From abc9cd14cf55845885c5c6130e585d511dfeee74 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 21:59:26 +0000 Subject: [PATCH 15/21] refactor: move InterestSet to commons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure data class (only @Stable) — extract to commons/model/nip51Lists/interestSets so Desktop/CLI/iOS can reuse the interest-set model. Re-points the four interest-set UI files and the sibling InterestSetsState. https://claude.ai/code/session_01JFbYZdVV4QmDC4eQYvEccb --- .../amethyst/model/nip51Lists/interestSets/InterestSetsState.kt | 1 + .../com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt | 2 +- .../screen/loggedIn/interestSets/display/InterestSetScreen.kt | 2 +- .../ui/screen/loggedIn/interestSets/list/InterestSetItem.kt | 2 +- .../interestSets/list/metadata/InterestSetMetadataViewModel.kt | 2 +- .../commons}/model/nip51Lists/interestSets/InterestSet.kt | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip51Lists/interestSets/InterestSet.kt (95%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt index baeff9cd6b..d12fc049b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.interestSets import com.vitorpamplona.amethyst.commons.model.anyNotNullEvent import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events +import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index 37ef8ea4e0..666c1e153b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -24,10 +24,10 @@ import android.content.Context import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.TopFilter -import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSet import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/display/InterestSetScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/display/InterestSetScreen.kt index 83af47d057..e57d5ef146 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/display/InterestSetScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/display/InterestSetScreen.kt @@ -49,7 +49,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSet +import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/list/InterestSetItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/list/InterestSetItem.kt index a181a46a20..ca968526ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/list/InterestSetItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/list/InterestSetItem.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSet +import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.ui.components.M3ActionRow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/list/metadata/InterestSetMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/list/metadata/InterestSetMetadataViewModel.kt index 9b77a396e6..e3458289e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/list/metadata/InterestSetMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/interestSets/list/metadata/InterestSetMetadataViewModel.kt @@ -27,8 +27,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSet import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSet.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/interestSets/InterestSet.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSet.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/interestSets/InterestSet.kt index f8f86f088a..d5606a207b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSet.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/interestSets/InterestSet.kt @@ -18,7 +18,7 @@ * 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.model.nip51Lists.interestSets +package com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets import androidx.compose.runtime.Stable From a70e4cebc074f74199255ff3df71dfc0159e5320 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 22:24:28 +0000 Subject: [PATCH 16/21] fix: tidy wallet screen UI - Remove the Taproot address block from the onchain card (Copy/Send actions still operate on the address; it's just no longer displayed). - Drop the redundant leading spacer above the NWC wallet list so the gap below the onchain card no longer doubles up. - Render the Cashu logo in a circular chip mirroring the Bitcoin chip so both payment rails read as the same kind of object. - Remove NWC wallet drag-to-reorder: order was purely cosmetic since a default wallet is always set, so setDefault is the real selector. Drops moveWallet/moveNwcWallet and the unused wallet_reorder string. https://claude.ai/code/session_014zaY9EVdxgtdssyVTaeEZd --- .../amethyst/model/AccountSettings.kt | 15 ---- .../screen/loggedIn/wallet/OnchainSection.kt | 22 ------ .../ui/screen/loggedIn/wallet/WalletScreen.kt | 75 ++++++++----------- .../screen/loggedIn/wallet/WalletViewModel.kt | 9 --- amethyst/src/main/res/values/strings.xml | 1 - 5 files changed, 31 insertions(+), 91 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index b113c5ca78..92b0224654 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -385,21 +385,6 @@ class AccountSettings( return false } - fun moveNwcWallet( - fromIndex: Int, - toIndex: Int, - ): Boolean { - val wallets = nwcWallets.value.toMutableList() - if (fromIndex in wallets.indices && toIndex in wallets.indices && fromIndex != toIndex) { - val item = wallets.removeAt(fromIndex) - wallets.add(toIndex, item) - nwcWallets.tryEmit(wallets) - saveAccountSettings() - return true - } - return false - } - fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URINorm?): Boolean { if (newServer == null) { if (nwcWallets.value.isNotEmpty()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainSection.kt index edb46d9c02..c7b87a65b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainSection.kt @@ -58,9 +58,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -154,9 +152,6 @@ fun OnchainSection( color = MaterialTheme.colorScheme.onSurfaceVariant, ) } else { - Spacer(modifier = Modifier.height(12.dp)) - AddressBlock(address = address) - Spacer(modifier = Modifier.height(12.dp)) ActionRow(address = address, accountViewModel = accountViewModel, orange = orange) } @@ -337,23 +332,6 @@ private fun BalanceBlock( } } -@Composable -private fun AddressBlock(address: String) { - Text( - text = "Your Taproot address", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = address, - style = MaterialTheme.typography.bodySmall, - fontFamily = FontFamily.Monospace, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) -} - @Composable private fun ActionRow( address: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt index ad602b0f64..cf096aa501 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt @@ -21,8 +21,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -34,8 +36,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -61,6 +64,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -76,10 +81,6 @@ import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.draggableRelayItem -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relayDragHandle -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.launch import java.text.NumberFormat @@ -221,12 +222,6 @@ private fun MultiWalletHomeContent( } } - val dragState = - rememberRelayDragState( - onMove = { from, to -> walletViewModel.moveWallet(from, to) }, - itemCount = { walletInfoList.size }, - ) - LazyColumn( state = listState, modifier = @@ -234,17 +229,10 @@ private fun MultiWalletHomeContent( .fillMaxSize() .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), - userScrollEnabled = !dragState.isDragging, ) { - item { - Spacer(modifier = Modifier.height(8.dp)) - } - - itemsIndexed(walletInfoList, key = { _, info -> info.walletId }) { index, walletInfo -> + items(walletInfoList, key = { info -> info.walletId }) { walletInfo -> WalletCard( walletInfo = walletInfo, - index = index, - dragState = if (walletInfoList.size > 1) dragState else null, onSelect = { walletViewModel.selectWallet(walletInfo.walletId) nav.nav(Route.WalletDetail(walletInfo.walletId)) @@ -293,8 +281,6 @@ private fun MultiWalletHomeContent( @Composable private fun WalletCard( walletInfo: WalletInfo, - index: Int, - dragState: RelayDragState?, onSelect: () -> Unit, onSetDefault: () -> Unit, onRename: (String) -> Unit, @@ -339,7 +325,6 @@ private fun WalletCard( modifier = Modifier .fillMaxWidth() - .let { if (dragState != null) it.draggableRelayItem(index, dragState) else it } .clickable(onClick = onSelect), shape = RoundedCornerShape(16.dp), border = @@ -366,22 +351,6 @@ private fun WalletCard( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - // Drag handle leads the row — same convention as the - // relay reorder list — so a center-aligned icon doesn't - // land between the balance amount and its "sats" label. - if (dragState != null) { - Icon( - MaterialSymbols.DragIndicator, - contentDescription = stringRes(R.string.wallet_reorder), - modifier = - Modifier - .size(24.dp) - .relayDragHandle(index, dragState), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.width(8.dp)) - } - Column(modifier = Modifier.weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { Text( @@ -489,6 +458,29 @@ private fun WalletCard( } } +// Mirrors OnchainSection's BitcoinChip so both payment rails render their +// logo the same way — a filled circular chip with a white glyph — instead of +// one being a bare tinted vector. Cashu uses the theme primary in place of +// bitcoin orange to keep the two rails distinct at a glance. +@Composable +private fun CashuChip() { + Box( + modifier = + Modifier + .size(28.dp) + .clip(CircleShape) + .background(color = MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.Center, + ) { + Material3Icon( + imageVector = CustomHashTagIcons.Cashu, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(18.dp), + ) + } +} + @Composable private fun CashuWalletRow( balanceSats: Long, @@ -512,12 +504,7 @@ private fun CashuWalletRow( .padding(16.dp), verticalAlignment = Alignment.CenterVertically, ) { - Material3Icon( - imageVector = CustomHashTagIcons.Cashu, - contentDescription = null, - modifier = Modifier.size(28.dp), - tint = MaterialTheme.colorScheme.primary, - ) + CashuChip() Spacer(modifier = Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt index c17037f630..e335b033a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt @@ -296,15 +296,6 @@ class WalletViewModel : ViewModel() { refreshWalletList() } - fun moveWallet( - fromIndex: Int, - toIndex: Int, - ) { - val acc = account ?: return - acc.settings.moveNwcWallet(fromIndex, toIndex) - refreshWalletList() - } - fun selectWallet(walletId: String) { _selectedWalletId.value = walletId _balanceSats.value = walletInfoMap.value[walletId]?.balanceSats diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index cf1d12b9e4..f4889dc9f6 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2058,7 +2058,6 @@ Invalid NWC connection URI Move Up Move Down - Reorder wallet Onchain Transactions No on-chain address available for this account. No chain backend is configured. From dddbb4568012691abee67aae98c62695e8e1a2ad Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 22:35:37 +0000 Subject: [PATCH 17/21] feat(cashu): show per-mint balance, drop inline recommend button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display spendable sats per mint in the wallet screen's Mint section and remove the thumbs-up recommend button from each mint row — mint recommendations are managed from Cashu Wallet Settings. - Add reactive CashuWalletState.mintBalances (StateFlow>) derived from token entries, mirroring balanceSats. - Expose it through CashuWalletViewModel and render the per-mint total in MintRow using the existing wallet_sats label. - Remove the per-row recommend (ThumbUp) action and its wiring; recommendMint() stays for the Settings screen. --- .../model/nip60Cashu/CashuWalletState.kt | 16 ++++++++++ .../loggedIn/wallet/CashuWalletScreen.kt | 30 ++++++++++--------- .../loggedIn/wallet/CashuWalletViewModel.kt | 1 + 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt index 8aed359088..08270487a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt @@ -203,6 +203,22 @@ class CashuWalletState( .flowOn(Dispatchers.Default) .stateIn(scope, SharingStarted.Eagerly, 0L) + /** + * Spendable cashu balance per mint URL — the reactive twin of + * [peekMintBalances], for UI that needs to re-render as proofs arrive + * (the wallet screen's mint list). Derived from [_tokenEntries] exactly + * like [balanceSats]. May overstate by NUT-07 stale proofs; the actual + * spend scrubs before melting. + */ + val mintBalances: StateFlow> = + _tokenEntries + .map { entries -> + entries + .groupBy { it.content.mint } + .mapValues { (_, byMint) -> byMint.sumOf { it.content.totalAmount() } } + }.flowOn(Dispatchers.Default) + .stateIn(scope, SharingStarted.Eagerly, emptyMap()) + private val _history = MutableStateFlow>(emptyList()) val history: StateFlow> = _history.asStateFlow() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt index 5e54f6b328..0d93f9828d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt @@ -115,6 +115,7 @@ fun CashuWalletScreen( val discovering by viewModel.discovering.collectAsState() val mints by viewModel.mints.collectAsState() val balanceSats by viewModel.balanceSats.collectAsState() + val mintBalances by viewModel.mintBalances.collectAsState() val history by viewModel.history.collectAsState() val pendingQuotes by viewModel.pendingQuotes.collectAsState() @@ -158,6 +159,7 @@ fun CashuWalletScreen( modifier = Modifier.padding(padding), balanceSats = balanceSats, mints = mints, + mintBalances = mintBalances, history = history, pendingQuoteCount = pendingQuotes.size, accountViewModel = accountViewModel, @@ -166,7 +168,6 @@ fun CashuWalletScreen( onSendLn = { sendLnOpen = true }, onSendToken = { sendTokenOpen = true }, onRedeem = { redeemOpen = true }, - onRecommendMint = { viewModel.recommendMint(it) }, onResumePendingQuote = { pendingQuotes.firstOrNull()?.let { viewModel.resumeMintQuote(it) @@ -295,6 +296,7 @@ private fun CashuWalletContent( modifier: Modifier, balanceSats: Long, mints: List, + mintBalances: Map, history: List, pendingQuoteCount: Int, accountViewModel: AccountViewModel, @@ -303,7 +305,6 @@ private fun CashuWalletContent( onSendLn: () -> Unit, onSendToken: () -> Unit, onRedeem: () -> Unit, - onRecommendMint: (String) -> Unit, onResumePendingQuote: () -> Unit, ) { LazyColumn( @@ -340,7 +341,7 @@ private fun CashuWalletContent( ) } items(mints, key = { it }) { mint -> - MintRow(mint = mint, onRecommend = { onRecommendMint(mint) }) + MintRow(mint = mint, balanceSats = mintBalances[mint] ?: 0L) } if (history.isNotEmpty()) { @@ -526,8 +527,12 @@ private fun ActionTile( @Composable private fun MintRow( mint: String, - onRecommend: (() -> Unit)? = null, + balanceSats: Long, ) { + val formattedBalance = + remember(balanceSats) { + NumberFormat.getIntegerInstance().format(balanceSats) + } Card( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), @@ -543,16 +548,13 @@ private fun MintRow( ) Spacer(modifier = Modifier.width(12.dp)) Text(text = mint, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f)) - if (onRecommend != null) { - IconButton(onClick = onRecommend, modifier = Modifier.size(28.dp)) { - Icon( - symbol = MaterialSymbols.ThumbUp, - contentDescription = stringRes(R.string.cashu_recommend_mint), - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.primary, - ) - } - } + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "$formattedBalance ${stringRes(R.string.wallet_sats)}", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt index 63323e2f0a..789dc1b927 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt @@ -146,6 +146,7 @@ class CashuWalletViewModel : ViewModel() { val walletEvent get() = state.walletEvent val mints get() = state.mints val balanceSats get() = state.balanceSats + val mintBalances get() = state.mintBalances val tokenEntries: StateFlow> get() = state.tokenEntries val history get() = state.history val pendingQuotes get() = state.pendingQuotes From 94dc759f8acbaaeeb3e140a8539aefb7cf404481 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 22:59:08 +0000 Subject: [PATCH 18/21] feat(cashu): add standalone mint top-up screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a dedicated TopUpMintScreen + TopUpMintViewModel for funding a specific mint outside the zap flow. Each mint row on the wallet screen gets an add-funds icon that opens the screen with that mint as the fixed target. The screen reuses the same funding primitives as the zap-driven Reload screen — CashuWalletState.rebalance for a mint-to-mint move and startMintFromLightning/completeMintFromLightning for an LN top-up (NWC or external invoice) — but drops all zap machinery: no recipient, no shared-mint intersection, no fixed send amount/shortfall, no terminal nutzap, no fund-then-send atomicity. This keeps the double-spend- sensitive zap pipeline untouched. Shares SectionHeader/SourceRow/shortMint/sats from ReloadMintScreen (widened to internal) instead of duplicating them. --- .../amethyst/ui/navigation/AppNavigation.kt | 3 + .../amethyst/ui/navigation/routes/Routes.kt | 5 + .../loggedIn/wallet/CashuWalletScreen.kt | 15 +- .../loggedIn/wallet/ReloadMintScreen.kt | 8 +- .../screen/loggedIn/wallet/TopUpMintScreen.kt | 224 +++++++++++++ .../loggedIn/wallet/TopUpMintViewModel.kt | 307 ++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 4 + 7 files changed, 560 insertions(+), 6 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/TopUpMintScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/TopUpMintViewModel.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 8c03c0d26c..67fec8ec37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -201,6 +201,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.CashuWalletScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.CashuWalletSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainTransactionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.ReloadMintScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.TopUpMintScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletDetailScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletReceiveScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletScreen @@ -348,6 +349,8 @@ fun BuildNavigation( composableFromBottomArgs { ReloadMintScreen(it.requestId, accountViewModel, nav) } + composableFromBottomArgs { TopUpMintScreen(it.mintUrl, accountViewModel, nav) } + composableFromBottomArgs { NewUserMetadataScreen(nav, accountViewModel) } composable { SearchScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index 02dcc6c61c..3c8579065a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -650,6 +650,11 @@ sealed class Route { data class ReloadMint( val requestId: String, ) : Route() + + @Serializable + data class TopUpMint( + val mintUrl: String, + ) : Route() } inline fun isBaseRoute(navController: NavHostController): Boolean = navController.currentBackStackEntry?.destination?.hasRoute() == true diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt index 0d93f9828d..1c700dae95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt @@ -168,6 +168,7 @@ fun CashuWalletScreen( onSendLn = { sendLnOpen = true }, onSendToken = { sendTokenOpen = true }, onRedeem = { redeemOpen = true }, + onTopUpMint = { nav.nav(Route.TopUpMint(it)) }, onResumePendingQuote = { pendingQuotes.firstOrNull()?.let { viewModel.resumeMintQuote(it) @@ -305,6 +306,7 @@ private fun CashuWalletContent( onSendLn: () -> Unit, onSendToken: () -> Unit, onRedeem: () -> Unit, + onTopUpMint: (String) -> Unit, onResumePendingQuote: () -> Unit, ) { LazyColumn( @@ -341,7 +343,7 @@ private fun CashuWalletContent( ) } items(mints, key = { it }) { mint -> - MintRow(mint = mint, balanceSats = mintBalances[mint] ?: 0L) + MintRow(mint = mint, balanceSats = mintBalances[mint] ?: 0L, onTopUp = { onTopUpMint(mint) }) } if (history.isNotEmpty()) { @@ -528,6 +530,7 @@ private fun ActionTile( private fun MintRow( mint: String, balanceSats: Long, + onTopUp: () -> Unit, ) { val formattedBalance = remember(balanceSats) { @@ -538,7 +541,7 @@ private fun MintRow( colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), ) { Row( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), + modifier = Modifier.padding(start = 12.dp, end = 4.dp, top = 6.dp, bottom = 6.dp), verticalAlignment = Alignment.CenterVertically, ) { Material3Icon( @@ -555,6 +558,14 @@ private fun MintRow( fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary, ) + IconButton(onClick = onTopUp) { + Icon( + symbol = MaterialSymbols.AddCircle, + contentDescription = stringRes(R.string.topup_mint_action), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintScreen.kt index 5aef6bdd64..cafdd4c2e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintScreen.kt @@ -99,9 +99,9 @@ fun navigateToReloadMint( nav.nav(Route.ReloadMint(uid)) } -private fun shortMint(url: String): String = url.removePrefix("https://").removePrefix("http://").removeSuffix("/") +internal fun shortMint(url: String): String = url.removePrefix("https://").removePrefix("http://").removeSuffix("/") -private fun sats(value: Long): String = showAmount(value.toBigDecimal().setScale(1)) +internal fun sats(value: Long): String = showAmount(value.toBigDecimal().setScale(1)) @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -359,7 +359,7 @@ fun ReloadMintScreen( } @Composable -private fun SectionHeader(text: String) { +internal fun SectionHeader(text: String) { Text( text = text, color = MaterialTheme.colorScheme.primary, @@ -368,7 +368,7 @@ private fun SectionHeader(text: String) { } @Composable -private fun SourceRow( +internal fun SourceRow( source: ReloadSource, selected: Boolean, onSelect: () -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/TopUpMintScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/TopUpMintScreen.kt new file mode 100644 index 0000000000..f6770a8e06 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/TopUpMintScreen.kt @@ -0,0 +1,224 @@ +/* + * 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.ui.screen.loggedIn.wallet + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.hashtags.Cashu +import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.ui.components.util.setText +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.coroutines.launch +import androidx.compose.material3.Icon as Material3Icon + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopUpMintScreen( + mintUrl: String, + accountViewModel: AccountViewModel, + nav: INav, +) { + val viewModel: TopUpMintViewModel = viewModel() + LaunchedEffect(mintUrl) { viewModel.init(accountViewModel, mintUrl) } + + val ui by viewModel.uiState.collectAsStateWithLifecycle() + val clipboard = LocalClipboard.current + val scope = rememberCoroutineScope() + + var amountText by remember(mintUrl) { mutableStateOf("") } + + if (ui.status is TopUpStatus.Done) { + LaunchedEffect(Unit) { nav.popBack() } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringRes(R.string.topup_mint_title)) }, + navigationIcon = { + IconButton(onClick = { nav.popBack() }) { + Icon( + symbol = MaterialSymbols.AutoMirrored.ArrowBack, + contentDescription = stringRes(R.string.back), + ) + } + }, + ) + }, + ) { padding -> + Column( + modifier = + Modifier + .padding(padding) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + // ── Header: the mint we're topping up + its current balance ──── + Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { + Row( + modifier = Modifier.fillMaxWidth().padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Material3Icon( + imageVector = CustomHashTagIcons.Cashu, + contentDescription = null, + modifier = Modifier.size(26.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = shortMint(ui.targetMint), + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(R.string.reload_mint_available, sats(ui.targetBalanceSats)), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.placeholderText, + ) + } + } + } + + // ── Amount to add ───────────────────────────────────────────── + SectionHeader(stringRes(R.string.topup_mint_amount_label)) + OutlinedTextField( + value = amountText, + onValueChange = { input -> + val digits = input.filter { it.isDigit() }.take(12) + amountText = digits + viewModel.setAmount(digits.toLongOrNull() ?: 0L) + }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + suffix = { Text(stringRes(R.string.sats)) }, + modifier = Modifier.fillMaxWidth(), + ) + + // ── Funds from ──────────────────────────────────────────────── + SectionHeader(stringRes(R.string.reload_mint_section_from)) + ui.sources.forEach { source -> + SourceRow( + source = source, + selected = source == ui.selectedSource, + onSelect = { viewModel.selectSource(source) }, + ) + } + + Spacer(Modifier.height(4.dp)) + + when (val status = ui.status) { + is TopUpStatus.Working -> { + LinearProgressIndicator(progress = { status.progress }, modifier = Modifier.fillMaxWidth()) + Text(status.step, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText) + } + + is TopUpStatus.AwaitingInvoice -> { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { + CircularProgressIndicator(modifier = Modifier.height(20.dp)) + Text(stringRes(R.string.reload_mint_awaiting_payment), style = MaterialTheme.typography.bodyMedium) + } + OutlinedButton( + onClick = { scope.launch { clipboard.setText(status.invoice) } }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringRes(R.string.reload_mint_copy_invoice)) + } + } + + is TopUpStatus.Failed -> { + Text(status.message, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodyMedium) + Button(onClick = { viewModel.confirm() }, modifier = Modifier.fillMaxWidth()) { + Text(stringRes(R.string.reload_mint_retry)) + } + } + + else -> { + val source = ui.selectedSource + val enabled = + ui.amountSats > 0 && + ( + source is ReloadSource.LightningWallet || + source is ReloadSource.LightningExternal || + (source is ReloadSource.Mint && source.canCover) + ) + Button( + onClick = { viewModel.confirm() }, + enabled = enabled, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringRes(R.string.topup_mint_confirm)) + } + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/TopUpMintViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/TopUpMintViewModel.kt new file mode 100644 index 0000000000..5a73201124 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/TopUpMintViewModel.kt @@ -0,0 +1,307 @@ +/* + * 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.ui.screen.loggedIn.wallet + +import androidx.compose.runtime.Immutable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.model.nip60Cashu.CashuWalletState +import com.vitorpamplona.amethyst.model.nip60Cashu.describeMintError +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect +import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +/** + * Presenter for the standalone "Top up a mint" screen. + * + * Reuses the same funding primitives as [ReloadMintViewModel] — + * [CashuWalletState.rebalance] for a mint-to-mint move and + * [com.vitorpamplona.amethyst.model.nip60Cashu.CashuWalletOps.startMintFromLightning] / + * `completeMintFromLightning` for an LN top-up — but without any of the + * zap-specific machinery (recipient, shared-mint intersection, fixed send + * amount + shortfall, terminal nutzap, fund-then-send atomicity). The user + * picks an amount and a source; we add ecash to a fixed target mint and pop. + */ +class TopUpMintViewModel : ViewModel() { + private var accountViewModel: AccountViewModel? = null + private var targetMint: String = "" + private val state: CashuWalletState? get() = accountViewModel?.account?.cashuWalletState + + /** The in-flight pipeline, cancelled when the screen leaves (it runs on the + * long-lived AccountViewModel scope, not this VM's). */ + private var job: Job? = null + + private val _uiState = MutableStateFlow(TopUpUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun init( + accountViewModel: AccountViewModel, + mintUrl: String, + ) { + if (this.accountViewModel != null) return // already initialized + this.accountViewModel = accountViewModel + this.targetMint = mintUrl + _uiState.update { it.copy(targetMint = mintUrl) } + + // Wallet proofs/mints arrive from relays and can land after the screen + // opens, so keep the source list + target balance in sync as the wallet + // fills in. Project to the per-mint balance map and distinctUntilChanged + // so unrelated wallet churn doesn't re-run the rebuild on every emission. + val st = state ?: return + viewModelScope.launch { + combine(st.tokenEntries, st.mints) { _, _ -> st.peekMintBalances() } + .distinctUntilChanged() + .collect { rebuild(it) } + } + } + + /** Rebuild target balance + funding sources from the current wallet state, + * keeping the user's amount and source pick intact. */ + private fun rebuild(balances: Map) { + _uiState.update { cur -> recompute(cur.copy(targetBalanceSats = balances[targetMint] ?: 0L), balances) } + } + + /** Recompute fee estimate + sources + a default source for the current amount. */ + private fun recompute( + base: TopUpUiState, + balances: Map, + ): TopUpUiState { + val moveSats = base.amountSats + // Rough fee cushion for *enabling* a mint source — the real Lightning + // feeReserve is only known once rebalance() fetches the melt quote, so this + // is a heuristic (1%, min 2 sat). A source that clears the estimate but not + // the real quote fails recoverably without moving funds. + val estFee = if (moveSats <= 0L) 0L else (moveSats / 100L).coerceAtLeast(2L) + val needFromSource = moveSats + estFee + + val mintSources = + balances + .filterKeys { it != targetMint } + .entries + .sortedByDescending { it.value } + .map { ReloadSource.Mint(it.key, it.value, canCover = moveSats <= 0L || it.value >= needFromSource) } + + val lightningSources: List = + nwcWallets() + .map { ReloadSource.LightningWallet(it.id, it.name) } + .ifEmpty { listOf(ReloadSource.LightningExternal) } + val sources = mintSources + lightningSources + + // Keep the current pick if still valid; else default to the richest mint that + // can cover (no new sats), else the configured/first Lightning wallet. + val keep = + when (val prev = base.selectedSource) { + is ReloadSource.Mint -> mintSources.firstOrNull { it.mintUrl == prev.mintUrl && it.canCover } + is ReloadSource.LightningWallet -> lightningSources.firstOrNull { it is ReloadSource.LightningWallet && it.walletId == prev.walletId } + ReloadSource.LightningExternal -> lightningSources.firstOrNull { it is ReloadSource.LightningExternal } + null -> null + } + val selectedSource: ReloadSource? = + keep + ?: mintSources.filter { it.canCover }.maxByOrNull { it.balanceSats } + ?: defaultLightningSource(lightningSources) + + return base.copy( + estFeeSats = estFee, + sources = sources.toImmutableList(), + selectedSource = selectedSource, + ) + } + + fun selectSource(source: ReloadSource) { + _uiState.update { it.copy(selectedSource = source) } + } + + fun setAmount(sats: Long) { + _uiState.update { recompute(it.copy(amountSats = sats.coerceAtLeast(0L)), currentBalances()) } + } + + private fun currentBalances(): Map = state?.peekMintBalances().orEmpty() + + fun confirm() { + val vm = accountViewModel ?: return + val s = _uiState.value + val source = s.selectedSource ?: return + val moveSats = s.amountSats + if (moveSats <= 0L) return + + // In-flight guard: only start from a resting state so a double tap (or the + // Failed-state retry) can't launch a second pipeline. + if (s.status !is TopUpStatus.Configuring && s.status !is TopUpStatus.Failed) return + setStatus(TopUpStatus.Working("Starting", 0.05f)) + + job?.cancel() + job = + vm.launchSigner { + try { + when (source) { + is ReloadSource.Mint -> topUpFromMint(source.mintUrl, moveSats) + is ReloadSource.LightningWallet -> topUpFromLightning(moveSats, walletUriFor(source.walletId)) + ReloadSource.LightningExternal -> topUpFromLightning(moveSats, null) + } + setStatus(TopUpStatus.Done) + } catch (e: CancellationException) { + throw e // screen left mid-flow — don't mask as a Failed state + } catch (e: Exception) { + setStatus(TopUpStatus.Failed(describeMintError(e))) + } + } + } + + private suspend fun topUpFromMint( + sourceMint: String, + moveSats: Long, + ) { + val st = state ?: return + setStatus(TopUpStatus.Working("Moving funds", 0.1f)) + st.rebalance( + sourceMintUrl = sourceMint, + targetMintUrl = targetMint, + sats = moveSats, + onProgress = { p -> setStatus(TopUpStatus.Working("Moving funds", p.coerceIn(0.1f, 0.95f))) }, + ) + } + + private suspend fun topUpFromLightning( + moveSats: Long, + walletUri: Nip47WalletConnect.Nip47URINorm?, + ) { + val vm = accountViewModel ?: return + val st = state ?: return + val ops = st.ops + + setStatus(TopUpStatus.Working("Requesting invoice", 0.1f)) + val flow = ops.startMintFromLightning(targetMint, moveSats) + + if (walletUri != null) { + setStatus(TopUpStatus.Working("Paying from your wallet", 0.35f)) + // Fire-and-forget: the mint-quote poll below is the source of truth for + // whether the payment actually landed. + runCatching { + vm.account.sendNwcRequestToWallet(walletUri, PayInvoiceMethod.create(flow.invoice)) { } + } + } else { + // No NWC — surface the invoice for an external wallet and keep polling. + setStatus(TopUpStatus.AwaitingInvoice(flow.invoice, moveSats)) + } + + // External payment can take a while; the poll runs on a job tied to the + // screen (cancelled in onCleared), so leaving stops it. + val attempts = 90 + val delayMs = 2_000L + var paid = false + var attempt = 0 + while (!paid && attempt < attempts) { + paid = ops.checkMintQuote(targetMint, flow.mintQuote.quote).isSettled() + if (!paid) { + delay(delayMs) + attempt++ + } + } + if (!paid) { + setStatus(TopUpStatus.Failed("Invoice not paid yet — you can finish it later from the pending quote banner")) + return + } + setStatus(TopUpStatus.Working("Issuing ecash", 0.85f)) + ops.completeMintFromLightning(targetMint, flow.quoteEvent, moveSats) + } + + private fun setStatus(status: TopUpStatus) { + _uiState.update { it.copy(status = status) } + } + + private fun nwcWallets() = + accountViewModel + ?.account + ?.settings + ?.nwcWallets + ?.value + .orEmpty() + + private fun walletUriFor(walletId: String) = nwcWallets().firstOrNull { it.id == walletId }?.uri + + /** The configured default NWC wallet's source, else the first Lightning source. */ + private fun defaultLightningSource(lightning: List): ReloadSource? { + val defaultId = + accountViewModel + ?.account + ?.settings + ?.defaultNwcWallet() + ?.id + return lightning.firstOrNull { it is ReloadSource.LightningWallet && it.walletId == defaultId } + ?: lightning.firstOrNull() + } + + override fun onCleared() { + // The pipeline runs on the AccountViewModel scope, not this VM's, so it would + // outlive the screen — cancel it when the screen goes away. + job?.cancel() + super.onCleared() + } +} + +/** Progress of a standalone mint top-up. Mirrors [ReloadStatus] minus the zap step. */ +@Immutable +sealed interface TopUpStatus { + data object Configuring : TopUpStatus + + data class Working( + val step: String, + val progress: Float, + ) : TopUpStatus + + /** External Lightning — the user must pay [invoice]; we keep polling. */ + data class AwaitingInvoice( + val invoice: String, + val sats: Long, + ) : TopUpStatus + + data object Done : TopUpStatus + + data class Failed( + val message: String, + ) : TopUpStatus +} + +@Immutable +data class TopUpUiState( + val targetMint: String = "", + val targetBalanceSats: Long = 0L, + /** The amount to add to the target mint. */ + val amountSats: Long = 0L, + val estFeeSats: Long = 0L, + val sources: ImmutableList = persistentListOf(), + val selectedSource: ReloadSource? = null, + val status: TopUpStatus = TopUpStatus.Configuring, +) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index cf1d12b9e4..4630d800e7 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -904,6 +904,10 @@ needs %1$s sat more funded Copy invoice + Top up mint + Top up this mint + Amount to add + Top up Zap Privacy Controls how your identity is shown when you send a zap. Connect Wallet From 62a8841a186c694c594372f4ae7c81e2ccf3afa4 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 31 May 2026 09:26:33 +0200 Subject: [PATCH 19/21] i18n: translate Cashu wallet, top-up/nutzap, hashtag-label, podcast & music strings (cs, de, sv, pt-BR) Catches up four locales on the missing strings/plurals for the new Cashu (NIP-60) wallet, mint top-up/reload, NIP-61 nutzaps, NIP-32 hashtag labels, podcasts, and (pt-BR) music tracks/playlists & NIP-82 software releases. pt-BR was furthest behind (199 keys); cs/de/sv each add 123 strings + 5 plurals. Czech plurals carry the full one/few/many/ other CLDR set. Notification-channel id calendar_reminder_channel_id left untranslated by design. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/res/values-cs-rCZ/strings.xml | 153 ++++++++++++ .../src/main/res/values-de-rDE/strings.xml | 143 +++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 228 ++++++++++++++++++ .../src/main/res/values-sv-rSE/strings.xml | 143 +++++++++++ 4 files changed, 667 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 0aca6852e8..06447dd601 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -2881,4 +2881,157 @@ Nowhere Drop Nowhere Umění Nowhere Fórum + Přidat hashtag + Přidat + Veřejně označte tento příspěvek hashtagem (štítek NIP-32). Lidé, kteří vás sledují, jej uvidí ve feedu daného hashtagu. + Hashtag + Přidat hashtag + Přijmout + Uplatnit + Poslat LN + Poslat token + Částka (sats) + Zůstatek + Vyměňuji proofy… + Vydávám proofy… + Kopírovat fakturu + Vytvořit token + Zahodit fakturu + Hledám vaši peněženku… + Peněženky NIP-60 se synchronizují mezi klienty. Pokud jste si nějakou vytvořili v jiné aplikaci pod tímto klíčem Nostr, měla by se za pár sekund objevit. + Hotovo + Upravit peněženku + Získat nabídku + Žádám mint o nabídku… + Historie + Přijato + Odesláno + Lightning faktura (bolt11) + Poznámka (volitelné) + Odpovídající minty + Zatím žádná doporučení + Oblíbené minty + ✓ Mint je dostupný + ✓ %1$s + Doporučuje + Nelze se připojit k mintu: %1$s + URL mintu + Minty + Zatím žádná peněženka Cashu + Vytvořte si peněženku Cashu pro uchovávání tokenů ecash a příjem nutzapů. + Vygenerovat nový klíč + Zneplatní všechny čekající nutzapy uzamčené ke stávajícímu klíči. + Samostatný soukromý klíč používaný pouze k příjmu nutzapů NIP-61. Není to klíč vaší identity Nostr. + Ponechat stávající klíč + Soukromý klíč P2PK (hex) + Klíč pro nutzap (pokročilé) + Zaplaceno %1$s sats (poplatky: %2$s sats). + Zaplatit fakturu + Platím přes mint… + Pokračovat + Klepnutím pokračujte a zkontrolujte stav. + Vyberte mint + Zaplatit %1$s sat + až %2$s sat na poplatcích? + Zaplaťte tuto fakturu pro vydání %1$s sats v ecash. + Přijato %1$s sats. + Doporučit tento mint + Uplatnit + Uplatněno %1$s sats do vaší peněženky. + Vyměňuji proofy do vaší peněženky… + Odebrat mint + Vyžádat fakturu + Žádám mint o fakturu… + Doporučit mint + Publikovat žádost o smazání vašeho doporučení kind:38000 pro %1$s. Relaye, které dodržují NIP-09, jej odstraní. + Stáhnout doporučení? + Smazat doporučení + Upravit údaje peněženky + Minty, klíč pro nutzap + Moje doporučení mintů + Zatím jste nedoporučili žádný mint. Klepnutím na palec nahoru vedle mintu jej veřejně doporučíte. + Nenalezeny žádné obnovitelné proofy. + Obnoveno %1$s sats z %2$s proofů. + Prohledávám minty… + Znovu odvoďte každé minulé tajemství a požádejte každý mint o vrácení všech slepých podpisů, které stále eviduje. Užitečné po ztrátě zařízení nebo smazání vašich token událostí podvodným relayem. + Obnovit ze seedu + Nastavení peněženky Cashu + Token Cashu + Token na %1$s sats je připraven. Sdílejte jej s příjemcem. + Ověřit + Čekám na zaplacení faktury… + Peněženka Cashu + přidal(a) + Vytvořené podcasty + Oblíbené podcasty + Epizoda podcastu + Podcast + Nutzap + Nelze sestavit odkaz na událost + V poznámce chybí veřejný klíč příjemce + Nutzap selhal + Epizody + Zatím nenalezeny žádné epizody + Zobrazit epizody + %1$s sats k dispozici + Čekám na platbu… + Dobít a poslat zap + Kopírovat fakturu + financováno + Vydejte nový ecash z vaší Lightning peněženky + chybí %1$s sat + Tady není dost + Zaplatit pomocí Lightning + příjemce + Zkusit znovu + %1$s sats + Prostředky z + Mint k dobití + Poslat zap + Dobít %1$s sats do %2$s a poslat %3$s zap %4$s sats · poplatek ≈ %5$s sats + Poslat zap %1$s %2$s sats z %3$s + Dobít mint + Částka dobití + Epizody + Podcasty + Dobít tento mint + Částka k přidání + Dobít + Dobít mint + Ecash peněženka v aplikaci založená na mintu Cashu. Tokeny jsou uloženy zašifrované na relayích. + Peněženka Cashu (NIP-60) + Zvolte typ peněženky + Plaťte přes externí Lightning peněženku pomocí Nostr Wallet Connect. + Připojit Lightning peněženku (NWC) + Upravit peněženku Cashu + Uložit změny + + +%1$d další + +%1$d další + +%1$d dalšího + +%1$d dalších + + + Doporučuje %1$d další + Doporučují %1$d další + Doporučuje %1$d dalšího + Doporučuje %1$d dalších + + + %1$d čekající faktura + %1$d čekající faktury + %1$d čekající faktury + %1$d čekajících faktur + + + %1$d mint + %1$d minty + %1$d mintu + %1$d mintů + + + %1$d epizoda + %1$d epizody + %1$d epizody + %1$d epizod + diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index b2026ee0d6..fecb150533 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -2844,4 +2844,147 @@ anz der Bedingungen ist erforderlich Nowhere Drop Nowhere Kunst Nowhere Forum + Hashtag hinzufügen + Hinzufügen + Markiere diesen Beitrag öffentlich mit einem Hashtag (NIP-32-Label). Wer dir folgt, sieht ihn im Feed dieses Hashtags. + Hashtag + Hashtag hinzufügen + Empfangen + Einlösen + LN senden + Token senden + Betrag (Sats) + Guthaben + Tausche Proofs… + Stelle Proofs aus… + Rechnung kopieren + Token erstellen + Rechnung verwerfen + Suche nach deiner Wallet… + NIP-60-Wallets werden über Clients hinweg synchronisiert. Wenn du in einer anderen App eine mit diesem Nostr-Schlüssel erstellt hast, sollte sie in wenigen Sekunden erscheinen. + Fertig + Wallet bearbeiten + Angebot einholen + Frage Mint nach einem Angebot… + Verlauf + Empfangen + Gesendet + Lightning-Rechnung (bolt11) + Notiz (optional) + Passende Mints + Noch keine Empfehlungen + Beliebte Mints + ✓ Mint ist erreichbar + ✓ %1$s + Empfohlen von + Mint nicht erreichbar: %1$s + Mint-URL + Mints + Noch keine Cashu-Wallet + Erstelle eine Cashu-Wallet, um Ecash-Tokens zu halten und Nutzaps zu empfangen. + Neuen Schlüssel erzeugen + Macht alle ausstehenden Nutzaps ungültig, die an den aktuellen Schlüssel gebunden sind. + Ein separater privater Schlüssel, der nur zum Empfangen von NIP-61-Nutzaps dient. Nicht dein Nostr-Identitätsschlüssel. + Aktuellen Schlüssel behalten + Privater P2PK-Schlüssel (hex) + Nutzap-Schlüssel (erweitert) + %1$s Sats bezahlt (Gebühren: %2$s Sats). + Rechnung bezahlen + Zahle über Mint… + Fortsetzen + Tippe, um fortzusetzen und den Status zu prüfen. + Mint auswählen + %1$s Sat + bis zu %2$s Sat Gebühren bezahlen? + Bezahle diese Rechnung, um %1$s Sats Ecash zu prägen. + %1$s Sats empfangen. + Diesen Mint empfehlen + Einlösen + %1$s Sats in deine Wallet eingelöst. + Tausche Proofs in deine Wallet… + Mint entfernen + Rechnung anfordern + Frage Mint nach einer Rechnung… + Einen Mint empfehlen + Eine Löschanfrage für deine kind:38000-Empfehlung von %1$s veröffentlichen. Relays, die NIP-09 beachten, entfernen sie. + Empfehlung zurückziehen? + Empfehlung löschen + Wallet-Details bearbeiten + Mints, Nutzap-Schlüssel + Meine Mint-Empfehlungen + Du hast noch keine Mints empfohlen. Tippe auf das Daumen-hoch-Symbol neben einem Mint, um ihn öffentlich zu empfehlen. + Keine wiederherstellbaren Proofs gefunden. + %1$s Sats über %2$s Proofs wiederhergestellt. + Durchsuche Mints… + Leite jedes frühere Geheimnis neu ab und bitte jeden Mint, alle noch gespeicherten blinden Signaturen zurückzugeben. Nützlich nach Geräteverlust oder dem Löschen deiner Token-Events durch ein bösartiges Relay. + Aus Seed wiederherstellen + Cashu-Wallet-Einstellungen + Cashu-Token + Token über %1$s Sats bereit. Teile ihn mit dem Empfänger. + Prüfen + Warte auf die Bezahlung der Rechnung… + Cashu-Wallet + hinzugefügt von + Erstellte Podcasts + Lieblings-Podcasts + Podcast-Folge + Podcast-Sendung + Nutzap + Ereignisreferenz kann nicht erstellt werden + Kein Empfänger-Pubkey in der Notiz + Nutzap fehlgeschlagen + Folgen + Noch keine Folgen gefunden + Folgen ansehen + %1$s Sats verfügbar + Warte auf Zahlung… + Aufladen & Zap senden + Rechnung kopieren + finanziert + Präge neues Ecash aus deiner Lightning-Wallet + braucht %1$s Sat mehr + Hier nicht genug + Mit Lightning bezahlen + der Empfänger + Erneut versuchen + %1$s Sats + Mittel von + Aufzuladender Mint + Zap senden + Lade %1$s Sats zu %2$s auf und zappe %3$s %4$s Sats · Gebühr ≈ %5$s Sats + Zappe %1$s %2$s Sats von %3$s + Mint aufladen + Aufladebetrag + Folgen + Podcasts + Diesen Mint aufladen + Hinzuzufügender Betrag + Aufladen + Mint aufladen + Eine In-App-Ecash-Wallet, die von einem Cashu-Mint gestützt wird. Tokens werden verschlüsselt auf Relays gespeichert. + Cashu-Wallet (NIP-60) + Wallet-Typ wählen + Bezahle über eine externe Lightning-Wallet mit Nostr Wallet Connect. + Lightning-Wallet verbinden (NWC) + Cashu-Wallet bearbeiten + Änderungen speichern + + +%1$d weiterer + +%1$d weitere + + + Von %1$d weiterem empfohlen + Von %1$d weiteren empfohlen + + + %1$d ausstehende Rechnung + %1$d ausstehende Rechnungen + + + %1$d Mint + %1$d Mints + + + %1$d Folge + %1$d Folgen + diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 298247864c..4b30cb7139 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -2752,4 +2752,232 @@ Nowhere Mensagem Nowhere Arte Nowhere Fórum + Adicionar hashtag + Adicionar + Marque publicamente este post com uma hashtag (etiqueta NIP-32). Quem segue você o verá no feed dessa hashtag. + Hashtag + Adicionar uma hashtag + Receber + Resgatar + Enviar LN + Enviar token + Quantia (sats) + Saldo + Trocando provas… + Emitindo provas… + Copiar fatura + Criar token + Descartar fatura + Procurando sua carteira… + Carteiras NIP-60 sincronizam entre clientes. Se você criou uma em outro app com esta chave Nostr, ela deve aparecer em alguns segundos. + Concluído + Editar carteira + Obter cotação + Pedindo cotação ao mint… + Histórico + Recebido + Enviado + Fatura Lightning (bolt11) + Memo (opcional) + Mints correspondentes + Ainda sem recomendações + Mints populares + ✓ O mint está acessível + ✓ %1$s + Recomendado por + Não foi possível acessar o mint: %1$s + URL do mint + Mints + Ainda sem carteira Cashu + Crie uma carteira Cashu para guardar tokens ecash e receber nutzaps. + Gerar uma nova chave + Invalidará quaisquer nutzaps pendentes vinculados à chave atual. + Uma chave privada separada, usada apenas para receber nutzaps NIP-61. Não é a chave da sua identidade Nostr. + Manter chave atual + Chave privada P2PK (hex) + Chave de nutzap (avançado) + Pago %1$s sats (taxas: %2$s sats). + Pagar fatura + Pagando via mint… + Retomar + Toque para retomar e verificar o status. + Escolha um mint + Pagar %1$s sat + até %2$s sat em taxas? + Pague esta fatura para emitir %1$s sats de ecash. + Recebido %1$s sats. + Recomendar este mint + Resgatar + Resgatado %1$s sats para sua carteira. + Trocando provas para sua carteira… + Remover mint + Solicitar fatura + Pedindo uma fatura ao mint… + Recomendar um mint + Publicar um pedido de exclusão para sua recomendação kind:38000 de %1$s. Relays que respeitam o NIP-09 irão removê-la. + Retirar recomendação? + Excluir recomendação + Editar detalhes da carteira + Mints, chave de nutzap + Minhas recomendações de mint + Você ainda não recomendou nenhum mint. Toque no joinha ao lado de um mint para recomendá-lo publicamente. + Nenhuma prova recuperável encontrada. + Recuperados %1$s sats em %2$s provas. + Escaneando mints… + Rederive cada segredo antigo e peça a cada mint que retorne quaisquer assinaturas cegas ainda registradas. Útil após a perda de um dispositivo ou a exclusão dos seus eventos de token por um relay malicioso. + Recuperar a partir da seed + Configurações da carteira Cashu + Token Cashu + Token de %1$s sats pronto. Compartilhe com o destinatário. + Verificar + Aguardando o pagamento da fatura… + Carteira Cashu + adicionado por + Podcasts criados + Podcasts favoritos + Episódio de podcast + Programa de podcast + Nutzap + Não foi possível construir a referência do evento + Sem chave pública do destinatário na nota + Falha no nutzap + Episódios + Nenhum episódio encontrado ainda + Ver episódios + %1$s sats disponíveis + Aguardando pagamento… + Recarregar e enviar zap + Copiar fatura + financiado + Emita novo ecash a partir da sua carteira Lightning + faltam %1$s sat + Não há o suficiente aqui + Pagar com Lightning + o destinatário + Tentar de novo + %1$s sats + Fundos de + Mint a recarregar + Enviar zap + Recarregue %1$s sats para %2$s e envie zap de %3$s %4$s sats · taxa ≈ %5$s sats + Zap de %1$s %2$s sats de %3$s + Recarregar mint + Quantia da recarga + Episódios + Podcasts + Recarregar este mint + Quantia a adicionar + Recarregar + Recarregar mint + Uma carteira ecash dentro do app, baseada em um mint Cashu. Os tokens são armazenados criptografados em relays. + Carteira Cashu (NIP-60) + Escolha o tipo de carteira + Pague por uma carteira Lightning externa usando o Nostr Wallet Connect. + Conectar uma carteira Lightning (NWC) + Editar carteira Cashu + Salvar alterações + Sobre e Jurídico + Adicionar à playlist + Você ainda não tem playlists. Use o campo acima para criar uma. + Gerenciar todas as playlists + Adicionar à playlist + Não foi possível remover os metadados do AVIF: %1$s + Confirmações (%1$d) + Feed + Padrões de segurança infantil + Abrir no navegador + Não é possível reproduzir este vídeo + O codec ou formato não é compatível com este dispositivo (%1$s). Tente abri-lo no seu navegador. + Links + Issues + Playlist de música + Faixa de música + Colaborativa + Criar + Carregando faixa… + Nova playlist + Nome da playlist + Privada + Faixa desconhecida + (playlist sem título) + Álbum (opcional) + Artista + Nome do artista + Arquivo de áudio pronto para envio + Enviar um arquivo de áudio + Escolha um MP3, WAV ou FLAC. Ele será enviado ao seu servidor de mídia ao salvar. + URL do áudio + https://… .mp3 + Enviar uma imagem de capa + Escolha uma imagem quadrada para ser a arte desta faixa. + Excluir esta faixa + Publicar uma exclusão NIP-09 para esta faixa de música? + Letra ou créditos (opcional) + Duração em segundos (opcional) + URL da imagem de capa (opcional) + Título + Título da faixa + Falha no envio: %1$s + Enviando… + Enviando capa + áudio e publicando. Isso continuará mesmo se você sair da tela. + Nova playlist + Nova faixa de música + Baixar + Seja o primeiro a comentar. + Ocultar versões antigas + Mostrar versões antigas + Fonte: %1$s + Sobre + Comentários + Versão mais recente + Links + Plataformas + Tópicos + v%1$s + Nenhum app instalado para processar pagamentos %1$s. Instale uma carteira compatível. + Nowhere Drop + Playlists + Música + Apps + Enviar + Apps + + %1$d min + %1$d min + + + +%1$d mais + +%1$d mais + + + Recomendado por %1$d outro + Recomendado por %1$d outros + + + %1$d fatura pendente + %1$d faturas pendentes + + + %1$d mint + %1$d mints + + + +%1$d faixa a mais + +%1$d faixas a mais + + + %1$d faixa + %1$d faixas + + + %1$d faixa + %1$d faixas + + + %1$d recurso incluído + %1$d recursos incluídos + + + %1$d episódio + %1$d episódios + diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 41c9dbf800..8531f15316 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -2838,4 +2838,147 @@ Nowhere Drop Nowhere Konst Nowhere Forum + Lägg till hashtag + Lägg till + Tagga det här inlägget offentligt med en hashtag (NIP-32-etikett). De som följer dig ser det i den hashtaggens flöde. + Hashtag + Lägg till en hashtag + Ta emot + Lös in + Skicka LN + Skicka token + Belopp (sats) + Saldo + Byter proofs… + Utfärdar proofs… + Kopiera faktura + Skapa token + Förkasta faktura + Letar efter din plånbok… + NIP-60-plånböcker synkroniseras mellan klienter. Om du skapade en i en annan app med den här Nostr-nyckeln bör den dyka upp inom några sekunder. + Klar + Redigera plånbok + Hämta offert + Ber mint om en offert… + Historik + Mottaget + Skickat + Lightning-faktura (bolt11) + Notering (valfritt) + Matchande mints + Inga rekommendationer än + Populära mints + ✓ Mint är nåbar + ✓ %1$s + Rekommenderad av + Kunde inte nå mint: %1$s + Mint-URL + Mints + Ingen Cashu-plånbok än + Skapa en Cashu-plånbok för att hålla ecash-tokens och ta emot nutzaps. + Generera en ny nyckel + Ogiltigförklarar alla väntande nutzaps som är låsta till den nuvarande nyckeln. + En separat privat nyckel som endast används för att ta emot NIP-61-nutzaps. Inte din Nostr-identitetsnyckel. + Behåll nuvarande nyckel + Privat P2PK-nyckel (hex) + Nutzap-nyckel (avancerat) + Betalade %1$s sats (avgifter: %2$s sats). + Betala faktura + Betalar via mint… + Återuppta + Tryck för att återuppta och kontrollera status. + Välj en mint + Betala %1$s sat + upp till %2$s sat i avgifter? + Betala den här fakturan för att utfärda %1$s sats ecash. + Tog emot %1$s sats. + Rekommendera den här mint + Lös in + Löste in %1$s sats till din plånbok. + Byter proofs till din plånbok… + Ta bort mint + Begär faktura + Ber mint om en faktura… + Rekommendera en mint + Publicera en raderingsbegäran för din kind:38000-rekommendation av %1$s. Relayer som följer NIP-09 tar bort den. + Dra tillbaka rekommendation? + Radera rekommendation + Redigera plånboksuppgifter + Mints, nutzap-nyckel + Mina mint-rekommendationer + Du har inte rekommenderat några mints än. Tryck på tummen upp bredvid en mint för att rekommendera den offentligt. + Inga återvinningsbara proofs hittades. + Återställde %1$s sats över %2$s proofs. + Skannar mints… + Härled varje tidigare hemlighet på nytt och be varje mint att returnera alla blinda signaturer som fortfarande finns sparade. Användbart efter en förlorad enhet eller en oseriös relays radering av dina token-händelser. + Återställ från seed + Inställningar för Cashu-plånbok + Cashu-token + Token för %1$s sats redo. Dela den med mottagaren. + Verifiera + Väntar på att fakturan ska betalas… + Cashu-plånbok + tillagd av + Skapade poddar + Favoritpoddar + Poddavsnitt + Poddprogram + Nutzap + Kan inte bygga händelsereferens + Ingen mottagar-pubkey på noten + Nutzap misslyckades + Avsnitt + Inga avsnitt hittades än + Visa avsnitt + %1$s sats tillgängliga + Väntar på betalning… + Fyll på & skicka zap + Kopiera faktura + finansierad + Utfärda nytt ecash från din Lightning-plånbok + behöver %1$s sat till + Inte tillräckligt här + Betala med Lightning + mottagaren + Försök igen + %1$s sats + Medel från + Mint att fylla på + Skicka zap + Fyll på %1$s sats till %2$s och zappa %3$s %4$s sats · avgift ≈ %5$s sats + Zappa %1$s %2$s sats från %3$s + Fyll på mint + Påfyllningsbelopp + Avsnitt + Poddar + Fyll på den här mint + Belopp att lägga till + Fyll på + Fyll på mint + En ecash-plånbok i appen som backas av en Cashu-mint. Tokens lagras krypterade på relayer. + Cashu-plånbok (NIP-60) + Välj plånbokstyp + Betala via en extern Lightning-plånbok med Nostr Wallet Connect. + Anslut en Lightning-plånbok (NWC) + Redigera Cashu-plånbok + Spara ändringar + + +%1$d till + +%1$d till + + + Rekommenderad av %1$d annan + Rekommenderad av %1$d andra + + + %1$d väntande faktura + %1$d väntande fakturor + + + %1$d mint + %1$d mints + + + %1$d avsnitt + %1$d avsnitt + From e54086d8515ae3ccfb47d9808d9a8f451ff4bd20 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 31 May 2026 09:30:16 +0200 Subject: [PATCH 20/21] docs(skill): drop sync-timestamp filter from find-missing-translations The Step 2.5 git "sync-timestamp" heuristic (skip keys added before the last Crowdin export commit) produced false negatives: a key added shortly before an export that translators hadn't reached yet is genuinely missing, but the filter classified it as "Crowdin already decided" and dropped real work. Replace it with the raw on-disk diff reconciled against the Crowdin web UI's untranslated count; source-identical entries are skipped by inspection instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../skills/find-missing-translations/SKILL.md | 89 +++++-------------- 1 file changed, 23 insertions(+), 66 deletions(-) diff --git a/.claude/skills/find-missing-translations/SKILL.md b/.claude/skills/find-missing-translations/SKILL.md index 1f4e249aab..3e195b7088 100644 --- a/.claude/skills/find-missing-translations/SKILL.md +++ b/.claude/skills/find-missing-translations/SKILL.md @@ -19,13 +19,18 @@ Extract string resource keys from the default `values/strings.xml` that are abse This repo syncs translations via Crowdin (branch `l10n_crowdin_translations`). Crowdin's default export behavior **omits any translation that exactly equals the source**, so a key that the translator deliberately kept as English (common for brand terms like `"Nowhere Drop"`, single-word loanwords like `"Apps"` / `"Feed"` / `"Issues"`, or version prefixes like `"v%1$s"`) will not appear in the locale's `strings.xml` even though the Crowdin UI shows it as 100% translated. -Consequences for this skill: +What this means for this skill: -1. **A "missing" key on disk is not always actionable.** It may be Crowdin-stripped (translator already chose source-identical and Crowdin didn't export it) rather than genuinely new. -2. **Don't add source-identical fallbacks locally.** Android's resource resolution falls back to `values/strings.xml` at runtime, so the user already sees the correct text. Local additions will be silently overwritten on Crowdin's next sync anyway. -3. **The only actionable cases are keys Crowdin has never exported.** Whether the translator picked "use English" or simply hasn't translated the key yet, both states are owned by Crowdin and look identical on disk. The local repo cannot distinguish them. +1. **The raw on-disk diff is the candidate set.** A key missing from a locale file is either genuinely untranslated *or* a source-identical entry Crowdin stripped. Both are reported; the human decides which to skip. The Crowdin web UI ("N untranslated") is the ground truth for what genuinely needs work. +2. **Source-identical entries are a small, recognizable minority.** Brand terms (`Nowhere X`), single-word loanwords (`Apps` / `Feed` / `Issues`), and bare version/format strings (`v%1$s`) are the usual cases. Skip these by inspection rather than translating them to something identical. +3. **Don't add source-identical fallbacks.** Android falls back to `values/strings.xml` at runtime, so a key intentionally kept as English already renders correctly, and Crowdin's next sync would strip a local duplicate anyway. -The Step 2.5 filter below uses the **most recent Crowdin export commit reachable from `HEAD`** (subject: `"New Crowdin translations by GitHub Action"`) as the cutoff: any key added to `values/strings.xml` after that commit is genuinely new (Crowdin hasn't exported it yet); anything older is Crowdin's responsibility regardless of why it's missing. The reachable-from-HEAD check survives the common workflow of deleting the `l10n_crowdin_translations` branch after merging. +> **Historical note:** an earlier version of this skill tried to auto-filter the +> candidate list with a git "sync-timestamp" heuristic (skip any key added before +> the last `New Crowdin translations` commit). It was **dropped** because it +> produced false negatives: a key added shortly before an export that translators +> simply hadn't reached yet is genuinely missing, but the heuristic classified it +> as "Crowdin already decided." Trust the raw diff + the Crowdin UI instead. ## Target Locales @@ -72,74 +77,27 @@ comm -23 \ | sed 's/.*name="\([^"]*\)".*/\1/' | sort) ``` -This gives two lists of missing key names — keep them separate; `` translations need the per-locale CLDR category set (see Step 5 → "Plurals: handle with care"). Do NOT diff each locale separately for strings — assume the same keys are missing in all target locales (but DO repeat the `` diff per locale if you suspect Crowdin asymmetric stripping). +This gives two lists of missing key names — keep them separate; `` translations need the per-locale CLDR category set (see Step 5 → "Plurals: handle with care"). -> **Caveat:** Crowdin can asymmetrically strip keys across locales (each translator independently chose source-identical for different keys). If the cs-rCZ list looks suspiciously short, run the same diff for each target locale individually and union the results before Step 2.5. - -### 2.5. Filter out keys Crowdin has already seen (sync-timestamp check) - -A missing key is **only actionable if Crowdin has never exported it**. Once a key has been pushed to Crowdin and exported back, the translator may have chosen "use English" — Crowdin stores that choice in its own database and strips the entry from the exported `strings.xml`. From disk we cannot tell "translator picked English" from "Crowdin never saw the key": both look identical. - -The reliable signal is **time**: compare when the key was added to `values/strings.xml` against the timestamp of the **most recent Crowdin export that has been merged into the current branch**. Crowdin's GitHub Action produces commits with the literal subject `New Crowdin translations by GitHub Action`; finding the latest such commit reachable from `HEAD` works even if the `l10n_crowdin_translations` branch has been deleted post-merge (a common cleanup workflow). - -- Key added **before** that commit → Crowdin saw it on a prior export; translator made a decision; the absence on disk is a deliberate "use English" or "leave blank" choice. **Skip.** -- Key added **after** → Crowdin has not exported it yet; genuinely new and actionable. +Crowdin can asymmetrically strip keys across locales (each translator independently chose source-identical for different keys), so **cs-rCZ is not a reliable upper bound**. Diff **every** target locale and union the results — don't assume the cs-rCZ set covers the others. A quick per-locale count is a useful sanity check against the Crowdin UI's "N untranslated": ```bash -# Latest Crowdin export reachable from HEAD (survives branch deletion). -sync_ts=$(git log -1 --format=%ct --grep='^New Crowdin translations by GitHub Action$' 2>/dev/null) -if [ -z "$sync_ts" ]; then - echo "WARNING: no Crowdin export commit found in history; treating all missing as actionable" >&2 - sync_ts=0 -else - echo "Crowdin sync cutoff: $(git log -1 --format='%ci %h' --grep='^New Crowdin translations by GitHub Action$')" -fi - -# For each locale, list only keys added after the Crowdin sync (truly new). -# Run for BOTH and . -for locale in cs-rCZ de-rDE sv-rSE; do - echo "=== $locale: genuinely new (post-sync) keys ===" - comm -23 \ +for locale in cs-rCZ de-rDE sv-rSE pt-rBR; do + ns=$(comm -23 \ <(grep ' keys ===" - comm -23 \ + | sed 's/.*name="\([^"]*\)".*/\1/' | sort) | wc -l) + np=$(comm -23 \ <(grep '` is a separate resource type; a source `` missing from a locale will never show up in a `` diff. Always run the diff twice (once per resource type) as shown in Step 2. The same goes for `` if the project uses it. -- **Treating every missing key as actionable** — Crowdin strips on export any translation the translator marked as "use English", and we cannot distinguish that from "never seen" by looking at disk. Use the Step 2.5 sync-timestamp filter: only keys added to `values/strings.xml` after the last `l10n_crowdin_translations` sync are genuinely new. -- **Trying to detect "stripped" from git history alone** — the on-disk locale file only sees keys the translator typed a non-identical value for. The "translator opened the key and picked English from the start" case never touches disk, so a history-only check misses it. Use the sync-timestamp cutoff instead. -- **Adding source-identical fallbacks locally** — they get overwritten on the next Crowdin sync. Android falls back to `values/strings.xml` at runtime anyway, so there is no user-visible bug to fix. -- **Skipping per-locale diffs when only diffing cs-rCZ** — Crowdin can strip different keys in different locales (each translator's choice), so cs-rCZ is not a reliable upper bound. Diff each target locale, then apply the sync-timestamp filter. +- **Trusting a git "sync-timestamp" heuristic to pre-filter the list** — this skill used to skip keys added before the last `New Crowdin translations` commit, on the theory that Crowdin had already "decided" them. It was dropped: a key added shortly before an export that translators hadn't reached yet is genuinely missing, so the heuristic silently dropped real work. Use the raw on-disk diff and reconcile against the Crowdin web UI's untranslated count instead. +- **Adding source-identical fallbacks locally** — they get overwritten on the next Crowdin sync. Android falls back to `values/strings.xml` at runtime anyway, so a key intentionally kept as English already renders correctly. Skip these by inspection (brand terms, loanwords, `v%1$s`-style strings); don't translate them to an identical value. +- **Skipping per-locale diffs when only diffing cs-rCZ** — Crowdin can strip different keys in different locales (each translator's choice), so cs-rCZ is not a reliable upper bound. Diff each target locale and union the results. - **Inserting strings in a specific position** — always append at the bottom; ordering is handled separately - **Hardcoding `"1"` in a `` `quantity="one"` item** — always use the count placeholder; otherwise non-English `one` categories produce wrong text - **Copying English's `one`/`other` set into every locale** — each language must include all CLDR plural categories it uses (e.g. Czech needs `one`, `few`, `many`, `other`) From 5adab91a78a7ddfb2d0c84d0fd2337636ee5d293 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 31 May 2026 07:32:01 +0000 Subject: [PATCH 21/21] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 297 ++++++------ .../src/main/res/values-de-rDE/strings.xml | 282 ++++++------ .../src/main/res/values-hi-rIN/strings.xml | 1 - .../src/main/res/values-hu-rHU/strings.xml | 1 - .../src/main/res/values-pl-rPL/strings.xml | 11 +- .../src/main/res/values-pt-rBR/strings.xml | 432 +++++++++--------- .../src/main/res/values-sv-rSE/strings.xml | 278 ++++++----- .../src/main/res/values-zh-rCN/strings.xml | 30 +- 8 files changed, 656 insertions(+), 676 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 06447dd601..388fb84420 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -747,6 +747,17 @@ Články Hudba Playlisty + Epizody + Podcasty + Zobrazit epizody + Epizody + Zatím nenalezeny žádné epizody + + %1$d epizoda + %1$d epizody + %1$d epizody + %1$d epizod + Nová hudební stopa Nový playlist Název @@ -781,6 +792,11 @@ Přidat do veřejných záložek Odebrat ze soukromých záložek Odebrat z veřejných záložek + Přidat hashtag + Přidat hashtag + Veřejně označte tento příspěvek hashtagem (štítek NIP-32). Lidé, kteří vás sledují, jej uvidí ve feedu daného hashtagu. + Přidat + přidal(a) Připnuté poznámky Vaše připnuté poznámky Připnout na profil @@ -849,6 +865,28 @@ Rychlé částky Zap Zobrazí se při stisknutí tlačítka zap. Klepnutím na částku ji odeberete. Pokud ponecháte prázdné, otevře se pokaždé dialog pro zadání částky. Odeslat on-chain + Dobít mint + Částka dobití + Mint k dobití + Prostředky z + Zaplatit pomocí Lightning + Vydejte nový ecash z vaší Lightning peněženky + Tady není dost + %1$s sats k dispozici + Dobít a poslat zap + Poslat zap + Čekám na platbu… + Zkusit znovu + Dobít %1$s sats do %2$s a poslat %3$s zap %4$s sats · poplatek ≈ %5$s sats + Poslat zap %1$s %2$s sats z %3$s + příjemce + chybí %1$s sat + financováno + Kopírovat fakturu + Dobít mint + Dobít tento mint + Částka k přidání + Dobít Soukromí Zap Určuje, jak je zobrazena vaše identita při odesílání zapsu. Připojit peněženku @@ -1733,6 +1771,99 @@ Žádné peněženky nejsou připojeny Připojte jednu nebo více NWC peněženek pro odesílání a přijímání plateb. Přidat NWC připojení + Zvolte typ peněženky + Připojit Lightning peněženku (NWC) + Plaťte přes externí Lightning peněženku pomocí Nostr Wallet Connect. + Peněženka Cashu (NIP-60) + Ecash peněženka v aplikaci založená na mintu Cashu. Tokeny jsou uloženy zašifrované na relayích. + Peněženka Cashu + Zatím žádná peněženka Cashu + Vytvořte si peněženku Cashu pro uchovávání tokenů ecash a příjem nutzapů. + Hledám vaši peněženku… + Peněženky NIP-60 se synchronizují mezi klienty. Pokud jste si nějakou vytvořili v jiné aplikaci pod tímto klíčem Nostr, měla by se za pár sekund objevit. + Zůstatek + Minty + URL mintu + Odebrat mint + Historie + Upravit peněženku + Klíč pro nutzap (pokročilé) + Samostatný soukromý klíč používaný pouze k příjmu nutzapů NIP-61. Není to klíč vaší identity Nostr. + Vygenerovat nový klíč + Zneplatní všechny čekající nutzapy uzamčené ke stávajícímu klíči. + Ponechat stávající klíč + Soukromý klíč P2PK (hex) + Upravit peněženku Cashu + Uložit změny + Zatím žádná doporučení + Doporučuje + + +%1$d další + +%1$d další + +%1$d dalšího + +%1$d dalších + + + Doporučuje %1$d další + Doporučují %1$d další + Doporučuje %1$d dalšího + Doporučuje %1$d dalších + + Oblíbené minty + Odpovídající minty + Doporučit tento mint + Přijmout + Poslat LN + Poslat token + Uplatnit + Částka (sats) + Vyberte mint + Poznámka (volitelné) + Lightning faktura (bolt11) + Token Cashu + Kopírovat fakturu + Vyžádat fakturu + Zahodit fakturu + Přijato + Odesláno + Nastavení peněženky Cashu + Upravit údaje peněženky + Minty, klíč pro nutzap + Moje doporučení mintů + Zatím jste nedoporučili žádný mint. Klepnutím na palec nahoru vedle mintu jej veřejně doporučíte. + Smazat doporučení + Stáhnout doporučení? + Publikovat žádost o smazání vašeho doporučení kind:38000 pro %1$s. Relaye, které dodržují NIP-09, jej odstraní. + Doporučit mint + Obnovit ze seedu + Znovu odvoďte každé minulé tajemství a požádejte každý mint o vrácení všech slepých podpisů, které stále eviduje. Užitečné po ztrátě zařízení nebo smazání vašich token událostí podvodným relayem. + Prohledávám minty… + Nenalezeny žádné obnovitelné proofy. + Obnoveno %1$s sats z %2$s proofů. + Zaplatit fakturu + Získat nabídku + Žádám mint o nabídku… + Zaplatit %1$s sat + až %2$s sat na poplatcích? + Ověřit + ✓ Mint je dostupný + Nelze se připojit k mintu: %1$s + Nutzap selhal + V poznámce chybí veřejný klíč příjemce + Nelze sestavit odkaz na událost + Vytvořit token + Uplatnit + Hotovo + Žádám mint o fakturu… + Čekám na zaplacení faktury… + Vydávám proofy… + Platím přes mint… + Vyměňuji proofy… + Vyměňuji proofy do vaší peněženky… + Zaplaťte tuto fakturu pro vydání %1$s sats v ecash. + Přijato %1$s sats. + Zaplaceno %1$s sats (poplatky: %2$s sats). + Token na %1$s sats je připraven. Sdílejte jej s příjemcem. + Uplatněno %1$s sats do vaší peněženky. Vložit URI nostr+walletconnect:// Uložit Přejmenovat @@ -1741,7 +1872,6 @@ Neplatné URI NWC připojení Posunout nahoru Posunout dolů - Změnit pořadí peněženek On-chain transakce Pro tento účet není k dispozici žádná on-chain adresa. Není nakonfigurován žádný on-chain backend. @@ -2342,6 +2472,10 @@ Audio stopa Hudební stopa Hudební playlist + Epizoda podcastu + Podcast + Vytvořené podcasty + Oblíbené podcasty Ocenění odznaků Definice odznaků Přijatá sada odznaků @@ -2596,6 +2730,14 @@ %1$d let %1$d let + + %1$d čekající faktura + %1$d čekající faktury + %1$d čekající faktury + %1$d čekajících faktur + + Klepnutím pokračujte a zkontrolujte stav. + Pokračovat <%1$s Připojování Stahování @@ -2881,157 +3023,4 @@ Nowhere Drop Nowhere Umění Nowhere Fórum - Přidat hashtag - Přidat - Veřejně označte tento příspěvek hashtagem (štítek NIP-32). Lidé, kteří vás sledují, jej uvidí ve feedu daného hashtagu. - Hashtag - Přidat hashtag - Přijmout - Uplatnit - Poslat LN - Poslat token - Částka (sats) - Zůstatek - Vyměňuji proofy… - Vydávám proofy… - Kopírovat fakturu - Vytvořit token - Zahodit fakturu - Hledám vaši peněženku… - Peněženky NIP-60 se synchronizují mezi klienty. Pokud jste si nějakou vytvořili v jiné aplikaci pod tímto klíčem Nostr, měla by se za pár sekund objevit. - Hotovo - Upravit peněženku - Získat nabídku - Žádám mint o nabídku… - Historie - Přijato - Odesláno - Lightning faktura (bolt11) - Poznámka (volitelné) - Odpovídající minty - Zatím žádná doporučení - Oblíbené minty - ✓ Mint je dostupný - ✓ %1$s - Doporučuje - Nelze se připojit k mintu: %1$s - URL mintu - Minty - Zatím žádná peněženka Cashu - Vytvořte si peněženku Cashu pro uchovávání tokenů ecash a příjem nutzapů. - Vygenerovat nový klíč - Zneplatní všechny čekající nutzapy uzamčené ke stávajícímu klíči. - Samostatný soukromý klíč používaný pouze k příjmu nutzapů NIP-61. Není to klíč vaší identity Nostr. - Ponechat stávající klíč - Soukromý klíč P2PK (hex) - Klíč pro nutzap (pokročilé) - Zaplaceno %1$s sats (poplatky: %2$s sats). - Zaplatit fakturu - Platím přes mint… - Pokračovat - Klepnutím pokračujte a zkontrolujte stav. - Vyberte mint - Zaplatit %1$s sat + až %2$s sat na poplatcích? - Zaplaťte tuto fakturu pro vydání %1$s sats v ecash. - Přijato %1$s sats. - Doporučit tento mint - Uplatnit - Uplatněno %1$s sats do vaší peněženky. - Vyměňuji proofy do vaší peněženky… - Odebrat mint - Vyžádat fakturu - Žádám mint o fakturu… - Doporučit mint - Publikovat žádost o smazání vašeho doporučení kind:38000 pro %1$s. Relaye, které dodržují NIP-09, jej odstraní. - Stáhnout doporučení? - Smazat doporučení - Upravit údaje peněženky - Minty, klíč pro nutzap - Moje doporučení mintů - Zatím jste nedoporučili žádný mint. Klepnutím na palec nahoru vedle mintu jej veřejně doporučíte. - Nenalezeny žádné obnovitelné proofy. - Obnoveno %1$s sats z %2$s proofů. - Prohledávám minty… - Znovu odvoďte každé minulé tajemství a požádejte každý mint o vrácení všech slepých podpisů, které stále eviduje. Užitečné po ztrátě zařízení nebo smazání vašich token událostí podvodným relayem. - Obnovit ze seedu - Nastavení peněženky Cashu - Token Cashu - Token na %1$s sats je připraven. Sdílejte jej s příjemcem. - Ověřit - Čekám na zaplacení faktury… - Peněženka Cashu - přidal(a) - Vytvořené podcasty - Oblíbené podcasty - Epizoda podcastu - Podcast - Nutzap - Nelze sestavit odkaz na událost - V poznámce chybí veřejný klíč příjemce - Nutzap selhal - Epizody - Zatím nenalezeny žádné epizody - Zobrazit epizody - %1$s sats k dispozici - Čekám na platbu… - Dobít a poslat zap - Kopírovat fakturu - financováno - Vydejte nový ecash z vaší Lightning peněženky - chybí %1$s sat - Tady není dost - Zaplatit pomocí Lightning - příjemce - Zkusit znovu - %1$s sats - Prostředky z - Mint k dobití - Poslat zap - Dobít %1$s sats do %2$s a poslat %3$s zap %4$s sats · poplatek ≈ %5$s sats - Poslat zap %1$s %2$s sats z %3$s - Dobít mint - Částka dobití - Epizody - Podcasty - Dobít tento mint - Částka k přidání - Dobít - Dobít mint - Ecash peněženka v aplikaci založená na mintu Cashu. Tokeny jsou uloženy zašifrované na relayích. - Peněženka Cashu (NIP-60) - Zvolte typ peněženky - Plaťte přes externí Lightning peněženku pomocí Nostr Wallet Connect. - Připojit Lightning peněženku (NWC) - Upravit peněženku Cashu - Uložit změny - - +%1$d další - +%1$d další - +%1$d dalšího - +%1$d dalších - - - Doporučuje %1$d další - Doporučují %1$d další - Doporučuje %1$d dalšího - Doporučuje %1$d dalších - - - %1$d čekající faktura - %1$d čekající faktury - %1$d čekající faktury - %1$d čekajících faktur - - - %1$d mint - %1$d minty - %1$d mintu - %1$d mintů - - - %1$d epizoda - %1$d epizody - %1$d epizody - %1$d epizod - diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index fecb150533..696ec8d6a4 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -737,6 +737,14 @@ anz der Bedingungen ist erforderlich Artikel Musik Wiedergabelisten + Folgen + Folgen ansehen + Folgen + Noch keine Folgen gefunden + + %1$d Folge + %1$d Folgen + Neuer Musiktitel Neue Playlist Titel @@ -769,6 +777,11 @@ anz der Bedingungen ist erforderlich Zu den öffentlichen Lesezeichen hinzufügen Aus den privaten Lesezeichen entfernen Aus den öffentlichen Lesezeichen entfernen + Hashtag hinzufügen + Hashtag hinzufügen + Markiere diesen Beitrag öffentlich mit einem Hashtag (NIP-32-Label). Wer dir folgt, sieht ihn im Feed dieses Hashtags. + Hinzufügen + hinzugefügt von Angeheftete Notizen Deine angepinnten Notizen An Profil anheften @@ -837,6 +850,29 @@ anz der Bedingungen ist erforderlich Schnelle Zap-Beträge Wird beim Drücken der Zap-Schaltfläche angezeigt. Tippe auf einen Betrag, um ihn zu entfernen. Wenn du es leer lässt, wird jedes Mal ein Dialog zur Eingabe eines Betrags geöffnet. Stattdessen On-Chain senden + Mint aufladen + %1$s Sats + Aufladebetrag + Aufzuladender Mint + Mittel von + Mit Lightning bezahlen + Präge neues Ecash aus deiner Lightning-Wallet + Hier nicht genug + %1$s Sats verfügbar + Aufladen & Zap senden + Zap senden + Warte auf Zahlung… + Erneut versuchen + Lade %1$s Sats zu %2$s auf und zappe %3$s %4$s Sats · Gebühr ≈ %5$s Sats + Zappe %1$s %2$s Sats von %3$s + der Empfänger + braucht %1$s Sat mehr + finanziert + Rechnung kopieren + Mint aufladen + Diesen Mint aufladen + Hinzuzufügender Betrag + Aufladen Zap-Datenschutz Legt fest, wie deine Identität beim Senden eines Zaps angezeigt wird. Wallet verbinden @@ -1716,6 +1752,94 @@ anz der Bedingungen ist erforderlich Keine Wallets verbunden Verbinde eine oder mehrere NWC-Wallets, um Zahlungen zu senden und zu empfangen. NWC-Verbindung hinzufügen + Wallet-Typ wählen + Lightning-Wallet verbinden (NWC) + Bezahle über eine externe Lightning-Wallet mit Nostr Wallet Connect. + Cashu-Wallet (NIP-60) + Eine In-App-Ecash-Wallet, die von einem Cashu-Mint gestützt wird. Tokens werden verschlüsselt auf Relays gespeichert. + Cashu-Wallet + Noch keine Cashu-Wallet + Erstelle eine Cashu-Wallet, um Ecash-Tokens zu halten und Nutzaps zu empfangen. + Suche nach deiner Wallet… + NIP-60-Wallets werden über Clients hinweg synchronisiert. Wenn du in einer anderen App eine mit diesem Nostr-Schlüssel erstellt hast, sollte sie in wenigen Sekunden erscheinen. + Guthaben + Mint-URL + Mint entfernen + Verlauf + Wallet bearbeiten + Nutzap-Schlüssel (erweitert) + Ein separater privater Schlüssel, der nur zum Empfangen von NIP-61-Nutzaps dient. Nicht dein Nostr-Identitätsschlüssel. + Neuen Schlüssel erzeugen + Macht alle ausstehenden Nutzaps ungültig, die an den aktuellen Schlüssel gebunden sind. + Aktuellen Schlüssel behalten + Privater P2PK-Schlüssel (hex) + Cashu-Wallet bearbeiten + Änderungen speichern + Noch keine Empfehlungen + Empfohlen von + + +%1$d weiterer + +%1$d weitere + + + Von %1$d weiterem empfohlen + Von %1$d weiteren empfohlen + + Beliebte Mints + Passende Mints + Diesen Mint empfehlen + Empfangen + LN senden + Token senden + Einlösen + Betrag (Sats) + Mint auswählen + Notiz (optional) + Lightning-Rechnung (bolt11) + Cashu-Token + Rechnung kopieren + Rechnung anfordern + Rechnung verwerfen + Empfangen + Gesendet + Cashu-Wallet-Einstellungen + Wallet-Details bearbeiten + Mints, Nutzap-Schlüssel + Meine Mint-Empfehlungen + Du hast noch keine Mints empfohlen. Tippe auf das Daumen-hoch-Symbol neben einem Mint, um ihn öffentlich zu empfehlen. + Empfehlung löschen + Empfehlung zurückziehen? + Eine Löschanfrage für deine kind:38000-Empfehlung von %1$s veröffentlichen. Relays, die NIP-09 beachten, entfernen sie. + Einen Mint empfehlen + Aus Seed wiederherstellen + Leite jedes frühere Geheimnis neu ab und bitte jeden Mint, alle noch gespeicherten blinden Signaturen zurückzugeben. Nützlich nach Geräteverlust oder dem Löschen deiner Token-Events durch ein bösartiges Relay. + Durchsuche Mints… + Keine wiederherstellbaren Proofs gefunden. + %1$s Sats über %2$s Proofs wiederhergestellt. + Rechnung bezahlen + Angebot einholen + Frage Mint nach einem Angebot… + %1$s Sat + bis zu %2$s Sat Gebühren bezahlen? + Prüfen + ✓ Mint ist erreichbar + Mint nicht erreichbar: %1$s + Nutzap fehlgeschlagen + Kein Empfänger-Pubkey in der Notiz + Ereignisreferenz kann nicht erstellt werden + Token erstellen + Einlösen + Fertig + Frage Mint nach einer Rechnung… + Warte auf die Bezahlung der Rechnung… + Stelle Proofs aus… + Zahle über Mint… + Tausche Proofs… + Tausche Proofs in deine Wallet… + Bezahle diese Rechnung, um %1$s Sats Ecash zu prägen. + %1$s Sats empfangen. + %1$s Sats bezahlt (Gebühren: %2$s Sats). + Token über %1$s Sats bereit. Teile ihn mit dem Empfänger. + %1$s Sats in deine Wallet eingelöst. nostr+walletconnect://-URI einfügen Speichern Umbenennen @@ -1724,7 +1848,6 @@ anz der Bedingungen ist erforderlich Ungültige NWC-Verbindungs-URI Nach oben Nach unten - Wallets neu anordnen On-Chain-Transaktionen Keine On-Chain-Adresse für dieses Konto verfügbar. Kein On-Chain-Backend konfiguriert. @@ -2317,6 +2440,10 @@ anz der Bedingungen ist erforderlich Audiospur Musiktitel Musik-Playlist + Podcast-Folge + Podcast-Sendung + Erstellte Podcasts + Lieblings-Podcasts Abzeichen-Auszeichnungen Abzeichen-Definitionen Akzeptiertes Abzeichen-Set @@ -2559,6 +2686,16 @@ anz der Bedingungen ist erforderlich %1$d Jahr %1$d Jahre + + %1$d Mint + %1$d Mints + + + %1$d ausstehende Rechnung + %1$d ausstehende Rechnungen + + Tippe, um fortzusetzen und den Status zu prüfen. + Fortsetzen <%1$s Verbinden Herunterladen @@ -2844,147 +2981,4 @@ anz der Bedingungen ist erforderlich Nowhere Drop Nowhere Kunst Nowhere Forum - Hashtag hinzufügen - Hinzufügen - Markiere diesen Beitrag öffentlich mit einem Hashtag (NIP-32-Label). Wer dir folgt, sieht ihn im Feed dieses Hashtags. - Hashtag - Hashtag hinzufügen - Empfangen - Einlösen - LN senden - Token senden - Betrag (Sats) - Guthaben - Tausche Proofs… - Stelle Proofs aus… - Rechnung kopieren - Token erstellen - Rechnung verwerfen - Suche nach deiner Wallet… - NIP-60-Wallets werden über Clients hinweg synchronisiert. Wenn du in einer anderen App eine mit diesem Nostr-Schlüssel erstellt hast, sollte sie in wenigen Sekunden erscheinen. - Fertig - Wallet bearbeiten - Angebot einholen - Frage Mint nach einem Angebot… - Verlauf - Empfangen - Gesendet - Lightning-Rechnung (bolt11) - Notiz (optional) - Passende Mints - Noch keine Empfehlungen - Beliebte Mints - ✓ Mint ist erreichbar - ✓ %1$s - Empfohlen von - Mint nicht erreichbar: %1$s - Mint-URL - Mints - Noch keine Cashu-Wallet - Erstelle eine Cashu-Wallet, um Ecash-Tokens zu halten und Nutzaps zu empfangen. - Neuen Schlüssel erzeugen - Macht alle ausstehenden Nutzaps ungültig, die an den aktuellen Schlüssel gebunden sind. - Ein separater privater Schlüssel, der nur zum Empfangen von NIP-61-Nutzaps dient. Nicht dein Nostr-Identitätsschlüssel. - Aktuellen Schlüssel behalten - Privater P2PK-Schlüssel (hex) - Nutzap-Schlüssel (erweitert) - %1$s Sats bezahlt (Gebühren: %2$s Sats). - Rechnung bezahlen - Zahle über Mint… - Fortsetzen - Tippe, um fortzusetzen und den Status zu prüfen. - Mint auswählen - %1$s Sat + bis zu %2$s Sat Gebühren bezahlen? - Bezahle diese Rechnung, um %1$s Sats Ecash zu prägen. - %1$s Sats empfangen. - Diesen Mint empfehlen - Einlösen - %1$s Sats in deine Wallet eingelöst. - Tausche Proofs in deine Wallet… - Mint entfernen - Rechnung anfordern - Frage Mint nach einer Rechnung… - Einen Mint empfehlen - Eine Löschanfrage für deine kind:38000-Empfehlung von %1$s veröffentlichen. Relays, die NIP-09 beachten, entfernen sie. - Empfehlung zurückziehen? - Empfehlung löschen - Wallet-Details bearbeiten - Mints, Nutzap-Schlüssel - Meine Mint-Empfehlungen - Du hast noch keine Mints empfohlen. Tippe auf das Daumen-hoch-Symbol neben einem Mint, um ihn öffentlich zu empfehlen. - Keine wiederherstellbaren Proofs gefunden. - %1$s Sats über %2$s Proofs wiederhergestellt. - Durchsuche Mints… - Leite jedes frühere Geheimnis neu ab und bitte jeden Mint, alle noch gespeicherten blinden Signaturen zurückzugeben. Nützlich nach Geräteverlust oder dem Löschen deiner Token-Events durch ein bösartiges Relay. - Aus Seed wiederherstellen - Cashu-Wallet-Einstellungen - Cashu-Token - Token über %1$s Sats bereit. Teile ihn mit dem Empfänger. - Prüfen - Warte auf die Bezahlung der Rechnung… - Cashu-Wallet - hinzugefügt von - Erstellte Podcasts - Lieblings-Podcasts - Podcast-Folge - Podcast-Sendung - Nutzap - Ereignisreferenz kann nicht erstellt werden - Kein Empfänger-Pubkey in der Notiz - Nutzap fehlgeschlagen - Folgen - Noch keine Folgen gefunden - Folgen ansehen - %1$s Sats verfügbar - Warte auf Zahlung… - Aufladen & Zap senden - Rechnung kopieren - finanziert - Präge neues Ecash aus deiner Lightning-Wallet - braucht %1$s Sat mehr - Hier nicht genug - Mit Lightning bezahlen - der Empfänger - Erneut versuchen - %1$s Sats - Mittel von - Aufzuladender Mint - Zap senden - Lade %1$s Sats zu %2$s auf und zappe %3$s %4$s Sats · Gebühr ≈ %5$s Sats - Zappe %1$s %2$s Sats von %3$s - Mint aufladen - Aufladebetrag - Folgen - Podcasts - Diesen Mint aufladen - Hinzuzufügender Betrag - Aufladen - Mint aufladen - Eine In-App-Ecash-Wallet, die von einem Cashu-Mint gestützt wird. Tokens werden verschlüsselt auf Relays gespeichert. - Cashu-Wallet (NIP-60) - Wallet-Typ wählen - Bezahle über eine externe Lightning-Wallet mit Nostr Wallet Connect. - Lightning-Wallet verbinden (NWC) - Cashu-Wallet bearbeiten - Änderungen speichern - - +%1$d weiterer - +%1$d weitere - - - Von %1$d weiterem empfohlen - Von %1$d weiteren empfohlen - - - %1$d ausstehende Rechnung - %1$d ausstehende Rechnungen - - - %1$d Mint - %1$d Mints - - - %1$d Folge - %1$d Folgen - diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index ab8718e9b8..3649b1555c 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -1824,7 +1824,6 @@ अमान्य नोस्टर धनकोष संयोजन पता ऊर्ध्वगमन अधोगमन - धनकोष पुनःव्यवस्थित करें खण्डश्रृंखला सत्यापित लेनदेन इस लेखा के लिए कोई खण्डश्रृंखला पता उपलब्ध नहीं। कोई खणडश्रृंखला पृष्ठभाग समाकृत नहीं। diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index c6ced7eaad..e9128b7f57 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -1824,7 +1824,6 @@ Érvénytelen NWC-kapcsolati cím Mozgatás felfelé Mozgatás lefelé - Pénztárca újrarendezése Láncon belüli tranzakciók Ehhez a fiókhoz nincs elérhető láncon belüli cím. Nincs beállítva lánc-háttérprogram. diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index c3398245ba..d641f25c76 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -792,6 +792,12 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Dodaj do publicznych zakładek Usuń z prywatnych zakładek Usuń z publicznych zakładek + Dodaj hasztag + Dodaj hasztag + Publicznie oznacz ten post hasztagiem (etykietą NIP-32). Osoby, które Cię obserwują, zobaczą go w kanale tego hasztagu. + Hasztag + Dodaj + dodany przez Przypięte notatki Twoje przypięte notatki Przypnij do profilu @@ -1436,7 +1442,7 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Dodaje Geohash twojej lokalizacji do wpisu. Użytkownicy będą wiedzieli, że jesteś mniej niż 5 km od bieżącej lokalizacji Post ekskluzywny dla lokalizacji Tylko obserwatorzy z twojej lokalizacji to zobaczą. Twoi ogólni obserwatorzy nie zobaczą tego. - Wpis hashtagowy + Wpis hasztagowy Tylko obserwujący ten hashtag to zobaczą. Twoi ogólni obserwujący nie zobaczą tego. Skomentuj na stronie internetowej Skomentuj na zewnętrznym serwisie @@ -1869,7 +1875,6 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Nieprawidłowy URI połączenia NWC W górę W dół - Powtórne zamów portfel Transakcje w łańcuchu Brak dostępnego adresu w łańcuchu dla tego konta. Nie skonfigurowano backendu łańcucha. @@ -2332,7 +2337,7 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Udostępnij film… Nie można udostępnić filmu, spróbuj ponownie później… Pobieranie filmu… - Szukaj tagu: #%1$s + Szukaj hasztagu: #%1$s Wszytko Osoby Wpisy diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 4b30cb7139..770c604570 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -12,6 +12,10 @@ Esta publicação tem mais de %1$d hashtag Esta publicação tem mais de %1$d hashtags + + %1$d recurso incluído + %1$d recursos incluídos + postagem não encontrada 👀 Imagem do Canal @@ -127,6 +131,7 @@ Escreva uma mensagem… Salvar Salvar + Enviar Criar Renomear Cancelar @@ -550,6 +555,16 @@ Escolha quais dos selos que você recebeu aparecerão no seu perfil. Você ainda não recebeu nenhum selo. Imagens + Fonte: %1$s + Baixar + Sobre + Plataformas + Tópicos + Versão mais recente + Comentários + Seja o primeiro a comentar. + Mostrar versões antigas + Ocultar versões antigas Calendários Curtas Chats públicos @@ -710,12 +725,51 @@ Ainda não há servidores. Adicione um abaixo ou escolha um recomendado. Vídeos Artigos + Música + Episódios + Ver episódios + Episódios + Nenhum episódio encontrado ainda + + %1$d episódio + %1$d episódios + + Nova faixa de música + Nova playlist + Título + Título da faixa + Artista + Nome do artista + URL do áudio + URL da imagem de capa (opcional) + Álbum (opcional) + Duração em segundos (opcional) + Letra ou créditos (opcional) + Excluir esta faixa + Publicar uma exclusão NIP-09 para esta faixa de música? + Adicionar à playlist + Adicionar à playlist + Você ainda não tem playlists. Use o campo acima para criar uma. + Gerenciar todas as playlists + Nova playlist + Nome da playlist + Criar + (playlist sem título) + + %1$d faixa + %1$d faixas + Itens Salvos Privados Itens Salvos Públicos Adicionar aos Itens Salvos Privados Adicionar aos Itens Salvos Públicos Remover dos Itens Salvos Privados Remover dos Itens Salvos Públicos + Adicionar hashtag + Adicionar uma hashtag + Marque publicamente este post com uma hashtag (etiqueta NIP-32). Quem segue você o verá no feed dessa hashtag. + Adicionar + adicionado por Notas Fixadas Suas notas fixadas Fixar no Perfil @@ -784,6 +838,28 @@ Valores Rápidos de Zap Exibido ao pressionar o botão de zap. Toque em um valor para removê-lo. Se deixar vazio, abrirá o diálogo para inserir um valor todas as vezes. Enviar on-chain em vez disso + Recarregar mint + Quantia da recarga + Mint a recarregar + Fundos de + Pagar com Lightning + Emita novo ecash a partir da sua carteira Lightning + Não há o suficiente aqui + %1$s sats disponíveis + Recarregar e enviar zap + Enviar zap + Aguardando pagamento… + Tentar de novo + Recarregue %1$s sats para %2$s e envie zap de %3$s %4$s sats · taxa ≈ %5$s sats + Zap de %1$s %2$s sats de %3$s + o destinatário + faltam %1$s sat + financiado + Copiar fatura + Recarregar mint + Recarregar este mint + Quantia a adicionar + Recarregar Privacidade do Zap Controla como sua identidade é exibida ao enviar um Zap. Conectar Carteira @@ -874,6 +950,7 @@ Endereço Excluir destino de pagamento Nenhum aplicativo encontrado para processar este pagamento. Por favor, instale uma carteira compatível. + Nenhum app instalado para processar pagamentos %1$s. Instale uma carteira compatível. Não foi possível abrir o pagamento Não iniciado Compactando @@ -1182,6 +1259,8 @@ Política de postagem Política de privacidade Termos & condições + Padrões de segurança infantil + Sobre e Jurídico N/A Erros e Avisos deste Relé Relatórios do Monitor de Relay @@ -1341,6 +1420,26 @@ Comentar em um site Comentar em um recurso externo %1$d min de leitura + + %1$d faixa + %1$d faixas + + + +%1$d faixa a mais + +%1$d faixas a mais + + Colaborativa + Privada + Faixa desconhecida + Carregando faixa… + Enviar uma imagem de capa + Escolha uma imagem quadrada para ser a arte desta faixa. + Enviar um arquivo de áudio + Escolha um MP3, WAV ou FLAC. Ele será enviado ao seu servidor de mídia ao salvar. + Arquivo de áudio pronto para envio + Enviando… + Enviando capa + áudio e publicando. Isso continuará mesmo se você sair da tela. + Falha no envio: %1$s Carregando localização Sem permissões para localização Adiciona aviso de conteúdo sensível antes de mostrar seu conteúdo. Isso é ideal para qualquer conteúdo NSFW ou conteúdo que algumas pessoas possam considerar ofensivo ou perturbador @@ -1435,6 +1534,9 @@ Carteira %1$s Erro ao abrir o aplicativo para assinar evento O aplicativo de assinatura não pôde ser encontrado. Verifique se o aplicativo não foi desinstalado + Não é possível reproduzir este vídeo + O codec ou formato não é compatível com este dispositivo (%1$s). Tente abri-lo no seu navegador. + Abrir no navegador Solicitação de assinatura rejeitada Solicitação de assinatura rejeitada Certifique-se de que a aplicação assinante autorizou esta transação @@ -1583,6 +1685,7 @@ Enviar mesmo assim Não foi possível remover metadados privados da mídia. Envio cancelado. Envio cancelado + Não foi possível remover os metadados do AVIF: %1$s Editar rascunho Entrar com Código QR Rota @@ -1637,6 +1740,94 @@ Nenhuma carteira conectada Conecte uma ou mais carteiras NWC para enviar e receber pagamentos. Adicionar conexão NWC + Escolha o tipo de carteira + Conectar uma carteira Lightning (NWC) + Pague por uma carteira Lightning externa usando o Nostr Wallet Connect. + Carteira Cashu (NIP-60) + Uma carteira ecash dentro do app, baseada em um mint Cashu. Os tokens são armazenados criptografados em relays. + Carteira Cashu + Ainda sem carteira Cashu + Crie uma carteira Cashu para guardar tokens ecash e receber nutzaps. + Procurando sua carteira… + Carteiras NIP-60 sincronizam entre clientes. Se você criou uma em outro app com esta chave Nostr, ela deve aparecer em alguns segundos. + Saldo + URL do mint + Remover mint + Histórico + Editar carteira + Chave de nutzap (avançado) + Uma chave privada separada, usada apenas para receber nutzaps NIP-61. Não é a chave da sua identidade Nostr. + Gerar uma nova chave + Invalidará quaisquer nutzaps pendentes vinculados à chave atual. + Manter chave atual + Chave privada P2PK (hex) + Editar carteira Cashu + Salvar alterações + Ainda sem recomendações + Recomendado por + + +%1$d mais + +%1$d mais + + + Recomendado por %1$d outro + Recomendado por %1$d outros + + Mints populares + Mints correspondentes + Recomendar este mint + Receber + Enviar LN + Enviar token + Resgatar + Quantia (sats) + Escolha um mint + Memo (opcional) + Fatura Lightning (bolt11) + Token Cashu + Copiar fatura + Solicitar fatura + Descartar fatura + Recebido + Enviado + Configurações da carteira Cashu + Editar detalhes da carteira + Mints, chave de nutzap + Minhas recomendações de mint + Você ainda não recomendou nenhum mint. Toque no joinha ao lado de um mint para recomendá-lo publicamente. + Excluir recomendação + Retirar recomendação? + Publicar um pedido de exclusão para sua recomendação kind:38000 de %1$s. Relays que respeitam o NIP-09 irão removê-la. + Recomendar um mint + Recuperar a partir da seed + Rederive cada segredo antigo e peça a cada mint que retorne quaisquer assinaturas cegas ainda registradas. Útil após a perda de um dispositivo ou a exclusão dos seus eventos de token por um relay malicioso. + Escaneando mints… + Nenhuma prova recuperável encontrada. + Recuperados %1$s sats em %2$s provas. + Pagar fatura + Obter cotação + Pedindo cotação ao mint… + Pagar %1$s sat + até %2$s sat em taxas? + Verificar + ✓ O mint está acessível + Não foi possível acessar o mint: %1$s + Falha no nutzap + Sem chave pública do destinatário na nota + Não foi possível construir a referência do evento + Criar token + Resgatar + Concluído + Pedindo uma fatura ao mint… + Aguardando o pagamento da fatura… + Emitindo provas… + Pagando via mint… + Trocando provas… + Trocando provas para sua carteira… + Pague esta fatura para emitir %1$s sats de ecash. + Recebido %1$s sats. + Pago %1$s sats (taxas: %2$s sats). + Token de %1$s sats pronto. Compartilhe com o destinatário. + Resgatado %1$s sats para sua carteira. Colar URI nostr+walletconnect:// Salvar Renomear @@ -1738,6 +1929,7 @@ Vou Talvez Não vou + Confirmações (%1$d) Sem RSVPs ainda. Participantes (%1$d) Em calendários (%1$d) @@ -2226,6 +2418,12 @@ Configurações do Usuário Cabeçalho de Áudio Faixa de Áudio + Faixa de música + Playlist de música + Episódio de podcast + Programa de podcast + Podcasts criados + Podcasts favoritos Concessões de Emblemas Definições de Emblemas Conjunto de Distintivos Aceito @@ -2468,6 +2666,12 @@ %1$d ano %1$d anos + + %1$d fatura pendente + %1$d faturas pendentes + + Toque para retomar e verificar o status. + Retomar <%1$s Conectando Baixando @@ -2752,232 +2956,4 @@ Nowhere Mensagem Nowhere Arte Nowhere Fórum - Adicionar hashtag - Adicionar - Marque publicamente este post com uma hashtag (etiqueta NIP-32). Quem segue você o verá no feed dessa hashtag. - Hashtag - Adicionar uma hashtag - Receber - Resgatar - Enviar LN - Enviar token - Quantia (sats) - Saldo - Trocando provas… - Emitindo provas… - Copiar fatura - Criar token - Descartar fatura - Procurando sua carteira… - Carteiras NIP-60 sincronizam entre clientes. Se você criou uma em outro app com esta chave Nostr, ela deve aparecer em alguns segundos. - Concluído - Editar carteira - Obter cotação - Pedindo cotação ao mint… - Histórico - Recebido - Enviado - Fatura Lightning (bolt11) - Memo (opcional) - Mints correspondentes - Ainda sem recomendações - Mints populares - ✓ O mint está acessível - ✓ %1$s - Recomendado por - Não foi possível acessar o mint: %1$s - URL do mint - Mints - Ainda sem carteira Cashu - Crie uma carteira Cashu para guardar tokens ecash e receber nutzaps. - Gerar uma nova chave - Invalidará quaisquer nutzaps pendentes vinculados à chave atual. - Uma chave privada separada, usada apenas para receber nutzaps NIP-61. Não é a chave da sua identidade Nostr. - Manter chave atual - Chave privada P2PK (hex) - Chave de nutzap (avançado) - Pago %1$s sats (taxas: %2$s sats). - Pagar fatura - Pagando via mint… - Retomar - Toque para retomar e verificar o status. - Escolha um mint - Pagar %1$s sat + até %2$s sat em taxas? - Pague esta fatura para emitir %1$s sats de ecash. - Recebido %1$s sats. - Recomendar este mint - Resgatar - Resgatado %1$s sats para sua carteira. - Trocando provas para sua carteira… - Remover mint - Solicitar fatura - Pedindo uma fatura ao mint… - Recomendar um mint - Publicar um pedido de exclusão para sua recomendação kind:38000 de %1$s. Relays que respeitam o NIP-09 irão removê-la. - Retirar recomendação? - Excluir recomendação - Editar detalhes da carteira - Mints, chave de nutzap - Minhas recomendações de mint - Você ainda não recomendou nenhum mint. Toque no joinha ao lado de um mint para recomendá-lo publicamente. - Nenhuma prova recuperável encontrada. - Recuperados %1$s sats em %2$s provas. - Escaneando mints… - Rederive cada segredo antigo e peça a cada mint que retorne quaisquer assinaturas cegas ainda registradas. Útil após a perda de um dispositivo ou a exclusão dos seus eventos de token por um relay malicioso. - Recuperar a partir da seed - Configurações da carteira Cashu - Token Cashu - Token de %1$s sats pronto. Compartilhe com o destinatário. - Verificar - Aguardando o pagamento da fatura… - Carteira Cashu - adicionado por - Podcasts criados - Podcasts favoritos - Episódio de podcast - Programa de podcast - Nutzap - Não foi possível construir a referência do evento - Sem chave pública do destinatário na nota - Falha no nutzap - Episódios - Nenhum episódio encontrado ainda - Ver episódios - %1$s sats disponíveis - Aguardando pagamento… - Recarregar e enviar zap - Copiar fatura - financiado - Emita novo ecash a partir da sua carteira Lightning - faltam %1$s sat - Não há o suficiente aqui - Pagar com Lightning - o destinatário - Tentar de novo - %1$s sats - Fundos de - Mint a recarregar - Enviar zap - Recarregue %1$s sats para %2$s e envie zap de %3$s %4$s sats · taxa ≈ %5$s sats - Zap de %1$s %2$s sats de %3$s - Recarregar mint - Quantia da recarga - Episódios - Podcasts - Recarregar este mint - Quantia a adicionar - Recarregar - Recarregar mint - Uma carteira ecash dentro do app, baseada em um mint Cashu. Os tokens são armazenados criptografados em relays. - Carteira Cashu (NIP-60) - Escolha o tipo de carteira - Pague por uma carteira Lightning externa usando o Nostr Wallet Connect. - Conectar uma carteira Lightning (NWC) - Editar carteira Cashu - Salvar alterações - Sobre e Jurídico - Adicionar à playlist - Você ainda não tem playlists. Use o campo acima para criar uma. - Gerenciar todas as playlists - Adicionar à playlist - Não foi possível remover os metadados do AVIF: %1$s - Confirmações (%1$d) - Feed - Padrões de segurança infantil - Abrir no navegador - Não é possível reproduzir este vídeo - O codec ou formato não é compatível com este dispositivo (%1$s). Tente abri-lo no seu navegador. - Links - Issues - Playlist de música - Faixa de música - Colaborativa - Criar - Carregando faixa… - Nova playlist - Nome da playlist - Privada - Faixa desconhecida - (playlist sem título) - Álbum (opcional) - Artista - Nome do artista - Arquivo de áudio pronto para envio - Enviar um arquivo de áudio - Escolha um MP3, WAV ou FLAC. Ele será enviado ao seu servidor de mídia ao salvar. - URL do áudio - https://… .mp3 - Enviar uma imagem de capa - Escolha uma imagem quadrada para ser a arte desta faixa. - Excluir esta faixa - Publicar uma exclusão NIP-09 para esta faixa de música? - Letra ou créditos (opcional) - Duração em segundos (opcional) - URL da imagem de capa (opcional) - Título - Título da faixa - Falha no envio: %1$s - Enviando… - Enviando capa + áudio e publicando. Isso continuará mesmo se você sair da tela. - Nova playlist - Nova faixa de música - Baixar - Seja o primeiro a comentar. - Ocultar versões antigas - Mostrar versões antigas - Fonte: %1$s - Sobre - Comentários - Versão mais recente - Links - Plataformas - Tópicos - v%1$s - Nenhum app instalado para processar pagamentos %1$s. Instale uma carteira compatível. - Nowhere Drop - Playlists - Música - Apps - Enviar - Apps - - %1$d min - %1$d min - - - +%1$d mais - +%1$d mais - - - Recomendado por %1$d outro - Recomendado por %1$d outros - - - %1$d fatura pendente - %1$d faturas pendentes - - - %1$d mint - %1$d mints - - - +%1$d faixa a mais - +%1$d faixas a mais - - - %1$d faixa - %1$d faixas - - - %1$d faixa - %1$d faixas - - - %1$d recurso incluído - %1$d recursos incluídos - - - %1$d episódio - %1$d episódios - diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 8531f15316..c35576fd0e 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -731,6 +731,15 @@ Artiklar Musik Spellistor + Avsnitt + Poddar + Visa avsnitt + Avsnitt + Inga avsnitt hittades än + + %1$d avsnitt + %1$d avsnitt + Nytt musikspår Ny spellista Titel @@ -763,6 +772,11 @@ Lägg till i Publika Bokmärken Ta bort från Privata Bokmärken Ta bort från Publika Bokmärken + Lägg till hashtag + Lägg till en hashtag + Tagga det här inlägget offentligt med en hashtag (NIP-32-etikett). De som följer dig ser det i den hashtaggens flöde. + Lägg till + tillagd av Fästa anteckningar Dina fastnålade anteckningar Fäst på profil @@ -831,6 +845,28 @@ Snabba Zap-belopp Visas när du trycker på Zap-knappen. Tryck på ett belopp för att ta bort det. Om du lämnar det tomt öppnas en dialog för att ange ett belopp varje gång. Skicka on-chain istället + Fyll på mint + Påfyllningsbelopp + Mint att fylla på + Medel från + Betala med Lightning + Utfärda nytt ecash från din Lightning-plånbok + Inte tillräckligt här + %1$s sats tillgängliga + Fyll på & skicka zap + Skicka zap + Väntar på betalning… + Försök igen + Fyll på %1$s sats till %2$s och zappa %3$s %4$s sats · avgift ≈ %5$s sats + Zappa %1$s %2$s sats från %3$s + mottagaren + behöver %1$s sat till + finansierad + Kopiera faktura + Fyll på mint + Fyll på den här mint + Belopp att lägga till + Fyll på Zap-sekretess Styr hur din identitet visas när du skickar ett Zap. Anslut plånbok @@ -1710,6 +1746,94 @@ Inga plånböcker anslutna Anslut en eller flera NWC-plånböcker för att skicka och ta emot betalningar. Lägg till NWC-anslutning + Välj plånbokstyp + Anslut en Lightning-plånbok (NWC) + Betala via en extern Lightning-plånbok med Nostr Wallet Connect. + Cashu-plånbok (NIP-60) + En ecash-plånbok i appen som backas av en Cashu-mint. Tokens lagras krypterade på relayer. + Cashu-plånbok + Ingen Cashu-plånbok än + Skapa en Cashu-plånbok för att hålla ecash-tokens och ta emot nutzaps. + Letar efter din plånbok… + NIP-60-plånböcker synkroniseras mellan klienter. Om du skapade en i en annan app med den här Nostr-nyckeln bör den dyka upp inom några sekunder. + Saldo + Mint-URL + Ta bort mint + Historik + Redigera plånbok + Nutzap-nyckel (avancerat) + En separat privat nyckel som endast används för att ta emot NIP-61-nutzaps. Inte din Nostr-identitetsnyckel. + Generera en ny nyckel + Ogiltigförklarar alla väntande nutzaps som är låsta till den nuvarande nyckeln. + Behåll nuvarande nyckel + Privat P2PK-nyckel (hex) + Redigera Cashu-plånbok + Spara ändringar + Inga rekommendationer än + Rekommenderad av + + +%1$d till + +%1$d till + + + Rekommenderad av %1$d annan + Rekommenderad av %1$d andra + + Populära mints + Matchande mints + Rekommendera den här mint + Ta emot + Skicka LN + Skicka token + Lös in + Belopp (sats) + Välj en mint + Notering (valfritt) + Lightning-faktura (bolt11) + Cashu-token + Kopiera faktura + Begär faktura + Förkasta faktura + Mottaget + Skickat + Inställningar för Cashu-plånbok + Redigera plånboksuppgifter + Mints, nutzap-nyckel + Mina mint-rekommendationer + Du har inte rekommenderat några mints än. Tryck på tummen upp bredvid en mint för att rekommendera den offentligt. + Radera rekommendation + Dra tillbaka rekommendation? + Publicera en raderingsbegäran för din kind:38000-rekommendation av %1$s. Relayer som följer NIP-09 tar bort den. + Rekommendera en mint + Återställ från seed + Härled varje tidigare hemlighet på nytt och be varje mint att returnera alla blinda signaturer som fortfarande finns sparade. Användbart efter en förlorad enhet eller en oseriös relays radering av dina token-händelser. + Skannar mints… + Inga återvinningsbara proofs hittades. + Återställde %1$s sats över %2$s proofs. + Betala faktura + Hämta offert + Ber mint om en offert… + Betala %1$s sat + upp till %2$s sat i avgifter? + Verifiera + ✓ Mint är nåbar + Kunde inte nå mint: %1$s + Nutzap misslyckades + Ingen mottagar-pubkey på noten + Kan inte bygga händelsereferens + Skapa token + Lös in + Klar + Ber mint om en faktura… + Väntar på att fakturan ska betalas… + Utfärdar proofs… + Betalar via mint… + Byter proofs… + Byter proofs till din plånbok… + Betala den här fakturan för att utfärda %1$s sats ecash. + Tog emot %1$s sats. + Betalade %1$s sats (avgifter: %2$s sats). + Token för %1$s sats redo. Dela den med mottagaren. + Löste in %1$s sats till din plånbok. Klistra in nostr+walletconnect://-URI Spara Byt namn @@ -1718,7 +1842,6 @@ Ogiltig URI för NWC-anslutning Flytta upp Flytta ner - Ändra ordning på plånböcker On-chain-transaktioner Ingen on-chain-adress tillgänglig för detta konto. Ingen on-chain-backend är konfigurerad. @@ -2311,6 +2434,10 @@ Ljudspår Musikspår Musikspellista + Poddavsnitt + Poddprogram + Skapade poddar + Favoritpoddar Utmärkelser Utmärkelsedefinitioner Accepterad emblemuppsättning @@ -2553,6 +2680,12 @@ %1$d år %1$d år + + %1$d väntande faktura + %1$d väntande fakturor + + Tryck för att återuppta och kontrollera status. + Återuppta <%1$s Ansluter Laddar ner @@ -2838,147 +2971,4 @@ Nowhere Drop Nowhere Konst Nowhere Forum - Lägg till hashtag - Lägg till - Tagga det här inlägget offentligt med en hashtag (NIP-32-etikett). De som följer dig ser det i den hashtaggens flöde. - Hashtag - Lägg till en hashtag - Ta emot - Lös in - Skicka LN - Skicka token - Belopp (sats) - Saldo - Byter proofs… - Utfärdar proofs… - Kopiera faktura - Skapa token - Förkasta faktura - Letar efter din plånbok… - NIP-60-plånböcker synkroniseras mellan klienter. Om du skapade en i en annan app med den här Nostr-nyckeln bör den dyka upp inom några sekunder. - Klar - Redigera plånbok - Hämta offert - Ber mint om en offert… - Historik - Mottaget - Skickat - Lightning-faktura (bolt11) - Notering (valfritt) - Matchande mints - Inga rekommendationer än - Populära mints - ✓ Mint är nåbar - ✓ %1$s - Rekommenderad av - Kunde inte nå mint: %1$s - Mint-URL - Mints - Ingen Cashu-plånbok än - Skapa en Cashu-plånbok för att hålla ecash-tokens och ta emot nutzaps. - Generera en ny nyckel - Ogiltigförklarar alla väntande nutzaps som är låsta till den nuvarande nyckeln. - En separat privat nyckel som endast används för att ta emot NIP-61-nutzaps. Inte din Nostr-identitetsnyckel. - Behåll nuvarande nyckel - Privat P2PK-nyckel (hex) - Nutzap-nyckel (avancerat) - Betalade %1$s sats (avgifter: %2$s sats). - Betala faktura - Betalar via mint… - Återuppta - Tryck för att återuppta och kontrollera status. - Välj en mint - Betala %1$s sat + upp till %2$s sat i avgifter? - Betala den här fakturan för att utfärda %1$s sats ecash. - Tog emot %1$s sats. - Rekommendera den här mint - Lös in - Löste in %1$s sats till din plånbok. - Byter proofs till din plånbok… - Ta bort mint - Begär faktura - Ber mint om en faktura… - Rekommendera en mint - Publicera en raderingsbegäran för din kind:38000-rekommendation av %1$s. Relayer som följer NIP-09 tar bort den. - Dra tillbaka rekommendation? - Radera rekommendation - Redigera plånboksuppgifter - Mints, nutzap-nyckel - Mina mint-rekommendationer - Du har inte rekommenderat några mints än. Tryck på tummen upp bredvid en mint för att rekommendera den offentligt. - Inga återvinningsbara proofs hittades. - Återställde %1$s sats över %2$s proofs. - Skannar mints… - Härled varje tidigare hemlighet på nytt och be varje mint att returnera alla blinda signaturer som fortfarande finns sparade. Användbart efter en förlorad enhet eller en oseriös relays radering av dina token-händelser. - Återställ från seed - Inställningar för Cashu-plånbok - Cashu-token - Token för %1$s sats redo. Dela den med mottagaren. - Verifiera - Väntar på att fakturan ska betalas… - Cashu-plånbok - tillagd av - Skapade poddar - Favoritpoddar - Poddavsnitt - Poddprogram - Nutzap - Kan inte bygga händelsereferens - Ingen mottagar-pubkey på noten - Nutzap misslyckades - Avsnitt - Inga avsnitt hittades än - Visa avsnitt - %1$s sats tillgängliga - Väntar på betalning… - Fyll på & skicka zap - Kopiera faktura - finansierad - Utfärda nytt ecash från din Lightning-plånbok - behöver %1$s sat till - Inte tillräckligt här - Betala med Lightning - mottagaren - Försök igen - %1$s sats - Medel från - Mint att fylla på - Skicka zap - Fyll på %1$s sats till %2$s och zappa %3$s %4$s sats · avgift ≈ %5$s sats - Zappa %1$s %2$s sats från %3$s - Fyll på mint - Påfyllningsbelopp - Avsnitt - Poddar - Fyll på den här mint - Belopp att lägga till - Fyll på - Fyll på mint - En ecash-plånbok i appen som backas av en Cashu-mint. Tokens lagras krypterade på relayer. - Cashu-plånbok (NIP-60) - Välj plånbokstyp - Betala via en extern Lightning-plånbok med Nostr Wallet Connect. - Anslut en Lightning-plånbok (NWC) - Redigera Cashu-plånbok - Spara ändringar - - +%1$d till - +%1$d till - - - Rekommenderad av %1$d annan - Rekommenderad av %1$d andra - - - %1$d väntande faktura - %1$d väntande fakturor - - - %1$d mint - %1$d mints - - - %1$d avsnitt - %1$d avsnitt - diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 607cd89182..f9096b9854 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -764,6 +764,12 @@ 添加到公开书签 从私人书签中移除 从公开书签中移除 + 添加话题标签 + 添加话题标签 + 给这篇帖子加公开话题标签 (NIP-32 标签) 。关注你的人会在话题的源里看到。 + 话题标签 + 添加 + 添加者 置顶的笔记 您置顶的笔记 置顶到个人资料 @@ -832,6 +838,29 @@ 快速打闪金额 按下打闪按钮时显示。点击接即可删除它。 如果留空,每次都会打开对话框来插入金融。 在链上发送 + 铸币厂充值 + %1$s sats + 充值金额 + 要充值的铸币厂 + 资金来源 + 用闪电付款 + 从闪电钱包铸造新的电子现金 + 这里还不够 + %1$s sats 可用 + 充值 & 发送打闪 + 发送打闪 + 等待支付中… + 重试 + 充值 %1$s sats 到 %2$s 并打闪 %3$s %4$s sats · fee ≈ %5$s sats + 从 %3$s 打闪 %1$s %2$s sats + 接收者 + 需要另外 %1$s sat + 已支付 + 复制发票 + 铸币厂充值 + 向该铸币厂充值 + 要添加的金额 + 充值 打闪隐私 控制发送打闪时如何显示您的身份。 连接钱包 @@ -1810,7 +1839,6 @@ 无效的 NWC 连接 URI 上移 下移 - 调整钱包顺序 链上交易 此账户没有链上地址可用。 没有配置链后端。