Skip to content

Commit e66e076

Browse files
authored
Merge pull request #88 from flutter-news-app-full-source-code/reactor/user-generated-content
Reactor/user generated content
2 parents b3bf1fb + dc18e93 commit e66e076

File tree

15 files changed

+214
-51
lines changed

15 files changed

+214
-51
lines changed

lib/src/enums/enums.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export 'feed_item_density.dart';
2121
export 'feed_item_image_style.dart';
2222
export 'headline_report_reason.dart';
2323
export 'moderation_status.dart';
24+
export 'positive_interaction_type.dart';
2425
export 'push_notification_provider.dart';
2526
export 'push_notification_subscription_delivery_type.dart';
2627
export 'reaction_type.dart';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import 'package:json_annotation/json_annotation.dart';
2+
3+
/// {@template positive_interaction_type}
4+
/// Defines the abstract types of user actions that can be considered positive
5+
/// interactions, potentially contributing to triggering events like the
6+
/// in-app review prompt.
7+
/// {@endtemplate}
8+
@JsonEnum()
9+
enum PositiveInteractionType {
10+
/// The user saved a content item (e.g., a headline).
11+
@JsonValue('saveItem')
12+
saveItem,
13+
14+
/// The user followed an entity (e.g., a topic, source, or country).
15+
@JsonValue('followItem')
16+
followItem,
17+
18+
/// The user shared a content item (e.g., a headline).
19+
@JsonValue('shareContent')
20+
shareContent,
21+
22+
/// The user created a saved filter.
23+
@JsonValue('saveFilter')
24+
saveFilter,
25+
}

lib/src/enums/reportable_entity.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ enum ReportableEntity {
1616
@JsonValue('source')
1717
source,
1818

19-
/// The report is for a user engagement (mainly for engagements with comments).
20-
@JsonValue('engagement')
21-
engagement,
19+
/// The report is for a user engagement comment.
20+
@JsonValue('comment')
21+
comment,
2222
}

lib/src/fixtures/engagements.dart

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,32 @@ List<Engagement> getEngagementsFixturesData({
2727
final user = users[i];
2828
final headline = headlines[index];
2929
final reaction = reactions[index];
30-
// Pair every other reaction with a comment for variety
31-
final comment = index.isEven ? comments[index] : null;
30+
final comment = comments[index];
31+
32+
Reaction? engagementReaction;
33+
Comment? engagementComment;
34+
35+
// Create varied engagement types for realistic test data.
36+
if (index % 3 == 0) {
37+
// Engagement with both reaction and comment
38+
engagementReaction = reaction;
39+
engagementComment = comment;
40+
} else if (index % 3 == 1) {
41+
// Engagement with only a reaction
42+
engagementReaction = reaction;
43+
} else {
44+
// Engagement with only a comment
45+
engagementComment = comment;
46+
}
3247

3348
engagements.add(
3449
Engagement(
3550
id: _engagementIds[index],
3651
userId: user.id,
3752
entityId: headline.id,
3853
entityType: EngageableType.headline,
39-
reaction: reaction,
40-
comment: comment,
54+
reaction: engagementReaction,
55+
comment: engagementComment,
4156
createdAt: referenceTime.subtract(Duration(days: i, hours: j)),
4257
updatedAt: referenceTime.subtract(Duration(days: i, hours: j)),
4358
),

lib/src/fixtures/remote_configs.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,13 @@ final remoteConfigsFixturesData = <RemoteConfig>[
228228
),
229229
appReview: AppReviewConfig(
230230
enabled: true,
231-
// User must perform 5 positive actions (e.g., save headline)
232-
// to become eligible for the review prompt.
233-
positiveInteractionThreshold: 5,
231+
interactionCycleThreshold: 5,
232+
eligiblePositiveInteractions: [
233+
PositiveInteractionType.saveItem,
234+
PositiveInteractionType.followItem,
235+
PositiveInteractionType.shareContent,
236+
PositiveInteractionType.saveFilter,
237+
],
234238
initialPromptCooldownDays: 3,
235239
isPositiveFeedbackFollowUpEnabled: true,
236240
isNegativeFeedbackFollowUpEnabled: true,

lib/src/fixtures/reports.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ List<Report> getReportsFixturesData({DateTime? now}) {
7676
Report(
7777
id: reportIds[i],
7878
reporterUserId: user.id,
79-
entityType: ReportableEntity.engagement,
79+
entityType: ReportableEntity.comment,
8080
entityId: engagementsWithComments[i].id,
8181
reason: commentReasons[i % commentReasons.length].name,
8282
additionalComments: 'This comment is spam.',

lib/src/models/config/app_review_config.dart

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:core/src/enums/positive_interaction_type.dart';
12
import 'package:equatable/equatable.dart';
23
import 'package:json_annotation/json_annotation.dart';
34
import 'package:meta/meta.dart';
@@ -15,8 +16,9 @@ part 'app_review_config.g.dart';
1516
/// ### Architectural Workflow
1617
///
1718
/// 1. **Eligibility**: A user becomes eligible to see the internal prompt after
18-
/// reaching the [positiveInteractionThreshold] of positive actions (e.g.,
19-
/// saving headlines).
19+
/// performing a total number of positive actions, as defined in
20+
/// [eligiblePositiveInteractions]. The required number of actions is set by
21+
/// `interactionCycleThreshold`.
2022
///
2123
/// 2. **Display Logic**: The `FeedDecoratorType.rateApp` decorator's visibility
2224
/// is controlled by the user's `UserFeedDecoratorStatus` for `rateApp`. The
@@ -50,8 +52,9 @@ class AppReviewConfig extends Equatable {
5052
/// {@macro app_review_config}
5153
const AppReviewConfig({
5254
required this.enabled,
53-
required this.positiveInteractionThreshold,
55+
required this.interactionCycleThreshold,
5456
required this.initialPromptCooldownDays,
57+
required this.eligiblePositiveInteractions,
5558
required this.isNegativeFeedbackFollowUpEnabled,
5659
required this.isPositiveFeedbackFollowUpEnabled,
5760
});
@@ -63,14 +66,18 @@ class AppReviewConfig extends Equatable {
6366
/// A master switch to enable or disable the entire app review funnel.
6467
final bool enabled;
6568

66-
/// The number of positive interactions (e.g., saving a headline) required
67-
/// to trigger the initial review prompt.
68-
final int positiveInteractionThreshold;
69+
/// The number of positive interactions required to trigger the review prompt.
70+
/// the user's action counter resets after each prompt cycle.
71+
final int interactionCycleThreshold;
6972

7073
/// The number of days to wait before showing the initial prompt again if the
7174
/// user provides negative feedback.
7275
final int initialPromptCooldownDays;
7376

77+
/// A list of user actions that are considered "positive" and count towards
78+
/// the `interactionCycleThreshold`.
79+
final List<PositiveInteractionType> eligiblePositiveInteractions;
80+
7481
/// A switch to enable or disable the follow-up prompt that asks for a
7582
/// text reason after a user provides negative feedback.
7683
final bool isNegativeFeedbackFollowUpEnabled;
@@ -87,8 +94,9 @@ class AppReviewConfig extends Equatable {
8794
@override
8895
List<Object> get props => [
8996
enabled,
90-
positiveInteractionThreshold,
97+
interactionCycleThreshold,
9198
initialPromptCooldownDays,
99+
eligiblePositiveInteractions,
92100
isNegativeFeedbackFollowUpEnabled,
93101
isPositiveFeedbackFollowUpEnabled,
94102
];
@@ -97,17 +105,20 @@ class AppReviewConfig extends Equatable {
97105
/// replaced with the new values.
98106
AppReviewConfig copyWith({
99107
bool? enabled,
100-
int? positiveInteractionThreshold,
108+
int? interactionCycleThreshold,
101109
int? initialPromptCooldownDays,
110+
List<PositiveInteractionType>? eligiblePositiveInteractions,
102111
bool? isNegativeFeedbackFollowUpEnabled,
103112
bool? isPositiveFeedbackFollowUpEnabled,
104113
}) {
105114
return AppReviewConfig(
106115
enabled: enabled ?? this.enabled,
107-
positiveInteractionThreshold:
108-
positiveInteractionThreshold ?? this.positiveInteractionThreshold,
116+
interactionCycleThreshold:
117+
interactionCycleThreshold ?? this.interactionCycleThreshold,
109118
initialPromptCooldownDays:
110119
initialPromptCooldownDays ?? this.initialPromptCooldownDays,
120+
eligiblePositiveInteractions:
121+
eligiblePositiveInteractions ?? this.eligiblePositiveInteractions,
111122
isNegativeFeedbackFollowUpEnabled:
112123
isNegativeFeedbackFollowUpEnabled ??
113124
this.isNegativeFeedbackFollowUpEnabled,

lib/src/models/config/app_review_config.g.dart

Lines changed: 19 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/src/models/user_generated_content/engagement.dart

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ part 'engagement.g.dart';
88

99
/// {@template engagement}
1010
/// Represents a user's engagement with a specific piece of content.
11-
/// An engagement consists of a mandatory reaction and an optional comment,
12-
/// and is stored as a single document in the database.
11+
///
12+
/// An engagement must contain at least a [reaction] or a [comment], or both.
13+
/// This is enforced by an assertion in the constructor.
1314
/// {@endtemplate}
1415
@immutable
1516
@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true)
@@ -20,11 +21,14 @@ class Engagement extends Equatable {
2021
required this.userId,
2122
required this.entityId,
2223
required this.entityType,
23-
required this.reaction,
2424
required this.createdAt,
2525
required this.updatedAt,
26+
this.reaction,
2627
this.comment,
27-
});
28+
}) : assert(
29+
reaction != null || comment != null,
30+
'An engagement must have at least a reaction or a comment.',
31+
);
2832

2933
/// Creates an [Engagement] from JSON data.
3034
factory Engagement.fromJson(Map<String, dynamic> json) =>
@@ -42,10 +46,10 @@ class Engagement extends Equatable {
4246
/// The type of entity being engaged with.
4347
final EngageableType entityType;
4448

45-
/// The user's reaction. This is a mandatory part of the engagement.
46-
final Reaction reaction;
49+
/// The user's reaction. Can be null if a comment is provided.
50+
final Reaction? reaction;
4751

48-
/// The user's optional comment, provided along with the reaction.
52+
/// The user's comment. Can be null if a reaction is provided.
4953
final Comment? comment;
5054

5155
/// The timestamp when the engagement was created.
@@ -77,7 +81,7 @@ class Engagement extends Equatable {
7781
String? userId,
7882
String? entityId,
7983
EngageableType? entityType,
80-
Reaction? reaction,
84+
ValueWrapper<Reaction?>? reaction,
8185
ValueWrapper<Comment?>? comment,
8286
DateTime? createdAt,
8387
}) {
@@ -86,7 +90,7 @@ class Engagement extends Equatable {
8690
userId: userId ?? this.userId,
8791
entityId: entityId ?? this.entityId,
8892
entityType: entityType ?? this.entityType,
89-
reaction: reaction ?? this.reaction,
93+
reaction: reaction != null ? reaction.value : this.reaction,
9094
comment: comment != null ? comment.value : this.comment,
9195
createdAt: createdAt ?? this.createdAt,
9296
updatedAt: DateTime.now(),

lib/src/models/user_generated_content/engagement.g.dart

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)