mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 16:33:27 +00:00
feat: window NIP-04 DMs in lockstep with gift wraps in the rooms list
The rooms list merges NIP-04 (kind 4) and NIP-17 (gift wrap) conversations into one time-sorted list, but only the gift-wrap loader was windowed — NIP-04 (`DMsFromUserFilterSubAssembler`) still used an EOSE-only `since` with no limit, so it loaded all kind-4 history at boot. That asymmetry broke scroll-to-load-more: gift wraps filled only the recent top of the list while NIP-04 filled the whole tail, so reaching the list end (deep in the NIP-04 tail) fired `giftWraps.loadMore()`, and the newly fetched 7-14d gift wraps inserted in the *middle* of the feed instead of extending the end — and could re-fire step after step while the user sat in the NIP-04 tail. Apply the same TimeWindowPagination to the NIP-04 rooms-list loader and advance both windows together from the scroll handler, so the merged list is bounded uniformly and reaching the end extends the actual end. The loading footer now reflects either protocol still loading. https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
This commit is contained in:
+3
-1
@@ -35,9 +35,11 @@ class ChatroomListState(
|
||||
class ChatroomListFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<ChatroomListState>() {
|
||||
val nip04Dms = DMsFromUserFilterSubAssembler(client, ::allKeys)
|
||||
|
||||
val group =
|
||||
listOf(
|
||||
DMsFromUserFilterSubAssembler(client, ::allKeys),
|
||||
nip04Dms,
|
||||
FollowingPublicChatSubAssembler(client, ::allKeys),
|
||||
FollowingEphemeralChatSubAssembler(client, ::allKeys),
|
||||
)
|
||||
|
||||
+38
-2
@@ -20,15 +20,22 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.pagination.TimeWindowPagination
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -36,21 +43,50 @@ class DMsFromUserFilterSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<ChatroomListState>,
|
||||
) : PerUserEoseManager<ChatroomListState>(client, allKeys) {
|
||||
// Same moving time window as the gift-wrap (NIP-17) loader, so the merged rooms list is
|
||||
// bounded uniformly across both DM protocols. Without this, NIP-04 loaded all history while
|
||||
// NIP-17 only loaded the recent window, so scroll-to-end (which widens the windows) landed
|
||||
// new NIP-17 rooms in the middle of the NIP-04 tail instead of extending the list end.
|
||||
private val windows = mutableMapOf<HexKey, TimeWindowPagination>()
|
||||
|
||||
private fun windowFor(user: User) = windows.getOrPut(user.pubkeyHex) { TimeWindowPagination() }
|
||||
|
||||
private val _loadingMore = MutableStateFlow(false)
|
||||
val loadingMore: StateFlow<Boolean> = _loadingMore.asStateFlow()
|
||||
|
||||
override fun updateFilter(
|
||||
key: ChatroomListState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter>? =
|
||||
if (key.account.isWriteable()) {
|
||||
val windowSince = windowFor(user(key)).since
|
||||
key.account.homeRelays.flow.value.map {
|
||||
filterNip04DMsFromMe(key.account.userProfile(), it, since?.get(it)?.time)
|
||||
filterNip04DMsFromMe(key.account.userProfile(), it, windowSince)
|
||||
} +
|
||||
key.account.dmRelays.flow.value.map {
|
||||
filterNip04DMsToMe(key.account.userProfile(), it, since?.get(it)?.time)
|
||||
filterNip04DMsToMe(key.account.userProfile(), it, windowSince)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
/** Widens the NIP-04 time window for [user] one step back. Kept in lockstep with the gift-wrap window. */
|
||||
fun loadMore(user: User) {
|
||||
windowFor(user).loadMore()
|
||||
_loadingMore.value = true
|
||||
invalidateFilters()
|
||||
}
|
||||
|
||||
override fun newEose(
|
||||
key: ChatroomListState,
|
||||
relay: NormalizedRelayUrl,
|
||||
time: Long,
|
||||
filters: List<Filter>?,
|
||||
) {
|
||||
if (_loadingMore.value) _loadingMore.value = false
|
||||
super.newEose(key, relay, time, filters)
|
||||
}
|
||||
|
||||
override fun user(key: ChatroomListState) = key.account.userProfile()
|
||||
|
||||
val userJobMap = mutableMapOf<User, List<Job>>()
|
||||
|
||||
+19
-7
@@ -127,7 +127,10 @@ private fun FeedLoaded(
|
||||
val myPubKey = accountViewModel.userProfile().pubkeyHex
|
||||
|
||||
val giftWraps = remember(accountViewModel) { accountViewModel.dataSources().account.giftWraps }
|
||||
val loadingMore by giftWraps.loadingMore.collectAsStateWithLifecycle()
|
||||
val nip04Dms = remember(accountViewModel) { accountViewModel.dataSources().chatroomList.nip04Dms }
|
||||
val loadingGiftWraps by giftWraps.loadingMore.collectAsStateWithLifecycle()
|
||||
val loadingNip04 by nip04Dms.loadingMore.collectAsStateWithLifecycle()
|
||||
val loadingMore = loadingGiftWraps || loadingNip04
|
||||
|
||||
LoadMoreWhenReachingEnd(listState, items.list.size, accountViewModel)
|
||||
|
||||
@@ -170,10 +173,16 @@ private fun FeedLoaded(
|
||||
private const val LOAD_MORE_THRESHOLD = 5
|
||||
|
||||
/**
|
||||
* Widens the DM time window when the messages list is scrolled near its end, so
|
||||
* Widens the DM time windows when the messages list is scrolled near its end, so
|
||||
* older conversations stream in on demand instead of all at boot. Re-evaluates as
|
||||
* the list grows so a near-empty screen keeps filling; the per-account
|
||||
* [AccountGiftWrapsEoseManager.loadingMore] guard prevents overlapping requests.
|
||||
* the list grows so a near-empty screen keeps filling.
|
||||
*
|
||||
* Both DM protocols are advanced in lockstep: NIP-17 gift wraps (always-on account
|
||||
* loader) and NIP-04 (this screen's loader). They must move together — if only one
|
||||
* were windowed, the merged time-sorted list would mix a deep tail of one protocol
|
||||
* with a shallow window of the other, and reaching the list end would pull rooms
|
||||
* that land in the middle of the feed instead of extending the end. The combined
|
||||
* loadingMore guard prevents overlapping requests.
|
||||
*/
|
||||
@Composable
|
||||
private fun LoadMoreWhenReachingEnd(
|
||||
@@ -190,11 +199,14 @@ private fun LoadMoreWhenReachingEnd(
|
||||
.filter { it && itemCount > 0 }
|
||||
.collect {
|
||||
val giftWraps = accountViewModel.dataSources().account.giftWraps
|
||||
if (giftWraps.loadingMore.value) {
|
||||
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" }
|
||||
} else {
|
||||
Log.d("DMPagination") { "rooms list scrolled near end ($itemCount items), requesting an older window of conversations" }
|
||||
giftWraps.loadMore(accountViewModel.userProfile())
|
||||
Log.d("DMPagination") { "rooms list scrolled near end ($itemCount items), widening NIP-17 + NIP-04 windows" }
|
||||
val user = accountViewModel.userProfile()
|
||||
giftWraps.loadMore(user)
|
||||
nip04Dms.loadMore(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user