From e82e1ee226d06c06ff732e6c10ec1026cec3db3d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 10:05:51 +0000 Subject: [PATCH] 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 a0a006ceb..01949a0f4 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 09bff4c1d..f4dcd202d 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 1d931de19..c501e2039 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 000000000..096f77559 --- /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 18f667c64..6e66f791f 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 8733ddbf2..fe9c984c5 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 1080389ef..2b1101cc3 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