From e82e1ee226d06c06ff732e6c10ec1026cec3db3d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 10:05:51 +0000 Subject: [PATCH 1/4] feat: add account setting to forward kind 0 events to local relay Adds a toggle in the Local Relays settings section that, when enabled, forwards all received kind 0 (profile/metadata) events to the user's configured local relays. - AccountSettings: add sendKind0EventsToLocalRelay MutableStateFlow + toggle method - LocalPreferences: persist the new setting - ForwardKind0ToLocalRelayState: uses EventCollector to intercept kind 0 events from any relay and publish them to local relays when enabled - Account: instantiate ForwardKind0ToLocalRelayState - AccountViewModel: expose toggleSendKind0ToLocalRelay - LocalRelayListView: add Switch toggle above the relay list - strings.xml: add label and description strings https://claude.ai/code/session_01Er9VV7nHyDb5pihrunb81F --- .../amethyst/LocalPreferences.kt | 5 ++ .../vitorpamplona/amethyst/model/Account.kt | 3 ++ .../amethyst/model/AccountSettings.kt | 8 ++++ .../ForwardKind0ToLocalRelayState.kt | 46 +++++++++++++++++++ .../ui/screen/loggedIn/AccountViewModel.kt | 4 ++ .../relays/local/LocalRelayListView.kt | 25 ++++++++++ amethyst/src/main/res/values/strings.xml | 2 + 7 files changed, 93 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index a0a006ceb0..01949a0f40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -91,6 +91,7 @@ private object PrefKeys { const val NOSTR_PRIVKEY = "nostr_privkey" const val NOSTR_PUBKEY = "nostr_pubkey" const val LOCAL_RELAY_SERVERS = "localRelayServers" + const val SEND_KIND0_TO_LOCAL_RELAY = "sendKind0ToLocalRelay" const val DEFAULT_FILE_SERVER = "defaultFileServer" const val STRIP_LOCATION_ON_UPLOAD = "stripLocationOnUpload" const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList" @@ -359,6 +360,8 @@ object LocalPreferences { remove(PrefKeys.LOCAL_RELAY_SERVERS) } + putBoolean(PrefKeys.SEND_KIND0_TO_LOCAL_RELAY, settings.sendKind0EventsToLocalRelay.value) + putOrRemove(PrefKeys.LATEST_MUTE_LIST, settings.backupMuteList) putOrRemove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST, settings.backupPrivateHomeRelayList) putOrRemove(PrefKeys.LATEST_APP_SPECIFIC_DATA, settings.backupAppSpecificData) @@ -474,6 +477,7 @@ object LocalPreferences { val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false) val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf() val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf() + val sendKind0ToLocalRelay = getBoolean(PrefKeys.SEND_KIND0_TO_LOCAL_RELAY, false) val defaultHomeFollowListStr = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) val defaultStoriesFollowListStr = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) @@ -560,6 +564,7 @@ object LocalPreferences { transientAccount = false, externalSignerPackageName = externalSignerPackageName, localRelayServers = MutableStateFlow(localRelayServers), + sendKind0EventsToLocalRelay = MutableStateFlow(sendKind0ToLocalRelay), defaultFileServer = defaultFileServer.await(), stripLocationOnUpload = stripLocationOnUpload, defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList.await()), 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 09bff4c1df..f4dcd202d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.localRelays.ForwardKind0ToLocalRelayState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountHomeRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState @@ -262,6 +263,8 @@ class Account( val userMetadata = UserMetadataState(signer, cache, scope, settings) + val forwardKind0ToLocalRelay = ForwardKind0ToLocalRelayState(client, localRelayList, settings) + override val nip47SignerState = NwcSignerState(signer, nwcFilterAssembler, cache, scope, settings.zapPaymentRequest) val nip65RelayList = Nip65RelayListState(signer, cache, scope, settings) 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 1d931de193..c501e2039b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -159,6 +159,7 @@ class AccountSettings( val transientAccount: Boolean = false, var externalSignerPackageName: String? = null, var localRelayServers: MutableStateFlow> = MutableStateFlow(setOf()), + val sendKind0EventsToLocalRelay: MutableStateFlow = MutableStateFlow(false), var defaultFileServer: ServerName = DEFAULT_MEDIA_SERVERS[0], var stripLocationOnUpload: Boolean = true, val defaultHomeFollowList: MutableStateFlow = MutableStateFlow(TopFilter.AllFollows), @@ -424,6 +425,13 @@ class AccountSettings( } } + fun changeSendKind0EventsToLocalRelay(send: Boolean) { + if (sendKind0EventsToLocalRelay.value != send) { + sendKind0EventsToLocalRelay.tryEmit(send) + saveAccountSettings() + } + } + fun updateUserMetadata(newMetadata: MetadataEvent?) { if (newMetadata == null) return diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt new file mode 100644 index 0000000000..096f775599 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.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.model.localRelays + +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.EventCollector + +class ForwardKind0ToLocalRelayState( + val client: INostrClient, + val localRelayList: LocalRelayListState, + val settings: AccountSettings, +) { + private val eventCollector = + EventCollector(client) { event, _ -> + if (event is MetadataEvent && settings.sendKind0EventsToLocalRelay.value) { + val localRelays = localRelayList.flow.value + if (localRelays.isNotEmpty()) { + client.publish(event, localRelays) + } + } + } + + fun destroy() { + eventCollector.destroy() + } +} 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 18f667c647..6e66f791ff 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 @@ -1144,6 +1144,10 @@ class AccountViewModel( fun filterSpamFromStrangers() = account.settings.syncedSettings.security.filterSpamFromStrangers + fun toggleSendKind0ToLocalRelay(enabled: Boolean) { + account.settings.changeSendKind0EventsToLocalRelay(enabled) + } + fun updateWarnReports(warnReports: Boolean) = launchSigner { account.updateWarnReports(warnReports) } fun updateFilterSpam(filterSpam: Boolean) = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt index 8733ddbf2d..fe9c984c55 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt @@ -25,11 +25,16 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Switch 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.Alignment import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -37,6 +42,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySet import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer @@ -65,6 +71,25 @@ fun LazyListScope.renderLocalItems( accountViewModel: AccountViewModel, nav: INav, ) { + item { + val sendKind0 by accountViewModel.account.settings.sendKind0EventsToLocalRelay + .collectAsStateWithLifecycle() + var checked by remember(sendKind0) { mutableStateOf(sendKind0) } + + SettingsRow( + R.string.send_kind0_to_local_relay_title, + R.string.send_kind0_to_local_relay_description, + ) { + Switch( + checked = checked, + onCheckedChange = { + checked = it + accountViewModel.toggleSendKind0ToLocalRelay(it) + }, + ) + } + } + itemsIndexed(feedState, key = { _, item -> "Local" + item.relay.url }) { index, item -> BasicRelaySetupInfoDialog( item, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 1080389ef4..2b1101cc38 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1517,6 +1517,8 @@ List of relays to use when searching content or users. Tagging and search will not work if no options are available. Make sure they implement NIP-50. Local Relays List of relays that are running in this device. + Forward profiles to local relay + Send all received profile (kind 0) events to your local relay Trusted Relays Trusted Relays From 30aa997a93a719f62c2431425e455d0e72cf8a84 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 19:26:31 +0000 Subject: [PATCH 2/4] refactor: move sendKind0EventsToLocalRelay to AccountSyncedSettings - Move the setting from a raw AccountSettings field into AccountSyncedSettingsInternal / AccountSecurityPreferences so it is serialised with the rest of the synced app-specific data (NIP-78) - Remove one-off LocalPreferences persistence (no longer needed) - Update ForwardKind0ToLocalRelayState to read from syncedSettings.security - Move the UI toggle from inside renderLocalItems into the relay settings screen (AllRelayListScreen) as a proper SettingsRow item in the local-relay section, with title and description - Fix Account initialisation order so localRelayList is declared before ForwardKind0ToLocalRelayState https://claude.ai/code/session_01Er9VV7nHyDb5pihrunb81F --- .../amethyst/LocalPreferences.kt | 5 ---- .../vitorpamplona/amethyst/model/Account.kt | 4 +-- .../amethyst/model/AccountSettings.kt | 8 +++--- .../amethyst/model/AccountSyncedSettings.kt | 15 +++++++++++ .../model/AccountSyncedSettingsInternal.kt | 1 + .../ForwardKind0ToLocalRelayState.kt | 2 +- .../loggedIn/relays/AllRelayListScreen.kt | 20 +++++++++++++++ .../relays/local/LocalRelayListView.kt | 25 ------------------- 8 files changed, 43 insertions(+), 37 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 01949a0f40..a0a006ceb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -91,7 +91,6 @@ private object PrefKeys { const val NOSTR_PRIVKEY = "nostr_privkey" const val NOSTR_PUBKEY = "nostr_pubkey" const val LOCAL_RELAY_SERVERS = "localRelayServers" - const val SEND_KIND0_TO_LOCAL_RELAY = "sendKind0ToLocalRelay" const val DEFAULT_FILE_SERVER = "defaultFileServer" const val STRIP_LOCATION_ON_UPLOAD = "stripLocationOnUpload" const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList" @@ -360,8 +359,6 @@ object LocalPreferences { remove(PrefKeys.LOCAL_RELAY_SERVERS) } - putBoolean(PrefKeys.SEND_KIND0_TO_LOCAL_RELAY, settings.sendKind0EventsToLocalRelay.value) - putOrRemove(PrefKeys.LATEST_MUTE_LIST, settings.backupMuteList) putOrRemove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST, settings.backupPrivateHomeRelayList) putOrRemove(PrefKeys.LATEST_APP_SPECIFIC_DATA, settings.backupAppSpecificData) @@ -477,7 +474,6 @@ object LocalPreferences { val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false) val hasDonatedInVersion = getStringSet(PrefKeys.HAS_DONATED_IN_VERSION, null) ?: setOf() val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf() - val sendKind0ToLocalRelay = getBoolean(PrefKeys.SEND_KIND0_TO_LOCAL_RELAY, false) val defaultHomeFollowListStr = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) val defaultStoriesFollowListStr = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) @@ -564,7 +560,6 @@ object LocalPreferences { transientAccount = false, externalSignerPackageName = externalSignerPackageName, localRelayServers = MutableStateFlow(localRelayServers), - sendKind0EventsToLocalRelay = MutableStateFlow(sendKind0ToLocalRelay), defaultFileServer = defaultFileServer.await(), stripLocationOnUpload = stripLocationOnUpload, defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList.await()), 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 f4dcd202d9..a6374326e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -263,13 +263,13 @@ class Account( val userMetadata = UserMetadataState(signer, cache, scope, settings) - val forwardKind0ToLocalRelay = ForwardKind0ToLocalRelayState(client, localRelayList, settings) - override val nip47SignerState = NwcSignerState(signer, nwcFilterAssembler, cache, scope, settings.zapPaymentRequest) val nip65RelayList = Nip65RelayListState(signer, cache, scope, settings) val localRelayList = LocalRelayListState(signer, cache, scope, settings) + val forwardKind0ToLocalRelay = ForwardKind0ToLocalRelayState(client, localRelayList, settings) + val dmRelayList = DmRelayListState(signer, cache, scope, settings) val privateStorageDecryptionCache = PrivateStorageRelayListDecryptionCache(signer) 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 c501e2039b..a600384ac6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -159,7 +159,6 @@ class AccountSettings( val transientAccount: Boolean = false, var externalSignerPackageName: String? = null, var localRelayServers: MutableStateFlow> = MutableStateFlow(setOf()), - val sendKind0EventsToLocalRelay: MutableStateFlow = MutableStateFlow(false), var defaultFileServer: ServerName = DEFAULT_MEDIA_SERVERS[0], var stripLocationOnUpload: Boolean = true, val defaultHomeFollowList: MutableStateFlow = MutableStateFlow(TopFilter.AllFollows), @@ -425,11 +424,12 @@ class AccountSettings( } } - fun changeSendKind0EventsToLocalRelay(send: Boolean) { - if (sendKind0EventsToLocalRelay.value != send) { - sendKind0EventsToLocalRelay.tryEmit(send) + fun changeSendKind0EventsToLocalRelay(send: Boolean): Boolean { + if (syncedSettings.security.updateSendKind0EventsToLocalRelay(send)) { saveAccountSettings() + return true } + return false } fun updateUserMetadata(newMetadata: MetadataEvent?) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index ed5e58ca0f..fae2ef54c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -54,6 +54,7 @@ class AccountSyncedSettings( internalSettings.security.warnAboutPostsWithReports, MutableStateFlow(internalSettings.security.filterSpamFromStrangers), MutableStateFlow(internalSettings.security.maxHashtagLimit), + MutableStateFlow(internalSettings.security.sendKind0EventsToLocalRelay), ) fun toInternal(): AccountSyncedSettingsInternal = @@ -76,6 +77,7 @@ class AccountSyncedSettings( security.warnAboutPostsWithReports, security.filterSpamFromStrangers.value, security.maxHashtagLimit.value, + security.sendKind0EventsToLocalRelay.value, ), ) @@ -126,6 +128,10 @@ class AccountSyncedSettings( if (security.maxHashtagLimit.value != syncedSettingsInternal.security.maxHashtagLimit) { security.maxHashtagLimit.tryEmit(syncedSettingsInternal.security.maxHashtagLimit) } + + if (security.sendKind0EventsToLocalRelay.value != syncedSettingsInternal.security.sendKind0EventsToLocalRelay) { + security.sendKind0EventsToLocalRelay.tryEmit(syncedSettingsInternal.security.sendKind0EventsToLocalRelay) + } } fun dontTranslateFromFilteredBySpokenLanguages(): Set = languages.dontTranslateFrom.value - getLanguagesSpokenByUser() @@ -211,6 +217,7 @@ class AccountSecurityPreferences( var warnAboutPostsWithReports: Boolean = true, var filterSpamFromStrangers: MutableStateFlow = MutableStateFlow(true), val maxHashtagLimit: MutableStateFlow = MutableStateFlow(5), + var sendKind0EventsToLocalRelay: MutableStateFlow = MutableStateFlow(false), ) { fun updateShowSensitiveContent(show: Boolean?): Boolean { if (showSensitiveContent.value != show) { @@ -243,4 +250,12 @@ class AccountSecurityPreferences( } else { false } + + fun updateSendKind0EventsToLocalRelay(send: Boolean): Boolean = + if (send != sendKind0EventsToLocalRelay.value) { + sendKind0EventsToLocalRelay.tryEmit(send) + true + } else { + false + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt index d10a027f40..104149a2c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt @@ -106,4 +106,5 @@ class AccountSecurityPreferencesInternal( var warnAboutPostsWithReports: Boolean = true, var filterSpamFromStrangers: Boolean = true, val maxHashtagLimit: Int = 5, + var sendKind0EventsToLocalRelay: Boolean = false, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt index 096f775599..b8b5517d35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt @@ -32,7 +32,7 @@ class ForwardKind0ToLocalRelayState( ) { private val eventCollector = EventCollector(client) { event, _ -> - if (event is MetadataEvent && settings.sendKind0EventsToLocalRelay.value) { + if (event is MetadataEvent && settings.syncedSettings.security.sendKind0EventsToLocalRelay.value) { val localRelays = localRelayList.flow.value if (localRelays.isNotEmpty()) { client.publish(event, localRelays) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index c4d65da7b0..9a444574fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -88,6 +89,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayLi import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.renderSearchItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.TrustedRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.renderTrustedItems +import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.RowColSpacing @@ -351,6 +353,24 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } + item { + val sendKind0 by accountViewModel.account.settings.syncedSettings.security.sendKind0EventsToLocalRelay + .collectAsStateWithLifecycle() + var checked by remember(sendKind0) { mutableStateOf(sendKind0) } + + SettingsRow( + R.string.send_kind0_to_local_relay_title, + R.string.send_kind0_to_local_relay_description, + ) { + Switch( + checked = checked, + onCheckedChange = { + checked = it + accountViewModel.toggleSendKind0ToLocalRelay(it) + }, + ) + } + } renderLocalItems(localFeedState, localViewModel, accountViewModel, nav) item { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt index fe9c984c55..8733ddbf2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt @@ -25,16 +25,11 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Switch 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.Alignment import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -42,7 +37,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySet import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer @@ -71,25 +65,6 @@ fun LazyListScope.renderLocalItems( accountViewModel: AccountViewModel, nav: INav, ) { - item { - val sendKind0 by accountViewModel.account.settings.sendKind0EventsToLocalRelay - .collectAsStateWithLifecycle() - var checked by remember(sendKind0) { mutableStateOf(sendKind0) } - - SettingsRow( - R.string.send_kind0_to_local_relay_title, - R.string.send_kind0_to_local_relay_description, - ) { - Switch( - checked = checked, - onCheckedChange = { - checked = it - accountViewModel.toggleSendKind0ToLocalRelay(it) - }, - ) - } - } - itemsIndexed(feedState, key = { _, item -> "Local" + item.relay.url }) { index, item -> BasicRelaySetupInfoDialog( item, From 68623a43f9ca76888135b4150a413a9bda874b02 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 19:31:50 +0000 Subject: [PATCH 3/4] fix: remove unused destroy() from ForwardKind0ToLocalRelayState MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EventCollector lives for the lifetime of Account, matching the same pattern used by CacheClientConnector — no explicit teardown needed. https://claude.ai/code/session_01Er9VV7nHyDb5pihrunb81F --- .../model/localRelays/ForwardKind0ToLocalRelayState.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt index b8b5517d35..3fc1e0bdf6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt @@ -39,8 +39,4 @@ class ForwardKind0ToLocalRelayState( } } } - - fun destroy() { - eventCollector.destroy() - } } From 5796cac9476691cfec860168b7c4cd53c99b20eb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 19:41:40 +0000 Subject: [PATCH 4/4] fix: republish NIP-78 AppSpecificDataEvent when sendKind0EventsToLocalRelay changes Add Account.updateSendKind0EventsToLocalRelay() suspend fun that calls sendNewAppSpecificData() after updating the setting, matching the pattern used by updateFilterSpam/updateWarnReports. Update AccountViewModel to call this via launchSigner instead of directly mutating settings. https://claude.ai/code/session_01Er9VV7nHyDb5pihrunb81F --- .../main/java/com/vitorpamplona/amethyst/model/Account.kt | 8 ++++++++ .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 4 +--- 2 files changed, 9 insertions(+), 3 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 a6374326e9..0a1377559f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -436,6 +436,14 @@ class Account( return false } + suspend fun updateSendKind0EventsToLocalRelay(send: Boolean): Boolean { + if (settings.changeSendKind0EventsToLocalRelay(send)) { + sendNewAppSpecificData() + return true + } + return false + } + suspend fun updateFilterSpam(filterSpam: Boolean): Boolean { if (settings.updateFilterSpam(filterSpam)) { if (!settings.syncedSettings.security.filterSpamFromStrangers.value) { 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 6e66f791ff..84889bb951 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 @@ -1144,9 +1144,7 @@ class AccountViewModel( fun filterSpamFromStrangers() = account.settings.syncedSettings.security.filterSpamFromStrangers - fun toggleSendKind0ToLocalRelay(enabled: Boolean) { - account.settings.changeSendKind0EventsToLocalRelay(enabled) - } + fun toggleSendKind0ToLocalRelay(enabled: Boolean) = launchSigner { account.updateSendKind0EventsToLocalRelay(enabled) } fun updateWarnReports(warnReports: Boolean) = launchSigner { account.updateWarnReports(warnReports) }