Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions packages/stream_feeds/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Add location filtering support for activities with `ActivitiesFilterField.near` and `ActivitiesFilterField.withinBounds` filter fields.
- Add new activity filter fields: `ActivitiesFilterField.feed` and `ActivitiesFilterField.interestTags`.
- Export previously missing public APIs: models, state objects, and queries.
- Add `ownFollows` field to `FeedData` to store the follow relationships of the current user in the feed.

## 0.4.0
- [BREAKING] Change `queryFollowSuggestions` return type to `List<FeedSuggestionData>`.
Expand Down
32 changes: 32 additions & 0 deletions packages/stream_feeds/lib/src/models/feed_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import '../generated/api/models.dart';
import 'feed_id.dart';
import 'feed_member_data.dart';
import 'follow_data.dart';
import 'user_data.dart';

part 'feed_data.freezed.dart';
Expand Down Expand Up @@ -30,6 +31,7 @@ class FeedData with _$FeedData {
required this.name,
required this.ownCapabilities,
this.ownMembership,
this.ownFollows,
required this.pinCount,
required this.updatedAt,
this.visibility,
Expand Down Expand Up @@ -92,6 +94,10 @@ class FeedData with _$FeedData {
@override
final FeedMemberData? ownMembership;

/// The follow relationships of the current user in the feed.
@override
final List<FollowData>? ownFollows;

/// The number of pinned items in the feed.
@override
final int pinCount;
Expand Down Expand Up @@ -131,10 +137,36 @@ extension FeedResponseMapper on FeedResponse {
name: name,
ownCapabilities: ownCapabilities ?? const [],
ownMembership: ownMembership?.toModel(),
ownFollows: ownFollows?.map((f) => f.toModel()).toList(),
pinCount: pinCount,
updatedAt: updatedAt,
visibility: visibility,
custom: custom,
);
}
}

/// Extension functions for [FeedData] to handle common operations.
extension FeedDataMutations on FeedData {
/// Updates this feed with new data while preserving own data.
///
/// Merges [updated] feed data with this instance, preserving [ownCapabilities],
/// [ownMembership], and [ownFollows] from this instance when not provided. This
/// ensures that user-specific data is not lost when updating from WebSocket events.
///
/// Returns a new [FeedData] instance with the merged data.
FeedData updateWith(
FeedData updated, {
List<FeedOwnCapability>? ownCapabilities,
FeedMemberData? ownMembership,
List<FollowData>? ownFollows,
}) {
return updated.copyWith(
// Preserve own data from the current instance if not provided
// as they may not be reliable from WS events.
ownCapabilities: ownCapabilities ?? this.ownCapabilities,
ownMembership: ownMembership ?? this.ownMembership,
ownFollows: ownFollows ?? this.ownFollows,
);
}
}
53 changes: 32 additions & 21 deletions packages/stream_feeds/lib/src/models/feed_data.freezed.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions packages/stream_feeds/lib/src/state/feed_list_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ class FeedListStateNotifier extends StateNotifier<FeedListState> {

/// Handles updates to a specific feed.
void onFeedUpdated(FeedData feed) {
final updatedFeeds = state.feeds.map((it) {
if (it.fid.rawValue != feed.fid.rawValue) return it;
return feed;
}).toList();
final updatedFeeds = state.feeds.sortedUpsert(
feed,
key: (it) => it.fid.rawValue,
compare: feedsSort.compare,
update: (existing, updated) => existing.updateWith(updated),
);

state = state.copyWith(feeds: updatedFeeds);
}
Expand Down
5 changes: 4 additions & 1 deletion packages/stream_feeds/lib/src/state/feed_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,11 @@ class FeedStateNotifier extends StateNotifier<FeedState> {

/// Handles updates to the feed state when the feed is updated.
void onFeedUpdated(FeedData feed) {
final currentFeed = state.feed;
final updatedFeed = currentFeed?.updateWith(feed) ?? feed;

// Update the feed data in the state
state = state.copyWith(feed: feed);
state = state.copyWith(feed: updatedFeed);
}

/// Handles updates to the feed state when a follow is added.
Expand Down
77 changes: 77 additions & 0 deletions packages/stream_feeds/test/state/feed_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2097,4 +2097,81 @@ void main() {
},
);
});

// ============================================================
// FEATURE: Feed Updated Event
// ============================================================

group('FeedUpdatedEvent', () {
const feedId = FeedId(group: 'user', id: 'john');
const userId = 'luke_skywalker';

feedTest(
'should preserve own fields when feed is updated',
build: (client) => client.feedFromId(feedId),
setUp: (tester) => tester.getOrCreate(
modifyResponse: (it) => it.copyWith(
feed: createDefaultFeedResponse(
id: feedId.id,
groupId: feedId.group,
ownCapabilities: [
FeedOwnCapability.createFeed,
FeedOwnCapability.deleteFeed,
],
ownMembership: createDefaultFeedMemberResponse(
id: userId,
role: 'admin',
),
ownFollows: [
createDefaultFollowResponse(id: 'follow-1'),
createDefaultFollowResponse(id: 'follow-2'),
],
),
),
),
body: (tester) async {
// Verify initial state has own fields
final initialFeed = tester.feedState.feed;
expect(initialFeed, isNotNull);
expect(initialFeed!.ownCapabilities, hasLength(2));
expect(initialFeed.ownMembership, isNotNull);
expect(initialFeed.ownFollows, hasLength(2));

final originalCapabilities = initialFeed.ownCapabilities;
final originalMembership = initialFeed.ownMembership;
final originalFollows = initialFeed.ownFollows;

// Emit FeedUpdatedEvent without own fields
await tester.emitEvent(
FeedUpdatedEvent(
type: EventTypes.feedUpdated,
createdAt: DateTime.timestamp(),
custom: const {},
fid: feedId.rawValue,
feed: createDefaultFeedResponse(
id: feedId.id,
groupId: feedId.group,
).copyWith(
name: 'Updated Name',
description: 'Updated Description',
followerCount: 100,
// Note: ownCapabilities, ownMembership, ownFollows are not included
),
),
);

// Verify own fields are preserved
final updatedFeed = tester.feedState.feed;
expect(updatedFeed, isNotNull);
expect(updatedFeed!.name, equals('Updated Name'));
expect(updatedFeed.description, equals('Updated Description'));
expect(updatedFeed.followerCount, equals(100));

// Own fields should be preserved
expect(updatedFeed.ownCapabilities, equals(originalCapabilities));
expect(updatedFeed.ownMembership, equals(originalMembership));
expect(updatedFeed.ownFollows, equals(originalFollows));
},
);
});
}
6 changes: 6 additions & 0 deletions packages/stream_feeds/test/test_utils/fakes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ FeedResponse createDefaultFeedResponse({
String groupId = 'group',
int followerCount = 0,
int followingCount = 0,
List<FeedOwnCapability>? ownCapabilities,
FeedMemberResponse? ownMembership,
List<FollowResponse>? ownFollows,
}) {
return FeedResponse(
id: id,
Expand All @@ -224,6 +227,9 @@ FeedResponse createDefaultFeedResponse({
memberCount: 0,
pinCount: 0,
updatedAt: DateTime.now(),
ownCapabilities: ownCapabilities,
ownMembership: ownMembership,
ownFollows: ownFollows,
);
}

Expand Down
Loading