fix(cache): use relayStatuses (available) instead of connectedRelays for subscriptions

Root cause of "0 relays, 0 notes, 0 follows": all screens used
connectedRelays as the relay set for subscriptions, but NostrClient
uses relay-on-demand — relays only connect when openReqSubscription
is called with their URL. This created a deadlock: screens waited for
connected relays, but relays only connect when screens subscribe.

Fix: use relayStatuses.keys (registered/available relays) which are
populated by addDefaultRelays() at startup. openReqSubscription
triggers connection on-demand.

Affected screens: FeedScreen, UserProfileScreen, ThreadScreen,
NotificationsScreen, BookmarksScreen, ReadsScreen, NewDmDialog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-03-23 13:50:14 +02:00
parent 1e1bc1b172
commit 3e41bab5d5
7 changed files with 23 additions and 13 deletions
@@ -76,7 +76,8 @@ fun BookmarksScreen(
onNavigateToThread: (String) -> Unit = {}, onNavigateToThread: (String) -> Unit = {},
onZapFeedback: (ZapFeedback) -> Unit = {}, onZapFeedback: (ZapFeedback) -> Unit = {},
) { ) {
val connectedRelays by relayManager.connectedRelays.collectAsState() val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays = remember(relayStatuses) { relayStatuses.keys }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// Tab state // Tab state
@@ -166,18 +166,22 @@ fun FeedScreen(
onNavigateToThread: (String) -> Unit = {}, onNavigateToThread: (String) -> Unit = {},
onZapFeedback: (ZapFeedback) -> Unit = {}, onZapFeedback: (ZapFeedback) -> Unit = {},
) { ) {
val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays by relayManager.connectedRelays.collectAsState() val connectedRelays by relayManager.connectedRelays.collectAsState()
val followedUsers by localCache.followedUsers.collectAsState() val followedUsers by localCache.followedUsers.collectAsState()
// Available relay URLs — openReqSubscription triggers connection on-demand
val allRelayUrls = remember(relayStatuses) { relayStatuses.keys }
var replyToEvent by remember { mutableStateOf<Event?>(null) } var replyToEvent by remember { mutableStateOf<Event?>(null) }
var lightboxState by remember { mutableStateOf<LightboxState?>(null) } var lightboxState by remember { mutableStateOf<LightboxState?>(null) }
var feedMode by remember { mutableStateOf(initialFeedMode ?: DesktopPreferences.feedMode) } var feedMode by remember { mutableStateOf(initialFeedMode ?: DesktopPreferences.feedMode) }
// Subscribe to contact list (kind 3) — populates localCache.followedUsers // Subscribe to contact list (kind 3) — populates localCache.followedUsers
rememberSubscription(connectedRelays, account, relayManager = relayManager) { rememberSubscription(allRelayUrls, account, relayManager = relayManager) {
if (connectedRelays.isNotEmpty() && account != null) { if (allRelayUrls.isNotEmpty() && account != null) {
createContactListSubscription( createContactListSubscription(
relays = connectedRelays, relays = allRelayUrls,
pubKeyHex = account.pubKeyHex, pubKeyHex = account.pubKeyHex,
onEvent = { event, _, relay, _ -> onEvent = { event, _, relay, _ ->
subscriptionsCoordinator?.consumeEvent(event, relay) subscriptionsCoordinator?.consumeEvent(event, relay)
@@ -189,13 +193,13 @@ fun FeedScreen(
} }
// Subscribe to feed events (kind 1) — populates cache via coordinator // Subscribe to feed events (kind 1) — populates cache via coordinator
rememberSubscription(connectedRelays, feedMode, followedUsers, relayManager = relayManager) { rememberSubscription(allRelayUrls, feedMode, followedUsers, relayManager = relayManager) {
if (connectedRelays.isEmpty()) return@rememberSubscription null if (allRelayUrls.isEmpty()) return@rememberSubscription null
when (feedMode) { when (feedMode) {
FeedMode.GLOBAL -> { FeedMode.GLOBAL -> {
createGlobalFeedSubscription( createGlobalFeedSubscription(
relays = connectedRelays, relays = allRelayUrls,
onEvent = { event, _, relay, _ -> onEvent = { event, _, relay, _ ->
subscriptionsCoordinator?.consumeEvent(event, relay) subscriptionsCoordinator?.consumeEvent(event, relay)
}, },
@@ -206,7 +210,7 @@ fun FeedScreen(
val follows = followedUsers.toList() val follows = followedUsers.toList()
if (follows.isNotEmpty()) { if (follows.isNotEmpty()) {
createFollowingFeedSubscription( createFollowingFeedSubscription(
relays = connectedRelays, relays = allRelayUrls,
followedUsers = follows, followedUsers = follows,
onEvent = { event, _, relay, _ -> onEvent = { event, _, relay, _ ->
subscriptionsCoordinator?.consumeEvent(event, relay) subscriptionsCoordinator?.consumeEvent(event, relay)
@@ -112,7 +112,8 @@ fun NotificationsScreen(
account: AccountState.LoggedIn, account: AccountState.LoggedIn,
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null, subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator? = null,
) { ) {
val connectedRelays by relayManager.connectedRelays.collectAsState() val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays = remember(relayStatuses) { relayStatuses.keys }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val notificationState = val notificationState =
remember { remember {
@@ -187,7 +187,8 @@ fun ReadsScreen(
onNavigateToThread: (String) -> Unit = {}, onNavigateToThread: (String) -> Unit = {},
onZapFeedback: (ZapFeedback) -> Unit = {}, onZapFeedback: (ZapFeedback) -> Unit = {},
) { ) {
val connectedRelays by relayManager.connectedRelays.collectAsState() val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays = remember(relayStatuses) { relayStatuses.keys }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val eventState = val eventState =
@@ -89,7 +89,8 @@ fun ThreadScreen(
onZapFeedback: (ZapFeedback) -> Unit = {}, onZapFeedback: (ZapFeedback) -> Unit = {},
onReply: (Event) -> Unit = {}, onReply: (Event) -> Unit = {},
) { ) {
val connectedRelays by relayManager.connectedRelays.collectAsState() val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays = remember(relayStatuses) { relayStatuses.keys }
// Lightbox state // Lightbox state
var lightboxState by remember { mutableStateOf<LightboxState?>(null) } var lightboxState by remember { mutableStateOf<LightboxState?>(null) }
@@ -124,7 +124,8 @@ fun UserProfileScreen(
onNavigateToArticle: (String) -> Unit = {}, onNavigateToArticle: (String) -> Unit = {},
onZapFeedback: (ZapFeedback) -> Unit = {}, onZapFeedback: (ZapFeedback) -> Unit = {},
) { ) {
val connectedRelays by relayManager.connectedRelays.collectAsState() val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays = remember(relayStatuses) { relayStatuses.keys }
// User metadata // User metadata
var displayName by remember { mutableStateOf<String?>(null) } var displayName by remember { mutableStateOf<String?>(null) }
@@ -80,7 +80,8 @@ fun NewDmDialog(
val cachedUsers by searchState.cachedUserResults.collectAsState() val cachedUsers by searchState.cachedUserResults.collectAsState()
val relaySearchResults by searchState.relaySearchResults.collectAsState() val relaySearchResults by searchState.relaySearchResults.collectAsState()
val isSearchingRelays by searchState.isSearchingRelays.collectAsState() val isSearchingRelays by searchState.isSearchingRelays.collectAsState()
val connectedRelays by relayManager.connectedRelays.collectAsState() val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays = remember(relayStatuses) { relayStatuses.keys }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
// NIP-50 relay search when local cache has few/no results // NIP-50 relay search when local cache has few/no results