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
2 changes: 1 addition & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ command:
shared_preferences: ^2.5.3
state_notifier: ^1.0.0
stream_feeds: ^0.4.0
stream_core: ^0.3.0
stream_core: ^0.3.1
video_player: ^2.10.0
uuid: ^4.5.1

Expand Down
4 changes: 4 additions & 0 deletions packages/stream_feeds/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Upcoming
- [BREAKING] Unified `ThreadedCommentData` into `CommentData` to handle both flat and threaded comments.
- Add support for `enforceUnique` parameter while adding reactions.

## 0.4.0
- [BREAKING] Change `queryFollowSuggestions` return type to `List<FeedSuggestionData>`.
- [BREAKING] Remove `activitySelectorOptions` from `FeedQuery`.
Expand Down
1 change: 0 additions & 1 deletion packages/stream_feeds/lib/src/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,4 @@ export 'models/request/activity_update_comment_request.dart'
show ActivityUpdateCommentRequest;
export 'models/request/feed_add_activity_request.dart'
show FeedAddActivityRequest;
export 'models/threaded_comment_data.dart';
export 'models/user_data.dart';
221 changes: 107 additions & 114 deletions packages/stream_feeds/lib/src/models/activity_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -309,168 +309,161 @@ extension ActivityResponseMapper on ActivityResponse {

/// Extension functions for [ActivityData] to handle common operations.
extension ActivityDataMutations on ActivityData {
/// Adds a comment to the activity, updating the comment count and the list of comments.
/// Updates this activity with new data while preserving own data.
///
/// @param comment The comment to be added.
/// @return A new [ActivityData] instance with the updated comments and comment count.
ActivityData addComment(CommentData comment) {
final updatedComments = comments.upsert(comment, key: (it) => it.id);
/// Merges [updated] activity data with this instance, preserving [ownBookmarks] and
/// [ownReactions] from this instance when not provided. This ensures that user-specific
/// data is not lost when updating from WebSocket events.
///
/// Returns a new [ActivityData] instance with the merged data.
ActivityData updateWith(
ActivityData updated, {
List<BookmarkData>? ownBookmarks,
List<FeedsReactionData>? ownReactions,
}) {
return updated.copyWith(
// Preserve own data from the current instance if not provided
// as they may not be reliable from WS events.
ownBookmarks: ownBookmarks ?? this.ownBookmarks,
ownReactions: ownReactions ?? this.ownReactions,
poll: updated.poll?.let((it) => poll?.updateWith(it) ?? it),
);
}

/// Adds or updates a comment in this activity.
///
/// Updates the comments list by adding or updating [comment]. If the comment already
/// exists, it will be updated. The comment count is automatically recalculated.
///
/// Returns a new [ActivityData] instance with the updated comments and comment count.
ActivityData upsertComment(CommentData comment) {
final currentComments = [...comments];
final updatedComments = currentComments.upsert(comment, key: (it) => it.id);

final difference = updatedComments.length - currentComments.length;
final updatedCommentCount = math.max(0, commentCount + difference);

return copyWith(
comments: updatedComments,
commentCount: math.max(0, commentCount + 1),
commentCount: updatedCommentCount,
);
}

/// Removes a comment from the activity, updating the comment count and the list of comments.
/// Removes a comment from this activity.
///
/// Updates the comments list by removing [comment]. The comment count is automatically
/// recalculated.
///
/// @param comment The comment to be removed.
/// @return A new [ActivityData] instance with the updated comments and comment count.
/// Returns a new [ActivityData] instance with the updated comments and comment count.
ActivityData removeComment(CommentData comment) {
final updatedComments = comments.where((it) {
final currentComments = [...comments];
final updatedComments = currentComments.where((it) {
return it.id != comment.id;
}).toList();

final difference = updatedComments.length - currentComments.length;
final updatedCommentCount = math.max(0, commentCount + difference);

return copyWith(
comments: updatedComments,
commentCount: math.max(0, commentCount - 1),
commentCount: updatedCommentCount,
);
}

/// Adds a bookmark to the activity, updating the own bookmarks and bookmark count.
/// Adds or updates a bookmark in this activity.
///
/// Updates the own bookmarks list by adding or updating [bookmark]. Only adds bookmarks
/// that belong to [currentUserId]. If the bookmark already exists, it will be updated.
///
/// @param bookmark The bookmark to be added.
/// @param currentUserId The ID of the current user, used to determine if the bookmark belongs to
/// them.
/// @return A new [ActivityData] instance with the updated own bookmarks and bookmark count.
ActivityData addBookmark(
/// Returns a new [ActivityData] instance with the updated own bookmarks and bookmark count.
ActivityData upsertBookmark(
BookmarkData bookmark,
String currentUserId,
) {
final updatedOwnBookmarks = switch (bookmark.user.id == currentUserId) {
true => ownBookmarks.upsert(bookmark, key: (it) => it.id),
false => ownBookmarks,
};
final updatedOwnBookmarks = ownBookmarks.let((it) {
if (bookmark.user.id != currentUserId) return it;
return it.upsert(bookmark, key: (it) => it.id);
});

return copyWith(
ownBookmarks: updatedOwnBookmarks,
bookmarkCount: math.max(0, bookmarkCount + 1),
);
return updateWith(bookmark.activity, ownBookmarks: updatedOwnBookmarks);
}

/// Removes a bookmark from the activity, updating the own bookmarks and bookmark count.
/// Removes a bookmark from this activity.
///
/// @param bookmark The bookmark to be deleted.
/// @param currentUserId The ID of the current user, used to determine if the bookmark belongs to
/// them.
/// @return A new [ActivityData] instance with the updated own bookmarks and bookmark count.
/// Updates the own bookmarks list by removing [bookmark]. Only removes bookmarks
/// that belong to [currentUserId].
///
/// Returns a new [ActivityData] instance with the updated own bookmarks and bookmark count.
ActivityData removeBookmark(
BookmarkData bookmark,
String currentUserId,
) {
final updatedOwnBookmarks = switch (bookmark.user.id == currentUserId) {
true => ownBookmarks.where((it) => it.id != bookmark.id).toList(),
false => ownBookmarks,
};
final updatedOwnBookmarks = ownBookmarks.let((it) {
if (bookmark.user.id != currentUserId) return it;
return it.where((it) => it.id != bookmark.id).toList();
});

return copyWith(
ownBookmarks: updatedOwnBookmarks,
bookmarkCount: math.max(0, bookmarkCount - 1),
);
return updateWith(bookmark.activity, ownBookmarks: updatedOwnBookmarks);
}

/// Adds a reaction to the activity, updating the latest reactions, reaction groups,
/// reaction count, and own reactions.
/// Adds or updates a reaction in this activity with unique enforcement.
///
/// Updates the own reactions list by adding or updating [reaction]. Only adds reactions
/// that belong to [currentUserId]. When unique enforcement is enabled, replaces any
/// existing reaction from the same user.
///
/// @param reaction The reaction to be added.
/// @param currentUserId The ID of the current user, used to determine if the reaction belongs to.
/// @return A new [ActivityData] instance with the updated reactions and counts.
ActivityData addReaction(
/// Returns a new [ActivityData] instance with the updated own reactions.
ActivityData upsertUniqueReaction(
ActivityData updatedActivity,
FeedsReactionData reaction,
String currentUserId,
) {
final updatedOwnReactions = switch (reaction.user.id == currentUserId) {
true => ownReactions.upsert(reaction, key: (it) => it.id),
false => ownReactions,
};

final updatedLatestReactions = latestReactions.upsert(
return upsertReaction(
updatedActivity,
reaction,
key: (reaction) => reaction.id,
currentUserId,
enforceUnique: true,
);
}

final reactionGroup = switch (reactionGroups[reaction.type]) {
final existingGroup? => existingGroup,
_ => ReactionGroupData(
count: 1,
firstReactionAt: reaction.createdAt,
lastReactionAt: reaction.createdAt,
),
};

final updatedReactionGroups = {
...reactionGroups,
reaction.type: reactionGroup.increment(reaction.createdAt),
};

final updatedReactionCount = updatedReactionGroups.values.sumOf(
(group) => group.count,
);
/// Adds or updates a reaction in this activity.
///
/// Updates the own reactions list by adding or updating [reaction]. Only adds reactions
/// that belong to [currentUserId]. When [enforceUnique] is true, replaces any existing
/// reaction from the same user; otherwise, allows multiple reactions from the same user.
///
/// Returns a new [ActivityData] instance with the updated own reactions.
ActivityData upsertReaction(
ActivityData updatedActivity,
FeedsReactionData reaction,
String currentUserId, {
bool enforceUnique = false,
}) {
final updatedOwnReactions = ownReactions.let((it) {
if (reaction.user.id != currentUserId) return it;
return it.upsertReaction(reaction, enforceUnique: enforceUnique);
});

return copyWith(
ownReactions: updatedOwnReactions,
latestReactions: updatedLatestReactions,
reactionGroups: updatedReactionGroups,
reactionCount: updatedReactionCount,
);
return updateWith(updatedActivity, ownReactions: updatedOwnReactions);
}

/// Removes a reaction from the activity, updating the latest reactions, reaction groups,
/// reaction count, and own reactions.
/// Removes a reaction from this activity.
///
/// Updates the own reactions list by removing [reaction]. Only removes reactions
/// that belong to [currentUserId].
///
/// @param reaction The reaction to be removed.
/// @param currentUserId The ID of the current user, used to determine if the reaction belongs to.
/// @return A new [ActivityData] instance with the updated reactions and counts.
/// Returns a new [ActivityData] instance with the updated own reactions.
ActivityData removeReaction(
ActivityData updatedActivity,
FeedsReactionData reaction,
String currentUserId,
) {
final updatedOwnReactions = switch (reaction.user.id == currentUserId) {
true => ownReactions.where((it) => it.id != reaction.id).toList(),
false => ownReactions,
};

final updatedLatestReactions = latestReactions.where((it) {
return it.id != reaction.id;
}).toList();

final updatedReactionGroups = {...reactionGroups};
final reactionGroup = updatedReactionGroups.remove(reaction.type);

if (reactionGroup == null) {
// If there is no reaction group for this type, just update latest and own reactions.
// Note: This is only a hypothetical case, as we should always have a reaction group.
return copyWith(
latestReactions: updatedLatestReactions,
ownReactions: updatedOwnReactions,
);
}

final updatedReactionGroup = reactionGroup.decrement(reaction.createdAt);
if (updatedReactionGroup.count > 0) {
updatedReactionGroups[reaction.type] = updatedReactionGroup;
}

final updatedReactionCount = updatedReactionGroups.values.sumOf(
(group) => group.count,
);
final updatedOwnReactions = ownReactions.let((it) {
if (reaction.user.id != currentUserId) return it;
return it.where((it) => it.id != reaction.id).toList();
});

return copyWith(
ownReactions: updatedOwnReactions,
latestReactions: updatedLatestReactions,
reactionGroups: updatedReactionGroups,
reactionCount: updatedReactionCount,
);
return updateWith(updatedActivity, ownReactions: updatedOwnReactions);
}
}

Expand Down
Loading