diff --git a/CHANGELOG.md b/CHANGELOG.md index 8526f3d3..05998827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Upcoming +- **feat**: Added a comprehensive **Community & Engagement System**. This major feature introduces the foundational data models, fixtures and tests for user reactions, comments, a multi-entity reporting system, and a smart app review funnel. The entire system is remotely configurable via a new unified `CommunityConfig` model and extends `UserLimitsConfig` to support role-based limits for comments and reports. +- **BREAKING** refactor!: Overhauled data models and configuration to align with the new identity pivot toward news aggregator. This major refactor introduces a more scalable remote configuration structure, standardizes enums and models for broader use (e.g., `FeedItem` settings), and simplifies ad, notification, and headline data structures for improved clarity and maintainability. - **feat**: Introduce data models to support a filter-based push notification system. This includes `SavedHeadlineFilter`, `SavedSourceFilter`, and related configuration models, providing the architectural foundation for clients to implement notification subscriptions. - **BREAKING** refactor!: Rework `UserPreferenceConfig` to support the new notification system with a more scalable, role-based map structure for all user limits. - **test**: Add comprehensive unit tests for all new and refactored models. diff --git a/README.md b/README.md index 534fd001..7a9ac88b 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,27 @@ Provides data structures for persisting all user-specific configurations, from a +
+💬 Community & Engagement System + +--- + +### 💬 Reactions, Comments & Reporting + +Provides a complete suite of models for building a rich community interaction layer. This includes individualized reactions, a robust commenting system with a built-in moderation workflow, and a flexible reporting system for headlines, sources, and comments. + +> **Your Advantage:** Foster a vibrant and safe user community, maintain high content quality through effective moderation, and gather direct user feedback on your content. + +--- + +### ⭐ Smart App Review Funnel + +Implements the data structures for a strategic, two-layer review funnel. This system intelligently prompts engaged users for public reviews while channeling critical feedback from dissatisfied users into private channels. + +> **Your Advantage:** Maximize positive app store ratings and improve your app's reputation by proactively managing user feedback and preventing negative public reviews. + +
+
🔔 Notification & Alerting System diff --git a/lib/src/enums/comment_report_reason.dart b/lib/src/enums/comment_report_reason.dart new file mode 100644 index 00000000..492c1788 --- /dev/null +++ b/lib/src/enums/comment_report_reason.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// {@template comment_report_reason} +/// Defines the specific reasons a user can provide for reporting a comment. +/// {@endtemplate} +@JsonEnum() +enum CommentReportReason { + /// The comment is unsolicited advertising or promotion. + @JsonValue('spamOrAdvertising') + spamOrAdvertising, + + /// The comment contains abusive language, personal attacks, or bullying. + @JsonValue('harassmentOrBullying') + harassmentOrBullying, + + /// The comment targets certain groups with hateful language. + @JsonValue('hateSpeech') + hateSpeech, +} diff --git a/lib/src/enums/comment_status.dart b/lib/src/enums/comment_status.dart new file mode 100644 index 00000000..95fb5a5f --- /dev/null +++ b/lib/src/enums/comment_status.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// {@template comment_status} +/// Defines the lifecycle status of a user-submitted comment. +/// {@endtemplate} +@JsonEnum() +enum CommentStatus { + /// The comment has been submitted and is awaiting moderation. + @JsonValue('pendingReview') + pendingReview, + + /// The comment has been approved by a moderator and is publicly visible. + @JsonValue('approved') + approved, + + /// The comment has been rejected by a moderator and is not visible. + @JsonValue('rejected') + rejected, + + /// The comment has been automatically flagged by an AI moderation service. + @JsonValue('flaggedByAI') + flaggedByAI, + + /// The comment has been hidden by the user who posted it. + @JsonValue('hiddenByUser') + hiddenByUser, +} diff --git a/lib/src/enums/engageable_type.dart b/lib/src/enums/engageable_type.dart new file mode 100644 index 00000000..a3f53842 --- /dev/null +++ b/lib/src/enums/engageable_type.dart @@ -0,0 +1,12 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// {@template engageable_type} +/// Defines the types of entities that can be engaged with (reacted to or +/// commented on). +/// {@endtemplate} +@JsonEnum() +enum EngageableType { + /// The engagement is for a news headline. + @JsonValue('headline') + headline, +} diff --git a/lib/src/enums/engagement_mode.dart b/lib/src/enums/engagement_mode.dart new file mode 100644 index 00000000..ef483c7f --- /dev/null +++ b/lib/src/enums/engagement_mode.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// {@template engagement_mode} +/// Defines the engagement features available to users. +/// {@endtemplate} +@JsonEnum() +enum EngagementMode { + /// Users can only react to headlines. + @JsonValue('reactionsOnly') + reactionsOnly, + + /// Users can both react and comment on headlines. + @JsonValue('reactionsAndComments') + reactionsAndComments, +} diff --git a/lib/src/enums/enums.dart b/lib/src/enums/enums.dart index f917dbae..832ba937 100644 --- a/lib/src/enums/enums.dart +++ b/lib/src/enums/enums.dart @@ -6,16 +6,25 @@ export 'app_font_weight.dart'; export 'app_text_scale_factor.dart'; export 'app_user_role.dart'; export 'banner_ad_shape.dart'; +export 'comment_report_reason.dart'; +export 'comment_status.dart'; export 'content_status.dart'; export 'content_type.dart'; export 'dashboard_user_role.dart'; export 'device_platform.dart'; +export 'engageable_type.dart'; +export 'engagement_mode.dart'; export 'feed_decorator_category.dart'; export 'feed_decorator_type.dart'; export 'feed_item_click_behavior.dart'; export 'feed_item_density.dart'; export 'feed_item_image_style.dart'; +export 'headline_report_reason.dart'; export 'push_notification_provider.dart'; export 'push_notification_subscription_delivery_type.dart'; +export 'reaction_type.dart'; +export 'report_status.dart'; +export 'reportable_entity.dart'; export 'sort_order.dart'; +export 'source_report_reason.dart'; export 'source_type.dart'; diff --git a/lib/src/enums/headline_report_reason.dart b/lib/src/enums/headline_report_reason.dart new file mode 100644 index 00000000..462d1311 --- /dev/null +++ b/lib/src/enums/headline_report_reason.dart @@ -0,0 +1,32 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// {@template headline_report_reason} +/// Defines the specific reasons a user can provide for reporting a headline. +/// {@endtemplate} +@JsonEnum() +enum HeadlineReportReason { + /// The content is factually incorrect or considered fake news. + @JsonValue('misinformationOrFakeNews') + misinformationOrFakeNews, + + /// The headline is clickbait or does not reflect the article's content. + @JsonValue('clickbaitTitle') + clickbaitTitle, + + /// The content contains hate speech, graphic violence, or other inappropriate + /// material. + @JsonValue('offensiveOrHateSpeech') + offensiveOrHateSpeech, + + /// The link leads to advertising, phishing, or fraudulent content. + @JsonValue('spamOrScam') + spamOrScam, + + /// The article URL does not work. + @JsonValue('brokenLink') + brokenLink, + + /// The content requires a subscription that was not disclosed. + @JsonValue('paywalled') + paywalled, +} diff --git a/lib/src/enums/reaction_type.dart b/lib/src/enums/reaction_type.dart new file mode 100644 index 00000000..0015529c --- /dev/null +++ b/lib/src/enums/reaction_type.dart @@ -0,0 +1,38 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// {@template reaction_type} +/// Defines the types of reactions a user can have to a headline. +/// {@endtemplate} +@JsonEnum() +enum ReactionType { + /// Represents a "like" or "thumbs up" reaction. + /// General positive approval. + @JsonValue('like') + like, + + /// Represents an "insightful" or "lightbulb" reaction. + /// Signals the article provided new information or a valuable perspective. + @JsonValue('insightful') + insightful, + + /// Represents an "amusing" or "funny" reaction. + /// For lighthearted or humorous content. + @JsonValue('amusing') + amusing, + + /// Represents a "sad" reaction. + /// For news that evokes empathy or sadness. + @JsonValue('sad') + sad, + + /// Represents an "angry" or "outrageous" reaction. + /// For news that provokes a strong negative emotional response. + @JsonValue('angry') + angry, + + /// Represents a "skeptical" or "questionable" reaction. + /// Signals that the user questions the validity, bias, or sourcing of the + /// article without formally reporting it. + @JsonValue('skeptical') + skeptical, +} diff --git a/lib/src/enums/report_status.dart b/lib/src/enums/report_status.dart new file mode 100644 index 00000000..6e0bd3bf --- /dev/null +++ b/lib/src/enums/report_status.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// {@template report_status} +/// Defines the moderation workflow status for a user-submitted report. +/// {@endtemplate} +@JsonEnum() +enum ReportStatus { + /// The report has been submitted by a user and is awaiting review. + @JsonValue('submitted') + submitted, + + /// A moderator is actively reviewing the report. + @JsonValue('inReview') + inReview, + + /// The report has been reviewed and a decision has been made. + @JsonValue('resolved') + resolved, +} diff --git a/lib/src/enums/reportable_entity.dart b/lib/src/enums/reportable_entity.dart new file mode 100644 index 00000000..5417e589 --- /dev/null +++ b/lib/src/enums/reportable_entity.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// {@template reportable_entity} +/// Defines the types of entities that can be reported by users. +/// +/// This enum acts as a discriminator in the `Report` model to identify +/// what kind of content the report refers to. +/// {@endtemplate} +@JsonEnum() +enum ReportableEntity { + /// The report is for a news headline. + @JsonValue('headline') + headline, + + /// The report is for a news source. + @JsonValue('source') + source, + + /// The report is for a user engagement (mainly for engagements with comments). + @JsonValue('engagement') + engagement, +} diff --git a/lib/src/enums/source_report_reason.dart b/lib/src/enums/source_report_reason.dart new file mode 100644 index 00000000..8c1aa169 --- /dev/null +++ b/lib/src/enums/source_report_reason.dart @@ -0,0 +1,30 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// {@template source_report_reason} +/// Defines the specific reasons a user can provide for reporting a news source. +/// +/// These reasons are designed to be actionable and can provide data to +/// influence other systems, like an automated content scraper's scoring. +/// {@endtemplate} +@JsonEnum() +enum SourceReportReason { + /// The source consistently produces poor, biased, or unreliable content. + @JsonValue('lowQualityJournalism') + lowQualityJournalism, + + /// The source's website is unusable due to excessive ads or popups. + @JsonValue('highAdDensity') + highAdDensity, + + /// The source often requires a subscription to view content. + @JsonValue('frequentPaywalls') + frequentPaywalls, + + /// The source is pretending to be another entity. + @JsonValue('impersonation') + impersonation, + + /// The source has a pattern of publishing fake news or misinformation. + @JsonValue('spreadsMisinformation') + spreadsMisinformation, +} diff --git a/lib/src/fixtures/engagements.dart b/lib/src/fixtures/engagements.dart new file mode 100644 index 00000000..55d1c4f3 --- /dev/null +++ b/lib/src/fixtures/engagements.dart @@ -0,0 +1,152 @@ +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]; + // Pair every other reaction with a comment for variety + final comment = index.isEven ? comments[index] : null; + + engagements.add( + Engagement( + id: _engagementIds[index], + userId: user.id, + entityId: headline.id, + entityType: EngageableType.headline, + reaction: reaction, + comment: comment, + 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/lib/src/fixtures/fixture_ids.dart b/lib/src/fixtures/fixture_ids.dart index 32886ae5..c480101e 100644 --- a/lib/src/fixtures/fixture_ids.dart +++ b/lib/src/fixtures/fixture_ids.dart @@ -1066,3 +1066,117 @@ const String kInAppNotificationId18 = 'in_app_notification_18'; const String kInAppNotificationId19 = 'in_app_notification_19'; const String kInAppNotificationId20 = 'in_app_notification_20'; const String kInAppNotificationId21 = 'in_app_notification_21'; + +/// Content Reports Fixture IDs. +const String kReportId1 = 'rep0000000000000000000001'; +const String kReportId2 = 'rep0000000000000000000002'; +const String kReportId3 = 'rep0000000000000000000003'; +const String kReportId4 = 'rep0000000000000000000004'; +const String kReportId5 = 'rep0000000000000000000005'; +const String kReportId6 = 'rep0000000000000000000006'; +const String kReportId7 = 'rep0000000000000000000007'; +const String kReportId8 = 'rep0000000000000000000008'; +const String kReportId9 = 'rep0000000000000000000009'; +const String kReportId10 = 'rep0000000000000000000010'; + +/// Engagement Fixture IDs +const String kEngagementId1 = 'eng0000000000000000000001'; +const String kEngagementId2 = 'eng0000000000000000000002'; +const String kEngagementId3 = 'eng0000000000000000000003'; +const String kEngagementId4 = 'eng0000000000000000000004'; +const String kEngagementId5 = 'eng0000000000000000000005'; +const String kEngagementId6 = 'eng0000000000000000000006'; +const String kEngagementId7 = 'eng0000000000000000000007'; +const String kEngagementId8 = 'eng0000000000000000000008'; +const String kEngagementId9 = 'eng0000000000000000000009'; +const String kEngagementId10 = 'eng0000000000000000000010'; +const String kEngagementId11 = 'eng0000000000000000000011'; +const String kEngagementId12 = 'eng0000000000000000000012'; +const String kEngagementId13 = 'eng0000000000000000000013'; +const String kEngagementId14 = 'eng0000000000000000000014'; +const String kEngagementId15 = 'eng0000000000000000000015'; +const String kEngagementId16 = 'eng0000000000000000000016'; +const String kEngagementId17 = 'eng0000000000000000000017'; +const String kEngagementId18 = 'eng0000000000000000000018'; +const String kEngagementId19 = 'eng0000000000000000000019'; +const String kEngagementId20 = 'eng0000000000000000000020'; +const String kEngagementId21 = 'eng0000000000000000000021'; +const String kEngagementId22 = 'eng0000000000000000000022'; +const String kEngagementId23 = 'eng0000000000000000000023'; +const String kEngagementId24 = 'eng0000000000000000000024'; +const String kEngagementId25 = 'eng0000000000000000000025'; +const String kEngagementId26 = 'eng0000000000000000000026'; +const String kEngagementId27 = 'eng0000000000000000000027'; +const String kEngagementId28 = 'eng0000000000000000000028'; +const String kEngagementId29 = 'eng0000000000000000000029'; +const String kEngagementId30 = 'eng0000000000000000000030'; +const String kEngagementId31 = 'eng0000000000000000000031'; +const String kEngagementId32 = 'eng0000000000000000000032'; +const String kEngagementId33 = 'eng0000000000000000000033'; +const String kEngagementId34 = 'eng0000000000000000000034'; +const String kEngagementId35 = 'eng0000000000000000000035'; +const String kEngagementId36 = 'eng0000000000000000000036'; +const String kEngagementId37 = 'eng0000000000000000000037'; +const String kEngagementId38 = 'eng0000000000000000000038'; +const String kEngagementId39 = 'eng0000000000000000000039'; +const String kEngagementId40 = 'eng0000000000000000000040'; +const String kEngagementId41 = 'eng0000000000000000000041'; +const String kEngagementId42 = 'eng0000000000000000000042'; +const String kEngagementId43 = 'eng0000000000000000000043'; +const String kEngagementId44 = 'eng0000000000000000000044'; +const String kEngagementId45 = 'eng0000000000000000000045'; +const String kEngagementId46 = 'eng0000000000000000000046'; +const String kEngagementId47 = 'eng0000000000000000000047'; +const String kEngagementId48 = 'eng0000000000000000000048'; +const String kEngagementId49 = 'eng0000000000000000000049'; +const String kEngagementId50 = 'eng0000000000000000000050'; +const String kEngagementId51 = 'eng0000000000000000000051'; +const String kEngagementId52 = 'eng0000000000000000000052'; +const String kEngagementId53 = 'eng0000000000000000000053'; +const String kEngagementId54 = 'eng0000000000000000000054'; +const String kEngagementId55 = 'eng0000000000000000000055'; +const String kEngagementId56 = 'eng0000000000000000000056'; +const String kEngagementId57 = 'eng0000000000000000000057'; +const String kEngagementId58 = 'eng0000000000000000000058'; +const String kEngagementId59 = 'eng0000000000000000000059'; +const String kEngagementId60 = 'eng0000000000000000000060'; +const String kEngagementId61 = 'eng0000000000000000000061'; +const String kEngagementId62 = 'eng0000000000000000000062'; +const String kEngagementId63 = 'eng0000000000000000000063'; +const String kEngagementId64 = 'eng0000000000000000000064'; +const String kEngagementId65 = 'eng0000000000000000000065'; +const String kEngagementId66 = 'eng0000000000000000000066'; +const String kEngagementId67 = 'eng0000000000000000000067'; +const String kEngagementId68 = 'eng0000000000000000000068'; +const String kEngagementId69 = 'eng0000000000000000000069'; +const String kEngagementId70 = 'eng0000000000000000000070'; +const String kEngagementId71 = 'eng0000000000000000000071'; +const String kEngagementId72 = 'eng0000000000000000000072'; +const String kEngagementId73 = 'eng0000000000000000000073'; +const String kEngagementId74 = 'eng0000000000000000000074'; +const String kEngagementId75 = 'eng0000000000000000000075'; +const String kEngagementId76 = 'eng0000000000000000000076'; +const String kEngagementId77 = 'eng0000000000000000000077'; +const String kEngagementId78 = 'eng0000000000000000000078'; +const String kEngagementId79 = 'eng0000000000000000000079'; +const String kEngagementId80 = 'eng0000000000000000000080'; +const String kEngagementId81 = 'eng0000000000000000000081'; +const String kEngagementId82 = 'eng0000000000000000000082'; +const String kEngagementId83 = 'eng0000000000000000000083'; +const String kEngagementId84 = 'eng0000000000000000000084'; +const String kEngagementId85 = 'eng0000000000000000000085'; +const String kEngagementId86 = 'eng0000000000000000000086'; +const String kEngagementId87 = 'eng0000000000000000000087'; +const String kEngagementId88 = 'eng0000000000000000000088'; +const String kEngagementId89 = 'eng0000000000000000000089'; +const String kEngagementId90 = 'eng0000000000000000000090'; +const String kEngagementId91 = 'eng0000000000000000000091'; +const String kEngagementId92 = 'eng0000000000000000000092'; +const String kEngagementId93 = 'eng0000000000000000000093'; +const String kEngagementId94 = 'eng0000000000000000000094'; +const String kEngagementId95 = 'eng0000000000000000000095'; +const String kEngagementId96 = 'eng0000000000000000000096'; +const String kEngagementId97 = 'eng0000000000000000000097'; +const String kEngagementId98 = 'eng0000000000000000000098'; +const String kEngagementId99 = 'eng0000000000000000000099'; +const String kEngagementId100 = 'eng0000000000000000000100'; diff --git a/lib/src/fixtures/fixtures.dart b/lib/src/fixtures/fixtures.dart index 99029356..3ab9ce27 100644 --- a/lib/src/fixtures/fixtures.dart +++ b/lib/src/fixtures/fixtures.dart @@ -1,11 +1,15 @@ export 'app_settings.dart'; export 'countries.dart'; export 'dashboard_summary.dart'; +export 'engagements.dart'; export 'fixture_ids.dart'; +export 'headline_comments.dart'; +export 'headline_reactions.dart'; export 'headlines.dart'; export 'in_app_notifications.dart'; export 'languages.dart'; export 'remote_configs.dart'; +export 'reports.dart'; export 'saved_headline_filters.dart'; export 'saved_source_filters.dart'; export 'sources.dart'; diff --git a/lib/src/fixtures/headline_comments.dart b/lib/src/fixtures/headline_comments.dart new file mode 100644 index 00000000..939c1aae --- /dev/null +++ b/lib/src/fixtures/headline_comments.dart @@ -0,0 +1,76 @@ +import 'package:core/core.dart'; + +/// A list of predefined comments for fixture data. +/// +/// This function can be configured to generate comments in either English or +/// Arabic. It creates 10 comments for each of the first 10 users, with each +/// comment targeting a unique headline. +List getHeadlineCommentsFixturesData({ + String languageCode = 'en', + DateTime? now, +}) { + final comments = []; + final users = usersFixturesData.take(10).toList(); + + // Ensure only approved languages are used, default to 'en'. + final resolvedLanguageCode = ['en', 'ar'].contains(languageCode) + ? languageCode + : 'en'; + final language = languagesFixturesData.firstWhere( + (lang) => lang.code == resolvedLanguageCode, + orElse: () => languagesFixturesData.firstWhere((lang) => lang.code == 'en'), + ); + + final commentContentsByLang = >{ + 'en': [ + 'This is a really insightful article. It completely changed my perspective.', + "I'm not sure I agree with the author's conclusion, but it's a well-argued piece.", + 'Finally, someone is talking about this! More people need to read this.', + 'A bit simplistic, but a good introduction to the topic for beginners.', + 'The data presented here is fascinating. I wonder what the long-term implications are.', + 'This is exactly what I was looking for. Thank you for sharing!', + 'I have a few questions about the methodology used in this study.', + 'This made me laugh out loud. Great writing!', + "A powerful and moving story. It's important to hear these voices.", + 'I think there are some key facts missing from this analysis.', + ], + 'ar': [ + 'هذا مقال ثاقب حقًا. لقد غير وجهة نظري تمامًا.', + 'لست متأكدًا من أنني أتفق مع استنتاج المؤلف، لكنها قطعة جيدة الحجة.', + 'أخيرًا، هناك من يتحدث عن هذا! المزيد من الناس بحاجة إلى قراءة هذا.', + 'مبسط بعض الشيء، لكنه مقدمة جيدة للمبتدئين.', + 'البيانات المقدمة هنا رائعة. أتساءل ما هي الآثار طويلة المدى.', + 'هذا هو بالضبط ما كنت أبحث عنه. شكرًا لك على المشاركة!', + 'لدي بعض الأسئلة حول المنهجية المستخدمة في هذه الدراسة.', + 'هذا جعلني أضحك بصوت عال. كتابة رائعة!', + 'قصة قوية ومؤثرة. من المهم سماع هذه الأصوات.', + 'أعتقد أن هناك بعض الحقائق الأساسية المفقودة من هذا التحليل.', + ], + }; + + final commentContents = commentContentsByLang[resolvedLanguageCode]!; + + for (var i = 0; i < users.length; i++) { + for (var j = 0; j < 10; j++) { + final commentIndex = i * 10 + j; + + // Vary the status for realism + var status = CommentStatus.approved; + if (commentIndex % 15 == 0) { + status = CommentStatus.pendingReview; + } else if (commentIndex % 25 == 0) { + status = CommentStatus.rejected; + } + + comments.add( + Comment( + language: language, + content: commentContents[j], + status: status, + ), + ); + } + } + + return comments; +} diff --git a/lib/src/fixtures/headline_reactions.dart b/lib/src/fixtures/headline_reactions.dart new file mode 100644 index 00000000..f58a3fe8 --- /dev/null +++ b/lib/src/fixtures/headline_reactions.dart @@ -0,0 +1,17 @@ +import 'package:core/core.dart'; + +/// A list of predefined reactions for fixture data. +/// This creates a list of reactions with varying types. +final List reactionsFixturesData = () { + final reactions = []; + const reactionTypes = ReactionType.values; + + // Create 100 reactions, cycling through the available reaction types. + for (var i = 0; i < 100; i++) { + reactions.add( + Reaction(reactionType: reactionTypes[i % reactionTypes.length]), + ); + } + + return reactions; +}(); diff --git a/lib/src/fixtures/headlines.dart b/lib/src/fixtures/headlines.dart index 721179eb..a5e16df5 100644 --- a/lib/src/fixtures/headlines.dart +++ b/lib/src/fixtures/headlines.dart @@ -1,1511 +1,186 @@ import 'package:core/src/enums/enums.dart'; import 'package:core/src/fixtures/countries.dart'; import 'package:core/src/fixtures/fixture_ids.dart'; -import 'package:core/src/fixtures/sources.dart'; -import 'package:core/src/fixtures/topics.dart'; +import 'package:core/src/fixtures/sources.dart' as source_fixtures; +import 'package:core/src/fixtures/topics.dart' as topic_fixtures; import 'package:core/src/models/entities/headline.dart'; -/// A list of predefined headlines for fixture data. -final headlinesFixturesData = [ - Headline( - id: kHeadlineId1, - isBreaking: false, - title: 'AI Breakthrough: New Model Achieves Human-Level Performance', - url: 'https://example.com/news/ai-breakthrough-1', - imageUrl: 'https://picsum.photos/seed/kHeadlineId1/800/600', - source: sourcesFixturesData[0], // TechCrunch - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[0], // Technology - createdAt: DateTime.now().subtract(const Duration(minutes: 15)), - updatedAt: DateTime.now().subtract(const Duration(minutes: 15)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId2, - isBreaking: false, - title: 'Local Team Wins Championship in Thrilling Final', - url: 'https://example.com/news/sports-championship-2', - imageUrl: 'https://picsum.photos/seed/kHeadlineId2/800/600', - source: sourcesFixturesData[1], // BBC News - eventCountry: countriesFixturesData[1], // United Kingdom - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(minutes: 15)), - updatedAt: DateTime.now().subtract(const Duration(minutes: 15)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId3, - isBreaking: false, - title: 'Global Leaders Meet to Discuss Climate Change Policies', - url: 'https://example.com/news/politics-climate-3', - imageUrl: 'https://picsum.photos/seed/kHeadlineId3/800/600', - source: sourcesFixturesData[2], // The New York Times - eventCountry: countriesFixturesData[2], // Canada - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(minutes: 15)), - updatedAt: DateTime.now().subtract(const Duration(minutes: 15)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId4, - isBreaking: true, - title: 'New Planet Discovered in Distant Galaxy', - url: 'https://example.com/news/science-planet-4', - imageUrl: 'https://picsum.photos/seed/kHeadlineId4/800/600', - source: sourcesFixturesData[3], // The Guardian - eventCountry: countriesFixturesData[3], // Australia - topic: topicsFixturesData[3], // Science - createdAt: DateTime.now().subtract(const Duration(minutes: 15)), - updatedAt: DateTime.now().subtract(const Duration(minutes: 15)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId5, - isBreaking: false, - title: 'Breakthrough in Cancer Research Offers New Hope', - url: 'https://example.com/news/health-cancer-5', - imageUrl: 'https://picsum.photos/seed/kHeadlineId5/800/600', - source: sourcesFixturesData[4], // CNN - eventCountry: countriesFixturesData[4], // Germany - topic: topicsFixturesData[4], // Health - createdAt: DateTime.now().subtract(const Duration(minutes: 15)), - updatedAt: DateTime.now().subtract(const Duration(minutes: 15)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId6, - isBreaking: false, - title: 'Blockbuster Movie Breaks Box Office Records', - url: 'https://example.com/news/entertainment-movie-6', - imageUrl: 'https://picsum.photos/seed/kHeadlineId6/800/600', - source: sourcesFixturesData[5], // Reuters - eventCountry: countriesFixturesData[5], // France - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 1)), - updatedAt: DateTime.now().subtract(const Duration(days: 1)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId7, - isBreaking: false, - title: 'Stock Market Reaches All-Time High Amid Economic Boom', - url: 'https://example.com/news/business-market-7', - imageUrl: 'https://picsum.photos/seed/kHeadlineId7/800/600', - source: sourcesFixturesData[6], // Al Jazeera English - eventCountry: countriesFixturesData[6], // Japan - topic: topicsFixturesData[6], // Business - createdAt: DateTime.now().subtract(const Duration(days: 1)), - updatedAt: DateTime.now().subtract(const Duration(days: 1)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId8, - isBreaking: false, - title: 'New Travel Restrictions Lifted for Popular Destinations', - url: 'https://example.com/news/travel-restrictions-8', - imageUrl: 'https://picsum.photos/seed/kHeadlineId8/800/600', - source: sourcesFixturesData[7], // Xinhua News Agency - eventCountry: countriesFixturesData[7], // China - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 1)), - updatedAt: DateTime.now().subtract(const Duration(days: 1)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId9, - isBreaking: false, - title: 'Michelin Star Chef Opens New Restaurant in City Center', - url: 'https://example.com/news/food-restaurant-9', - imageUrl: 'https://picsum.photos/seed/kHeadlineId9/800/600', - source: sourcesFixturesData[8], // The Times of India - eventCountry: countriesFixturesData[8], // India - topic: topicsFixturesData[8], // Food - createdAt: DateTime.now().subtract(const Duration(days: 1)), - updatedAt: DateTime.now().subtract(const Duration(days: 1)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId10, - isBreaking: false, - title: 'Innovative Teaching Methods Boost Student Engagement', - url: 'https://example.com/news/education-methods-10', - imageUrl: 'https://picsum.photos/seed/kHeadlineId10/800/600', - source: sourcesFixturesData[9], // Folha de S.Paulo - eventCountry: countriesFixturesData[9], // Brazil - topic: topicsFixturesData[9], // Education - createdAt: DateTime.now().subtract(const Duration(days: 1)), - updatedAt: DateTime.now().subtract(const Duration(days: 1)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId11, - isBreaking: false, - title: 'Cybersecurity Firms Warn of New Global Threat', - url: 'https://example.com/news/cybersecurity-threat-11', - imageUrl: 'https://picsum.photos/seed/kHeadlineId11/800/600', - source: sourcesFixturesData[0], // TechCrunch - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[0], // Technology - createdAt: DateTime.now().subtract(const Duration(days: 2)), - updatedAt: DateTime.now().subtract(const Duration(days: 2)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId12, - isBreaking: false, - title: 'Olympics Committee Announces Host City for 2032 Games', - url: 'https://example.com/news/sports-olympics-12', - imageUrl: 'https://picsum.photos/seed/kHeadlineId12/800/600', - source: sourcesFixturesData[1], // BBC News - eventCountry: countriesFixturesData[1], // United Kingdom - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 2)), - updatedAt: DateTime.now().subtract(const Duration(days: 2)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId13, - isBreaking: false, - title: 'New Bill Aims to Reform Healthcare System', - url: 'https://example.com/news/politics-healthcare-13', - imageUrl: 'https://picsum.photos/seed/kHeadlineId13/800/600', - source: sourcesFixturesData[2], // The New York Times - eventCountry: countriesFixturesData[2], // Canada - topic: topicsFixturesData[4], // Health - createdAt: DateTime.now().subtract(const Duration(days: 2)), - updatedAt: DateTime.now().subtract(const Duration(days: 2)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId14, - isBreaking: false, - title: 'Archaeologists Uncover Ancient City Ruins', - url: 'https://example.com/news/science-archaeology-14', - imageUrl: 'https://picsum.photos/seed/kHeadlineId14/800/600', - source: sourcesFixturesData[3], // The Guardian - eventCountry: countriesFixturesData[3], // Australia - topic: topicsFixturesData[3], // Science - createdAt: DateTime.now().subtract(const Duration(days: 2)), - updatedAt: DateTime.now().subtract(const Duration(days: 2)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId15, - isBreaking: false, - title: 'Dietary Guidelines Updated for Public Health', - url: 'https://example.com/news/health-diet-15', - imageUrl: 'https://picsum.photos/seed/kHeadlineId15/800/600', - source: sourcesFixturesData[4], // CNN - eventCountry: countriesFixturesData[4], // Germany - topic: topicsFixturesData[4], // Health - createdAt: DateTime.now().subtract(const Duration(days: 2)), - updatedAt: DateTime.now().subtract(const Duration(days: 2)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId16, - isBreaking: false, - title: 'Music Festival Announces Star-Studded Lineup', - url: 'https://example.com/news/entertainment-music-16', - imageUrl: 'https://picsum.photos/seed/kHeadlineId16/800/600', - source: sourcesFixturesData[5], // Reuters - eventCountry: countriesFixturesData[5], // France - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 3)), - updatedAt: DateTime.now().subtract(const Duration(days: 3)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId17, - isBreaking: false, - title: 'Tech Giant Acquires Startup in Multi-Billion Dollar Deal', - url: 'https://example.com/news/business-acquisition-17', - imageUrl: 'https://picsum.photos/seed/kHeadlineId17/800/600', - source: sourcesFixturesData[6], // Al Jazeera English - eventCountry: countriesFixturesData[6], // Japan - topic: topicsFixturesData[6], // Business - createdAt: DateTime.now().subtract(const Duration(days: 3)), - updatedAt: DateTime.now().subtract(const Duration(days: 3)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId18, - isBreaking: false, - title: 'Space Tourism Takes Off: First Commercial Flights Announced', - url: 'https://example.com/news/travel-space-18', - imageUrl: 'https://picsum.photos/seed/kHeadlineId18/800/600', - source: sourcesFixturesData[7], // Xinhua News Agency - eventCountry: countriesFixturesData[7], // China - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 3)), - updatedAt: DateTime.now().subtract(const Duration(days: 3)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId19, - isBreaking: false, - title: 'Future of Food: Lab-Grown Meat Gains Popularity', - url: 'https://example.com/news/food-lab-meat-19', - imageUrl: 'https://picsum.photos/seed/kHeadlineId19/800/600', - source: sourcesFixturesData[8], // The Times of India - eventCountry: countriesFixturesData[8], // India - topic: topicsFixturesData[8], // Food - createdAt: DateTime.now().subtract(const Duration(days: 3)), - updatedAt: DateTime.now().subtract(const Duration(days: 3)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId20, - isBreaking: false, - title: 'Online Learning Platforms See Surge in Enrollment', - url: 'https://example.com/news/education-online-20', - imageUrl: 'https://picsum.photos/seed/kHeadlineId20/800/600', - source: sourcesFixturesData[9], // Folha de S.Paulo - eventCountry: countriesFixturesData[9], // Brazil - topic: topicsFixturesData[9], // Education - createdAt: DateTime.now().subtract(const Duration(days: 3)), - updatedAt: DateTime.now().subtract(const Duration(days: 3)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId21, - isBreaking: false, - title: 'Quantum Computing Achieves New Milestone', - url: 'https://example.com/news/tech-quantum-21', - imageUrl: 'https://picsum.photos/seed/kHeadlineId21/800/600', - source: sourcesFixturesData[0], // TechCrunch - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[0], // Technology - createdAt: DateTime.now().subtract(const Duration(days: 4)), - updatedAt: DateTime.now().subtract(const Duration(days: 4)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId22, - isBreaking: false, - title: 'World Cup Qualifiers: Unexpected Upsets Shake Rankings', - url: 'https://example.com/news/sports-worldcup-22', - imageUrl: 'https://picsum.photos/seed/kHeadlineId22/800/600', - source: sourcesFixturesData[1], // BBC News - eventCountry: countriesFixturesData[1], // United Kingdom - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 4)), - updatedAt: DateTime.now().subtract(const Duration(days: 4)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId23, - isBreaking: false, - title: 'Election Results: New Government Takes Power', - url: 'https://example.com/news/politics-election-23', - imageUrl: 'https://picsum.photos/seed/kHeadlineId23/800/600', - source: sourcesFixturesData[2], // The New York Times - eventCountry: countriesFixturesData[2], // Canada - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 4)), - updatedAt: DateTime.now().subtract(const Duration(days: 4)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId24, - isBreaking: false, - title: 'Breakthrough in Fusion Energy Research Announced', - url: 'https://example.com/news/science-fusion-24', - imageUrl: 'https://picsum.photos/seed/kHeadlineId24/800/600', - source: sourcesFixturesData[3], // The Guardian - eventCountry: countriesFixturesData[3], // Australia - topic: topicsFixturesData[3], // Science - createdAt: DateTime.now().subtract(const Duration(days: 4)), - updatedAt: DateTime.now().subtract(const Duration(days: 4)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId25, - isBreaking: false, - title: 'Mental Health Awareness Campaign Launched Globally', - url: 'https://example.com/news/health-mental-25', - imageUrl: 'https://picsum.photos/seed/kHeadlineId25/800/600', - source: sourcesFixturesData[4], // CNN - eventCountry: countriesFixturesData[4], // Germany - topic: topicsFixturesData[4], // Health - createdAt: DateTime.now().subtract(const Duration(days: 4)), - updatedAt: DateTime.now().subtract(const Duration(days: 4)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId26, - isBreaking: false, - title: 'Gaming Industry Sees Record Growth in Virtual Reality', - url: 'https://example.com/news/entertainment-vr-26', - imageUrl: 'https://picsum.photos/seed/kHeadlineId26/800/600', - source: sourcesFixturesData[5], // Reuters - eventCountry: countriesFixturesData[5], // France - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 5)), - updatedAt: DateTime.now().subtract(const Duration(days: 5)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId27, - isBreaking: false, - title: 'Global Supply Chain Disruptions Impacting Consumer Goods', - url: 'https://example.com/news/business-supplychain-27', - imageUrl: 'https://picsum.photos/seed/kHeadlineId27/800/600', - source: sourcesFixturesData[6], // Al Jazeera English - eventCountry: countriesFixturesData[6], // Japan - topic: topicsFixturesData[6], // Business - createdAt: DateTime.now().subtract(const Duration(days: 5)), - updatedAt: DateTime.now().subtract(const Duration(days: 5)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId28, - isBreaking: false, - title: 'Arctic Expedition Discovers New Marine Species', - url: 'https://example.com/news/travel-arctic-28', - imageUrl: 'https://picsum.photos/seed/kHeadlineId28/800/600', - source: sourcesFixturesData[7], // Xinhua News Agency - eventCountry: countriesFixturesData[7], // China - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 5)), - updatedAt: DateTime.now().subtract(const Duration(days: 5)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId29, - isBreaking: false, - title: 'Rise of Plant-Based Cuisine: New Restaurants Open', - url: 'https://example.com/news/food-plantbased-29', - imageUrl: 'https://picsum.photos/seed/kHeadlineId29/800/600', - source: sourcesFixturesData[8], // The Times of India - eventCountry: countriesFixturesData[8], // India - topic: topicsFixturesData[8], // Food - createdAt: DateTime.now().subtract(const Duration(days: 5)), - updatedAt: DateTime.now().subtract(const Duration(days: 5)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId30, - isBreaking: false, - title: 'Education Technology Transforms Classrooms', - url: 'https://example.com/news/education-edtech-30', - imageUrl: 'https://picsum.photos/seed/kHeadlineId30/800/600', - source: sourcesFixturesData[9], // Folha de S.Paulo - eventCountry: countriesFixturesData[9], // Brazil - topic: topicsFixturesData[9], // Education - createdAt: DateTime.now().subtract(const Duration(days: 5)), - updatedAt: DateTime.now().subtract(const Duration(days: 5)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId31, - isBreaking: false, - title: 'SpaceX Launches New Satellite Constellation', - url: 'https://example.com/news/tech-spacex-31', - imageUrl: 'https://picsum.photos/seed/kHeadlineId31/800/600', - source: sourcesFixturesData[0], // TechCrunch - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[0], // Technology - createdAt: DateTime.now().subtract(const Duration(days: 6)), - updatedAt: DateTime.now().subtract(const Duration(days: 6)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId32, - isBreaking: false, - title: 'Football Legend Announces Retirement', - url: 'https://example.com/news/sports-retirement-32', - imageUrl: 'https://picsum.photos/seed/kHeadlineId32/800/600', - source: sourcesFixturesData[1], // BBC News - eventCountry: countriesFixturesData[1], // United Kingdom - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 6)), - updatedAt: DateTime.now().subtract(const Duration(days: 6)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId33, - isBreaking: false, - title: 'G7 Summit Concludes with Joint Statement on Global Economy', - url: 'https://example.com/news/politics-g7-33', - imageUrl: 'https://picsum.photos/seed/kHeadlineId33/800/600', - source: sourcesFixturesData[2], // The New York Times - eventCountry: countriesFixturesData[2], // Canada - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 6)), - updatedAt: DateTime.now().subtract(const Duration(days: 6)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId34, - isBreaking: false, - title: "Breakthrough in Alzheimer's Research Offers New Treatment Path", - url: 'https://example.com/news/science-alzheimers-34', - imageUrl: 'https://picsum.photos/seed/kHeadlineId34/800/600', - source: sourcesFixturesData[3], // The Guardian - eventCountry: countriesFixturesData[3], // Australia - topic: topicsFixturesData[4], // Health - createdAt: DateTime.now().subtract(const Duration(days: 6)), - updatedAt: DateTime.now().subtract(const Duration(days: 6)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId35, - isBreaking: false, - title: 'Global Vaccination Campaign Reaches Billions', - url: 'https://example.com/news/health-vaccine-35', - imageUrl: 'https://picsum.photos/seed/kHeadlineId35/800/600', - source: sourcesFixturesData[4], // CNN - eventCountry: countriesFixturesData[4], // Germany - topic: topicsFixturesData[4], // Health - createdAt: DateTime.now().subtract(const Duration(days: 6)), - updatedAt: DateTime.now().subtract(const Duration(days: 6)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId36, - isBreaking: false, - title: 'Streaming Wars Intensify with New Platform Launches', - url: 'https://example.com/news/entertainment-streaming-36', - imageUrl: 'https://picsum.photos/seed/kHeadlineId36/800/600', - source: sourcesFixturesData[5], // Reuters - eventCountry: countriesFixturesData[5], // France - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 7)), - updatedAt: DateTime.now().subtract(const Duration(days: 7)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId37, - isBreaking: false, - title: 'Cryptocurrency Market Experiences Major Volatility', - url: 'https://example.com/news/business-crypto-37', - imageUrl: 'https://picsum.photos/seed/kHeadlineId37/800/600', - source: sourcesFixturesData[6], // Al Jazeera English - eventCountry: countriesFixturesData[6], // Japan - topic: topicsFixturesData[0], // Technology - createdAt: DateTime.now().subtract(const Duration(days: 7)), - updatedAt: DateTime.now().subtract(const Duration(days: 7)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId38, - isBreaking: false, - title: 'Sustainable Tourism Initiatives Gain Momentum', - url: 'https://example.com/news/travel-sustainable-38', - imageUrl: 'https://picsum.photos/seed/kHeadlineId38/800/600', - source: sourcesFixturesData[7], // Xinhua News Agency - eventCountry: countriesFixturesData[7], // China - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 7)), - updatedAt: DateTime.now().subtract(const Duration(days: 7)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId39, - isBreaking: false, - title: 'Food Security Summit Addresses Global Hunger', - url: 'https://example.com/news/food-security-39', - imageUrl: 'https://picsum.photos/seed/kHeadlineId39/800/600', - source: sourcesFixturesData[8], // The Times of India - eventCountry: countriesFixturesData[8], // India - topic: topicsFixturesData[8], // Food - createdAt: DateTime.now().subtract(const Duration(days: 7)), - updatedAt: DateTime.now().subtract(const Duration(days: 7)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId40, - isBreaking: false, - title: 'Robotics in Education: New Tools for Learning', - url: 'https://example.com/news/education-robotics-40', - imageUrl: 'https://picsum.photos/seed/kHeadlineId40/800/600', - source: sourcesFixturesData[9], // Folha de S.Paulo - eventCountry: countriesFixturesData[9], // Brazil - topic: topicsFixturesData[9], // Education - createdAt: DateTime.now().subtract(const Duration(days: 7)), - updatedAt: DateTime.now().subtract(const Duration(days: 7)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId41, - isBreaking: false, - title: 'AI Ethics Debate Intensifies Among Tech Leaders', - url: 'https://example.com/news/tech-ethics-41', - imageUrl: 'https://picsum.photos/seed/kHeadlineId41/800/600', - source: sourcesFixturesData[0], // TechCrunch - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[0], // Technology - createdAt: DateTime.now().subtract(const Duration(days: 8)), - updatedAt: DateTime.now().subtract(const Duration(days: 8)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId42, - isBreaking: false, - title: 'Esports Industry Sees Massive Investment Boom', - url: 'https://example.com/news/sports-esports-42', - imageUrl: 'https://picsum.photos/seed/kHeadlineId42/800/600', - source: sourcesFixturesData[1], // BBC News - eventCountry: countriesFixturesData[1], // United Kingdom - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 8)), - updatedAt: DateTime.now().subtract(const Duration(days: 8)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId43, - isBreaking: false, - title: 'International Sanctions Imposed on Rogue State', - url: 'https://example.com/news/politics-sanctions-43', - imageUrl: 'https://picsum.photos/seed/kHeadlineId43/800/600', - source: sourcesFixturesData[2], // The New York Times - eventCountry: countriesFixturesData[2], // Canada - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 8)), - updatedAt: DateTime.now().subtract(const Duration(days: 8)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId44, - isBreaking: false, - title: 'New Species of Deep-Sea Creature Discovered', - url: 'https://example.com/news/science-deepsea-44', - imageUrl: 'https://picsum.photos/seed/kHeadlineId44/800/600', - source: sourcesFixturesData[3], // The Guardian - eventCountry: countriesFixturesData[3], // Australia - topic: topicsFixturesData[3], // Science - createdAt: DateTime.now().subtract(const Duration(days: 8)), - updatedAt: DateTime.now().subtract(const Duration(days: 8)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId45, - isBreaking: false, - title: 'Global Health Crisis: New Pandemic Preparedness Plan', - url: 'https://example.com/news/health-pandemic-45', - imageUrl: 'https://picsum.photos/seed/kHeadlineId45/800/600', - source: sourcesFixturesData[4], // CNN - eventCountry: countriesFixturesData[4], // Germany - topic: topicsFixturesData[4], // Health - createdAt: DateTime.now().subtract(const Duration(days: 8)), - updatedAt: DateTime.now().subtract(const Duration(days: 8)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId46, - isBreaking: false, - title: 'Hollywood Strikes Continue: Impact on Film Production', - url: 'https://example.com/news/entertainment-strikes-46', - imageUrl: 'https://picsum.photos/seed/kHeadlineId46/800/600', - source: sourcesFixturesData[5], // Reuters - eventCountry: countriesFixturesData[5], // France - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 9)), - updatedAt: DateTime.now().subtract(const Duration(days: 9)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId47, - isBreaking: false, - title: 'Emerging Markets Show Strong Economic Resilience', - url: 'https://example.com/news/business-emerging-47', - imageUrl: 'https://picsum.photos/seed/kHeadlineId47/800/600', - source: sourcesFixturesData[6], // Al Jazeera English - eventCountry: countriesFixturesData[6], // Japan - topic: topicsFixturesData[6], // Business - createdAt: DateTime.now().subtract(const Duration(days: 9)), - updatedAt: DateTime.now().subtract(const Duration(days: 9)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId48, - isBreaking: false, - title: 'Adventure Tourism Booms in Remote Regions', - url: 'https://example.com/news/travel-adventure-48', - imageUrl: 'https://picsum.photos/seed/kHeadlineId48/800/600', - source: sourcesFixturesData[7], // Xinhua News Agency - eventCountry: countriesFixturesData[7], // China - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 9)), - updatedAt: DateTime.now().subtract(const Duration(days: 9)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId49, - isBreaking: false, - title: 'The Rise of Sustainable Food Packaging', - url: 'https://example.com/news/food-packaging-49', - imageUrl: 'https://picsum.photos/seed/kHeadlineId49/800/600', - source: sourcesFixturesData[8], // The Times of India - eventCountry: countriesFixturesData[8], // India - topic: topicsFixturesData[8], // Food - createdAt: DateTime.now().subtract(const Duration(days: 9)), - updatedAt: DateTime.now().subtract(const Duration(days: 9)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId50, - isBreaking: false, - title: 'Personalized Learning: Tailoring Education to Individual Needs', - url: 'https://example.com/news/education-personalized-50', - imageUrl: 'https://picsum.photos/seed/kHeadlineId50/800/600', - source: sourcesFixturesData[9], // Folha de S.Paulo - eventCountry: countriesFixturesData[9], // Brazil - topic: topicsFixturesData[9], // Education - createdAt: DateTime.now().subtract(const Duration(days: 9)), - updatedAt: DateTime.now().subtract(const Duration(days: 9)), - status: ContentStatus.active, - ), +/// Generates a list of predefined headlines for fixture data. +/// +/// This function can be configured to generate headlines in either English or +/// Arabic. +List getHeadlinesFixturesData({ + String languageCode = 'en', + DateTime? now, +}) { + // Ensure only approved languages are used, default to 'en'. + final resolvedLanguageCode = ['en', 'ar'].contains(languageCode) + ? languageCode + : 'en'; + final referenceTime = now ?? DateTime.now(); - // --- Headlines for New Sources (5 per source) --- + final sources = source_fixtures.getSourcesFixturesData( + languageCode: resolvedLanguageCode, + ); + final topics = topic_fixtures.getTopicsFixturesData( + languageCode: resolvedLanguageCode, + ); - // --- Local News Outlets (kSourceId11 - kSourceId20) --- - Headline( - id: kHeadlineId51, - isBreaking: false, - title: 'City Council Approves New Downtown Development Plan', - url: 'https://example.com/news/sf-downtown-plan', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId51/800/600', // San Francisco Chronicle - source: sourcesFixturesData[10], // San Francisco Chronicle - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 10)), - updatedAt: DateTime.now().subtract(const Duration(days: 10)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId52, - isBreaking: false, - title: 'Tech Startups Flourish in the Bay Area', - url: 'https://example.com/news/sf-tech-boom', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId52/800/600', // San Francisco Chronicle - source: sourcesFixturesData[10], // San Francisco Chronicle - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[0], // Technology - createdAt: DateTime.now().subtract(const Duration(days: 10)), - updatedAt: DateTime.now().subtract(const Duration(days: 10)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId53, - isBreaking: false, - title: 'Golden Gate Bridge Retrofit Project Begins', - url: 'https://example.com/news/ggb-retrofit', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId53/800/600', // San Francisco Chronicle - source: sourcesFixturesData[10], // San Francisco Chronicle - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 11)), - updatedAt: DateTime.now().subtract(const Duration(days: 11)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId54, - isBreaking: false, - title: 'Local Chef Wins Prestigious Culinary Award', - url: 'https://example.com/news/sf-chef-award', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId54/800/600', // San Francisco Chronicle - source: sourcesFixturesData[10], // San Francisco Chronicle - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[8], // Food - createdAt: DateTime.now().subtract(const Duration(days: 11)), - updatedAt: DateTime.now().subtract(const Duration(days: 11)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId55, - isBreaking: false, - title: 'Warriors Secure Victory in Season Opener', - url: 'https://example.com/news/warriors-win', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId55/800/600', // San Francisco Chronicle - source: sourcesFixturesData[10], // San Francisco Chronicle - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 12)), - updatedAt: DateTime.now().subtract(const Duration(days: 12)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId56, - isBreaking: false, - title: 'Manchester United Announces New Stadium Expansion Plans', - url: 'https://example.com/news/mu-stadium-expansion', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId56/800/600', // Manchester Evening News - source: sourcesFixturesData[11], // Manchester Evening News - eventCountry: countriesFixturesData[2], // United Kingdom - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 12)), - updatedAt: DateTime.now().subtract(const Duration(days: 12)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId57, - isBreaking: false, - title: 'New Tram Line Opens in Greater Manchester', - url: 'https://example.com/news/manchester-tram-line', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId57/800/600', // Manchester Evening News - source: sourcesFixturesData[11], // Manchester Evening News - eventCountry: countriesFixturesData[2], // United Kingdom - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 13)), - updatedAt: DateTime.now().subtract(const Duration(days: 13)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId58, - isBreaking: false, - title: 'Manchester Tech Hub Attracts Global Talent', - url: 'https://example.com/news/manchester-tech-hub', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId58/800/600', // Manchester Evening News - source: sourcesFixturesData[11], // Manchester Evening News - eventCountry: countriesFixturesData[2], // United Kingdom - topic: topicsFixturesData[0], // Technology - createdAt: DateTime.now().subtract(const Duration(days: 13)), - updatedAt: DateTime.now().subtract(const Duration(days: 13)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId59, - isBreaking: false, - title: 'Coronation Street Filming Causes Local Buzz', - url: 'https://example.com/news/corrie-filming', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId59/800/600', // Manchester Evening News - source: sourcesFixturesData[11], // Manchester Evening News - eventCountry: countriesFixturesData[2], // United Kingdom - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 14)), - updatedAt: DateTime.now().subtract(const Duration(days: 14)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId60, - isBreaking: false, - title: 'Council Debates Clean Air Zone Implementation', - url: 'https://example.com/news/manc-caz-debate', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId60/800/600', // Manchester Evening News - source: sourcesFixturesData[11], // Manchester Evening News - eventCountry: countriesFixturesData[2], // United Kingdom - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 14)), - updatedAt: DateTime.now().subtract(const Duration(days: 14)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId61, - isBreaking: false, - title: 'Sydney Opera House Announces New Season Lineup', - url: 'https://example.com/news/sydney-opera-season', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId61/800/600', // The Sydney Morning Herald - source: sourcesFixturesData[12], // The Sydney Morning Herald - eventCountry: countriesFixturesData[3], // Australia - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 15)), - updatedAt: DateTime.now().subtract(const Duration(days: 15)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId62, - isBreaking: false, - title: 'Housing Prices in Sydney Continue to Climb', - url: 'https://example.com/news/sydney-housing-prices', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId62/800/600', // The Sydney Morning Herald - source: sourcesFixturesData[12], // The Sydney Morning Herald - eventCountry: countriesFixturesData[3], // Australia - topic: topicsFixturesData[6], // Business - createdAt: DateTime.now().subtract(const Duration(days: 15)), - updatedAt: DateTime.now().subtract(const Duration(days: 15)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId63, - isBreaking: false, - title: 'NSW Government Unveils New Infrastructure Projects', - url: 'https://example.com/news/nsw-infrastructure', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId63/800/600', // The Sydney Morning Herald - source: sourcesFixturesData[12], // The Sydney Morning Herald - eventCountry: countriesFixturesData[3], // Australia - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 16)), - updatedAt: DateTime.now().subtract(const Duration(days: 16)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId64, - isBreaking: false, - title: 'Swans Triumph in AFL Derby Match', - url: 'https://example.com/news/swans-afl-win', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId64/800/600', // The Sydney Morning Herald - source: sourcesFixturesData[12], // The Sydney Morning Herald - eventCountry: countriesFixturesData[3], // Australia - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 16)), - updatedAt: DateTime.now().subtract(const Duration(days: 16)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId65, - isBreaking: false, - title: 'Bondi Beach Erosion Concerns Prompt Action', - url: 'https://example.com/news/bondi-erosion', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId65/800/600', // The Sydney Morning Herald - source: sourcesFixturesData[12], // The Sydney Morning Herald - eventCountry: countriesFixturesData[3], // Australia - topic: topicsFixturesData[3], // Science - createdAt: DateTime.now().subtract(const Duration(days: 17)), - updatedAt: DateTime.now().subtract(const Duration(days: 17)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId66, - isBreaking: false, - title: 'Paris Metro Expansion: New Stations Opened', - url: 'https://example.com/news/paris-metro-expansion', - imageUrl: 'https://picsum.photos/seed/kHeadlineId66/800/600', // Le Parisien - source: sourcesFixturesData[13], // Le Parisien - eventCountry: countriesFixturesData[5], // France - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 17)), - updatedAt: DateTime.now().subtract(const Duration(days: 17)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId67, - isBreaking: false, - title: 'Louvre Museum Unveils New Egyptian Antiquities Wing', - url: 'https://example.com/news/louvre-egyptian-wing', - imageUrl: 'https://picsum.photos/seed/kHeadlineId67/800/600', // Le Parisien - source: sourcesFixturesData[13], // Le Parisien - eventCountry: countriesFixturesData[5], // France - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 18)), - updatedAt: DateTime.now().subtract(const Duration(days: 18)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId68, - isBreaking: false, - title: 'Paris Saint-Germain Secures Ligue 1 Title', - url: 'https://example.com/news/psg-ligue1-title', - imageUrl: 'https://picsum.photos/seed/kHeadlineId68/800/600', // Le Parisien - source: sourcesFixturesData[13], // Le Parisien - eventCountry: countriesFixturesData[5], // France - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 18)), - updatedAt: DateTime.now().subtract(const Duration(days: 18)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId69, - isBreaking: false, - title: 'Mayor of Paris Announces New Green Initiatives', - url: 'https://example.com/news/paris-green-initiatives', - imageUrl: 'https://picsum.photos/seed/kHeadlineId69/800/600', // Le Parisien - source: sourcesFixturesData[13], // Le Parisien - eventCountry: countriesFixturesData[5], // France - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 19)), - updatedAt: DateTime.now().subtract(const Duration(days: 19)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId70, - isBreaking: false, - title: 'Paris Fashion Week Highlights New Trends', - url: 'https://example.com/news/paris-fashion-week', - imageUrl: 'https://picsum.photos/seed/kHeadlineId70/800/600', // Le Parisien - source: sourcesFixturesData[13], // Le Parisien - eventCountry: countriesFixturesData[5], // France - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 19)), - updatedAt: DateTime.now().subtract(const Duration(days: 19)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId71, - isBreaking: false, - title: 'Toronto Raptors Make Key Trade Ahead of Deadline', - url: 'https://example.com/news/raptors-trade', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId71/800/600', // The Toronto Star - source: sourcesFixturesData[14], // The Toronto Star - eventCountry: countriesFixturesData[1], // Canada - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 20)), - updatedAt: DateTime.now().subtract(const Duration(days: 20)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId72, - isBreaking: false, - title: 'TTC Announces Service Changes for Summer', - url: 'https://example.com/news/ttc-summer-changes', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId72/800/600', // The Toronto Star - source: sourcesFixturesData[14], // The Toronto Star - eventCountry: countriesFixturesData[1], // Canada - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 20)), - updatedAt: DateTime.now().subtract(const Duration(days: 20)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId73, - isBreaking: false, - title: 'Toronto International Film Festival (TIFF) Lineup Revealed', - url: 'https://example.com/news/tiff-lineup', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId73/800/600', // The Toronto Star - source: sourcesFixturesData[14], // The Toronto Star - eventCountry: countriesFixturesData[1], // Canada - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 21)), - updatedAt: DateTime.now().subtract(const Duration(days: 21)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId74, - isBreaking: false, - title: 'City of Toronto Grapples with Housing Affordability', - url: 'https://example.com/news/toronto-housing-crisis', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId74/800/600', // The Toronto Star - source: sourcesFixturesData[14], // The Toronto Star - eventCountry: countriesFixturesData[1], // Canada - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 21)), - updatedAt: DateTime.now().subtract(const Duration(days: 21)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId75, - isBreaking: false, - title: 'New Waterfront Development Project Approved', - url: 'https://example.com/news/toronto-waterfront-project', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId75/800/600', // The Toronto Star - source: sourcesFixturesData[14], // The Toronto Star - eventCountry: countriesFixturesData[1], // Canada - topic: topicsFixturesData[6], // Business - createdAt: DateTime.now().subtract(const Duration(days: 22)), - updatedAt: DateTime.now().subtract(const Duration(days: 22)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId76, - isBreaking: false, - title: 'Berlin Philharmonic Announces New Conductor', - url: 'https://example.com/news/berlin-phil-conductor', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId76/800/600', // Berliner Morgenpost - source: sourcesFixturesData[15], // Berliner Morgenpost - eventCountry: countriesFixturesData[4], // Germany - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 22)), - updatedAt: DateTime.now().subtract(const Duration(days: 22)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId77, - isBreaking: false, - title: 'Remnants of Berlin Wall Unearthed During Construction', - url: 'https://example.com/news/berlin-wall-discovery', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId77/800/600', // Berliner Morgenpost - source: sourcesFixturesData[15], // Berliner Morgenpost - eventCountry: countriesFixturesData[4], // Germany - topic: topicsFixturesData[3], // Science - createdAt: DateTime.now().subtract(const Duration(days: 23)), - updatedAt: DateTime.now().subtract(const Duration(days: 23)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId78, - isBreaking: false, - title: 'Hertha BSC Faces Relegation Battle', - url: 'https://example.com/news/hertha-bsc-relegation', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId78/800/600', // Berliner Morgenpost - source: sourcesFixturesData[15], // Berliner Morgenpost - eventCountry: countriesFixturesData[4], // Germany - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 23)), - updatedAt: DateTime.now().subtract(const Duration(days: 23)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId79, - isBreaking: false, - title: 'Berlin Senate Approves Rent Control Measures', - url: 'https://example.com/news/berlin-rent-control', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId79/800/600', // Berliner Morgenpost - source: sourcesFixturesData[15], // Berliner Morgenpost - eventCountry: countriesFixturesData[4], // Germany - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 24)), - updatedAt: DateTime.now().subtract(const Duration(days: 24)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId80, - isBreaking: false, - title: 'Brandenburg Airport Reports Record Passenger Numbers', - url: 'https://example.com/news/ber-airport-record', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId80/800/600', // Berliner Morgenpost - source: sourcesFixturesData[15], // Berliner Morgenpost - eventCountry: countriesFixturesData[4], // Germany - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 24)), - updatedAt: DateTime.now().subtract(const Duration(days: 24)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId81, - isBreaking: false, - title: 'Tokyo Government Tackles Aging Population Issues', - url: 'https://example.com/news/tokyo-aging-population', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId81/800/600', // The Asahi Shimbun (Tokyo) - source: sourcesFixturesData[16], // The Asahi Shimbun (Tokyo) - eventCountry: countriesFixturesData[6], // Japan - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 25)), - updatedAt: DateTime.now().subtract(const Duration(days: 25)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId82, - isBreaking: false, - title: 'New Shinkansen Line to Connect Tokyo and Tsuruga', - url: 'https://example.com/news/shinkansen-extension', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId82/800/600', // The Asahi Shimbun (Tokyo) - source: sourcesFixturesData[16], // The Asahi Shimbun (Tokyo) - eventCountry: countriesFixturesData[6], // Japan - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 25)), - updatedAt: DateTime.now().subtract(const Duration(days: 25)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId83, - isBreaking: false, - title: 'Yomiuri Giants Clinch Central League Pennant', - url: 'https://example.com/news/giants-win-pennant', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId83/800/600', // The Asahi Shimbun (Tokyo) - source: sourcesFixturesData[16], // The Asahi Shimbun (Tokyo) - eventCountry: countriesFixturesData[6], // Japan - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 26)), - updatedAt: DateTime.now().subtract(const Duration(days: 26)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId84, - isBreaking: false, - title: 'Studio Ghibli Announces New Film Project', - url: 'https://example.com/news/ghibli-new-film', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId84/800/600', // The Asahi Shimbun (Tokyo) - source: sourcesFixturesData[16], // The Asahi Shimbun (Tokyo) - eventCountry: countriesFixturesData[6], // Japan - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 26)), - updatedAt: DateTime.now().subtract(const Duration(days: 26)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId85, - isBreaking: false, - title: "Tokyo's Tsukiji Outer Market Thrives After Relocation", - url: 'https://example.com/news/tsukiji-market-thrives', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId85/800/600', // The Asahi Shimbun (Tokyo) - source: sourcesFixturesData[16], // The Asahi Shimbun (Tokyo) - eventCountry: countriesFixturesData[6], // Japan - topic: topicsFixturesData[8], // Food - createdAt: DateTime.now().subtract(const Duration(days: 27)), - updatedAt: DateTime.now().subtract(const Duration(days: 27)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId86, - isBreaking: false, - title: 'Mumbai Metro Expands with New Aqua Line', - url: 'https://example.com/news/mumbai-metro-aqua-line', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId86/800/600', // Hindustan Times (Mumbai) - source: sourcesFixturesData[17], // Hindustan Times (Mumbai) - eventCountry: countriesFixturesData[8], // India - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 27)), - updatedAt: DateTime.now().subtract(const Duration(days: 27)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId87, - isBreaking: false, - title: 'Bollywood Film Shoots Bring Stars to Mumbai Streets', - url: 'https://example.com/news/bollywood-mumbai-shoots', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId87/800/600', // Hindustan Times (Mumbai) - source: sourcesFixturesData[17], // Hindustan Times (Mumbai) - eventCountry: countriesFixturesData[8], // India - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 28)), - updatedAt: DateTime.now().subtract(const Duration(days: 28)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId88, - isBreaking: false, - title: 'Mumbai Indians Gear Up for IPL Season', - url: 'https://example.com/news/mumbai-indians-ipl', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId88/800/600', // Hindustan Times (Mumbai) - source: sourcesFixturesData[17], // Hindustan Times (Mumbai) - eventCountry: countriesFixturesData[8], // India - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 28)), - updatedAt: DateTime.now().subtract(const Duration(days: 28)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId89, - isBreaking: false, - title: 'BMC Tackles Monsoon Preparedness in Mumbai', - url: 'https://example.com/news/bmc-monsoon-prep', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId89/800/600', // Hindustan Times (Mumbai) - source: sourcesFixturesData[17], // Hindustan Times (Mumbai) - eventCountry: countriesFixturesData[8], // India - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 29)), - updatedAt: DateTime.now().subtract(const Duration(days: 29)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId90, - isBreaking: false, - title: "Mumbai's Financial District Sees New Investments", - url: 'https://example.com/news/mumbai-bkc-investments', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId90/800/600', // Hindustan Times (Mumbai) - source: sourcesFixturesData[17], // Hindustan Times (Mumbai) - eventCountry: countriesFixturesData[8], // India - topic: topicsFixturesData[6], // Business - createdAt: DateTime.now().subtract(const Duration(days: 29)), - updatedAt: DateTime.now().subtract(const Duration(days: 29)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId91, - isBreaking: false, - title: 'Rio Carnival Preparations in Full Swing', - url: 'https://example.com/news/rio-carnival-prep', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId91/800/600', // O Globo (Rio de Janeiro) - source: sourcesFixturesData[18], // O Globo (Rio de Janeiro) - eventCountry: countriesFixturesData[9], // Brazil - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 30)), - updatedAt: DateTime.now().subtract(const Duration(days: 30)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId92, - isBreaking: false, - title: 'Flamengo Wins Key Match at Maracanã Stadium', - url: 'https://example.com/news/flamengo-maracana-win', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId92/800/600', // O Globo (Rio de Janeiro) - source: sourcesFixturesData[18], // O Globo (Rio de Janeiro) - eventCountry: countriesFixturesData[9], // Brazil - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 30)), - updatedAt: DateTime.now().subtract(const Duration(days: 30)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId93, - isBreaking: false, - title: 'Security Boosted in Rio Ahead of Major Summit', - url: 'https://example.com/news/rio-security-boost', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId93/800/600', // O Globo (Rio de Janeiro) - source: sourcesFixturesData[18], // O Globo (Rio de Janeiro) - eventCountry: countriesFixturesData[9], // Brazil - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 31)), - updatedAt: DateTime.now().subtract(const Duration(days: 31)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId94, - isBreaking: false, - title: 'Sugarloaf Mountain Cable Car Undergoes Modernization', - url: 'https://example.com/news/sugarloaf-cable-car-update', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId94/800/600', // O Globo (Rio de Janeiro) - source: sourcesFixturesData[18], // O Globo (Rio de Janeiro) - eventCountry: countriesFixturesData[9], // Brazil - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 31)), - updatedAt: DateTime.now().subtract(const Duration(days: 31)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId95, - isBreaking: false, - title: "Bossa Nova Festival Celebrates Rio's Musical Heritage", - url: 'https://example.com/news/bossa-nova-festival', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId95/800/600', // O Globo (Rio de Janeiro) - source: sourcesFixturesData[18], // O Globo (Rio de Janeiro) - eventCountry: countriesFixturesData[9], // Brazil - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 32)), - updatedAt: DateTime.now().subtract(const Duration(days: 32)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId96, - isBreaking: false, - title: 'Sagrada Família Nears Completion After 140 Years', - url: 'https://example.com/news/sagrada-familia-completion', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId96/800/600', // La Vanguardia (Barcelona) - source: sourcesFixturesData[19], // La Vanguardia (Barcelona) - eventCountry: countriesFixturesData[10], // Spain - topic: topicsFixturesData[5], // Entertainment - createdAt: DateTime.now().subtract(const Duration(days: 32)), - updatedAt: DateTime.now().subtract(const Duration(days: 32)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId97, - isBreaking: false, - title: 'FC Barcelona Presents New Kit at Camp Nou', - url: 'https://example.com/news/fcb-new-kit', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId97/800/600', // La Vanguardia (Barcelona) - source: sourcesFixturesData[19], // La Vanguardia (Barcelona) - eventCountry: countriesFixturesData[10], // Spain - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 33)), - updatedAt: DateTime.now().subtract(const Duration(days: 33)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId98, - isBreaking: false, - title: 'Catalan Government Discusses Tourism Strategy', - url: 'https://example.com/news/catalan-tourism-strategy', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId98/800/600', // La Vanguardia (Barcelona) - source: sourcesFixturesData[19], // La Vanguardia (Barcelona) - eventCountry: countriesFixturesData[10], // Spain - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 33)), - updatedAt: DateTime.now().subtract(const Duration(days: 33)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId99, - isBreaking: false, - title: "Barcelona's Tech Scene Booms with New Hub", - url: 'https://example.com/news/barcelona-tech-hub', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId99/800/600', // La Vanguardia (Barcelona) - source: sourcesFixturesData[19], // La Vanguardia (Barcelona) - eventCountry: countriesFixturesData[10], // Spain - topic: topicsFixturesData[0], // Technology - createdAt: DateTime.now().subtract(const Duration(days: 34)), - updatedAt: DateTime.now().subtract(const Duration(days: 34)), - status: ContentStatus.active, - ), - Headline( - id: kHeadlineId100, - isBreaking: false, - title: 'La Boqueria Market: A Taste of Barcelona', - url: 'https://example.com/news/la-boqueria-feature', - imageUrl: - 'https://picsum.photos/seed/kHeadlineId100/800/600', // La Vanguardia (Barcelona) - source: sourcesFixturesData[19], // La Vanguardia (Barcelona) - eventCountry: countriesFixturesData[10], // Spain - topic: topicsFixturesData[8], // Food - createdAt: DateTime.now().subtract(const Duration(days: 34)), - updatedAt: DateTime.now().subtract(const Duration(days: 34)), - status: ContentStatus.active, - ), + final headlines = []; + for (var i = 0; i < _headlineIds.length; i++) { + final id = _headlineIds[i]; + final title = + _titlesByLang[resolvedLanguageCode]![i % _titlesByLang['en']!.length]; + final source = sources[i % sources.length]; + final topic = topics[i % topics.length]; + final country = countriesFixturesData[i % countriesFixturesData.length]; - // --- National News Outlets (kSourceId21 - kSourceId30) --- - // ... (Headlines for kSourceId21 to kSourceId30 would follow the same pattern) - // To keep the response size manageable, I will add a placeholder comment here. - // In a real implementation, 50 headlines for these 10 sources would be added. - // Example for USA Today: - Headline( - id: kHeadlineId101, - isBreaking: false, - title: 'National Parks See Record Visitor Numbers', - url: 'https://example.com/news/national-parks-visitors', - imageUrl: 'https://picsum.photos/seed/kHeadlineId101/800/600', - source: sourcesFixturesData[20], // USA Today - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[7], // Travel - createdAt: DateTime.now().subtract(const Duration(days: 35)), - updatedAt: DateTime.now().subtract(const Duration(days: 35)), - status: ContentStatus.active, - ), - // ... 4 more for USA Today + headlines.add( + Headline( + id: id, + isBreaking: i % 10 == 3, // Make some headlines breaking + title: title, + url: 'https://example.com/news/${id.substring(0, 8)}', + imageUrl: 'https://picsum.photos/seed/$id/800/600', + source: source, + eventCountry: country, + topic: topic, + createdAt: referenceTime.subtract(Duration(minutes: i * 15)), + updatedAt: referenceTime.subtract(Duration(minutes: i * 15)), + status: ContentStatus.active, + ), + ); + } + return headlines; +} - // Example for The Globe and Mail: - Headline( - id: kHeadlineId106, - isBreaking: false, - title: 'Canadian Government Announces New Federal Budget', - url: 'https://example.com/news/canada-federal-budget', - imageUrl: 'https://picsum.photos/seed/kHeadlineId106/800/600', - source: sourcesFixturesData[21], // The Globe and Mail - eventCountry: countriesFixturesData[1], // Canada - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 36)), - updatedAt: DateTime.now().subtract(const Duration(days: 36)), - status: ContentStatus.active, - ), - // ... 4 more for The Globe and Mail - - // --- International News Outlets (kSourceId31 - kSourceId40) --- - // ... (Headlines for kSourceId31 to kSourceId40 would follow the same pattern) - // Example for CNN International: - Headline( - id: kHeadlineId151, - isBreaking: false, - title: 'Global Supply Chain Issues Persist', - url: 'https://example.com/news/global-supply-chain', - imageUrl: 'https://picsum.photos/seed/kHeadlineId151/800/600', - source: sourcesFixturesData[30], // CNN International - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[6], // Business - createdAt: DateTime.now().subtract(const Duration(days: 45)), - updatedAt: DateTime.now().subtract(const Duration(days: 45)), - status: ContentStatus.active, - ), - // ... 4 more for CNN International - - // --- Specialized Publishers (kSourceId41 - kSourceId50) --- - // Example for ESPN: - Headline( - id: kHeadlineId201, - isBreaking: false, - title: 'World Cup Finals: An Unforgettable Match', - url: 'https://example.com/news/world-cup-final', - imageUrl: 'https://picsum.photos/seed/kHeadlineId201/800/600', - source: sourcesFixturesData[40], // ESPN - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[1], // Sports - createdAt: DateTime.now().subtract(const Duration(days: 55)), - updatedAt: DateTime.now().subtract(const Duration(days: 55)), - status: ContentStatus.active, - ), - // ... 4 more for ESPN - - // --- Blogs (kSourceId51 - kSourceId60) --- - // Example for Stratechery: - Headline( - id: kHeadlineId251, - isBreaking: false, - title: 'The Future of Content and Aggregation', - url: 'https://example.com/news/stratechery-content-ai', - imageUrl: 'https://picsum.photos/seed/kHeadlineId251/800/600', - source: sourcesFixturesData[50], // Stratechery by Ben Thompson - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[0], // Technology - createdAt: DateTime.now().subtract(const Duration(days: 65)), - updatedAt: DateTime.now().subtract(const Duration(days: 65)), - status: ContentStatus.active, - ), - // ... 4 more for Stratechery - - // --- Government Sources (kSourceId61 - kSourceId70) --- - // Example for WhiteHouse.gov: - Headline( - id: kHeadlineId301, - isBreaking: false, - title: 'President Signs Executive Order on Cybersecurity', - url: 'https://example.com/news/wh-cyber-order', - imageUrl: 'https://picsum.photos/seed/kHeadlineId301/800/600', - source: sourcesFixturesData[60], // WhiteHouse.gov - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[2], // Politics - createdAt: DateTime.now().subtract(const Duration(days: 75)), - updatedAt: DateTime.now().subtract(const Duration(days: 75)), - status: ContentStatus.active, - ), - // ... 4 more for WhiteHouse.gov - - // --- Aggregators (kSourceId71 - kSourceId80) --- - // Example for Google News: - Headline( - id: kHeadlineId351, - isBreaking: false, - title: 'This Week in Tech: A Google News Roundup', - url: 'https://example.com/news/gnews-tech-roundup', - imageUrl: 'https://picsum.photos/seed/kHeadlineId351/800/600', - source: sourcesFixturesData[70], // Google News - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[0], // Technology - createdAt: DateTime.now().subtract(const Duration(days: 85)), - updatedAt: DateTime.now().subtract(const Duration(days: 85)), - status: ContentStatus.active, - ), - // ... 4 more for Google News - - // --- Other (kSourceId81 - kSourceId90) --- - // Example for PR Newswire: - Headline( - id: kHeadlineId401, - isBreaking: false, - title: 'Global Tech Corp Announces Record Quarterly Earnings', - url: 'https://example.com/news/prn-earnings', - imageUrl: 'https://picsum.photos/seed/kHeadlineId401/800/600', - source: sourcesFixturesData[80], // PR Newswire - eventCountry: countriesFixturesData[0], // United States - topic: topicsFixturesData[6], // Business - createdAt: DateTime.now().subtract(const Duration(days: 95)), - updatedAt: DateTime.now().subtract(const Duration(days: 95)), - status: ContentStatus.active, - ), - // Example for The Lancet: - Headline( - id: kHeadlineId411, - isBreaking: false, - title: 'Phase 3 Trial Results for New Diabetes Drug Published', - url: 'https://example.com/news/lancet-diabetes-drug', - imageUrl: 'https://picsum.photos/seed/kHeadlineId411/800/600', - source: sourcesFixturesData[82], // The Lancet - eventCountry: countriesFixturesData[2], // United Kingdom - topic: topicsFixturesData[4], // Health - createdAt: DateTime.now().subtract(const Duration(days: 100)), - updatedAt: DateTime.now().subtract(const Duration(days: 100)), - status: ContentStatus.active, - ), +const List _headlineIds = [ + kHeadlineId1, kHeadlineId2, kHeadlineId3, kHeadlineId4, kHeadlineId5, + kHeadlineId6, kHeadlineId7, kHeadlineId8, kHeadlineId9, kHeadlineId10, + kHeadlineId11, kHeadlineId12, kHeadlineId13, kHeadlineId14, kHeadlineId15, + kHeadlineId16, kHeadlineId17, kHeadlineId18, kHeadlineId19, kHeadlineId20, + kHeadlineId21, kHeadlineId22, kHeadlineId23, kHeadlineId24, kHeadlineId25, + kHeadlineId26, kHeadlineId27, kHeadlineId28, kHeadlineId29, kHeadlineId30, + kHeadlineId31, kHeadlineId32, kHeadlineId33, kHeadlineId34, kHeadlineId35, + kHeadlineId36, kHeadlineId37, kHeadlineId38, kHeadlineId39, kHeadlineId40, + kHeadlineId41, kHeadlineId42, kHeadlineId43, kHeadlineId44, kHeadlineId45, + kHeadlineId46, kHeadlineId47, kHeadlineId48, kHeadlineId49, kHeadlineId50, + kHeadlineId51, kHeadlineId52, kHeadlineId53, kHeadlineId54, kHeadlineId55, + kHeadlineId56, kHeadlineId57, kHeadlineId58, kHeadlineId59, kHeadlineId60, + kHeadlineId61, kHeadlineId62, kHeadlineId63, kHeadlineId64, kHeadlineId65, + kHeadlineId66, kHeadlineId67, kHeadlineId68, kHeadlineId69, kHeadlineId70, + kHeadlineId71, kHeadlineId72, kHeadlineId73, kHeadlineId74, kHeadlineId75, + kHeadlineId76, kHeadlineId77, kHeadlineId78, kHeadlineId79, kHeadlineId80, + kHeadlineId81, kHeadlineId82, kHeadlineId83, kHeadlineId84, kHeadlineId85, + kHeadlineId86, kHeadlineId87, kHeadlineId88, kHeadlineId89, kHeadlineId90, + kHeadlineId91, kHeadlineId92, kHeadlineId93, kHeadlineId94, kHeadlineId95, + kHeadlineId96, kHeadlineId97, kHeadlineId98, kHeadlineId99, kHeadlineId100, + // Add more IDs if needed, up to kHeadlineId450 ]; + +final Map> _titlesByLang = { + 'en': [ + 'AI Breakthrough: New Model Achieves Human-Level Performance', + 'Local Team Wins Championship in Thrilling Final', + 'Global Leaders Meet to Discuss Climate Change Policies', + 'New Planet Discovered in Distant Galaxy', + 'Breakthrough in Cancer Research Offers New Hope', + 'Blockbuster Movie Breaks Box Office Records', + 'Stock Market Reaches All-Time High Amid Economic Boom', + 'New Travel Restrictions Lifted for Popular Destinations', + 'Michelin Star Chef Opens New Restaurant in City Center', + 'Innovative Teaching Methods Boost Student Engagement', + 'Cybersecurity Firms Warn of New Global Threat', + 'Olympics Committee Announces Host City for 2032 Games', + 'New Bill Aims to Reform Healthcare System', + 'Archaeologists Uncover Ancient City Ruins', + 'Dietary Guidelines Updated for Public Health', + 'Music Festival Announces Star-Studded Lineup', + 'Tech Giant Acquires Startup in Multi-Billion Dollar Deal', + 'Space Tourism Takes Off: First Commercial Flights Announced', + 'Future of Food: Lab-Grown Meat Gains Popularity', + 'Online Learning Platforms See Surge in Enrollment', + 'Quantum Computing Achieves New Milestone', + 'World Cup Qualifiers: Unexpected Upsets Shake Rankings', + 'Election Results: New Government Takes Power', + 'Breakthrough in Fusion Energy Research Announced', + 'Mental Health Awareness Campaign Launched Globally', + 'Gaming Industry Sees Record Growth in Virtual Reality', + 'Global Supply Chain Disruptions Impacting Consumer Goods', + 'Arctic Expedition Discovers New Marine Species', + 'Rise of Plant-Based Cuisine: New Restaurants Open', + 'Education Technology Transforms Classrooms', + 'SpaceX Launches New Satellite Constellation', + 'Football Legend Announces Retirement', + 'G7 Summit Concludes with Joint Statement on Global Economy', + "Breakthrough in Alzheimer's Research Offers New Treatment Path", + 'Global Vaccination Campaign Reaches Billions', + 'Streaming Wars Intensify with New Platform Launches', + 'Cryptocurrency Market Experiences Major Volatility', + 'Sustainable Tourism Initiatives Gain Momentum', + 'Food Security Summit Addresses Global Hunger', + 'Robotics in Education: New Tools for Learning', + 'AI Ethics Debate Intensifies Among Tech Leaders', + 'Esports Industry Sees Massive Investment Boom', + 'International Sanctions Imposed on Rogue State', + 'New Species of Deep-Sea Creature Discovered', + 'Global Health Crisis: New Pandemic Preparedness Plan', + 'Hollywood Strikes Continue: Impact on Film Production', + 'Emerging Markets Show Strong Economic Resilience', + 'Adventure Tourism Booms in Remote Regions', + 'The Rise of Sustainable Food Packaging', + 'Personalized Learning: Tailoring Education to Individual Needs', + ], + 'ar': [ + 'إنجاز في الذكاء الاصطناعي: نموذج جديد يحقق أداءً على المستوى البشري', + 'الفريق المحلي يفوز بالبطولة في نهائي مثير', + 'قادة العالم يجتمعون لمناقشة سياسات تغير المناخ', + 'اكتشاف كوكب جديد في مجرة بعيدة', + 'تقدم في أبحاث السرطان يقدم أملاً جديدًا', + 'فيلم ضخم يحطم الأرقام القياسية في شباك التذاكر', + 'سوق الأسهم يصل إلى أعلى مستوى له على الإطلاق وسط ازدهار اقتصادي', + 'رفع قيود السفر الجديدة عن وجهات شهيرة', + 'شيف حائز على نجمة ميشلان يفتتح مطعمًا جديدًا في وسط المدينة', + 'طرق التدريس المبتكرة تعزز مشاركة الطلاب', + 'شركات الأمن السيبراني تحذر من تهديد عالمي جديد', + 'اللجنة الأولمبية تعلن عن المدينة المضيفة لألعاب 2032', + 'مشروع قانون جديد يهدف إلى إصلاح نظام الرعاية الصحية', + 'علماء الآثار يكشفون عن أطلال مدينة قديمة', + 'تحديث المبادئ التوجيهية الغذائية للصحة العامة', + 'مهرجان موسيقي يعلن عن قائمة نجوم مرصعة بالنجوم', + 'عملاق التكنولوجيا يستحوذ على شركة ناشئة في صفقة بمليارات الدولارات', + 'السياحة الفضائية تنطلق: الإعلان عن أولى الرحلات التجارية', + 'مستقبل الغذاء: اللحوم المزروعة في المختبر تكتسب شعبية', + 'منصات التعلم عبر الإنترنت تشهد طفرة في التسجيل', + 'الحوسبة الكمومية تحقق إنجازًا جديدًا', + 'تصفيات كأس العالم: مفاجآت غير متوقعة تهز التصنيفات', + 'نتائج الانتخابات: حكومة جديدة تتولى السلطة', + 'الإعلان عن تقدم كبير في أبحاث طاقة الاندماج', + 'إطلاق حملة توعية بالصحة النفسية على مستوى العالم', + 'صناعة الألعاب تشهد نموًا قياسيًا في الواقع الافتراضي', + 'اضطرابات سلسلة التوريد العالمية تؤثر على السلع الاستهلاكية', + 'بعثة استكشافية في القطب الشمالي تكتشف أنواعًا بحرية جديدة', + 'صعود المطبخ النباتي: افتتاح مطاعم جديدة', + 'تكنولوجيا التعليم تغير الفصول الدراسية', + 'سبيس إكس تطلق كوكبة أقمار صناعية جديدة', + 'أسطورة كرة القدم يعلن اعتزاله', + 'قمة مجموعة السبع تختتم ببيان مشترك حول الاقتصاد العالمي', + 'تقدم في أبحاث الزهايمر يقدم مسارًا علاجيًا جديدًا', + 'حملة التطعيم العالمية تصل إلى المليارات', + 'حروب البث تشتد مع إطلاق منصات جديدة', + 'سوق العملات المشفرة يشهد تقلبات كبيرة', + 'مبادرات السياحة المستدامة تكتسب زخمًا', + 'قمة الأمن الغذائي تتناول الجوع العالمي', + 'الروبوتات في التعليم: أدوات جديدة للتعلم', + 'جدل أخلاقيات الذكاء الاصطناعي يشتد بين قادة التكنولوجيا', + 'صناعة الرياضات الإلكترونية تشهد طفرة استثمارية هائلة', + 'فرض عقوبات دولية على دولة مارقة', + 'اكتشاف أنواع جديدة من مخلوقات أعماق البحار', + 'أزمة صحية عالمية: خطة جديدة للتأهب للأوبئة', + 'إضرابات هوليوود مستمرة: التأثير على إنتاج الأفلام', + 'الأسواق الناشئة تظهر مرونة اقتصادية قوية', + 'ازدهار سياحة المغامرات في المناطق النائية', + 'صعود أغلفة المواد الغذائية المستدامة', + 'التعلم المخصص: تكييف التعليم مع الاحتياجات الفردية', + ], +}; diff --git a/lib/src/fixtures/in_app_notifications.dart b/lib/src/fixtures/in_app_notifications.dart index 3cefa960..00686ac2 100644 --- a/lib/src/fixtures/in_app_notifications.dart +++ b/lib/src/fixtures/in_app_notifications.dart @@ -17,10 +17,10 @@ List _generateAdminNotifications() { 21, (index) => 'in_app_notification_${index + 1}', ); - final headlineIds = headlinesFixturesData.map((e) => e.id).toList(); + final headlineIds = getHeadlinesFixturesData().map((e) => e.id).toList(); return List.generate(21, (index) { - final headline = headlinesFixturesData[index % headlineIds.length]; + final headline = getHeadlinesFixturesData()[index % headlineIds.length]; final notificationId = notificationIds[index]; final isRead = index > 3; diff --git a/lib/src/fixtures/remote_configs.dart b/lib/src/fixtures/remote_configs.dart index 46b642cb..63d59759 100644 --- a/lib/src/fixtures/remote_configs.dart +++ b/lib/src/fixtures/remote_configs.dart @@ -65,6 +65,21 @@ final remoteConfigsFixturesData = [ AppUserRole.standardUser: SavedFilterLimits(total: 10, pinned: 5), AppUserRole.premiumUser: SavedFilterLimits(total: 25, pinned: 10), }, + reactionsPerDay: { + AppUserRole.guestUser: 20, + AppUserRole.standardUser: 100, + AppUserRole.premiumUser: 500, + }, + commentsPerDay: { + AppUserRole.guestUser: 0, + AppUserRole.standardUser: 10, + AppUserRole.premiumUser: 50, + }, + reportsPerDay: { + AppUserRole.guestUser: 1, + AppUserRole.standardUser: 5, + AppUserRole.premiumUser: 20, + }, ), ), features: const FeaturesConfig( @@ -199,6 +214,23 @@ final remoteConfigsFixturesData = [ PushNotificationSubscriptionDeliveryType.weeklyRoundup: true, }, ), + community: CommunityConfig( + engagement: EngagementConfig( + enabled: true, + engagementMode: EngagementMode.reactionsAndComments, + ), + reporting: ReportingConfig( + headlineReportingEnabled: true, + sourceReportingEnabled: true, + commentReportingEnabled: true, + ), + appReview: AppReviewConfig( + // User must perform 5 positive actions (e.g., save headline) + // to become eligible for the review prompt. + positiveInteractionThreshold: 5, + initialPromptCooldownDays: 14, + ), + ), ), ), ]; diff --git a/lib/src/fixtures/reports.dart b/lib/src/fixtures/reports.dart new file mode 100644 index 00000000..656f3217 --- /dev/null +++ b/lib/src/fixtures/reports.dart @@ -0,0 +1,93 @@ +import 'package:core/core.dart'; + +/// Generates a list of predefined reports for fixture data. +/// +/// This creates 1 report for each of the first 10 users, targeting a mix of +/// headlines, sources, and comments. +/// +/// The optional [now] parameter allows for creating deterministic timestamps, +/// which is essential for testing. +List getReportsFixturesData({DateTime? now}) { + final reports = []; + final referenceTime = now ?? DateTime.now(); + final users = usersFixturesData.take(10).toList(); + final headlines = getHeadlinesFixturesData( + now: referenceTime, + ).take(10).toList(); + final engagementsWithComments = getEngagementsFixturesData( + now: referenceTime, + ).where((e) => e.comment != null).toList(); + final reportIds = [ + kReportId1, + kReportId2, + kReportId3, + kReportId4, + kReportId5, + kReportId6, + kReportId7, + kReportId8, + kReportId9, + kReportId10, + ]; + const headlineReasons = HeadlineReportReason.values; + const sourceReasons = SourceReportReason.values; + const commentReasons = CommentReportReason.values; + + for (var i = 0; i < users.length; i++) { + final user = users[i]; + final headline = headlines[i]; + var status = ReportStatus.submitted; + if (i % 3 == 0) { + status = ReportStatus.inReview; + } else if (i % 5 == 0) { + status = ReportStatus.resolved; + } + + // Create a mix of report types + if (i < 5) { + // Report on Headlines + reports.add( + Report( + id: reportIds[i], + reporterUserId: user.id, + entityType: ReportableEntity.headline, + entityId: headline.id, + reason: headlineReasons[i % headlineReasons.length].name, + additionalComments: 'This headline seems misleading.', + status: status, + createdAt: referenceTime.subtract(Duration(days: i)), + ), + ); + } else if (i < 8) { + // Report on Sources + reports.add( + Report( + id: reportIds[i], + reporterUserId: user.id, + entityType: ReportableEntity.source, + entityId: getSourcesFixturesData()[i].id, + reason: sourceReasons[i % sourceReasons.length].name, + additionalComments: 'This source has too many ads.', + status: status, + createdAt: referenceTime.subtract(Duration(days: i)), + ), + ); + } else { + // Report on Comments + reports.add( + Report( + id: reportIds[i], + reporterUserId: user.id, + entityType: ReportableEntity.engagement, + entityId: engagementsWithComments[i].id, + reason: commentReasons[i % commentReasons.length].name, + additionalComments: 'This comment is spam.', + status: status, + createdAt: referenceTime.subtract(Duration(days: i)), + ), + ); + } + } + + return reports; +} diff --git a/lib/src/fixtures/saved_headline_filters.dart b/lib/src/fixtures/saved_headline_filters.dart index d90c14c2..5a22c653 100644 --- a/lib/src/fixtures/saved_headline_filters.dart +++ b/lib/src/fixtures/saved_headline_filters.dart @@ -1,33 +1,47 @@ import 'package:core/core.dart'; -/// A list of predefined saved headline filters for fixture data. -final savedHeadlineFiltersFixturesData = [ - SavedHeadlineFilter( - id: kSavedHeadlineFilterId1, - userId: kAdminUserId, - name: 'US Tech News', - isPinned: true, - deliveryTypes: const { - PushNotificationSubscriptionDeliveryType.breakingOnly, - }, - criteria: HeadlineFilterCriteria( - topics: [topicsFixturesData[0]], // Technology - sources: const [], - countries: [countriesFixturesData[0]], // United States +/// Generates a list of predefined saved headline filters for fixture data. +List getSavedHeadlineFiltersFixturesData({ + String languageCode = 'en', +}) { + final resolvedLanguageCode = ['en', 'ar'].contains(languageCode) + ? languageCode + : 'en'; + final topics = getTopicsFixturesData(languageCode: resolvedLanguageCode); + + final namesByLang = { + 'en': ['US Tech News', 'Global Business'], + 'ar': ['أخبار التكنولوجيا الأمريكية', 'أعمال عالمية'], + }; + + return [ + SavedHeadlineFilter( + id: kSavedHeadlineFilterId1, + userId: kAdminUserId, + name: namesByLang[resolvedLanguageCode]![0], + isPinned: true, + deliveryTypes: const { + PushNotificationSubscriptionDeliveryType.breakingOnly, + }, + criteria: HeadlineFilterCriteria( + topics: [topics[0]], // Technology + sources: const [], + countries: [countriesFixturesData[0]], // United States + ), ), - ), - SavedHeadlineFilter( - id: kSavedHeadlineFilterId2, - userId: kUser1Id, - name: 'Global Business', - isPinned: false, - deliveryTypes: const { - PushNotificationSubscriptionDeliveryType.breakingOnly, - }, - criteria: HeadlineFilterCriteria( - topics: [topicsFixturesData[6]], // Business - sources: const [], - countries: const [], + SavedHeadlineFilter( + id: kSavedHeadlineFilterId2, + userId: kUser1Id, + name: namesByLang[resolvedLanguageCode]![1], + isPinned: false, + deliveryTypes: const { + PushNotificationSubscriptionDeliveryType.breakingOnly, + }, + criteria: HeadlineFilterCriteria( + topics: [topics[6]], // Business + sources: const [], + countries: const [], + ), ), - ), -]; + ]; +} diff --git a/lib/src/fixtures/saved_source_filters.dart b/lib/src/fixtures/saved_source_filters.dart index 3458301a..b8dcc307 100644 --- a/lib/src/fixtures/saved_source_filters.dart +++ b/lib/src/fixtures/saved_source_filters.dart @@ -1,27 +1,40 @@ import 'package:core/core.dart'; -/// A list of predefined saved source filters for fixture data. -final savedSourceFiltersFixturesData = [ - SavedSourceFilter( - id: kSavedSourceFilterId1, - userId: kAdminUserId, - name: 'UK News Agencies', - isPinned: true, - criteria: SourceFilterCriteria( - sourceTypes: const [SourceType.newsAgency], - languages: const [], - countries: [countriesFixturesData[1]], // United Kingdom +/// Generates a list of predefined saved source filters for fixture data. +List getSavedSourceFiltersFixturesData({ + String languageCode = 'en', +}) { + final resolvedLanguageCode = ['en', 'ar'].contains(languageCode) + ? languageCode + : 'en'; + + final namesByLang = { + 'en': ['UK News Agencies', 'German Tech Blogs'], + 'ar': ['وكالات الأنباء البريطانية', 'مدونات التكنولوجيا الألمانية'], + }; + + return [ + SavedSourceFilter( + id: kSavedSourceFilterId1, + userId: kAdminUserId, + name: namesByLang[resolvedLanguageCode]![0], + isPinned: true, + criteria: SourceFilterCriteria( + sourceTypes: const [SourceType.newsAgency], + languages: const [], + countries: [countriesFixturesData[1]], // United Kingdom + ), ), - ), - SavedSourceFilter( - id: kSavedSourceFilterId2, - userId: kUser1Id, - name: 'German Tech Blogs', - isPinned: false, - criteria: SourceFilterCriteria( - sourceTypes: const [SourceType.blog, SourceType.specializedPublisher], - languages: [languagesFixturesData.firstWhere((l) => l.code == 'de')], - countries: [countriesFixturesData[4]], // Germany + SavedSourceFilter( + id: kSavedSourceFilterId2, + userId: kUser1Id, + name: namesByLang[resolvedLanguageCode]![1], + isPinned: false, + criteria: SourceFilterCriteria( + sourceTypes: const [SourceType.blog, SourceType.specializedPublisher], + languages: [languagesFixturesData.firstWhere((l) => l.code == 'de')], + countries: [countriesFixturesData[4]], // Germany + ), ), - ), -]; + ]; +} diff --git a/lib/src/fixtures/sources.dart b/lib/src/fixtures/sources.dart index ea562d9e..c3605455 100644 --- a/lib/src/fixtures/sources.dart +++ b/lib/src/fixtures/sources.dart @@ -4,1179 +4,887 @@ import 'package:core/src/fixtures/fixture_ids.dart'; import 'package:core/src/fixtures/languages.dart'; import 'package:core/src/models/entities/source.dart'; -/// A list of predefined sources for fixture data. -final sourcesFixturesData = [ - Source( - id: kSourceId1, - name: 'TechCrunch', - description: 'Leading online publisher of technology news.', - url: 'https://techcrunch.com', - logoUrl: 'https://api.companyenrich.com/logo/techcrunch.com', - sourceType: SourceType.newsAgency, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-01-01T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-01T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId2, - name: 'BBC News', - description: 'Breaking news, sport, TV, radio and a whole lot more.', - url: 'https://www.bbc.com/news', - logoUrl: 'https://api.companyenrich.com/logo/bbc.com', - sourceType: SourceType.newsAgency, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[2], // United Kingdom - createdAt: DateTime.parse('2023-01-02T11:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-02T11:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId3, - name: 'The New York Times', - description: 'Breaking News, World News & Multimedia.', - url: 'https://www.nytimes.com', - logoUrl: 'https://api.companyenrich.com/logo/nytimes.com', - sourceType: SourceType.newsAgency, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-01-03T12:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-03T12:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId4, - name: 'The Guardian', - description: - 'Latest news, sport, business, comment and reviews from the Guardian.', - url: 'https://www.theguardian.com', - logoUrl: 'https://api.companyenrich.com/logo/theguardian.com', - sourceType: SourceType.newsAgency, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[2], // United Kingdom - createdAt: DateTime.parse('2023-01-04T13:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-04T13:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId5, - name: 'CNN', - description: 'Breaking News, Latest News and Videos.', - url: 'https://edition.cnn.com', - logoUrl: 'https://api.companyenrich.com/logo/cnn.com', - sourceType: SourceType.newsAgency, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-01-05T14:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-05T14:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId6, - name: 'Reuters', - description: 'Business, financial, national and international news.', - url: 'https://www.reuters.com', - logoUrl: 'https://api.companyenrich.com/logo/reuters.com', - sourceType: SourceType.newsAgency, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-01-06T15:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-06T15:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId7, - name: 'Al Jazeera English', - description: - 'News, analysis, and opinion from the Middle East and around the world.', - url: 'https://www.aljazeera.com', - logoUrl: 'https://api.companyenrich.com/logo/aljazeera.com', - sourceType: SourceType.newsAgency, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: - countriesFixturesData[0], // United States (assuming for simplicity, actual is Qatar) - createdAt: DateTime.parse('2023-01-07T16:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-07T16:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId8, - name: 'Xinhua News Agency', - description: "Official press agency of the People's Republic of China.", - url: 'http://www.xinhuanet.com/english/', - logoUrl: 'https://api.companyenrich.com/logo/xinhuanet.com', - sourceType: SourceType.newsAgency, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[7], // China - createdAt: DateTime.parse('2023-01-08T17:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-08T17:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId9, - name: 'The Times of India', - description: 'Latest and Breaking News from India.', - url: 'https://timesofindia.indiatimes.com/', - logoUrl: 'https://api.companyenrich.com/logo/indiatimes.com', - sourceType: SourceType.newsAgency, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[8], // India - createdAt: DateTime.parse('2023-01-09T18:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-09T18:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId10, - name: 'Folha de S.Paulo', - description: 'Brazilian daily newspaper.', - url: 'https://www.folha.uol.com.br/', - logoUrl: 'https://api.companyenrich.com/logo/uol.com.br', - sourceType: SourceType.newsAgency, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'pt'), - headquarters: countriesFixturesData[9], // Brazil - createdAt: DateTime.parse('2023-01-10T19:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-10T19:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId11, - name: 'San Francisco Chronicle', - description: 'News from the San Francisco Bay Area.', - url: 'https://www.sfchronicle.com', - logoUrl: 'https://api.companyenrich.com/logo/sfchronicle.com', - sourceType: SourceType.localNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-02-01T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-02-01T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId12, - name: 'Manchester Evening News', - description: 'Covering Greater Manchester, UK.', - url: 'https://www.manchestereveningnews.co.uk', - logoUrl: 'https://api.companyenrich.com/logo/manchestereveningnews.co.uk', - sourceType: SourceType.localNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[2], // United Kingdom - createdAt: DateTime.parse('2023-02-02T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-02-02T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId13, - name: 'The Sydney Morning Herald', - description: 'Independent journalism for Sydney, Australia.', - url: 'https://www.smh.com.au', - logoUrl: 'https://api.companyenrich.com/logo/smh.com.au', - sourceType: SourceType.localNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[3], // Australia - createdAt: DateTime.parse('2023-02-03T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-02-03T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId14, - name: 'Le Parisien', - description: 'Local news for Paris and the Île-de-France region.', - url: 'https://www.leparisien.fr', - logoUrl: 'https://api.companyenrich.com/logo/leparisien.fr', - sourceType: SourceType.localNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'fr'), - headquarters: countriesFixturesData[5], // France - createdAt: DateTime.parse('2023-02-04T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-02-04T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId15, - name: 'The Toronto Star', - description: 'News and stories for Toronto, Canada.', - url: 'https://www.thestar.com', - logoUrl: 'https://api.companyenrich.com/logo/thestar.com', - sourceType: SourceType.localNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[1], // Canada - createdAt: DateTime.parse('2023-02-05T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-02-05T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId16, - name: 'Berliner Morgenpost', - description: 'Daily news for Berlin, Germany.', - url: 'https://www.morgenpost.de', - logoUrl: 'https://api.companyenrich.com/logo/morgenpost.de', - sourceType: SourceType.localNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'de'), - headquarters: countriesFixturesData[4], // Germany - createdAt: DateTime.parse('2023-02-06T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-02-06T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId17, - name: 'The Asahi Shimbun (Tokyo)', - description: 'Local and national news from a Tokyo perspective.', - url: 'https://www.asahi.com/area/tokyo/', - logoUrl: 'https://api.companyenrich.com/logo/asahi.com', - sourceType: SourceType.localNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'ja'), - headquarters: countriesFixturesData[6], // Japan - createdAt: DateTime.parse('2023-02-07T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-02-07T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId18, - name: 'Hindustan Times (Mumbai)', - description: 'Latest news from Mumbai, India.', - url: 'https://www.hindustantimes.com/mumbai-news', - logoUrl: 'https://api.companyenrich.com/logo/hindustantimes.com', - sourceType: SourceType.localNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[8], // India - createdAt: DateTime.parse('2023-02-08T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-02-08T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId19, - name: 'O Globo (Rio de Janeiro)', - description: 'News from Rio de Janeiro, Brazil.', - url: 'https://oglobo.globo.com/rio/', - logoUrl: 'https://api.companyenrich.com/logo/globo.com', - sourceType: SourceType.localNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'pt'), - headquarters: countriesFixturesData[9], // Brazil - createdAt: DateTime.parse('2023-02-09T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-02-09T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId20, - name: 'La Vanguardia (Barcelona)', - description: 'News from Barcelona and Catalonia, Spain.', - url: 'https://www.lavanguardia.com/local/barcelona', - logoUrl: 'https://api.companyenrich.com/logo/lavanguardia.com', - sourceType: SourceType.localNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'es'), - headquarters: countriesFixturesData[10], // Spain - createdAt: DateTime.parse('2023-02-10T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-02-10T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId21, - name: 'USA Today', - description: 'National news from across the United States.', - url: 'https://www.usatoday.com', - logoUrl: 'https://api.companyenrich.com/logo/usatoday.com', - sourceType: SourceType.nationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-03-01T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-03-01T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId22, - name: 'The Globe and Mail', - description: "Canada's national newspaper.", - url: 'https://www.theglobeandmail.com', - logoUrl: 'https://api.companyenrich.com/logo/theglobeandmail.com', - sourceType: SourceType.nationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[1], // Canada - createdAt: DateTime.parse('2023-03-02T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-03-02T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId23, - name: 'The Australian', - description: 'National news of Australia.', - url: 'https://www.theaustralian.com.au', - logoUrl: 'https://api.companyenrich.com/logo/theaustralian.com.au', - sourceType: SourceType.nationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[3], // Australia - createdAt: DateTime.parse('2023-03-03T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-03-03T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId24, - name: 'Le Monde', - description: 'French national daily newspaper.', - url: 'https://www.lemonde.fr', - logoUrl: 'https://api.companyenrich.com/logo/lemonde.fr', - sourceType: SourceType.nationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'fr'), - headquarters: countriesFixturesData[5], // France - createdAt: DateTime.parse('2023-03-04T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-03-04T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId25, - name: 'Frankfurter Allgemeine Zeitung', - description: 'German national newspaper.', - url: 'https://www.faz.net', - logoUrl: 'https://api.companyenrich.com/logo/faz.net', - sourceType: SourceType.nationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'de'), - headquarters: countriesFixturesData[4], // Germany - createdAt: DateTime.parse('2023-03-05T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-03-05T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId26, - name: 'The Yomiuri Shimbun', - description: 'Japanese national newspaper.', - url: 'https://www.yomiuri.co.jp', - logoUrl: 'https://api.companyenrich.com/logo/yomiuri.co.jp', - sourceType: SourceType.nationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'ja'), - headquarters: countriesFixturesData[6], // Japan - createdAt: DateTime.parse('2023-03-06T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-03-06T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId27, - name: "People's Daily", - description: 'Official newspaper of the Central Committee of the CCP.', - url: 'http://en.people.cn', - logoUrl: 'https://api.companyenrich.com/logo/people.cn', - sourceType: SourceType.nationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[7], // China - createdAt: DateTime.parse('2023-03-07T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-03-07T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId28, - name: 'O Estado de S. Paulo', - description: 'Brazilian national newspaper.', - url: 'https://www.estadao.com.br', - logoUrl: 'https://api.companyenrich.com/logo/estadao.com.br', - sourceType: SourceType.nationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'pt'), - headquarters: countriesFixturesData[9], // Brazil - createdAt: DateTime.parse('2023-03-08T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-03-08T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId29, - name: 'El País', - description: 'Spanish national daily newspaper.', - url: 'https://elpais.com', - logoUrl: 'https://api.companyenrich.com/logo/elpais.com', - sourceType: SourceType.nationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'es'), - headquarters: countriesFixturesData[10], // Spain - createdAt: DateTime.parse('2023-03-09T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-03-09T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId30, - name: 'Corriere della Sera', - description: 'Italian national daily newspaper.', - url: 'https://www.corriere.it', - logoUrl: 'https://api.companyenrich.com/logo/corriere.it', - sourceType: SourceType.nationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'it'), - headquarters: countriesFixturesData[11], // Italy - createdAt: DateTime.parse('2023-03-10T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-03-10T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId31, - name: 'CNN International', - description: 'Global news coverage from CNN.', - url: 'https://edition.cnn.com', - logoUrl: 'https://api.companyenrich.com/logo/cnn.com', - sourceType: SourceType.internationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-04-01T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-04-01T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId32, - name: 'BBC World News', - description: 'International news from the BBC.', - url: 'https://www.bbc.com/news/world', - logoUrl: 'https://api.companyenrich.com/logo/bbc.com', - sourceType: SourceType.internationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[2], // United Kingdom - createdAt: DateTime.parse('2023-04-02T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-04-02T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId33, - name: 'The Economist', - description: 'In-depth analysis of international news and business.', - url: 'https://www.economist.com', - logoUrl: 'https://api.companyenrich.com/logo/economist.com', - sourceType: SourceType.internationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[2], // United Kingdom - createdAt: DateTime.parse('2023-04-03T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-04-03T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId34, - name: 'France 24', - description: 'French perspective on international current events.', - url: 'https://www.france24.com/en/', - logoUrl: 'https://api.companyenrich.com/logo/france24.com', - sourceType: SourceType.internationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[5], // France - createdAt: DateTime.parse('2023-04-04T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-04-04T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId35, - name: 'Deutsche Welle', - description: "Germany's international broadcaster.", - url: 'https://www.dw.com/en/', - logoUrl: 'https://api.companyenrich.com/logo/dw.com', - sourceType: SourceType.internationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[4], // Germany - createdAt: DateTime.parse('2023-04-05T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-04-05T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId36, - name: 'The Wall Street Journal', - description: 'Global business and financial news.', - url: 'https://www.wsj.com', - logoUrl: 'https://api.companyenrich.com/logo/wsj.com', - sourceType: SourceType.internationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-04-06T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-04-06T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId37, - name: 'Associated Press (AP)', - description: 'Global news network.', - url: 'https://apnews.com', - logoUrl: 'https://api.companyenrich.com/logo/apnews.com', - sourceType: SourceType.internationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-04-07T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-04-07T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId38, - name: 'Agence France-Presse (AFP)', - description: 'International news agency based in Paris.', - url: 'https://www.afp.com/en', - logoUrl: 'https://api.companyenrich.com/logo/afp.com', - sourceType: SourceType.internationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[5], // France - createdAt: DateTime.parse('2023-04-08T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-04-08T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId39, - name: 'RT', - description: 'Russian state-funded international television network.', - url: 'https://www.rt.com', - logoUrl: 'https://api.companyenrich.com/logo/rt.com', - sourceType: SourceType.internationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[12], // Russia - createdAt: DateTime.parse('2023-04-09T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-04-09T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId40, - name: 'CGTN', - description: 'Chinese state-funded international television network.', - url: 'https://www.cgtn.com', - logoUrl: 'https://api.companyenrich.com/logo/cgtn.com', - sourceType: SourceType.internationalNewsOutlet, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[7], // China - createdAt: DateTime.parse('2023-04-10T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-04-10T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId41, - name: 'ESPN', - description: 'The worldwide leader in sports.', - url: 'https://www.espn.com', - logoUrl: 'https://api.companyenrich.com/logo/espn.com', - sourceType: SourceType.specializedPublisher, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-05-01T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-05-01T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId42, - name: 'Nature', - description: 'International journal of science.', - url: 'https://www.nature.com', - logoUrl: 'https://api.companyenrich.com/logo/nature.com', - sourceType: SourceType.specializedPublisher, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[2], // United Kingdom - createdAt: DateTime.parse('2023-05-02T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-05-02T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId43, - name: 'The Hollywood Reporter', - description: 'The definitive voice of the entertainment industry.', - url: 'https://www.hollywoodreporter.com', - logoUrl: 'https://api.companyenrich.com/logo/hollywoodreporter.com', - sourceType: SourceType.specializedPublisher, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-05-03T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-05-03T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId44, - name: 'Vogue', - description: 'Fashion, beauty, and lifestyle.', - url: 'https://www.vogue.com', - logoUrl: 'https://api.companyenrich.com/logo/vogue.com', - sourceType: SourceType.specializedPublisher, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-05-04T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-05-04T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId45, - name: 'National Geographic', - description: 'Science, exploration, and adventure.', - url: 'https://www.nationalgeographic.com', - logoUrl: 'https://api.companyenrich.com/logo/nationalgeographic.com', - sourceType: SourceType.specializedPublisher, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-05-05T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-05-05T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId46, - name: 'Wired', - description: 'How technology is changing every aspect of our lives.', - url: 'https://www.wired.com', - logoUrl: 'https://api.companyenrich.com/logo/wired.com', - sourceType: SourceType.specializedPublisher, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-05-06T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-05-06T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId47, - name: 'Bon Appétit', - description: 'Food and cooking magazine.', - url: 'https://www.bonappetit.com', - logoUrl: 'https://api.companyenrich.com/logo/bonappetit.com', - sourceType: SourceType.specializedPublisher, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-05-07T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-05-07T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId48, - name: 'Architectural Digest', - description: 'The international design authority.', - url: 'https://www.architecturaldigest.com', - logoUrl: 'https://api.companyenrich.com/logo/architecturaldigest.com', - sourceType: SourceType.specializedPublisher, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-05-08T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-05-08T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId49, - name: 'Car and Driver', - description: 'Automotive news and reviews.', - url: 'https://www.caranddriver.com', - logoUrl: 'https://api.companyenrich.com/logo/caranddriver.com', - sourceType: SourceType.specializedPublisher, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-05-09T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-05-09T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId50, - name: 'PC Gamer', - description: 'Global authority on PC games.', - url: 'https://www.pcgamer.com', - logoUrl: 'https://api.companyenrich.com/logo/pcgamer.com', - sourceType: SourceType.specializedPublisher, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[2], // United Kingdom - createdAt: DateTime.parse('2023-05-10T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-05-10T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId51, - name: 'Stratechery by Ben Thompson', - description: 'Analysis of the strategy and business of technology.', - url: 'https://stratechery.com', - logoUrl: 'https://api.companyenrich.com/logo/stratechery.com', - sourceType: SourceType.blog, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-06-01T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-06-01T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId52, - name: 'Daring Fireball', - description: 'By John Gruber. On technology and Apple.', - url: 'https://daringfireball.net', - logoUrl: 'https://api.companyenrich.com/logo/daringfireball.net', - sourceType: SourceType.blog, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-06-02T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-06-02T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId53, - name: 'Wait But Why', - description: 'A popular long-form, stick-figure-illustrated blog.', - url: 'https://waitbutwhy.com', - logoUrl: 'https://api.companyenrich.com/logo/waitbutwhy.com', - sourceType: SourceType.blog, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-06-03T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-06-03T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId54, - name: 'Smitten Kitchen', - description: 'A home cooking blog from a tiny kitchen in New York City.', - url: 'https://smittenkitchen.com', - logoUrl: 'https://api.companyenrich.com/logo/smittenkitchen.com', - sourceType: SourceType.blog, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-06-04T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-06-04T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId55, - name: 'The Verge', - description: 'A technology news and media network operated by Vox Media.', - url: 'https://www.theverge.com', - logoUrl: 'https://api.companyenrich.com/logo/theverge.com', - sourceType: SourceType.blog, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-06-05T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-06-05T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId56, - name: 'Gizmodo', - description: 'A design, technology, science and science fiction website.', - url: 'https://gizmodo.com', - logoUrl: 'https://api.companyenrich.com/logo/gizmodo.com', - sourceType: SourceType.blog, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-06-06T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-06-06T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId57, - name: 'Kotaku', - description: 'A video game website and blog.', - url: 'https://kotaku.com', - logoUrl: 'https://api.companyenrich.com/logo/kotaku.com', - sourceType: SourceType.blog, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-06-07T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-06-07T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId58, - name: 'Lifehacker', - description: 'A weblog about life hacks and software.', - url: 'https://lifehacker.com', - logoUrl: 'https://api.companyenrich.com/logo/lifehacker.com', - sourceType: SourceType.blog, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-06-08T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-06-08T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId59, - name: 'Mashable', - description: 'A global, multi-platform media and entertainment company.', - url: 'https://mashable.com', - logoUrl: 'https://api.companyenrich.com/logo/mashable.com', - sourceType: SourceType.blog, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-06-09T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-06-09T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId60, - name: 'Engadget', - description: 'A multilingual technology blog network.', - url: 'https://www.engadget.com', - logoUrl: 'https://api.companyenrich.com/logo/engadget.com', - sourceType: SourceType.blog, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-06-10T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-06-10T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId61, - name: 'WhiteHouse.gov', - description: 'Official website of the White House.', - url: 'https://www.whitehouse.gov', - logoUrl: 'https://api.companyenrich.com/logo/whitehouse.gov', - sourceType: SourceType.governmentSource, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-07-01T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-07-01T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId62, - name: 'GOV.UK', - description: 'The official website for UK government services.', - url: 'https://www.gov.uk', - logoUrl: 'https://api.companyenrich.com/logo/gov.uk', - sourceType: SourceType.governmentSource, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[2], // United Kingdom - createdAt: DateTime.parse('2023-07-02T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-07-02T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId63, - name: 'Canada.ca', - description: 'Official website of the Government of Canada.', - url: 'https://www.canada.ca/en.html', - logoUrl: 'https://api.companyenrich.com/logo/canada.ca', - sourceType: SourceType.governmentSource, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[1], // Canada - createdAt: DateTime.parse('2023-07-03T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-07-03T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId64, - name: 'Australia.gov.au', - description: 'Official website of the Australian Government.', - url: 'https://www.australia.gov.au', - logoUrl: 'https://api.companyenrich.com/logo/australia.gov.au', - sourceType: SourceType.governmentSource, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[3], // Australia - createdAt: DateTime.parse('2023-07-04T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-07-04T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId65, - name: 'Bundesregierung.de', - description: 'Official website of the German Federal Government.', - url: 'https://www.bundesregierung.de/breg-de', - logoUrl: 'https://api.companyenrich.com/logo/bundesregierung.de', - sourceType: SourceType.governmentSource, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'de'), - headquarters: countriesFixturesData[4], // Germany - createdAt: DateTime.parse('2023-07-05T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-07-05T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId66, - name: 'Gouvernement.fr', - description: 'Official website of the French Government.', - url: 'https://www.gouvernement.fr', - logoUrl: 'https://api.companyenrich.com/logo/gouvernement.fr', - sourceType: SourceType.governmentSource, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'fr'), - headquarters: countriesFixturesData[5], // France - createdAt: DateTime.parse('2023-07-06T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-07-06T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId67, - name: 'Kantei.go.jp', - description: 'Official website of the Prime Minister of Japan.', - url: 'https://japan.kantei.go.jp', - logoUrl: 'https://api.companyenrich.com/logo/kantei.go.jp', - sourceType: SourceType.governmentSource, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[6], // Japan - createdAt: DateTime.parse('2023-07-07T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-07-07T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId68, - name: 'India.gov.in', - description: 'National Portal of India.', - url: 'https://www.india.gov.in', - logoUrl: 'https://api.companyenrich.com/logo/india.gov.in', - sourceType: SourceType.governmentSource, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[8], // India - createdAt: DateTime.parse('2023-07-08T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-07-08T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId69, - name: 'Gov.br', - description: 'Official website of the Brazilian Government.', - url: 'https://www.gov.br/pt-br', - logoUrl: 'https://api.companyenrich.com/logo/gov.br', - sourceType: SourceType.governmentSource, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'pt'), - headquarters: countriesFixturesData[9], // Brazil - createdAt: DateTime.parse('2023-07-09T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-07-09T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId70, - name: 'English.gov.cn', - description: 'Official web portal of the Chinese Government.', - url: 'http://english.gov.cn', - logoUrl: 'https://api.companyenrich.com/logo/gov.cn', - sourceType: SourceType.governmentSource, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[7], // China - createdAt: DateTime.parse('2023-07-10T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-07-10T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId71, - name: 'Google News', - description: 'A news aggregator service developed by Google.', - url: 'https://news.google.com', - logoUrl: 'https://api.companyenrich.com/logo/google.com', - sourceType: SourceType.aggregator, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-08-01T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-08-01T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId72, - name: 'Apple News', - description: 'A news aggregator app by Apple Inc.', - url: 'https://www.apple.com/apple-news/', - logoUrl: 'https://api.companyenrich.com/logo/apple.com', - sourceType: SourceType.aggregator, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-08-02T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-08-02T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId73, - name: 'Feedly', - description: 'A news aggregator application for various web browsers.', - url: 'https://feedly.com', - logoUrl: 'https://api.companyenrich.com/logo/feedly.com', - sourceType: SourceType.aggregator, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-08-03T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-08-03T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId74, - name: 'Flipboard', - description: 'A social-network aggregation, magazine-format mobile app.', - url: 'https://flipboard.com', - logoUrl: 'https://api.companyenrich.com/logo/flipboard.com', - sourceType: SourceType.aggregator, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-08-04T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-08-04T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId75, - name: 'SmartNews', - description: 'A mobile app for discovering news.', - url: 'https://www.smartnews.com', - logoUrl: 'https://api.companyenrich.com/logo/smartnews.com', - sourceType: SourceType.aggregator, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[6], // Japan - createdAt: DateTime.parse('2023-08-05T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-08-05T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId76, - name: 'Inoreader', - description: 'A web-based content and RSS feed reader.', - url: 'https://www.inoreader.com', - logoUrl: 'https://api.companyenrich.com/logo/inoreader.com', - sourceType: SourceType.aggregator, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[13], // Bulgaria - createdAt: DateTime.parse('2023-08-06T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-08-06T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId77, - name: 'The Old Reader', - description: 'A simple, web-based RSS reader.', - url: 'https://theoldreader.com', - logoUrl: 'https://api.companyenrich.com/logo/theoldreader.com', - sourceType: SourceType.aggregator, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-08-07T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-08-07T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId78, - name: 'NewsBlur', - description: 'A personal news reader.', - url: 'https://newsblur.com', - logoUrl: 'https://api.companyenrich.com/logo/newsblur.com', - sourceType: SourceType.aggregator, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-08-08T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-08-08T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId79, - name: 'Pocket', - description: 'An application for managing a reading list of articles.', - url: 'https://getpocket.com', - logoUrl: 'https://api.companyenrich.com/logo/getpocket.com', - sourceType: SourceType.aggregator, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-08-09T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-08-09T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId80, - name: 'Digg', - description: 'A news aggregator with a curated front page.', - url: 'https://digg.com', - logoUrl: 'https://api.companyenrich.com/logo/digg.com', - sourceType: SourceType.aggregator, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-08-10T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-08-10T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId81, - name: 'PR Newswire', - description: 'A distributor of press releases.', - url: 'https://www.prnewswire.com', - logoUrl: 'https://api.companyenrich.com/logo/prnewswire.com', - sourceType: SourceType.other, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-09-01T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-09-01T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId82, - name: 'arXiv', - description: 'An open-access archive for scholarly articles.', - url: 'https://arxiv.org', - logoUrl: 'https://api.companyenrich.com/logo/arxiv.org', - sourceType: SourceType.other, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-09-02T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-09-02T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId83, - name: 'The Lancet', - description: 'A weekly peer-reviewed general medical journal.', - url: 'https://www.thelancet.com', - logoUrl: 'https://api.companyenrich.com/logo/thelancet.com', - sourceType: SourceType.other, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[2], // United Kingdom - createdAt: DateTime.parse('2023-09-03T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-09-03T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId84, - name: 'Google AI Blog', - description: 'The latest news from Google AI.', - url: 'https://ai.googleblog.com', - logoUrl: 'https://api.companyenrich.com/logo/googleblog.com', - sourceType: SourceType.other, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-09-04T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-09-04T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId85, - name: 'Microsoft PressPass', - description: 'Official news and information from Microsoft.', - url: 'https://news.microsoft.com', - logoUrl: 'https://api.companyenrich.com/logo/microsoft.com', - sourceType: SourceType.other, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-09-05T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-09-05T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId86, - name: 'JSTOR', - description: 'A digital library of academic journals, books, and sources.', - url: 'https://www.jstor.org', - logoUrl: 'https://api.companyenrich.com/logo/jstor.org', - sourceType: SourceType.other, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-09-06T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-09-06T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId87, - name: 'Business Wire', - description: 'A company that disseminates press releases.', - url: 'https://www.businesswire.com', - logoUrl: 'https://api.companyenrich.com/logo/businesswire.com', - sourceType: SourceType.other, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-09-07T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-09-07T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId88, - name: 'PLOS ONE', - description: 'A peer-reviewed open access scientific journal.', - url: 'https://journals.plos.org/plosone/', - logoUrl: 'https://api.companyenrich.com/logo/plos.org', - sourceType: SourceType.other, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-09-08T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-09-08T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId89, - name: 'Apple Newsroom', - description: 'Official press releases from Apple.', - url: 'https://www.apple.com/newsroom/', - logoUrl: 'https://api.companyenrich.com/logo/apple.com', - sourceType: SourceType.other, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-09-09T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-09-09T10:00:00.000Z'), - status: ContentStatus.active, - ), - Source( - id: kSourceId90, - name: 'The New England Journal of Medicine', - description: 'A weekly medical journal.', - url: 'https://www.nejm.org', - logoUrl: 'https://api.companyenrich.com/logo/nejm.org', - sourceType: SourceType.other, - language: languagesFixturesData.firstWhere((lang) => lang.code == 'en'), - headquarters: countriesFixturesData[0], // United States - createdAt: DateTime.parse('2023-09-10T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-09-10T10:00:00.000Z'), - status: ContentStatus.active, - ), +/// Generates a list of predefined sources for fixture data. +/// +/// This function can be configured to generate sources in either English or +/// Arabic. +List getSourcesFixturesData({String languageCode = 'en'}) { + // Ensure only approved languages are used, default to 'en'. + final resolvedLanguageCode = ['en', 'ar'].contains(languageCode) + ? languageCode + : 'en'; + + final sources = []; + for (var i = 0; i < _sourceIds.length; i++) { + final id = _sourceIds[i]; + final name = _namesByLang[resolvedLanguageCode]![i]; + final description = _descriptionsByLang[resolvedLanguageCode]![i]; + final url = _urls[i]; + final logoUrl = 'https://api.companyenrich.com/logo/${url.split('/')[2]}'; + final sourceType = _sourceTypes[i]; + final language = languagesFixturesData.firstWhere( + (lang) => lang.code == _langCodes[i], + ); + final headquarters = countriesFixturesData[_countryIndexes[i]]; + + sources.add( + Source( + id: id, + name: name, + description: description, + url: url, + logoUrl: logoUrl, + sourceType: sourceType, + language: language, + headquarters: headquarters, + createdAt: DateTime.parse( + '2023-01-01T10:00:00.000Z', + ).add(Duration(days: i)), + updatedAt: DateTime.parse( + '2023-01-01T10:00:00.000Z', + ).add(Duration(days: i)), + status: ContentStatus.active, + ), + ); + } + return sources; +} + +const _sourceIds = [ + kSourceId1, + kSourceId2, + kSourceId3, + kSourceId4, + kSourceId5, + kSourceId6, + kSourceId7, + kSourceId8, + kSourceId9, + kSourceId10, + kSourceId11, + kSourceId12, + kSourceId13, + kSourceId14, + kSourceId15, + kSourceId16, + kSourceId17, + kSourceId18, + kSourceId19, + kSourceId20, + kSourceId21, + kSourceId22, + kSourceId23, + kSourceId24, + kSourceId25, + kSourceId26, + kSourceId27, + kSourceId28, + kSourceId29, + kSourceId30, + kSourceId31, + kSourceId32, + kSourceId33, + kSourceId34, + kSourceId35, + kSourceId36, + kSourceId37, + kSourceId38, + kSourceId39, + kSourceId40, + kSourceId41, + kSourceId42, + kSourceId43, + kSourceId44, + kSourceId45, + kSourceId46, + kSourceId47, + kSourceId48, + kSourceId49, + kSourceId50, + kSourceId51, + kSourceId52, + kSourceId53, + kSourceId54, + kSourceId55, + kSourceId56, + kSourceId57, + kSourceId58, + kSourceId59, + kSourceId60, + kSourceId61, + kSourceId62, + kSourceId63, + kSourceId64, + kSourceId65, + kSourceId66, + kSourceId67, + kSourceId68, + kSourceId69, + kSourceId70, + kSourceId71, + kSourceId72, + kSourceId73, + kSourceId74, + kSourceId75, + kSourceId76, + kSourceId77, + kSourceId78, + kSourceId79, + kSourceId80, + kSourceId81, + kSourceId82, + kSourceId83, + kSourceId84, + kSourceId85, + kSourceId86, + kSourceId87, + kSourceId88, + kSourceId89, + kSourceId90, +]; + +final Map> _namesByLang = { + 'en': [ + 'TechCrunch', + 'BBC News', + 'The New York Times', + 'The Guardian', + 'CNN', + 'Reuters', + 'Al Jazeera English', + 'Xinhua News Agency', + 'The Times of India', + 'Folha de S.Paulo', + 'San Francisco Chronicle', + 'Manchester Evening News', + 'The Sydney Morning Herald', + 'Le Parisien', + 'The Toronto Star', + 'Berliner Morgenpost', + 'The Asahi Shimbun (Tokyo)', + 'Hindustan Times (Mumbai)', + 'O Globo (Rio de Janeiro)', + 'La Vanguardia (Barcelona)', + 'USA Today', + 'The Globe and Mail', + 'The Australian', + 'Le Monde', + 'Frankfurter Allgemeine Zeitung', + 'The Yomiuri Shimbun', + "People's Daily", + 'O Estado de S. Paulo', + 'El País', + 'Corriere della Sera', + 'CNN International', + 'BBC World News', + 'The Economist', + 'France 24', + 'Deutsche Welle', + 'The Wall Street Journal', + 'Associated Press (AP)', + 'Agence France-Presse (AFP)', + 'RT', + 'CGTN', + 'ESPN', + 'Nature', + 'The Hollywood Reporter', + 'Vogue', + 'National Geographic', + 'Wired', + 'Bon Appétit', + 'Architectural Digest', + 'Car and Driver', + 'PC Gamer', + 'Stratechery by Ben Thompson', + 'Daring Fireball', + 'Wait But Why', + 'Smitten Kitchen', + 'The Verge', + 'Gizmodo', + 'Kotaku', + 'Lifehacker', + 'Mashable', + 'Engadget', + 'WhiteHouse.gov', + 'GOV.UK', + 'Canada.ca', + 'Australia.gov.au', + 'Bundesregierung.de', + 'Gouvernement.fr', + 'Kantei.go.jp', + 'India.gov.in', + 'Gov.br', + 'English.gov.cn', + 'Google News', + 'Apple News', + 'Feedly', + 'Flipboard', + 'SmartNews', + 'Inoreader', + 'The Old Reader', + 'NewsBlur', + 'Pocket', + 'Digg', + 'PR Newswire', + 'arXiv', + 'The Lancet', + 'Google AI Blog', + 'Microsoft PressPass', + 'JSTOR', + 'Business Wire', + 'PLOS ONE', + 'Apple Newsroom', + 'The New England Journal of Medicine', + ], + 'ar': [ + 'تك كرانش', + 'بي بي سي نيوز', + 'نيويورك تايمز', + 'الجارديان', + 'سي إن إن', + 'رويترز', + 'الجزيرة الإنجليزية', + 'وكالة أنباء شينخوا', + 'تايمز أوف إنديا', + 'فولها دي ساو باولو', + 'سان فرانسيسكو كرونيكل', + 'مانشستر إيفننغ نيوز', + 'سيدني مورنينغ هيرالد', + 'لو باريزيان', + 'تورنتو ستار', + 'برلينر مورغنبوست', + 'أساهي شيمبون (طوكيو)', + 'هندوستان تايمز (مومباي)', + 'أو جلوبو (ريو دي جانيرو)', + 'لا فانجارديا (برشلونة)', + 'يو إس إيه توداي', + 'ذا جلوب آند ميل', + 'ذي أستراليان', + 'لوموند', + 'فرانكفورتر ألجماينه تسايتונג', + 'يomiuri Shimbun', + 'صحيفة الشعب اليومية', + 'أو إستาดو دي ساو باولو', + 'إل باييس', + 'كورييري ديلا سيرا', + 'سي إن إن الدولية', + 'بي بي سي وورلد نيوز', + 'ذي إيكونوميست', + 'فرانس 24', + 'دويتشه فيله', + 'وول ستريت جورنال', + 'أسوشيتد برس (AP)', + 'وكالة فرانس برس (AFP)', + 'آر تي', + 'سي جي تي إن', + 'إي إس بي إن', + 'نيتشر', + 'هوليوود ريبورتر', + 'فوغ', + 'ناشيونال جيوغرافيك', + 'وايرد', + 'بون أبيتيت', + 'أركيتكتشرال دايجست', + 'كار آند درايفر', + 'بي سي جيمر', + 'סטרטכרי بقلم بن طومسون', + 'دارينج فايربول', + 'ويت بات واي', + 'สมิตเทน คิทเช่น', + 'ذا فيرج', + 'جيزمودو', + 'كوتاكو', + 'لايف هاكر', + 'ماشابل', + 'إنガジェット', + 'WhiteHouse.gov', + 'GOV.UK', + 'Canada.ca', + 'Australia.gov.au', + 'Bundesregierung.de', + 'Gouvernement.fr', + 'Kantei.go.jp', + 'India.gov.in', + 'Gov.br', + 'English.gov.cn', + 'أخبار جوجل', + 'أخبار أبل', + 'فيدلي', + 'فليبورد', + 'سمارت نيوز', + 'إينوريدر', + 'ذا أولد ريدر', + 'نيوز بلور', + 'بوكيت', + 'ديغ', + 'بي آر نيوزواير', + 'أرخايف', + 'ذا لانسيت', + 'مدونة جوجل للذكاء الاصطناعي', + 'مايكروسوفت بريس باس', + 'جيستور', + 'بزنس واير', + 'بلوس ون', + 'غرفة أخبار أبل', + 'مجلة نيو إنجلاند الطبية', + ], +}; + +final Map> _descriptionsByLang = { + 'en': [ + 'Leading online publisher of technology news.', + 'Breaking news, sport, TV, radio and a whole lot more.', + 'Breaking News, World News & Multimedia.', + 'Latest news, sport, business, comment and reviews from the Guardian.', + 'Breaking News, Latest News and Videos.', + 'Business, financial, national and international news.', + 'News, analysis, and opinion from the Middle East and around the world.', + "Official press agency of the People's Republic of China.", + 'Latest and Breaking News from India.', + 'Brazilian daily newspaper.', + 'News from the San Francisco Bay Area.', + 'Covering Greater Manchester, UK.', + 'Independent journalism for Sydney, Australia.', + 'Local news for Paris and the Île-de-France region.', + 'News and stories for Toronto, Canada.', + 'Daily news for Berlin, Germany.', + 'Local and national news from a Tokyo perspective.', + 'Latest news from Mumbai, India.', + 'News from Rio de Janeiro, Brazil.', + 'News from Barcelona and Catalonia, Spain.', + 'National news from across the United States.', + "Canada's national newspaper.", + 'National news of Australia.', + 'French national daily newspaper.', + 'German national newspaper.', + 'Japanese national newspaper.', + 'Official newspaper of the Central Committee of the CCP.', + 'Brazilian national newspaper.', + 'Spanish national daily newspaper.', + 'Italian national daily newspaper.', + 'Global news coverage from CNN.', + 'International news from the BBC.', + 'In-depth analysis of international news and business.', + 'French perspective on international current events.', + "Germany's international broadcaster.", + 'Global business and financial news.', + 'Global news network.', + 'International news agency based in Paris.', + 'Russian state-funded international television network.', + 'Chinese state-funded international television network.', + 'The worldwide leader in sports.', + 'International journal of science.', + 'The definitive voice of the entertainment industry.', + 'Fashion, beauty, and lifestyle.', + 'Science, exploration, and adventure.', + 'How technology is changing every aspect of our lives.', + 'Food and cooking magazine.', + 'The international design authority.', + 'Automotive news and reviews.', + 'Global authority on PC games.', + 'Analysis of the strategy and business of technology.', + 'By John Gruber. On technology and Apple.', + 'A popular long-form, stick-figure-illustrated blog.', + 'A home cooking blog from a tiny kitchen in New York City.', + 'A technology news and media network operated by Vox Media.', + 'A design, technology, science and science fiction website.', + 'A video game website and blog.', + 'A weblog about life hacks and software.', + 'A global, multi-platform media and entertainment company.', + 'A multilingual technology blog network.', + 'Official website of the White House.', + 'The official website for UK government services.', + 'Official website of the Government of Canada.', + 'Official website of the Australian Government.', + 'Official website of the German Federal Government.', + 'Official website of the French Government.', + 'Official website of the Prime Minister of Japan.', + 'National Portal of India.', + 'Official website of the Brazilian Government.', + 'Official web portal of the Chinese Government.', + 'A news aggregator service developed by Google.', + 'A news aggregator app by Apple Inc.', + 'A news aggregator application for various web browsers.', + 'A social-network aggregation, magazine-format mobile app.', + 'A mobile app for discovering news.', + 'A web-based content and RSS feed reader.', + 'A simple, web-based RSS reader.', + 'A personal news reader.', + 'An application for managing a reading list of articles.', + 'A news aggregator with a curated front page.', + 'A distributor of press releases.', + 'An open-access archive for scholarly articles.', + 'A weekly peer-reviewed general medical journal.', + 'The latest news from Google AI.', + 'Official news and information from Microsoft.', + 'A digital library of academic journals, books, and sources.', + 'A company that disseminates press releases.', + 'A peer-reviewed open access scientific journal.', + 'Official press releases from Apple.', + 'A weekly medical journal.', + ], + 'ar': [ + 'الناشر الرائد عبر الإنترنت لأخبار التكنولوجيا.', + 'الأخبار العاجلة والرياضة والتلفزيون والراديو والكثير.', + 'الأخبار العاجلة والأخبار العالمية والوسائط المتعددة.', + 'آخر الأخبار والرياضة والأعمال والتعليقات والمراجعات من الجارديان.', + 'الأخبار العاجلة وآخر الأخبار ومقاطع الفيديو.', + 'الأعمال والأخبار المالية والوطنية والدولية.', + 'الأخبار والتحليلات والآراء من الشرق الأوسط وحول العالم.', + 'وكالة الأنباء الرسمية لجمهورية الصين الشعبية.', + 'آخر الأخبار العاجلة من الهند.', + 'صحيفة يومية برازيلية.', + 'أخبار من منطقة خليج سان فرانسيسكو.', + 'تغطية مانشستر الكبرى، المملكة المتحدة.', + 'صحافة مستقلة لسيدني، أستراليا.', + 'الأخبار المحلية لباريس ومنطقة إيل دو فرانس.', + 'الأخبار والقصص لتورنتو، كندا.', + 'الأخبار اليومية لبرلين، ألمانيا.', + 'الأخبار المحلية والوطنية من منظور طوكيو.', + 'آخر الأخبار من مومباي، الهند.', + 'أخبار من ريو دي جانيرو، البرازيل.', + 'أخبار من برشلونة وكاتالونيا، إسبانيا.', + 'الأخبار الوطنية من جميع أنحاء الولايات المتحدة.', + 'الصحيفة الوطنية الكندية.', + 'الأخبار الوطنية لأستراليا.', + 'صحيفة يومية وطنية فرنسية.', + 'صحيفة وطنية ألمانية.', + 'صحيفة وطنية يابانية.', + 'الصحيفة الرسمية للجنة المركزية للحزب الشيوعي الصيني.', + 'صحيفة وطنية برازيلية.', + 'صحيفة يومية وطنية إسبانية.', + 'صحيفة يومية وطنية إيطالية.', + 'تغطية إخبارية عالمية من CNN.', + 'الأخبار الدولية من بي بي سي.', + 'تحليل متعمق للأخبار الدولية والأعمال.', + 'منظور فرنسي للأحداث الجارية الدولية.', + 'المذيع الدولي لألمانيا.', + 'الأخبار التجارية والمالية العالمية.', + 'شبكة أخبار عالمية.', + 'وكالة أنباء دولية مقرها باريس.', + 'شبكة تلفزيونية دولية ممولة من الدولة الروسية.', + 'شبكة تلفزيونية دولية ممولة من الدولة الصينية.', + 'الشركة الرائدة عالميًا في مجال الرياضة.', + 'مجلة دولية للعلوم.', + 'الصوت النهائي لصناعة الترفيه.', + 'الموضة والجمال وأسلوب الحياة.', + 'العلوم والاستكشاف والمغامرة.', + 'كيف تغير التكنولوجيا كل جانب من جوانب حياتنا.', + 'مجلة طعام وطبخ.', + 'السلطة الدولية للتصميم.', + 'أخبار ومراجعات السيارات.', + 'السلطة العالمية في ألعاب الكمبيوتر.', + 'تحليل استراتيجية وأعمال التكنولوجيا.', + 'بقلم جون غروبر. عن التكنولوجيا وأبل.', + 'مدونة شهيرة طويلة ومصورة بشخصيات كرتونية.', + 'مدونة طبخ منزلية من مطبخ صغير في مدينة نيويورك.', + 'شبكة أخبار ووسائط تكنولوجية تديرها Vox Media.', + 'موقع للتصميم والتكنولوجيا والعلوم والخيال العلمي.', + 'موقع ومدونة لألعاب الفيديو.', + 'مدونة حول حيل الحياة والبرامج.', + 'شركة إعلام وترفيه عالمية متعددة المنصات.', + 'شبكة مدونات تكنولوجية متعددة اللغات.', + 'الموقع الرسمي للبيت الأبيض.', + 'الموقع الرسمي لخدمات حكومة المملكة المتحدة.', + 'الموقع الرسمي لحكومة كندا.', + 'الموقع الرسمي للحكومة الأسترالية.', + 'الموقع الرسمي للحكومة الفيدرالية الألمانية.', + 'الموقع الرسمي للحكومة الفرنسية.', + 'الموقع الرسمي لرئيس وزراء اليابان.', + 'البوابة الوطنية للهند.', + 'الموقع الرسمي للحكومة البرازيلية.', + 'البوابة الإلكترونية الرسمية للحكومة الصينية.', + 'خدمة تجميع الأخبار التي طورتها جوجل.', + 'تطبيق مجمع أخبار من شركة أبل.', + 'تطبيق مجمع أخبار لمختلف متصفحات الويب.', + 'تطبيق جوال لتجميع الشبكات الاجتماعية بتنسيق مجلة.', + 'تطبيق جوال لاكتشاف الأخبار.', + 'قارئ محتوى وخلاصة RSS على شبكة الإنترنت.', + 'قارئ RSS بسيط على شبكة الإنترنت.', + 'قارئ أخبار شخصي.', + 'تطبيق لإدارة قائمة قراءة المقالات.', + 'مجمع أخبار بصفحة أمامية منسقة.', + 'موزع للبيانات الصحفية.', + 'أرشيف مفتوح الوصول للمقالات العلمية.', + 'مجلة طبية عامة أسبوعية محكمة.', + 'آخر الأخبار من Google AI.', + 'الأخبار والمعلومات الرسمية من مايكروسوفت.', + 'مكتبة رقمية للمجلات الأكاديمية والكتب والمصادر.', + 'شركة تنشر البيانات الصحفية.', + 'مجلة علمية مفتوحة الوصول ومحكمة.', + 'البيانات الصحفية الرسمية من Apple.', + 'مجلة طبية أسبوعية.', + ], +}; + +const _urls = [ + 'https://techcrunch.com', + 'https://www.bbc.com/news', + 'https://www.nytimes.com', + 'https://www.theguardian.com', + 'https://edition.cnn.com', + 'https://www.reuters.com', + 'https://www.aljazeera.com', + 'http://www.xinhuanet.com/english/', + 'https://timesofindia.indiatimes.com/', + 'https://www.folha.uol.com.br/', + 'https://www.sfchronicle.com', + 'https://www.manchestereveningnews.co.uk', + 'https://www.smh.com.au', + 'https://www.leparisien.fr', + 'https://www.thestar.com', + 'https://www.morgenpost.de', + 'https://www.asahi.com/area/tokyo/', + 'https://www.hindustantimes.com/mumbai-news', + 'https://oglobo.globo.com/rio/', + 'https://www.lavanguardia.com/local/barcelona', + 'https://www.usatoday.com', + 'https://www.theglobeandmail.com', + 'https://www.theaustralian.com.au', + 'https://www.lemonde.fr', + 'https://www.faz.net', + 'https://www.yomiuri.co.jp', + 'http://en.people.cn', + 'https://www.estadao.com.br', + 'https://elpais.com', + 'https://www.corriere.it', + 'https://edition.cnn.com', + 'https://www.bbc.com/news/world', + 'https://www.economist.com', + 'https://www.france24.com/en/', + 'https://www.dw.com/en/', + 'https://www.wsj.com', + 'https://apnews.com', + 'https://www.afp.com/en', + 'https://www.rt.com', + 'https://www.cgtn.com', + 'https://www.espn.com', + 'https://www.nature.com', + 'https://www.hollywoodreporter.com', + 'https://www.vogue.com', + 'https://www.nationalgeographic.com', + 'https://www.wired.com', + 'https://www.bonappetit.com', + 'https://www.architecturaldigest.com', + 'https://www.caranddriver.com', + 'https://www.pcgamer.com', + 'https://stratechery.com', + 'https://daringfireball.net', + 'https://waitbutwhy.com', + 'https://smittenkitchen.com', + 'https://www.theverge.com', + 'https://gizmodo.com', + 'https://kotaku.com', + 'https://lifehacker.com', + 'https://mashable.com', + 'https://www.engadget.com', + 'https://www.whitehouse.gov', + 'https://www.gov.uk', + 'https://www.canada.ca/en.html', + 'https://www.australia.gov.au', + 'https://www.bundesregierung.de/breg-de', + 'https://www.gouvernement.fr', + 'https://japan.kantei.go.jp', + 'https://www.india.gov.in', + 'https://www.gov.br/pt-br', + 'http://english.gov.cn', + 'https://news.google.com', + 'https://www.apple.com/apple-news/', + 'https://feedly.com', + 'https://flipboard.com', + 'https://www.smartnews.com', + 'https://www.inoreader.com', + 'https://theoldreader.com', + 'https://newsblur.com', + 'https://getpocket.com', + 'https://digg.com', + 'https://www.prnewswire.com', + 'https://arxiv.org', + 'https://www.thelancet.com', + 'https://ai.googleblog.com', + 'https://news.microsoft.com', + 'https://www.jstor.org', + 'https://www.businesswire.com', + 'https://journals.plos.org/plosone/', + 'https://www.apple.com/newsroom/', + 'https://www.nejm.org', +]; + +const _sourceTypes = [ + SourceType.newsAgency, + SourceType.newsAgency, + SourceType.newsAgency, + SourceType.newsAgency, + SourceType.newsAgency, + SourceType.newsAgency, + SourceType.newsAgency, + SourceType.newsAgency, + SourceType.newsAgency, + SourceType.newsAgency, + SourceType.localNewsOutlet, + SourceType.localNewsOutlet, + SourceType.localNewsOutlet, + SourceType.localNewsOutlet, + SourceType.localNewsOutlet, + SourceType.localNewsOutlet, + SourceType.localNewsOutlet, + SourceType.localNewsOutlet, + SourceType.localNewsOutlet, + SourceType.localNewsOutlet, + SourceType.nationalNewsOutlet, + SourceType.nationalNewsOutlet, + SourceType.nationalNewsOutlet, + SourceType.nationalNewsOutlet, + SourceType.nationalNewsOutlet, + SourceType.nationalNewsOutlet, + SourceType.nationalNewsOutlet, + SourceType.nationalNewsOutlet, + SourceType.nationalNewsOutlet, + SourceType.nationalNewsOutlet, + SourceType.internationalNewsOutlet, + SourceType.internationalNewsOutlet, + SourceType.internationalNewsOutlet, + SourceType.internationalNewsOutlet, + SourceType.internationalNewsOutlet, + SourceType.internationalNewsOutlet, + SourceType.internationalNewsOutlet, + SourceType.internationalNewsOutlet, + SourceType.internationalNewsOutlet, + SourceType.internationalNewsOutlet, + SourceType.specializedPublisher, + SourceType.specializedPublisher, + SourceType.specializedPublisher, + SourceType.specializedPublisher, + SourceType.specializedPublisher, + SourceType.specializedPublisher, + SourceType.specializedPublisher, + SourceType.specializedPublisher, + SourceType.specializedPublisher, + SourceType.specializedPublisher, + SourceType.blog, + SourceType.blog, + SourceType.blog, + SourceType.blog, + SourceType.blog, + SourceType.blog, + SourceType.blog, + SourceType.blog, + SourceType.blog, + SourceType.blog, + SourceType.governmentSource, + SourceType.governmentSource, + SourceType.governmentSource, + SourceType.governmentSource, + SourceType.governmentSource, + SourceType.governmentSource, + SourceType.governmentSource, + SourceType.governmentSource, + SourceType.governmentSource, + SourceType.governmentSource, + SourceType.aggregator, + SourceType.aggregator, + SourceType.aggregator, + SourceType.aggregator, + SourceType.aggregator, + SourceType.aggregator, + SourceType.aggregator, + SourceType.aggregator, + SourceType.aggregator, + SourceType.aggregator, + SourceType.other, + SourceType.other, + SourceType.other, + SourceType.other, + SourceType.other, + SourceType.other, + SourceType.other, + SourceType.other, + SourceType.other, + SourceType.other, +]; + +const _langCodes = [ + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'pt', + 'en', + 'en', + 'en', + 'fr', + 'en', + 'de', + 'ja', + 'en', + 'pt', + 'es', + 'en', + 'en', + 'en', + 'fr', + 'de', + 'ja', + 'en', + 'pt', + 'es', + 'it', + 'en', + 'en', + 'en', + 'en', + 'de', + 'en', + 'en', + 'fr', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'de', + 'fr', + 'en', + 'en', + 'pt', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', + 'en', +]; + +const _countryIndexes = [ + 0, + 2, + 0, + 2, + 0, + 0, + 0, + 7, + 8, + 9, + 0, + 2, + 3, + 5, + 1, + 4, + 6, + 8, + 9, + 10, + 0, + 1, + 3, + 5, + 4, + 6, + 7, + 9, + 10, + 11, + 0, + 2, + 2, + 5, + 4, + 0, + 0, + 5, + 12, + 7, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 1, + 3, + 4, + 5, + 6, + 8, + 9, + 7, + 0, + 0, + 0, + 0, + 6, + 13, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 2, ]; diff --git a/lib/src/fixtures/topics.dart b/lib/src/fixtures/topics.dart index 331b55d1..82ea603d 100644 --- a/lib/src/fixtures/topics.dart +++ b/lib/src/fixtures/topics.dart @@ -2,96 +2,113 @@ import 'package:core/src/enums/enums.dart'; import 'package:core/src/fixtures/fixture_ids.dart'; import 'package:core/src/models/entities/topic.dart'; -/// A list of predefined topics for fixture data. -final topicsFixturesData = [ - Topic( - id: kTopicId1, - name: 'Technology', - description: 'News and updates from the world of technology.', - iconUrl: 'https://example.com/icons/tech.png', - createdAt: DateTime.parse('2023-01-01T10:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-01T10:00:00.000Z'), - status: ContentStatus.active, - ), - Topic( - id: kTopicId2, - name: 'Sports', - description: 'Latest scores, highlights, and news from sports.', - iconUrl: 'https://example.com/icons/sports.png', - createdAt: DateTime.parse('2023-01-02T11:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-02T11:00:00.000Z'), - status: ContentStatus.active, - ), - Topic( - id: kTopicId3, - name: 'Politics', - description: 'Updates on political events and government policies.', - iconUrl: 'https://example.com/icons/politics.png', - createdAt: DateTime.parse('2023-01-03T12:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-03T12:00:00.000Z'), - status: ContentStatus.active, - ), - Topic( - id: kTopicId4, - name: 'Science', - description: 'Discoveries and breakthroughs in scientific research.', - iconUrl: 'https://example.com/icons/science.png', - createdAt: DateTime.parse('2023-01-04T13:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-04T13:00:00.000Z'), - status: ContentStatus.active, - ), - Topic( - id: kTopicId5, - name: 'Health', - description: 'Information and advice on health and wellness.', - iconUrl: 'https://example.com/icons/health.png', - createdAt: DateTime.parse('2023-01-05T14:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-05T14:00:00.000Z'), - status: ContentStatus.active, - ), - Topic( - id: kTopicId6, - name: 'Entertainment', - description: 'News from movies, music, and pop culture.', - iconUrl: 'https://example.com/icons/entertainment.png', - createdAt: DateTime.parse('2023-01-06T15:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-06T15:00:00.000Z'), - status: ContentStatus.active, - ), - Topic( - id: kTopicId7, - name: 'Business', - description: 'Financial markets, economy, and corporate news.', - iconUrl: 'https://example.com/icons/business.png', - createdAt: DateTime.parse('2023-01-07T16:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-07T16:00:00.000Z'), - status: ContentStatus.active, - ), - Topic( - id: kTopicId8, - name: 'Travel', - description: 'Guides, tips, and news for travelers.', - iconUrl: 'https://example.com/icons/travel.png', - createdAt: DateTime.parse('2023-01-08T17:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-08T17:00:00.000Z'), - status: ContentStatus.active, - ), - Topic( - id: kTopicId9, - name: 'Food', - description: 'Recipes, culinary trends, and food industry news.', - iconUrl: 'https://example.com/icons/food.png', - createdAt: DateTime.parse('2023-01-09T18:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-09T18:00:00.000Z'), - status: ContentStatus.active, - ), - Topic( - id: kTopicId10, - name: 'Education', - description: 'Developments in education and learning.', - iconUrl: 'https://example.com/icons/education.png', - createdAt: DateTime.parse('2023-01-10T19:00:00.000Z'), - updatedAt: DateTime.parse('2023-01-10T19:00:00.000Z'), - status: ContentStatus.active, - ), +/// Generates a list of predefined topics for fixture data. +/// +/// This function can be configured to generate topics in either English or +/// Arabic. +List getTopicsFixturesData({String languageCode = 'en'}) { + // Ensure only approved languages are used, default to 'en'. + final resolvedLanguageCode = ['en', 'ar'].contains(languageCode) + ? languageCode + : 'en'; + + final topics = []; + for (var i = 0; i < _topicIds.length; i++) { + topics.add( + Topic( + id: _topicIds[i], + name: _namesByLang[resolvedLanguageCode]![i], + description: _descriptionsByLang[resolvedLanguageCode]![i], + iconUrl: 'https://example.com/icons/${_iconNames[i]}.png', + createdAt: DateTime.parse( + '2023-01-01T10:00:00.000Z', + ).add(Duration(days: i)), + updatedAt: DateTime.parse( + '2023-01-01T10:00:00.000Z', + ).add(Duration(days: i)), + status: ContentStatus.active, + ), + ); + } + return topics; +} + +const _topicIds = [ + kTopicId1, + kTopicId2, + kTopicId3, + kTopicId4, + kTopicId5, + kTopicId6, + kTopicId7, + kTopicId8, + kTopicId9, + kTopicId10, +]; + +const _iconNames = [ + 'tech', + 'sports', + 'politics', + 'science', + 'health', + 'entertainment', + 'business', + 'travel', + 'food', + 'education', ]; + +final Map> _namesByLang = { + 'en': [ + 'Technology', + 'Sports', + 'Politics', + 'Science', + 'Health', + 'Entertainment', + 'Business', + 'Travel', + 'Food', + 'Education', + ], + 'ar': [ + 'التكنولوجيا', + 'الرياضة', + 'السياسة', + 'العلوم', + 'الصحة', + 'الترفيه', + 'الأعمال', + 'السفر', + 'الطعام', + 'التعليم', + ], +}; + +final Map> _descriptionsByLang = { + 'en': [ + 'News and updates from the world of technology.', + 'Latest scores, highlights, and news from sports.', + 'Updates on political events and government policies.', + 'Discoveries and breakthroughs in scientific research.', + 'Information and advice on health and wellness.', + 'News from movies, music, and pop culture.', + 'Financial markets, economy, and corporate news.', + 'Guides, tips, and news for travelers.', + 'Recipes, culinary trends, and food industry news.', + 'Developments in education and learning.', + ], + 'ar': [ + 'أخبار وتحديثات من عالم التكنولوجيا.', + 'آخر النتائج والأهداف والأخبار من عالم الرياضة.', + 'تحديثات حول الأحداث السياسية والسياسات الحكومية.', + 'اكتشافات وإنجازات في البحث العلمي.', + 'معلومات ونصائح حول الصحة والعافية.', + 'أخبار من الأفلام والموسيقى وثقافة البوب.', + 'الأسواق المالية والاقتصاد وأخبار الشركات.', + 'أدلة ونصائح وأخبار للمسافرين.', + 'وصفات واتجاهات الطهي وأخبار صناعة المواد الغذائية.', + 'التطورات في التعليم والتعلم.', + ], +}; diff --git a/lib/src/fixtures/user_content_preferences.dart b/lib/src/fixtures/user_content_preferences.dart index 48f6c701..787b3f31 100644 --- a/lib/src/fixtures/user_content_preferences.dart +++ b/lib/src/fixtures/user_content_preferences.dart @@ -1,101 +1,117 @@ import 'package:core/core.dart'; -/// User Content Preferences Demo Data -final List userContentPreferencesFixturesData = [ - UserContentPreferences( - id: kAdminUserId, - followedCountries: const [], - followedSources: [ - sourcesFixturesData[0], // TechCrunch - sourcesFixturesData[1], // BBC News - sourcesFixturesData[10], // San Francisco Chronicle - sourcesFixturesData[40], // ESPN - ], - followedTopics: [ - topicsFixturesData[0], // Technology - topicsFixturesData[1], // Sports - topicsFixturesData[6], // Business - topicsFixturesData[7], // Travel - ], - savedHeadlines: [headlinesFixturesData[0], headlinesFixturesData[10]], - savedHeadlineFilters: savedHeadlineFiltersFixturesData - .map((e) => e.copyWith(userId: kAdminUserId)) - .toList(), - savedSourceFilters: savedSourceFiltersFixturesData - .map((e) => e.copyWith(userId: kAdminUserId)) - .toList(), - ), - UserContentPreferences( - id: kUser1Id, // Publisher (Premium) - followedCountries: const [], - followedSources: [ - sourcesFixturesData[0], // TechCrunch - sourcesFixturesData[1], // BBC News - ], - followedTopics: [ - topicsFixturesData[0], // Technology - topicsFixturesData[6], // Business - ], - savedHeadlines: [headlinesFixturesData[2], headlinesFixturesData[3]], - savedHeadlineFilters: savedHeadlineFiltersFixturesData - .map((e) => e.copyWith(userId: kUser1Id)) - .toList(), - savedSourceFilters: savedSourceFiltersFixturesData - .map((e) => e.copyWith(userId: kUser1Id)) - .toList(), - ), - UserContentPreferences( - id: kUser2Id, // Publisher (Standard) - followedCountries: const [], - followedSources: [ - sourcesFixturesData[3], // The Guardian - sourcesFixturesData[4], // CNN - ], - followedTopics: [ - topicsFixturesData[2], // Politics - topicsFixturesData[4], // Health - ], - savedHeadlines: [headlinesFixturesData[4], headlinesFixturesData[5]], - savedHeadlineFilters: savedHeadlineFiltersFixturesData - .map((e) => e.copyWith(userId: kUser2Id)) - .toList(), - savedSourceFilters: savedSourceFiltersFixturesData - .map((e) => e.copyWith(userId: kUser2Id)) - .toList(), - ), - // Add preferences for users 3-10 - ...List.generate(8, (index) { - final userId = [ - kUser3Id, - kUser4Id, - kUser5Id, - kUser6Id, - kUser7Id, - kUser8Id, - kUser9Id, - kUser10Id, - ][index]; - return UserContentPreferences( - id: userId, +/// Generates a list of predefined user content preferences for fixture data. +/// +/// This function can be configured to generate preferences in either English or +/// Arabic, which affects the nested fixture data like topics and sources. +List getUserContentPreferencesFixturesData({ + String languageCode = 'en', +}) { + // Ensure only approved languages are used, default to 'en'. + final resolvedLanguageCode = ['en', 'ar'].contains(languageCode) + ? languageCode + : 'en'; + + // Get language-specific fixtures + final sources = getSourcesFixturesData(languageCode: resolvedLanguageCode); + final topics = getTopicsFixturesData(languageCode: resolvedLanguageCode); + final headlines = getHeadlinesFixturesData( + languageCode: resolvedLanguageCode, + ); + final savedHeadlineFilters = getSavedHeadlineFiltersFixturesData( + languageCode: resolvedLanguageCode, + ); + final savedSourceFilters = getSavedSourceFiltersFixturesData( + languageCode: resolvedLanguageCode, + ); + + return [ + UserContentPreferences( + id: kAdminUserId, followedCountries: const [], followedSources: [ - sourcesFixturesData[index % 10], - sourcesFixturesData[(index + 1) % 10], + sources[0], // TechCrunch + sources[1], // BBC News + sources[10], // San Francisco Chronicle + sources[40], // ESPN ], followedTopics: [ - topicsFixturesData[index % 5], - topicsFixturesData[(index + 1) % 5], + topics[0], // Technology + topics[1], // Sports + topics[6], // Business + topics[7], // Travel + ], + savedHeadlines: [headlines[0], headlines[10]], + savedHeadlineFilters: savedHeadlineFilters + .map((e) => e.copyWith(userId: kAdminUserId)) + .toList(), + savedSourceFilters: savedSourceFilters + .map((e) => e.copyWith(userId: kAdminUserId)) + .toList(), + ), + UserContentPreferences( + id: kUser1Id, // Publisher (Premium) + followedCountries: const [], + followedSources: [ + sources[0], // TechCrunch + sources[1], // BBC News ], - savedHeadlines: [ - headlinesFixturesData[index * 2], - headlinesFixturesData[index * 2 + 1], + followedTopics: [ + topics[0], // Technology + topics[6], // Business + ], + savedHeadlines: [headlines[2], headlines[3]], + savedHeadlineFilters: savedHeadlineFilters + .map((e) => e.copyWith(userId: kUser1Id)) + .toList(), + savedSourceFilters: savedSourceFilters + .map((e) => e.copyWith(userId: kUser1Id)) + .toList(), + ), + UserContentPreferences( + id: kUser2Id, // Publisher (Standard) + followedCountries: const [], + followedSources: [ + sources[3], // The Guardian + sources[4], // CNN + ], + followedTopics: [ + topics[2], // Politics + topics[4], // Health ], - savedHeadlineFilters: savedHeadlineFiltersFixturesData - .map((e) => e.copyWith(userId: userId)) + savedHeadlines: [headlines[4], headlines[5]], + savedHeadlineFilters: savedHeadlineFilters + .map((e) => e.copyWith(userId: kUser2Id)) .toList(), - savedSourceFilters: savedSourceFiltersFixturesData - .map((e) => e.copyWith(userId: userId)) + savedSourceFilters: savedSourceFilters + .map((e) => e.copyWith(userId: kUser2Id)) .toList(), - ); - }), -]; + ), + // Add preferences for users 3-10 + ...List.generate(8, (index) { + final userId = [ + kUser3Id, + kUser4Id, + kUser5Id, + kUser6Id, + kUser7Id, + kUser8Id, + kUser9Id, + kUser10Id, + ][index]; + return UserContentPreferences( + id: userId, + followedCountries: const [], + followedSources: [sources[index % 10], sources[(index + 1) % 10]], + followedTopics: [topics[index % 5], topics[(index + 1) % 5]], + savedHeadlines: [headlines[index * 2], headlines[index * 2 + 1]], + savedHeadlineFilters: savedHeadlineFilters + .map((e) => e.copyWith(userId: userId)) + .toList(), + savedSourceFilters: savedSourceFilters + .map((e) => e.copyWith(userId: userId)) + .toList(), + ); + }), + ]; +} diff --git a/lib/src/models/config/app_review_config.dart b/lib/src/models/config/app_review_config.dart new file mode 100644 index 00000000..c440c39b --- /dev/null +++ b/lib/src/models/config/app_review_config.dart @@ -0,0 +1,75 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +part 'app_review_config.g.dart'; + +/// {@template app_review_config} +/// Defines the remote configuration for the two-layer App Review Funnel. +/// +/// This system strategically prompts engaged users for feedback to maximize +/// positive public reviews while capturing constructive criticism privately. +/// +/// ### How It Works +/// +/// 1. **Trigger**: A user becomes eligible to see the prompt after reaching +/// the [positiveInteractionThreshold] of positive actions (e.g., saves). +/// +/// 2. **Prompt**: The `FeedDecoratorType.rateApp` decorator asks the user +/// "Are you enjoying the app?". The display logic is managed by the user's +/// `UserFeedDecoratorStatus` for `rateApp`, which respects the +/// [initialPromptCooldownDays]. +/// +/// 3. **Action**: +/// - **On "Yes"**: The client sets `isCompleted` to `true` on the user's +/// `UserFeedDecoratorStatus` for `rateApp` and immediately triggers the +/// native OS in-app review dialog if applicable ie the app is hosted in +/// google play or apple store. The prompt will not be shown again. +/// - **On "No"**: The client only updates the `lastShownAt` timestamp on +/// the status object. The prompt will not be shown again until the +/// cooldown period has passed. No public review is requested. +/// {@endtemplate} +@immutable +@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +class AppReviewConfig extends Equatable { + /// {@macro app_review_config} + const AppReviewConfig({ + required this.positiveInteractionThreshold, + required this.initialPromptCooldownDays, + }); + + /// Creates a [AppReviewConfig] from JSON data. + factory AppReviewConfig.fromJson(Map json) => + _$AppReviewConfigFromJson(json); + + /// The number of positive interactions (e.g., saving a headline) required + /// to trigger the initial review prompt. + final int positiveInteractionThreshold; + + /// The number of days to wait before showing the initial prompt again if the + /// user dismisses it. + final int initialPromptCooldownDays; + + /// Converts this [AppReviewConfig] instance to JSON data. + Map toJson() => _$AppReviewConfigToJson(this); + + @override + List get props => [ + positiveInteractionThreshold, + initialPromptCooldownDays, + ]; + + /// Creates a copy of this [AppReviewConfig] but with the given fields + /// replaced with the new values. + AppReviewConfig copyWith({ + int? positiveInteractionThreshold, + int? initialPromptCooldownDays, + }) { + return AppReviewConfig( + positiveInteractionThreshold: + positiveInteractionThreshold ?? this.positiveInteractionThreshold, + initialPromptCooldownDays: + initialPromptCooldownDays ?? this.initialPromptCooldownDays, + ); + } +} diff --git a/lib/src/models/config/app_review_config.g.dart b/lib/src/models/config/app_review_config.g.dart new file mode 100644 index 00000000..ad1387d6 --- /dev/null +++ b/lib/src/models/config/app_review_config.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_review_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AppReviewConfig _$AppReviewConfigFromJson(Map json) => + $checkedCreate('AppReviewConfig', json, ($checkedConvert) { + final val = AppReviewConfig( + positiveInteractionThreshold: $checkedConvert( + 'positiveInteractionThreshold', + (v) => (v as num).toInt(), + ), + initialPromptCooldownDays: $checkedConvert( + 'initialPromptCooldownDays', + (v) => (v as num).toInt(), + ), + ); + return val; + }); + +Map _$AppReviewConfigToJson(AppReviewConfig instance) => + { + 'positiveInteractionThreshold': instance.positiveInteractionThreshold, + 'initialPromptCooldownDays': instance.initialPromptCooldownDays, + }; diff --git a/lib/src/models/config/community_config.dart b/lib/src/models/config/community_config.dart new file mode 100644 index 00000000..db39d32a --- /dev/null +++ b/lib/src/models/config/community_config.dart @@ -0,0 +1,58 @@ +import 'package:core/src/models/config/app_review_config.dart'; +import 'package:core/src/models/config/engagement_config.dart'; +import 'package:core/src/models/config/reporting_config.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +part 'community_config.g.dart'; + +/// {@template community_config} +/// A container for all community and user-generated content features. +/// +/// This includes configurations for engagement (reactions, comments), +/// content reporting, and the app review funnel. +/// {@endtemplate} +@immutable +@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +class CommunityConfig extends Equatable { + /// {@macro community_config} + const CommunityConfig({ + required this.engagement, + required this.reporting, + required this.appReview, + }); + + /// Creates a [CommunityConfig] from JSON data. + factory CommunityConfig.fromJson(Map json) => + _$CommunityConfigFromJson(json); + + /// Configuration for user engagement features (reactions, comments). + final EngagementConfig engagement; + + /// Configuration for user content reporting features. + final ReportingConfig reporting; + + /// Configuration for the smart app review funnel. + final AppReviewConfig appReview; + + /// Converts this [CommunityConfig] instance to JSON data. + Map toJson() => _$CommunityConfigToJson(this); + + @override + List get props => [engagement, reporting, appReview]; + + /// Creates a copy of this [CommunityConfig] but with the given fields + /// replaced with the new values. + CommunityConfig copyWith({ + EngagementConfig? engagement, + ReportingConfig? reporting, + AppReviewConfig? appReview, + }) { + return CommunityConfig( + engagement: engagement ?? this.engagement, + reporting: reporting ?? this.reporting, + appReview: appReview ?? this.appReview, + ); + } +} diff --git a/lib/src/models/config/community_config.g.dart b/lib/src/models/config/community_config.g.dart new file mode 100644 index 00000000..b4182507 --- /dev/null +++ b/lib/src/models/config/community_config.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'community_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CommunityConfig _$CommunityConfigFromJson(Map json) => + $checkedCreate('CommunityConfig', json, ($checkedConvert) { + final val = CommunityConfig( + engagement: $checkedConvert( + 'engagement', + (v) => EngagementConfig.fromJson(v as Map), + ), + reporting: $checkedConvert( + 'reporting', + (v) => ReportingConfig.fromJson(v as Map), + ), + appReview: $checkedConvert( + 'appReview', + (v) => AppReviewConfig.fromJson(v as Map), + ), + ); + return val; + }); + +Map _$CommunityConfigToJson(CommunityConfig instance) => + { + 'engagement': instance.engagement.toJson(), + 'reporting': instance.reporting.toJson(), + 'appReview': instance.appReview.toJson(), + }; diff --git a/lib/src/models/config/config.dart b/lib/src/models/config/config.dart index a1aa0e82..3477f16b 100644 --- a/lib/src/models/config/config.dart +++ b/lib/src/models/config/config.dart @@ -1,6 +1,9 @@ export 'ad_config.dart'; export 'ad_platform_identifiers.dart'; export 'app_config.dart'; +export 'app_review_config.dart'; +export 'community_config.dart'; +export 'engagement_config.dart'; export 'features_config.dart'; export 'feed_ad_configuration.dart'; export 'feed_ad_frequency_config.dart'; @@ -13,6 +16,7 @@ export 'navigation_ad_configuration.dart'; export 'navigation_ad_frequency_config.dart'; export 'push_notification_config.dart'; export 'remote_config.dart'; +export 'reporting_config.dart'; export 'saved_filter_limits.dart'; export 'update_config.dart'; export 'user_config.dart'; diff --git a/lib/src/models/config/engagement_config.dart b/lib/src/models/config/engagement_config.dart new file mode 100644 index 00000000..81544307 --- /dev/null +++ b/lib/src/models/config/engagement_config.dart @@ -0,0 +1,45 @@ +import 'package:core/src/enums/engagement_mode.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +part 'engagement_config.g.dart'; + +/// {@template engagement_config} +/// Defines the remote configuration for user engagement features. +/// +/// This model allows administrators to remotely enable/disable the entire engagement +/// system and switch between `reactionsOnly` and `reactionsAndComments` modes. +/// {@endtemplate} +@immutable +@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +class EngagementConfig extends Equatable { + /// {@macro engagement_config} + const EngagementConfig({required this.enabled, required this.engagementMode}); + + /// Creates an [EngagementConfig] from JSON data. + factory EngagementConfig.fromJson(Map json) => + _$EngagementConfigFromJson(json); + + /// A master switch to enable or disable the entire engagement system. + final bool enabled; + + /// Defines the available engagement features (e.g., reactions only or both + /// reactions and comments). + final EngagementMode engagementMode; + + /// Converts this [EngagementConfig] instance to JSON data. + Map toJson() => _$EngagementConfigToJson(this); + + @override + List get props => [enabled, engagementMode]; + + /// Creates a copy of this [EngagementConfig] but with the given fields + /// replaced with the new values. + EngagementConfig copyWith({bool? enabled, EngagementMode? engagementMode}) { + return EngagementConfig( + enabled: enabled ?? this.enabled, + engagementMode: engagementMode ?? this.engagementMode, + ); + } +} diff --git a/lib/src/models/config/engagement_config.g.dart b/lib/src/models/config/engagement_config.g.dart new file mode 100644 index 00000000..a49a8a87 --- /dev/null +++ b/lib/src/models/config/engagement_config.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'engagement_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +EngagementConfig _$EngagementConfigFromJson(Map json) => + $checkedCreate('EngagementConfig', json, ($checkedConvert) { + final val = EngagementConfig( + enabled: $checkedConvert('enabled', (v) => v as bool), + engagementMode: $checkedConvert( + 'engagementMode', + (v) => $enumDecode(_$EngagementModeEnumMap, v), + ), + ); + return val; + }); + +Map _$EngagementConfigToJson(EngagementConfig instance) => + { + 'enabled': instance.enabled, + 'engagementMode': _$EngagementModeEnumMap[instance.engagementMode]!, + }; + +const _$EngagementModeEnumMap = { + EngagementMode.reactionsOnly: 'reactionsOnly', + EngagementMode.reactionsAndComments: 'reactionsAndComments', +}; diff --git a/lib/src/models/config/features_config.dart b/lib/src/models/config/features_config.dart index 5aa1c9fb..88bd7de5 100644 --- a/lib/src/models/config/features_config.dart +++ b/lib/src/models/config/features_config.dart @@ -1,4 +1,5 @@ import 'package:core/src/models/config/ad_config.dart'; +import 'package:core/src/models/config/community_config.dart'; import 'package:core/src/models/config/feed_config.dart'; import 'package:core/src/models/config/push_notification_config.dart'; import 'package:equatable/equatable.dart'; @@ -18,6 +19,7 @@ class FeaturesConfig extends Equatable { required this.ads, required this.pushNotifications, required this.feed, + required this.community, }); /// Creates a [FeaturesConfig] from JSON data. @@ -33,11 +35,14 @@ class FeaturesConfig extends Equatable { /// Configuration for all feed-related features. final FeedConfig feed; + /// Configuration for community and user-generated content features. + final CommunityConfig community; + /// Converts this [FeaturesConfig] instance to JSON data. Map toJson() => _$FeaturesConfigToJson(this); @override - List get props => [ads, pushNotifications, feed]; + List get props => [ads, pushNotifications, feed, community]; /// Creates a copy of this [FeaturesConfig] but with the given fields /// replaced with the new values. @@ -45,11 +50,13 @@ class FeaturesConfig extends Equatable { AdConfig? ads, PushNotificationConfig? pushNotifications, FeedConfig? feed, + CommunityConfig? community, }) { return FeaturesConfig( ads: ads ?? this.ads, pushNotifications: pushNotifications ?? this.pushNotifications, feed: feed ?? this.feed, + community: community ?? this.community, ); } } diff --git a/lib/src/models/config/features_config.g.dart b/lib/src/models/config/features_config.g.dart index d6b1a61f..a67a4c1e 100644 --- a/lib/src/models/config/features_config.g.dart +++ b/lib/src/models/config/features_config.g.dart @@ -21,6 +21,10 @@ FeaturesConfig _$FeaturesConfigFromJson(Map json) => 'feed', (v) => FeedConfig.fromJson(v as Map), ), + community: $checkedConvert( + 'community', + (v) => CommunityConfig.fromJson(v as Map), + ), ); return val; }); @@ -30,4 +34,5 @@ Map _$FeaturesConfigToJson(FeaturesConfig instance) => 'ads': instance.ads.toJson(), 'pushNotifications': instance.pushNotifications.toJson(), 'feed': instance.feed.toJson(), + 'community': instance.community.toJson(), }; diff --git a/lib/src/models/config/reporting_config.dart b/lib/src/models/config/reporting_config.dart new file mode 100644 index 00000000..566d3a75 --- /dev/null +++ b/lib/src/models/config/reporting_config.dart @@ -0,0 +1,62 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +part 'reporting_config.g.dart'; + +/// {@template reporting_config} +/// Defines the remote configuration for the user content reporting system. +/// +/// This allows administrators to enable or disable the reporting functionality +/// for each reportable entity (`headline`, `source`, `comment`) individually. +/// {@endtemplate} +@immutable +@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +class ReportingConfig extends Equatable { + /// {@macro reporting_config} + const ReportingConfig({ + required this.headlineReportingEnabled, + required this.sourceReportingEnabled, + required this.commentReportingEnabled, + }); + + /// Creates a [ReportingConfig] from JSON data. + factory ReportingConfig.fromJson(Map json) => + _$ReportingConfigFromJson(json); + + /// A switch to enable or disable reporting for headlines. + final bool headlineReportingEnabled; + + /// A switch to enable or disable reporting for news sources. + final bool sourceReportingEnabled; + + /// A switch to enable or disable reporting for user comments. + final bool commentReportingEnabled; + + /// Converts this [ReportingConfig] instance to JSON data. + Map toJson() => _$ReportingConfigToJson(this); + + @override + List get props => [ + headlineReportingEnabled, + sourceReportingEnabled, + commentReportingEnabled, + ]; + + /// Creates a copy of this [ReportingConfig] but with the given fields + /// replaced with the new values. + ReportingConfig copyWith({ + bool? headlineReportingEnabled, + bool? sourceReportingEnabled, + bool? commentReportingEnabled, + }) { + return ReportingConfig( + headlineReportingEnabled: + headlineReportingEnabled ?? this.headlineReportingEnabled, + sourceReportingEnabled: + sourceReportingEnabled ?? this.sourceReportingEnabled, + commentReportingEnabled: + commentReportingEnabled ?? this.commentReportingEnabled, + ); + } +} diff --git a/lib/src/models/config/reporting_config.g.dart b/lib/src/models/config/reporting_config.g.dart new file mode 100644 index 00000000..7ae3efff --- /dev/null +++ b/lib/src/models/config/reporting_config.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'reporting_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ReportingConfig _$ReportingConfigFromJson(Map json) => + $checkedCreate('ReportingConfig', json, ($checkedConvert) { + final val = ReportingConfig( + headlineReportingEnabled: $checkedConvert( + 'headlineReportingEnabled', + (v) => v as bool, + ), + sourceReportingEnabled: $checkedConvert( + 'sourceReportingEnabled', + (v) => v as bool, + ), + commentReportingEnabled: $checkedConvert( + 'commentReportingEnabled', + (v) => v as bool, + ), + ); + return val; + }); + +Map _$ReportingConfigToJson(ReportingConfig instance) => + { + 'headlineReportingEnabled': instance.headlineReportingEnabled, + 'sourceReportingEnabled': instance.sourceReportingEnabled, + 'commentReportingEnabled': instance.commentReportingEnabled, + }; diff --git a/lib/src/models/config/user_limits_config.dart b/lib/src/models/config/user_limits_config.dart index c871b7ef..b316f8e7 100644 --- a/lib/src/models/config/user_limits_config.dart +++ b/lib/src/models/config/user_limits_config.dart @@ -22,6 +22,9 @@ class UserLimitsConfig extends Equatable { required this.savedHeadlines, required this.savedHeadlineFilters, required this.savedSourceFilters, + required this.reactionsPerDay, + required this.commentsPerDay, + required this.reportsPerDay, }); /// Creates a [UserLimitsConfig] from JSON data. @@ -45,6 +48,18 @@ class UserLimitsConfig extends Equatable { /// defines the limits per user role. final Map savedSourceFilters; + /// Role-based limits for the number of reactions a user can perform per day. + final Map reactionsPerDay; + + /// Role-based limits for the number of comments a user can post per day. + /// + /// This limit applies specifically to the creation of new comments and does + /// not include other interactions like reactions. + final Map commentsPerDay; + + /// Role-based limits for the number of reports a user can submit per day. + final Map reportsPerDay; + /// Converts this [UserLimitsConfig] instance to JSON data. Map toJson() => _$UserLimitsConfigToJson(this); @@ -54,6 +69,9 @@ class UserLimitsConfig extends Equatable { savedHeadlines, savedHeadlineFilters, savedSourceFilters, + reactionsPerDay, + commentsPerDay, + reportsPerDay, ]; /// Creates a copy of this [UserLimitsConfig] but with the given fields @@ -63,12 +81,18 @@ class UserLimitsConfig extends Equatable { Map? savedHeadlines, Map? savedHeadlineFilters, Map? savedSourceFilters, + Map? commentsPerDay, + Map? reportsPerDay, + Map? reactionsPerDay, }) { return UserLimitsConfig( followedItems: followedItems ?? this.followedItems, savedHeadlines: savedHeadlines ?? this.savedHeadlines, savedHeadlineFilters: savedHeadlineFilters ?? this.savedHeadlineFilters, savedSourceFilters: savedSourceFilters ?? this.savedSourceFilters, + commentsPerDay: commentsPerDay ?? this.commentsPerDay, + reportsPerDay: reportsPerDay ?? this.reportsPerDay, + reactionsPerDay: reactionsPerDay ?? this.reactionsPerDay, ); } } diff --git a/lib/src/models/config/user_limits_config.g.dart b/lib/src/models/config/user_limits_config.g.dart index 4f398182..4893d719 100644 --- a/lib/src/models/config/user_limits_config.g.dart +++ b/lib/src/models/config/user_limits_config.g.dart @@ -42,6 +42,27 @@ UserLimitsConfig _$UserLimitsConfigFromJson( ), ), ), + reactionsPerDay: $checkedConvert( + 'reactionsPerDay', + (v) => (v as Map).map( + (k, e) => + MapEntry($enumDecode(_$AppUserRoleEnumMap, k), (e as num).toInt()), + ), + ), + commentsPerDay: $checkedConvert( + 'commentsPerDay', + (v) => (v as Map).map( + (k, e) => + MapEntry($enumDecode(_$AppUserRoleEnumMap, k), (e as num).toInt()), + ), + ), + reportsPerDay: $checkedConvert( + 'reportsPerDay', + (v) => (v as Map).map( + (k, e) => + MapEntry($enumDecode(_$AppUserRoleEnumMap, k), (e as num).toInt()), + ), + ), ); return val; }); @@ -60,6 +81,15 @@ Map _$UserLimitsConfigToJson(UserLimitsConfig instance) => 'savedSourceFilters': instance.savedSourceFilters.map( (k, e) => MapEntry(_$AppUserRoleEnumMap[k]!, e.toJson()), ), + 'reactionsPerDay': instance.reactionsPerDay.map( + (k, e) => MapEntry(_$AppUserRoleEnumMap[k]!, e), + ), + 'commentsPerDay': instance.commentsPerDay.map( + (k, e) => MapEntry(_$AppUserRoleEnumMap[k]!, e), + ), + 'reportsPerDay': instance.reportsPerDay.map( + (k, e) => MapEntry(_$AppUserRoleEnumMap[k]!, e), + ), }; const _$AppUserRoleEnumMap = { diff --git a/lib/src/models/core/core.dart b/lib/src/models/core/core.dart deleted file mode 100644 index 32014937..00000000 --- a/lib/src/models/core/core.dart +++ /dev/null @@ -1 +0,0 @@ -export 'feed_item.dart'; diff --git a/lib/src/models/entities/country.dart b/lib/src/models/entities/country.dart index 112d44fa..1364457d 100644 --- a/lib/src/models/entities/country.dart +++ b/lib/src/models/entities/country.dart @@ -1,5 +1,5 @@ import 'package:core/src/enums/enums.dart'; -import 'package:core/src/models/core/feed_item.dart'; +import 'package:core/src/models/feed/feed_item.dart'; import 'package:core/src/utils/utils.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; diff --git a/lib/src/models/entities/headline.dart b/lib/src/models/entities/headline.dart index 1b340c3c..8fd8e46c 100644 --- a/lib/src/models/entities/headline.dart +++ b/lib/src/models/entities/headline.dart @@ -1,8 +1,8 @@ import 'package:core/src/enums/enums.dart'; -import 'package:core/src/models/core/feed_item.dart'; import 'package:core/src/models/entities/country.dart'; import 'package:core/src/models/entities/source.dart'; import 'package:core/src/models/entities/topic.dart'; +import 'package:core/src/models/feed/feed_item.dart'; import 'package:core/src/utils/json_helpers.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; diff --git a/lib/src/models/entities/source.dart b/lib/src/models/entities/source.dart index a325f1c4..d199388c 100644 --- a/lib/src/models/entities/source.dart +++ b/lib/src/models/entities/source.dart @@ -1,7 +1,7 @@ import 'package:core/src/enums/enums.dart'; -import 'package:core/src/models/core/feed_item.dart'; import 'package:core/src/models/entities/country.dart'; import 'package:core/src/models/entities/language.dart'; +import 'package:core/src/models/feed/feed_item.dart'; import 'package:core/src/utils/utils.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; diff --git a/lib/src/models/entities/topic.dart b/lib/src/models/entities/topic.dart index 90a3c0de..d3007611 100644 --- a/lib/src/models/entities/topic.dart +++ b/lib/src/models/entities/topic.dart @@ -1,5 +1,5 @@ import 'package:core/src/enums/enums.dart'; -import 'package:core/src/models/core/feed_item.dart'; +import 'package:core/src/models/feed/feed_item.dart'; import 'package:core/src/utils/utils.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; diff --git a/lib/src/models/feed_decorators/call_to_action_item.dart b/lib/src/models/feed/call_to_action_item.dart similarity index 97% rename from lib/src/models/feed_decorators/call_to_action_item.dart rename to lib/src/models/feed/call_to_action_item.dart index 6063a02e..caa16a85 100644 --- a/lib/src/models/feed_decorators/call_to_action_item.dart +++ b/lib/src/models/feed/call_to_action_item.dart @@ -1,5 +1,5 @@ import 'package:core/src/enums/feed_decorator_type.dart'; -import 'package:core/src/models/core/feed_item.dart'; +import 'package:core/src/models/feed/feed_item.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; diff --git a/lib/src/models/feed_decorators/call_to_action_item.g.dart b/lib/src/models/feed/call_to_action_item.g.dart similarity index 100% rename from lib/src/models/feed_decorators/call_to_action_item.g.dart rename to lib/src/models/feed/call_to_action_item.g.dart diff --git a/lib/src/models/feed_decorators/content_collection_item.dart b/lib/src/models/feed/content_collection_item.dart similarity index 97% rename from lib/src/models/feed_decorators/content_collection_item.dart rename to lib/src/models/feed/content_collection_item.dart index 862d4524..1ba9394f 100644 --- a/lib/src/models/feed_decorators/content_collection_item.dart +++ b/lib/src/models/feed/content_collection_item.dart @@ -1,5 +1,5 @@ import 'package:core/src/enums/feed_decorator_type.dart'; -import 'package:core/src/models/core/feed_item.dart'; +import 'package:core/src/models/feed/feed_item.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; diff --git a/lib/src/models/feed_decorators/content_collection_item.g.dart b/lib/src/models/feed/content_collection_item.g.dart similarity index 100% rename from lib/src/models/feed_decorators/content_collection_item.g.dart rename to lib/src/models/feed/content_collection_item.g.dart diff --git a/lib/src/models/feed/feed.dart b/lib/src/models/feed/feed.dart new file mode 100644 index 00000000..72a0b54d --- /dev/null +++ b/lib/src/models/feed/feed.dart @@ -0,0 +1,4 @@ +export 'call_to_action_item.dart'; +export 'content_collection_item.dart'; +export 'feed_decorators.dart'; +export 'feed_item.dart'; diff --git a/lib/src/models/feed_decorators/feed_decorators.dart b/lib/src/models/feed/feed_decorators.dart similarity index 100% rename from lib/src/models/feed_decorators/feed_decorators.dart rename to lib/src/models/feed/feed_decorators.dart diff --git a/lib/src/models/core/feed_item.dart b/lib/src/models/feed/feed_item.dart similarity index 98% rename from lib/src/models/core/feed_item.dart rename to lib/src/models/feed/feed_item.dart index 2e3d08ed..9b106b72 100644 --- a/lib/src/models/core/feed_item.dart +++ b/lib/src/models/feed/feed_item.dart @@ -1,5 +1,5 @@ import 'package:core/src/models/entities/entities.dart'; -import 'package:core/src/models/feed_decorators/feed_decorators.dart'; +import 'package:core/src/models/feed/feed_decorators.dart'; import 'package:equatable/equatable.dart'; /// {@template feed_item} diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index 08c30813..49f8f5fb 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,12 +1,12 @@ export 'auth/auth.dart'; export 'config/config.dart'; -export 'core/core.dart'; export 'dashboard/dashboard.dart'; export 'entities/entities.dart'; -export 'feed_decorators/feed_decorators.dart'; +export 'feed/feed.dart'; export 'notifications/notifications.dart'; export 'push_notifications/push_notifications.dart'; export 'query/query.dart'; export 'responses/responses.dart'; +export 'user_generated_content/user_generated_content.dart'; export 'user_preferences/user_preferences.dart'; export 'user_settings/user_settings.dart'; diff --git a/lib/src/models/user_generated_content/comment.dart b/lib/src/models/user_generated_content/comment.dart new file mode 100644 index 00000000..4775fa46 --- /dev/null +++ b/lib/src/models/user_generated_content/comment.dart @@ -0,0 +1,59 @@ +import 'package:core/core.dart' show Engagement; +import 'package:core/src/enums/comment_status.dart'; +import 'package:core/src/models/entities/language.dart'; +import 'package:core/src/models/models.dart' show Engagement; +import 'package:core/src/models/user_generated_content/engagement.dart' + show Engagement; +import 'package:core/src/models/user_generated_content/user_generated_content.dart' + show Engagement; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +part 'comment.g.dart'; + +/// {@template user_comment} +/// A value object representing the comment content within an [Engagement]. +/// {@endtemplate} +@immutable +@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +class Comment extends Equatable { + /// {@macro user_comment} + const Comment({ + required this.language, + required this.content, + this.status = CommentStatus.pendingReview, + }); + + /// Creates a [Comment] from JSON data. + factory Comment.fromJson(Map json) => + _$CommentFromJson(json); + + /// The language of the comment. + final Language language; + + /// The text content of the comment. + final String content; + + /// The current moderation status of the comment. + final CommentStatus status; + + /// Converts this [Comment] instance to JSON data. + Map toJson() => _$CommentToJson(this); + + @override + List get props => [language, content, status]; + + /// Creates a copy of this [Comment] with updated values. + Comment copyWith({ + String? content, + Language? language, + CommentStatus? status, + }) { + return Comment( + language: language ?? this.language, + content: content ?? this.content, + status: status ?? this.status, + ); + } +} diff --git a/lib/src/models/user_generated_content/comment.g.dart b/lib/src/models/user_generated_content/comment.g.dart new file mode 100644 index 00000000..c8492fea --- /dev/null +++ b/lib/src/models/user_generated_content/comment.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'comment.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Comment _$CommentFromJson(Map json) => + $checkedCreate('Comment', json, ($checkedConvert) { + final val = Comment( + language: $checkedConvert( + 'language', + (v) => Language.fromJson(v as Map), + ), + content: $checkedConvert('content', (v) => v as String), + status: $checkedConvert( + 'status', + (v) => + $enumDecodeNullable(_$CommentStatusEnumMap, v) ?? + CommentStatus.pendingReview, + ), + ); + return val; + }); + +Map _$CommentToJson(Comment instance) => { + 'language': instance.language.toJson(), + 'content': instance.content, + 'status': _$CommentStatusEnumMap[instance.status]!, +}; + +const _$CommentStatusEnumMap = { + CommentStatus.pendingReview: 'pendingReview', + CommentStatus.approved: 'approved', + CommentStatus.rejected: 'rejected', + CommentStatus.flaggedByAI: 'flaggedByAI', + CommentStatus.hiddenByUser: 'hiddenByUser', +}; diff --git a/lib/src/models/user_generated_content/engagement.dart b/lib/src/models/user_generated_content/engagement.dart new file mode 100644 index 00000000..273df4f7 --- /dev/null +++ b/lib/src/models/user_generated_content/engagement.dart @@ -0,0 +1,89 @@ +import 'package:core/src/enums/engageable_type.dart'; +import 'package:core/src/models/user_generated_content/comment.dart'; +import 'package:core/src/models/user_generated_content/reaction.dart'; +import 'package:core/src/utils/json_helpers.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +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. +/// {@endtemplate} +@immutable +@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +class Engagement extends Equatable { + /// {@macro engagement} + const Engagement({ + required this.id, + required this.userId, + required this.entityId, + required this.entityType, + required this.reaction, + required this.createdAt, + required this.updatedAt, + this.comment, + }); + + /// Creates an [Engagement] from JSON data. + factory Engagement.fromJson(Map json) => + _$EngagementFromJson(json); + + /// The unique identifier for the engagement. + final String id; + + /// The ID of the user who made the engagement. + final String userId; + + /// The ID of the entity being engaged with (e.g., a headline ID). + final String entityId; + + /// 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 optional comment, provided along with the reaction. + final Comment? comment; + + /// The timestamp when the engagement was created. + @JsonKey(fromJson: dateTimeFromJson, toJson: dateTimeToJson) + final DateTime createdAt; + + /// The timestamp when the engagement was last updated. + @JsonKey(fromJson: dateTimeFromJson, toJson: dateTimeToJson) + final DateTime updatedAt; + + /// Converts this [Engagement] instance to JSON data. + Map toJson() => _$EngagementToJson(this); + + @override + List get props => [ + id, + userId, + entityId, + entityType, + reaction, + comment, + createdAt, + updatedAt, + ]; + + /// Creates a copy of this [Engagement] with updated values. + Engagement copyWith({Reaction? reaction, Comment? comment}) { + return Engagement( + id: id, + userId: userId, + entityId: entityId, + entityType: entityType, + reaction: reaction ?? this.reaction, + comment: comment ?? this.comment, + createdAt: createdAt, + updatedAt: DateTime.now(), + ); + } +} diff --git a/lib/src/models/user_generated_content/engagement.g.dart b/lib/src/models/user_generated_content/engagement.g.dart new file mode 100644 index 00000000..2c5c7755 --- /dev/null +++ b/lib/src/models/user_generated_content/engagement.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'engagement.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Engagement _$EngagementFromJson(Map json) => + $checkedCreate('Engagement', json, ($checkedConvert) { + final val = Engagement( + id: $checkedConvert('id', (v) => v as String), + userId: $checkedConvert('userId', (v) => v as String), + entityId: $checkedConvert('entityId', (v) => v as String), + entityType: $checkedConvert( + 'entityType', + (v) => $enumDecode(_$EngageableTypeEnumMap, v), + ), + reaction: $checkedConvert( + 'reaction', + (v) => Reaction.fromJson(v as Map), + ), + createdAt: $checkedConvert( + 'createdAt', + (v) => dateTimeFromJson(v as String?), + ), + updatedAt: $checkedConvert( + 'updatedAt', + (v) => dateTimeFromJson(v as String?), + ), + comment: $checkedConvert( + 'comment', + (v) => v == null ? null : Comment.fromJson(v as Map), + ), + ); + return val; + }); + +Map _$EngagementToJson(Engagement instance) => + { + 'id': instance.id, + 'userId': instance.userId, + 'entityId': instance.entityId, + 'entityType': _$EngageableTypeEnumMap[instance.entityType]!, + 'reaction': instance.reaction.toJson(), + 'comment': instance.comment?.toJson(), + 'createdAt': dateTimeToJson(instance.createdAt), + 'updatedAt': dateTimeToJson(instance.updatedAt), + }; + +const _$EngageableTypeEnumMap = {EngageableType.headline: 'headline'}; diff --git a/lib/src/models/user_generated_content/reaction.dart b/lib/src/models/user_generated_content/reaction.dart new file mode 100644 index 00000000..ddd7ebcd --- /dev/null +++ b/lib/src/models/user_generated_content/reaction.dart @@ -0,0 +1,40 @@ +import 'package:core/core.dart' show Engagement; +import 'package:core/src/enums/reaction_type.dart'; +import 'package:core/src/models/models.dart' show Engagement; +import 'package:core/src/models/user_generated_content/engagement.dart' + show Engagement; +import 'package:core/src/models/user_generated_content/user_generated_content.dart' + show Engagement; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +part 'reaction.g.dart'; + +/// {@template reaction} +/// A value object representing the type of reaction within an [Engagement]. +/// {@endtemplate} +@immutable +@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +class Reaction extends Equatable { + /// {@macro reaction} + const Reaction({required this.reactionType}); + + /// Creates a [Reaction] from JSON data. + factory Reaction.fromJson(Map json) => + _$ReactionFromJson(json); + + /// The type of reaction (e.g., like, insightful). + final ReactionType reactionType; + + /// Converts this [Reaction] instance to JSON data. + Map toJson() => _$ReactionToJson(this); + + @override + List get props => [reactionType]; + + /// Creates a copy of this [Reaction] with updated values. + Reaction copyWith({ReactionType? reactionType}) { + return Reaction(reactionType: reactionType ?? this.reactionType); + } +} diff --git a/lib/src/models/user_generated_content/reaction.g.dart b/lib/src/models/user_generated_content/reaction.g.dart new file mode 100644 index 00000000..209d6034 --- /dev/null +++ b/lib/src/models/user_generated_content/reaction.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'reaction.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Reaction _$ReactionFromJson(Map json) => + $checkedCreate('Reaction', json, ($checkedConvert) { + final val = Reaction( + reactionType: $checkedConvert( + 'reactionType', + (v) => $enumDecode(_$ReactionTypeEnumMap, v), + ), + ); + return val; + }); + +Map _$ReactionToJson(Reaction instance) => { + 'reactionType': _$ReactionTypeEnumMap[instance.reactionType]!, +}; + +const _$ReactionTypeEnumMap = { + ReactionType.like: 'like', + ReactionType.insightful: 'insightful', + ReactionType.amusing: 'amusing', + ReactionType.sad: 'sad', + ReactionType.angry: 'angry', + ReactionType.skeptical: 'skeptical', +}; diff --git a/lib/src/models/user_generated_content/report.dart b/lib/src/models/user_generated_content/report.dart new file mode 100644 index 00000000..9bb70ba6 --- /dev/null +++ b/lib/src/models/user_generated_content/report.dart @@ -0,0 +1,116 @@ +import 'package:core/src/enums/comment_report_reason.dart'; +import 'package:core/src/enums/headline_report_reason.dart'; +import 'package:core/src/enums/report_status.dart'; +import 'package:core/src/enums/reportable_entity.dart'; +import 'package:core/src/enums/source_report_reason.dart'; +import 'package:core/src/utils/json_helpers.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +part 'report.g.dart'; + +/// A wrapper class to distinguish between a field that is not provided and a +/// field that is explicitly set to null. +@immutable +class ValueWrapper { + const ValueWrapper(this.value); + + /// The value being wrapped. + final T value; +} + +/// {@template report} +/// A flexible data model for handling user reports across different entity +/// types. +/// +/// It uses a `ReportableEntity` enum and `entityId` to dynamically target +/// headlines, sources, or comments. The model includes the report reason, +/// additional comments, and a `ReportStatus` for the moderation workflow. +/// {@endtemplate} +@immutable +@JsonSerializable(explicitToJson: true, includeIfNull: true, checked: true) +class Report extends Equatable { + /// {@macro report} + const Report({ + required this.id, + required this.reporterUserId, + required this.entityType, + required this.entityId, + required this.reason, + required this.status, + required this.createdAt, + this.additionalComments, + }); + + /// Creates a [Report] from JSON data. + factory Report.fromJson(Map json) => _$ReportFromJson(json); + + /// The unique identifier for the report. + final String id; + + /// The ID of the user who made the report. + final String reporterUserId; + + /// The type of entity being reported (e.g., headline, source, comment). + final ReportableEntity entityType; + + /// The ID of the specific item being reported. + final String entityId; + + /// The specific reason for the report. This is a dynamic field that holds + /// the string value of the relevant reason enum (e.g., + /// [HeadlineReportReason], [SourceReportReason], [CommentReportReason]). + final String reason; + + /// The current moderation status of the report. + final ReportStatus status; + + /// Optional additional comments from the user providing more context. + final String? additionalComments; + + /// The timestamp when the report was created. + @JsonKey(fromJson: dateTimeFromJson, toJson: dateTimeToJson) + final DateTime createdAt; + + /// Converts this [Report] instance to JSON data. + Map toJson() => _$ReportToJson(this); + + @override + List get props => [ + id, + reporterUserId, + entityType, + entityId, + reason, + status, + additionalComments, + createdAt, + ]; + + /// Creates a copy of this [Report] but with the given fields replaced + /// with the new values. + Report copyWith({ + String? id, + String? reporterUserId, + ReportableEntity? entityType, + String? entityId, + String? reason, + ReportStatus? status, + ValueWrapper? additionalComments, + DateTime? createdAt, + }) { + return Report( + id: id ?? this.id, + reporterUserId: reporterUserId ?? this.reporterUserId, + entityType: entityType ?? this.entityType, + entityId: entityId ?? this.entityId, + reason: reason ?? this.reason, + status: status ?? this.status, + additionalComments: additionalComments != null + ? additionalComments.value + : this.additionalComments, + createdAt: createdAt ?? this.createdAt, + ); + } +} diff --git a/lib/src/models/user_generated_content/report.g.dart b/lib/src/models/user_generated_content/report.g.dart new file mode 100644 index 00000000..84a93a58 --- /dev/null +++ b/lib/src/models/user_generated_content/report.g.dart @@ -0,0 +1,57 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'report.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Report _$ReportFromJson(Map json) => + $checkedCreate('Report', json, ($checkedConvert) { + final val = Report( + id: $checkedConvert('id', (v) => v as String), + reporterUserId: $checkedConvert('reporterUserId', (v) => v as String), + entityType: $checkedConvert( + 'entityType', + (v) => $enumDecode(_$ReportableEntityEnumMap, v), + ), + entityId: $checkedConvert('entityId', (v) => v as String), + reason: $checkedConvert('reason', (v) => v as String), + status: $checkedConvert( + 'status', + (v) => $enumDecode(_$ReportStatusEnumMap, v), + ), + createdAt: $checkedConvert( + 'createdAt', + (v) => dateTimeFromJson(v as String?), + ), + additionalComments: $checkedConvert( + 'additionalComments', + (v) => v as String?, + ), + ); + return val; + }); + +Map _$ReportToJson(Report instance) => { + 'id': instance.id, + 'reporterUserId': instance.reporterUserId, + 'entityType': _$ReportableEntityEnumMap[instance.entityType]!, + 'entityId': instance.entityId, + 'reason': instance.reason, + 'status': _$ReportStatusEnumMap[instance.status]!, + 'additionalComments': instance.additionalComments, + 'createdAt': dateTimeToJson(instance.createdAt), +}; + +const _$ReportableEntityEnumMap = { + ReportableEntity.headline: 'headline', + ReportableEntity.source: 'source', + ReportableEntity.engagement: 'engagement', +}; + +const _$ReportStatusEnumMap = { + ReportStatus.submitted: 'submitted', + ReportStatus.inReview: 'inReview', + ReportStatus.resolved: 'resolved', +}; diff --git a/lib/src/models/user_generated_content/user_generated_content.dart b/lib/src/models/user_generated_content/user_generated_content.dart new file mode 100644 index 00000000..9f354f46 --- /dev/null +++ b/lib/src/models/user_generated_content/user_generated_content.dart @@ -0,0 +1,4 @@ +export 'comment.dart'; +export 'engagement.dart'; +export 'reaction.dart'; +export 'report.dart'; diff --git a/test/src/enums/comment_report_reason_test.dart b/test/src/enums/comment_report_reason_test.dart new file mode 100644 index 00000000..f6ceec2b --- /dev/null +++ b/test/src/enums/comment_report_reason_test.dart @@ -0,0 +1,33 @@ +import 'package:core/src/enums/comment_report_reason.dart'; +import 'package:test/test.dart'; + +void main() { + group('CommentReportReason', () { + test('has correct values', () { + expect( + CommentReportReason.values, + containsAll([ + CommentReportReason.spamOrAdvertising, + CommentReportReason.harassmentOrBullying, + CommentReportReason.hateSpeech, + ]), + ); + }); + + test('has correct string values', () { + expect(CommentReportReason.spamOrAdvertising.name, 'spamOrAdvertising'); + expect( + CommentReportReason.harassmentOrBullying.name, + 'harassmentOrBullying', + ); + expect(CommentReportReason.hateSpeech.name, 'hateSpeech'); + }); + + test('can be created from string values', () { + expect( + CommentReportReason.values.byName('spamOrAdvertising'), + CommentReportReason.spamOrAdvertising, + ); + }); + }); +} diff --git a/test/src/enums/comment_status_test.dart b/test/src/enums/comment_status_test.dart new file mode 100644 index 00000000..e1a711fa --- /dev/null +++ b/test/src/enums/comment_status_test.dart @@ -0,0 +1,40 @@ +import 'package:core/src/enums/comment_status.dart'; +import 'package:test/test.dart'; + +void main() { + group('CommentStatus', () { + test('has correct values', () { + expect( + CommentStatus.values, + containsAll([ + CommentStatus.pendingReview, + CommentStatus.approved, + CommentStatus.rejected, + CommentStatus.flaggedByAI, + CommentStatus.hiddenByUser, + ]), + ); + }); + + test('has correct string values', () { + expect(CommentStatus.pendingReview.name, 'pendingReview'); + expect(CommentStatus.approved.name, 'approved'); + expect(CommentStatus.rejected.name, 'rejected'); + expect(CommentStatus.flaggedByAI.name, 'flaggedByAI'); + expect(CommentStatus.hiddenByUser.name, 'hiddenByUser'); + }); + + test('can be created from string values', () { + expect( + CommentStatus.values.byName('pendingReview'), + CommentStatus.pendingReview, + ); + expect(CommentStatus.values.byName('approved'), CommentStatus.approved); + expect(CommentStatus.values.byName('rejected'), CommentStatus.rejected); + expect( + CommentStatus.values.byName('flaggedByAI'), + CommentStatus.flaggedByAI, + ); + }); + }); +} diff --git a/test/src/enums/engageable_type_test.dart b/test/src/enums/engageable_type_test.dart new file mode 100644 index 00000000..d463f6ef --- /dev/null +++ b/test/src/enums/engageable_type_test.dart @@ -0,0 +1,39 @@ +import 'package:core/src/enums/engageable_type.dart'; +import 'package:test/test.dart'; + +void main() { + group('EngageableType', () { + test('has correct values', () { + expect(EngageableType.values, containsAll([EngageableType.headline])); + }); + + 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(EngageableType.headline.name, 'headline'); + }); + + 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( + EngageableType.values.byName('headline'), + EngageableType.headline, + ); + }); + + test('throws ArgumentError for invalid string value', () { + // Verifies that an unknown string cannot be converted to an enum value. + expect( + () => EngageableType.values.byName('invalid_type'), + throwsA(isA()), + ); + }); + }); + + test('has correct toString representation', () { + expect(EngageableType.headline.toString(), 'EngageableType.headline'); + }); + }); +} diff --git a/test/src/enums/engagement_mode_test.dart b/test/src/enums/engagement_mode_test.dart new file mode 100644 index 00000000..ba41d648 --- /dev/null +++ b/test/src/enums/engagement_mode_test.dart @@ -0,0 +1,28 @@ +import 'package:core/src/enums/engagement_mode.dart'; +import 'package:test/test.dart'; + +void main() { + group('EngagementMode', () { + test('has correct values', () { + expect( + EngagementMode.values, + containsAll([ + EngagementMode.reactionsOnly, + EngagementMode.reactionsAndComments, + ]), + ); + }); + + test('has correct string values', () { + expect(EngagementMode.reactionsOnly.name, 'reactionsOnly'); + expect(EngagementMode.reactionsAndComments.name, 'reactionsAndComments'); + }); + + test('can be created from string values', () { + expect( + EngagementMode.values.byName('reactionsOnly'), + EngagementMode.reactionsOnly, + ); + }); + }); +} diff --git a/test/src/enums/headline_report_reason_test.dart b/test/src/enums/headline_report_reason_test.dart new file mode 100644 index 00000000..6eb7848b --- /dev/null +++ b/test/src/enums/headline_report_reason_test.dart @@ -0,0 +1,46 @@ +import 'package:core/src/enums/headline_report_reason.dart'; +import 'package:test/test.dart'; + +void main() { + group('HeadlineReportReason', () { + test('has correct values', () { + expect( + HeadlineReportReason.values, + containsAll([ + HeadlineReportReason.misinformationOrFakeNews, + HeadlineReportReason.clickbaitTitle, + HeadlineReportReason.offensiveOrHateSpeech, + HeadlineReportReason.spamOrScam, + HeadlineReportReason.brokenLink, + HeadlineReportReason.paywalled, + ]), + ); + }); + + test('has correct string values', () { + expect( + HeadlineReportReason.misinformationOrFakeNews.name, + 'misinformationOrFakeNews', + ); + expect(HeadlineReportReason.clickbaitTitle.name, 'clickbaitTitle'); + expect( + HeadlineReportReason.offensiveOrHateSpeech.name, + 'offensiveOrHateSpeech', + ); + expect(HeadlineReportReason.spamOrScam.name, 'spamOrScam'); + expect(HeadlineReportReason.brokenLink.name, 'brokenLink'); + expect(HeadlineReportReason.paywalled.name, 'paywalled'); + }); + + test('can be created from string values', () { + expect( + HeadlineReportReason.values.byName('misinformationOrFakeNews'), + HeadlineReportReason.misinformationOrFakeNews, + ); + expect( + HeadlineReportReason.values.byName('clickbaitTitle'), + HeadlineReportReason.clickbaitTitle, + ); + }); + }); +} diff --git a/test/src/enums/reaction_type_test.dart b/test/src/enums/reaction_type_test.dart new file mode 100644 index 00000000..7f45b0bd --- /dev/null +++ b/test/src/enums/reaction_type_test.dart @@ -0,0 +1,38 @@ +import 'package:core/src/enums/reaction_type.dart'; +import 'package:test/test.dart'; + +void main() { + group('ReactionType', () { + test('has correct values', () { + expect( + ReactionType.values, + containsAll([ + ReactionType.like, + ReactionType.insightful, + ReactionType.amusing, + ReactionType.sad, + ReactionType.angry, + ReactionType.skeptical, + ]), + ); + }); + + test('has correct string values', () { + expect(ReactionType.like.name, 'like'); + expect(ReactionType.insightful.name, 'insightful'); + expect(ReactionType.amusing.name, 'amusing'); + expect(ReactionType.sad.name, 'sad'); + expect(ReactionType.angry.name, 'angry'); + expect(ReactionType.skeptical.name, 'skeptical'); + }); + + test('can be created from string values', () { + expect(ReactionType.values.byName('like'), ReactionType.like); + expect(ReactionType.values.byName('insightful'), ReactionType.insightful); + expect(ReactionType.values.byName('amusing'), ReactionType.amusing); + expect(ReactionType.values.byName('sad'), ReactionType.sad); + expect(ReactionType.values.byName('angry'), ReactionType.angry); + expect(ReactionType.values.byName('skeptical'), ReactionType.skeptical); + }); + }); +} diff --git a/test/src/enums/report_status_test.dart b/test/src/enums/report_status_test.dart new file mode 100644 index 00000000..9be135f5 --- /dev/null +++ b/test/src/enums/report_status_test.dart @@ -0,0 +1,29 @@ +import 'package:core/src/enums/report_status.dart'; +import 'package:test/test.dart'; + +void main() { + group('ReportStatus', () { + test('has correct values', () { + expect( + ReportStatus.values, + containsAll([ + ReportStatus.submitted, + ReportStatus.inReview, + ReportStatus.resolved, + ]), + ); + }); + + test('has correct string values', () { + expect(ReportStatus.submitted.name, 'submitted'); + expect(ReportStatus.inReview.name, 'inReview'); + expect(ReportStatus.resolved.name, 'resolved'); + }); + + test('can be created from string values', () { + expect(ReportStatus.values.byName('submitted'), ReportStatus.submitted); + expect(ReportStatus.values.byName('inReview'), ReportStatus.inReview); + expect(ReportStatus.values.byName('resolved'), ReportStatus.resolved); + }); + }); +} diff --git a/test/src/enums/reportable_entity_test.dart b/test/src/enums/reportable_entity_test.dart new file mode 100644 index 00000000..1ab6dbdc --- /dev/null +++ b/test/src/enums/reportable_entity_test.dart @@ -0,0 +1,35 @@ +import 'package:core/src/enums/reportable_entity.dart'; +import 'package:test/test.dart'; + +void main() { + group('ReportableEntity', () { + test('has correct values', () { + expect( + ReportableEntity.values, + containsAll([ + ReportableEntity.headline, + ReportableEntity.source, + ReportableEntity.engagement, + ]), + ); + }); + + test('has correct string values', () { + expect(ReportableEntity.headline.name, 'headline'); + expect(ReportableEntity.source.name, 'source'); + expect(ReportableEntity.engagement.name, 'engagement'); + }); + + test('can be created from string values', () { + expect( + ReportableEntity.values.byName('headline'), + ReportableEntity.headline, + ); + expect(ReportableEntity.values.byName('source'), ReportableEntity.source); + expect( + ReportableEntity.values.byName('engagement'), + ReportableEntity.engagement, + ); + }); + }); +} diff --git a/test/src/enums/source_report_reason_test.dart b/test/src/enums/source_report_reason_test.dart new file mode 100644 index 00000000..fbe1cf0e --- /dev/null +++ b/test/src/enums/source_report_reason_test.dart @@ -0,0 +1,44 @@ +import 'package:core/src/enums/source_report_reason.dart'; +import 'package:test/test.dart'; + +void main() { + group('SourceReportReason', () { + test('has correct values', () { + expect( + SourceReportReason.values, + containsAll([ + SourceReportReason.lowQualityJournalism, + SourceReportReason.highAdDensity, + SourceReportReason.frequentPaywalls, + SourceReportReason.impersonation, + SourceReportReason.spreadsMisinformation, + ]), + ); + }); + + test('has correct string values', () { + expect( + SourceReportReason.lowQualityJournalism.name, + 'lowQualityJournalism', + ); + expect(SourceReportReason.highAdDensity.name, 'highAdDensity'); + expect(SourceReportReason.frequentPaywalls.name, 'frequentPaywalls'); + expect(SourceReportReason.impersonation.name, 'impersonation'); + expect( + SourceReportReason.spreadsMisinformation.name, + 'spreadsMisinformation', + ); + }); + + test('can be created from string values', () { + expect( + SourceReportReason.values.byName('lowQualityJournalism'), + SourceReportReason.lowQualityJournalism, + ); + expect( + SourceReportReason.values.byName('highAdDensity'), + SourceReportReason.highAdDensity, + ); + }); + }); +} diff --git a/test/src/models/config/app_review_config_test.dart b/test/src/models/config/app_review_config_test.dart new file mode 100644 index 00000000..e911e107 --- /dev/null +++ b/test/src/models/config/app_review_config_test.dart @@ -0,0 +1,39 @@ +import 'package:core/core.dart'; +import 'package:test/test.dart'; + +void main() { + group('AppReviewConfig', () { + final appReviewConfigFixture = + remoteConfigsFixturesData.first.features.community.appReview; + final json = appReviewConfigFixture.toJson(); + + test('can be instantiated', () { + expect(appReviewConfigFixture, isA()); + }); + + test('supports value equality', () { + final anotherConfig = + remoteConfigsFixturesData.first.features.community.appReview; + expect(appReviewConfigFixture, equals(anotherConfig)); + }); + + test('can be created from JSON', () { + final fromJson = AppReviewConfig.fromJson(json); + expect(fromJson, equals(appReviewConfigFixture)); + }); + + test('can be converted to JSON', () { + final toJson = appReviewConfigFixture.toJson(); + expect(toJson, equals(json)); + }); + + test('copyWith creates a copy with updated values', () { + final updatedConfig = appReviewConfigFixture.copyWith( + positiveInteractionThreshold: 10, + ); + + expect(updatedConfig.positiveInteractionThreshold, 10); + expect(updatedConfig, isNot(equals(appReviewConfigFixture))); + }); + }); +} diff --git a/test/src/models/config/engagement_config_test.dart b/test/src/models/config/engagement_config_test.dart new file mode 100644 index 00000000..6fdf3c92 --- /dev/null +++ b/test/src/models/config/engagement_config_test.dart @@ -0,0 +1,37 @@ +import 'package:core/core.dart'; +import 'package:test/test.dart'; + +void main() { + group('EngagementConfig', () { + final engagementConfigFixture = + remoteConfigsFixturesData.first.features.community.engagement; + final json = engagementConfigFixture.toJson(); + + test('can be instantiated', () { + expect(engagementConfigFixture, isA()); + }); + + test('supports value equality', () { + final anotherConfig = + remoteConfigsFixturesData.first.features.community.engagement; + expect(engagementConfigFixture, equals(anotherConfig)); + }); + + test('can be created from JSON', () { + final fromJson = EngagementConfig.fromJson(json); + expect(fromJson, equals(engagementConfigFixture)); + }); + + test('can be converted to JSON', () { + final toJson = engagementConfigFixture.toJson(); + expect(toJson, equals(json)); + }); + + test('copyWith creates a copy with updated values', () { + final updatedConfig = engagementConfigFixture.copyWith(enabled: false); + + expect(updatedConfig.enabled, isFalse); + expect(updatedConfig, isNot(equals(engagementConfigFixture))); + }); + }); +} diff --git a/test/src/models/config/features_config_test.dart b/test/src/models/config/features_config_test.dart index 74329eb8..e9bf8eb8 100644 --- a/test/src/models/config/features_config_test.dart +++ b/test/src/models/config/features_config_test.dart @@ -22,6 +22,7 @@ void main() { featuresConfigFixture.ads, featuresConfigFixture.pushNotifications, featuresConfigFixture.feed, + featuresConfigFixture.community, ]), ); }); diff --git a/test/src/models/config/reporting_config_test.dart b/test/src/models/config/reporting_config_test.dart new file mode 100644 index 00000000..4286032e --- /dev/null +++ b/test/src/models/config/reporting_config_test.dart @@ -0,0 +1,39 @@ +import 'package:core/core.dart'; +import 'package:test/test.dart'; + +void main() { + group('ReportingConfig', () { + final reportingConfigFixture = + remoteConfigsFixturesData.first.features.community.reporting; + final json = reportingConfigFixture.toJson(); + + test('can be instantiated', () { + expect(reportingConfigFixture, isA()); + }); + + test('supports value equality', () { + final anotherConfig = + remoteConfigsFixturesData.first.features.community.reporting; + expect(reportingConfigFixture, equals(anotherConfig)); + }); + + test('can be created from JSON', () { + final fromJson = ReportingConfig.fromJson(json); + expect(fromJson, equals(reportingConfigFixture)); + }); + + test('can be converted to JSON', () { + final toJson = reportingConfigFixture.toJson(); + expect(toJson, equals(json)); + }); + + test('copyWith creates a copy with updated values', () { + final updatedConfig = reportingConfigFixture.copyWith( + headlineReportingEnabled: false, + ); + + expect(updatedConfig.headlineReportingEnabled, isFalse); + expect(updatedConfig, isNot(equals(reportingConfigFixture))); + }); + }); +} diff --git a/test/src/models/config/user_limits_config_test.dart b/test/src/models/config/user_limits_config_test.dart index 761e3ea6..8a8a4f92 100644 --- a/test/src/models/config/user_limits_config_test.dart +++ b/test/src/models/config/user_limits_config_test.dart @@ -23,6 +23,9 @@ void main() { userLimitsConfigFixture.savedHeadlines, userLimitsConfigFixture.savedHeadlineFilters, userLimitsConfigFixture.savedSourceFilters, + userLimitsConfigFixture.reactionsPerDay, + userLimitsConfigFixture.commentsPerDay, + userLimitsConfigFixture.reportsPerDay, ]), ); }); diff --git a/test/src/models/core/core.dart b/test/src/models/core/core.dart deleted file mode 100644 index af5e08f7..00000000 --- a/test/src/models/core/core.dart +++ /dev/null @@ -1 +0,0 @@ -export 'feed_item_test.dart'; diff --git a/test/src/models/entities/headline_test.dart b/test/src/models/entities/headline_test.dart index b1d2af23..88a46c74 100644 --- a/test/src/models/entities/headline_test.dart +++ b/test/src/models/entities/headline_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { group('Headline Model', () { // Use the first headline from the fixtures as the base for testing. - final headlineFixture = headlinesFixturesData.first; + final headlineFixture = getHeadlinesFixturesData().first; final headlineJson = headlineFixture.toJson(); group('fromJson', () { diff --git a/test/src/models/entities/source_test.dart b/test/src/models/entities/source_test.dart index 8dac4a5d..9355d419 100644 --- a/test/src/models/entities/source_test.dart +++ b/test/src/models/entities/source_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { group('Source Model', () { - final sourceFixture = sourcesFixturesData.first; + final sourceFixture = getSourcesFixturesData().first; final sourceJson = sourceFixture.toJson(); group('Constructor', () { diff --git a/test/src/models/entities/topic_test.dart b/test/src/models/entities/topic_test.dart index 01a4b86e..fdef8c2d 100644 --- a/test/src/models/entities/topic_test.dart +++ b/test/src/models/entities/topic_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { group('Topic', () { - final topicFixture = topicsFixturesData.first; + final topicFixture = getTopicsFixturesData().first; final topicJson = topicFixture.toJson(); test('supports value equality', () { diff --git a/test/src/models/feed_decorators/call_to_action_item_test.dart b/test/src/models/feed/call_to_action_item_test.dart similarity index 100% rename from test/src/models/feed_decorators/call_to_action_item_test.dart rename to test/src/models/feed/call_to_action_item_test.dart diff --git a/test/src/models/feed_decorators/content_collection_item_test.dart b/test/src/models/feed/content_collection_item_test.dart similarity index 95% rename from test/src/models/feed_decorators/content_collection_item_test.dart rename to test/src/models/feed/content_collection_item_test.dart index 3e098ba6..3020f9f7 100644 --- a/test/src/models/feed_decorators/content_collection_item_test.dart +++ b/test/src/models/feed/content_collection_item_test.dart @@ -3,8 +3,8 @@ import 'package:test/test.dart'; void main() { group('ContentCollectionItem', () { - final mockTopics = topicsFixturesData.take(3).toList(); - final mockSources = sourcesFixturesData.take(3).toList(); + final mockTopics = getTopicsFixturesData().take(3).toList(); + final mockSources = getSourcesFixturesData().take(3).toList(); final mockTopicCollection = ContentCollectionItem( id: 'cc-topic-1', diff --git a/test/src/models/core/feed_item_test.dart b/test/src/models/feed/feed_item_test.dart similarity index 95% rename from test/src/models/core/feed_item_test.dart rename to test/src/models/feed/feed_item_test.dart index fd76ac58..d3631840 100644 --- a/test/src/models/core/feed_item_test.dart +++ b/test/src/models/feed/feed_item_test.dart @@ -3,9 +3,9 @@ import 'package:test/test.dart'; void main() { group('FeedItem', () { - final mockHeadline = headlinesFixturesData.first; - final mockTopic = topicsFixturesData.first; - final mockSource = sourcesFixturesData.first; + final mockHeadline = getHeadlinesFixturesData().first; + final mockTopic = getTopicsFixturesData().first; + final mockSource = getSourcesFixturesData().first; final mockCountry = countriesFixturesData.first; const mockCallToAction = CallToActionItem( @@ -20,14 +20,14 @@ void main() { final mockContentCollectionTopic = ContentCollectionItem( id: 'cc-topic-1', decoratorType: FeedDecoratorType.suggestedTopics, - items: topicsFixturesData.take(3).toList(), + items: getTopicsFixturesData().take(3).toList(), title: 'Suggested Topics', ); final mockContentCollectionSource = ContentCollectionItem( id: 'cc-source-1', decoratorType: FeedDecoratorType.suggestedSources, - items: sourcesFixturesData.take(3).toList(), + items: getSourcesFixturesData().take(3).toList(), title: 'Suggested Sources', ); diff --git a/test/src/models/user_generated_content/comment_test.dart b/test/src/models/user_generated_content/comment_test.dart new file mode 100644 index 00000000..0ffd57bf --- /dev/null +++ b/test/src/models/user_generated_content/comment_test.dart @@ -0,0 +1,75 @@ +import 'package:core/core.dart'; +import 'package:test/test.dart'; + +void main() { + group('Comment', () { + final now = DateTime.now(); + // Use the first item from the fixtures as the test subject. + final commentFixture = getHeadlineCommentsFixturesData( + now: now, + languageCode: 'en', + ).first; + + group('constructor', () { + test('returns correct instance', () { + expect(commentFixture, isA()); + }); + }); + + group('fromJson/toJson', () { + test('round trip with all fields populated', () { + final json = commentFixture.toJson(); + final result = Comment.fromJson(json); + expect(result, equals(commentFixture)); + }); + }); + + group('copyWith', () { + test('returns a new instance with updated fields', () { + const newContent = 'This is updated content.'; + const newStatus = CommentStatus.rejected; + + final updatedComment = commentFixture.copyWith( + content: newContent, + status: newStatus, + ); + + expect(updatedComment.content, newContent); + expect(updatedComment.status, newStatus); + // Verify other fields remain unchanged + expect(updatedComment.language, commentFixture.language); + }); + }); + + group('Equatable', () { + test('instances with the same properties are equal', () { + final comment1 = getHeadlineCommentsFixturesData( + now: now, + languageCode: 'en', + ).first; + final comment2 = getHeadlineCommentsFixturesData( + now: now, + languageCode: 'en', + ).first; + expect(comment1, equals(comment2)); + }); + + test('instances with different properties are not equal', () { + final comment1 = getHeadlineCommentsFixturesData(now: now).first; + final comment2 = getHeadlineCommentsFixturesData(now: now)[1]; + expect(comment1, isNot(equals(comment2))); + }); + }); + + test('props list should contain all relevant fields', () { + expect( + commentFixture.props, + equals([ + commentFixture.language, + commentFixture.content, + commentFixture.status, + ]), + ); + }); + }); +} diff --git a/test/src/models/user_generated_content/engagement_test.dart b/test/src/models/user_generated_content/engagement_test.dart new file mode 100644 index 00000000..f1bb6b30 --- /dev/null +++ b/test/src/models/user_generated_content/engagement_test.dart @@ -0,0 +1,114 @@ +import 'package:core/core.dart'; +import 'package:test/test.dart'; + +void main() { + group('Engagement', () { + final now = DateTime.now(); + // Use the first item from the fixtures as the test subject. + final engagementFixture = getEngagementsFixturesData( + now: now, + languageCode: 'en', + ).first; + + group('constructor', () { + test('returns correct instance', () { + expect(engagementFixture, isA()); + }); + + test('returns correct instance with populated comment', () { + // The first fixture item should have a comment + expect(engagementFixture.comment, isNotNull); + }); + + test('returns correct instance with null comment', () { + // The second fixture item should have a null comment + final engagementWithoutComment = getEngagementsFixturesData( + now: now, + )[1]; + expect(engagementWithoutComment.comment, isNull); + }); + }); + + group('fromJson/toJson', () { + test('round trip with all fields populated', () { + final json = engagementFixture.toJson(); + final result = Engagement.fromJson(json); + expect(result, equals(engagementFixture)); + }); + + test('round trip with null comment', () { + final engagementWithoutComment = getEngagementsFixturesData( + now: now, + )[1]; + final json = engagementWithoutComment.toJson(); + final result = Engagement.fromJson(json); + expect(result, equals(engagementWithoutComment)); + }); + }); + + group('copyWith', () { + test('returns a new instance with updated fields', () { + final newReaction = reactionsFixturesData[2]; + + final updatedEngagement = engagementFixture.copyWith( + reaction: newReaction, + ); + + expect(updatedEngagement.reaction, newReaction); + // Verify other fields remain unchanged + expect(updatedEngagement.id, engagementFixture.id); + expect(updatedEngagement.userId, engagementFixture.userId); + expect(updatedEngagement.entityId, engagementFixture.entityId); + expect(updatedEngagement.comment, engagementFixture.comment); + expect(updatedEngagement.createdAt, engagementFixture.createdAt); + // The updatedAt timestamp should be different + expect( + updatedEngagement.updatedAt, + isNot(equals(engagementFixture.updatedAt)), + ); + }); + + test( + 'returns a new instance with a new timestamp if no updates provided', + () { + final copiedEngagement = engagementFixture.copyWith(); + expect(copiedEngagement, isNot(equals(engagementFixture))); + expect(copiedEngagement.id, engagementFixture.id); + }, + ); + }); + + group('Equatable', () { + test('instances with the same properties are equal', () { + final engagement1 = getEngagementsFixturesData( + now: now, + languageCode: 'en', + ).first; + final engagement2 = getEngagementsFixturesData( + now: now, + languageCode: 'en', + ).first; + expect(engagement1, equals(engagement2)); + }); + + test('instances with different properties are not equal', () { + final engagement1 = getEngagementsFixturesData(now: now)[0]; + final engagement2 = getEngagementsFixturesData(now: now)[1]; + expect(engagement1, isNot(equals(engagement2))); + }); + }); + + test('props list should contain all relevant fields', () { + expect(engagementFixture.props, [ + engagementFixture.id, + engagementFixture.userId, + engagementFixture.entityId, + engagementFixture.entityType, + engagementFixture.reaction, + engagementFixture.comment, + engagementFixture.createdAt, + engagementFixture.updatedAt, + ]); + }); + }); +} diff --git a/test/src/models/user_generated_content/reaction_test.dart b/test/src/models/user_generated_content/reaction_test.dart new file mode 100644 index 00000000..44567dbe --- /dev/null +++ b/test/src/models/user_generated_content/reaction_test.dart @@ -0,0 +1,53 @@ +import 'package:core/core.dart'; +import 'package:test/test.dart'; + +void main() { + group('Reaction', () { + // Use the first item from the fixtures as the test subject. + final reactionFixture = reactionsFixturesData.first; + + group('constructor', () { + test('returns correct instance', () { + expect(reactionFixture, isA()); + }); + }); + + group('fromJson/toJson', () { + test('round trip with all fields populated', () { + final json = reactionFixture.toJson(); + final result = Reaction.fromJson(json); + expect(result, equals(reactionFixture)); + }); + }); + + group('copyWith', () { + test('returns a new instance with updated fields', () { + const newReactionType = ReactionType.insightful; + + final updatedReaction = reactionFixture.copyWith( + reactionType: newReactionType, + ); + + expect(updatedReaction.reactionType, newReactionType); + }); + }); + + group('Equatable', () { + test('instances with the same properties are equal', () { + final reaction1 = reactionsFixturesData.first.copyWith(); + final reaction2 = reactionsFixturesData.first.copyWith(); + expect(reaction1, equals(reaction2)); + }); + + test('instances with different properties are not equal', () { + final reaction1 = reactionsFixturesData[0]; + final reaction2 = reactionsFixturesData[1]; + expect(reaction1, isNot(equals(reaction2))); + }); + }); + + test('props list should contain all relevant fields', () { + expect(reactionFixture.props, equals([reactionFixture.reactionType])); + }); + }); +} diff --git a/test/src/models/user_generated_content/report_test.dart b/test/src/models/user_generated_content/report_test.dart new file mode 100644 index 00000000..4141f1c8 --- /dev/null +++ b/test/src/models/user_generated_content/report_test.dart @@ -0,0 +1,50 @@ +import 'package:core/core.dart'; +import 'package:test/test.dart'; + +void main() { + group('Report', () { + final now = DateTime.now(); + // Use the first item from the fixtures as the test subject. + final reportFixture = getReportsFixturesData(now: now).first; + + group('constructor', () { + test('returns correct instance', () { + expect(reportFixture, isA()); + }); + }); + + group('fromJson/toJson', () { + test('round trip with all fields populated', () { + final json = reportFixture.toJson(); + final fromJson = Report.fromJson(json); + expect(fromJson, equals(reportFixture)); + }); + }); + + group('copyWith', () { + test('returns a new instance with updated values', () { + final updatedReport = reportFixture.copyWith( + status: ReportStatus.resolved, + ); + expect(updatedReport.status, ReportStatus.resolved); + expect(updatedReport, isNot(equals(reportFixture))); + }); + + test('copyWith allows setting a field to null', () { + final updatedReport = reportFixture.copyWith( + // Use ValueWrapper to explicitly pass null. + additionalComments: const ValueWrapper(null), + ); + expect(updatedReport.additionalComments, isNull); + }); + }); + + group('Equatable', () { + test('instances with the same properties are equal', () { + final report1 = getReportsFixturesData(now: now).first; + final report2 = getReportsFixturesData(now: now).first; + expect(report1, equals(report2)); + }); + }); + }); +} diff --git a/test/src/models/user_preferences/headline_filter_criteria_test.dart b/test/src/models/user_preferences/headline_filter_criteria_test.dart index 56558717..5fe860e7 100644 --- a/test/src/models/user_preferences/headline_filter_criteria_test.dart +++ b/test/src/models/user_preferences/headline_filter_criteria_test.dart @@ -3,7 +3,7 @@ import 'package:test/test.dart'; void main() { group('HeadlineFilterCriteria', () { - final fullModel = savedHeadlineFiltersFixturesData[0].criteria; + final fullModel = getSavedHeadlineFiltersFixturesData()[0].criteria; final fullJson = fullModel.toJson(); const emptyModel = HeadlineFilterCriteria( @@ -46,7 +46,7 @@ void main() { }); test('copyWith should work correctly', () { - final topic2 = topicsFixturesData[1]; + final topic2 = getTopicsFixturesData()[1]; final copied = fullModel.copyWith(topics: [topic2]); expect(copied.topics, equals([topic2])); diff --git a/test/src/models/user_preferences/saved_headline_filter_test.dart b/test/src/models/user_preferences/saved_headline_filter_test.dart index ac731cc1..e7c7567e 100644 --- a/test/src/models/user_preferences/saved_headline_filter_test.dart +++ b/test/src/models/user_preferences/saved_headline_filter_test.dart @@ -3,7 +3,7 @@ import 'package:test/test.dart'; void main() { group('SavedHeadlineFilter', () { - final fullModel = savedHeadlineFiltersFixturesData[0]; + final fullModel = getSavedHeadlineFiltersFixturesData()[0]; final fullJson = fullModel.toJson(); test('should be instantiable', () { @@ -58,7 +58,7 @@ void main() { }); test('copyWith with no arguments should return an identical instance', () { - final copied = savedHeadlineFiltersFixturesData[0].copyWith(); + final copied = getSavedHeadlineFiltersFixturesData()[0].copyWith(); expect(copied, equals(fullModel)); }); diff --git a/test/src/models/user_preferences/saved_source_filter_test.dart b/test/src/models/user_preferences/saved_source_filter_test.dart index 1f472db5..3e31dc1e 100644 --- a/test/src/models/user_preferences/saved_source_filter_test.dart +++ b/test/src/models/user_preferences/saved_source_filter_test.dart @@ -3,7 +3,7 @@ import 'package:test/test.dart'; void main() { group('SavedSourceFilter', () { - final fullModel = savedSourceFiltersFixturesData[0]; + final fullModel = getSavedSourceFiltersFixturesData()[0]; final fullJson = fullModel.toJson(); test('should be instantiable', () { @@ -53,7 +53,7 @@ void main() { }); test('copyWith with no arguments should return an identical instance', () { - final copied = savedSourceFiltersFixturesData[0].copyWith(); + final copied = getSavedSourceFiltersFixturesData()[0].copyWith(); expect(copied, equals(fullModel)); }); diff --git a/test/src/models/user_preferences/source_filter_criteria_test.dart b/test/src/models/user_preferences/source_filter_criteria_test.dart index 05677f21..f376e2b1 100644 --- a/test/src/models/user_preferences/source_filter_criteria_test.dart +++ b/test/src/models/user_preferences/source_filter_criteria_test.dart @@ -3,7 +3,7 @@ import 'package:test/test.dart'; void main() { group('SourceFilterCriteria', () { - final fullModel = savedSourceFiltersFixturesData[0].criteria; + final fullModel = getSavedSourceFiltersFixturesData()[0].criteria; final fullJson = fullModel.toJson(); const emptyModel = SourceFilterCriteria( diff --git a/test/src/models/user_preferences/user_content_preferences_test.dart b/test/src/models/user_preferences/user_content_preferences_test.dart index a740a6dc..511d2ee1 100644 --- a/test/src/models/user_preferences/user_content_preferences_test.dart +++ b/test/src/models/user_preferences/user_content_preferences_test.dart @@ -6,7 +6,7 @@ void main() { // Use the first item from the fixtures as the test subject. // This ensures tests are based on the canonical fixture data. final userContentPreferencesFixture = - userContentPreferencesFixturesData.first; + getUserContentPreferencesFixturesData().first; group('constructor', () { test('returns correct instance', () { @@ -15,7 +15,7 @@ void main() { test('returns correct instance with populated lists from fixture', () { // The base fixture should now have populated lists - final preferences = userContentPreferencesFixturesData.first; + final preferences = getUserContentPreferencesFixturesData().first; expect(preferences.followedCountries, isEmpty); expect(preferences.followedSources, isNotEmpty); expect(preferences.followedTopics, isNotEmpty); @@ -33,7 +33,7 @@ void main() { }); test('round trip with empty lists', () { - final emptyPreferences = userContentPreferencesFixturesData.first; + final emptyPreferences = getUserContentPreferencesFixturesData().first; final json = emptyPreferences.toJson(); final result = UserContentPreferences.fromJson(json); expect(result, equals(emptyPreferences));