From c30043a0467bc45e9b8555a13213be388a4dfe67 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 03:33:04 +0100 Subject: [PATCH 01/17] fix(engagement): make reaction or comment optional but not both - Update Engagement class to allow null reaction or comment, but not both - Add assertion in constructor to enforce at least one must be provided - Update copyWith method to handle nullable reaction - Adjust documentation to reflect new behavior --- .../user_generated_content/engagement.dart | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/src/models/user_generated_content/engagement.dart b/lib/src/models/user_generated_content/engagement.dart index cb99bc10..744f156b 100644 --- a/lib/src/models/user_generated_content/engagement.dart +++ b/lib/src/models/user_generated_content/engagement.dart @@ -8,8 +8,9 @@ part 'engagement.g.dart'; /// {@template engagement} /// Represents a user's engagement with a specific piece of content. -/// An engagement consists of a mandatory reaction and an optional comment, -/// and is stored as a single document in the database. +/// +/// An engagement must contain at least a [reaction] or a [comment], or both. +/// This is enforced by an assertion in the constructor. /// {@endtemplate} @immutable @JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) @@ -20,11 +21,14 @@ class Engagement extends Equatable { required this.userId, required this.entityId, required this.entityType, - required this.reaction, required this.createdAt, required this.updatedAt, + this.reaction, this.comment, - }); + }) : assert( + reaction != null || comment != null, + 'An engagement must have at least a reaction or a comment.', + ); /// Creates an [Engagement] from JSON data. factory Engagement.fromJson(Map json) => @@ -42,10 +46,10 @@ class Engagement extends Equatable { /// The type of entity being engaged with. final EngageableType entityType; - /// The user's reaction. This is a mandatory part of the engagement. - final Reaction reaction; + /// The user's reaction. Can be null if a comment is provided. + final Reaction? reaction; - /// The user's optional comment, provided along with the reaction. + /// The user's comment. Can be null if a reaction is provided. final Comment? comment; /// The timestamp when the engagement was created. @@ -77,7 +81,7 @@ class Engagement extends Equatable { String? userId, String? entityId, EngageableType? entityType, - Reaction? reaction, + ValueWrapper? reaction, ValueWrapper? comment, DateTime? createdAt, }) { @@ -86,7 +90,7 @@ class Engagement extends Equatable { userId: userId ?? this.userId, entityId: entityId ?? this.entityId, entityType: entityType ?? this.entityType, - reaction: reaction ?? this.reaction, + reaction: reaction != null ? reaction.value : this.reaction, comment: comment != null ? comment.value : this.comment, createdAt: createdAt ?? this.createdAt, updatedAt: DateTime.now(), From 9f5a3f74209cc0dd10fdb89352458e4631c3312f Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 03:33:39 +0100 Subject: [PATCH 02/17] test: enhance engagement fixtures with varied reaction/comment combinations - Introduce diverse engagement scenarios in fixtures: - Both reaction and comment - Reaction only - Comment only - Replace uniform pairing with a more realistic distribution --- lib/src/fixtures/engagements.dart | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/src/fixtures/engagements.dart b/lib/src/fixtures/engagements.dart index 55d1c4f3..87092b71 100644 --- a/lib/src/fixtures/engagements.dart +++ b/lib/src/fixtures/engagements.dart @@ -27,8 +27,23 @@ List getEngagementsFixturesData({ final user = users[i]; final headline = headlines[index]; final reaction = reactions[index]; - // Pair every other reaction with a comment for variety - final comment = index.isEven ? comments[index] : null; + final comment = comments[index]; + + Reaction? engagementReaction; + Comment? engagementComment; + + // Create varied engagement types for realistic test data. + if (index % 3 == 0) { + // Engagement with both reaction and comment + engagementReaction = reaction; + engagementComment = comment; + } else if (index % 3 == 1) { + // Engagement with only a reaction + engagementReaction = reaction; + } else { + // Engagement with only a comment + engagementComment = comment; + } engagements.add( Engagement( @@ -36,8 +51,8 @@ List getEngagementsFixturesData({ userId: user.id, entityId: headline.id, entityType: EngageableType.headline, - reaction: reaction, - comment: comment, + reaction: engagementReaction, + comment: engagementComment, createdAt: referenceTime.subtract(Duration(days: i, hours: j)), updatedAt: referenceTime.subtract(Duration(days: i, hours: j)), ), From 68a560d0b19fd4efb12cc776df17bc52af7bdcce Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 03:33:59 +0100 Subject: [PATCH 03/17] build(serialization): sync --- .../models/user_generated_content/engagement.g.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/models/user_generated_content/engagement.g.dart b/lib/src/models/user_generated_content/engagement.g.dart index 2c5c7755..bb8f3a54 100644 --- a/lib/src/models/user_generated_content/engagement.g.dart +++ b/lib/src/models/user_generated_content/engagement.g.dart @@ -16,10 +16,6 @@ Engagement _$EngagementFromJson(Map json) => 'entityType', (v) => $enumDecode(_$EngageableTypeEnumMap, v), ), - reaction: $checkedConvert( - 'reaction', - (v) => Reaction.fromJson(v as Map), - ), createdAt: $checkedConvert( 'createdAt', (v) => dateTimeFromJson(v as String?), @@ -28,6 +24,11 @@ Engagement _$EngagementFromJson(Map json) => 'updatedAt', (v) => dateTimeFromJson(v as String?), ), + reaction: $checkedConvert( + 'reaction', + (v) => + v == null ? null : Reaction.fromJson(v as Map), + ), comment: $checkedConvert( 'comment', (v) => v == null ? null : Comment.fromJson(v as Map), @@ -42,7 +43,7 @@ Map _$EngagementToJson(Engagement instance) => 'userId': instance.userId, 'entityId': instance.entityId, 'entityType': _$EngageableTypeEnumMap[instance.entityType]!, - 'reaction': instance.reaction.toJson(), + 'reaction': instance.reaction?.toJson(), 'comment': instance.comment?.toJson(), 'createdAt': dateTimeToJson(instance.createdAt), 'updatedAt': dateTimeToJson(instance.updatedAt), From 0ceec9b21f7bf3014c62a36d97e58a0e3f1725ca Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 03:35:12 +0100 Subject: [PATCH 04/17] fix(engagement): handle comment-only and reaction-only engagements - Update Engagement model to support instances with only comment or only reaction - Add new test cases for comment-only and reaction-only scenarios - Modify existing tests to accommodate the new model structure - Ensure JSON serialization and deserialization work correctly for all scenarios --- .../user_generated_content/engagements.dart | 167 ++++++++++++++++++ .../engagement_test.dart | 44 ++++- 2 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 lib/src/models/user_generated_content/engagements.dart diff --git a/lib/src/models/user_generated_content/engagements.dart b/lib/src/models/user_generated_content/engagements.dart new file mode 100644 index 00000000..87092b71 --- /dev/null +++ b/lib/src/models/user_generated_content/engagements.dart @@ -0,0 +1,167 @@ +import 'package:core/core.dart'; + +/// Generates a list of predefined engagements for fixture data. +/// +/// This function can be configured to generate data in either English or +/// Arabic. It pairs reactions with comments to create realistic engagement +/// scenarios. +List getEngagementsFixturesData({ + String languageCode = 'en', + DateTime? now, +}) { + final engagements = []; + final users = usersFixturesData.take(10).toList(); + final headlines = getHeadlinesFixturesData( + languageCode: languageCode, + ).take(100).toList(); + final reactions = reactionsFixturesData; + final comments = getHeadlineCommentsFixturesData( + languageCode: languageCode, + now: now, + ); + final referenceTime = now ?? DateTime.now(); + + for (var i = 0; i < 10; i++) { + for (var j = 0; j < 10; j++) { + final index = i * 10 + j; + final user = users[i]; + final headline = headlines[index]; + final reaction = reactions[index]; + final comment = comments[index]; + + Reaction? engagementReaction; + Comment? engagementComment; + + // Create varied engagement types for realistic test data. + if (index % 3 == 0) { + // Engagement with both reaction and comment + engagementReaction = reaction; + engagementComment = comment; + } else if (index % 3 == 1) { + // Engagement with only a reaction + engagementReaction = reaction; + } else { + // Engagement with only a comment + engagementComment = comment; + } + + engagements.add( + Engagement( + id: _engagementIds[index], + userId: user.id, + entityId: headline.id, + entityType: EngageableType.headline, + reaction: engagementReaction, + comment: engagementComment, + createdAt: referenceTime.subtract(Duration(days: i, hours: j)), + updatedAt: referenceTime.subtract(Duration(days: i, hours: j)), + ), + ); + } + } + + return engagements; +} + +const _engagementIds = [ + kEngagementId1, + kEngagementId2, + kEngagementId3, + kEngagementId4, + kEngagementId5, + kEngagementId6, + kEngagementId7, + kEngagementId8, + kEngagementId9, + kEngagementId10, + kEngagementId11, + kEngagementId12, + kEngagementId13, + kEngagementId14, + kEngagementId15, + kEngagementId16, + kEngagementId17, + kEngagementId18, + kEngagementId19, + kEngagementId20, + kEngagementId21, + kEngagementId22, + kEngagementId23, + kEngagementId24, + kEngagementId25, + kEngagementId26, + kEngagementId27, + kEngagementId28, + kEngagementId29, + kEngagementId30, + kEngagementId31, + kEngagementId32, + kEngagementId33, + kEngagementId34, + kEngagementId35, + kEngagementId36, + kEngagementId37, + kEngagementId38, + kEngagementId39, + kEngagementId40, + kEngagementId41, + kEngagementId42, + kEngagementId43, + kEngagementId44, + kEngagementId45, + kEngagementId46, + kEngagementId47, + kEngagementId48, + kEngagementId49, + kEngagementId50, + kEngagementId51, + kEngagementId52, + kEngagementId53, + kEngagementId54, + kEngagementId55, + kEngagementId56, + kEngagementId57, + kEngagementId58, + kEngagementId59, + kEngagementId60, + kEngagementId61, + kEngagementId62, + kEngagementId63, + kEngagementId64, + kEngagementId65, + kEngagementId66, + kEngagementId67, + kEngagementId68, + kEngagementId69, + kEngagementId70, + kEngagementId71, + kEngagementId72, + kEngagementId73, + kEngagementId74, + kEngagementId75, + kEngagementId76, + kEngagementId77, + kEngagementId78, + kEngagementId79, + kEngagementId80, + kEngagementId81, + kEngagementId82, + kEngagementId83, + kEngagementId84, + kEngagementId85, + kEngagementId86, + kEngagementId87, + kEngagementId88, + kEngagementId89, + kEngagementId90, + kEngagementId91, + kEngagementId92, + kEngagementId93, + kEngagementId94, + kEngagementId95, + kEngagementId96, + kEngagementId97, + kEngagementId98, + kEngagementId99, + kEngagementId100, +]; diff --git a/test/src/models/user_generated_content/engagement_test.dart b/test/src/models/user_generated_content/engagement_test.dart index fdd36f68..471b8677 100644 --- a/test/src/models/user_generated_content/engagement_test.dart +++ b/test/src/models/user_generated_content/engagement_test.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; import 'package:test/test.dart'; +import 'package:core/src/models/user_generated_content/report.dart'; void main() { group('Engagement', () { @@ -15,17 +16,28 @@ void main() { expect(engagementFixture, isA()); }); - test('returns correct instance with populated comment', () { - // The first fixture item should have a comment + test('returns correct instance with comment and reaction', () { + // The first fixture item should have a comment and a reaction expect(engagementFixture.comment, isNotNull); + expect(engagementFixture.reaction, isNotNull); }); - test('returns correct instance with null comment', () { - // The second fixture item should have a null comment + test('returns correct instance with reaction only', () { + // The second fixture item should have a reaction but no comment final engagementWithoutComment = getEngagementsFixturesData( now: now, )[1]; expect(engagementWithoutComment.comment, isNull); + expect(engagementWithoutComment.reaction, isNotNull); + }); + + test('returns correct instance with comment only', () { + // The third fixture item should have a comment but no reaction + final engagementWithoutReaction = getEngagementsFixturesData( + now: now, + )[2]; + expect(engagementWithoutReaction.comment, isNotNull); + expect(engagementWithoutReaction.reaction, isNull); }); }); @@ -36,7 +48,7 @@ void main() { expect(result, equals(engagementFixture)); }); - test('round trip with null comment', () { + test('round trip with null comment (reaction only)', () { final engagementWithoutComment = getEngagementsFixturesData( now: now, )[1]; @@ -44,6 +56,15 @@ void main() { final result = Engagement.fromJson(json); expect(result, equals(engagementWithoutComment)); }); + + test('round trip with null reaction (comment only)', () { + final engagementWithCommentOnly = getEngagementsFixturesData( + now: now, + )[2]; + final json = engagementWithCommentOnly.toJson(); + final result = Engagement.fromJson(json); + expect(result, equals(engagementWithCommentOnly)); + }); }); group('copyWith', () { @@ -51,7 +72,7 @@ void main() { final newReaction = reactionsFixturesData[2]; final updatedEngagement = engagementFixture.copyWith( - reaction: newReaction, + reaction: ValueWrapper(newReaction), ); expect(updatedEngagement.reaction, newReaction); @@ -68,6 +89,17 @@ void main() { ); }); + test('returns a new instance with a null reaction', () { + // Start with a fixture that has a reaction. + final updatedEngagement = engagementFixture.copyWith( + reaction: const ValueWrapper(null), + ); + + expect(updatedEngagement.reaction, isNull); + // Verify other fields remain unchanged + expect(updatedEngagement.id, engagementFixture.id); + }); + test('returns a new instance with an updated comment', () { final newComment = Comment( language: languagesFixturesData.first, From d7e04bcabc6c2a0239910d718b429a4bb9722554 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:05:38 +0100 Subject: [PATCH 05/17] feat(enums): add positive interaction type enum - Define new enum for user actions considered positive interactions - Includes actions like saving content, following entities, sharing content, and creating saved filters - Prepares for implementing in-app review prompt functionality --- lib/src/enums/positive_interaction_type.dart | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 lib/src/enums/positive_interaction_type.dart diff --git a/lib/src/enums/positive_interaction_type.dart b/lib/src/enums/positive_interaction_type.dart new file mode 100644 index 00000000..318bb828 --- /dev/null +++ b/lib/src/enums/positive_interaction_type.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// {@template positive_interaction_type} +/// Defines the abstract types of user actions that can be considered positive +/// interactions, potentially contributing to triggering events like the +/// in-app review prompt. +/// {@endtemplate} +@JsonEnum() +enum PositiveInteractionType { + /// The user saved a content item (e.g., a headline). + @JsonValue('saveItem') + saveItem, + + /// The user followed an entity (e.g., a topic, source, or country). + @JsonValue('followItem') + followItem, + + /// The user shared a content item (e.g., a headline). + @JsonValue('shareContent') + shareContent, + + /// The user created a saved filter. + @JsonValue('saveFilter') + saveFilter, +} From bedf3793df69642b556d319b9070edfd37f5fd01 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:06:08 +0100 Subject: [PATCH 06/17] chore: barrels --- lib/src/enums/enums.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/enums/enums.dart b/lib/src/enums/enums.dart index e3cb9ec6..4a6b09eb 100644 --- a/lib/src/enums/enums.dart +++ b/lib/src/enums/enums.dart @@ -21,6 +21,7 @@ export 'feed_item_density.dart'; export 'feed_item_image_style.dart'; export 'headline_report_reason.dart'; export 'moderation_status.dart'; +export 'positive_interaction_type.dart'; export 'push_notification_provider.dart'; export 'push_notification_subscription_delivery_type.dart'; export 'reaction_type.dart'; From dddbfafcbacd5cfc0231ed0114bd7dcbd820a462 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:07:01 +0100 Subject: [PATCH 07/17] refactor(remote_configs): update app review configuration - Replace positiveInteractionThreshold with interactionCycleThreshold - Add eligiblePositiveInteractions list with specific interaction types - Maintain other existing configuration values --- lib/src/fixtures/remote_configs.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/src/fixtures/remote_configs.dart b/lib/src/fixtures/remote_configs.dart index 973274f5..d2080489 100644 --- a/lib/src/fixtures/remote_configs.dart +++ b/lib/src/fixtures/remote_configs.dart @@ -228,9 +228,13 @@ final remoteConfigsFixturesData = [ ), appReview: AppReviewConfig( enabled: true, - // User must perform 5 positive actions (e.g., save headline) - // to become eligible for the review prompt. - positiveInteractionThreshold: 5, + interactionCycleThreshold: 5, + eligiblePositiveInteractions: [ + PositiveInteractionType.saveItem, + PositiveInteractionType.followItem, + PositiveInteractionType.shareContent, + PositiveInteractionType.saveFilter, + ], initialPromptCooldownDays: 3, isPositiveFeedbackFollowUpEnabled: true, isNegativeFeedbackFollowUpEnabled: true, From ac6a512c806465d906f0bbfcaae2ec8d4b0388fe Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:08:39 +0100 Subject: [PATCH 08/17] feat(app_review): enhance eligibility criteria for app review prompt - Rename 'positiveInteractionThreshold' to 'interactionCycleThreshold' - Add 'eligiblePositiveInteractions' to specify counted user actions - Update documentation to reflect new eligibility requirements --- lib/src/models/config/app_review_config.dart | 31 +++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/src/models/config/app_review_config.dart b/lib/src/models/config/app_review_config.dart index 65306b79..b081d762 100644 --- a/lib/src/models/config/app_review_config.dart +++ b/lib/src/models/config/app_review_config.dart @@ -1,3 +1,4 @@ +import 'package:core/src/enums/positive_interaction_type.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; @@ -15,8 +16,9 @@ part 'app_review_config.g.dart'; /// ### Architectural Workflow /// /// 1. **Eligibility**: A user becomes eligible to see the internal prompt after -/// reaching the [positiveInteractionThreshold] of positive actions (e.g., -/// saving headlines). +/// performing a total number of positive actions, as defined in +/// [eligiblePositiveInteractions]. The required number of actions is set by +/// `interactionCycleThreshold`. /// /// 2. **Display Logic**: The `FeedDecoratorType.rateApp` decorator's visibility /// is controlled by the user's `UserFeedDecoratorStatus` for `rateApp`. The @@ -50,8 +52,9 @@ class AppReviewConfig extends Equatable { /// {@macro app_review_config} const AppReviewConfig({ required this.enabled, - required this.positiveInteractionThreshold, + required this.interactionCycleThreshold, required this.initialPromptCooldownDays, + required this.eligiblePositiveInteractions, required this.isNegativeFeedbackFollowUpEnabled, required this.isPositiveFeedbackFollowUpEnabled, }); @@ -63,14 +66,18 @@ class AppReviewConfig extends Equatable { /// A master switch to enable or disable the entire app review funnel. final bool enabled; - /// The number of positive interactions (e.g., saving a headline) required - /// to trigger the initial review prompt. - final int positiveInteractionThreshold; + /// The number of positive interactions required to trigger the review prompt. + /// the user's action counter resets after each prompt cycle. + final int interactionCycleThreshold; /// The number of days to wait before showing the initial prompt again if the /// user provides negative feedback. final int initialPromptCooldownDays; + /// A list of user actions that are considered "positive" and count towards + /// the `interactionCycleThreshold`. + final List eligiblePositiveInteractions; + /// A switch to enable or disable the follow-up prompt that asks for a /// text reason after a user provides negative feedback. final bool isNegativeFeedbackFollowUpEnabled; @@ -87,8 +94,9 @@ class AppReviewConfig extends Equatable { @override List get props => [ enabled, - positiveInteractionThreshold, + interactionCycleThreshold, initialPromptCooldownDays, + eligiblePositiveInteractions, isNegativeFeedbackFollowUpEnabled, isPositiveFeedbackFollowUpEnabled, ]; @@ -97,17 +105,20 @@ class AppReviewConfig extends Equatable { /// replaced with the new values. AppReviewConfig copyWith({ bool? enabled, - int? positiveInteractionThreshold, + int? interactionCycleThreshold, int? initialPromptCooldownDays, + List? eligiblePositiveInteractions, bool? isNegativeFeedbackFollowUpEnabled, bool? isPositiveFeedbackFollowUpEnabled, }) { return AppReviewConfig( enabled: enabled ?? this.enabled, - positiveInteractionThreshold: - positiveInteractionThreshold ?? this.positiveInteractionThreshold, + interactionCycleThreshold: + interactionCycleThreshold ?? this.interactionCycleThreshold, initialPromptCooldownDays: initialPromptCooldownDays ?? this.initialPromptCooldownDays, + eligiblePositiveInteractions: + eligiblePositiveInteractions ?? this.eligiblePositiveInteractions, isNegativeFeedbackFollowUpEnabled: isNegativeFeedbackFollowUpEnabled ?? this.isNegativeFeedbackFollowUpEnabled, From 211a251a8b1a2e5d2de9408d581996a08276b13b Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:08:56 +0100 Subject: [PATCH 09/17] build(serialization): sync --- .../models/config/app_review_config.g.dart | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/src/models/config/app_review_config.g.dart b/lib/src/models/config/app_review_config.g.dart index 49c7c1fe..9d7f26a2 100644 --- a/lib/src/models/config/app_review_config.g.dart +++ b/lib/src/models/config/app_review_config.g.dart @@ -10,14 +10,20 @@ AppReviewConfig _$AppReviewConfigFromJson(Map json) => $checkedCreate('AppReviewConfig', json, ($checkedConvert) { final val = AppReviewConfig( enabled: $checkedConvert('enabled', (v) => v as bool), - positiveInteractionThreshold: $checkedConvert( - 'positiveInteractionThreshold', + interactionCycleThreshold: $checkedConvert( + 'interactionCycleThreshold', (v) => (v as num).toInt(), ), initialPromptCooldownDays: $checkedConvert( 'initialPromptCooldownDays', (v) => (v as num).toInt(), ), + eligiblePositiveInteractions: $checkedConvert( + 'eligiblePositiveInteractions', + (v) => (v as List) + .map((e) => $enumDecode(_$PositiveInteractionTypeEnumMap, e)) + .toList(), + ), isNegativeFeedbackFollowUpEnabled: $checkedConvert( 'isNegativeFeedbackFollowUpEnabled', (v) => v as bool, @@ -33,10 +39,20 @@ AppReviewConfig _$AppReviewConfigFromJson(Map json) => Map _$AppReviewConfigToJson(AppReviewConfig instance) => { 'enabled': instance.enabled, - 'positiveInteractionThreshold': instance.positiveInteractionThreshold, + 'interactionCycleThreshold': instance.interactionCycleThreshold, 'initialPromptCooldownDays': instance.initialPromptCooldownDays, + 'eligiblePositiveInteractions': instance.eligiblePositiveInteractions + .map((e) => _$PositiveInteractionTypeEnumMap[e]!) + .toList(), 'isNegativeFeedbackFollowUpEnabled': instance.isNegativeFeedbackFollowUpEnabled, 'isPositiveFeedbackFollowUpEnabled': instance.isPositiveFeedbackFollowUpEnabled, }; + +const _$PositiveInteractionTypeEnumMap = { + PositiveInteractionType.saveItem: 'saveItem', + PositiveInteractionType.followItem: 'followItem', + PositiveInteractionType.shareContent: 'shareContent', + PositiveInteractionType.saveFilter: 'saveFilter', +}; From 3ab9876af20db4bc78c2cbbb66a8b3343c319caf Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:09:20 +0100 Subject: [PATCH 10/17] test(core): add PositiveInteractionType enum tests - Add unit tests for PositiveInteractionType enum - Verify correct number of enum values - Test serialization and deserialization from string values - Ensure proper error handling for invalid string values --- .../enums/positive_interaction_type_test.dart | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 test/src/enums/positive_interaction_type_test.dart diff --git a/test/src/enums/positive_interaction_type_test.dart b/test/src/enums/positive_interaction_type_test.dart new file mode 100644 index 00000000..fe46dce2 --- /dev/null +++ b/test/src/enums/positive_interaction_type_test.dart @@ -0,0 +1,49 @@ +import 'package:core/src/enums/positive_interaction_type.dart'; +import 'package:test/test.dart'; + +void main() { + group('PositiveInteractionType', () { + test('has correct number of values', () { + expect(PositiveInteractionType.values.length, 4); + }); + + group('serialization', () { + test('uses correct string values for json serialization', () { + // This test verifies that the enum's string representation, + // which is used by json_serializable, matches the expected value. + expect(PositiveInteractionType.saveItem.name, 'saveItem'); + expect(PositiveInteractionType.followItem.name, 'followItem'); + expect(PositiveInteractionType.shareContent.name, 'shareContent'); + expect(PositiveInteractionType.saveFilter.name, 'saveFilter'); + }); + + test('can be created from string value for json deserialization', () { + // This test verifies that the enum can be created from its + // string representation, mimicking json_serializable's behavior. + expect( + PositiveInteractionType.values.byName('saveItem'), + PositiveInteractionType.saveItem, + ); + expect( + PositiveInteractionType.values.byName('followItem'), + PositiveInteractionType.followItem, + ); + expect( + PositiveInteractionType.values.byName('shareContent'), + PositiveInteractionType.shareContent, + ); + expect( + PositiveInteractionType.values.byName('saveFilter'), + PositiveInteractionType.saveFilter, + ); + }); + + test('throws ArgumentError for invalid string value', () { + expect( + () => PositiveInteractionType.values.byName('invalid_interaction'), + throwsA(isA()), + ); + }); + }); + }); +} From db79e9ae2d301c3fb1f6aa2bab2f69316b47cce7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:09:37 +0100 Subject: [PATCH 11/17] test(app_review_config): update tests for eligiblePositiveInteractions - Add test for non-empty eligiblePositiveInteractions in fixture - Update property name from positiveInteractionThreshold to interactionCycleThreshold - Add test for eligiblePositiveInteractions in copyWith method --- test/src/models/config/app_review_config_test.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/src/models/config/app_review_config_test.dart b/test/src/models/config/app_review_config_test.dart index 2e66e17c..6afeab18 100644 --- a/test/src/models/config/app_review_config_test.dart +++ b/test/src/models/config/app_review_config_test.dart @@ -12,6 +12,7 @@ void main() { expect(appReviewConfigFixture.enabled, isTrue); expect(appReviewConfigFixture.isNegativeFeedbackFollowUpEnabled, isTrue); expect(appReviewConfigFixture.isPositiveFeedbackFollowUpEnabled, isTrue); + expect(appReviewConfigFixture.eligiblePositiveInteractions, isNotEmpty); }); test('supports value equality', () { @@ -33,13 +34,18 @@ void main() { test('copyWith creates a copy with updated values', () { final updatedConfig = appReviewConfigFixture.copyWith( enabled: false, - positiveInteractionThreshold: 10, + interactionCycleThreshold: 10, + eligiblePositiveInteractions: const [PositiveInteractionType.saveItem], isNegativeFeedbackFollowUpEnabled: false, isPositiveFeedbackFollowUpEnabled: false, ); expect(updatedConfig.enabled, isFalse); - expect(updatedConfig.positiveInteractionThreshold, 10); + expect(updatedConfig.interactionCycleThreshold, 10); + expect( + updatedConfig.eligiblePositiveInteractions, + equals([PositiveInteractionType.saveItem]), + ); expect(updatedConfig.isNegativeFeedbackFollowUpEnabled, isFalse); expect(updatedConfig.isPositiveFeedbackFollowUpEnabled, isFalse); expect(updatedConfig, isNot(equals(appReviewConfigFixture))); From 5a80b0dc1d9442329403eef5f0a081259d5a4ef6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:09:59 +0100 Subject: [PATCH 12/17] style: format --- test/src/models/user_generated_content/engagement_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/src/models/user_generated_content/engagement_test.dart b/test/src/models/user_generated_content/engagement_test.dart index 471b8677..ed01c6fb 100644 --- a/test/src/models/user_generated_content/engagement_test.dart +++ b/test/src/models/user_generated_content/engagement_test.dart @@ -1,6 +1,5 @@ import 'package:core/core.dart'; import 'package:test/test.dart'; -import 'package:core/src/models/user_generated_content/report.dart'; void main() { group('Engagement', () { From 2ac9d91c4e3bbe00fc8898364eb9f5f779941f54 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:12:18 +0100 Subject: [PATCH 13/17] fix(enums): correct reportable entity value for comments - Change 'engagement' to 'comment' for ReportableEntity enum value - Update documentation to reflect that the report is specifically for user engagement comments --- lib/src/enums/reportable_entity.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/enums/reportable_entity.dart b/lib/src/enums/reportable_entity.dart index 5417e589..ebd2739e 100644 --- a/lib/src/enums/reportable_entity.dart +++ b/lib/src/enums/reportable_entity.dart @@ -16,7 +16,7 @@ enum ReportableEntity { @JsonValue('source') source, - /// The report is for a user engagement (mainly for engagements with comments). - @JsonValue('engagement') - engagement, + /// The report is for a user engagement comment. + @JsonValue('comment') + comment, } From a47704bc33027985a9805dbdc7d6a0ebd02ee196 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:12:41 +0100 Subject: [PATCH 14/17] test(enums): correct ReportableEntity tests - Replace engagement with comment in ReportableEntity tests - Update containsAll, string values, and byName tests to reflect the change --- test/src/enums/reportable_entity_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/src/enums/reportable_entity_test.dart b/test/src/enums/reportable_entity_test.dart index 1ab6dbdc..ce9e261c 100644 --- a/test/src/enums/reportable_entity_test.dart +++ b/test/src/enums/reportable_entity_test.dart @@ -9,7 +9,7 @@ void main() { containsAll([ ReportableEntity.headline, ReportableEntity.source, - ReportableEntity.engagement, + ReportableEntity.comment, ]), ); }); @@ -17,7 +17,7 @@ void main() { test('has correct string values', () { expect(ReportableEntity.headline.name, 'headline'); expect(ReportableEntity.source.name, 'source'); - expect(ReportableEntity.engagement.name, 'engagement'); + expect(ReportableEntity.comment.name, 'comment'); }); test('can be created from string values', () { @@ -27,8 +27,8 @@ void main() { ); expect(ReportableEntity.values.byName('source'), ReportableEntity.source); expect( - ReportableEntity.values.byName('engagement'), - ReportableEntity.engagement, + ReportableEntity.values.byName('comment'), + ReportableEntity.comment, ); }); }); From d015e7d02db69031c7da76208109da5c8ec9a223 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:13:50 +0100 Subject: [PATCH 15/17] fix(fixtures): change report entity type to comment - Updated Report entity type from engagement to comment in getReportsFixturesData function - This change ensures that the report fixtures are consistent with the type of entity being reported (comment) --- lib/src/fixtures/reports.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/fixtures/reports.dart b/lib/src/fixtures/reports.dart index ea467ac0..ed506bb2 100644 --- a/lib/src/fixtures/reports.dart +++ b/lib/src/fixtures/reports.dart @@ -76,7 +76,7 @@ List getReportsFixturesData({DateTime? now}) { Report( id: reportIds[i], reporterUserId: user.id, - entityType: ReportableEntity.engagement, + entityType: ReportableEntity.comment, entityId: engagementsWithComments[i].id, reason: commentReasons[i % commentReasons.length].name, additionalComments: 'This comment is spam.', From 713e2420562927faf326541d9c3cd560b52b718b Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:14:21 +0100 Subject: [PATCH 16/17] build(serialziation): sync --- lib/src/models/user_generated_content/report.g.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/models/user_generated_content/report.g.dart b/lib/src/models/user_generated_content/report.g.dart index e3daac72..73df680e 100644 --- a/lib/src/models/user_generated_content/report.g.dart +++ b/lib/src/models/user_generated_content/report.g.dart @@ -47,7 +47,7 @@ Map _$ReportToJson(Report instance) => { const _$ReportableEntityEnumMap = { ReportableEntity.headline: 'headline', ReportableEntity.source: 'source', - ReportableEntity.engagement: 'engagement', + ReportableEntity.comment: 'comment', }; const _$ModerationStatusEnumMap = { From dc18e93de17de998178103249eea178a6269d634 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 5 Dec 2025 04:21:49 +0100 Subject: [PATCH 17/17] chore: delete absolete file --- .../user_generated_content/engagements.dart | 167 ------------------ 1 file changed, 167 deletions(-) delete mode 100644 lib/src/models/user_generated_content/engagements.dart diff --git a/lib/src/models/user_generated_content/engagements.dart b/lib/src/models/user_generated_content/engagements.dart deleted file mode 100644 index 87092b71..00000000 --- a/lib/src/models/user_generated_content/engagements.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:core/core.dart'; - -/// Generates a list of predefined engagements for fixture data. -/// -/// This function can be configured to generate data in either English or -/// Arabic. It pairs reactions with comments to create realistic engagement -/// scenarios. -List getEngagementsFixturesData({ - String languageCode = 'en', - DateTime? now, -}) { - final engagements = []; - final users = usersFixturesData.take(10).toList(); - final headlines = getHeadlinesFixturesData( - languageCode: languageCode, - ).take(100).toList(); - final reactions = reactionsFixturesData; - final comments = getHeadlineCommentsFixturesData( - languageCode: languageCode, - now: now, - ); - final referenceTime = now ?? DateTime.now(); - - for (var i = 0; i < 10; i++) { - for (var j = 0; j < 10; j++) { - final index = i * 10 + j; - final user = users[i]; - final headline = headlines[index]; - final reaction = reactions[index]; - final comment = comments[index]; - - Reaction? engagementReaction; - Comment? engagementComment; - - // Create varied engagement types for realistic test data. - if (index % 3 == 0) { - // Engagement with both reaction and comment - engagementReaction = reaction; - engagementComment = comment; - } else if (index % 3 == 1) { - // Engagement with only a reaction - engagementReaction = reaction; - } else { - // Engagement with only a comment - engagementComment = comment; - } - - engagements.add( - Engagement( - id: _engagementIds[index], - userId: user.id, - entityId: headline.id, - entityType: EngageableType.headline, - reaction: engagementReaction, - comment: engagementComment, - createdAt: referenceTime.subtract(Duration(days: i, hours: j)), - updatedAt: referenceTime.subtract(Duration(days: i, hours: j)), - ), - ); - } - } - - return engagements; -} - -const _engagementIds = [ - kEngagementId1, - kEngagementId2, - kEngagementId3, - kEngagementId4, - kEngagementId5, - kEngagementId6, - kEngagementId7, - kEngagementId8, - kEngagementId9, - kEngagementId10, - kEngagementId11, - kEngagementId12, - kEngagementId13, - kEngagementId14, - kEngagementId15, - kEngagementId16, - kEngagementId17, - kEngagementId18, - kEngagementId19, - kEngagementId20, - kEngagementId21, - kEngagementId22, - kEngagementId23, - kEngagementId24, - kEngagementId25, - kEngagementId26, - kEngagementId27, - kEngagementId28, - kEngagementId29, - kEngagementId30, - kEngagementId31, - kEngagementId32, - kEngagementId33, - kEngagementId34, - kEngagementId35, - kEngagementId36, - kEngagementId37, - kEngagementId38, - kEngagementId39, - kEngagementId40, - kEngagementId41, - kEngagementId42, - kEngagementId43, - kEngagementId44, - kEngagementId45, - kEngagementId46, - kEngagementId47, - kEngagementId48, - kEngagementId49, - kEngagementId50, - kEngagementId51, - kEngagementId52, - kEngagementId53, - kEngagementId54, - kEngagementId55, - kEngagementId56, - kEngagementId57, - kEngagementId58, - kEngagementId59, - kEngagementId60, - kEngagementId61, - kEngagementId62, - kEngagementId63, - kEngagementId64, - kEngagementId65, - kEngagementId66, - kEngagementId67, - kEngagementId68, - kEngagementId69, - kEngagementId70, - kEngagementId71, - kEngagementId72, - kEngagementId73, - kEngagementId74, - kEngagementId75, - kEngagementId76, - kEngagementId77, - kEngagementId78, - kEngagementId79, - kEngagementId80, - kEngagementId81, - kEngagementId82, - kEngagementId83, - kEngagementId84, - kEngagementId85, - kEngagementId86, - kEngagementId87, - kEngagementId88, - kEngagementId89, - kEngagementId90, - kEngagementId91, - kEngagementId92, - kEngagementId93, - kEngagementId94, - kEngagementId95, - kEngagementId96, - kEngagementId97, - kEngagementId98, - kEngagementId99, - kEngagementId100, -];