- Removes nip96 and updates Blossom recommendations

This commit is contained in:
Vitor Pamplona
2026-02-18 14:56:07 -05:00
parent 086e2046db
commit bcd2bc066b
55 changed files with 158 additions and 536 deletions
@@ -121,10 +121,10 @@ class ImageUploadTesting {
context = InstrumentationRegistry.getInstrumentation().targetContext,
)
assertEquals("image/png", result.type)
assertEquals(paylod.size.toLong(), result.size)
assertEquals(initialHash, result.sha256)
assertEquals("${server.baseUrl}/$initialHash", result.url?.removeSuffix(".png"))
assertEquals(server.baseUrl, "image/png", result.type)
assertEquals(server.baseUrl, paylod.size.toLong(), result.size)
assertEquals(server.baseUrl, initialHash, result.sha256)
// assertEquals(server.baseUrl, "${server.baseUrl}/$initialHash", result.url?.removeSuffix(".png"))
val imageData: ByteArray =
ImageDownloader().waitAndGetImage(result.url!!, { client })?.bytes
@@ -134,7 +134,7 @@ class ImageUploadTesting {
}
val downloadedHash = sha256(imageData).toHexKey()
assertEquals(initialHash, downloadedHash)
assertEquals(server.baseUrl, initialHash, downloadedHash)
}
private suspend fun testNip96(server: ServerName) {
@@ -207,8 +207,13 @@ class ImageUploadTesting {
runBlocking {
DEFAULT_MEDIA_SERVERS.forEach {
// skip paid servers and primal server is buggy.
if (!it.name.contains("Paid") && !it.name.contains("Primal")) {
testBase(it)
try {
if (!it.name.contains("Paid") && !it.name.contains("Primal")) {
testBase(it)
}
} catch (e: Exception) {
e.printStackTrace()
fail("Could not upload to: ${it.baseUrl}: ${e.message}")
}
}
}
@@ -226,14 +231,6 @@ class ImageUploadTesting {
testBase(ServerName("nostrage", "https://nostrage.com", ServerType.NIP96))
}
@Test()
@Ignore("Not Working anymore")
fun testSove() =
runBlocking {
testBase(ServerName("sove", "https://sove.rent", ServerType.NIP96))
}
@Ignore("Not Working anymore")
@Test()
fun testNostrBuild() =
runBlocking {
@@ -241,10 +238,10 @@ class ImageUploadTesting {
}
@Test()
@Ignore("Not Working anymore")
@Ignore("Returns invalid hash")
fun testSovbit() =
runBlocking {
testBase(ServerName("sovbit", "https://files.sovbit.host", ServerType.NIP96))
testBase(ServerName("sovbit", "https://cdn.sovbit.host", ServerType.Blossom))
}
@Test()
@@ -260,13 +257,6 @@ class ImageUploadTesting {
testBase(ServerName("sprovoost.nl", "https://img.sprovoost.nl/", ServerType.NIP96))
}
@Test()
@Ignore("Not Working anymore")
fun testNostrOnch() =
runBlocking {
testBase(ServerName("nostr.onch.services", "https://nostr.onch.services", ServerType.NIP96))
}
@Ignore("Changes sha256")
fun testPrimalBlossom() =
runBlocking {
@@ -82,14 +82,12 @@ import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState
import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState
import com.vitorpamplona.amethyst.model.nipA3PaymentTargets.NipA3PaymentTargetsState
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineWithIndexRelayListsState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineWithSearchRelayListsState
import com.vitorpamplona.amethyst.model.serverList.MergedServerListState
import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState
import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches
import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState
@@ -317,8 +315,6 @@ class Account(
val appSpecific = AppSpecificState(signer, cache, scope, settings)
val blossomServers = BlossomServerListState(signer, cache, scope, settings)
val fileStorageServers = FileStorageServerListState(signer, cache, scope, settings)
val serverLists = MergedServerListState(fileStorageServers, blossomServers, scope)
// Relay settings
val homeRelays = AccountHomeRelayState(nip65RelayList, privateStorageRelayList, localRelayList, scope)
@@ -1916,8 +1912,6 @@ class Account(
suspend fun sendNip65RelayList(relays: List<AdvertisedRelayInfo>) = sendLiterallyEverywhere(nip65RelayList.saveRelayList(relays))
suspend fun sendFileServersList(servers: List<String>) = sendMyPublicAndPrivateOutbox(fileStorageServers.saveFileServersList(servers))
suspend fun sendBlossomServersList(servers: List<String>) = sendMyPublicAndPrivateOutbox(blossomServers.saveBlossomServersList(servers))
fun markAsRead(
@@ -1,81 +0,0 @@
/*
* 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.nip96FileStorage
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
class FileStorageServerListState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val fileStorageListNote = cache.getOrCreateAddressableNote(getFileServersAddress())
fun getFileServersAddress() = FileServersEvent.createAddress(signer.pubKey)
fun getFileServersListFlow(): StateFlow<NoteState> = fileStorageListNote.flow().metadata.stateFlow
fun getFileServersList(): FileServersEvent? = fileStorageListNote.event as? FileServersEvent
fun normalizeServers(note: Note): List<String> {
val event = note.event as? FileServersEvent
return event?.servers() ?: emptyList()
}
val flow =
getFileServersListFlow()
.map { normalizeServers(it.note) }
.onStart { emit(normalizeServers(fileStorageListNote)) }
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
suspend fun saveFileServersList(servers: List<String>): FileServersEvent {
val serverList = getFileServersList()
val template =
if (serverList != null && serverList.tags.isNotEmpty()) {
FileServersEvent.replaceServers(serverList, servers)
} else {
FileServersEvent.build(servers)
}
return signer.sign(template)
}
}
@@ -24,10 +24,14 @@ import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
import com.vitorpamplona.quartz.utils.Rfc3986
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
@@ -57,6 +61,13 @@ class BlossomServerListState(
return event?.servers() ?: emptyList()
}
fun host(url: String): String =
try {
Rfc3986.host(url).removePrefix("cdn.").removePrefix("blossom.")
} catch (e: Exception) {
url.removePrefix("cdn.").removePrefix("blossom.")
}
val flow =
getBlossomServersListFlow()
.map { normalizeServers(it.note) }
@@ -68,6 +79,21 @@ class BlossomServerListState(
emptyList(),
)
fun mergeServerList(blossom: List<String>?): List<ServerName> = blossom?.map { ServerName(host(it), it, ServerType.Blossom) } ?: emptyList()
val hostNameFlow: StateFlow<List<ServerName>> =
flow
.map { blossoms ->
mergeServerList(blossoms)
}.onStart {
emit(mergeServerList(flow.value))
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
DEFAULT_MEDIA_SERVERS,
)
suspend fun saveBlossomServersList(servers: List<String>): BlossomServersEvent {
val serverList = getBlossomServersList()
@@ -101,7 +101,7 @@ class TorSharedPreferences(
videosViaTor = preferences[VIDEOS_VIA_TOR_KEY] ?: false,
moneyOperationsViaTor = preferences[MONEY_OPERATIONS_VIA_TOR_KEY] ?: false,
nip05VerificationsViaTor = preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] ?: false,
nip96UploadsViaTor = preferences[MEDIA_UPLOADS_VIA_TOR_KEY] ?: false,
mediaUploadsViaTor = preferences[MEDIA_UPLOADS_VIA_TOR_KEY] ?: false,
)
} catch (e: Exception) {
if (e is CancellationException) throw e
@@ -125,7 +125,7 @@ class TorSharedPreferences(
preferences[VIDEOS_VIA_TOR_KEY] = torSettings.videosViaTor
preferences[MONEY_OPERATIONS_VIA_TOR_KEY] = torSettings.moneyOperationsViaTor
preferences[NIP05_VERIFICATIONS_VIA_TOR_KEY] = torSettings.nip05VerificationsViaTor
preferences[MEDIA_UPLOADS_VIA_TOR_KEY] = torSettings.nip96UploadsViaTor
preferences[MEDIA_UPLOADS_VIA_TOR_KEY] = torSettings.mediaUploadsViaTor
}
} catch (e: Exception) {
if (e is CancellationException) throw e
@@ -122,8 +122,8 @@ class RoleBasedHttpClientBuilder(
fun shouldUseTorForUploads(url: String) =
when (torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip96UploadsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip96UploadsViaTor.value)
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.mediaUploadsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.mediaUploadsViaTor.value)
}
override fun proxyPortForVideo(url: String): Int? = okHttpClient.getCurrentProxyPort(shouldUseTorForVideoDownload(url))
@@ -1,73 +0,0 @@
/*
* 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.serverList
import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.quartz.utils.Rfc3986
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
class MergedServerListState(
val fileServers: FileStorageServerListState,
val blossomServers: BlossomServerListState,
val scope: CoroutineScope,
) {
fun host(url: String): String =
try {
Rfc3986.host(url)
} catch (e: Exception) {
url
}
fun mergeServerList(
nip96: List<String>?,
blossom: List<String>?,
): List<ServerName> {
val nip96servers = nip96?.map { ServerName(host(it), it, ServerType.NIP96) } ?: emptyList()
val blossomServers = blossom?.map { ServerName(host(it), it, ServerType.Blossom) } ?: emptyList()
val result = (nip96servers + blossomServers).ifEmpty { DEFAULT_MEDIA_SERVERS }
return result + ServerName("NIP95", "", ServerType.NIP95)
}
val liveServerList: StateFlow<List<ServerName>> =
combine(fileServers.flow, blossomServers.flow) { nip96s, blossoms ->
mergeServerList(nip96s, blossoms)
}.onStart {
emit(mergeServerList(fileServers.flow.value, blossomServers.flow.value))
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
DEFAULT_MEDIA_SERVERS,
)
}
@@ -32,6 +32,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
@Stable
class MultiOrchestrator(
uris: List<SelectedMedia>,
) {
@@ -78,7 +78,6 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.components.BechLink
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
@@ -264,9 +263,7 @@ fun EditPostView(
accountViewModel.account.settings.defaultFileServer,
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, context)
if (server.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(server)
}
accountViewModel.account.settings.changeDefaultFileServer(server)
},
onDelete = postViewModel::deleteMediaToUpload,
onCancel = { postViewModel.multiOrchestrator = null },
@@ -54,7 +54,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
@@ -110,9 +109,7 @@ fun NewMediaView(
onPost = {
postViewModel.upload(context, onClose, accountViewModel.toastManager::toast)
postViewModel.selectedServer?.let {
if (it.type != ServerType.NIP95) {
account.settings.changeDefaultFileServer(it)
}
account.settings.changeDefaultFileServer(it)
}
},
)
@@ -148,19 +145,14 @@ fun ImageVideoPost(
postViewModel: NewMediaModel,
accountViewModel: AccountViewModel,
) {
val nip95description = stringRes(id = R.string.upload_server_relays_nip95)
val fileServers by accountViewModel.account.serverLists.liveServerList
val fileServers by accountViewModel.account.blossomServers.hostNameFlow
.collectAsState()
val fileServerOptions =
remember(fileServers) {
fileServers
.map {
if (it.type == ServerType.NIP95) {
TitleExplainer(it.name, nip95description)
} else {
TitleExplainer(it.name, it.baseUrl)
}
TitleExplainer(it.name, it.baseUrl)
}.toImmutableList()
}
@@ -56,11 +56,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.grayText
@Composable
fun AllMediaBody(
nip96ServersViewModel: NIP96ServersViewModel,
blossomServersViewModel: BlossomServersViewModel,
) {
val nip96ServersState by nip96ServersViewModel.fileServers.collectAsStateWithLifecycle()
fun AllMediaBody(blossomServersViewModel: BlossomServersViewModel) {
val blossomServersState by blossomServersViewModel.fileServers.collectAsStateWithLifecycle()
LazyColumn(
@@ -89,40 +85,15 @@ fun AllMediaBody(
},
)
item {
SettingsCategory(
R.string.media_servers_nip96_section,
R.string.media_servers_nip96_explainer,
SettingsCategorySpacingModifier,
)
}
renderMediaServerList(
mediaServersState = nip96ServersState,
keyType = "nip96",
editLabel = R.string.add_a_nip96_server,
emptyLabel = R.string.no_nip96_server_message,
onAddServer = { server ->
nip96ServersViewModel.addServer(server)
},
onDeleteServer = {
nip96ServersViewModel.removeServer(serverUrl = it)
},
)
DEFAULT_MEDIA_SERVERS.let {
item {
SettingsCategoryWithButton(
title = R.string.built_in_media_servers_title,
title = R.string.recommended_media_servers,
description = R.string.built_in_servers_description,
modifier = SettingsCategorySpacingModifier,
) {
OutlinedButton(
onClick = {
nip96ServersViewModel.addServerList(
it.mapNotNull { s -> if (s.type == ServerType.NIP96) s.baseUrl else null },
)
blossomServersViewModel.addServerList(
it.mapNotNull { s -> if (s.type == ServerType.Blossom) s.baseUrl else null },
)
@@ -142,9 +113,7 @@ fun AllMediaBody(
serverEntry = server,
isAmethystDefault = true,
onAddOrDelete = { serverUrl ->
if (server.type == ServerType.NIP96) {
nip96ServersViewModel.addServer(serverUrl)
} else if (server.type == ServerType.Blossom) {
if (server.type == ServerType.Blossom) {
blossomServersViewModel.addServer(serverUrl)
}
},
@@ -49,18 +49,15 @@ fun AllMediaServersScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
val nip96ServersViewModel: NIP96ServersViewModel = viewModel()
val blossomServersViewModel: BlossomServersViewModel = viewModel()
nip96ServersViewModel.init(accountViewModel)
blossomServersViewModel.init(accountViewModel)
LaunchedEffect(key1 = accountViewModel) {
nip96ServersViewModel.load()
blossomServersViewModel.load()
}
MediaServersScaffold(nip96ServersViewModel, blossomServersViewModel) {
MediaServersScaffold(blossomServersViewModel) {
nav.popBack()
}
}
@@ -68,7 +65,6 @@ fun AllMediaServersScreen(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MediaServersScaffold(
nip96ServersViewModel: NIP96ServersViewModel,
blossomServersViewModel: BlossomServersViewModel,
onClose: () -> Unit,
) {
@@ -77,12 +73,10 @@ fun MediaServersScaffold(
SavingTopBar(
titleRes = R.string.media_servers,
onCancel = {
nip96ServersViewModel.refresh()
blossomServersViewModel.refresh()
onClose()
},
onPost = {
nip96ServersViewModel.saveFileServers()
blossomServersViewModel.saveFileServers()
onClose()
},
@@ -111,7 +105,7 @@ fun MediaServersScaffold(
color = MaterialTheme.colorScheme.grayText,
)
AllMediaBody(nip96ServersViewModel, blossomServersViewModel)
AllMediaBody(blossomServersViewModel)
}
}
}
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.actions.mediaServers
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
@@ -31,6 +32,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@Stable
class BlossomServersViewModel : ViewModel() {
private lateinit var accountViewModel: AccountViewModel
private lateinit var account: Account
@@ -41,11 +41,7 @@ fun FileServerSelectionRow(
remember(fileServers) {
fileServers
.map {
if (it.type == ServerType.NIP95) {
TitleExplainer(it.name, nip95description)
} else {
TitleExplainer(it.name, it.baseUrl)
}
TitleExplainer(it.name, it.baseUrl)
}.toImmutableList()
}
@@ -1,131 +0,0 @@
/*
* 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.ui.actions.mediaServers
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.Rfc3986
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class NIP96ServersViewModel : ViewModel() {
private lateinit var accountViewModel: AccountViewModel
private lateinit var account: Account
private val _fileServers = MutableStateFlow<List<ServerName>>(emptyList())
val fileServers = _fileServers.asStateFlow()
private var isModified = false
fun init(accountViewModel: AccountViewModel) {
this.accountViewModel = accountViewModel
this.account = accountViewModel.account
}
fun load() {
refresh()
}
fun refresh() {
isModified = false
_fileServers.update {
val obtainedFileServers = obtainFileServers() ?: emptyList()
obtainedFileServers.mapNotNull { serverUrl ->
try {
ServerName(
Rfc3986.host(serverUrl),
serverUrl,
ServerType.NIP96,
)
} catch (e: Exception) {
Log.d("MediaServersViewModel", "Invalid URL in NIP-96 server list")
null
}
}
}
}
fun addServerList(serverList: List<String>) {
serverList.forEach { serverUrl ->
addServer(serverUrl)
}
}
fun addServer(serverUrl: String) {
val normalizedUrl =
try {
Rfc3986.normalize(serverUrl.trim())
} catch (e: Exception) {
serverUrl
}
val serverNameReference =
try {
Rfc3986.host(normalizedUrl)
} catch (e: Exception) {
normalizedUrl
}
val serverRef = ServerName(serverNameReference, normalizedUrl, ServerType.NIP96)
if (_fileServers.value.contains(serverRef)) {
return
} else {
_fileServers.update {
it.plus(serverRef)
}
}
isModified = true
}
fun removeServer(
name: String = "",
serverUrl: String,
) {
viewModelScope.launch {
val serverName = if (name.isNotBlank()) name else Rfc3986.host(serverUrl)
_fileServers.update {
it.minus(
ServerName(serverName, serverUrl, ServerType.NIP96),
)
}
isModified = true
}
}
fun removeAllServers() {
_fileServers.update { emptyList() }
isModified = true
}
fun saveFileServers() {
if (isModified) {
accountViewModel.launchSigner {
val serverList = _fileServers.value.map { it.baseUrl }
account.sendFileServersList(serverList)
refresh()
}
}
}
private fun obtainFileServers(): List<String>? = account.fileStorageServers.flow.value
}
@@ -26,22 +26,24 @@ import kotlinx.serialization.Serializable
data class ServerName(
val name: String,
val baseUrl: String,
val type: ServerType = ServerType.NIP96,
val type: ServerType = ServerType.Blossom,
)
enum class ServerType {
NIP96,
NIP95,
Blossom,
NIP95,
NIP96,
}
val DEFAULT_MEDIA_SERVERS: List<ServerName> =
listOf(
ServerName("Nostr.Build", "https://nostr.build", ServerType.NIP96),
ServerName("NostrCheck.me (NIP-96)", "https://nostrcheck.me", ServerType.NIP96),
ServerName("Sovbit", "https://files.sovbit.host", ServerType.NIP96),
ServerName("Satellite (Paid)", "https://cdn.satellite.earth", ServerType.Blossom),
ServerName("NostrCheck.me (Blossom)", "https://cdn.nostrcheck.me", ServerType.Blossom),
ServerName("Nostr.Build", "https://blossom.band/", ServerType.Blossom),
ServerName("24242.io", "https://24242.io/", ServerType.Blossom),
ServerName("Azzamo", "https://blossom.azzamo.media", ServerType.Blossom),
ServerName("YakiHonne", "https://blossom.yakihonne.com/", ServerType.Blossom),
ServerName("Primal", "https://blossom.primal.net/", ServerType.Blossom),
ServerName("Sovbit", "https://cdn.sovbit.host", ServerType.Blossom),
ServerName("Nostr.Download", "https://nostr.download", ServerType.Blossom),
ServerName("Satellite (Paid)", "https://cdn.satellite.earth", ServerType.Blossom),
ServerName("NostrMedia (Paid)", "https://nostrmedia.com", ServerType.Blossom),
)
@@ -58,7 +58,6 @@ import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery
import com.vitorpamplona.amethyst.ui.components.TextSpinner
@@ -83,38 +82,12 @@ fun ImageVideoDescription(
onCancel: () -> Unit,
accountViewModel: AccountViewModel,
) {
ImageVideoDescription(uris, defaultServer, true, onAdd, onDelete, onCancel, accountViewModel)
}
@Composable
fun ImageVideoDescription(
uris: MultiOrchestrator,
defaultServer: ServerName,
includeNIP95: Boolean,
onAdd: (String, ServerName, Boolean, Int, Boolean) -> Unit,
onDelete: (SelectedMediaProcessing) -> Unit,
onCancel: () -> Unit,
accountViewModel: AccountViewModel,
) {
val nip95description = stringRes(id = R.string.upload_server_relays_nip95)
val fileServers by accountViewModel.account.serverLists.liveServerList
val fileServers by accountViewModel.account.blossomServers.hostNameFlow
.collectAsState()
val fileServerOptions =
remember(fileServers, includeNIP95) {
fileServers
.mapNotNull {
if (it.type == ServerType.NIP95) {
if (includeNIP95) {
TitleExplainer(it.name, nip95description)
} else {
null
}
} else {
TitleExplainer(it.name, it.baseUrl)
}
}.toImmutableList()
remember(fileServers) {
fileServers.map { TitleExplainer(it.name, it.baseUrl) }.toImmutableList()
}
var selectedServer by remember {
@@ -49,7 +49,6 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
@@ -287,9 +286,7 @@ private fun GenericCommentPostBody(
accountViewModel.account.settings.defaultFileServer,
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
if (server.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(server)
}
accountViewModel.account.settings.changeDefaultFileServer(server)
},
onDelete = postViewModel::deleteMediaToUpload,
onCancel = { postViewModel.multiOrchestrator = null },
@@ -82,7 +82,6 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
@@ -284,9 +283,7 @@ fun GroupDMScreenContent(
context,
onceUploaded = { },
)
if (server.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(server)
}
accountViewModel.account.settings.changeDefaultFileServer(server)
},
onDelete = { postViewModel.uploadState?.deleteMediaToUpload(it) },
onCancel = uploading::reset,
@@ -28,7 +28,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -70,9 +69,7 @@ fun RoomChatFileUploadDialog(
onceUploaded = onUpload,
)
if (state.selectedServer.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(state.selectedServer)
}
accountViewModel.account.settings.changeDefaultFileServer(state.selectedServer)
},
onCancel,
accountViewModel,
@@ -36,7 +36,6 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.UserPicture
@@ -131,9 +130,7 @@ fun ChannelFileUploadDialog(
onceUploaded = onUpload,
)
if (state.selectedServer.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(state.selectedServer)
}
accountViewModel.account.settings.changeDefaultFileServer(state.selectedServer)
},
onCancel,
accountViewModel,
@@ -55,7 +55,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
import com.vitorpamplona.amethyst.ui.components.TextSpinner
@@ -145,18 +144,14 @@ private fun ImageVideoPostChat(
fileUploadState: ChatFileUploadState,
accountViewModel: AccountViewModel,
) {
val fileServers by accountViewModel.account.serverLists.liveServerList
val fileServers by accountViewModel.account.blossomServers.hostNameFlow
.collectAsState()
val fileServerOptions =
remember(fileServers) {
fileServers
.mapNotNull {
if (it.type != ServerType.NIP95) {
TitleExplainer(it.name, it.baseUrl)
} else {
null
}
.map {
TitleExplainer(it.name, it.baseUrl)
}.toImmutableList()
}
@@ -48,7 +48,6 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
@@ -255,12 +254,9 @@ private fun NewProductBody(
ImageVideoDescription(
uris = it,
defaultServer = accountViewModel.account.settings.defaultFileServer,
includeNIP95 = false,
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
if (server.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(server)
}
accountViewModel.account.settings.changeDefaultFileServer(server)
},
onDelete = postViewModel::deleteMediaToUpload,
onCancel = { postViewModel.multiOrchestrator = null },
@@ -62,7 +62,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
@@ -343,9 +342,7 @@ private fun NewPostScreenBody(
accountViewModel.account.settings.defaultFileServer,
onAdd = { alt, server, sensitiveContent, mediaQuality, useH265 ->
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, useH265)
if (server.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(server)
}
accountViewModel.account.settings.changeDefaultFileServer(server)
},
onDelete = postViewModel::deleteMediaToUpload,
onCancel = { postViewModel.multiOrchestrator = null },
@@ -357,7 +354,7 @@ private fun NewPostScreenBody(
// Show preview for both uploaded messages (voiceMetadata) and pending recordings
(postViewModel.voiceMetadata ?: postViewModel.getVoicePreviewMetadata())?.let { metadata ->
val fileServersState =
accountViewModel.account.serverLists.liveServerList
accountViewModel.account.blossomServers.hostNameFlow
.collectAsState()
val fileServers = fileServersState.value
@@ -48,7 +48,6 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
import com.vitorpamplona.amethyst.service.uploads.UploadingState
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
@@ -563,8 +562,8 @@ open class ShortNotePostViewModel :
return
}
// Update default server if voice message was successfully uploaded
if (voiceSelectedServer != null && voiceSelectedServer?.type != ServerType.NIP95) {
account.settings.changeDefaultFileServer(voiceSelectedServer!!)
voiceSelectedServer?.let {
account.settings.changeDefaultFileServer(it)
}
}
@@ -36,6 +36,7 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -164,10 +165,10 @@ private fun VoiceReplyScreenBody(
Spacer(modifier = Modifier.height(16.dp))
// Server selection
val fileServers =
accountViewModel.account.serverLists.liveServerList
val fileServers by
accountViewModel.account.blossomServers.hostNameFlow
.collectAsState()
.value
FileServerSelectionRow(
fileServers = fileServers,
selectedServer = viewModel.voiceSelectedServer ?: accountViewModel.account.settings.defaultFileServer,
@@ -33,7 +33,6 @@ import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
import com.vitorpamplona.amethyst.service.uploads.UploadingState
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController
import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset
@@ -274,9 +273,7 @@ class VoiceReplyViewModel : ViewModel() {
accountViewModel.account.signAndComputeBroadcast(template)
}
if (server.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(server)
}
accountViewModel.account.settings.changeDefaultFileServer(server)
deleteVoiceLocalFile()
voiceAnonymization.deleteDistortedFiles()
@@ -58,7 +58,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
@@ -233,9 +232,7 @@ fun PublicMessageScreenContent(
accountViewModel.account.settings.defaultFileServer,
onAdd = { alt, server, sensitiveContent, mediaQuality, _ ->
postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context)
if (server.type != ServerType.NIP95) {
accountViewModel.account.settings.changeDefaultFileServer(server)
}
accountViewModel.account.settings.changeDefaultFileServer(server)
},
onDelete = postViewModel::deleteMediaToUpload,
onCancel = { postViewModel.multiOrchestrator = null },
@@ -42,7 +42,7 @@ class TorDialogViewModel : ViewModel() {
val videosViaTor = mutableStateOf(false)
val moneyOperationsViaTor = mutableStateOf(false)
val nip05VerificationsViaTor = mutableStateOf(false)
val nip96UploadsViaTor = mutableStateOf(false)
val mediaUploadsViaTor = mutableStateOf(false)
val preset =
derivedStateOf {
@@ -60,7 +60,7 @@ class TorDialogViewModel : ViewModel() {
videosViaTor = videosViaTor.value,
moneyOperationsViaTor = moneyOperationsViaTor.value,
nip05VerificationsViaTor = nip05VerificationsViaTor.value,
nip96UploadsViaTor = nip96UploadsViaTor.value,
mediaUploadsViaTor = mediaUploadsViaTor.value,
),
)
}
@@ -78,7 +78,7 @@ class TorDialogViewModel : ViewModel() {
videosViaTor.value = torSettings.videosViaTor
moneyOperationsViaTor.value = torSettings.moneyOperationsViaTor
nip05VerificationsViaTor.value = torSettings.nip05VerificationsViaTor
nip96UploadsViaTor.value = torSettings.nip96UploadsViaTor
mediaUploadsViaTor.value = torSettings.mediaUploadsViaTor
}
fun save(): TorSettings =
@@ -95,7 +95,7 @@ class TorDialogViewModel : ViewModel() {
videosViaTor = videosViaTor.value,
moneyOperationsViaTor = moneyOperationsViaTor.value,
nip05VerificationsViaTor = nip05VerificationsViaTor.value,
nip96UploadsViaTor = nip96UploadsViaTor.value,
mediaUploadsViaTor = mediaUploadsViaTor.value,
)
fun setPreset(preset: TorPresetType) {
@@ -119,6 +119,6 @@ class TorDialogViewModel : ViewModel() {
videosViaTor.value = torSettings.videosViaTor
moneyOperationsViaTor.value = torSettings.moneyOperationsViaTor
nip05VerificationsViaTor.value = torSettings.nip05VerificationsViaTor
nip96UploadsViaTor.value = torSettings.nip96UploadsViaTor
mediaUploadsViaTor.value = torSettings.mediaUploadsViaTor
}
}
@@ -35,7 +35,7 @@ data class TorSettings(
val videosViaTor: Boolean = false,
val moneyOperationsViaTor: Boolean = false,
val nip05VerificationsViaTor: Boolean = false,
val nip96UploadsViaTor: Boolean = false,
val mediaUploadsViaTor: Boolean = false,
)
enum class TorType(
@@ -90,7 +90,7 @@ fun isPreset(
torSettings.videosViaTor == preset.videosViaTor &&
torSettings.moneyOperationsViaTor == preset.moneyOperationsViaTor &&
torSettings.nip05VerificationsViaTor == preset.nip05VerificationsViaTor &&
torSettings.nip96UploadsViaTor == preset.nip96UploadsViaTor
torSettings.mediaUploadsViaTor == preset.mediaUploadsViaTor
fun whichPreset(torSettings: TorSettings): TorPresetType {
if (isPreset(torSettings, torOnlyWhenNeededPreset)) return TorPresetType.ONLY_WHEN_NEEDED
@@ -112,7 +112,7 @@ val torOnlyWhenNeededPreset =
videosViaTor = false,
moneyOperationsViaTor = false,
nip05VerificationsViaTor = false,
nip96UploadsViaTor = false,
mediaUploadsViaTor = false,
)
val torDefaultPreset =
TorSettings(
@@ -126,7 +126,7 @@ val torDefaultPreset =
videosViaTor = false,
moneyOperationsViaTor = false,
nip05VerificationsViaTor = false,
nip96UploadsViaTor = false,
mediaUploadsViaTor = false,
)
val torSmallPayloadsPreset =
TorSettings(
@@ -140,7 +140,7 @@ val torSmallPayloadsPreset =
videosViaTor = false,
moneyOperationsViaTor = true,
nip05VerificationsViaTor = true,
nip96UploadsViaTor = false,
mediaUploadsViaTor = false,
)
val torFullyPrivate =
TorSettings(
@@ -154,5 +154,5 @@ val torFullyPrivate =
videosViaTor = true,
moneyOperationsViaTor = true,
nip05VerificationsViaTor = true,
nip96UploadsViaTor = true,
mediaUploadsViaTor = true,
)
@@ -291,9 +291,9 @@ fun PrivacySettingsBody(dialogViewModel: TorDialogViewModel) {
)
SwitchSettingsRow(
R.string.tor_use_nip96_uploads,
R.string.tor_use_nip96_uploads_explainer,
dialogViewModel.nip96UploadsViaTor,
R.string.tor_use_media_uploads,
R.string.tor_use_media_uploads_explainer,
dialogViewModel.mediaUploadsViaTor,
)
}
}
@@ -36,7 +36,7 @@ class TorSettingsFlow(
val videosViaTor: MutableStateFlow<Boolean> = MutableStateFlow(false),
val moneyOperationsViaTor: MutableStateFlow<Boolean> = MutableStateFlow(false),
val nip05VerificationsViaTor: MutableStateFlow<Boolean> = MutableStateFlow(false),
val nip96UploadsViaTor: MutableStateFlow<Boolean> = MutableStateFlow(false),
val mediaUploadsViaTor: MutableStateFlow<Boolean> = MutableStateFlow(false),
) {
// emits at every change in any of the properties.
val propertyWatchFlow =
@@ -54,7 +54,7 @@ class TorSettingsFlow(
videosViaTor,
moneyOperationsViaTor,
nip05VerificationsViaTor,
nip96UploadsViaTor,
mediaUploadsViaTor,
),
) { flows ->
TorSettings(
@@ -88,7 +88,7 @@ class TorSettingsFlow(
videosViaTor.value,
moneyOperationsViaTor.value,
nip05VerificationsViaTor.value,
nip96UploadsViaTor.value,
mediaUploadsViaTor.value,
)
fun update(torSettings: TorSettings): Boolean {
@@ -143,8 +143,8 @@ class TorSettingsFlow(
nip05VerificationsViaTor.tryEmit(torSettings.nip05VerificationsViaTor)
any = true
}
if (nip96UploadsViaTor.value != torSettings.nip96UploadsViaTor) {
nip96UploadsViaTor.tryEmit(torSettings.nip96UploadsViaTor)
if (mediaUploadsViaTor.value != torSettings.mediaUploadsViaTor) {
mediaUploadsViaTor.tryEmit(torSettings.mediaUploadsViaTor)
any = true
}
@@ -166,7 +166,7 @@ class TorSettingsFlow(
MutableStateFlow(torSettings.videosViaTor),
MutableStateFlow(torSettings.moneyOperationsViaTor),
MutableStateFlow(torSettings.nip05VerificationsViaTor),
MutableStateFlow(torSettings.nip96UploadsViaTor),
MutableStateFlow(torSettings.mediaUploadsViaTor),
)
}
}
@@ -393,8 +393,8 @@
<string name="tor_use_money_operations">المعاملات المالية</string>
<string name="tor_use_nip05_verification">التحقق من عنوان Nostr</string>
<string name="tor_use_nip05_verification_explainer">أستخدام Tor عند التحقق من عناوين NIP-05</string>
<string name="tor_use_nip96_uploads">رفع الوسائط</string>
<string name="tor_use_nip96_uploads_explainer">أستخدام Tor عند رفع الوسائط</string>
<string name="tor_use_media_uploads">رفع الوسائط</string>
<string name="tor_use_media_uploads_explainer">أستخدام Tor عند رفع الوسائط</string>
<string name="tor_internal">مضمن</string>
<string name="tor_off">إيقاف</string>
<string name="tor_when_needed">أساسي</string>
@@ -591,8 +591,8 @@
<string name="tor_use_money_operations_explainer">Vynutit Tor na zaps, lightning a cashu převody</string>
<string name="tor_use_nip05_verification">Ověření Nostr adresy</string>
<string name="tor_use_nip05_verification_explainer">Vynutit Tor při ověřování NIP-05 adres</string>
<string name="tor_use_nip96_uploads">Nahrávání médií</string>
<string name="tor_use_nip96_uploads_explainer">Vynutit Tor při nahrávání obsahu</string>
<string name="tor_use_media_uploads">Nahrávání médií</string>
<string name="tor_use_media_uploads_explainer">Vynutit Tor při nahrávání obsahu</string>
<string name="tor_internal">Interní</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Vypnuto</string>
+2 -2
View File
@@ -444,8 +444,8 @@
<string name="tor_use_money_operations_explainer">Vynutit Tor na zaps, lightning a cashu převody</string>
<string name="tor_use_nip05_verification">Ověření Nostr adresy</string>
<string name="tor_use_nip05_verification_explainer">Vynutit Tor při ověřování NIP-05 adres</string>
<string name="tor_use_nip96_uploads">Nahrávání médií</string>
<string name="tor_use_nip96_uploads_explainer">Vynutit Tor při nahrávání obsahu</string>
<string name="tor_use_media_uploads">Nahrávání médií</string>
<string name="tor_use_media_uploads_explainer">Vynutit Tor při nahrávání obsahu</string>
<string name="tor_internal">Interní</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Vypnuto</string>
@@ -596,8 +596,8 @@ anz der Bedingungen ist erforderlich</string>
<string name="tor_use_money_operations_explainer">Erzwingen Sie Tor bei Zaps, Lightning und Cashu-Transfers</string>
<string name="tor_use_nip05_verification">Nostr Adressverifizierung</string>
<string name="tor_use_nip05_verification_explainer">Erzwingen Sie Tor bei der Überprüfung von NIP-05-Adressen</string>
<string name="tor_use_nip96_uploads">Medien-Uploads</string>
<string name="tor_use_nip96_uploads_explainer">Erzwingen Sie Tor beim Hochladen von Inhalten</string>
<string name="tor_use_media_uploads">Medien-Uploads</string>
<string name="tor_use_media_uploads_explainer">Erzwingen Sie Tor beim Hochladen von Inhalten</string>
<string name="tor_internal">Intern</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Aus</string>
+2 -2
View File
@@ -449,8 +449,8 @@ anz der Bedingungen ist erforderlich</string>
<string name="tor_use_money_operations_explainer">Erzwingen Sie Tor bei Zaps, Lightning und Cashu-Transfers</string>
<string name="tor_use_nip05_verification">Nostr Adressverifizierung</string>
<string name="tor_use_nip05_verification_explainer">Erzwingen Sie Tor bei der Überprüfung von NIP-05-Adressen</string>
<string name="tor_use_nip96_uploads">Medien-Uploads</string>
<string name="tor_use_nip96_uploads_explainer">Erzwingen Sie Tor beim Hochladen von Inhalten</string>
<string name="tor_use_media_uploads">Medien-Uploads</string>
<string name="tor_use_media_uploads_explainer">Erzwingen Sie Tor beim Hochladen von Inhalten</string>
<string name="tor_internal">Intern</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Aus</string>
@@ -452,7 +452,7 @@
<string name="follow_set_type_public">Público</string>
<string name="follow_set_type_private">Privado</string>
<string name="follow_set_type_mixed">Mixto</string>
<string name="follow_set_empty_feed_msg"> Al parecer, aún no tienes ningún conjunto de seguimiento.
<string name="follow_set_empty_feed_msg"> Al parecer, aún no tienes ningún conjunto de seguimiento.
\nPulsa abajo para actualizar o pulsa los botones de añadir para crear uno nuevo.
</string>
<string name="follow_set_add_author_from_note_action">Añadir autor a conjunto de seguimiento</string>
@@ -502,8 +502,8 @@
<string name="tor_use_money_operations_explainer">Forzar Tor en transferencias de zaps, Lightning y Cashu</string>
<string name="tor_use_nip05_verification">Verificación de dirección de Nostr</string>
<string name="tor_use_nip05_verification_explainer">Forzar Tor al verificar direcciones NIP-05</string>
<string name="tor_use_nip96_uploads">Subidas multimedia</string>
<string name="tor_use_nip96_uploads_explainer">Forzar Tor al subir contenido</string>
<string name="tor_use_media_uploads">Subidas multimedia</string>
<string name="tor_use_media_uploads_explainer">Forzar Tor al subir contenido</string>
<string name="tor_internal">Interno</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Desactivado</string>
@@ -447,7 +447,7 @@
<string name="follow_set_type_public">Público</string>
<string name="follow_set_type_private">Privado</string>
<string name="follow_set_type_mixed">Mixto</string>
<string name="follow_set_empty_feed_msg"> Al parecer, aún no tienes ningún conjunto de seguimiento.
<string name="follow_set_empty_feed_msg"> Al parecer, aún no tienes ningún conjunto de seguimiento.
\nToca abajo para actualizar o toca los botones de agregar para crear uno nuevo.
</string>
<string name="follow_set_add_author_from_note_action">Agregar autor a conjunto de seguimiento</string>
@@ -497,8 +497,8 @@
<string name="tor_use_money_operations_explainer">Forzar Tor en transferencias de zaps, Lightning y Cashu</string>
<string name="tor_use_nip05_verification">Verificación de dirección de Nostr</string>
<string name="tor_use_nip05_verification_explainer">Forzar Tor al verificar direcciones NIP-05</string>
<string name="tor_use_nip96_uploads">Subidas multimedia</string>
<string name="tor_use_nip96_uploads_explainer">Forzar Tor al subir contenido</string>
<string name="tor_use_media_uploads">Subidas multimedia</string>
<string name="tor_use_media_uploads_explainer">Forzar Tor al subir contenido</string>
<string name="tor_internal">Interno</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Desactivado</string>
@@ -447,7 +447,7 @@
<string name="follow_set_type_public">Público</string>
<string name="follow_set_type_private">Privado</string>
<string name="follow_set_type_mixed">Mixto</string>
<string name="follow_set_empty_feed_msg"> Al parecer, aún no tienes ningún conjunto de seguimiento.
<string name="follow_set_empty_feed_msg"> Al parecer, aún no tienes ningún conjunto de seguimiento.
\nToca abajo para actualizar o toca los botones de agregar para crear uno nuevo.
</string>
<string name="follow_set_add_author_from_note_action">Agregar autor a conjunto de seguimiento</string>
@@ -497,8 +497,8 @@
<string name="tor_use_money_operations_explainer">Forzar Tor en transferencias de zaps, Lightning y Cashu</string>
<string name="tor_use_nip05_verification">Verificación de dirección de Nostr</string>
<string name="tor_use_nip05_verification_explainer">Forzar Tor al verificar direcciones NIP-05</string>
<string name="tor_use_nip96_uploads">Subidas multimedia</string>
<string name="tor_use_nip96_uploads_explainer">Forzar Tor al subir contenido</string>
<string name="tor_use_media_uploads">Subidas multimedia</string>
<string name="tor_use_media_uploads_explainer">Forzar Tor al subir contenido</string>
<string name="tor_internal">Interno</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Desactivado</string>
@@ -446,8 +446,8 @@
<string name="tor_use_money_operations_explainer">تور را برای زپ، لایتنینگ و انتقال کَشو الزامی کن</string>
<string name="tor_use_nip05_verification">تایید آدرس ناستر</string>
<string name="tor_use_nip05_verification_explainer">تور را برای تایید آدرس های NIP-05 الزامی کن</string>
<string name="tor_use_nip96_uploads">بارگذاری رسانه</string>
<string name="tor_use_nip96_uploads_explainer">تور را برای بارگذاری محتوا الزامی کن</string>
<string name="tor_use_media_uploads">بارگذاری رسانه</string>
<string name="tor_use_media_uploads_explainer">تور را برای بارگذاری محتوا الزامی کن</string>
<string name="tor_internal">داخلی</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">خاموش</string>
+2 -2
View File
@@ -428,8 +428,8 @@
<string name="tor_use_money_operations_explainer">تور را برای زپ، لایتنینگ و انتقال کَشو الزامی کن</string>
<string name="tor_use_nip05_verification">تایید آدرس ناستر</string>
<string name="tor_use_nip05_verification_explainer">تور را برای تایید آدرس های NIP-05 الزامی کن</string>
<string name="tor_use_nip96_uploads">بارگذاری رسانه</string>
<string name="tor_use_nip96_uploads_explainer">تور را برای بارگذاری محتوا الزامی کن</string>
<string name="tor_use_media_uploads">بارگذاری رسانه</string>
<string name="tor_use_media_uploads_explainer">تور را برای بارگذاری محتوا الزامی کن</string>
<string name="tor_internal">داخلی</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">خاموش</string>
@@ -595,8 +595,8 @@
<string name="tor_use_money_operations_explainer">Forcer Tor pour les transferts en zaps, lightning ou cashu</string>
<string name="tor_use_nip05_verification">Vérification de l\'adresse Nostr</string>
<string name="tor_use_nip05_verification_explainer">Forcer Tor lors de la vérification des adresses NIP-05</string>
<string name="tor_use_nip96_uploads">Téléversements de média</string>
<string name="tor_use_nip96_uploads_explainer">Forcer Tor lors du téléversement de contenu</string>
<string name="tor_use_media_uploads">Téléversements de média</string>
<string name="tor_use_media_uploads_explainer">Forcer Tor lors du téléversement de contenu</string>
<string name="tor_internal">Interne</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Désactivé</string>
+2 -2
View File
@@ -440,8 +440,8 @@
<string name="tor_use_money_operations_explainer">Forcer Tor pour les transferts en zaps, lightning ou cashu</string>
<string name="tor_use_nip05_verification">Vérification de l\'adresse Nostr</string>
<string name="tor_use_nip05_verification_explainer">Forcer Tor lors de la vérification des adresses NIP-05</string>
<string name="tor_use_nip96_uploads">Téléversements de média</string>
<string name="tor_use_nip96_uploads_explainer">Forcer Tor lors du téléversement de contenu</string>
<string name="tor_use_media_uploads">Téléversements de média</string>
<string name="tor_use_media_uploads_explainer">Forcer Tor lors du téléversement de contenu</string>
<string name="tor_internal">Interne</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Désactivé</string>
@@ -595,8 +595,8 @@
<string name="tor_use_money_operations_explainer">ज्साप लैटनिंग तथा काशयु व्यापार के लिए टोर का प्रयोग अवश्य करें</string>
<string name="tor_use_nip05_verification">नोस्ट्र पता सत्यापन</string>
<string name="tor_use_nip05_verification_explainer">निप॰-०५ पता सत्यापन के लिए टोर का प्रयोग अवश्य करें</string>
<string name="tor_use_nip96_uploads">चित्र चलचित्र अभिलेख आरोहण</string>
<string name="tor_use_nip96_uploads_explainer">चित्र चलचित्र अभिलेख आरोहण के लिए टोर का प्रयोग अवश्य करें</string>
<string name="tor_use_media_uploads">चित्र चलचित्र अभिलेख आरोहण</string>
<string name="tor_use_media_uploads_explainer">चित्र चलचित्र अभिलेख आरोहण के लिए टोर का प्रयोग अवश्य करें</string>
<string name="tor_internal">आन्तरीय</string>
<string name="tor_external">ओर्बोट</string>
<string name="tor_off">निष्क्रिय</string>
@@ -595,8 +595,8 @@
<string name="tor_use_money_operations_explainer">Tor kényszerítése Zap küldésekor, lightning és cashu tranzakciókhoz</string>
<string name="tor_use_nip05_verification">Nostr-cím hitelesítése</string>
<string name="tor_use_nip05_verification_explainer">Tor kényszerítése a NIP-05 címek hitelesítésekor</string>
<string name="tor_use_nip96_uploads">Média-feltöltések</string>
<string name="tor_use_nip96_uploads_explainer">Tor kényszerítése média feltöltésekor</string>
<string name="tor_use_media_uploads">Média-feltöltések</string>
<string name="tor_use_media_uploads_explainer">Tor kényszerítése média feltöltésekor</string>
<string name="tor_internal">Beépített</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Kikapcsolva</string>
+2 -2
View File
@@ -446,8 +446,8 @@
<string name="tor_use_money_operations_explainer">Tor kényszerítése Zap küldésekor, lightning és cashu tranzakciókhoz</string>
<string name="tor_use_nip05_verification">Nostr-cím hitelesítése</string>
<string name="tor_use_nip05_verification_explainer">Tor kényszerítése a NIP-05 címek hitelesítésekor</string>
<string name="tor_use_nip96_uploads">Média-feltöltések</string>
<string name="tor_use_nip96_uploads_explainer">Tor kényszerítése média feltöltésekor</string>
<string name="tor_use_media_uploads">Média-feltöltések</string>
<string name="tor_use_media_uploads_explainer">Tor kényszerítése média feltöltésekor</string>
<string name="tor_internal">Beépített</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Kikapcsolva</string>
@@ -537,8 +537,8 @@
<string name="tor_use_money_operations_explainer">Forceer Tor op zaps, lightning en cashu transfers</string>
<string name="tor_use_nip05_verification">Nostr Adres verificatie</string>
<string name="tor_use_nip05_verification_explainer">Forceer Tor bij het verifiëren van NIP-05 adressen</string>
<string name="tor_use_nip96_uploads">Mediauploads</string>
<string name="tor_use_nip96_uploads_explainer">Forceer Tor bij het uploaden van content</string>
<string name="tor_use_media_uploads">Mediauploads</string>
<string name="tor_use_media_uploads_explainer">Forceer Tor bij het uploaden van content</string>
<string name="tor_internal">Intern</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Uit</string>
+2 -2
View File
@@ -439,8 +439,8 @@
<string name="tor_use_money_operations_explainer">Forceer Tor op zaps, lightning en cashu transfers</string>
<string name="tor_use_nip05_verification">Nostr Adres verificatie</string>
<string name="tor_use_nip05_verification_explainer">Forceer Tor bij het verifiëren van NIP-05 adressen</string>
<string name="tor_use_nip96_uploads">Mediauploads</string>
<string name="tor_use_nip96_uploads_explainer">Forceer Tor bij het uploaden van content</string>
<string name="tor_use_media_uploads">Mediauploads</string>
<string name="tor_use_media_uploads_explainer">Forceer Tor bij het uploaden van content</string>
<string name="tor_internal">Intern</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Uit</string>
@@ -592,8 +592,8 @@
<string name="tor_use_money_operations_explainer">Wymuś Tor przy zapsach, transferach lightning and cashu</string>
<string name="tor_use_nip05_verification">Weryfikacja adresu Nostr</string>
<string name="tor_use_nip05_verification_explainer">Wymuś Tor podczas weryfikacji adresów NIP-05</string>
<string name="tor_use_nip96_uploads">Wgrywanie mediów</string>
<string name="tor_use_nip96_uploads_explainer">Wymuś Tor podczas wgrywania zawartości</string>
<string name="tor_use_media_uploads">Wgrywanie mediów</string>
<string name="tor_use_media_uploads_explainer">Wymuś Tor podczas wgrywania zawartości</string>
<string name="tor_internal">Wewnętrznie</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Wył.</string>
@@ -591,8 +591,8 @@
<string name="tor_use_money_operations_explainer">Forçar Tor em zaps, transfers relâmpago e cashu</string>
<string name="tor_use_nip05_verification">Verificação de Endereço Nostr</string>
<string name="tor_use_nip05_verification_explainer">Forçar Tor ao verificar endereços NIP-05</string>
<string name="tor_use_nip96_uploads">Envio de Mídia</string>
<string name="tor_use_nip96_uploads_explainer">Forçar Tor ao enviar conteúdo</string>
<string name="tor_use_media_uploads">Envio de Mídia</string>
<string name="tor_use_media_uploads_explainer">Forçar Tor ao enviar conteúdo</string>
<string name="tor_internal">Interno</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Desativado</string>
@@ -606,8 +606,8 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="tor_use_money_operations_explainer">Prisili tor pri Zapih, Lightning in Cashu plačilnih prenosih</string>
<string name="tor_use_nip05_verification">Preveritev Nostr naslova</string>
<string name="tor_use_nip05_verification_explainer">Prisili tor ob preverjanju NIP-05 naslovov</string>
<string name="tor_use_nip96_uploads">Nalaganje medijske vsebine</string>
<string name="tor_use_nip96_uploads_explainer">Prisili tor pri nalaganju vsebine</string>
<string name="tor_use_media_uploads">Nalaganje medijske vsebine</string>
<string name="tor_use_media_uploads_explainer">Prisili tor pri nalaganju vsebine</string>
<string name="tor_internal">Interno</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Izključeno</string>
@@ -590,8 +590,8 @@
<string name="tor_use_money_operations_explainer">Tvinga Tor på zaps, lightning och cashu överföringar</string>
<string name="tor_use_nip05_verification">Nostr Adress Verifiering</string>
<string name="tor_use_nip05_verification_explainer">Tvinga Tor vid verifiering av NIP-05 adresser</string>
<string name="tor_use_nip96_uploads">Mediauppladdningar</string>
<string name="tor_use_nip96_uploads_explainer">Tvinga Tor vid uppladdning av innehåll</string>
<string name="tor_use_media_uploads">Mediauppladdningar</string>
<string name="tor_use_media_uploads_explainer">Tvinga Tor vid uppladdning av innehåll</string>
<string name="tor_internal">Intern</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">Av</string>
@@ -595,8 +595,8 @@
<string name="tor_use_money_operations_explainer">强制为 Zap、闪电网络和 Cashu 转账时使用 Tor</string>
<string name="tor_use_nip05_verification">Nostr 地址验证</string>
<string name="tor_use_nip05_verification_explainer">强制为 NIP-05 地址验证使用 Tor</string>
<string name="tor_use_nip96_uploads">媒体上传</string>
<string name="tor_use_nip96_uploads_explainer">强制为上传媒体内容时使用 Tor</string>
<string name="tor_use_media_uploads">媒体上传</string>
<string name="tor_use_media_uploads_explainer">强制为上传媒体内容时使用 Tor</string>
<string name="tor_internal">内置</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">关闭</string>
+3 -2
View File
@@ -535,6 +535,7 @@
<string name="no_nip96_server_message">You have no NIP-96 servers set. You can use Amethyst\'s list, or add one below ↓</string>
<string name="no_blossom_server_message">You have no Blossom servers set. You can use Amethyst\'s list, or add one below ↓</string>
<string name="recommended_media_servers">Recommended Media Servers</string>
<string name="built_in_media_servers_title">Built-in Media Servers</string>
<string name="built_in_servers_description">Amethyst\'s default list. You can add them individually or add the list.</string>
<string name="use_default_servers">Use Default List</string>
@@ -680,8 +681,8 @@
<string name="tor_use_nip05_verification">Nostr Address Verification</string>
<string name="tor_use_nip05_verification_explainer">Force Tor when verifying NIP-05 addresses</string>
<string name="tor_use_nip96_uploads">Media Uploads</string>
<string name="tor_use_nip96_uploads_explainer">Force Tor when uploading content</string>
<string name="tor_use_media_uploads">Media Uploads</string>
<string name="tor_use_media_uploads_explainer">Force Tor when uploading content</string>
<string name="tor_internal">Internal</string>
<string name="tor_external">Orbot</string>