diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt index 69ae9de98a..8668f8a920 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt @@ -111,22 +111,26 @@ class AccountGiftWrapsHistoryEoseManager( return requestNewSubscription(historyListener(key)) } - private fun historyListener(key: AccountQueryState): SubscriptionListener = - object : SubscriptionListener { + private fun historyListener(key: AccountQueryState): SubscriptionListener { + // A just-backgrounded account's subscription can still deliver after the orchestrator rebinds to + // another account; gate the pager (single-active) on whether it's still bound to THIS account's + // cursors so a late callback can't move another account's cursors. newEose runs regardless. + val myCursors = key.account.chatroomList.giftWrapHistory + return object : SubscriptionListener { override fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, forFilters: List?, ) { - pager.onEvent(relay, event.createdAt) + if (pager.isBoundTo(myCursors)) pager.onEvent(relay, event.createdAt) } override fun onEose( relay: NormalizedRelayUrl, forFilters: List?, ) { - if (pager.onEose(relay)) { + if (pager.isBoundTo(myCursors) && pager.onEose(relay)) { Log.d(TAG) { "[giftwrap.history] ${relay.url} reached the bottom (done)" } } // No auto-advance: the relay parks here until its marker asks for the next page. @@ -138,7 +142,7 @@ class AccountGiftWrapsHistoryEoseManager( relay: NormalizedRelayUrl, forFilters: List?, ) { - pager.onClosed(relay, message) + if (pager.isBoundTo(myCursors)) pager.onClosed(relay, message) } override fun onCannotConnect( @@ -146,9 +150,10 @@ class AccountGiftWrapsHistoryEoseManager( message: String, forFilters: List?, ) { - pager.onCannotConnect(relay, message) + if (pager.isBoundTo(myCursors)) pager.onCannotConnect(relay, message) } } + } companion object { private const val TAG = "DMPagination" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt index 484621e230..e087790fcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt @@ -114,22 +114,26 @@ class ChatroomNip04HistorySubAssembler( return requestNewSubscription(historyListener(key)) } - private fun historyListener(key: ChatroomQueryState): SubscriptionListener = - object : SubscriptionListener { + private fun historyListener(key: ChatroomQueryState): SubscriptionListener { + // A just-backgrounded room's subscription can still deliver after the orchestrator rebinds to + // another room; gate the pager (single-active) on whether it's still bound to THIS room's cursors + // so a late callback can't move another room's cursors. newEose (framework bookkeeping) runs anyway. + val myCursors = cursorsFor(key) + return object : SubscriptionListener { override fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, forFilters: List?, ) { - pager.onEvent(relay, event.createdAt) + if (pager.isBoundTo(myCursors)) pager.onEvent(relay, event.createdAt) } override fun onEose( relay: NormalizedRelayUrl, forFilters: List?, ) { - if (pager.onEose(relay)) { + if (pager.isBoundTo(myCursors) && pager.onEose(relay)) { Log.d("DMPagination") { "[convo.nip04.history] ${relay.url} reached the bottom (done)" } } newEose(key, relay, TimeUtils.now(), forFilters) @@ -140,7 +144,7 @@ class ChatroomNip04HistorySubAssembler( relay: NormalizedRelayUrl, forFilters: List?, ) { - pager.onClosed(relay, message) + if (pager.isBoundTo(myCursors)) pager.onClosed(relay, message) } override fun onCannotConnect( @@ -148,7 +152,8 @@ class ChatroomNip04HistorySubAssembler( message: String, forFilters: List?, ) { - pager.onCannotConnect(relay, message) + if (pager.isBoundTo(myCursors)) pager.onCannotConnect(relay, message) } } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt index 88b8af03c4..ffd84d0290 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt @@ -104,22 +104,26 @@ class ChatroomListNip04HistorySubAssembler( return requestNewSubscription(historyListener(key)) } - private fun historyListener(key: ChatroomListState): SubscriptionListener = - object : SubscriptionListener { + private fun historyListener(key: ChatroomListState): SubscriptionListener { + // A just-backgrounded account's subscription can still deliver after the orchestrator rebinds to + // another account; gate the pager (single-active) on whether it's still bound to THIS account's + // cursors so a late callback can't move another account's cursors. newEose runs regardless. + val myCursors = key.account.chatroomList.nip04History + return object : SubscriptionListener { override fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, forFilters: List?, ) { - pager.onEvent(relay, event.createdAt) + if (pager.isBoundTo(myCursors)) pager.onEvent(relay, event.createdAt) } override fun onEose( relay: NormalizedRelayUrl, forFilters: List?, ) { - if (pager.onEose(relay)) { + if (pager.isBoundTo(myCursors) && pager.onEose(relay)) { Log.d("DMPagination") { "[rooms.nip04.history] ${relay.url} reached the bottom (done)" } } newEose(key, relay, TimeUtils.now(), forFilters) @@ -130,7 +134,7 @@ class ChatroomListNip04HistorySubAssembler( relay: NormalizedRelayUrl, forFilters: List?, ) { - pager.onClosed(relay, message) + if (pager.isBoundTo(myCursors)) pager.onClosed(relay, message) } override fun onCannotConnect( @@ -138,7 +142,8 @@ class ChatroomListNip04HistorySubAssembler( message: String, forFilters: List?, ) { - pager.onCannotConnect(relay, message) + if (pager.isBoundTo(myCursors)) pager.onCannotConnect(relay, message) } } + } } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/relayClient/paging/BackwardRelayPager.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/relayClient/paging/BackwardRelayPager.kt index 40806c2c7f..b8b45ad107 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/relayClient/paging/BackwardRelayPager.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/relayClient/paging/BackwardRelayPager.kt @@ -147,6 +147,15 @@ class BackwardRelayPager( recomputeExhausted() } + /** + * Whether [c] is the currently-bound scope's cursor object. The orchestrator is single-active: it can + * only correctly process callbacks for the bound scope. A caller whose subscription may still be alive + * for a *just-backgrounded* scope (navigation overlap, a second pane) must gate its forwarded callbacks + * on this — otherwise a late EOSE from scope A would move scope B's cursors. The cursor object is the + * scope identity (one per `Chatroom`/`ChatroomList`), so reference identity is the check. + */ + fun isBoundTo(c: RelayLoadingCursors): Boolean = c === cursors + // --- Filter building support: the caller assembles the actual REQ from these. --- /** Relays of the active scope that have been advanced (armed) and aren't done — i.e. carry a REQ. */