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 @@ -111,7 +111,8 @@ internal class FeedImpl(
memberListState = memberList.mutableState,
)

private val eventHandler = FeedEventHandler(fid = fid, state = _state)
private val eventHandler =
FeedEventHandler(fid = fid, activityFilter = query.activityFilter, state = _state)

init {
subscriptionManager.subscribe(eventHandler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ internal class FeedListImpl(

private val _state: FeedListStateImpl = FeedListStateImpl(query)

private val eventHandler = FeedListEventHandler(_state)
private val eventHandler = FeedListEventHandler(query.filter, _state)

init {
subscriptionManager.subscribe(eventHandler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import io.getstream.feeds.android.client.api.state.query.FeedsSort
import io.getstream.feeds.android.client.internal.model.PaginationResult
import io.getstream.feeds.android.client.internal.state.query.FeedsQueryConfig
import io.getstream.feeds.android.client.internal.utils.mergeSorted
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 @@ -55,22 +56,6 @@ internal class FeedListStateImpl(override val query: FeedsQuery) : FeedListMutab
override val pagination: PaginationData?
get() = _pagination

override fun onFeedUpdated(feed: FeedData) {
_feeds.update { current ->
current.map {
if (it.fid == feed.fid) {
feed
} else {
it
}
}
}
}

override fun onFeedRemoved(feedId: String) {
_feeds.update { current -> current.filter { it.fid.rawValue != feedId } }
}

override fun onQueryMoreFeeds(
result: PaginationResult<FeedData>,
queryConfig: FeedsQueryConfig,
Expand All @@ -82,6 +67,14 @@ internal class FeedListStateImpl(override val query: FeedsQuery) : FeedListMutab
current.mergeSorted(result.models, { it.fid.rawValue }, feedsSorting)
}
}

override fun onFeedRemoved(feedId: String) {
_feeds.update { current -> current.filter { it.fid.rawValue != feedId } }
}

override fun onFeedUpserted(feed: FeedData) {
_feeds.update { current -> current.upsertSorted(feed, FeedData::fid, feedsSorting) }
}
}

/**
Expand All @@ -100,12 +93,12 @@ internal interface FeedListMutableState : FeedListState, FeedListStateUpdates
*/
internal interface FeedListStateUpdates {

/** Handles updates to a specific feed. */
fun onFeedUpdated(feed: FeedData)

/** Handles the result of a query for more feeds. */
fun onQueryMoreFeeds(result: PaginationResult<FeedData>, queryConfig: FeedsQueryConfig)

/** Handles the removal of a feed by its ID. */
fun onFeedRemoved(feedId: String)

/** Handles updates to a specific feed. */
fun onFeedUpserted(feed: FeedData)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package io.getstream.feeds.android.client.internal.state

import io.getstream.android.core.api.filter.matches
import io.getstream.android.core.api.sort.Sort
import io.getstream.feeds.android.client.api.model.ActivityData
import io.getstream.feeds.android.client.api.model.ActivityPinData
Expand Down Expand Up @@ -159,16 +158,12 @@ internal class FeedStateImpl(
}

override fun onActivityAdded(activity: ActivityData) {
if (feedQuery.activityFilter?.matches(activity) == false) return

_activities.update { current ->
current.upsertSorted(activity, ActivityData::id, activitiesSorting)
}
}

override fun onActivityUpdated(activity: ActivityData) {
if (feedQuery.activityFilter?.matches(activity) == false) return

updateActivitiesWhere({ it.id == activity.id }) { it.update(activity) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import io.getstream.feeds.android.network.models.CommentReactionAddedEvent
import io.getstream.feeds.android.network.models.CommentReactionDeletedEvent
import io.getstream.feeds.android.network.models.CommentReactionUpdatedEvent
import io.getstream.feeds.android.network.models.CommentUpdatedEvent
import io.getstream.feeds.android.network.models.FeedCreatedEvent
import io.getstream.feeds.android.network.models.FeedDeletedEvent
import io.getstream.feeds.android.network.models.FeedMemberAddedEvent
import io.getstream.feeds.android.network.models.FeedMemberRemovedEvent
Expand Down Expand Up @@ -140,10 +141,12 @@ internal sealed interface StateUpdateEvent {
val reaction: FeedsReactionData,
) : StateUpdateEvent

data class FeedUpdated(val feed: FeedData) : StateUpdateEvent
data class FeedAdded(val feed: FeedData) : StateUpdateEvent

data class FeedDeleted(val fid: String) : StateUpdateEvent

data class FeedUpdated(val feed: FeedData) : StateUpdateEvent

data class FeedMemberAdded(val fid: String, val member: FeedMemberData) : StateUpdateEvent

data class FeedMemberRemoved(val fid: String, val memberId: String) : StateUpdateEvent
Expand Down Expand Up @@ -232,6 +235,8 @@ internal fun WSEvent.toModel(): StateUpdateEvent? =
is CommentReactionUpdatedEvent ->
StateUpdateEvent.CommentReactionUpdated(fid, comment.toModel(), reaction.toModel())

is FeedCreatedEvent -> StateUpdateEvent.FeedAdded(feed.toModel())

is FeedUpdatedEvent -> StateUpdateEvent.FeedUpdated(feed.toModel())

is FeedDeletedEvent -> StateUpdateEvent.FeedDeleted(fid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ package io.getstream.feeds.android.client.internal.state.event.handler

import io.getstream.feeds.android.client.api.model.FeedId
import io.getstream.feeds.android.client.api.model.FollowData
import io.getstream.feeds.android.client.api.state.query.ActivitiesFilter
import io.getstream.feeds.android.client.internal.state.FeedStateUpdates
import io.getstream.feeds.android.client.internal.state.event.StateUpdateEvent
import io.getstream.feeds.android.client.internal.state.query.matches
import io.getstream.feeds.android.client.internal.subscribe.StateUpdateEventListener

/**
Expand All @@ -29,8 +31,11 @@ import io.getstream.feeds.android.client.internal.subscribe.StateUpdateEventList
* @param fid The unique identifier for the feed this handler is associated with.
* @property state The instance that manages updates to the feed state.
*/
internal class FeedEventHandler(private val fid: FeedId, private val state: FeedStateUpdates) :
StateUpdateEventListener {
internal class FeedEventHandler(
private val fid: FeedId,
private val activityFilter: ActivitiesFilter?,
private val state: FeedStateUpdates,
) : StateUpdateEventListener {

/**
* Processes a state update event and updates the feed state.
Expand All @@ -40,7 +45,7 @@ internal class FeedEventHandler(private val fid: FeedId, private val state: Feed
override fun onEvent(event: StateUpdateEvent) {
when (event) {
is StateUpdateEvent.ActivityAdded -> {
if (event.fid == fid.rawValue) {
if (event.fid == fid.rawValue && event.activity matches activityFilter) {
state.onActivityAdded(event.activity)
}
}
Expand All @@ -51,6 +56,12 @@ internal class FeedEventHandler(private val fid: FeedId, private val state: Feed
}
}

is StateUpdateEvent.ActivityUpdated -> {
if (event.fid == fid.rawValue && event.activity matches activityFilter) {
state.onActivityUpdated(event.activity)
}
}

is StateUpdateEvent.ActivityRemovedFromFeed -> {
if (event.fid == fid.rawValue) {
state.onActivityRemoved(event.activityId)
Expand All @@ -75,12 +86,6 @@ internal class FeedEventHandler(private val fid: FeedId, private val state: Feed
}
}

is StateUpdateEvent.ActivityUpdated -> {
if (event.fid == fid.rawValue) {
state.onActivityUpdated(event.activity)
}
}

is StateUpdateEvent.ActivityPinned -> {
if (event.fid == fid.rawValue) {
state.onActivityPinned(event.pinnedActivity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,35 @@
*/
package io.getstream.feeds.android.client.internal.state.event.handler

import io.getstream.feeds.android.client.api.state.query.FeedsFilter
import io.getstream.feeds.android.client.internal.state.FeedListStateUpdates
import io.getstream.feeds.android.client.internal.state.event.StateUpdateEvent
import io.getstream.feeds.android.client.internal.state.query.matches
import io.getstream.feeds.android.client.internal.subscribe.StateUpdateEventListener

internal class FeedListEventHandler(private val state: FeedListStateUpdates) :
StateUpdateEventListener {
internal class FeedListEventHandler(
private val filter: FeedsFilter?,
private val state: FeedListStateUpdates,
) : StateUpdateEventListener {

override fun onEvent(event: StateUpdateEvent) {
when (event) {
is StateUpdateEvent.FeedUpdated -> {
state.onFeedUpdated(event.feed)
is StateUpdateEvent.FeedAdded -> {
if (event.feed matches filter) {
state.onFeedUpserted(event.feed)
}
}

is StateUpdateEvent.FeedDeleted -> {
state.onFeedRemoved(event.fid)
}

is StateUpdateEvent.FeedUpdated -> {
if (event.feed matches filter) {
state.onFeedUpserted(event.feed)
}
}

else -> {}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-feeds-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.getstream.feeds.android.client.internal.state.query

import io.getstream.android.core.api.filter.Filter
import io.getstream.android.core.api.filter.FilterField
import io.getstream.android.core.api.filter.matches

internal infix fun <M, F : FilterField<M>> M.matches(filter: Filter<M, F>?): Boolean =
// Null filter means "no filter", so everything matches
filter == null || filter.matches(this)
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ internal fun <T> MutableList<T>.insertSorted(element: T, sort: List<Sort<T>>): L
* will be updated and repositioned; otherwise, the new element will be inserted in the correct
* sorted position as-is.
*/
internal fun <T> List<T>.upsertSorted(
internal fun <T, ID> List<T>.upsertSorted(
element: T,
idSelector: (T) -> String,
idSelector: (T) -> ID,
comparator: Comparator<in T>,
update: (old: T, new: T) -> T = { _, new -> new },
): List<T> {
Expand Down Expand Up @@ -189,9 +189,9 @@ internal fun <T> List<T>.upsertSorted(
* will be updated and repositioned; otherwise, the new element will be inserted in the correct
* sorted position as-is.
*/
internal fun <T> List<T>.upsertSorted(
internal fun <T, ID> List<T>.upsertSorted(
element: T,
idSelector: (T) -> String,
idSelector: (T) -> ID,
sort: List<Sort<T>>,
update: (old: T, new: T) -> T = { _, new -> new },
): List<T> = upsertSorted(element, idSelector, CompositeComparator(sort), update)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ internal class FeedListStateImplTest {
}

@Test
fun `on feedUpdated, then update specific feed`() = runTest {
fun `on onFeedUpserted with existing feed, then update specific feed`() = runTest {
val feed1 = feedData(id = "feed-1", groupId = "user", name = "First Feed")
val feed2 = feedData(id = "feed-2", groupId = "user", name = "Second Feed")
val initialFeeds = listOf(feed1, feed2)
Expand All @@ -65,25 +65,26 @@ internal class FeedListStateImplTest {
name = "Updated Feed",
description = "Updated description",
)
feedListState.onFeedUpdated(updatedFeed)
feedListState.onFeedUpserted(updatedFeed)

val updatedFeeds = feedListState.feeds.value
assertEquals(listOf(updatedFeed, feed2), updatedFeeds)
}

@Test
fun `on feedUpdated with non-existent feed, then keep existing feeds unchanged`() = runTest {
val feed1 = feedData(id = "feed-1", groupId = "user", name = "First Feed")
val feed2 = feedData(id = "feed-2", groupId = "user", name = "Second Feed")
fun `on feedUpserted with non-existent feed, then insert feed in sorted position`() = runTest {
val feed1 = feedData(id = "feed-1", groupId = "group", name = "F1", createdAt = 1000)
val feed2 = feedData(id = "feed-2", groupId = "group", name = "F2", createdAt = 2000)
val initialFeeds = listOf(feed1, feed2)
val paginationResult = defaultPaginationResult(initialFeeds)
feedListState.onQueryMoreFeeds(paginationResult, defaultQueryConfig)

val nonExistentFeed =
feedData(id = "non-existent", groupId = "user", name = "Non-existent Feed")
feedListState.onFeedUpdated(nonExistentFeed)
val newFeed = feedData(id = "feed-new", groupId = "group", name = "F1.5", createdAt = 1500)
feedListState.onFeedUpserted(newFeed)

assertEquals(initialFeeds, feedListState.feeds.value)
// Expect the new feed to be inserted in a sorted position based on createdAt
val expectedFeeds = listOf(feed1, newFeed, feed2)
assertEquals(expectedFeeds, feedListState.feeds.value)
}

@Test
Expand Down
Loading