Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,38 @@ private fun ThreadedCommentData.upsertReply(
}
return copy(replies = updatedReplies, replyCount = updatedCount)
}

/**
* Removes a comment with the given [commentId] from the list.
*
* @param commentId The ID of the comment to remove.
* @return A new list of [ThreadedCommentData] with the specified comment removed.
*/
internal fun List<ThreadedCommentData>.removeComment(commentId: String): List<ThreadedCommentData> {
val indexToRemove = indexOfFirst { it.id == commentId }
return if (indexToRemove >= 0) {
// A top-level comment was removed, update the state
toMutableList().apply { removeAt(indexToRemove) }
} else {
// It might be a nested reply, search and remove recursively
map { parent -> parent.removeReply(commentId) }
}
}

private fun ThreadedCommentData.removeReply(commentIdToRemove: String): ThreadedCommentData {
// If this comment has no replies, nothing to remove
if (replies.isNullOrEmpty()) {
return this
}
// Check if the comment to remove is a direct reply
val indexToRemove = replies.indexOfFirst { it.id == commentIdToRemove }
if (indexToRemove >= 0) {
// Found and removed a direct child, update reply count
return copy(
replies = replies.toMutableList().apply { removeAt(indexToRemove) },
replyCount = replyCount - 1,
)
}
// If not found, recursively check each reply
return copy(replies = replies.map { child -> child.removeReply(commentIdToRemove) })
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ import io.getstream.feeds.android.client.api.model.ThreadedCommentData
import io.getstream.feeds.android.client.api.state.ActivityCommentListState
import io.getstream.feeds.android.client.api.state.query.ActivityCommentsQuery
import io.getstream.feeds.android.client.internal.model.PaginationResult
import io.getstream.feeds.android.client.internal.model.removeComment
import io.getstream.feeds.android.client.internal.model.removeReaction
import io.getstream.feeds.android.client.internal.model.update
import io.getstream.feeds.android.client.internal.model.upsertNestedReply
import io.getstream.feeds.android.client.internal.model.upsertReaction
import io.getstream.feeds.android.client.internal.state.query.toComparator
import io.getstream.feeds.android.client.internal.utils.mergeSorted
import io.getstream.feeds.android.client.internal.utils.treeRemoveFirst
import io.getstream.feeds.android.client.internal.utils.upsertSorted
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -92,15 +92,7 @@ internal class ActivityCommentListStateImpl(
}

override fun onCommentRemoved(commentId: String) {
_comments.update { current ->
current.treeRemoveFirst(
matcher = { it.id == commentId },
childrenSelector = { it.replies.orEmpty() },
updateChildren = { parent, children ->
parent.copy(replies = children, replyCount = parent.replyCount - 1)
},
)
}
_comments.update { current -> current.removeComment(commentId) }
}

override fun onCommentReactionUpserted(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import io.getstream.feeds.android.client.api.state.query.ActivityReactionsSort
import io.getstream.feeds.android.client.internal.model.PaginationResult
import io.getstream.feeds.android.client.internal.state.query.ActivityReactionsQueryConfig
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
Expand Down Expand Up @@ -76,7 +76,9 @@ internal class ActivityReactionListStateImpl(override val query: ActivityReactio
}

override fun onReactionUpserted(reaction: FeedsReactionData) {
_reactions.update { current -> current.upsert(reaction, FeedsReactionData::id) }
_reactions.update { current ->
current.upsertSorted(reaction, FeedsReactionData::id, reactionsSorting)
}
}

override fun onReactionRemoved(reaction: FeedsReactionData) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import io.getstream.feeds.android.client.api.model.ThreadedCommentData
import io.getstream.feeds.android.client.api.state.CommentReplyListState
import io.getstream.feeds.android.client.api.state.query.CommentRepliesQuery
import io.getstream.feeds.android.client.internal.model.PaginationResult
import io.getstream.feeds.android.client.internal.model.removeComment
import io.getstream.feeds.android.client.internal.model.removeReaction
import io.getstream.feeds.android.client.internal.model.update
import io.getstream.feeds.android.client.internal.model.upsertNestedReply
import io.getstream.feeds.android.client.internal.model.upsertReaction
import io.getstream.feeds.android.client.internal.state.query.toComparator
Expand Down Expand Up @@ -76,16 +76,7 @@ internal class CommentReplyListStateImpl(
}

private fun onReplyRemoved(commentId: String) {
_replies.update { current ->
val filteredTopLevel = current.filter { it.id != commentId }
if (filteredTopLevel.size != current.size) {
// A top-level comment was removed, update the state
filteredTopLevel
} else {
// It might be a nested reply, search and remove recursively
current.map { parent -> removeNestedReply(parent, commentId) }
}
}
_replies.update { current -> current.removeComment(commentId) }
}

override fun onCommentUpserted(comment: CommentData) {
Expand Down Expand Up @@ -114,26 +105,6 @@ internal class CommentReplyListStateImpl(
}
}

private fun removeNestedReply(
comment: ThreadedCommentData,
commentIdToRemove: String,
): ThreadedCommentData {
// If this comment has no replies, nothing to remove
if (comment.replies.isNullOrEmpty()) {
return comment
}
// Check if the comment to remove is a direct reply
val filteredReplies = comment.replies.filter { it.id != commentIdToRemove }
if (filteredReplies.size != comment.replies.size) {
// Found and removed a direct child, update reply count
return comment.copy(replies = filteredReplies, replyCount = comment.replyCount - 1)
}
// If not found, recursively check each reply
return comment.copy(
replies = comment.replies.map { child -> removeNestedReply(child, commentIdToRemove) }
)
}

private fun addNestedReplyReaction(
parent: ThreadedCommentData,
comment: CommentData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,122 +311,3 @@ internal fun <T> List<T>.mergeSorted(
idSelector: (T) -> String,
sort: List<Sort<T>>,
): List<T> = mergeSorted(other, idSelector, CompositeComparator(sort))

/**
* Updates an existing element in a tree-like list, and optionally sorts the result.
*
* This function traverses the tree, finds the first element for which [matcher] returns true and
* replaces it with the updated element provided by the [updateElement] function. If no matching
* element is found, the list remains unchanged. If a [comparator] is provided, the updated list
* will be sorted according to it.
*
* @param matcher A function that determines whether an element should be updated.
* @param childrenSelector A function that extracts the children of an element. This is used to
* recursively update nested elements.
* @param updateElement A function that takes the existing element and returns the updated element.
* @param updateChildren A function that takes the existing element and the updated children, and
* returns the updated element with the new children.
* @param comparator The comparator used to sort the list after the update.
* @return A list containing the updated element. If an existing element was found, it will be
* replaced and repositioned; otherwise, the list remains unchanged.
*/
internal fun <T> List<T>.treeUpdateFirst(
matcher: (T) -> Boolean,
childrenSelector: (T) -> List<T>,
updateElement: (T) -> T,
updateChildren: (T, List<T>) -> T,
comparator: Comparator<in T>? = null,
): List<T> {

return internalTreeUpdate(matcher, childrenSelector, updateElement, updateChildren, comparator)
?: this
}

/**
* Removes the first element matching the [matcher] from a tree-like list.
*
* This function traverses the tree, finds the first element for which [matcher] returns true and
* removes it from the list.
*
* @param matcher A function that determines whether an element should be removed.
* @param childrenSelector A function that extracts the children of an element. This is used to
* recursively remove nested elements.
* @param updateChildren A function that takes the existing element and the updated children, and
* returns the updated element with the new children.
* @return A list with the first matching element removed. If no matching element was found, the
* original list is returned.
*/
internal fun <T> List<T>.treeRemoveFirst(
matcher: (T) -> Boolean,
childrenSelector: (T) -> List<T>,
updateChildren: (T, List<T>) -> T,
): List<T> {
return internalTreeUpdate(matcher, childrenSelector, null, updateChildren, null) ?: this
}

/**
* Internal helper to update an element in a tree-like list.
*
* @return A new list with the updated element, or null if no update was made.
*/
private fun <T> List<T>.internalTreeUpdate(
matcher: (T) -> Boolean,
childrenSelector: (T) -> List<T>,
updateElement: ((T) -> T)?,
updateChildren: (T, List<T>) -> T,
comparator: Comparator<in T>?,
): List<T>? {
if (isEmpty()) {
return null
}

// 1. Check for a match at the current level
val index = indexOfFirst(matcher)
if (index >= 0) {
return toMutableList().apply {
if (updateElement == null) {
removeAt(index)
} else {
this[index] = updateElement(this[index])
comparator?.let(::sortWith)
}
}
}

// 2. If no match, recurse into children
var wasUpdated = false
val resultList =
buildList(this.size) {
for (item in this@internalTreeUpdate) {
// If a sibling was updated, add the remaining items as-is
if (wasUpdated) {
add(item)
continue
}

val newChildren =
childrenSelector(item)
.internalTreeUpdate(
matcher,
childrenSelector,
updateElement,
updateChildren,
comparator,
)

// If the result is not null, it means the children were updated
if (newChildren != null) {
wasUpdated = true
add(updateChildren(item, newChildren))
} else {
add(item)
}
}
}

return if (wasUpdated) {
comparator?.let(resultList::sortedWith) ?: resultList
} else {
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import io.getstream.feeds.android.client.api.state.query.ActivityReactionsSort
import io.getstream.feeds.android.client.internal.state.query.ActivityReactionsQueryConfig
import io.getstream.feeds.android.client.internal.test.TestData.defaultPaginationResult
import io.getstream.feeds.android.client.internal.test.TestData.feedsReactionData
import java.util.Date
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
Expand All @@ -39,7 +40,10 @@ internal class ActivityReactionListStateImplTest {
@Test
fun `on queryMoreActivityReactions, then update reactions and pagination`() = runTest {
val reactions =
listOf(feedsReactionData(), feedsReactionData("reaction-2", "activity-1", "user-2"))
listOf(
feedsReactionData(activityId = "activity-1", userId = "user-1"),
feedsReactionData(activityId = "activity-1", userId = "user-2"),
)
val paginationResult = defaultPaginationResult(reactions)

activityReactionListState.onQueryMoreActivityReactions(paginationResult, queryConfig)
Expand All @@ -52,7 +56,10 @@ internal class ActivityReactionListStateImplTest {
@Test
fun `on reactionRemoved, then remove specific reaction`() = runTest {
val initialReactions =
listOf(feedsReactionData(), feedsReactionData("reaction-2", "activity-1", "user-2"))
listOf(
feedsReactionData(activityId = "activity-1", userId = "user-1"),
feedsReactionData(activityId = "activity-1", userId = "user-2"),
)
val paginationResult = defaultPaginationResult(initialReactions)
activityReactionListState.onQueryMoreActivityReactions(paginationResult, queryConfig)

Expand All @@ -62,10 +69,32 @@ internal class ActivityReactionListStateImplTest {
assertEquals(listOf(initialReactions[1]), remainingReactions)
}

@Test
fun `on reactionUpserted with new reaction, then insert in sorted order`() = runTest {
val olderReaction =
feedsReactionData(activityId = "activity-1", userId = "user-1", createdAt = Date(1000))
val newerReaction =
feedsReactionData(activityId = "activity-1", userId = "user-3", createdAt = Date(3000))
val initialReactions = listOf(newerReaction, olderReaction) // newest first
val paginationResult = defaultPaginationResult(initialReactions)
activityReactionListState.onQueryMoreActivityReactions(paginationResult, queryConfig)

val middleReaction =
feedsReactionData(activityId = "activity-1", userId = "user-2", createdAt = Date(2000))
activityReactionListState.onReactionUpserted(middleReaction)

// The reaction is inserted in sorted position (newest first)
val expectedOrder = listOf(newerReaction, middleReaction, olderReaction)
assertEquals(expectedOrder, activityReactionListState.reactions.value)
}

@Test
fun `on onActivityRemoved, clear all reactions`() = runTest {
val reactions =
listOf(feedsReactionData(), feedsReactionData("reaction-2", "activity-1", "user-2"))
listOf(
feedsReactionData(activityId = "activity-1", userId = "user-1"),
feedsReactionData(activityId = "activity-1", userId = "user-2"),
)
val paginationResult = defaultPaginationResult(reactions)

activityReactionListState.onQueryMoreActivityReactions(paginationResult, queryConfig)
Expand Down
Loading
Loading