From 465f0acffbfb44c7617f0a93a026613bf3e808fc Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:43:05 +0200 Subject: [PATCH 1/2] Fix MemberListState processing of batch updates --- .../internal/state/MemberListStateImpl.kt | 20 +++++++---- .../internal/state/MemberListStateImplTest.kt | 33 +++++++++++-------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/stream-feeds-android-client/src/main/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImpl.kt b/stream-feeds-android-client/src/main/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImpl.kt index 6d0b98068..9be6f6f28 100644 --- a/stream-feeds-android-client/src/main/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImpl.kt +++ b/stream-feeds-android-client/src/main/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImpl.kt @@ -15,6 +15,7 @@ */ package io.getstream.feeds.android.client.internal.state +import io.getstream.android.core.api.sort.CompositeComparator import io.getstream.android.core.api.sort.Sort import io.getstream.feeds.android.client.api.model.FeedMemberData import io.getstream.feeds.android.client.api.model.ModelUpdates @@ -93,14 +94,19 @@ internal class MemberListStateImpl(override val query: MembersQuery) : MemberLis val updatesMap = updates.updated.associateBy(FeedMemberData::id) _members.update { current -> - // Apply updates and filter out removed members in a single pass - current.mapNotNull { member -> - if (member.id in updates.removedIds) { - null - } else { - updatesMap[member.id] ?: member + current + .mapNotNullTo(mutableListOf()) { member -> + // Apply updates and filter out removed members in a single pass + if (member.id in updates.removedIds) { + null + } else { + updatesMap[member.id] ?: member + } + } + .apply { + addAll(updates.added) + sortWith(CompositeComparator(membersSorting)) } - } } } diff --git a/stream-feeds-android-client/src/test/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImplTest.kt b/stream-feeds-android-client/src/test/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImplTest.kt index 745a13b9c..fa7190f9a 100644 --- a/stream-feeds-android-client/src/test/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImplTest.kt +++ b/stream-feeds-android-client/src/test/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImplTest.kt @@ -25,6 +25,7 @@ import io.getstream.feeds.android.client.api.state.query.MembersQueryConfig import io.getstream.feeds.android.client.api.state.query.MembersSort import io.getstream.feeds.android.client.internal.test.TestData.defaultPaginationResult import io.getstream.feeds.android.client.internal.test.TestData.feedMemberData +import java.util.Date import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -61,9 +62,8 @@ internal class MemberListStateImplTest { val updatedMember = feedMemberData("user-1", role = "admin") memberListState.onMemberUpdated(updatedMember) - val updatedMembers = memberListState.members.value - assertEquals(updatedMember, updatedMembers.find { it.id == updatedMember.id }) - assertEquals(initialMembers[1], updatedMembers.find { it.id == initialMembers[1].id }) + val expectedMembers = listOf(updatedMember, initialMembers[1]) + assertEquals(expectedMembers, memberListState.members.value) } @Test @@ -79,22 +79,29 @@ internal class MemberListStateImplTest { } @Test - fun `on membersUpdated, then apply multiple updates`() = runTest { - val initialMembers = listOf(feedMemberData(), feedMemberData("user-2")) + fun `on membersUpdated, then apply add update and remove operations`() = runTest { + val initialMembers = + listOf( + feedMemberData("user-3", createdAt = Date(3000)), + feedMemberData("user-2", createdAt = Date(2000)), + feedMemberData("user-1", createdAt = Date(1000)), + ) val paginationResult = defaultPaginationResult(initialMembers) memberListState.onQueryMoreMembers(paginationResult, queryConfig) - val updatedMember = feedMemberData("user-1", role = "admin") + val updatedMember = feedMemberData("user-1", role = "admin", createdAt = Date(5000)) + val newMember = feedMemberData("user-4", createdAt = Date(4000)) val updates = ModelUpdates( - added = emptyList(), + added = listOf(newMember), updated = listOf(updatedMember), - removedIds = listOf(initialMembers[1].id), + removedIds = listOf("user-2"), ) memberListState.onMembersUpdated(updates) - val finalMembers = memberListState.members.value - assertEquals(listOf(updatedMember), finalMembers) + // Members should be sorted by createdAt in descending order + val expectedMembers = listOf(updatedMember, newMember, initialMembers[0]) + assertEquals(expectedMembers, memberListState.members.value) } @Test @@ -119,10 +126,8 @@ internal class MemberListStateImplTest { val updatedMember = feedMemberData("user-1", role = "admin") memberListState.onMemberAdded(updatedMember) - val updatedMembers = memberListState.members.value - assertEquals(2, updatedMembers.size) - assertEquals(updatedMember, updatedMembers.find { it.id == updatedMember.id }) - assertEquals(initialMembers[1], updatedMembers.find { it.id == initialMembers[1].id }) + val expectedMembers = listOf(updatedMember, initialMembers[1]) + assertEquals(expectedMembers, memberListState.members.value) } @Test From 26e47a072c936352d9b69710ac50e50f5156ff85 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:23:59 +0200 Subject: [PATCH 2/2] Fix MemberListState not upserting in order --- .../internal/state/MemberListStateImpl.kt | 14 ++-- .../internal/state/MemberListStateImplTest.kt | 72 +++++++++++-------- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/stream-feeds-android-client/src/main/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImpl.kt b/stream-feeds-android-client/src/main/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImpl.kt index 9be6f6f28..15bff7297 100644 --- a/stream-feeds-android-client/src/main/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImpl.kt +++ b/stream-feeds-android-client/src/main/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImpl.kt @@ -26,7 +26,7 @@ import io.getstream.feeds.android.client.api.state.query.MembersQuery import io.getstream.feeds.android.client.api.state.query.MembersQueryConfig import io.getstream.feeds.android.client.api.state.query.MembersSort import io.getstream.feeds.android.client.internal.utils.mergeSorted -import io.getstream.feeds.android.client.internal.utils.upsert +import io.getstream.feeds.android.client.internal.utils.upsertSorted import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -70,7 +70,9 @@ internal class MemberListStateImpl(override val query: MembersQuery) : MemberLis } override fun onMemberAdded(member: FeedMemberData) { - _members.update { current -> current.upsert(member, FeedMemberData::id) } + _members.update { current -> + current.upsertSorted(member, FeedMemberData::id, membersSorting) + } } override fun onMemberRemoved(memberId: String) { @@ -79,13 +81,7 @@ internal class MemberListStateImpl(override val query: MembersQuery) : MemberLis override fun onMemberUpdated(member: FeedMemberData) { _members.update { current -> - current.map { - if (it.id == member.id) { - member - } else { - it - } - } + current.upsertSorted(member, FeedMemberData::id, membersSorting) } } diff --git a/stream-feeds-android-client/src/test/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImplTest.kt b/stream-feeds-android-client/src/test/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImplTest.kt index fa7190f9a..f5f5b7c09 100644 --- a/stream-feeds-android-client/src/test/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImplTest.kt +++ b/stream-feeds-android-client/src/test/kotlin/io/getstream/feeds/android/client/internal/state/MemberListStateImplTest.kt @@ -18,8 +18,6 @@ package io.getstream.feeds.android.client.internal.state import io.getstream.feeds.android.client.api.model.FeedId import io.getstream.feeds.android.client.api.model.FeedMemberData import io.getstream.feeds.android.client.api.model.ModelUpdates -import io.getstream.feeds.android.client.api.model.PaginationData -import io.getstream.feeds.android.client.api.model.PaginationResult import io.getstream.feeds.android.client.api.state.query.MembersQuery import io.getstream.feeds.android.client.api.state.query.MembersQueryConfig import io.getstream.feeds.android.client.api.state.query.MembersSort @@ -54,23 +52,27 @@ internal class MemberListStateImplTest { } @Test - fun `on memberUpdated, then update specific member`() = runTest { - val initialMembers = listOf(feedMemberData(), feedMemberData("user-2")) - val paginationResult = defaultPaginationResult(initialMembers) - memberListState.onQueryMoreMembers(paginationResult, queryConfig) + fun `on memberUpdated, then update and reposition member`() = runTest { + // Initial members already sorted by createdAt desc + val initialMembers = + listOf( + feedMemberData("user-2", createdAt = Date(2000)), + feedMemberData("user-1", createdAt = Date(1000)), + ) + setupInitialState(initialMembers) - val updatedMember = feedMemberData("user-1", role = "admin") + val updatedMember = feedMemberData("user-1", role = "admin", createdAt = Date(3000)) memberListState.onMemberUpdated(updatedMember) - val expectedMembers = listOf(updatedMember, initialMembers[1]) + // Member should be repositioned according to new sort criteria + val expectedMembers = listOf(updatedMember, initialMembers[0]) assertEquals(expectedMembers, memberListState.members.value) } @Test fun `on memberRemoved, then remove specific member`() = runTest { val initialMembers = listOf(feedMemberData(), feedMemberData("user-2")) - val paginationResult = defaultPaginationResult(initialMembers) - memberListState.onQueryMoreMembers(paginationResult, queryConfig) + setupInitialState(initialMembers) memberListState.onMemberRemoved(initialMembers[0].id) @@ -79,15 +81,14 @@ internal class MemberListStateImplTest { } @Test - fun `on membersUpdated, then apply add update and remove operations`() = runTest { + fun `on membersUpdated, then apply add, update, and remove operations`() = runTest { val initialMembers = listOf( feedMemberData("user-3", createdAt = Date(3000)), feedMemberData("user-2", createdAt = Date(2000)), feedMemberData("user-1", createdAt = Date(1000)), ) - val paginationResult = defaultPaginationResult(initialMembers) - memberListState.onQueryMoreMembers(paginationResult, queryConfig) + setupInitialState(initialMembers) val updatedMember = feedMemberData("user-1", role = "admin", createdAt = Date(5000)) val newMember = feedMemberData("user-4", createdAt = Date(4000)) @@ -105,42 +106,55 @@ internal class MemberListStateImplTest { } @Test - fun `on memberAdded, then add member`() = runTest { - val initialMembers = listOf(feedMemberData(), feedMemberData("user-2")) - val paginationResult = - PaginationResult(models = initialMembers, pagination = PaginationData()) - memberListState.onQueryMoreMembers(paginationResult, queryConfig) + fun `on memberAdded, then add member in sorted position`() = runTest { + // Initial members already sorted by createdAt desc + val initialMembers = + listOf( + feedMemberData("user-3", createdAt = Date(3000)), + feedMemberData("user-1", createdAt = Date(1000)), + ) + setupInitialState(initialMembers) - val newMember = feedMemberData("user-3") + val newMember = feedMemberData("user-2", createdAt = Date(2000)) memberListState.onMemberAdded(newMember) - assertEquals(initialMembers + newMember, memberListState.members.value) + // Member should be inserted in correct sorted position + val expectedMembers = listOf(initialMembers[0], newMember, initialMembers[1]) + assertEquals(expectedMembers, memberListState.members.value) } @Test - fun `on memberAdded with existing id, then update member`() = runTest { - val initialMembers = listOf(feedMemberData(), feedMemberData("user-2")) - val paginationResult = defaultPaginationResult(initialMembers) - memberListState.onQueryMoreMembers(paginationResult, queryConfig) + fun `on memberAdded with existing id, then update and reposition member`() = runTest { + // Initial members already sorted by createdAt desc + val initialMembers = + listOf( + feedMemberData("user-2", createdAt = Date(2000)), + feedMemberData("user-1", createdAt = Date(1000)), + ) + setupInitialState(initialMembers) - val updatedMember = feedMemberData("user-1", role = "admin") + // Add existing user-1 with newer createdAt that should move it to the front + val updatedMember = feedMemberData("user-1", role = "admin", createdAt = Date(3000)) memberListState.onMemberAdded(updatedMember) - val expectedMembers = listOf(updatedMember, initialMembers[1]) + // Member should be updated and repositioned according to new sort criteria (3000, 2000) + val expectedMembers = listOf(updatedMember, initialMembers[0]) assertEquals(expectedMembers, memberListState.members.value) } @Test fun `on clear, then remove all members`() = runTest { - val initialMembers = listOf(feedMemberData(), feedMemberData("user-2")) - val paginationResult = defaultPaginationResult(initialMembers) - memberListState.onQueryMoreMembers(paginationResult, queryConfig) + setupInitialState(listOf(feedMemberData(), feedMemberData("user-2"))) memberListState.clear() assertEquals(emptyList(), memberListState.members.value) } + private fun setupInitialState(members: List) { + memberListState.onQueryMoreMembers(defaultPaginationResult(members), queryConfig) + } + companion object { private val queryConfig = MembersQueryConfig(filter = null, sort = MembersSort.Default) }