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..0a1377559 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 @@ -267,6 +268,8 @@ class Account( 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) @@ -433,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/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 1d931de19..a600384ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -424,6 +424,14 @@ class AccountSettings( } } + fun changeSendKind0EventsToLocalRelay(send: Boolean): Boolean { + if (syncedSettings.security.updateSendKind0EventsToLocalRelay(send)) { + saveAccountSettings() + return true + } + return false + } + fun updateUserMetadata(newMetadata: MetadataEvent?) { if (newMetadata == null) return 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 ed5e58ca0..fae2ef54c 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 d10a027f4..104149a2c 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 new file mode 100644 index 000000000..3fc1e0bdf --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/ForwardKind0ToLocalRelayState.kt @@ -0,0 +1,42 @@ +/* + * 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.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/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 18f667c64..84889bb95 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,8 @@ class AccountViewModel( fun filterSpamFromStrangers() = account.settings.syncedSettings.security.filterSpamFromStrangers + fun toggleSendKind0ToLocalRelay(enabled: Boolean) = launchSigner { account.updateSendKind0EventsToLocalRelay(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/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index c4d65da7b..9a444574f 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/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