diff --git a/packages/stream_feeds/CHANGELOG.md b/packages/stream_feeds/CHANGELOG.md index 390dd538..4adbe7d7 100644 --- a/packages/stream_feeds/CHANGELOG.md +++ b/packages/stream_feeds/CHANGELOG.md @@ -3,6 +3,9 @@ - [BREAKING] Renamed `ActivitiesFilterField.type` to `ActivitiesFilterField.activityType`. - [BREAKING] Changed `ActivityData.location` field type from `ActivityLocation?` to `LocationCoordinate?`. - Add support for `enforceUnique` parameter while adding reactions. +- [BREAKING] Changed default behavior for `ActivityAddedEvent` in feeds: activities from other users + are now ignored by default. Only activities from the current user matching the feed's filter are + added. Add `onNewActivity` callback to `feed`, `feedFromId`, or `feedFromQuery` to customize. - Add location filtering support for activities with `ActivitiesFilterField.near` and `ActivitiesFilterField.withinBounds` filter fields. - Add new activity filter fields: `ActivitiesFilterField.feed` and `ActivitiesFilterField.interestTags`. diff --git a/packages/stream_feeds/lib/src/client/feeds_client_impl.dart b/packages/stream_feeds/lib/src/client/feeds_client_impl.dart index d4aad511..95fe2ca5 100644 --- a/packages/stream_feeds/lib/src/client/feeds_client_impl.dart +++ b/packages/stream_feeds/lib/src/client/feeds_client_impl.dart @@ -31,6 +31,7 @@ import '../state/bookmark_list.dart'; import '../state/comment_list.dart'; import '../state/comment_reaction_list.dart'; import '../state/comment_reply_list.dart'; +import '../state/event/on_activity_added.dart'; import '../state/feed.dart'; import '../state/feed_list.dart'; import '../state/follow_list.dart'; @@ -267,10 +268,14 @@ class StreamFeedsClientImpl implements StreamFeedsClient { } @override - Feed feedFromQuery(FeedQuery query) { + Feed feedFromQuery( + FeedQuery query, { + OnNewActivity onNewActivity = defaultOnNewActivity, + }) { return Feed( query: query, currentUserId: user.id, + onNewActivity: onNewActivity, activitiesRepository: _activitiesRepository, bookmarksRepository: _bookmarksRepository, commentsRepository: _commentsRepository, diff --git a/packages/stream_feeds/lib/src/feeds_client.dart b/packages/stream_feeds/lib/src/feeds_client.dart index a46cbf1b..814e28a0 100644 --- a/packages/stream_feeds/lib/src/feeds_client.dart +++ b/packages/stream_feeds/lib/src/feeds_client.dart @@ -18,6 +18,7 @@ import 'state/bookmark_list.dart'; import 'state/comment_list.dart'; import 'state/comment_reaction_list.dart'; import 'state/comment_reply_list.dart'; +import 'state/event/on_activity_added.dart'; import 'state/feed.dart'; import 'state/feed_list.dart'; import 'state/follow_list.dart'; @@ -223,6 +224,11 @@ abstract interface class StreamFeedsClient { /// Creates a [Feed] object using a [FeedQuery] that can include additional /// configuration such as activity filters, limits, and feed data for creation. /// + /// When [onNewActivity] is provided, it customizes how new activities received + /// via real-time events are inserted into the feed. When null, uses the default + /// behavior which adds activities created by the current user to the start of + /// the feed if they match the feed's filter. + /// /// Example: /// ```dart /// final feed = client.feedFromQuery(FeedQuery( @@ -236,7 +242,10 @@ abstract interface class StreamFeedsClient { /// ``` /// /// Returns a [Feed] instance that can be used to interact with the specified feed. - Feed feedFromQuery(FeedQuery query); + Feed feedFromQuery( + FeedQuery query, { + OnNewActivity onNewActivity, + }); /// Creates a feed list instance based on the provided [query]. /// @@ -764,6 +773,11 @@ extension StreamFeedsClientHelpers on StreamFeedsClient { /// Creates a [Feed] object that represents a specific feed. /// The feed can be used to fetch activities, manage follows, and receive real-time updates. /// + /// When [onNewActivity] is provided, it customizes how new activities received + /// via real-time events are inserted into the feed. Defaults to [defaultOnNewActivity], + /// which adds activities created by the current user to the start of the feed + /// if they match the feed's filter. + /// /// Example: /// ```dart /// final userFeed = client.feed('user', 'john'); @@ -783,9 +797,13 @@ extension StreamFeedsClientHelpers on StreamFeedsClient { /// ``` /// /// Returns a [Feed] instance that can be used to interact with the specified feed. - Feed feed({required String group, required String id}) { + Feed feed({ + required String group, + required String id, + OnNewActivity onNewActivity = defaultOnNewActivity, + }) { final fid = FeedId(group: group, id: id); - return feedFromId(fid); + return feedFromId(fid, onNewActivity: onNewActivity); } /// Creates a feed instance for the specified [fid]. @@ -793,6 +811,11 @@ extension StreamFeedsClientHelpers on StreamFeedsClient { /// Creates a [Feed] object that represents a specific feed. /// The feed can be used to fetch activities, manage follows, and receive real-time updates. /// + /// When [onNewActivity] is provided, it customizes how new activities received + /// via real-time events are inserted into the feed. Defaults to [defaultOnNewActivity], + /// which adds activities created by the current user to the start of the feed + /// if they match the feed's filter. + /// /// Example: /// ```dart /// final feedId = FeedId(group: 'user', id: 'john'); @@ -802,8 +825,11 @@ extension StreamFeedsClientHelpers on StreamFeedsClient { /// ``` /// /// Returns a [Feed] instance that can be used to interact with the specified feed. - Feed feedFromId(FeedId fid) { + Feed feedFromId( + FeedId fid, { + OnNewActivity onNewActivity = defaultOnNewActivity, + }) { final query = FeedQuery(fid: fid); - return feedFromQuery(query); + return feedFromQuery(query, onNewActivity: onNewActivity); } } diff --git a/packages/stream_feeds/lib/src/state.dart b/packages/stream_feeds/lib/src/state.dart index 9074d554..c5dec18a 100644 --- a/packages/stream_feeds/lib/src/state.dart +++ b/packages/stream_feeds/lib/src/state.dart @@ -20,6 +20,7 @@ export 'state/feed_list_state.dart'; export 'state/feed_state.dart'; export 'state/follow_list.dart'; export 'state/follow_list_state.dart'; +export 'state/insertion_action.dart'; export 'state/poll_list.dart'; export 'state/poll_list_state.dart'; export 'state/poll_vote_list.dart'; diff --git a/packages/stream_feeds/lib/src/state/activity.dart b/packages/stream_feeds/lib/src/state/activity.dart index 6aaf2983..be2ab8ad 100644 --- a/packages/stream_feeds/lib/src/state/activity.dart +++ b/packages/stream_feeds/lib/src/state/activity.dart @@ -20,7 +20,7 @@ import '../repository/comments_repository.dart'; import '../repository/polls_repository.dart'; import 'activity_comment_list.dart'; import 'activity_state.dart'; -import 'event/activity_event_handler.dart'; +import 'event/handler/activity_event_handler.dart'; import 'query/activity_comments_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/activity_comment_list.dart b/packages/stream_feeds/lib/src/state/activity_comment_list.dart index bf7bfa41..4641e259 100644 --- a/packages/stream_feeds/lib/src/state/activity_comment_list.dart +++ b/packages/stream_feeds/lib/src/state/activity_comment_list.dart @@ -7,7 +7,7 @@ import 'package:stream_core/stream_core.dart'; import '../models/comment_data.dart'; import '../repository/comments_repository.dart'; import 'activity_comment_list_state.dart'; -import 'event/activity_comment_list_event_handler.dart'; +import 'event/handler/activity_comment_list_event_handler.dart'; import 'query/activity_comments_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/activity_list.dart b/packages/stream_feeds/lib/src/state/activity_list.dart index 301f075a..dbe5983d 100644 --- a/packages/stream_feeds/lib/src/state/activity_list.dart +++ b/packages/stream_feeds/lib/src/state/activity_list.dart @@ -9,7 +9,7 @@ import '../models/query_configuration.dart'; import '../repository/activities_repository.dart'; import '../repository/capabilities_repository.dart'; import 'activity_list_state.dart'; -import 'event/activity_list_event_handler.dart'; +import 'event/handler/activity_list_event_handler.dart'; import 'query/activities_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/activity_reaction_list.dart b/packages/stream_feeds/lib/src/state/activity_reaction_list.dart index 500fdda9..0be74052 100644 --- a/packages/stream_feeds/lib/src/state/activity_reaction_list.dart +++ b/packages/stream_feeds/lib/src/state/activity_reaction_list.dart @@ -8,7 +8,7 @@ import '../models/feeds_reaction_data.dart'; import '../models/query_configuration.dart'; import '../repository/activities_repository.dart'; import 'activity_reaction_list_state.dart'; -import 'event/activity_reaction_list_event_handler.dart'; +import 'event/handler/activity_reaction_list_event_handler.dart'; import 'query/activity_reactions_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/bookmark_folder_list.dart b/packages/stream_feeds/lib/src/state/bookmark_folder_list.dart index 7e976831..7198b9f2 100644 --- a/packages/stream_feeds/lib/src/state/bookmark_folder_list.dart +++ b/packages/stream_feeds/lib/src/state/bookmark_folder_list.dart @@ -8,7 +8,7 @@ import '../models/bookmark_folder_data.dart'; import '../models/query_configuration.dart'; import '../repository/bookmarks_repository.dart'; import 'bookmark_folder_list_state.dart'; -import 'event/bookmark_folder_list_event_handler.dart'; +import 'event/handler/bookmark_folder_list_event_handler.dart'; import 'query/bookmark_folders_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/bookmark_list.dart b/packages/stream_feeds/lib/src/state/bookmark_list.dart index 617f471a..39d2fb83 100644 --- a/packages/stream_feeds/lib/src/state/bookmark_list.dart +++ b/packages/stream_feeds/lib/src/state/bookmark_list.dart @@ -8,7 +8,7 @@ import '../models/bookmark_data.dart'; import '../models/query_configuration.dart'; import '../repository/bookmarks_repository.dart'; import 'bookmark_list_state.dart'; -import 'event/bookmark_list_event_handler.dart'; +import 'event/handler/bookmark_list_event_handler.dart'; import 'query/bookmarks_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/comment_list.dart b/packages/stream_feeds/lib/src/state/comment_list.dart index da07af0a..d3946cd5 100644 --- a/packages/stream_feeds/lib/src/state/comment_list.dart +++ b/packages/stream_feeds/lib/src/state/comment_list.dart @@ -7,7 +7,7 @@ import 'package:stream_core/stream_core.dart'; import '../models/comment_data.dart'; import '../repository/comments_repository.dart'; import 'comment_list_state.dart'; -import 'event/comment_list_event_handler.dart'; +import 'event/handler/comment_list_event_handler.dart'; import 'query/comments_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/comment_reaction_list.dart b/packages/stream_feeds/lib/src/state/comment_reaction_list.dart index 7e4249e7..59b868fa 100644 --- a/packages/stream_feeds/lib/src/state/comment_reaction_list.dart +++ b/packages/stream_feeds/lib/src/state/comment_reaction_list.dart @@ -8,7 +8,7 @@ import '../models/feeds_reaction_data.dart'; import '../models/query_configuration.dart'; import '../repository/comments_repository.dart'; import 'comment_reaction_list_state.dart'; -import 'event/comment_reaction_list_event_handler.dart'; +import 'event/handler/comment_reaction_list_event_handler.dart'; import 'query/comment_reactions_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/comment_reply_list.dart b/packages/stream_feeds/lib/src/state/comment_reply_list.dart index f56c2f94..036cf0f3 100644 --- a/packages/stream_feeds/lib/src/state/comment_reply_list.dart +++ b/packages/stream_feeds/lib/src/state/comment_reply_list.dart @@ -7,7 +7,7 @@ import 'package:stream_core/stream_core.dart'; import '../models/comment_data.dart'; import '../repository/comments_repository.dart'; import 'comment_reply_list_state.dart'; -import 'event/comment_reply_list_event_handler.dart'; +import 'event/handler/comment_reply_list_event_handler.dart'; import 'query/comment_replies_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/event/activity_comment_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/activity_comment_list_event_handler.dart similarity index 93% rename from packages/stream_feeds/lib/src/state/event/activity_comment_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/activity_comment_list_event_handler.dart index 9384821b..c0bc07ac 100644 --- a/packages/stream_feeds/lib/src/state/event/activity_comment_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/activity_comment_list_event_handler.dart @@ -1,9 +1,9 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/comment_data.dart'; -import '../../models/feeds_reaction_data.dart'; -import '../activity_comment_list_state.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/comment_data.dart'; +import '../../../models/feeds_reaction_data.dart'; +import '../../activity_comment_list_state.dart'; import 'state_event_handler.dart'; /// Event handler for activity comment list real-time updates. diff --git a/packages/stream_feeds/lib/src/state/event/activity_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/activity_event_handler.dart similarity index 88% rename from packages/stream_feeds/lib/src/state/event/activity_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/activity_event_handler.dart index 0895daad..72c45d8d 100644 --- a/packages/stream_feeds/lib/src/state/event/activity_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/activity_event_handler.dart @@ -1,12 +1,12 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/feed_id.dart'; -import '../../models/poll_data.dart'; -import '../../models/poll_vote_data.dart'; -import '../../resolvers/poll/poll_answer_casted.dart'; -import '../../resolvers/poll/poll_answer_removed.dart'; -import '../activity_state.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/feed_id.dart'; +import '../../../models/poll_data.dart'; +import '../../../models/poll_vote_data.dart'; +import '../../../resolvers/poll/poll_answer_casted.dart'; +import '../../../resolvers/poll/poll_answer_removed.dart'; +import '../../activity_state.dart'; import 'state_event_handler.dart'; /// Event handler for activity real-time updates. diff --git a/packages/stream_feeds/lib/src/state/event/activity_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/activity_list_event_handler.dart similarity index 93% rename from packages/stream_feeds/lib/src/state/event/activity_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/activity_list_event_handler.dart index 7438fe0a..97f02a2d 100644 --- a/packages/stream_feeds/lib/src/state/event/activity_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/activity_list_event_handler.dart @@ -1,13 +1,13 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/activity_data.dart'; -import '../../models/bookmark_data.dart'; -import '../../models/comment_data.dart'; -import '../../models/feeds_reaction_data.dart'; -import '../../repository/capabilities_repository.dart'; -import '../activity_list_state.dart'; -import '../query/activities_query.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/activity_data.dart'; +import '../../../models/bookmark_data.dart'; +import '../../../models/comment_data.dart'; +import '../../../models/feeds_reaction_data.dart'; +import '../../../repository/capabilities_repository.dart'; +import '../../activity_list_state.dart'; +import '../../query/activities_query.dart'; import 'feed_capabilities_mixin.dart'; import 'state_event_handler.dart'; diff --git a/packages/stream_feeds/lib/src/state/event/activity_reaction_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/activity_reaction_list_event_handler.dart similarity index 85% rename from packages/stream_feeds/lib/src/state/event/activity_reaction_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/activity_reaction_list_event_handler.dart index 2be9b8c5..82de4482 100644 --- a/packages/stream_feeds/lib/src/state/event/activity_reaction_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/activity_reaction_list_event_handler.dart @@ -1,8 +1,8 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/feeds_reaction_data.dart'; -import '../activity_reaction_list_state.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/feeds_reaction_data.dart'; +import '../../activity_reaction_list_state.dart'; import 'state_event_handler.dart'; /// Event handler for activity reaction list real-time updates. diff --git a/packages/stream_feeds/lib/src/state/event/bookmark_folder_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/bookmark_folder_list_event_handler.dart similarity index 86% rename from packages/stream_feeds/lib/src/state/event/bookmark_folder_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/bookmark_folder_list_event_handler.dart index 586852f9..ed2d6b63 100644 --- a/packages/stream_feeds/lib/src/state/event/bookmark_folder_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/bookmark_folder_list_event_handler.dart @@ -1,9 +1,9 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/bookmark_folder_data.dart'; -import '../bookmark_folder_list_state.dart'; -import '../query/bookmark_folders_query.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/bookmark_folder_data.dart'; +import '../../bookmark_folder_list_state.dart'; +import '../../query/bookmark_folders_query.dart'; import 'state_event_handler.dart'; /// Event handler for bookmark folder list real-time updates. diff --git a/packages/stream_feeds/lib/src/state/event/bookmark_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/bookmark_list_event_handler.dart similarity index 85% rename from packages/stream_feeds/lib/src/state/event/bookmark_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/bookmark_list_event_handler.dart index 6b98e1d7..1e3d0c07 100644 --- a/packages/stream_feeds/lib/src/state/event/bookmark_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/bookmark_list_event_handler.dart @@ -1,10 +1,10 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/bookmark_data.dart'; -import '../../models/bookmark_folder_data.dart'; -import '../bookmark_list_state.dart'; -import '../query/bookmarks_query.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/bookmark_data.dart'; +import '../../../models/bookmark_folder_data.dart'; +import '../../bookmark_list_state.dart'; +import '../../query/bookmarks_query.dart'; import 'state_event_handler.dart'; /// Event handler for bookmark list real-time updates. diff --git a/packages/stream_feeds/lib/src/state/event/comment_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/comment_list_event_handler.dart similarity index 86% rename from packages/stream_feeds/lib/src/state/event/comment_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/comment_list_event_handler.dart index 9f4d4062..c83864f6 100644 --- a/packages/stream_feeds/lib/src/state/event/comment_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/comment_list_event_handler.dart @@ -1,10 +1,10 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; +import '../../../generated/api/models.dart' as api; -import '../../models/comment_data.dart'; -import '../comment_list_state.dart'; -import '../query/comments_query.dart'; +import '../../../models/comment_data.dart'; +import '../../comment_list_state.dart'; +import '../../query/comments_query.dart'; import 'state_event_handler.dart'; /// Event handler for comment list real-time updates. diff --git a/packages/stream_feeds/lib/src/state/event/comment_reaction_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/comment_reaction_list_event_handler.dart similarity index 82% rename from packages/stream_feeds/lib/src/state/event/comment_reaction_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/comment_reaction_list_event_handler.dart index 3f2bd685..6f268d85 100644 --- a/packages/stream_feeds/lib/src/state/event/comment_reaction_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/comment_reaction_list_event_handler.dart @@ -1,8 +1,8 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/feeds_reaction_data.dart'; -import '../comment_reaction_list_state.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/feeds_reaction_data.dart'; +import '../../comment_reaction_list_state.dart'; import 'state_event_handler.dart'; /// Event handler for comment reaction list real-time updates. diff --git a/packages/stream_feeds/lib/src/state/event/comment_reply_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/comment_reply_list_event_handler.dart similarity index 90% rename from packages/stream_feeds/lib/src/state/event/comment_reply_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/comment_reply_list_event_handler.dart index 1841bbf1..fb2175cf 100644 --- a/packages/stream_feeds/lib/src/state/event/comment_reply_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/comment_reply_list_event_handler.dart @@ -1,9 +1,9 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/comment_data.dart'; -import '../../models/feeds_reaction_data.dart'; -import '../comment_reply_list_state.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/comment_data.dart'; +import '../../../models/feeds_reaction_data.dart'; +import '../../comment_reply_list_state.dart'; import 'state_event_handler.dart'; /// Event handler for comment reply list real-time updates. diff --git a/packages/stream_feeds/lib/src/state/event/feed_capabilities_mixin.dart b/packages/stream_feeds/lib/src/state/event/handler/feed_capabilities_mixin.dart similarity index 82% rename from packages/stream_feeds/lib/src/state/event/feed_capabilities_mixin.dart rename to packages/stream_feeds/lib/src/state/event/handler/feed_capabilities_mixin.dart index 11a6aeeb..55e63047 100644 --- a/packages/stream_feeds/lib/src/state/event/feed_capabilities_mixin.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/feed_capabilities_mixin.dart @@ -1,5 +1,5 @@ -import '../../../stream_feeds.dart'; -import '../../repository/capabilities_repository.dart'; +import '../../../../stream_feeds.dart'; +import '../../../repository/capabilities_repository.dart'; mixin FeedCapabilitiesMixin { CapabilitiesRepository get capabilitiesRepository; diff --git a/packages/stream_feeds/lib/src/state/event/feed_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/feed_event_handler.dart similarity index 89% rename from packages/stream_feeds/lib/src/state/event/feed_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/feed_event_handler.dart index dea127da..f74c259d 100644 --- a/packages/stream_feeds/lib/src/state/event/feed_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/feed_event_handler.dart @@ -1,19 +1,20 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/activity_data.dart'; -import '../../models/activity_pin_data.dart'; -import '../../models/aggregated_activity_data.dart'; -import '../../models/bookmark_data.dart'; -import '../../models/comment_data.dart'; -import '../../models/feed_data.dart'; -import '../../models/feeds_reaction_data.dart'; -import '../../models/follow_data.dart'; -import '../../models/mark_activity_data.dart'; -import '../../repository/capabilities_repository.dart'; -import '../feed_state.dart'; - -import '../query/feed_query.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/activity_data.dart'; +import '../../../models/activity_pin_data.dart'; +import '../../../models/aggregated_activity_data.dart'; +import '../../../models/bookmark_data.dart'; +import '../../../models/comment_data.dart'; +import '../../../models/feed_data.dart'; +import '../../../models/feeds_reaction_data.dart'; +import '../../../models/follow_data.dart'; +import '../../../models/mark_activity_data.dart'; +import '../../../repository/capabilities_repository.dart'; +import '../../feed_state.dart'; + +import '../../query/feed_query.dart'; +import '../on_activity_added.dart'; import 'feed_capabilities_mixin.dart'; import 'state_event_handler.dart'; @@ -22,12 +23,14 @@ class FeedEventHandler with FeedCapabilitiesMixin implements StateEventHandler { required this.query, required this.state, required this.currentUserId, + required this.onNewActivity, required this.capabilitiesRepository, }); final FeedQuery query; - final FeedStateNotifier state; final String currentUserId; + final FeedStateNotifier state; + final OnNewActivity onNewActivity; @override final CapabilitiesRepository capabilitiesRepository; @@ -44,11 +47,10 @@ class FeedEventHandler with FeedCapabilitiesMixin implements StateEventHandler { if (event is api.ActivityAddedEvent) { if (event.fid != fid.rawValue) return; - final activity = event.activity.toModel(); - if (!matchesQueryFilter(activity)) return; - state.onActivityAdded(activity); + final insertionAction = onNewActivity(query, activity, currentUserId); + state.onActivityAdded(activity, insertionAction: insertionAction); final updatedActivity = await withUpdatedFeedCapabilities(activity); if (updatedActivity != null) state.onActivityUpdated(updatedActivity); diff --git a/packages/stream_feeds/lib/src/state/event/feed_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/feed_list_event_handler.dart similarity index 83% rename from packages/stream_feeds/lib/src/state/event/feed_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/feed_list_event_handler.dart index 3ae32761..c106e1a8 100644 --- a/packages/stream_feeds/lib/src/state/event/feed_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/feed_list_event_handler.dart @@ -1,10 +1,10 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/feed_data.dart'; -import '../feed_list_state.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/feed_data.dart'; +import '../../feed_list_state.dart'; -import '../query/feeds_query.dart'; +import '../../query/feeds_query.dart'; import 'state_event_handler.dart'; class FeedListEventHandler implements StateEventHandler { diff --git a/packages/stream_feeds/lib/src/state/event/follow_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/follow_list_event_handler.dart similarity index 85% rename from packages/stream_feeds/lib/src/state/event/follow_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/follow_list_event_handler.dart index 2833a2da..a240a052 100644 --- a/packages/stream_feeds/lib/src/state/event/follow_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/follow_list_event_handler.dart @@ -1,9 +1,9 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/follow_data.dart'; -import '../follow_list_state.dart'; -import '../query/follows_query.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/follow_data.dart'; +import '../../follow_list_state.dart'; +import '../../query/follows_query.dart'; import 'state_event_handler.dart'; /// Event handler for follow list real-time updates. diff --git a/packages/stream_feeds/lib/src/state/event/member_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/member_list_event_handler.dart similarity index 85% rename from packages/stream_feeds/lib/src/state/event/member_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/member_list_event_handler.dart index 09b15b36..4eae1a6f 100644 --- a/packages/stream_feeds/lib/src/state/event/member_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/member_list_event_handler.dart @@ -1,10 +1,10 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/feed_member_data.dart'; -import '../member_list_state.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/feed_member_data.dart'; +import '../../member_list_state.dart'; -import '../query/members_query.dart'; +import '../../query/members_query.dart'; import 'state_event_handler.dart'; class MemberListEventHandler implements StateEventHandler { diff --git a/packages/stream_feeds/lib/src/state/event/poll_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/poll_list_event_handler.dart similarity index 85% rename from packages/stream_feeds/lib/src/state/event/poll_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/poll_list_event_handler.dart index b4e0a948..8a9b287f 100644 --- a/packages/stream_feeds/lib/src/state/event/poll_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/poll_list_event_handler.dart @@ -1,9 +1,9 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/poll_data.dart'; -import '../poll_list_state.dart'; -import '../query/polls_query.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/poll_data.dart'; +import '../../poll_list_state.dart'; +import '../../query/polls_query.dart'; import 'state_event_handler.dart'; /// Event handler for poll list real-time updates. diff --git a/packages/stream_feeds/lib/src/state/event/poll_vote_list_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/poll_vote_list_event_handler.dart similarity index 88% rename from packages/stream_feeds/lib/src/state/event/poll_vote_list_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/poll_vote_list_event_handler.dart index 69f24f4f..97f3916b 100644 --- a/packages/stream_feeds/lib/src/state/event/poll_vote_list_event_handler.dart +++ b/packages/stream_feeds/lib/src/state/event/handler/poll_vote_list_event_handler.dart @@ -1,9 +1,9 @@ import 'package:stream_core/stream_core.dart'; -import '../../generated/api/models.dart' as api; -import '../../models/poll_vote_data.dart'; -import '../poll_vote_list_state.dart'; -import '../query/poll_votes_query.dart'; +import '../../../generated/api/models.dart' as api; +import '../../../models/poll_vote_data.dart'; +import '../../poll_vote_list_state.dart'; +import '../../query/poll_votes_query.dart'; import 'state_event_handler.dart'; /// Event handler for poll vote list real-time updates. diff --git a/packages/stream_feeds/lib/src/state/event/state_event_handler.dart b/packages/stream_feeds/lib/src/state/event/handler/state_event_handler.dart similarity index 100% rename from packages/stream_feeds/lib/src/state/event/state_event_handler.dart rename to packages/stream_feeds/lib/src/state/event/handler/state_event_handler.dart diff --git a/packages/stream_feeds/lib/src/state/event/on_activity_added.dart b/packages/stream_feeds/lib/src/state/event/on_activity_added.dart new file mode 100644 index 00000000..e2fe0c69 --- /dev/null +++ b/packages/stream_feeds/lib/src/state/event/on_activity_added.dart @@ -0,0 +1,71 @@ +import 'package:meta/meta.dart'; + +import '../../models/activity_data.dart'; +import '../../utils/filter.dart'; +import '../insertion_action.dart'; +import '../query/feed_query.dart'; + +/// A callback function that determines how to handle a new activity. +/// +/// This optional callback can be provided to customize insertion behavior when +/// new activities are received. It allows determining how activities should be +/// inserted based on the activity, query, and current user context. +/// +/// The function receives: +/// - [query]: The query that defines the filter and configuration +/// - [activity]: The newly added activity +/// - [currentUserId]: The ID of the current user +/// +/// Returns an [InsertionAction] that determines how the activity should be +/// inserted into the activity list: +/// - [InsertionAction.addToStart]: Add the activity at the beginning of the list +/// - [InsertionAction.addToEnd]: Add the activity at the end of the list +/// - [InsertionAction.ignore]: Do not add the activity to the list +/// +/// ## Example +/// ```dart +/// // Custom handler that adds all activities from followed users to the start +/// InsertionAction customOnNewActivity( +/// FeedQuery query, +/// ActivityData activity, +/// String currentUserId, +/// ) { +/// // Add activities from followed users to the start +/// if (activity.user.id != currentUserId) { +/// return InsertionAction.addToStart; +/// } +/// return InsertionAction.ignore; +/// } +/// +/// final feed = client.feed( +/// group: 'user', +/// id: 'john', +/// onNewActivity: customOnNewActivity, +/// ); +/// ``` +typedef OnNewActivity = InsertionAction Function( + FeedQuery query, + ActivityData activity, + String currentUserId, +); + +/// Default handler for new activities. +/// +/// Adds activities created by the current user to the start of the list if they +/// match the query's filter. Ignores all other activities. +@internal +InsertionAction defaultOnNewActivity( + FeedQuery query, + ActivityData activity, + String currentUserId, +) { + // If the activity was created by the current user and matches the filter, + // add it to the start of the list; otherwise, ignore it. + if (activity.user.id == currentUserId) { + if (activity.matches(query.activityFilter)) { + return InsertionAction.addToStart; + } + } + + return InsertionAction.ignore; +} diff --git a/packages/stream_feeds/lib/src/state/feed.dart b/packages/stream_feeds/lib/src/state/feed.dart index 57094000..78e9e3dc 100644 --- a/packages/stream_feeds/lib/src/state/feed.dart +++ b/packages/stream_feeds/lib/src/state/feed.dart @@ -24,7 +24,8 @@ import '../repository/capabilities_repository.dart'; import '../repository/comments_repository.dart'; import '../repository/feeds_repository.dart'; import '../repository/polls_repository.dart'; -import 'event/feed_event_handler.dart'; +import 'event/handler/feed_event_handler.dart'; +import 'event/on_activity_added.dart'; import 'feed_state.dart'; import 'member_list.dart'; import 'query/feed_query.dart'; @@ -45,6 +46,7 @@ class Feed with Disposable { Feed({ required this.query, required this.currentUserId, + required this.onNewActivity, required this.activitiesRepository, required this.bookmarksRepository, required this.commentsRepository, @@ -73,6 +75,7 @@ class Feed with Disposable { query: query, state: _stateNotifier, currentUserId: currentUserId, + onNewActivity: onNewActivity, capabilitiesRepository: capabilitiesRepository, ); @@ -88,6 +91,7 @@ class Feed with Disposable { final FeedQuery query; final String currentUserId; + final OnNewActivity onNewActivity; final ActivitiesRepository activitiesRepository; final BookmarksRepository bookmarksRepository; final CommentsRepository commentsRepository; diff --git a/packages/stream_feeds/lib/src/state/feed_list.dart b/packages/stream_feeds/lib/src/state/feed_list.dart index d82ae893..3be3f455 100644 --- a/packages/stream_feeds/lib/src/state/feed_list.dart +++ b/packages/stream_feeds/lib/src/state/feed_list.dart @@ -7,7 +7,7 @@ import 'package:stream_core/stream_core.dart'; import '../models/feed_data.dart'; import '../models/query_configuration.dart'; import '../repository/feeds_repository.dart'; -import 'event/feed_list_event_handler.dart'; +import 'event/handler/feed_list_event_handler.dart'; import 'feed_list_state.dart'; import 'query/feeds_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/feed_state.dart b/packages/stream_feeds/lib/src/state/feed_state.dart index 816b26ee..3d26d131 100644 --- a/packages/stream_feeds/lib/src/state/feed_state.dart +++ b/packages/stream_feeds/lib/src/state/feed_state.dart @@ -20,6 +20,7 @@ import '../models/get_or_create_feed_data.dart'; import '../models/mark_activity_data.dart'; import '../models/pagination_data.dart'; import '../models/query_configuration.dart'; +import 'insertion_action.dart'; import 'member_list_state.dart'; import 'query/activities_query.dart'; import 'query/feed_query.dart'; @@ -108,12 +109,24 @@ class FeedStateNotifier extends StateNotifier { } /// Handles updates to the feed state when a new activity is added. - void onActivityAdded(ActivityData activity) { + void onActivityAdded( + ActivityData activity, { + InsertionAction insertionAction = InsertionAction.addToStart, + }) { + final insertAt = switch (insertionAction) { + InsertionAction.addToStart => 0, + InsertionAction.addToEnd => state.activities.length, + InsertionAction.ignore => null, + }; + + // Return early if the activity should be ignored + if (insertAt == null) return; + // Upsert the new activity into the existing activities list - final updatedActivities = state.activities.sortedUpsert( + final updatedActivities = state.activities.upsert( activity, key: (it) => it.id, - compare: activitiesSort.compare, + insertAt: (_) => insertAt, ); state = state.copyWith(activities: updatedActivities); diff --git a/packages/stream_feeds/lib/src/state/follow_list.dart b/packages/stream_feeds/lib/src/state/follow_list.dart index 2bc4db05..a6f7e632 100644 --- a/packages/stream_feeds/lib/src/state/follow_list.dart +++ b/packages/stream_feeds/lib/src/state/follow_list.dart @@ -7,7 +7,7 @@ import 'package:stream_core/stream_core.dart'; import '../models/follow_data.dart'; import '../models/query_configuration.dart'; import '../repository/feeds_repository.dart'; -import 'event/follow_list_event_handler.dart'; +import 'event/handler/follow_list_event_handler.dart'; import 'follow_list_state.dart'; import 'query/follows_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/insertion_action.dart b/packages/stream_feeds/lib/src/state/insertion_action.dart new file mode 100644 index 00000000..ccf26978 --- /dev/null +++ b/packages/stream_feeds/lib/src/state/insertion_action.dart @@ -0,0 +1,13 @@ +/// Defines how an item should be inserted into a list. +/// +/// Used to specify the insertion position when adding new items to collections. +enum InsertionAction { + /// Do not add the item to the list. + ignore, + + /// Add the item at the end of the list. + addToEnd, + + /// Add the item at the beginning of the list. + addToStart, +} diff --git a/packages/stream_feeds/lib/src/state/member_list.dart b/packages/stream_feeds/lib/src/state/member_list.dart index fad23469..388b17c1 100644 --- a/packages/stream_feeds/lib/src/state/member_list.dart +++ b/packages/stream_feeds/lib/src/state/member_list.dart @@ -7,7 +7,7 @@ import 'package:stream_core/stream_core.dart'; import '../models/feed_member_data.dart'; import '../models/query_configuration.dart'; import '../repository/feeds_repository.dart'; -import 'event/member_list_event_handler.dart'; +import 'event/handler/member_list_event_handler.dart'; import 'member_list_state.dart'; import 'query/members_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/poll_list.dart b/packages/stream_feeds/lib/src/state/poll_list.dart index 6616ef8b..e2b5c6c2 100644 --- a/packages/stream_feeds/lib/src/state/poll_list.dart +++ b/packages/stream_feeds/lib/src/state/poll_list.dart @@ -7,7 +7,7 @@ import 'package:stream_core/stream_core.dart'; import '../models/poll_data.dart'; import '../models/query_configuration.dart'; import '../repository/polls_repository.dart'; -import 'event/poll_list_event_handler.dart'; +import 'event/handler/poll_list_event_handler.dart'; import 'poll_list_state.dart'; import 'query/polls_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/state/poll_vote_list.dart b/packages/stream_feeds/lib/src/state/poll_vote_list.dart index b207f3d6..9edbdf4b 100644 --- a/packages/stream_feeds/lib/src/state/poll_vote_list.dart +++ b/packages/stream_feeds/lib/src/state/poll_vote_list.dart @@ -7,7 +7,7 @@ import 'package:stream_core/stream_core.dart'; import '../models/poll_vote_data.dart'; import '../models/query_configuration.dart'; import '../repository/polls_repository.dart'; -import 'event/poll_vote_list_event_handler.dart'; +import 'event/handler/poll_vote_list_event_handler.dart'; import 'poll_vote_list_state.dart'; import 'query/poll_votes_query.dart'; import 'state_notifier_extentions.dart'; diff --git a/packages/stream_feeds/lib/src/utils/filter.dart b/packages/stream_feeds/lib/src/utils/filter.dart index b534c54e..0f792708 100644 --- a/packages/stream_feeds/lib/src/utils/filter.dart +++ b/packages/stream_feeds/lib/src/utils/filter.dart @@ -5,3 +5,11 @@ extension FilterRequestExtension on Filter { /// Converts this [Filter] instance to a request format suitable for API calls. Map toRequest() => toJson(); } + +/// Extension to check if an object [T] matches a given filter. +extension MatchesExtensions on T { + /// Null filter means "no filter", so everything matches. + bool matches(Filter? filter) { + return filter == null || filter.matches(this); + } +} diff --git a/packages/stream_feeds/test/state/feed_test.dart b/packages/stream_feeds/test/state/feed_test.dart index 37c285da..7bd2eb3e 100644 --- a/packages/stream_feeds/test/state/feed_test.dart +++ b/packages/stream_feeds/test/state/feed_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: avoid_redundant_argument_values import 'package:mocktail/mocktail.dart'; +import 'package:stream_feeds/src/utils/filter.dart'; import 'package:stream_feeds/stream_feeds.dart'; import 'package:test/test.dart'; @@ -751,6 +752,7 @@ void main() { fid: feedId.rawValue, activity: createDefaultActivityResponse( id: 'activity-4', + userId: 'luke_skywalker', ).copyWith( type: 'post', // Matches first condition filterTags: ['general'], // Doesn't match second condition @@ -790,6 +792,7 @@ void main() { fid: feedId.rawValue, activity: createDefaultActivityResponse( id: 'activity-4', + userId: 'luke_skywalker', // Doesn't match 'post' activity type ).copyWith(type: 'post'), ), @@ -1805,4 +1808,293 @@ void main() { }, ); }); + + // ============================================================ + // FEATURE: OnNewActivity + // ============================================================ + + group('OnNewActivity', () { + const feedId = FeedId(group: 'user', id: 'john'); + const currentUserId = 'luke_skywalker'; + const otherUserId = 'other_user'; + + final initialActivities = [ + createDefaultActivityResponse(id: 'activity-1', userId: currentUserId), + createDefaultActivityResponse(id: 'activity-2', userId: currentUserId), + createDefaultActivityResponse(id: 'activity-3', userId: otherUserId), + ]; + + feedTest( + 'defaultOnNewActivity - should add current user activity to start when matching filter', + build: (client) => client.feedFromQuery( + FeedQuery( + fid: feedId, + activityFilter: Filter.equal( + ActivitiesFilterField.activityType, + 'post', + ), + ), + ), + setUp: (tester) => tester.getOrCreate( + modifyResponse: (it) => it.copyWith(activities: initialActivities), + ), + body: (tester) async { + expect(tester.feedState.activities, hasLength(3)); + + // Send ActivityAddedEvent from current user matching filter + await tester.emitEvent( + ActivityAddedEvent( + type: EventTypes.activityAdded, + createdAt: DateTime.timestamp(), + custom: const {}, + fid: feedId.rawValue, + activity: createDefaultActivityResponse( + id: 'activity-4', + userId: currentUserId, + type: 'post', + ), + ), + ); + + expect(tester.feedState.activities, hasLength(4)); + expect(tester.feedState.activities.first.id, 'activity-4'); + }, + ); + + feedTest( + 'defaultOnNewActivity - should ignore activity from other user', + build: (client) => client.feedFromQuery( + FeedQuery( + fid: feedId, + activityFilter: Filter.equal( + ActivitiesFilterField.activityType, + 'post', + ), + ), + ), + setUp: (tester) => tester.getOrCreate( + modifyResponse: (it) => it.copyWith(activities: initialActivities), + ), + body: (tester) async { + expect(tester.feedState.activities, hasLength(3)); + + // Send ActivityAddedEvent from other user + await tester.emitEvent( + ActivityAddedEvent( + type: EventTypes.activityAdded, + createdAt: DateTime.timestamp(), + custom: const {}, + fid: feedId.rawValue, + activity: createDefaultActivityResponse( + id: 'activity-4', + userId: otherUserId, + type: 'post', + ), + ), + ); + + expect(tester.feedState.activities, hasLength(3)); + }, + ); + + feedTest( + 'defaultOnNewActivity - should ignore current user activity that does not match filter', + build: (client) => client.feedFromQuery( + FeedQuery( + fid: feedId, + activityFilter: Filter.equal( + ActivitiesFilterField.activityType, + 'post', + ), + ), + ), + setUp: (tester) => tester.getOrCreate( + modifyResponse: (it) => it.copyWith(activities: initialActivities), + ), + body: (tester) async { + expect(tester.feedState.activities, hasLength(3)); + + // Send ActivityAddedEvent from current user but doesn't match filter + await tester.emitEvent( + ActivityAddedEvent( + type: EventTypes.activityAdded, + createdAt: DateTime.timestamp(), + custom: const {}, + fid: feedId.rawValue, + activity: createDefaultActivityResponse( + id: 'activity-4', + userId: currentUserId, + type: 'comment', // Doesn't match 'post' filter + ), + ), + ); + + expect(tester.feedState.activities, hasLength(3)); + }, + ); + + feedTest( + 'custom onNewActivity - should add to start', + build: (client) => client.feedFromQuery( + const FeedQuery(fid: feedId), + onNewActivity: (query, activity, currentUserId) { + // Always add to start + return InsertionAction.addToStart; + }, + ), + setUp: (tester) => tester.getOrCreate( + modifyResponse: (it) => it.copyWith(activities: initialActivities), + ), + body: (tester) async { + expect(tester.feedState.activities, hasLength(3)); + + // Send ActivityAddedEvent from other user + await tester.emitEvent( + ActivityAddedEvent( + type: EventTypes.activityAdded, + createdAt: DateTime.timestamp(), + custom: const {}, + fid: feedId.rawValue, + activity: createDefaultActivityResponse( + id: 'activity-4', + userId: otherUserId, + ), + ), + ); + + expect(tester.feedState.activities, hasLength(4)); + expect(tester.feedState.activities.first.id, 'activity-4'); + }, + ); + + feedTest( + 'custom onNewActivity - should add to end', + build: (client) => client.feedFromQuery( + const FeedQuery(fid: feedId), + onNewActivity: (query, activity, currentUserId) { + // Always add to end + return InsertionAction.addToEnd; + }, + ), + setUp: (tester) => tester.getOrCreate( + modifyResponse: (it) => it.copyWith(activities: initialActivities), + ), + body: (tester) async { + expect(tester.feedState.activities, hasLength(3)); + + // Send ActivityAddedEvent from other user + await tester.emitEvent( + ActivityAddedEvent( + type: EventTypes.activityAdded, + createdAt: DateTime.timestamp(), + custom: const {}, + fid: feedId.rawValue, + activity: createDefaultActivityResponse( + id: 'activity-4', + userId: otherUserId, + ), + ), + ); + + expect(tester.feedState.activities, hasLength(4)); + expect(tester.feedState.activities.last.id, 'activity-4'); + }, + ); + + feedTest( + 'custom onNewActivity - should ignore', + build: (client) => client.feedFromQuery( + const FeedQuery(fid: feedId), + onNewActivity: (query, activity, currentUserId) { + // Always ignore + return InsertionAction.ignore; + }, + ), + setUp: (tester) => tester.getOrCreate( + modifyResponse: (it) => it.copyWith(activities: initialActivities), + ), + body: (tester) async { + expect(tester.feedState.activities, hasLength(3)); + + // Send ActivityAddedEvent from current user + await tester.emitEvent( + ActivityAddedEvent( + type: EventTypes.activityAdded, + createdAt: DateTime.timestamp(), + custom: const {}, + fid: feedId.rawValue, + activity: createDefaultActivityResponse( + id: 'activity-4', + userId: currentUserId, + ), + ), + ); + + expect(tester.feedState.activities, hasLength(3)); + }, + ); + + feedTest( + 'custom onNewActivity - should use query and activity context', + build: (client) => client.feedFromQuery( + FeedQuery( + fid: feedId, + activityFilter: Filter.equal( + ActivitiesFilterField.activityType, + 'post', + ), + ), + onNewActivity: (query, activity, currentUserId) { + // Add activities from other users that match the filter to the end + if (activity.user.id != currentUserId) { + if (activity.matches(query.activityFilter)) { + return InsertionAction.addToEnd; + } + } + return InsertionAction.ignore; + }, + ), + setUp: (tester) => tester.getOrCreate( + modifyResponse: (it) => it.copyWith(activities: initialActivities), + ), + body: (tester) async { + expect(tester.feedState.activities, hasLength(3)); + + // Send ActivityAddedEvent from other user matching filter + await tester.emitEvent( + ActivityAddedEvent( + type: EventTypes.activityAdded, + createdAt: DateTime.timestamp(), + custom: const {}, + fid: feedId.rawValue, + activity: createDefaultActivityResponse( + id: 'activity-4', + userId: otherUserId, + type: 'post', + ), + ), + ); + + expect(tester.feedState.activities, hasLength(4)); + expect(tester.feedState.activities.last.id, 'activity-4'); + + // Send ActivityAddedEvent from other user not matching filter + await tester.emitEvent( + ActivityAddedEvent( + type: EventTypes.activityAdded, + createdAt: DateTime.timestamp(), + custom: const {}, + fid: feedId.rawValue, + activity: createDefaultActivityResponse( + id: 'activity-5', + userId: otherUserId, + type: 'comment', // Doesn't match 'post' filter + ), + ), + ); + + expect(tester.feedState.activities, hasLength(4)); + }, + ); + }); } diff --git a/packages/stream_feeds/test/test_utils/fakes.dart b/packages/stream_feeds/test/test_utils/fakes.dart index 286bebdd..85401b86 100644 --- a/packages/stream_feeds/test/test_utils/fakes.dart +++ b/packages/stream_feeds/test/test_utils/fakes.dart @@ -83,6 +83,7 @@ GetActivityResponse createDefaultGetActivityResponse({ ActivityResponse createDefaultActivityResponse({ String id = 'id', String type = 'post', + String userId = 'user-1', List feeds = const [], PollResponseData? poll, bool hidden = false, @@ -125,7 +126,7 @@ ActivityResponse createDefaultActivityResponse({ type: type, isWatched: isWatched, updatedAt: DateTime(2021, 2, 1), - user: createDefaultUserResponse(), + user: createDefaultUserResponse(id: userId), visibility: ActivityResponseVisibility.public, visibilityTag: null, ); diff --git a/sample_app/lib/screens/user_feed/user_feed_screen.dart b/sample_app/lib/screens/user_feed/user_feed_screen.dart index 48c48703..47c22cb8 100644 --- a/sample_app/lib/screens/user_feed/user_feed_screen.dart +++ b/sample_app/lib/screens/user_feed/user_feed_screen.dart @@ -30,6 +30,11 @@ class _UserFeedScreenState extends State { late final storiesFeed = client.feedFromId( FeedId.stories(client.user.id), + onNewActivity: (query, activity, currentUserId) { + // By default new activities are added to the start of the list, + // but this is not what we want for stories + return InsertionAction.addToEnd; + }, ); late final userFeed = client.feedFromQuery(