diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt index 2b9c534502..d4876752b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt @@ -35,9 +35,11 @@ class ChatroomListState( class ChatroomListFilterAssembler( client: INostrClient, ) : ComposeSubscriptionManager() { + val nip04Dms = DMsFromUserFilterSubAssembler(client, ::allKeys) + val group = listOf( - DMsFromUserFilterSubAssembler(client, ::allKeys), + nip04Dms, FollowingPublicChatSubAssembler(client, ::allKeys), FollowingEphemeralChatSubAssembler(client, ::allKeys), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt index 410e03d59b..803e97a04d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt @@ -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, ) : PerUserEoseManager(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() + + private fun windowFor(user: User) = windows.getOrPut(user.pubkeyHex) { TimeWindowPagination() } + + private val _loadingMore = MutableStateFlow(false) + val loadingMore: StateFlow = _loadingMore.asStateFlow() + override fun updateFilter( key: ChatroomListState, since: SincePerRelayMap?, ): List? = 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?, + ) { + if (_loadingMore.value) _loadingMore.value = false + super.newEose(key, relay, time, filters) + } + override fun user(key: ChatroomListState) = key.account.userProfile() val userJobMap = mutableMapOf>() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index 43a1c4e01a..3a962a1217 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt @@ -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) } } }