mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 16:33:27 +00:00
fix: stop scroll-to-end widening from cascading the DM window
Re-key the rooms list edge-detector on listState only, instead of (listState, itemCount). Keying on item count re-armed the detector on every widen: loadMore pulled older conversations, the list grew, LaunchedEffect restarted, the edge-detector reset, and it fired again — walking the window 7->14->21->...->112 days back in a few seconds on a slow connection. Now distinctUntilChanged fires once per reach-the-end gesture and does not re-fire while parked at the end. Also surface initialLoadInFlight from both DM loaders (gift wraps + NIP-04) and keep a spinner up on the rooms screen until the first relay answers, so cold boot no longer flashes the empty state before the DMs land.
This commit is contained in:
+8
@@ -66,6 +66,12 @@ class AccountGiftWrapsEoseManager(
|
||||
private val _loadingMore = MutableStateFlow(false)
|
||||
val loadingMore: StateFlow<Boolean> = _loadingMore.asStateFlow()
|
||||
|
||||
// True from cold boot until the first EOSE arrives. Lets the rooms screen keep
|
||||
// showing a spinner during the (Tor-slow) initial load instead of flashing the
|
||||
// empty state before any relay has answered.
|
||||
private val _initialLoadInFlight = MutableStateFlow(true)
|
||||
val initialLoadInFlight: StateFlow<Boolean> = _initialLoadInFlight.asStateFlow()
|
||||
|
||||
override fun updateFilter(
|
||||
key: AccountQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
@@ -152,6 +158,7 @@ class AccountGiftWrapsEoseManager(
|
||||
bootStartMs[pubkey] = System.currentTimeMillis()
|
||||
bootEventCount[pubkey] = 0
|
||||
bootEoseLogged.remove(pubkey)
|
||||
_initialLoadInFlight.value = true
|
||||
Log.d(TAG) { "cold boot: pubkey=${pubkey.take(8)}… opening gift-wrap subscription, starting to load messages" }
|
||||
|
||||
// Custom listener so we can tell a real EOSE (load finished) apart from live
|
||||
@@ -163,6 +170,7 @@ class AccountGiftWrapsEoseManager(
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
if (bootEoseLogged.add(pubkey)) {
|
||||
_initialLoadInFlight.value = false
|
||||
val elapsed = System.currentTimeMillis() - (bootStartMs[pubkey] ?: System.currentTimeMillis())
|
||||
val count = bootEventCount[pubkey] ?: 0
|
||||
Log.d(TAG) {
|
||||
|
||||
+6
@@ -54,6 +54,11 @@ class DMsFromUserFilterSubAssembler(
|
||||
private val _loadingMore = MutableStateFlow(false)
|
||||
val loadingMore: StateFlow<Boolean> = _loadingMore.asStateFlow()
|
||||
|
||||
// True from (re)subscribe until the first relay response, so the rooms screen can
|
||||
// keep a spinner up during the initial load instead of flashing the empty state.
|
||||
private val _initialLoadInFlight = MutableStateFlow(true)
|
||||
val initialLoadInFlight: StateFlow<Boolean> = _initialLoadInFlight.asStateFlow()
|
||||
|
||||
override fun updateFilter(
|
||||
key: ChatroomListState,
|
||||
since: SincePerRelayMap?,
|
||||
@@ -84,6 +89,7 @@ class DMsFromUserFilterSubAssembler(
|
||||
filters: List<Filter>?,
|
||||
) {
|
||||
if (_loadingMore.value) _loadingMore.value = false
|
||||
_initialLoadInFlight.value = false
|
||||
super.newEose(key, relay, time, filters)
|
||||
}
|
||||
|
||||
|
||||
+26
-8
@@ -90,6 +90,15 @@ private fun CrossFadeState(
|
||||
) {
|
||||
val feedState by feedContentState.feedContent.collectAsStateWithLifecycle()
|
||||
|
||||
// While the first gift-wrap / NIP-04 window is still being fetched and decrypted, an
|
||||
// empty feed means "not loaded yet", not "no conversations". Keep the spinner up until
|
||||
// both initial loads answer so cold boot doesn't flash the empty state before the DMs land.
|
||||
val giftWraps = remember(accountViewModel) { accountViewModel.dataSources().account.giftWraps }
|
||||
val nip04Dms = remember(accountViewModel) { accountViewModel.dataSources().chatroomList.nip04Dms }
|
||||
val giftWrapsInitialLoad by giftWraps.initialLoadInFlight.collectAsStateWithLifecycle()
|
||||
val nip04InitialLoad by nip04Dms.initialLoadInFlight.collectAsStateWithLifecycle()
|
||||
val initialLoadInFlight = giftWrapsInitialLoad || nip04InitialLoad
|
||||
|
||||
CrossfadeIfEnabled(
|
||||
targetState = feedState,
|
||||
animationSpec = tween(durationMillis = 100),
|
||||
@@ -97,7 +106,11 @@ private fun CrossFadeState(
|
||||
) { state ->
|
||||
when (state) {
|
||||
is FeedState.Empty -> {
|
||||
FeedEmpty { feedContentState.invalidateData() }
|
||||
if (initialLoadInFlight) {
|
||||
LoadingFeed()
|
||||
} else {
|
||||
FeedEmpty { feedContentState.invalidateData() }
|
||||
}
|
||||
}
|
||||
|
||||
is FeedState.FeedError -> {
|
||||
@@ -132,7 +145,7 @@ private fun FeedLoaded(
|
||||
val loadingNip04 by nip04Dms.loadingMore.collectAsStateWithLifecycle()
|
||||
val loadingMore = loadingGiftWraps || loadingNip04
|
||||
|
||||
LoadMoreWhenReachingEnd(listState, items.list.size, accountViewModel)
|
||||
LoadMoreWhenReachingEnd(listState, accountViewModel)
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||
@@ -187,23 +200,28 @@ private const val LOAD_MORE_THRESHOLD = 5
|
||||
@Composable
|
||||
private fun LoadMoreWhenReachingEnd(
|
||||
listState: LazyListState,
|
||||
itemCount: Int,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
LaunchedEffect(listState, itemCount) {
|
||||
// Keyed only on listState so the edge-detector is NOT restarted when a widen adds
|
||||
// rooms. distinctUntilChanged then fires exactly once per reach-the-end gesture:
|
||||
// staying at the end does not re-fire, and scrolling back up (nearEnd -> false)
|
||||
// stops further loads. Keying on item count instead would re-arm on every item
|
||||
// growth and cascade the window back for minutes over a slow connection.
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow {
|
||||
val info = listState.layoutInfo
|
||||
val total = info.totalItemsCount
|
||||
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||
lastVisible >= info.totalItemsCount - LOAD_MORE_THRESHOLD
|
||||
total > 0 && lastVisible >= total - LOAD_MORE_THRESHOLD
|
||||
}.distinctUntilChanged()
|
||||
.filter { it && itemCount > 0 }
|
||||
.filter { it }
|
||||
.collect {
|
||||
val giftWraps = accountViewModel.dataSources().account.giftWraps
|
||||
val nip04Dms = accountViewModel.dataSources().chatroomList.nip04Dms
|
||||
if (giftWraps.loadingMore.value || nip04Dms.loadingMore.value) {
|
||||
Log.d("DMPagination") { "rooms list near end ($itemCount items) but a window load is already in flight, skipping" }
|
||||
Log.d("DMPagination") { "rooms list reached end but a window load is already in flight, skipping" }
|
||||
} else {
|
||||
Log.d("DMPagination") { "rooms list scrolled near end ($itemCount items), widening NIP-17 + NIP-04 windows" }
|
||||
Log.d("DMPagination") { "rooms list reached end, widening NIP-17 + NIP-04 windows one step" }
|
||||
val user = accountViewModel.userProfile()
|
||||
giftWraps.loadMore(user)
|
||||
nip04Dms.loadMore(user)
|
||||
|
||||
Reference in New Issue
Block a user