style: spotlessApply formatting + fix icon imports for upstream MaterialSymbols
This commit is contained in:
@@ -418,7 +418,7 @@ class AccountSettings(
|
|||||||
saveAccountSettings()
|
saveAccountSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateDisableClientTag(disable: Boolean): Boolean =
|
fun updateDisableClientTag(disable: Boolean): Boolean =
|
||||||
if (syncedSettings.security.updateDisableClientTag(disable)) {
|
if (syncedSettings.security.updateDisableClientTag(disable)) {
|
||||||
saveAccountSettings()
|
saveAccountSettings()
|
||||||
|
|||||||
+17
-13
@@ -116,12 +116,14 @@ class NamecoinSettingsTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `returns parsed list for valid custom servers`() {
|
fun `returns parsed list for valid custom servers`() {
|
||||||
val settings = NamecoinSettings(
|
val settings =
|
||||||
customServers = listOf(
|
NamecoinSettings(
|
||||||
"server1.com:50006",
|
customServers =
|
||||||
"server2.onion:50001:tcp",
|
listOf(
|
||||||
),
|
"server1.com:50006",
|
||||||
)
|
"server2.onion:50001:tcp",
|
||||||
|
),
|
||||||
|
)
|
||||||
val servers = settings.toElectrumxServers()
|
val servers = settings.toElectrumxServers()
|
||||||
assertNotNull(servers)
|
assertNotNull(servers)
|
||||||
assertEquals(2, servers!!.size)
|
assertEquals(2, servers!!.size)
|
||||||
@@ -134,13 +136,15 @@ class NamecoinSettingsTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `skips invalid entries in custom server list`() {
|
fun `skips invalid entries in custom server list`() {
|
||||||
val settings = NamecoinSettings(
|
val settings =
|
||||||
customServers = listOf(
|
NamecoinSettings(
|
||||||
"valid.com:50006",
|
customServers =
|
||||||
"invalid", // no port
|
listOf(
|
||||||
"also-invalid:abc", // non-numeric port
|
"valid.com:50006",
|
||||||
),
|
"invalid", // no port
|
||||||
)
|
"also-invalid:abc", // non-numeric port
|
||||||
|
),
|
||||||
|
)
|
||||||
val servers = settings.toElectrumxServers()
|
val servers = settings.toElectrumxServers()
|
||||||
assertNotNull(servers)
|
assertNotNull(servers)
|
||||||
assertEquals(1, servers!!.size)
|
assertEquals(1, servers!!.size)
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ import com.vitorpamplona.amethyst.desktop.platform.applyNativeWindowChrome
|
|||||||
import com.vitorpamplona.amethyst.desktop.service.highlights.DesktopHighlightStore
|
import com.vitorpamplona.amethyst.desktop.service.highlights.DesktopHighlightStore
|
||||||
import com.vitorpamplona.amethyst.desktop.service.images.DesktopImageLoaderSetup
|
import com.vitorpamplona.amethyst.desktop.service.images.DesktopImageLoaderSetup
|
||||||
import com.vitorpamplona.amethyst.desktop.service.media.VlcjPlayerPool
|
import com.vitorpamplona.amethyst.desktop.service.media.VlcjPlayerPool
|
||||||
|
import com.vitorpamplona.amethyst.desktop.service.namecoin.DesktopNamecoinNameService
|
||||||
|
import com.vitorpamplona.amethyst.desktop.service.namecoin.DesktopNamecoinPreferences
|
||||||
|
import com.vitorpamplona.amethyst.desktop.service.namecoin.LocalNamecoinPreferences
|
||||||
|
import com.vitorpamplona.amethyst.desktop.service.namecoin.LocalNamecoinService
|
||||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.ComposeNoteDialog
|
import com.vitorpamplona.amethyst.desktop.ui.ComposeNoteDialog
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.ConnectingRelaysScreen
|
import com.vitorpamplona.amethyst.desktop.ui.ConnectingRelaysScreen
|
||||||
@@ -110,16 +114,10 @@ import com.vitorpamplona.amethyst.desktop.ui.deck.param
|
|||||||
import com.vitorpamplona.amethyst.desktop.ui.media.LocalAwtWindow
|
import com.vitorpamplona.amethyst.desktop.ui.media.LocalAwtWindow
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.media.LocalIsImmersiveFullscreen
|
import com.vitorpamplona.amethyst.desktop.ui.media.LocalIsImmersiveFullscreen
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.media.LocalWindowState
|
import com.vitorpamplona.amethyst.desktop.ui.media.LocalWindowState
|
||||||
import com.vitorpamplona.amethyst.desktop.service.namecoin.DesktopNamecoinNameService
|
|
||||||
import com.vitorpamplona.amethyst.desktop.service.namecoin.DesktopNamecoinPreferences
|
|
||||||
import com.vitorpamplona.amethyst.desktop.service.namecoin.LocalNamecoinPreferences
|
|
||||||
import com.vitorpamplona.amethyst.desktop.service.namecoin.LocalNamecoinService
|
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.ImportFollowListDialog
|
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.profile.ProfileInfoCard
|
import com.vitorpamplona.amethyst.desktop.ui.profile.ProfileInfoCard
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.relay.LocalRelayCategories
|
import com.vitorpamplona.amethyst.desktop.ui.relay.LocalRelayCategories
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.relay.RelayStatusCard
|
import com.vitorpamplona.amethyst.desktop.ui.relay.RelayStatusCard
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.settings.MediaServerSettings
|
import com.vitorpamplona.amethyst.desktop.ui.settings.MediaServerSettings
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.settings.NamecoinSettingsSection
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
@@ -1670,7 +1668,6 @@ fun RelaySettingsScreen(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
) {
|
) {
|
||||||
items(relayStatuses.values.toList(), key = { it.url.url }) { status ->
|
items(relayStatuses.values.toList(), key = { it.url.url }) { status ->
|
||||||
RelayStatusCard(
|
RelayStatusCard(
|
||||||
|
|||||||
+19
-18
@@ -24,7 +24,6 @@ import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.Nam
|
|||||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinSettings
|
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinSettings
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_SERVERS
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_SERVERS
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumXClient
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumXClient
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer
|
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinLookupCache
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinLookupCache
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNostrResult
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNostrResult
|
||||||
@@ -49,17 +48,19 @@ class DesktopNamecoinNameService(
|
|||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
private val electrumxClient = ElectrumXClient(
|
private val electrumxClient =
|
||||||
socketFactory = { SocketFactory.getDefault() },
|
ElectrumXClient(
|
||||||
)
|
socketFactory = { SocketFactory.getDefault() },
|
||||||
|
)
|
||||||
|
|
||||||
private val resolver = NamecoinNameResolver(
|
private val resolver =
|
||||||
electrumxClient = electrumxClient,
|
NamecoinNameResolver(
|
||||||
serverListProvider = {
|
electrumxClient = electrumxClient,
|
||||||
val settings = preferencesProvider()
|
serverListProvider = {
|
||||||
settings.toElectrumxServers() ?: DEFAULT_ELECTRUMX_SERVERS
|
val settings = preferencesProvider()
|
||||||
},
|
settings.toElectrumxServers() ?: DEFAULT_ELECTRUMX_SERVERS
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
private val cache = NamecoinLookupCache()
|
private val cache = NamecoinLookupCache()
|
||||||
|
|
||||||
@@ -92,8 +93,7 @@ class DesktopNamecoinNameService(
|
|||||||
/**
|
/**
|
||||||
* Resolve with detailed outcome for error reporting.
|
* Resolve with detailed outcome for error reporting.
|
||||||
*/
|
*/
|
||||||
suspend fun resolveDetailed(identifier: String): NamecoinResolveOutcome =
|
suspend fun resolveDetailed(identifier: String): NamecoinResolveOutcome = resolver.resolveDetailed(identifier)
|
||||||
resolver.resolveDetailed(identifier)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that a Namecoin name maps to the expected pubkey.
|
* Verify that a Namecoin name maps to the expected pubkey.
|
||||||
@@ -120,11 +120,12 @@ class DesktopNamecoinNameService(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val result = resolve(identifier)
|
val result = resolve(identifier)
|
||||||
state.value = if (result != null) {
|
state.value =
|
||||||
NamecoinResolveState.Resolved(result)
|
if (result != null) {
|
||||||
} else {
|
NamecoinResolveState.Resolved(result)
|
||||||
NamecoinResolveState.NotFound
|
} else {
|
||||||
}
|
NamecoinResolveState.NotFound
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
state.value = NamecoinResolveState.Error(e.message ?: "Unknown error")
|
state.value = NamecoinResolveState.Error(e.message ?: "Unknown error")
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-15
@@ -20,10 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.desktop.service.namecoin
|
package com.vitorpamplona.amethyst.desktop.service.namecoin
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinSettings
|
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinSettings
|
||||||
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.util.prefs.Preferences
|
import java.util.prefs.Preferences
|
||||||
@@ -39,9 +39,10 @@ import java.util.prefs.Preferences
|
|||||||
* `serverListProvider` lambda).
|
* `serverListProvider` lambda).
|
||||||
*/
|
*/
|
||||||
class DesktopNamecoinPreferences(
|
class DesktopNamecoinPreferences(
|
||||||
private val prefs: Preferences = Preferences.userNodeForPackage(
|
private val prefs: Preferences =
|
||||||
DesktopNamecoinPreferences::class.java,
|
Preferences.userNodeForPackage(
|
||||||
),
|
DesktopNamecoinPreferences::class.java,
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
private val mapper = jacksonObjectMapper()
|
private val mapper = jacksonObjectMapper()
|
||||||
|
|
||||||
@@ -101,23 +102,23 @@ class DesktopNamecoinPreferences(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromDisk(): NamecoinSettings {
|
private fun loadFromDisk(): NamecoinSettings =
|
||||||
return try {
|
try {
|
||||||
val enabled = prefs.getBoolean(KEY_ENABLED, true)
|
val enabled = prefs.getBoolean(KEY_ENABLED, true)
|
||||||
val serversJson = prefs.get(KEY_CUSTOM_SERVERS, null)
|
val serversJson = prefs.get(KEY_CUSTOM_SERVERS, null)
|
||||||
val servers = if (serversJson != null) {
|
val servers =
|
||||||
try {
|
if (serversJson != null) {
|
||||||
mapper.readValue<List<String>>(serversJson)
|
try {
|
||||||
} catch (_: Exception) {
|
mapper.readValue<List<String>>(serversJson)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
NamecoinSettings(enabled = enabled, customServers = servers)
|
NamecoinSettings(enabled = enabled, customServers = servers)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
System.err.println("NamecoinPrefs: Error reading preferences: ${e.message}")
|
System.err.println("NamecoinPrefs: Error reading preferences: ${e.message}")
|
||||||
NamecoinSettings.DEFAULT
|
NamecoinSettings.DEFAULT
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+163
-136
@@ -31,13 +31,9 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
|
||||||
import androidx.compose.material.icons.filled.Person
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
@@ -65,6 +61,8 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||||
|
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||||
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||||
@@ -92,12 +90,25 @@ import java.util.concurrent.TimeUnit
|
|||||||
*/
|
*/
|
||||||
private sealed class ImportState {
|
private sealed class ImportState {
|
||||||
data object Idle : ImportState()
|
data object Idle : ImportState()
|
||||||
|
|
||||||
data object ResolvingIdentifier : ImportState()
|
data object ResolvingIdentifier : ImportState()
|
||||||
data class IdentifierResolved(val pubkey: String) : ImportState()
|
|
||||||
|
data class IdentifierResolved(
|
||||||
|
val pubkey: String,
|
||||||
|
) : ImportState()
|
||||||
|
|
||||||
data object FetchingFollowList : ImportState()
|
data object FetchingFollowList : ImportState()
|
||||||
data class FollowListLoaded(val sourcePubkey: String) : ImportState()
|
|
||||||
data class Error(val message: String) : ImportState()
|
data class FollowListLoaded(
|
||||||
|
val sourcePubkey: String,
|
||||||
|
) : ImportState()
|
||||||
|
|
||||||
|
data class Error(
|
||||||
|
val message: String,
|
||||||
|
) : ImportState()
|
||||||
|
|
||||||
data object Publishing : ImportState()
|
data object Publishing : ImportState()
|
||||||
|
|
||||||
data object Done : ImportState()
|
data object Done : ImportState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +134,12 @@ private suspend fun resolveNip05Http(identifier: String): String? {
|
|||||||
val url = "https://$domain/.well-known/nostr.json?name=$encodedName"
|
val url = "https://$domain/.well-known/nostr.json?name=$encodedName"
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val client = OkHttpClient.Builder()
|
val client =
|
||||||
.connectTimeout(10, TimeUnit.SECONDS)
|
OkHttpClient
|
||||||
.readTimeout(10, TimeUnit.SECONDS)
|
.Builder()
|
||||||
.build()
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
val request = Request.Builder().url(url).build()
|
val request = Request.Builder().url(url).build()
|
||||||
val response = client.newCall(request).execute()
|
val response = client.newCall(request).execute()
|
||||||
response.use { resp ->
|
response.use { resp ->
|
||||||
@@ -201,119 +214,127 @@ fun ImportFollowListDialog(
|
|||||||
|
|
||||||
relayManager.subscribe(
|
relayManager.subscribe(
|
||||||
subId = subId,
|
subId = subId,
|
||||||
filters = listOf(
|
filters =
|
||||||
Filter(
|
listOf(
|
||||||
kinds = listOf(ContactListEvent.KIND),
|
Filter(
|
||||||
authors = listOf(pubkey),
|
kinds = listOf(ContactListEvent.KIND),
|
||||||
limit = 1,
|
authors = listOf(pubkey),
|
||||||
|
limit = 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
relays = relays,
|
relays = relays,
|
||||||
listener = object : SubscriptionListener {
|
listener =
|
||||||
override fun onEvent(
|
object : SubscriptionListener {
|
||||||
event: Event,
|
override fun onEvent(
|
||||||
isLive: Boolean,
|
event: Event,
|
||||||
relay: NormalizedRelayUrl,
|
isLive: Boolean,
|
||||||
forFilters: List<Filter>?,
|
relay: NormalizedRelayUrl,
|
||||||
) {
|
forFilters: List<Filter>?,
|
||||||
if (event.kind == ContactListEvent.KIND && !receivedContactList) {
|
) {
|
||||||
receivedContactList = true
|
if (event.kind == ContactListEvent.KIND && !receivedContactList) {
|
||||||
val contactList = ContactListEvent(
|
receivedContactList = true
|
||||||
event.id,
|
val contactList =
|
||||||
event.pubKey,
|
ContactListEvent(
|
||||||
event.createdAt,
|
event.id,
|
||||||
event.tags,
|
event.pubKey,
|
||||||
event.content,
|
event.createdAt,
|
||||||
event.sig,
|
event.tags,
|
||||||
)
|
event.content,
|
||||||
val follows = contactList.unverifiedFollowKeySet()
|
event.sig,
|
||||||
|
)
|
||||||
|
val follows = contactList.unverifiedFollowKeySet()
|
||||||
|
|
||||||
// Dispatch state updates to main thread (relay callbacks
|
// Dispatch state updates to main thread (relay callbacks
|
||||||
// run on arbitrary threads; Compose state is not thread-safe)
|
// run on arbitrary threads; Compose state is not thread-safe)
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
followEntries.clear()
|
followEntries.clear()
|
||||||
followEntries.addAll(
|
followEntries.addAll(
|
||||||
follows.map { FollowEntry(pubkey = it, selected = true) },
|
follows.map { FollowEntry(pubkey = it, selected = true) },
|
||||||
)
|
)
|
||||||
importState = ImportState.FollowListLoaded(sourcePubkey = pubkey)
|
importState = ImportState.FollowListLoaded(sourcePubkey = pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up the contact list subscription
|
// Clean up the contact list subscription
|
||||||
try {
|
try {
|
||||||
relayManager.unsubscribe(subId)
|
relayManager.unsubscribe(subId)
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {
|
||||||
activeSubscriptions.remove(subId)
|
}
|
||||||
|
activeSubscriptions.remove(subId)
|
||||||
|
|
||||||
// Start fetching metadata for display names
|
// Start fetching metadata for display names
|
||||||
if (follows.isNotEmpty()) {
|
if (follows.isNotEmpty()) {
|
||||||
val metaSubId = "import-meta-${System.currentTimeMillis()}"
|
val metaSubId = "import-meta-${System.currentTimeMillis()}"
|
||||||
activeSubscriptions.add(metaSubId)
|
activeSubscriptions.add(metaSubId)
|
||||||
relayManager.subscribe(
|
relayManager.subscribe(
|
||||||
subId = metaSubId,
|
subId = metaSubId,
|
||||||
filters = listOf(
|
filters =
|
||||||
Filter(
|
listOf(
|
||||||
kinds = listOf(MetadataEvent.KIND),
|
Filter(
|
||||||
authors = follows,
|
kinds = listOf(MetadataEvent.KIND),
|
||||||
limit = follows.size,
|
authors = follows,
|
||||||
),
|
limit = follows.size,
|
||||||
),
|
),
|
||||||
listener = object : SubscriptionListener {
|
),
|
||||||
override fun onEvent(
|
listener =
|
||||||
event: Event,
|
object : SubscriptionListener {
|
||||||
isLive: Boolean,
|
override fun onEvent(
|
||||||
relay: NormalizedRelayUrl,
|
event: Event,
|
||||||
forFilters: List<Filter>?,
|
isLive: Boolean,
|
||||||
) {
|
relay: NormalizedRelayUrl,
|
||||||
if (event.kind == MetadataEvent.KIND) {
|
forFilters: List<Filter>?,
|
||||||
val bestName = try {
|
) {
|
||||||
val metaJson = jacksonObjectMapper().readTree(event.content)
|
if (event.kind == MetadataEvent.KIND) {
|
||||||
metaJson.get("display_name")?.asText()?.takeIf { it.isNotBlank() }
|
val bestName =
|
||||||
?: metaJson.get("name")?.asText()?.takeIf { it.isNotBlank() }
|
try {
|
||||||
} catch (_: Exception) {
|
val metaJson = jacksonObjectMapper().readTree(event.content)
|
||||||
null
|
metaJson.get("display_name")?.asText()?.takeIf { it.isNotBlank() }
|
||||||
}
|
?: metaJson.get("name")?.asText()?.takeIf { it.isNotBlank() }
|
||||||
if (bestName != null) {
|
} catch (_: Exception) {
|
||||||
// Dispatch to main thread
|
null
|
||||||
scope.launch(Dispatchers.Main) {
|
}
|
||||||
val idx = followEntries.indexOfFirst { it.pubkey == event.pubKey }
|
if (bestName != null) {
|
||||||
if (idx >= 0) {
|
// Dispatch to main thread
|
||||||
followEntries[idx] = followEntries[idx].copy(displayName = bestName)
|
scope.launch(Dispatchers.Main) {
|
||||||
|
val idx = followEntries.indexOfFirst { it.pubkey == event.pubKey }
|
||||||
|
if (idx >= 0) {
|
||||||
|
followEntries[idx] = followEntries[idx].copy(displayName = bestName)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onEose(
|
||||||
|
relay: NormalizedRelayUrl,
|
||||||
|
forFilters: List<Filter>?,
|
||||||
|
) {
|
||||||
|
// Metadata is best-effort; don't close early
|
||||||
|
// as multiple relays may have different metadata
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clean up metadata subscription after 20s
|
||||||
|
scope.launch {
|
||||||
|
delay(20_000)
|
||||||
|
if (metaSubId in activeSubscriptions) {
|
||||||
|
try {
|
||||||
|
relayManager.unsubscribe(metaSubId)
|
||||||
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
|
activeSubscriptions.remove(metaSubId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEose(
|
|
||||||
relay: NormalizedRelayUrl,
|
|
||||||
forFilters: List<Filter>?,
|
|
||||||
) {
|
|
||||||
// Metadata is best-effort; don't close early
|
|
||||||
// as multiple relays may have different metadata
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Clean up metadata subscription after 20s
|
|
||||||
scope.launch {
|
|
||||||
delay(20_000)
|
|
||||||
if (metaSubId in activeSubscriptions) {
|
|
||||||
try {
|
|
||||||
relayManager.unsubscribe(metaSubId)
|
|
||||||
} catch (_: Exception) {}
|
|
||||||
activeSubscriptions.remove(metaSubId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEose(
|
override fun onEose(
|
||||||
relay: NormalizedRelayUrl,
|
relay: NormalizedRelayUrl,
|
||||||
forFilters: List<Filter>?,
|
forFilters: List<Filter>?,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Timeout: if no contact list received after 15s, show error
|
// Timeout: if no contact list received after 15s, show error
|
||||||
@@ -322,7 +343,8 @@ fun ImportFollowListDialog(
|
|||||||
if (importState is ImportState.FetchingFollowList) {
|
if (importState is ImportState.FetchingFollowList) {
|
||||||
try {
|
try {
|
||||||
relayManager.unsubscribe(subId)
|
relayManager.unsubscribe(subId)
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
activeSubscriptions.remove(subId)
|
activeSubscriptions.remove(subId)
|
||||||
if (followEntries.isEmpty()) {
|
if (followEntries.isEmpty()) {
|
||||||
importState = ImportState.Error("No follow list found for this user. They may not have published a contact list.")
|
importState = ImportState.Error("No follow list found for this user. They may not have published a contact list.")
|
||||||
@@ -418,11 +440,12 @@ fun ImportFollowListDialog(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val contactTags = selected.map { ContactTag(it.pubkey) }
|
val contactTags = selected.map { ContactTag(it.pubkey) }
|
||||||
val newContactList = ContactListEvent.createFromScratch(
|
val newContactList =
|
||||||
followUsers = contactTags,
|
ContactListEvent.createFromScratch(
|
||||||
relayUse = null,
|
followUsers = contactTags,
|
||||||
signer = account.signer,
|
relayUse = null,
|
||||||
)
|
signer = account.signer,
|
||||||
|
)
|
||||||
relayManager.broadcastToAll(newContactList)
|
relayManager.broadcastToAll(newContactList)
|
||||||
importState = ImportState.Done
|
importState = ImportState.Done
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -457,9 +480,10 @@ fun ImportFollowListDialog(
|
|||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
// Input field + Resolve button (shown in Idle and Error states)
|
// Input field + Resolve button (shown in Idle and Error states)
|
||||||
val showInput = importState is ImportState.Idle ||
|
val showInput =
|
||||||
importState is ImportState.Error ||
|
importState is ImportState.Idle ||
|
||||||
importState is ImportState.ResolvingIdentifier
|
importState is ImportState.Error ||
|
||||||
|
importState is ImportState.ResolvingIdentifier
|
||||||
if (showInput) {
|
if (showInput) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -478,17 +502,19 @@ fun ImportFollowListDialog(
|
|||||||
placeholder = { Text("npub1..., alice@example.com, d/alice") },
|
placeholder = { Text("npub1..., alice@example.com, d/alice") },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
isError = importState is ImportState.Error,
|
isError = importState is ImportState.Error,
|
||||||
supportingText = (importState as? ImportState.Error)?.let { err ->
|
supportingText =
|
||||||
{ Text(err.message, color = MaterialTheme.colorScheme.error) }
|
(importState as? ImportState.Error)?.let { err ->
|
||||||
},
|
{ Text(err.message, color = MaterialTheme.colorScheme.error) }
|
||||||
modifier = Modifier.weight(1f).onPreviewKeyEvent { event ->
|
},
|
||||||
if (event.type == KeyEventType.KeyDown && event.key == Key.Enter) {
|
modifier =
|
||||||
resolveIdentifier()
|
Modifier.weight(1f).onPreviewKeyEvent { event ->
|
||||||
true
|
if (event.type == KeyEventType.KeyDown && event.key == Key.Enter) {
|
||||||
} else {
|
resolveIdentifier()
|
||||||
false
|
true
|
||||||
}
|
} else {
|
||||||
},
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
Button(
|
Button(
|
||||||
onClick = { resolveIdentifier() },
|
onClick = { resolveIdentifier() },
|
||||||
@@ -566,7 +592,7 @@ fun ImportFollowListDialog(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Person,
|
MaterialSymbols.Person,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -583,11 +609,12 @@ fun ImportFollowListDialog(
|
|||||||
"${entry.pubkey.take(12)}…${entry.pubkey.takeLast(8)}",
|
"${entry.pubkey.take(12)}…${entry.pubkey.takeLast(8)}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
color = if (entry.displayName != null) {
|
color =
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
if (entry.displayName != null) {
|
||||||
} else {
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
MaterialTheme.colorScheme.onSurface
|
} else {
|
||||||
},
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -618,7 +645,7 @@ fun ImportFollowListDialog(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CheckCircle,
|
MaterialSymbols.CheckCircle,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
|
|||||||
+39
-23
@@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.feeds.custom.canBecomeFeed
|
|||||||
import com.vitorpamplona.amethyst.commons.feeds.custom.toFeedDefinition
|
import com.vitorpamplona.amethyst.commons.feeds.custom.toFeedDefinition
|
||||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinResolveState
|
||||||
import com.vitorpamplona.amethyst.commons.search.AdvancedSearchBarState
|
import com.vitorpamplona.amethyst.commons.search.AdvancedSearchBarState
|
||||||
import com.vitorpamplona.amethyst.commons.search.QuerySerializer
|
import com.vitorpamplona.amethyst.commons.search.QuerySerializer
|
||||||
import com.vitorpamplona.amethyst.commons.search.SavedSearch
|
import com.vitorpamplona.amethyst.commons.search.SavedSearch
|
||||||
@@ -89,6 +90,8 @@ import com.vitorpamplona.amethyst.desktop.SearchHistoryStore
|
|||||||
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||||
|
import com.vitorpamplona.amethyst.desktop.service.namecoin.LocalNamecoinPreferences
|
||||||
|
import com.vitorpamplona.amethyst.desktop.service.namecoin.LocalNamecoinService
|
||||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||||
import com.vitorpamplona.amethyst.desktop.subscriptions.SearchFilterFactory
|
import com.vitorpamplona.amethyst.desktop.subscriptions.SearchFilterFactory
|
||||||
import com.vitorpamplona.amethyst.desktop.subscriptions.SubscriptionConfig
|
import com.vitorpamplona.amethyst.desktop.subscriptions.SubscriptionConfig
|
||||||
@@ -102,15 +105,10 @@ import com.vitorpamplona.amethyst.desktop.ui.relay.SearchRelayEditor
|
|||||||
import com.vitorpamplona.amethyst.desktop.ui.search.AdvancedSearchPanel
|
import com.vitorpamplona.amethyst.desktop.ui.search.AdvancedSearchPanel
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.search.SearchResultsList
|
import com.vitorpamplona.amethyst.desktop.ui.search.SearchResultsList
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.search.SearchSyncBanner
|
import com.vitorpamplona.amethyst.desktop.ui.search.SearchSyncBanner
|
||||||
import com.vitorpamplona.amethyst.desktop.service.namecoin.DesktopNamecoinNameService
|
|
||||||
import com.vitorpamplona.amethyst.desktop.service.namecoin.LocalNamecoinPreferences
|
|
||||||
import com.vitorpamplona.amethyst.desktop.service.namecoin.LocalNamecoinService
|
|
||||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinResolveState
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinResolveOutcome
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinResolveOutcome
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -166,10 +164,16 @@ fun SearchScreen(
|
|||||||
// Namecoin resolution
|
// Namecoin resolution
|
||||||
val namecoinService = LocalNamecoinService.current
|
val namecoinService = LocalNamecoinService.current
|
||||||
val namecoinPrefs = LocalNamecoinPreferences.current
|
val namecoinPrefs = LocalNamecoinPreferences.current
|
||||||
val namecoinEnabled = namecoinPrefs?.settings?.collectAsState()?.value?.enabled ?: false
|
val namecoinEnabled =
|
||||||
val isNamecoinQuery = remember(displayText) {
|
namecoinPrefs
|
||||||
displayText.isNotBlank() && NamecoinNameResolver.isNamecoinIdentifier(displayText.trim())
|
?.settings
|
||||||
}
|
?.collectAsState()
|
||||||
|
?.value
|
||||||
|
?.enabled ?: false
|
||||||
|
val isNamecoinQuery =
|
||||||
|
remember(displayText) {
|
||||||
|
displayText.isNotBlank() && NamecoinNameResolver.isNamecoinIdentifier(displayText.trim())
|
||||||
|
}
|
||||||
|
|
||||||
var namecoinState by remember { mutableStateOf<NamecoinResolveState?>(null) }
|
var namecoinState by remember { mutableStateOf<NamecoinResolveState?>(null) }
|
||||||
|
|
||||||
@@ -183,20 +187,32 @@ fun SearchScreen(
|
|||||||
}
|
}
|
||||||
namecoinState = NamecoinResolveState.Loading
|
namecoinState = NamecoinResolveState.Loading
|
||||||
val outcome = namecoinService.resolveDetailed(displayText.trim())
|
val outcome = namecoinService.resolveDetailed(displayText.trim())
|
||||||
namecoinState = when (outcome) {
|
namecoinState =
|
||||||
is NamecoinResolveOutcome.Success ->
|
when (outcome) {
|
||||||
NamecoinResolveState.Resolved(outcome.result)
|
is NamecoinResolveOutcome.Success -> {
|
||||||
is NamecoinResolveOutcome.NameNotFound ->
|
NamecoinResolveState.Resolved(outcome.result)
|
||||||
NamecoinResolveState.NotFound
|
}
|
||||||
is NamecoinResolveOutcome.NoNostrField ->
|
|
||||||
NamecoinResolveState.Error("Name exists but has no Nostr pubkey")
|
is NamecoinResolveOutcome.NameNotFound -> {
|
||||||
is NamecoinResolveOutcome.ServersUnreachable ->
|
NamecoinResolveState.NotFound
|
||||||
NamecoinResolveState.Error("ElectrumX servers unreachable — check your connection or try again")
|
}
|
||||||
is NamecoinResolveOutcome.InvalidIdentifier ->
|
|
||||||
NamecoinResolveState.Error("Invalid Namecoin identifier")
|
is NamecoinResolveOutcome.NoNostrField -> {
|
||||||
is NamecoinResolveOutcome.Timeout ->
|
NamecoinResolveState.Error("Name exists but has no Nostr pubkey")
|
||||||
NamecoinResolveState.Error("Resolution timed out — servers may be slow, try again")
|
}
|
||||||
}
|
|
||||||
|
is NamecoinResolveOutcome.ServersUnreachable -> {
|
||||||
|
NamecoinResolveState.Error("ElectrumX servers unreachable — check your connection or try again")
|
||||||
|
}
|
||||||
|
|
||||||
|
is NamecoinResolveOutcome.InvalidIdentifier -> {
|
||||||
|
NamecoinResolveState.Error("Invalid Namecoin identifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
is NamecoinResolveOutcome.Timeout -> {
|
||||||
|
NamecoinResolveState.Error("Resolution timed out — servers may be slow, try again")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip people search when query specifies kinds that don't include profile (kind 0)
|
// Skip people search when query specifies kinds that don't include profile (kind 0)
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import com.vitorpamplona.amethyst.desktop.DesktopScreen
|
import com.vitorpamplona.amethyst.desktop.DesktopScreen
|
||||||
import com.vitorpamplona.amethyst.desktop.RelaySettingsScreen
|
import com.vitorpamplona.amethyst.desktop.RelaySettingsScreen
|
||||||
import com.vitorpamplona.amethyst.desktop.account.AccountManager
|
import com.vitorpamplona.amethyst.desktop.account.AccountManager
|
||||||
import com.vitorpamplona.amethyst.desktop.service.namecoin.LocalNamecoinPreferences
|
|
||||||
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||||
import com.vitorpamplona.amethyst.desktop.chess.ChessScreen
|
import com.vitorpamplona.amethyst.desktop.chess.ChessScreen
|
||||||
@@ -47,6 +46,7 @@ import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
|||||||
import com.vitorpamplona.amethyst.desktop.network.Nip11Fetcher
|
import com.vitorpamplona.amethyst.desktop.network.Nip11Fetcher
|
||||||
import com.vitorpamplona.amethyst.desktop.service.drafts.DesktopDraftStore
|
import com.vitorpamplona.amethyst.desktop.service.drafts.DesktopDraftStore
|
||||||
import com.vitorpamplona.amethyst.desktop.service.highlights.DesktopHighlightStore
|
import com.vitorpamplona.amethyst.desktop.service.highlights.DesktopHighlightStore
|
||||||
|
import com.vitorpamplona.amethyst.desktop.service.namecoin.LocalNamecoinPreferences
|
||||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||||
import com.vitorpamplona.amethyst.desktop.subscriptions.FeedMode
|
import com.vitorpamplona.amethyst.desktop.subscriptions.FeedMode
|
||||||
import com.vitorpamplona.amethyst.desktop.ui.ArticleEditorScreen
|
import com.vitorpamplona.amethyst.desktop.ui.ArticleEditorScreen
|
||||||
|
|||||||
+55
-51
@@ -34,13 +34,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material.icons.filled.Lock
|
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -66,6 +60,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||||
|
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinSettings
|
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinSettings
|
||||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_SERVERS
|
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_SERVERS
|
||||||
|
|
||||||
@@ -143,7 +139,7 @@ fun NamecoinSettingsSection(
|
|||||||
) {
|
) {
|
||||||
TextButton(onClick = onReset) {
|
TextButton(onClick = onReset) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Refresh,
|
MaterialSymbols.Refresh,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
)
|
)
|
||||||
@@ -171,7 +167,7 @@ private fun NamecoinSectionHeader(
|
|||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Lock,
|
MaterialSymbols.Lock,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color(0xFF4A90D9), // Namecoin blue
|
tint = Color(0xFF4A90D9), // Namecoin blue
|
||||||
modifier = Modifier.size(22.dp),
|
modifier = Modifier.size(22.dp),
|
||||||
@@ -219,12 +215,12 @@ private fun NamecoinActiveServersDisplay(settings: NamecoinSettings) {
|
|||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color(0xFF4A90D9),
|
color = Color(0xFF4A90D9),
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.background(
|
Modifier
|
||||||
Color(0xFF4A90D9).copy(alpha = 0.1f),
|
.background(
|
||||||
RoundedCornerShape(4.dp),
|
Color(0xFF4A90D9).copy(alpha = 0.1f),
|
||||||
)
|
RoundedCornerShape(4.dp),
|
||||||
.padding(horizontal = 6.dp, vertical = 2.dp),
|
).padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
@@ -237,8 +233,9 @@ private fun NamecoinActiveServersDisplay(settings: NamecoinSettings) {
|
|||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
servers.forEach { server ->
|
servers.forEach { server ->
|
||||||
NamecoinServerRow(
|
NamecoinServerRow(
|
||||||
displayText = "${server.host}:${server.port}" +
|
displayText =
|
||||||
if (!server.useSsl) " (tcp)" else " (tls)",
|
"${server.host}:${server.port}" +
|
||||||
|
if (!server.useSsl) " (tcp)" else " (tls)",
|
||||||
isActive = true,
|
isActive = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -266,9 +263,10 @@ private fun NamecoinCustomServersList(
|
|||||||
)
|
)
|
||||||
servers.forEach { server ->
|
servers.forEach { server ->
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier
|
||||||
.padding(vertical = 2.dp),
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 2.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
@@ -285,7 +283,7 @@ private fun NamecoinCustomServersList(
|
|||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Close,
|
MaterialSymbols.Close,
|
||||||
contentDescription = "Remove server",
|
contentDescription = "Remove server",
|
||||||
tint = MaterialTheme.colorScheme.error,
|
tint = MaterialTheme.colorScheme.error,
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
@@ -332,37 +330,41 @@ private fun NamecoinAddServerInput(onAdd: (String) -> Unit) {
|
|||||||
placeholder = { Text("host:port or host:port:tcp") },
|
placeholder = { Text("host:port or host:port:tcp") },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
isError = validationError != null,
|
isError = validationError != null,
|
||||||
supportingText = validationError?.let { err ->
|
supportingText =
|
||||||
{ Text(err, color = MaterialTheme.colorScheme.error) }
|
validationError?.let { err ->
|
||||||
},
|
{ Text(err, color = MaterialTheme.colorScheme.error) }
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.onPreviewKeyEvent { event ->
|
|
||||||
if (event.type == KeyEventType.KeyDown && event.key == Key.Enter) {
|
|
||||||
tryAdd()
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.onPreviewKeyEvent { event ->
|
||||||
|
if (event.type == KeyEventType.KeyDown && event.key == Key.Enter) {
|
||||||
|
tryAdd()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
textStyle = MaterialTheme.typography.bodySmall.copy(
|
textStyle =
|
||||||
fontFamily = FontFamily.Monospace,
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
),
|
fontFamily = FontFamily.Monospace,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { tryAdd() },
|
onClick = { tryAdd() },
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.padding(top = 8.dp)
|
Modifier
|
||||||
.size(40.dp)
|
.padding(top = 8.dp)
|
||||||
.background(
|
.size(40.dp)
|
||||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
|
.background(
|
||||||
RoundedCornerShape(8.dp),
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
|
||||||
),
|
RoundedCornerShape(8.dp),
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Add,
|
MaterialSymbols.Add,
|
||||||
contentDescription = "Add server",
|
contentDescription = "Add server",
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
@@ -376,19 +378,21 @@ private fun NamecoinServerRow(
|
|||||||
isActive: Boolean,
|
isActive: Boolean,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier
|
||||||
.padding(vertical = 2.dp),
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 2.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "•",
|
text = "•",
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
color = if (isActive) {
|
color =
|
||||||
Color(0xFF2E8B57)
|
if (isActive) {
|
||||||
} else {
|
Color(0xFF2E8B57)
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
} else {
|
||||||
},
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
},
|
||||||
modifier = Modifier.padding(end = 6.dp),
|
modifier = Modifier.padding(end = 6.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
+78
-70
@@ -57,71 +57,77 @@ class DesktopNamecoinPreferencesTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `setEnabled persists and updates flow`() = runBlocking {
|
fun `setEnabled persists and updates flow`() =
|
||||||
namecoinPrefs.setEnabled(false)
|
runBlocking {
|
||||||
assertFalse(namecoinPrefs.current.enabled)
|
namecoinPrefs.setEnabled(false)
|
||||||
assertFalse(namecoinPrefs.settings.value.enabled)
|
assertFalse(namecoinPrefs.current.enabled)
|
||||||
|
assertFalse(namecoinPrefs.settings.value.enabled)
|
||||||
|
|
||||||
// Verify persistence by creating a new instance with the same prefs node
|
// Verify persistence by creating a new instance with the same prefs node
|
||||||
val reloaded = DesktopNamecoinPreferences(prefs = testPrefs)
|
val reloaded = DesktopNamecoinPreferences(prefs = testPrefs)
|
||||||
assertFalse(reloaded.current.enabled)
|
assertFalse(reloaded.current.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `addServer persists and updates flow`() = runBlocking {
|
fun `addServer persists and updates flow`() =
|
||||||
namecoinPrefs.addServer("example.com:50006")
|
runBlocking {
|
||||||
assertEquals(listOf("example.com:50006"), namecoinPrefs.current.customServers)
|
namecoinPrefs.addServer("example.com:50006")
|
||||||
assertTrue(namecoinPrefs.current.hasCustomServers)
|
assertEquals(listOf("example.com:50006"), namecoinPrefs.current.customServers)
|
||||||
|
assertTrue(namecoinPrefs.current.hasCustomServers)
|
||||||
|
|
||||||
// Verify persistence
|
// Verify persistence
|
||||||
val reloaded = DesktopNamecoinPreferences(prefs = testPrefs)
|
val reloaded = DesktopNamecoinPreferences(prefs = testPrefs)
|
||||||
assertEquals(listOf("example.com:50006"), reloaded.current.customServers)
|
assertEquals(listOf("example.com:50006"), reloaded.current.customServers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `addServer ignores blank strings`() = runBlocking {
|
fun `addServer ignores blank strings`() =
|
||||||
namecoinPrefs.addServer("")
|
runBlocking {
|
||||||
namecoinPrefs.addServer(" ")
|
namecoinPrefs.addServer("")
|
||||||
assertTrue(namecoinPrefs.current.customServers.isEmpty())
|
namecoinPrefs.addServer(" ")
|
||||||
}
|
assertTrue(namecoinPrefs.current.customServers.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `addServer ignores duplicates`() = runBlocking {
|
fun `addServer ignores duplicates`() =
|
||||||
namecoinPrefs.addServer("example.com:50006")
|
runBlocking {
|
||||||
namecoinPrefs.addServer("example.com:50006")
|
namecoinPrefs.addServer("example.com:50006")
|
||||||
assertEquals(1, namecoinPrefs.current.customServers.size)
|
namecoinPrefs.addServer("example.com:50006")
|
||||||
}
|
assertEquals(1, namecoinPrefs.current.customServers.size)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `removeServer persists and updates flow`() = runBlocking {
|
fun `removeServer persists and updates flow`() =
|
||||||
namecoinPrefs.addServer("server1.com:50006")
|
runBlocking {
|
||||||
namecoinPrefs.addServer("server2.com:50001:tcp")
|
namecoinPrefs.addServer("server1.com:50006")
|
||||||
assertEquals(2, namecoinPrefs.current.customServers.size)
|
namecoinPrefs.addServer("server2.com:50001:tcp")
|
||||||
|
assertEquals(2, namecoinPrefs.current.customServers.size)
|
||||||
|
|
||||||
namecoinPrefs.removeServer("server1.com:50006")
|
namecoinPrefs.removeServer("server1.com:50006")
|
||||||
assertEquals(listOf("server2.com:50001:tcp"), namecoinPrefs.current.customServers)
|
assertEquals(listOf("server2.com:50001:tcp"), namecoinPrefs.current.customServers)
|
||||||
|
|
||||||
// Verify persistence
|
// Verify persistence
|
||||||
val reloaded = DesktopNamecoinPreferences(prefs = testPrefs)
|
val reloaded = DesktopNamecoinPreferences(prefs = testPrefs)
|
||||||
assertEquals(listOf("server2.com:50001:tcp"), reloaded.current.customServers)
|
assertEquals(listOf("server2.com:50001:tcp"), reloaded.current.customServers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `reset restores defaults`() = runBlocking {
|
fun `reset restores defaults`() =
|
||||||
namecoinPrefs.setEnabled(false)
|
runBlocking {
|
||||||
namecoinPrefs.addServer("example.com:50006")
|
namecoinPrefs.setEnabled(false)
|
||||||
assertFalse(namecoinPrefs.current.enabled)
|
namecoinPrefs.addServer("example.com:50006")
|
||||||
assertTrue(namecoinPrefs.current.hasCustomServers)
|
assertFalse(namecoinPrefs.current.enabled)
|
||||||
|
assertTrue(namecoinPrefs.current.hasCustomServers)
|
||||||
|
|
||||||
namecoinPrefs.reset()
|
namecoinPrefs.reset()
|
||||||
assertTrue(namecoinPrefs.current.enabled)
|
assertTrue(namecoinPrefs.current.enabled)
|
||||||
assertFalse(namecoinPrefs.current.hasCustomServers)
|
assertFalse(namecoinPrefs.current.hasCustomServers)
|
||||||
|
|
||||||
// Verify persistence
|
// Verify persistence
|
||||||
val reloaded = DesktopNamecoinPreferences(prefs = testPrefs)
|
val reloaded = DesktopNamecoinPreferences(prefs = testPrefs)
|
||||||
assertTrue(reloaded.current.enabled)
|
assertTrue(reloaded.current.enabled)
|
||||||
assertFalse(reloaded.current.hasCustomServers)
|
assertFalse(reloaded.current.hasCustomServers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `customServersOrNull returns null when empty`() {
|
fun `customServersOrNull returns null when empty`() {
|
||||||
@@ -129,29 +135,31 @@ class DesktopNamecoinPreferencesTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `customServersOrNull returns parsed servers when configured`() = runBlocking {
|
fun `customServersOrNull returns parsed servers when configured`() =
|
||||||
namecoinPrefs.addServer("example.com:50006")
|
runBlocking {
|
||||||
val servers = namecoinPrefs.customServersOrNull
|
namecoinPrefs.addServer("example.com:50006")
|
||||||
assertEquals(1, servers?.size)
|
val servers = namecoinPrefs.customServersOrNull
|
||||||
assertEquals("example.com", servers?.first()?.host)
|
assertEquals(1, servers?.size)
|
||||||
assertEquals(50006, servers?.first()?.port)
|
assertEquals("example.com", servers?.first()?.host)
|
||||||
assertTrue(servers?.first()?.useSsl == true)
|
assertEquals(50006, servers?.first()?.port)
|
||||||
}
|
assertTrue(servers?.first()?.useSsl == true)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `round-trip multiple operations`() = runBlocking {
|
fun `round-trip multiple operations`() =
|
||||||
namecoinPrefs.setEnabled(true)
|
runBlocking {
|
||||||
namecoinPrefs.addServer("server1.com:50006")
|
namecoinPrefs.setEnabled(true)
|
||||||
namecoinPrefs.addServer("onion.onion:50001:tcp")
|
namecoinPrefs.addServer("server1.com:50006")
|
||||||
namecoinPrefs.setEnabled(false)
|
namecoinPrefs.addServer("onion.onion:50001:tcp")
|
||||||
namecoinPrefs.removeServer("server1.com:50006")
|
namecoinPrefs.setEnabled(false)
|
||||||
|
namecoinPrefs.removeServer("server1.com:50006")
|
||||||
|
|
||||||
val settings = namecoinPrefs.current
|
val settings = namecoinPrefs.current
|
||||||
assertFalse(settings.enabled)
|
assertFalse(settings.enabled)
|
||||||
assertEquals(listOf("onion.onion:50001:tcp"), settings.customServers)
|
assertEquals(listOf("onion.onion:50001:tcp"), settings.customServers)
|
||||||
|
|
||||||
// Verify full persistence round-trip
|
// Verify full persistence round-trip
|
||||||
val reloaded = DesktopNamecoinPreferences(prefs = testPrefs)
|
val reloaded = DesktopNamecoinPreferences(prefs = testPrefs)
|
||||||
assertEquals(settings, reloaded.current)
|
assertEquals(settings, reloaded.current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user