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.createFollowingFeedSubscription
|
||||||
import com.vitorpamplona.amethyst.commons.subscriptions.createGlobalFeedSubscription
|
import com.vitorpamplona.amethyst.commons.subscriptions.createGlobalFeedSubscription
|
||||||
import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription
|
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.components.LoadingState
|
||||||
import com.vitorpamplona.amethyst.commons.ui.note.NoteCard
|
import com.vitorpamplona.amethyst.commons.ui.note.NoteCard
|
||||||
import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData
|
import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData
|
||||||
@@ -127,6 +128,10 @@ fun FeedScreen(
|
|||||||
var feedMode by remember { mutableStateOf(FeedMode.GLOBAL) }
|
var feedMode by remember { mutableStateOf(FeedMode.GLOBAL) }
|
||||||
var followedUsers by remember { mutableStateOf<Set<String>>(emptySet()) }
|
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
|
// Load followed users for Following feed mode
|
||||||
rememberSubscription(relayStatuses, account, feedMode, relayManager = relayManager) {
|
rememberSubscription(relayStatuses, account, feedMode, relayManager = relayManager) {
|
||||||
val configuredRelays = relayStatuses.keys
|
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) {
|
remember(feedMode) {
|
||||||
eventState.clear()
|
eventState.clear()
|
||||||
|
eoseReceivedCount = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to feed based on mode
|
// Subscribe to feed based on mode
|
||||||
@@ -164,6 +170,9 @@ fun FeedScreen(
|
|||||||
onEvent = { event, _, _, _ ->
|
onEvent = { event, _, _, _ ->
|
||||||
eventState.addItem(event)
|
eventState.addItem(event)
|
||||||
},
|
},
|
||||||
|
onEose = { _, _ ->
|
||||||
|
eoseReceivedCount++
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
FeedMode.FOLLOWING -> {
|
FeedMode.FOLLOWING -> {
|
||||||
@@ -174,6 +183,9 @@ fun FeedScreen(
|
|||||||
onEvent = { event, _, _, _ ->
|
onEvent = { event, _, _, _ ->
|
||||||
eventState.addItem(event)
|
eventState.addItem(event)
|
||||||
},
|
},
|
||||||
|
onEose = { _, _ ->
|
||||||
|
eoseReceivedCount++
|
||||||
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -263,13 +275,23 @@ fun FeedScreen(
|
|||||||
LoadingState("Connecting to relays...")
|
LoadingState("Connecting to relays...")
|
||||||
} else if (feedMode == FeedMode.FOLLOWING && followedUsers.isEmpty()) {
|
} else if (feedMode == FeedMode.FOLLOWING && followedUsers.isEmpty()) {
|
||||||
LoadingState("Loading followed users...")
|
LoadingState("Loading followed users...")
|
||||||
} else if (events.isEmpty()) {
|
} else if (events.isEmpty() && !initialLoadComplete) {
|
||||||
LoadingState(
|
LoadingState("Loading notes...")
|
||||||
if (feedMode == FeedMode.FOLLOWING) {
|
} else if (events.isEmpty() && initialLoadComplete) {
|
||||||
"No notes from followed users yet"
|
EmptyState(
|
||||||
} else {
|
title =
|
||||||
"Loading notes..."
|
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 {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|||||||
+17
-1
@@ -41,8 +41,10 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
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.state.EventCollectionState
|
||||||
import com.vitorpamplona.amethyst.commons.subscriptions.createNotificationsSubscription
|
import com.vitorpamplona.amethyst.commons.subscriptions.createNotificationsSubscription
|
||||||
import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription
|
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.components.LoadingState
|
||||||
import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader
|
import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader
|
||||||
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
|
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
|
||||||
@@ -120,6 +123,10 @@ fun NotificationsScreen(
|
|||||||
}
|
}
|
||||||
val notifications by notificationState.items.collectAsState()
|
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
|
// Subscribe to notifications
|
||||||
rememberSubscription(relayStatuses, account.pubKeyHex, relayManager = relayManager) {
|
rememberSubscription(relayStatuses, account.pubKeyHex, relayManager = relayManager) {
|
||||||
val configuredRelays = relayStatuses.keys
|
val configuredRelays = relayStatuses.keys
|
||||||
@@ -168,6 +175,9 @@ fun NotificationsScreen(
|
|||||||
|
|
||||||
notificationState.addItem(notification)
|
notificationState.addItem(notification)
|
||||||
},
|
},
|
||||||
|
onEose = { _, _ ->
|
||||||
|
eoseReceivedCount++
|
||||||
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -185,8 +195,14 @@ fun NotificationsScreen(
|
|||||||
|
|
||||||
if (connectedRelays.isEmpty()) {
|
if (connectedRelays.isEmpty()) {
|
||||||
LoadingState("Connecting to relays...")
|
LoadingState("Connecting to relays...")
|
||||||
} else if (notifications.isEmpty()) {
|
} else if (notifications.isEmpty() && !initialLoadComplete) {
|
||||||
LoadingState("Loading notifications...")
|
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 {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
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.createNoteSubscription
|
||||||
import com.vitorpamplona.amethyst.commons.subscriptions.createThreadRepliesSubscription
|
import com.vitorpamplona.amethyst.commons.subscriptions.createThreadRepliesSubscription
|
||||||
import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription
|
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.components.LoadingState
|
||||||
import com.vitorpamplona.amethyst.commons.ui.note.NoteCard
|
import com.vitorpamplona.amethyst.commons.ui.note.NoteCard
|
||||||
import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel
|
import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel
|
||||||
@@ -98,6 +99,10 @@ fun ThreadScreen(
|
|||||||
// Cache for calculating reply levels
|
// Cache for calculating reply levels
|
||||||
val levelCache = remember(noteId) { mutableMapOf<String, Int>() }
|
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
|
// Subscribe to the root note
|
||||||
rememberSubscription(relayStatuses, noteId, relayManager = relayManager) {
|
rememberSubscription(relayStatuses, noteId, relayManager = relayManager) {
|
||||||
val configuredRelays = relayStatuses.keys
|
val configuredRelays = relayStatuses.keys
|
||||||
@@ -111,6 +116,9 @@ fun ThreadScreen(
|
|||||||
levelCache[event.id] = 0
|
levelCache[event.id] = 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onEose = { _, _ ->
|
||||||
|
rootNoteEoseReceived = true
|
||||||
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -127,6 +135,9 @@ fun ThreadScreen(
|
|||||||
onEvent = { event, _, _, _ ->
|
onEvent = { event, _, _, _ ->
|
||||||
replyEventState.addItem(event)
|
replyEventState.addItem(event)
|
||||||
},
|
},
|
||||||
|
onEose = { _, _ ->
|
||||||
|
repliesEoseReceived = true
|
||||||
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -172,8 +183,15 @@ fun ThreadScreen(
|
|||||||
|
|
||||||
if (connectedRelays.isEmpty()) {
|
if (connectedRelays.isEmpty()) {
|
||||||
LoadingState("Connecting to relays...")
|
LoadingState("Connecting to relays...")
|
||||||
} else if (rootNote == null) {
|
} else if (rootNote == null && !rootNoteEoseReceived) {
|
||||||
LoadingState("Loading thread...")
|
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 {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
@@ -241,7 +259,7 @@ fun ThreadScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Empty state for no replies
|
// Empty state for no replies
|
||||||
if (replyEvents.isEmpty()) {
|
if (replyEvents.isEmpty() && repliesEoseReceived) {
|
||||||
item {
|
item {
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -251,6 +269,16 @@ fun ThreadScreen(
|
|||||||
modifier = Modifier.padding(16.dp),
|
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