refactor(desktop): provide blossom servers via LocalBlossomServers

Replace the threaded blossomServers parameters with a LocalBlossomServers
CompositionLocal, provided once from the account's state holder
(iAccount.blossomServerList.flow) around the logged-in UI. Upload sites read
the list from context instead of receiving it down a parameter chain.

- Provide LocalBlossomServers in MainContent's existing provider (covers
  feeds, chats, profile, settings) and around the top-level compose dialog.
- ComposeNoteDialog, EditProfileDialog, ChatPane and the media-server
  settings section read LocalBlossomServers.current; drop the params and the
  prop-drilling through UserProfileScreen and DesktopMessagesScreen.
- This also covers quote-compose from a feed row (NoteActionsRow), which the
  parameter approach couldn't reach — the per-note card composables now get
  the list from context, so it no longer defaults to the primal server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011dkzkEY6cUsRfqEb7giHi2
This commit is contained in:
Claude
2026-07-15 23:44:49 +00:00
parent 2eaaa270c3
commit f727c226c0
10 changed files with 61 additions and 33 deletions
@@ -112,6 +112,7 @@ import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscription
import com.vitorpamplona.amethyst.desktop.ui.ComposeNoteDialog
import com.vitorpamplona.amethyst.desktop.ui.ConnectingRelaysScreen
import com.vitorpamplona.amethyst.desktop.ui.ImportFollowListDialog
import com.vitorpamplona.amethyst.desktop.ui.LocalBlossomServers
import com.vitorpamplona.amethyst.desktop.ui.LoginScreen
import com.vitorpamplona.amethyst.desktop.ui.ZapFeedback
import com.vitorpamplona.amethyst.desktop.ui.auth.ForceLogoutDialog
@@ -1471,19 +1472,23 @@ private fun AppInner(
}
}
// Compose dialog
// Compose dialog. Hosted outside MainContent's provider,
// so provide the account's blossom list here too.
if (showComposeDialog) {
ComposeNoteDialog(
onDismiss = onDismissComposeDialog,
relayManager = relayManager,
account = account,
localCache = localCache,
blossomServers = iAccount.blossomServerList.flow,
replyTo = replyToNote,
draftDTag = composeEditDraftTag,
draftInitialContent = composeEditContent,
initialScheduledForSec = composeEditScheduledForSec,
)
CompositionLocalProvider(
LocalBlossomServers provides iAccount.blossomServerList.flow,
) {
ComposeNoteDialog(
onDismiss = onDismissComposeDialog,
relayManager = relayManager,
account = account,
localCache = localCache,
replyTo = replyToNote,
draftDTag = composeEditDraftTag,
draftInitialContent = composeEditContent,
initialScheduledForSec = composeEditScheduledForSec,
)
}
}
// App Drawer overlay
@@ -1972,6 +1977,7 @@ fun MainContent(
CompositionLocalProvider(
LocalRelayCategories provides relayCategories,
LocalBlossomServers provides iAccount.blossomServerList.flow,
com.vitorpamplona.amethyst.desktop.ui.relay.LocalAccountRelays provides accountRelays,
com.vitorpamplona.amethyst.desktop.ui.deck.LocalDesktopCache provides localCache,
com.vitorpamplona.amethyst.desktop.ui.deck.LocalRelayManager provides relayManager,
@@ -2225,7 +2231,6 @@ fun RelaySettingsScreen(
.TorSettings(torType = com.vitorpamplona.amethyst.commons.tor.TorType.OFF),
onTorSettingsChanged: (com.vitorpamplona.amethyst.commons.tor.TorSettings) -> Unit = {},
namecoinPreferences: DesktopNamecoinPreferences? = null,
blossomServers: kotlinx.coroutines.flow.StateFlow<List<String>>? = null,
onBlossomServersChanged: (List<String>) -> Unit = {},
) {
val relayStatuses by relayManager.relayStatuses.collectAsState()
@@ -2351,7 +2356,7 @@ fun RelaySettingsScreen(
Spacer(Modifier.height(24.dp))
// Media Server Settings (Blossom, kind 10063 — synced with mobile)
val networkBlossomServers by (blossomServers?.collectAsState() ?: remember { mutableStateOf(emptyList<String>()) })
val networkBlossomServers by (LocalBlossomServers.current?.collectAsState() ?: remember { mutableStateOf(emptyList<String>()) })
// The kind-10063 list is authoritative; before it loads (or when the
// user has published none) show the default server.
val effectiveBlossomServers = networkBlossomServers.ifEmpty { listOf(DEFAULT_BLOSSOM_SERVER) }
@@ -118,7 +118,6 @@ 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,7 +139,6 @@ 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,
@@ -208,8 +206,9 @@ fun ComposeNoteDialog(
val uploadState by uploadTracker.state.collectAsState()
val orchestrator = remember { UploadOrchestrator() }
// 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>()) })
// (account.blossomServerList.flow), provided via LocalBlossomServers —
// reactively, no cache-poking, no prefs.
val serverList by (LocalBlossomServers.current?.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
@@ -1043,7 +1043,6 @@ fun FeedScreen(
relayManager = relayManager,
account = account,
localCache = localCache,
blossomServers = iAccount?.blossomServerList?.flow,
replyTo = replyToEvent,
)
}
@@ -0,0 +1,34 @@
/*
* 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.desktop.ui
import androidx.compose.runtime.staticCompositionLocalOf
import kotlinx.coroutines.flow.StateFlow
/**
* The logged-in account's Blossom media server list (NIP-B7 / kind 10063),
* exposed as the reactive flow from the account's `BlossomServerListState`
* (`iAccount.blossomServerList.flow`). Provided once around the logged-in UI so
* upload composables read the list from context instead of threading it through
* every screen. `null` when no account is logged in consumers fall back to
* [com.vitorpamplona.amethyst.desktop.model.DEFAULT_BLOSSOM_SERVER].
*/
val LocalBlossomServers = staticCompositionLocalOf<StateFlow<List<String>>?> { null }
@@ -1376,8 +1376,6 @@ 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,
)
}
@@ -122,7 +122,6 @@ 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,
@@ -1190,7 +1189,6 @@ fun UserProfileScreen(
EditProfileDialog(
account = account,
relayManager = relayManager,
blossomServers = blossomServers,
latestMetadata = latestMetadataEvent,
latestIdentities = latestIdentitiesEvent,
onDismiss = { showEditProfile = false },
@@ -89,6 +89,7 @@ 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.DEFAULT_BLOSSOM_SERVER
import com.vitorpamplona.amethyst.desktop.ui.LocalBlossomServers
import com.vitorpamplona.amethyst.desktop.ui.components.ToggleableTimeAgoText
import com.vitorpamplona.amethyst.desktop.ui.media.DesktopFilePicker
import com.vitorpamplona.amethyst.desktop.ui.media.MediaAttachmentRow
@@ -136,13 +137,14 @@ fun ChatPane(
cacheProvider: ICacheProvider,
feedViewModel: ChatroomFeedViewModel,
messageState: ChatNewMessageState,
blossomServers: StateFlow<List<String>>? = null,
dmBroadcastStatus: DmBroadcastStatus = DmBroadcastStatus.Idle,
onNavigateToProfile: (String) -> Unit = {},
onBack: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
// Account's Blossom media servers (kind 10063), read from context.
val blossomServers = LocalBlossomServers.current
val feedState by feedViewModel.feedState.feedContent.collectAsState()
val messageText by messageState.message.collectAsState()
val recipientsMissingRelays by messageState.recipientsMissingDmRelays.collectAsState()
@@ -271,7 +271,6 @@ private fun CompactMessagesContent(
cacheProvider = cacheProvider,
feedViewModel = feedViewModel,
messageState = messageState,
blossomServers = (account as? DesktopIAccount)?.blossomServerList?.flow,
dmBroadcastStatus = broadcastStatus,
onNavigateToProfile = onNavigateToProfile,
onBack = { listState.clearSelection() },
@@ -376,7 +375,6 @@ private fun SplitMessagesContent(
cacheProvider = cacheProvider,
feedViewModel = feedViewModel,
messageState = messageState,
blossomServers = (account as? DesktopIAccount)?.blossomServerList?.flow,
dmBroadcastStatus = broadcastStatus,
onNavigateToProfile = onNavigateToProfile,
)
@@ -472,7 +472,6 @@ internal fun RootContent(
relayManager = relayManager,
localCache = localCache,
account = account,
blossomServers = iAccount.blossomServerList.flow,
nwcConnection = nwcConnection,
subscriptionsCoordinator = subscriptionsCoordinator,
onBack = {},
@@ -503,7 +502,6 @@ internal fun RootContent(
torSettings = torState.settings,
onTorSettingsChanged = torState.onSettingsChanged,
namecoinPreferences = LocalNamecoinPreferences.current,
blossomServers = iAccount.blossomServerList.flow,
onBlossomServersChanged = { servers ->
// Publish a kind-10063 event so the list syncs to every
// Amethyst client, then consume it locally so state updates
@@ -554,7 +552,6 @@ internal fun RootContent(
relayManager = relayManager,
localCache = localCache,
account = account,
blossomServers = iAccount.blossomServerList.flow,
nwcConnection = nwcConnection,
subscriptionsCoordinator = subscriptionsCoordinator,
onBack = {},
@@ -707,7 +704,6 @@ internal fun OverlayContent(
relayManager = relayManager,
localCache = localCache,
account = account,
blossomServers = iAccount?.blossomServerList?.flow,
nwcConnection = nwcConnection,
subscriptionsCoordinator = subscriptionsCoordinator,
onBack = onBack,
@@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.commons.service.upload.UploadOrchestrator
import com.vitorpamplona.amethyst.desktop.account.AccountState
import com.vitorpamplona.amethyst.desktop.model.DEFAULT_BLOSSOM_SERVER
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
import com.vitorpamplona.amethyst.desktop.ui.LocalBlossomServers
import com.vitorpamplona.amethyst.desktop.ui.media.DesktopFilePicker
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client
@@ -92,7 +93,6 @@ 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,6 @@ sealed class Nip05Status {
fun EditProfileDialog(
account: AccountState.LoggedIn,
relayManager: DesktopRelayConnectionManager,
blossomServers: StateFlow<List<String>>? = null,
latestMetadata: MetadataEvent?,
latestIdentities: ExternalIdentitiesEvent?,
onDismiss: () -> Unit,
@@ -173,7 +172,7 @@ fun EditProfileDialog(
}
val orchestrator = remember { UploadOrchestrator() }
val serverBaseUrl = blossomServers?.value?.firstOrNull() ?: DEFAULT_BLOSSOM_SERVER
val serverBaseUrl = LocalBlossomServers.current?.value?.firstOrNull() ?: DEFAULT_BLOSSOM_SERVER
fun uploadFile(
file: File,