mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
refactor(desktop): read blossom servers from the account, not the cache
Replace the ICacheProvider.blossomServers(pubKey) cache helper with reads straight from the account's own state holder — iAccount.blossomServerList.flow (the shared BlossomServerListState) — threaded to each upload site. This is the reactive, per-account source of truth and drops the cache+pubkey indirection entirely. - Hoist iAccount (with dmSendTracker + accountRelays) out of MainContent into the LoggedIn branch so the top-level compose dialog can read the account's blossom flow too; pass them into MainContent as params. - Thread iAccount.blossomServerList.flow into ComposeNoteDialog (reactive: the server picker updates if the list loads after the dialog opens), EditProfileDialog (via UserProfileScreen), and ChatPane (via DesktopMessagesScreen). - Reduce BlossomServers.kt to just the DEFAULT_BLOSSOM_SERVER fallback used when the account has published no kind-10063 list yet. Known gap: quote-compose opened from a feed row (NoteActionsRow) still defaults to DEFAULT_BLOSSOM_SERVER — the per-note card composables don't carry the account handle, and threading it through the whole note-render tree isn't worth it for that secondary path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011dkzkEY6cUsRfqEb7giHi2
This commit is contained in:
@@ -1341,6 +1341,25 @@ private fun AppInner(
|
||||
val account = accountState as AccountState.LoggedIn
|
||||
val nwcConnection by accountManager.nwcConnection.collectAsState()
|
||||
|
||||
// Account state holders (relay lists, blossom servers, DMs, WoT).
|
||||
// Hoisted above MainContent so the top-level compose dialog can also
|
||||
// read the account's blossom server list from iAccount directly.
|
||||
val dmSendTracker = remember(relayManager) { DmSendTracker(relayManager.client) }
|
||||
// Created before iAccount so NIP-65 backup can be loaded.
|
||||
val accountRelays =
|
||||
remember(account, relayManager, scope) {
|
||||
DesktopAccountRelays(account.pubKeyHex, relayManager, scope)
|
||||
}
|
||||
val iAccount =
|
||||
remember(account, localCache, relayManager, dmSendTracker, accountRelays, dmInboxResolver) {
|
||||
DesktopIAccount(account, localCache, relayManager, dmSendTracker, scope, accountRelays, dmInboxResolver)
|
||||
}
|
||||
// When iAccount is replaced (account switch), close the previous
|
||||
// WoTService so its writer coroutine + ops Channel don't leak.
|
||||
DisposableEffect(iAccount) {
|
||||
onDispose { iAccount.wotService.close() }
|
||||
}
|
||||
|
||||
// Lazy-load Namecoin services. The Core RPC HTTP
|
||||
// client is sourced from the Tor-aware DesktopHttpClient
|
||||
// singleton so .onion RPC URLs route through the
|
||||
@@ -1414,6 +1433,9 @@ private fun AppInner(
|
||||
localCache = localCache,
|
||||
accountManager = accountManager,
|
||||
account = account,
|
||||
iAccount = iAccount,
|
||||
accountRelays = accountRelays,
|
||||
dmSendTracker = dmSendTracker,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
indexRelaysStore = indexRelaysStore,
|
||||
@@ -1456,6 +1478,7 @@ private fun AppInner(
|
||||
relayManager = relayManager,
|
||||
account = account,
|
||||
localCache = localCache,
|
||||
blossomServers = iAccount.blossomServerList.flow,
|
||||
replyTo = replyToNote,
|
||||
draftDTag = composeEditDraftTag,
|
||||
draftInitialContent = composeEditContent,
|
||||
@@ -1544,6 +1567,9 @@ fun MainContent(
|
||||
localCache: DesktopLocalCache,
|
||||
accountManager: AccountManager,
|
||||
account: AccountState.LoggedIn,
|
||||
iAccount: DesktopIAccount,
|
||||
accountRelays: DesktopAccountRelays,
|
||||
dmSendTracker: DmSendTracker,
|
||||
nwcConnection: Nip47WalletConnect.Nip47URINorm?,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator,
|
||||
indexRelaysStore: com.vitorpamplona.amethyst.commons.relays.index.PreferencesIndexRelays,
|
||||
@@ -1563,31 +1589,6 @@ fun MainContent(
|
||||
val signerConnectionState by accountManager.signerConnectionState.collectAsState()
|
||||
val lastPingTimeSec by accountManager.lastPingTimeSec.collectAsState()
|
||||
|
||||
// DM infrastructure — hoisted here so it survives screen navigation
|
||||
val dmSendTracker =
|
||||
remember(relayManager) {
|
||||
DmSendTracker(relayManager.client)
|
||||
}
|
||||
// Centralized relay state for all categories (DM, search, blocked, NIP-65 persistence)
|
||||
// Created before iAccount so NIP-65 backup can be loaded
|
||||
val accountRelays =
|
||||
remember(account, relayManager, scope) {
|
||||
DesktopAccountRelays(account.pubKeyHex, relayManager, scope)
|
||||
}
|
||||
|
||||
val iAccount =
|
||||
remember(account, localCache, relayManager, dmSendTracker, accountRelays, dmInboxResolver) {
|
||||
DesktopIAccount(account, localCache, relayManager, dmSendTracker, scope, accountRelays, dmInboxResolver)
|
||||
}
|
||||
|
||||
// When iAccount is replaced (account switch), the previous WoTService's
|
||||
// internal writer coroutine + ops Channel would otherwise leak — the
|
||||
// outer `scope` lives for the whole session. Close the previous
|
||||
// instance on dispose so account-switch is a clean teardown.
|
||||
DisposableEffect(iAccount) {
|
||||
onDispose { iAccount.wotService.close() }
|
||||
}
|
||||
|
||||
// Follow Packs state — single per-account holder for Discover + sidebar + naddr cards
|
||||
val followPacksState =
|
||||
remember(iAccount, localCache, relayManager, scope) {
|
||||
|
||||
+5
-18
@@ -20,23 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.model
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
|
||||
|
||||
/** Fallback media server used when the account has published no kind-10063 list yet. */
|
||||
const val DEFAULT_BLOSSOM_SERVER = "https://blossom.primal.net"
|
||||
|
||||
/**
|
||||
* The account's Blossom media servers (NIP-B7 / kind 10063), read straight from
|
||||
* the cache. This is the per-account source of truth — the same event the
|
||||
* account-config subscription loads and the mobile app uses — so upload sites
|
||||
* read it here instead of a process-global preference.
|
||||
* Fallback media server used when the account has published no kind-10063
|
||||
* Blossom server list yet. The list itself is read reactively from the account's
|
||||
* [com.vitorpamplona.amethyst.commons.model.nipB7Blossom.BlossomServerListState]
|
||||
* (`iAccount.blossomServerList.flow`); this is only the empty-list default.
|
||||
*/
|
||||
fun ICacheProvider.blossomServers(pubKeyHex: HexKey): List<String> =
|
||||
(getOrCreateAddressableNote(BlossomServersEvent.createAddress(pubKeyHex)).event as? BlossomServersEvent)
|
||||
?.servers()
|
||||
.orEmpty()
|
||||
|
||||
/** First declared Blossom server for [pubKeyHex], or [DEFAULT_BLOSSOM_SERVER] when none is set. */
|
||||
fun ICacheProvider.preferredBlossomServer(pubKeyHex: HexKey): String = blossomServers(pubKeyHex).firstOrNull() ?: DEFAULT_BLOSSOM_SERVER
|
||||
const val DEFAULT_BLOSSOM_SERVER = "https://blossom.primal.net"
|
||||
|
||||
+13
-9
@@ -78,7 +78,6 @@ import com.vitorpamplona.amethyst.desktop.ImageCompressionStore
|
||||
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||
import com.vitorpamplona.amethyst.desktop.model.DEFAULT_BLOSSOM_SERVER
|
||||
import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount
|
||||
import com.vitorpamplona.amethyst.desktop.model.blossomServers
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.service.drafts.LocalNoteDraftStore
|
||||
import com.vitorpamplona.amethyst.desktop.service.drafts.NoteDraft
|
||||
@@ -119,6 +118,7 @@ import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.isClient
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
@@ -140,6 +140,7 @@ fun ComposeNoteDialog(
|
||||
relayManager: DesktopRelayConnectionManager,
|
||||
account: AccountState.LoggedIn,
|
||||
localCache: com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache? = null,
|
||||
blossomServers: StateFlow<List<String>>? = null,
|
||||
replyTo: Event? = null,
|
||||
quoteOf: Event? = null,
|
||||
draftDTag: String? = null,
|
||||
@@ -206,13 +207,16 @@ fun ComposeNoteDialog(
|
||||
val uploadTracker = remember { DesktopUploadTracker() }
|
||||
val uploadState by uploadTracker.state.collectAsState()
|
||||
val orchestrator = remember { UploadOrchestrator() }
|
||||
// Media servers come from the account's kind-10063 list (per-account, loaded
|
||||
// by the account-config subscription), not a process-global preference.
|
||||
val blossomServers =
|
||||
remember(localCache, account) {
|
||||
localCache?.blossomServers(account.pubKeyHex).orEmpty().ifEmpty { listOf(DEFAULT_BLOSSOM_SERVER) }
|
||||
}
|
||||
var selectedServer by remember { mutableStateOf(blossomServers.first()) }
|
||||
// Media servers come straight from the account's kind-10063 list holder
|
||||
// (account.blossomServerList.flow), reactively — no cache-poking, no prefs.
|
||||
val serverList by (blossomServers?.collectAsState() ?: remember { mutableStateOf(emptyList<String>()) })
|
||||
val effectiveServers = serverList.ifEmpty { listOf(DEFAULT_BLOSSOM_SERVER) }
|
||||
var selectedServer by remember { mutableStateOf(effectiveServers.first()) }
|
||||
// If the list loads (or changes) after the dialog opens and the current pick
|
||||
// is no longer in it, snap to the first available server.
|
||||
LaunchedEffect(effectiveServers) {
|
||||
if (selectedServer !in effectiveServers) selectedServer = effectiveServers.first()
|
||||
}
|
||||
var postAsPicture by remember { mutableStateOf(false) }
|
||||
|
||||
// Scheduling — when non-null the note is stored for later publication instead of
|
||||
@@ -583,7 +587,7 @@ fun ComposeNoteDialog(
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
|
||||
) {
|
||||
ServerSelector(
|
||||
servers = blossomServers,
|
||||
servers = effectiveServers,
|
||||
selectedServer = selectedServer,
|
||||
onServerSelected = { selectedServer = it },
|
||||
)
|
||||
|
||||
@@ -1043,6 +1043,7 @@ fun FeedScreen(
|
||||
relayManager = relayManager,
|
||||
account = account,
|
||||
localCache = localCache,
|
||||
blossomServers = iAccount?.blossomServerList?.flow,
|
||||
replyTo = replyToEvent,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1376,6 +1376,8 @@ fun NoteActionsRow(
|
||||
relayManager = relayManager,
|
||||
account = account,
|
||||
localCache = localCache,
|
||||
// Quote-compose from a feed row falls back to the default media server:
|
||||
// the per-note card composables don't carry the account's blossom flow.
|
||||
quoteOf = quoteEvent,
|
||||
)
|
||||
}
|
||||
|
||||
+2
-1
@@ -122,6 +122,7 @@ fun UserProfileScreen(
|
||||
relayManager: DesktopRelayConnectionManager,
|
||||
localCache: DesktopLocalCache,
|
||||
account: AccountState.LoggedIn?,
|
||||
blossomServers: kotlinx.coroutines.flow.StateFlow<List<String>>? = null,
|
||||
nwcConnection: com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URINorm? = null,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null,
|
||||
onBack: () -> Unit,
|
||||
@@ -1189,7 +1190,7 @@ fun UserProfileScreen(
|
||||
EditProfileDialog(
|
||||
account = account,
|
||||
relayManager = relayManager,
|
||||
localCache = localCache,
|
||||
blossomServers = blossomServers,
|
||||
latestMetadata = latestMetadataEvent,
|
||||
latestIdentities = latestIdentitiesEvent,
|
||||
onDismiss = { showEditProfile = false },
|
||||
|
||||
+6
-2
@@ -88,7 +88,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.ChatNewMessageState
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.ChatroomFeedViewModel
|
||||
import com.vitorpamplona.amethyst.desktop.model.preferredBlossomServer
|
||||
import com.vitorpamplona.amethyst.desktop.model.DEFAULT_BLOSSOM_SERVER
|
||||
import com.vitorpamplona.amethyst.desktop.ui.components.ToggleableTimeAgoText
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.DesktopFilePicker
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.MediaAttachmentRow
|
||||
@@ -101,6 +101,7 @@ import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||
import com.vitorpamplona.quartz.utils.ciphers.AESGCM
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.dnd.DnDConstants
|
||||
@@ -135,6 +136,7 @@ fun ChatPane(
|
||||
cacheProvider: ICacheProvider,
|
||||
feedViewModel: ChatroomFeedViewModel,
|
||||
messageState: ChatNewMessageState,
|
||||
blossomServers: StateFlow<List<String>>? = null,
|
||||
dmBroadcastStatus: DmBroadcastStatus = DmBroadcastStatus.Idle,
|
||||
onNavigateToProfile: (String) -> Unit = {},
|
||||
onBack: (() -> Unit)? = null,
|
||||
@@ -379,6 +381,7 @@ fun ChatPane(
|
||||
roomKey = roomKey,
|
||||
account = account,
|
||||
cacheProvider = cacheProvider,
|
||||
blossomServers = blossomServers,
|
||||
)
|
||||
attachedFiles.clear()
|
||||
} catch (e: Exception) {
|
||||
@@ -880,9 +883,10 @@ private suspend fun sendEncryptedFiles(
|
||||
roomKey: ChatroomKey,
|
||||
account: IAccount,
|
||||
cacheProvider: ICacheProvider,
|
||||
blossomServers: StateFlow<List<String>>?,
|
||||
) {
|
||||
val orchestrator = UploadOrchestrator()
|
||||
val server = cacheProvider.preferredBlossomServer(account.pubKey)
|
||||
val server = blossomServers?.value?.firstOrNull() ?: DEFAULT_BLOSSOM_SERVER
|
||||
val recipients = roomKey.users.mapNotNull { cacheProvider.getUserIfExists(it) }.map { it.toPTag() }
|
||||
|
||||
for (file in files) {
|
||||
|
||||
+2
@@ -271,6 +271,7 @@ private fun CompactMessagesContent(
|
||||
cacheProvider = cacheProvider,
|
||||
feedViewModel = feedViewModel,
|
||||
messageState = messageState,
|
||||
blossomServers = (account as? DesktopIAccount)?.blossomServerList?.flow,
|
||||
dmBroadcastStatus = broadcastStatus,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onBack = { listState.clearSelection() },
|
||||
@@ -375,6 +376,7 @@ private fun SplitMessagesContent(
|
||||
cacheProvider = cacheProvider,
|
||||
feedViewModel = feedViewModel,
|
||||
messageState = messageState,
|
||||
blossomServers = (account as? DesktopIAccount)?.blossomServerList?.flow,
|
||||
dmBroadcastStatus = broadcastStatus,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
)
|
||||
|
||||
+3
@@ -472,6 +472,7 @@ internal fun RootContent(
|
||||
relayManager = relayManager,
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
blossomServers = iAccount.blossomServerList.flow,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
onBack = {},
|
||||
@@ -553,6 +554,7 @@ internal fun RootContent(
|
||||
relayManager = relayManager,
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
blossomServers = iAccount.blossomServerList.flow,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
onBack = {},
|
||||
@@ -705,6 +707,7 @@ internal fun OverlayContent(
|
||||
relayManager = relayManager,
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
blossomServers = iAccount?.blossomServerList?.flow,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
onBack = onBack,
|
||||
|
||||
+4
-4
@@ -80,8 +80,7 @@ import com.vitorpamplona.amethyst.commons.profile.ProfileBroadcastStatus
|
||||
import com.vitorpamplona.amethyst.commons.profile.ui.ProfileBroadcastBanner
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.model.preferredBlossomServer
|
||||
import com.vitorpamplona.amethyst.desktop.model.DEFAULT_BLOSSOM_SERVER
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.DesktopFilePicker
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
@@ -93,6 +92,7 @@ import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
@@ -125,7 +125,7 @@ sealed class Nip05Status {
|
||||
fun EditProfileDialog(
|
||||
account: AccountState.LoggedIn,
|
||||
relayManager: DesktopRelayConnectionManager,
|
||||
localCache: DesktopLocalCache,
|
||||
blossomServers: StateFlow<List<String>>? = null,
|
||||
latestMetadata: MetadataEvent?,
|
||||
latestIdentities: ExternalIdentitiesEvent?,
|
||||
onDismiss: () -> Unit,
|
||||
@@ -173,7 +173,7 @@ fun EditProfileDialog(
|
||||
}
|
||||
|
||||
val orchestrator = remember { UploadOrchestrator() }
|
||||
val serverBaseUrl = localCache.preferredBlossomServer(account.pubKeyHex)
|
||||
val serverBaseUrl = blossomServers?.value?.firstOrNull() ?: DEFAULT_BLOSSOM_SERVER
|
||||
|
||||
fun uploadFile(
|
||||
file: File,
|
||||
|
||||
-21
@@ -21,8 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.desktop.cache
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.nipB7Blossom.BlossomServerListState
|
||||
import com.vitorpamplona.amethyst.desktop.model.DEFAULT_BLOSSOM_SERVER
|
||||
import com.vitorpamplona.amethyst.desktop.model.preferredBlossomServer
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
@@ -98,23 +96,4 @@ class DesktopBlossomServerListTest {
|
||||
assertEquals(servers, state.getBlossomServersList()?.servers())
|
||||
assertEquals(servers, state.flow.first { it.isNotEmpty() })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `preferredBlossomServer reads the account's first server from cache`() =
|
||||
runTest {
|
||||
val cache = DesktopLocalCache()
|
||||
val signer = NostrSignerInternal(KeyPair())
|
||||
val servers = listOf("https://first.example.com", "https://second.example.com")
|
||||
cache.consume(signedServerList(servers, signer), relayUrl)
|
||||
|
||||
assertEquals("https://first.example.com", cache.preferredBlossomServer(signer.pubKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `preferredBlossomServer falls back to the default when the account has no list`() {
|
||||
val cache = DesktopLocalCache()
|
||||
val signer = NostrSignerInternal(KeyPair())
|
||||
|
||||
assertEquals(DEFAULT_BLOSSOM_SERVER, cache.preferredBlossomServer(signer.pubKey))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user