mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-27 18:36:37 +02:00
Adds Local Relays to the relay list screen, saving locally in the device.
This commit is contained in:
@@ -77,6 +77,7 @@ private object PrefKeys {
|
|||||||
const val NOSTR_PUBKEY = "nostr_pubkey"
|
const val NOSTR_PUBKEY = "nostr_pubkey"
|
||||||
const val RELAYS = "relays"
|
const val RELAYS = "relays"
|
||||||
const val DONT_TRANSLATE_FROM = "dontTranslateFrom"
|
const val DONT_TRANSLATE_FROM = "dontTranslateFrom"
|
||||||
|
const val LOCAL_RELAY_SERVERS = "localRelayServers"
|
||||||
const val LANGUAGE_PREFS = "languagePreferences"
|
const val LANGUAGE_PREFS = "languagePreferences"
|
||||||
const val TRANSLATE_TO = "translateTo"
|
const val TRANSLATE_TO = "translateTo"
|
||||||
const val ZAP_AMOUNTS = "zapAmounts"
|
const val ZAP_AMOUNTS = "zapAmounts"
|
||||||
@@ -284,6 +285,7 @@ object LocalPreferences {
|
|||||||
account.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
account.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
||||||
putString(PrefKeys.RELAYS, Event.mapper.writeValueAsString(account.localRelays))
|
putString(PrefKeys.RELAYS, Event.mapper.writeValueAsString(account.localRelays))
|
||||||
putStringSet(PrefKeys.DONT_TRANSLATE_FROM, account.dontTranslateFrom)
|
putStringSet(PrefKeys.DONT_TRANSLATE_FROM, account.dontTranslateFrom)
|
||||||
|
putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, account.localRelayServers)
|
||||||
putString(
|
putString(
|
||||||
PrefKeys.LANGUAGE_PREFS,
|
PrefKeys.LANGUAGE_PREFS,
|
||||||
Event.mapper.writeValueAsString(account.languagePreferences),
|
Event.mapper.writeValueAsString(account.languagePreferences),
|
||||||
@@ -410,6 +412,7 @@ object LocalPreferences {
|
|||||||
?: setOf<RelaySetupInfo>()
|
?: setOf<RelaySetupInfo>()
|
||||||
|
|
||||||
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
||||||
|
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
|
||||||
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
||||||
val defaultHomeFollowList =
|
val defaultHomeFollowList =
|
||||||
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
|
getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
|
||||||
@@ -577,6 +580,7 @@ object LocalPreferences {
|
|||||||
keyPair = keyPair,
|
keyPair = keyPair,
|
||||||
signer = signer,
|
signer = signer,
|
||||||
localRelays = localRelays,
|
localRelays = localRelays,
|
||||||
|
localRelayServers = localRelayServers,
|
||||||
dontTranslateFrom = dontTranslateFrom,
|
dontTranslateFrom = dontTranslateFrom,
|
||||||
languagePreferences = languagePreferences,
|
languagePreferences = languagePreferences,
|
||||||
translateTo = translateTo,
|
translateTo = translateTo,
|
||||||
|
@@ -176,6 +176,7 @@ class Account(
|
|||||||
val keyPair: KeyPair,
|
val keyPair: KeyPair,
|
||||||
val signer: NostrSigner = NostrSignerInternal(keyPair),
|
val signer: NostrSigner = NostrSignerInternal(keyPair),
|
||||||
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
|
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
|
||||||
|
var localRelayServers: Set<String> = setOf(),
|
||||||
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
|
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
|
||||||
var languagePreferences: Map<String, String> = mapOf(),
|
var languagePreferences: Map<String, String> = mapOf(),
|
||||||
var translateTo: String = Locale.getDefault().language,
|
var translateTo: String = Locale.getDefault().language,
|
||||||
@@ -2443,6 +2444,12 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateLocalRelayServers(servers: Set<String>) {
|
||||||
|
localRelayServers = servers
|
||||||
|
liveLanguages.invalidateData()
|
||||||
|
saveable.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
fun addDontTranslateFrom(languageCode: String) {
|
fun addDontTranslateFrom(languageCode: String) {
|
||||||
dontTranslateFrom = dontTranslateFrom.plus(languageCode)
|
dontTranslateFrom = dontTranslateFrom.plus(languageCode)
|
||||||
liveLanguages.invalidateData()
|
liveLanguages.invalidateData()
|
||||||
|
@@ -76,11 +76,15 @@ fun AllRelayListView(
|
|||||||
val searchViewModel: SearchRelayListViewModel = viewModel()
|
val searchViewModel: SearchRelayListViewModel = viewModel()
|
||||||
val searchFeedState by searchViewModel.relays.collectAsStateWithLifecycle()
|
val searchFeedState by searchViewModel.relays.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val localViewModel: LocalRelayListViewModel = viewModel()
|
||||||
|
val localFeedState by localViewModel.relays.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
kind3ViewModel.load(accountViewModel.account)
|
kind3ViewModel.load(accountViewModel.account)
|
||||||
dmViewModel.load(accountViewModel.account)
|
dmViewModel.load(accountViewModel.account)
|
||||||
nip65ViewModel.load(accountViewModel.account)
|
nip65ViewModel.load(accountViewModel.account)
|
||||||
searchViewModel.load(accountViewModel.account)
|
searchViewModel.load(accountViewModel.account)
|
||||||
|
localViewModel.load(accountViewModel.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
@@ -102,6 +106,7 @@ fun AllRelayListView(
|
|||||||
dmViewModel.create()
|
dmViewModel.create()
|
||||||
nip65ViewModel.create()
|
nip65ViewModel.create()
|
||||||
searchViewModel.create()
|
searchViewModel.create()
|
||||||
|
localViewModel.create()
|
||||||
onClose()
|
onClose()
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
@@ -173,6 +178,14 @@ fun AllRelayListView(
|
|||||||
}
|
}
|
||||||
renderSearchItems(searchFeedState, searchViewModel, accountViewModel, onClose, nav)
|
renderSearchItems(searchFeedState, searchViewModel, accountViewModel, onClose, nav)
|
||||||
|
|
||||||
|
item {
|
||||||
|
SettingsCategory(
|
||||||
|
stringResource(R.string.local_section),
|
||||||
|
stringResource(R.string.local_section_explainer),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderLocalItems(localFeedState, localViewModel, accountViewModel, onClose, nav)
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SettingsCategoryWithButton(
|
SettingsCategoryWithButton(
|
||||||
stringResource(R.string.kind_3_section),
|
stringResource(R.string.kind_3_section),
|
||||||
|
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 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.ui.actions.relays
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LocalRelayList(
|
||||||
|
postViewModel: LocalRelayListViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
val feedState by postViewModel.relays.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = FeedPadding,
|
||||||
|
) {
|
||||||
|
renderLocalItems(feedState, postViewModel, accountViewModel, onClose, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LazyListScope.renderLocalItems(
|
||||||
|
feedState: List<BasicRelaySetupInfo>,
|
||||||
|
postViewModel: LocalRelayListViewModel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
nav: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
itemsIndexed(feedState, key = { _, item -> "DM" + item.url }) { index, item ->
|
||||||
|
BasicRelaySetupInfoDialog(
|
||||||
|
item,
|
||||||
|
onDelete = { postViewModel.deleteRelay(item) },
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
) {
|
||||||
|
onClose()
|
||||||
|
nav(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
RelayUrlEditField { postViewModel.addRelay(it) }
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 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.ui.actions.relays
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class LocalRelayListViewModel : ViewModel() {
|
||||||
|
private lateinit var account: Account
|
||||||
|
|
||||||
|
private val _relays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList())
|
||||||
|
val relays = _relays.asStateFlow()
|
||||||
|
|
||||||
|
fun load(account: Account) {
|
||||||
|
this.account = account
|
||||||
|
clear()
|
||||||
|
loadRelayDocuments()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
account.updateLocalRelayServers(_relays.value.map { it.url }.toSet())
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadRelayDocuments() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_relays.value.forEach { item ->
|
||||||
|
Nip11CachedRetriever.loadRelayInfo(
|
||||||
|
dirtyUrl = item.url,
|
||||||
|
onInfo = {
|
||||||
|
togglePaidRelay(item, it.limitation?.payment_required ?: false)
|
||||||
|
},
|
||||||
|
onError = { url, errorCode, exceptionMessage -> },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
_relays.update {
|
||||||
|
val relayList = account.localRelayServers ?: emptyList()
|
||||||
|
|
||||||
|
relayList.map { relayUrl ->
|
||||||
|
val liveRelay = RelayPool.getRelay(relayUrl)
|
||||||
|
val errorCounter = liveRelay?.errorCounter ?: 0
|
||||||
|
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
|
||||||
|
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
|
||||||
|
val spamCounter = liveRelay?.spamCounter ?: 0
|
||||||
|
|
||||||
|
BasicRelaySetupInfo(
|
||||||
|
relayUrl,
|
||||||
|
errorCounter,
|
||||||
|
eventDownloadCounter,
|
||||||
|
eventUploadCounter,
|
||||||
|
spamCounter,
|
||||||
|
)
|
||||||
|
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addRelay(relay: BasicRelaySetupInfo) {
|
||||||
|
if (relays.value.any { it.url == relay.url }) return
|
||||||
|
|
||||||
|
_relays.update { it.plus(relay) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteRelay(relay: BasicRelaySetupInfo) {
|
||||||
|
_relays.update { it.minus(relay) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAll() {
|
||||||
|
_relays.update { relays -> emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun togglePaidRelay(
|
||||||
|
relay: BasicRelaySetupInfo,
|
||||||
|
paid: Boolean,
|
||||||
|
) {
|
||||||
|
_relays.update { it.updated(relay, relay.copy(paidRelay = paid)) }
|
||||||
|
}
|
||||||
|
}
|
@@ -828,6 +828,8 @@
|
|||||||
<string name="kind_3_section_description">Amethyst uses these relays to download posts for you.</string>
|
<string name="kind_3_section_description">Amethyst uses these relays to download posts for you.</string>
|
||||||
<string name="search_section">Search Relays</string>
|
<string name="search_section">Search Relays</string>
|
||||||
<string name="search_section_explainer">List of relays to use for search and tagging users. Tagging and search will not work if no options are available.</string>
|
<string name="search_section_explainer">List of relays to use for search and tagging users. Tagging and search will not work if no options are available.</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="zap_the_devs_title">Zap the Devs!</string>
|
<string name="zap_the_devs_title">Zap the Devs!</string>
|
||||||
<string name="zap_the_devs_description">Your donation helps us make a difference. Every sat counts!</string>
|
<string name="zap_the_devs_description">Your donation helps us make a difference. Every sat counts!</string>
|
||||||
|
Reference in New Issue
Block a user