Skip to content
Open
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
55 changes: 55 additions & 0 deletions docs/code_snippets/06_03_comments.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,58 @@ Future<void> commentThreading() async {
);
final replies = await replyList.get();
}

Future<void> restrictReplies() async {
// Post an activity that restricts who can reply/comment.
// Options: everyone (default), nobody, peopleIFollow.
// Note: the activity author can always comment regardless of this setting.
await feed.addActivity(
request: const FeedAddActivityRequest(
type: 'post',
text: 'Only my followers can reply to this.',
restrictReplies: AddActivityRequestRestrictReplies.peopleIFollow,
),
);

// To check whether the current user may comment on an activity whose
// restrictReplies is peopleIFollow, the server must enrich the response with
// the author's own_followings. Enable this via enrichmentOptions on the feed
// query — it is not included in the default getOrCreate() response.
final enrichedFeed = client.feedFromQuery(
const FeedQuery(
fid: FeedId(group: 'user', id: 'john'),
enrichmentOptions: EnrichmentOptions(enrichOwnFollowings: true),
),
);
await enrichedFeed.getOrCreate();
final activity = enrichedFeed.state.activities.firstOrNull;
if (activity == null) return;

switch (activity.restrictReplies) {
case ActivityRestrictReplies.everyone:
print('Anyone can comment');
case ActivityRestrictReplies.nobody:
print('Comments are disabled');
case ActivityRestrictReplies.peopleIFollow:
// own_followings lists follow relationships where the source feed is the
// activity author's and the target is the current user's feed. A non-empty
// list means the activity author follows the current user, so they may
// comment.
final ownFollowings = activity.currentFeed?.ownFollowings ?? [];
final canComment = ownFollowings.isNotEmpty;
print('Can current user comment? $canComment');
default: // future/unknown values from the server
print('Unknown restriction: ${activity.restrictReplies}');
}
}

Future<void> updateRestrictReplies() async {
// Update an existing activity's reply restriction after it was created.
// restrictReplies is passed directly on the generated UpdateActivityRequest.
await feed.updateActivity(
id: 'activity-id',
request: const UpdateActivityRequest(
restrictReplies: UpdateActivityRequestRestrictReplies.nobody,
),
);
}
3 changes: 3 additions & 0 deletions packages/stream_feeds/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## Upcoming

### New fields
- Added `restrictReplies` (`ActivityRestrictReplies`) to `ActivityData` to expose the comment-reply restriction on an activity (everyone / nobody / people_i_follow).
- Added `restrictReplies` (`AddActivityRequestRestrictReplies?`) to `FeedAddActivityRequest` so comment restrictions can be set when creating an activity.
- Added `enrichmentOptions` (`EnrichmentOptions?`) to `FeedQuery` so optional server enrichment can be enabled per feed. Use `EnrichmentOptions(enrichOwnFollowings: true)` to receive `ownFollowings` on each activity's feed — required to determine whether the current user may comment when an activity's `restrictReplies` is `people_i_follow`.
- Added `isRead` and `isSeen` fields to `ActivityData` and `AggregatedActivityData` for notification-feed read/seen state.
- Added `friendReactionCount` and `friendReactions` fields to `ActivityData` to expose reactions from friends.
- Added `metrics` field to `ActivityData` for server-side activity metrics (impressions, clicks, etc.).
Expand Down
44 changes: 44 additions & 0 deletions packages/stream_feeds/lib/src/models/activity_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class ActivityData with _$ActivityData {
this.preview = false,
this.reactionCount = 0,
this.reactionGroups = const {},
this.restrictReplies = ActivityRestrictReplies.everyone,
this.score = 0.0,
this.searchData = const {},
this.shareCount = 0,
Expand Down Expand Up @@ -226,6 +227,17 @@ class ActivityData with _$ActivityData {
@override
final Map<String, ReactionGroupData> reactionGroups;

/// Who is allowed to add comments/replies to this activity.
///
/// Controls the comment restriction setting for this activity:
/// - [ActivityRestrictReplies.everyone] (default) — anyone can comment.
/// - [ActivityRestrictReplies.nobody] — comments are disabled.
/// - [ActivityRestrictReplies.peopleIFollow] — only users followed by the
/// activity author may comment. Use [FeedData.ownFollowings] from the
/// current feed to decide whether to show the comment input.
@override
final ActivityRestrictReplies restrictReplies;

/// A relevance or quality score assigned to the activity.
@override
final double score;
Expand Down Expand Up @@ -348,6 +360,7 @@ extension ActivityResponseMapper on ActivityResponse {
reactionGroups: {
for (final entry in reactionGroups.entries) entry.key: entry.value.toModel(),
},
restrictReplies: restrictReplies.toModel(),
score: score,
searchData: searchData,
shareCount: shareCount,
Expand Down Expand Up @@ -613,6 +626,37 @@ extension ActivityDataMutations on ActivityData {
}
}

/// Controls who can add comments/replies to an activity.
///
/// Implemented as an extension type so any future server-side values are
/// handled without requiring a client update.
extension type const ActivityRestrictReplies(String value) implements String {
/// Anyone can comment on the activity (default).
static const everyone = ActivityRestrictReplies('everyone');

/// No one can comment on the activity.
static const nobody = ActivityRestrictReplies('nobody');

/// Only users followed by the activity author can comment.
static const peopleIFollow = ActivityRestrictReplies('people_i_follow');

/// Unknown value received from the API.
static const unknown = ActivityRestrictReplies('unknown');
}

/// Extension function to convert an [ActivityResponseRestrictReplies] to an [ActivityRestrictReplies].
extension ActivityResponseRestrictRepliesMapper on ActivityResponseRestrictReplies {
/// Converts this API restrict-replies enum to the domain [ActivityRestrictReplies].
ActivityRestrictReplies toModel() {
return switch (this) {
ActivityResponseRestrictReplies.everyone => ActivityRestrictReplies.everyone,
ActivityResponseRestrictReplies.nobody => ActivityRestrictReplies.nobody,
ActivityResponseRestrictReplies.peopleIFollow => ActivityRestrictReplies.peopleIFollow,
ActivityResponseRestrictReplies.unknown => ActivityRestrictReplies.unknown,
};
}
}

/// Extension function to convert an [ActivityResponseVisibility] to an [ActivityDataVisibility].
extension ActivityResponseVisibilityMapper on ActivityResponseVisibility {
/// Converts this API visibility enum to a domain [ActivityDataVisibility] extension type.
Expand Down

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

Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class FeedAddActivityRequest with _$FeedAddActivityRequest implements HasAttachm
this.mentionedUserIds,
this.parentId,
this.pollId,
this.restrictReplies,
this.searchData,
this.skipPush,
this.text,
Expand Down Expand Up @@ -107,6 +108,15 @@ class FeedAddActivityRequest with _$FeedAddActivityRequest implements HasAttachm
@override
final String? pollId;

/// Controls who can add comments/replies to this activity.
///
/// Defaults to [AddActivityRequestRestrictReplies.everyone] (no restriction).
/// Use [AddActivityRequestRestrictReplies.nobody] to disable comments, or
/// [AddActivityRequestRestrictReplies.peopleIFollow] to restrict to users
/// followed by the activity author.
@override
final AddActivityRequestRestrictReplies? restrictReplies;

/// Optional search metadata for enhanced discoverability.
@override
final Map<String, Object>? searchData;
Expand Down Expand Up @@ -169,6 +179,7 @@ extension FeedAddActivityRequestMapper on FeedAddActivityRequest {
mentionedUserIds: mentionedUserIds,
parentId: parentId,
pollId: pollId,
restrictReplies: restrictReplies,
searchData: searchData,
skipPush: skipPush,
text: text,
Expand Down

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

22 changes: 22 additions & 0 deletions packages/stream_feeds/lib/src/state/query/feed_query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:stream_core/stream_core.dart';

import '../../generated/api/models.dart';
import '../../models/activity_data.dart';
import '../../models/feed_data.dart';
import '../../models/feed_id.dart';
import '../../models/feed_input_data.dart';
import '../../utils/filter.dart';
Expand Down Expand Up @@ -33,6 +34,7 @@ class FeedQuery with _$FeedQuery {
this.activityNext,
this.activityPrevious,
this.data,
this.enrichmentOptions,
this.externalRanking,
this.followerLimit,
this.followingLimit,
Expand Down Expand Up @@ -68,6 +70,25 @@ class FeedQuery with _$FeedQuery {
@override
final FeedInputData? data;

/// Controls which optional fields the server enriches the response with.
///
/// Pass [EnrichmentOptions] to enable enrichment that is not included by
/// default. For example, set [EnrichmentOptions.enrichOwnFollowings] to
/// `true` to receive [FeedData.ownFollowings] on each activity's current
/// feed, which is required to determine whether the current user may comment
/// when an activity's `restrictReplies` is set to `people_i_follow`.
///
/// ```dart
/// client.feedFromQuery(
/// FeedQuery(
/// fid: feedId,
/// enrichmentOptions: const EnrichmentOptions(enrichOwnFollowings: true),
/// ),
/// );
/// ```
@override
final EnrichmentOptions? enrichmentOptions;

/// Additional data used for ranking activities in the feed.
@override
final Map<String, Object>? externalRanking;
Expand Down Expand Up @@ -107,6 +128,7 @@ extension FeedQueryRequest on FeedQuery {
view: view,
watch: watch,
data: data?.toRequest(),
enrichmentOptions: enrichmentOptions,
externalRanking: externalRanking,
filter: activityFilter?.toRequest(),
followersPagination: followerLimit?.let((it) => PagerRequest(limit: it)),
Expand Down
Loading
Loading