From f1650ad9ce231343e5d908c4ffc123ccec22478d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 08:44:32 +0000 Subject: [PATCH] feat(privacy): add per-account toggle to disable NIP-89 client tag Adds a "Don't add client tag to my events" switch under Security Filters. NostrSignerWithClientTag now consults a runtime predicate at sign-time, so the toggle takes effect immediately on the live session without rewrapping the signer or rebuilding Account. https://claude.ai/code/session_01KhNVTm8LLdVphqyxVkZtDS --- .../vitorpamplona/amethyst/LocalPreferences.kt | 4 ++++ .../amethyst/model/AccountSettings.kt | 8 ++++++++ .../model/accountsCache/AccountCacheState.kt | 7 ++++++- .../loggedIn/settings/SecurityFiltersScreen.kt | 15 +++++++++++++++ amethyst/src/main/res/values/strings.xml | 2 ++ .../clientTag/NostrSignerWithClientTag.kt | 14 +++++++++++++- 6 files changed, 48 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index ce6324ffc..864a2706a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -95,6 +95,7 @@ private object PrefKeys { const val LOCAL_RELAY_SERVERS = "localRelayServers" const val DEFAULT_FILE_SERVER = "defaultFileServer" const val STRIP_LOCATION_ON_UPLOAD = "stripLocationOnUpload" + const val DISABLE_CLIENT_TAG = "disableClientTag" const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList" const val DEFAULT_STORIES_FOLLOW_LIST = "defaultStoriesFollowList" const val DEFAULT_NOTIFICATION_FOLLOW_LIST = "defaultNotificationFollowList" @@ -346,6 +347,7 @@ object LocalPreferences { ) putBoolean(PrefKeys.STRIP_LOCATION_ON_UPLOAD, settings.stripLocationOnUpload) + putBoolean(PrefKeys.DISABLE_CLIENT_TAG, settings.disableClientTag) putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, JsonMapper.toJson(settings.defaultHomeFollowList.value)) putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultStoriesFollowList.value)) @@ -513,6 +515,7 @@ object LocalPreferences { Log.d("LocalPreferences") { "Load account from file $npub - keys ready" } val stripLocationOnUpload = getBoolean(PrefKeys.STRIP_LOCATION_ON_UPLOAD, true) + val disableClientTag = getBoolean(PrefKeys.DISABLE_CLIENT_TAG, false) val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false) val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false) @@ -620,6 +623,7 @@ object LocalPreferences { localRelayServers = MutableStateFlow(localRelayServers), defaultFileServer = defaultFileServer.await(), stripLocationOnUpload = stripLocationOnUpload, + disableClientTag = disableClientTag, defaultHomeFollowList = MutableStateFlow(followListPrefs.home), defaultStoriesFollowList = MutableStateFlow(followListPrefs.stories), defaultNotificationFollowList = MutableStateFlow(followListPrefs.notification), 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 b6aace912..ea8b1b3c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -149,6 +149,7 @@ class AccountSettings( var localRelayServers: MutableStateFlow> = MutableStateFlow(setOf()), var defaultFileServer: ServerName = DEFAULT_MEDIA_SERVERS[0], var stripLocationOnUpload: Boolean = true, + var disableClientTag: Boolean = false, val defaultHomeFollowList: MutableStateFlow = MutableStateFlow(TopFilter.AllFollows), val defaultStoriesFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), val defaultNotificationFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), @@ -403,6 +404,13 @@ class AccountSettings( } } + fun changeDisableClientTag(disable: Boolean) { + if (disableClientTag != disable) { + disableClientTag = disable + saveAccountSettings() + } + } + // --- // list names // --- diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt index bec0e724d..01d416674 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt @@ -135,7 +135,12 @@ class AccountCacheState( val cached = accounts.value[signer.pubKey] if (cached != null) return cached - val signerWithClientTag = NostrSignerWithClientTag(signer, CLIENT_TAG_NAME) + val signerWithClientTag = + NostrSignerWithClientTag( + inner = signer, + clientName = CLIENT_TAG_NAME, + disabled = { accountSettings.disableClientTag }, + ) val accountDir = File(rootFilesDir(), "accounts/${signer.pubKey}").apply { mkdirs() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index 0cd081e4d..544788b17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -371,6 +371,21 @@ private fun HeaderOptions(accountViewModel: AccountViewModel) { ) } + SettingsRow( + R.string.disable_client_tag_title, + R.string.disable_client_tag_explainer, + ) { + var disableClientTag by remember { mutableStateOf(accountViewModel.account.settings.disableClientTag) } + + Switch( + checked = disableClientTag, + onCheckedChange = { + disableClientTag = it + accountViewModel.account.settings.changeDisableClientTag(it) + }, + ) + } + SettingsRow( R.string.show_sensitive_content_title, R.string.show_sensitive_content_explainer, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 63e4dac1e..acc44f19e 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1172,6 +1172,8 @@ Filter spam Hides posts from strangers that were exactly the same for 5 or more times + Don\'t add client tag to my events + When enabled, Amethyst will not append a NIP-89 client tag to events you publish. Warn on reports Shows a warning message when posts have 5 or more reports from your follows Show sensitive content diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt index 1aa8b4de6..5856a0ebf 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt @@ -40,12 +40,19 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent class NostrSignerWithClientTag( val inner: NostrSigner, val clientTag: Array, + val disabled: () -> Boolean = { false }, ) : NostrSigner(inner.pubKey) { constructor( inner: NostrSigner, clientName: String, ) : this(inner, ClientTag.assemble(clientName)) + constructor( + inner: NostrSigner, + clientName: String, + disabled: () -> Boolean, + ) : this(inner, ClientTag.assemble(clientName), disabled) + constructor( inner: NostrSigner, clientName: String, @@ -60,7 +67,12 @@ class NostrSignerWithClientTag( kind: Int, tags: Array>, content: String, - ): T = inner.sign(createdAt, kind, appendClientTag(tags), content) + ): T = + if (disabled()) { + inner.sign(createdAt, kind, tags, content) + } else { + inner.sign(createdAt, kind, appendClientTag(tags), content) + } override suspend fun nip04Encrypt( plaintext: String,