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
This commit is contained in:
Claude
2026-04-03 10:05:51 +00:00
committed by greenart7c3
parent 843f557fee
commit e82e1ee226
7 changed files with 93 additions and 0 deletions
@@ -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()),
@@ -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)
@@ -159,6 +159,7 @@ class AccountSettings(
val transientAccount: Boolean = false,
var externalSignerPackageName: String? = null,
var localRelayServers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
val sendKind0EventsToLocalRelay: MutableStateFlow<Boolean> = MutableStateFlow(false),
var defaultFileServer: ServerName = DEFAULT_MEDIA_SERVERS[0],
var stripLocationOnUpload: Boolean = true,
val defaultHomeFollowList: MutableStateFlow<TopFilter> = 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
@@ -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()
}
}
@@ -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) =
@@ -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,
+2
View File
@@ -1517,6 +1517,8 @@
<string name="search_section_explainer">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.</string>
<string name="local_section">Local Relays</string>
<string name="local_section_explainer">List of relays that are running in this device.</string>
<string name="send_kind0_to_local_relay_title">Forward profiles to local relay</string>
<string name="send_kind0_to_local_relay_description">Send all received profile (kind 0) events to your local relay</string>
<string name="trusted_relays_title">Trusted Relays</string>
<string name="trusted_section">Trusted Relays</string>