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 = {},
onZapFeedback: (ZapFeedback) -> Unit = {},
) {
val connectedRelays by relayManager.connectedRelays.collectAsState()
val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays = remember(relayStatuses) { relayStatuses.keys }
val scope = rememberCoroutineScope()
// Tab state
@@ -166,18 +166,22 @@ fun FeedScreen(
onNavigateToThread: (String) -> Unit = {},
onZapFeedback: (ZapFeedback) -> Unit = {},
) {
val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays by relayManager.connectedRelays.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 lightboxState by remember { mutableStateOf<LightboxState?>(null) }
var feedMode by remember { mutableStateOf(initialFeedMode ?: DesktopPreferences.feedMode) }
// Subscribe to contact list (kind 3) — populates localCache.followedUsers
rememberSubscription(connectedRelays, account, relayManager = relayManager) {
if (connectedRelays.isNotEmpty() && account != null) {
rememberSubscription(allRelayUrls, account, relayManager = relayManager) {
if (allRelayUrls.isNotEmpty() && account != null) {
createContactListSubscription(
relays = connectedRelays,
relays = allRelayUrls,
pubKeyHex = account.pubKeyHex,
onEvent = { event, _, relay, _ ->
subscriptionsCoordinator?.consumeEvent(event, relay)
@@ -189,13 +193,13 @@ fun FeedScreen(
}
// Subscribe to feed events (kind 1) — populates cache via coordinator
rememberSubscription(connectedRelays, feedMode, followedUsers, relayManager = relayManager) {
if (connectedRelays.isEmpty()) return@rememberSubscription null
rememberSubscription(allRelayUrls, feedMode, followedUsers, relayManager = relayManager) {
if (allRelayUrls.isEmpty()) return@rememberSubscription null
when (feedMode) {
FeedMode.GLOBAL -> {
createGlobalFeedSubscription(
relays = connectedRelays,
relays = allRelayUrls,
onEvent = { event, _, relay, _ ->
subscriptionsCoordinator?.consumeEvent(event, relay)
},
@@ -206,7 +210,7 @@ fun FeedScreen(
val follows = followedUsers.toList()
if (follows.isNotEmpty()) {
createFollowingFeedSubscription(
relays = connectedRelays,
relays = allRelayUrls,
followedUsers = follows,
onEvent = { event, _, relay, _ ->
subscriptionsCoordinator?.consumeEvent(event, relay)
@@ -112,7 +112,8 @@ fun NotificationsScreen(
account: AccountState.LoggedIn,
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 notificationState =
remember {
@@ -187,7 +187,8 @@ fun ReadsScreen(
onNavigateToThread: (String) -> 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 eventState =
@@ -89,7 +89,8 @@ fun ThreadScreen(
onZapFeedback: (ZapFeedback) -> Unit = {},
onReply: (Event) -> Unit = {},
) {
val connectedRelays by relayManager.connectedRelays.collectAsState()
val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays = remember(relayStatuses) { relayStatuses.keys }
// Lightbox state
var lightboxState by remember { mutableStateOf<LightboxState?>(null) }
@@ -124,7 +124,8 @@ fun UserProfileScreen(
onNavigateToArticle: (String) -> Unit = {},
onZapFeedback: (ZapFeedback) -> Unit = {},
) {
val connectedRelays by relayManager.connectedRelays.collectAsState()
val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays = remember(relayStatuses) { relayStatuses.keys }
// User metadata
var displayName by remember { mutableStateOf<String?>(null) }
@@ -80,7 +80,8 @@ fun NewDmDialog(
val cachedUsers by searchState.cachedUserResults.collectAsState()
val relaySearchResults by searchState.relaySearchResults.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() }
// NIP-50 relay search when local cache has few/no results