fix: Add proper empty states with EOSE tracking
- NotificationsScreen: Show "No notifications yet" after EOSE instead of infinite loading - FeedScreen: Show "No notes found" after EOSE for both Global and Following feeds - ThreadScreen: Show "Note not found" if root note missing after EOSE, "No replies yet" for empty threads Uses existing EmptyState component from commons. Tracks EOSE (End of Stored Events) from relays to distinguish between "still loading" and "no data". Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.commons.subscriptions.createContactListSubscri
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.createFollowingFeedSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.createGlobalFeedSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.NoteCard
|
||||
import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData
|
||||
@@ -127,6 +128,10 @@ fun FeedScreen(
|
||||
var feedMode by remember { mutableStateOf(FeedMode.GLOBAL) }
|
||||
var followedUsers by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||
|
||||
// Track EOSE to know when initial load is complete
|
||||
var eoseReceivedCount by remember { mutableStateOf(0) }
|
||||
val initialLoadComplete = eoseReceivedCount > 0
|
||||
|
||||
// Load followed users for Following feed mode
|
||||
rememberSubscription(relayStatuses, account, feedMode, relayManager = relayManager) {
|
||||
val configuredRelays = relayStatuses.keys
|
||||
@@ -145,9 +150,10 @@ fun FeedScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Clear events when feed mode changes
|
||||
// Clear events and reset EOSE when feed mode changes
|
||||
remember(feedMode) {
|
||||
eventState.clear()
|
||||
eoseReceivedCount = 0
|
||||
}
|
||||
|
||||
// Subscribe to feed based on mode
|
||||
@@ -164,6 +170,9 @@ fun FeedScreen(
|
||||
onEvent = { event, _, _, _ ->
|
||||
eventState.addItem(event)
|
||||
},
|
||||
onEose = { _, _ ->
|
||||
eoseReceivedCount++
|
||||
},
|
||||
)
|
||||
}
|
||||
FeedMode.FOLLOWING -> {
|
||||
@@ -174,6 +183,9 @@ fun FeedScreen(
|
||||
onEvent = { event, _, _, _ ->
|
||||
eventState.addItem(event)
|
||||
},
|
||||
onEose = { _, _ ->
|
||||
eoseReceivedCount++
|
||||
},
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -263,13 +275,23 @@ fun FeedScreen(
|
||||
LoadingState("Connecting to relays...")
|
||||
} else if (feedMode == FeedMode.FOLLOWING && followedUsers.isEmpty()) {
|
||||
LoadingState("Loading followed users...")
|
||||
} else if (events.isEmpty()) {
|
||||
LoadingState(
|
||||
if (feedMode == FeedMode.FOLLOWING) {
|
||||
"No notes from followed users yet"
|
||||
} else {
|
||||
"Loading notes..."
|
||||
},
|
||||
} else if (events.isEmpty() && !initialLoadComplete) {
|
||||
LoadingState("Loading notes...")
|
||||
} else if (events.isEmpty() && initialLoadComplete) {
|
||||
EmptyState(
|
||||
title =
|
||||
if (feedMode == FeedMode.FOLLOWING) {
|
||||
"No notes from followed users"
|
||||
} else {
|
||||
"No notes found"
|
||||
},
|
||||
description =
|
||||
if (feedMode == FeedMode.FOLLOWING) {
|
||||
"Notes from people you follow will appear here"
|
||||
} else {
|
||||
"Notes from the network will appear here"
|
||||
},
|
||||
onRefresh = { relayManager.connect() },
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
|
||||
+17
-1
@@ -41,8 +41,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -53,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.icons.Zap
|
||||
import com.vitorpamplona.amethyst.commons.state.EventCollectionState
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.createNotificationsSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
||||
import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader
|
||||
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
|
||||
@@ -120,6 +123,10 @@ fun NotificationsScreen(
|
||||
}
|
||||
val notifications by notificationState.items.collectAsState()
|
||||
|
||||
// Track EOSE to know when initial load is complete
|
||||
var eoseReceivedCount by remember { mutableStateOf(0) }
|
||||
val initialLoadComplete = eoseReceivedCount > 0
|
||||
|
||||
// Subscribe to notifications
|
||||
rememberSubscription(relayStatuses, account.pubKeyHex, relayManager = relayManager) {
|
||||
val configuredRelays = relayStatuses.keys
|
||||
@@ -168,6 +175,9 @@ fun NotificationsScreen(
|
||||
|
||||
notificationState.addItem(notification)
|
||||
},
|
||||
onEose = { _, _ ->
|
||||
eoseReceivedCount++
|
||||
},
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -185,8 +195,14 @@ fun NotificationsScreen(
|
||||
|
||||
if (connectedRelays.isEmpty()) {
|
||||
LoadingState("Connecting to relays...")
|
||||
} else if (notifications.isEmpty()) {
|
||||
} else if (notifications.isEmpty() && !initialLoadComplete) {
|
||||
LoadingState("Loading notifications...")
|
||||
} else if (notifications.isEmpty() && initialLoadComplete) {
|
||||
EmptyState(
|
||||
title = "No notifications yet",
|
||||
description = "When someone interacts with your posts, you'll see it here",
|
||||
onRefresh = { relayManager.connect() },
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
||||
+30
-2
@@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.state.EventCollectionState
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.createNoteSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.createThreadRepliesSubscription
|
||||
import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.NoteCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel
|
||||
@@ -98,6 +99,10 @@ fun ThreadScreen(
|
||||
// Cache for calculating reply levels
|
||||
val levelCache = remember(noteId) { mutableMapOf<String, Int>() }
|
||||
|
||||
// Track EOSE to know when initial load is complete
|
||||
var rootNoteEoseReceived by remember(noteId) { mutableStateOf(false) }
|
||||
var repliesEoseReceived by remember(noteId) { mutableStateOf(false) }
|
||||
|
||||
// Subscribe to the root note
|
||||
rememberSubscription(relayStatuses, noteId, relayManager = relayManager) {
|
||||
val configuredRelays = relayStatuses.keys
|
||||
@@ -111,6 +116,9 @@ fun ThreadScreen(
|
||||
levelCache[event.id] = 0
|
||||
}
|
||||
},
|
||||
onEose = { _, _ ->
|
||||
rootNoteEoseReceived = true
|
||||
},
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -127,6 +135,9 @@ fun ThreadScreen(
|
||||
onEvent = { event, _, _, _ ->
|
||||
replyEventState.addItem(event)
|
||||
},
|
||||
onEose = { _, _ ->
|
||||
repliesEoseReceived = true
|
||||
},
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -172,8 +183,15 @@ fun ThreadScreen(
|
||||
|
||||
if (connectedRelays.isEmpty()) {
|
||||
LoadingState("Connecting to relays...")
|
||||
} else if (rootNote == null) {
|
||||
} else if (rootNote == null && !rootNoteEoseReceived) {
|
||||
LoadingState("Loading thread...")
|
||||
} else if (rootNote == null && rootNoteEoseReceived) {
|
||||
EmptyState(
|
||||
title = "Note not found",
|
||||
description = "This note may have been deleted or is not available from connected relays",
|
||||
onRefresh = onBack,
|
||||
refreshLabel = "Go back",
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
@@ -241,7 +259,7 @@ fun ThreadScreen(
|
||||
}
|
||||
|
||||
// Empty state for no replies
|
||||
if (replyEvents.isEmpty()) {
|
||||
if (replyEvents.isEmpty() && repliesEoseReceived) {
|
||||
item {
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Text(
|
||||
@@ -251,6 +269,16 @@ fun ThreadScreen(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
} else if (replyEvents.isEmpty() && !repliesEoseReceived) {
|
||||
item {
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Text(
|
||||
"Loading replies...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user