From 4b987f6a9d368b5fabb0e6e1ac0c51413abb918c Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:17:52 +0100 Subject: [PATCH 001/130] feat(community): register new data repositories Instantiates and registers `DataRepository` instances for `Engagement`, `Report`, and `AppReview`. This makes the data sources for user-generated content available to the rest of the application via dependency injection. --- lib/bootstrap.dart | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 962ca7d4..f2b1507a 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -65,6 +65,9 @@ Future bootstrap( DataClient countriesClient; DataClient languagesClient; DataClient usersClient; + DataClient engagementsClient; + DataClient reportsClient; + DataClient appReviewsClient; if (appConfig.environment == app_config.AppEnvironment.demo) { headlinesClient = DataInMemory( @@ -128,6 +131,24 @@ Future bootstrap( initialData: usersFixturesData, logger: Logger('DataInMemory'), ); + engagementsClient = DataInMemory( + toJson: (i) => i.toJson(), + getId: (i) => i.id, + initialData: getEngagementsFixturesData(), + logger: Logger('DataInMemory'), + ); + reportsClient = DataInMemory( + toJson: (i) => i.toJson(), + getId: (i) => i.id, + initialData: getReportsFixturesData(), + logger: Logger('DataInMemory'), + ); + appReviewsClient = DataInMemory( + toJson: (i) => i.toJson(), + getId: (i) => i.id, + initialData: getAppReviewsFixturesData(), + logger: Logger('DataInMemory'), + ); } else { headlinesClient = DataApi( httpClient: httpClient!, @@ -200,6 +221,27 @@ Future bootstrap( toJson: (user) => user.toJson(), logger: Logger('DataApi'), ); + engagementsClient = DataApi( + httpClient: httpClient, + modelName: 'engagement', + fromJson: Engagement.fromJson, + toJson: (engagement) => engagement.toJson(), + logger: Logger('DataApi'), + ); + reportsClient = DataApi( + httpClient: httpClient, + modelName: 'report', + fromJson: Report.fromJson, + toJson: (report) => report.toJson(), + logger: Logger('DataApi'), + ); + appReviewsClient = DataApi( + httpClient: httpClient, + modelName: 'app_review', + fromJson: AppReview.fromJson, + toJson: (appReview) => appReview.toJson(), + logger: Logger('DataApi'), + ); } pendingDeletionsService = PendingDeletionsServiceImpl( @@ -231,6 +273,15 @@ Future bootstrap( dataClient: languagesClient, ); final usersRepository = DataRepository(dataClient: usersClient); + final engagementsRepository = DataRepository( + dataClient: engagementsClient, + ); + final reportsRepository = DataRepository( + dataClient: reportsClient, + ); + final appReviewsRepository = DataRepository( + dataClient: appReviewsClient, + ); return App( authenticationRepository: authenticationRepository, @@ -244,6 +295,9 @@ Future bootstrap( countriesRepository: countriesRepository, languagesRepository: languagesRepository, usersRepository: usersRepository, + engagementsRepository: engagementsRepository, + reportsRepository: reportsRepository, + appReviewsRepository: appReviewsRepository, storageService: kvStorage, environment: environment, pendingDeletionsService: pendingDeletionsService, From 99c2f06989b9e27e71b30e091cd07f3b3679b5cb Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:28:01 +0100 Subject: [PATCH 002/130] feat(l10n): add community management translations - Add Arabic (app_ar.arb) and English (app_en.arb) translations for community management features - Include translations for navigation items, column headers, actions, statuses, and messages related to engagements, reports, and app reviews - Update descriptions for new and existing localization keys --- lib/l10n/arb/app_ar.arb | 283 +++++++++++++++++++++++++++++++++++++++ lib/l10n/arb/app_en.arb | 285 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 567 insertions(+), 1 deletion(-) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index c9059458..e203739f 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2249,5 +2249,288 @@ "enableCommunityFeaturesDescription": "ينشط أو يعطل عالميًا جميع الوظائف المتعلقة بالمجتمع، بما في ذلك المشاركة والإبلاغ.", "@enableCommunityFeaturesDescription": { "description": "وصف المفتاح الرئيسي لتفعيل جميع ميزات المجتمع." + }, + "communityManagement": "المجتمع", + "@communityManagement": { + "description": "تسمية عنصر التنقل لإدارة المجتمع" + }, + "communityManagementPageDescription": "إدارة المحتوى الذي ينشئه المستخدمون بما في ذلك التفاعلات (ردود الفعل والتعليقات) وتبليغات المحتوى ومراجعات التطبيق.", + "@communityManagementPageDescription": { + "description": "وصف صفحة إدارة المجتمع" + }, + "engagements": "التفاعلات", + "@engagements": { + "description": "تسمية الصفحة الفرعية للتفاعلات" + }, + "reports": "التبليغات", + "@reports": { + "description": "تسمية الصفحة الفرعية لالتبليغات" + }, + "appReviews": "مراجعات التطبيق", + "@appReviews": { + "description": "تسمية الصفحة الفرعية لمراجعات التطبيق" + }, + "user": "المستخدم", + "@user": { + "description": "رأس العمود للمستخدم" + }, + "engagedContent": "المحتوى المتفاعل معه", + "@engagedContent": { + "description": "رأس العمود للمحتوى المتفاعل معه" + }, + "reaction": "رد الفعل", + "@reaction": { + "description": "رأس العمود لرد الفعل" + }, + "comment": "التعليق", + "@comment": { + "description": "رأس العمود للتعليق" + }, + "commentStatus": "حالة التعليق", + "@commentStatus": { + "description": "رأس العمود لحالة التعليق" + }, + "date": "التاريخ", + "@date": { + "description": "رأس العمود للتاريخ" + }, + "approveComment": "الموافقة على التعليق", + "@approveComment": { + "description": "إجراء للموافقة على تعليق" + }, + "rejectComment": "رفض التعليق", + "@rejectComment": { + "description": "إجراء لرفض تعليق" + }, + "viewEngagedContent": "عرض المحتوى", + "@viewEngagedContent": { + "description": "إجراء لعرض المحتوى المتفاعل معه" + }, + "copyUserId": "نسخ معرف المستخدم", + "@copyUserId": { + "description": "إجراء لنسخ معرف المستخدم" + }, + "reporter": "المبلغ", + "@reporter": { + "description": "رأس العمود للمبلغ" + }, + "reportedItem": "العنصر المبلغ عنه", + "@reportedItem": { + "description": "رأس العمود للعنصر المبلغ عنه" + }, + "reason": "السبب", + "@reason": { + "description": "رأس العمود للسبب" + }, + "reportStatus": "حالة البلاغ", + "@reportStatus": { + "description": "رأس العمود لحالة البلاغ" + }, + "viewReportedItem": "عرض العنصر", + "@viewReportedItem": { + "description": "إجراء لعرض العنصر المبلغ عنه" + }, + "markAsInReview": "وضع علامة 'قيد المراجعة'", + "@markAsInReview": { + "description": "إجراء لوضع علامة على بلاغ بأنه قيد المراجعة" + }, + "resolveReport": "حل البلاغ", + "@resolveReport": { + "description": "إجراء لحل بلاغ" + }, + "initialFeedback": "التقييم الأولي", + "@initialFeedback": { + "description": "رأس العمود للتقييم الأولي" + }, + "osPromptRequested": "طُلب تقييم النظام؟", + "@osPromptRequested": { + "description": "رأس العمود لطلب تقييم النظام" + }, + "feedbackHistory": "سجل التقييمات", + "@feedbackHistory": { + "description": "رأس العمود لسجل التقييمات" + }, + "lastInteraction": "آخر تفاعل", + "@lastInteraction": { + "description": "رأس العمود لآخر تفاعل" + }, + "viewFeedbackHistory": "عرض السجل", + "@viewFeedbackHistory": { + "description": "إجراء لعرض سجل التقييمات" + }, + "reactionTypeLike": "إعجاب", + "@reactionTypeLike": { + "description": "نوع رد الفعل: إعجاب" + }, + "reactionTypeInsightful": "ثاقب", + "@reactionTypeInsightful": { + "description": "نوع رد الفعل: ثاقب" + }, + "reactionTypeAmusing": "مسلي", + "@reactionTypeAmusing": { + "description": "نوع رد الفعل: مسلي" + }, + "reactionTypeSad": "حزين", + "@reactionTypeSad": { + "description": "نوع رد الفعل: حزين" + }, + "reactionTypeAngry": "غاضب", + "@reactionTypeAngry": { + "description": "نوع رد الفعل: غاضب" + }, + "reactionTypeSkeptical": "متشكك", + "@reactionTypeSkeptical": { + "description": "نوع رد الفعل: متشكك" + }, + "commentStatusPendingReview": "قيد المراجعة", + "@commentStatusPendingReview": { + "description": "حالة التعليق: قيد المراجعة" + }, + "commentStatusApproved": "موافق عليه", + "@commentStatusApproved": { + "description": "حالة التعليق: موافق عليه" + }, + "commentStatusRejected": "مرفوض", + "@commentStatusRejected": { + "description": "حالة التعليق: مرفوض" + }, + "reportStatusSubmitted": "مقدم", + "@reportStatusSubmitted": { + "description": "حالة البلاغ: مقدم" + }, + "reportStatusInReview": "قيد المراجعة", + "@reportStatusInReview": { + "description": "حالة البلاغ: قيد المراجعة" + }, + "reportStatusResolved": "تم الحل", + "@reportStatusResolved": { + "description": "حالة البلاغ: تم الحل" + }, + "initialAppReviewFeedbackPositive": "إيجابي", + "@initialAppReviewFeedbackPositive": { + "description": "التقييم الأولي للتطبيق: إيجابي" + }, + "initialAppReviewFeedbackNegative": "سلبي", + "@initialAppReviewFeedbackNegative": { + "description": "التقييم الأولي للتطبيق: سلبي" + }, + "filterCommunity": "تصفية محتوى المجتمع", + "@filterCommunity": { + "description": "إجراء لتصفية محتوى المجتمع" + }, + "searchByEngagementUser": "البحث بالبريد الإلكتروني للمستخدم...", + "@searchByEngagementUser": { + "description": "تلميح للبحث عن طريق البريد الإلكتروني للمستخدم" + }, + "searchByReportReporter": "البحث بالبريد الإلكتروني للمبلغ...", + "@searchByReportReporter": { + "description": "تلميح للبحث عن طريق البريد الإلكتروني للمبلغ" + }, + "searchByAppReviewUser": "البحث بالبريد الإلكتروني للمستخدم...", + "@searchByAppReviewUser": { + "description": "تلميح للبحث عن طريق البريد الإلكتروني لمستخدم مراجعة التطبيق" + }, + "selectCommentStatus": "اختر حالة التعليق", + "@selectCommentStatus": { + "description": "إجراء لاختيار حالة التعليق" + }, + "selectReportStatus": "اختر حالة البلاغ", + "@selectReportStatus": { + "description": "إجراء لاختيار حالة البلاغ" + }, + "selectInitialFeedback": "اختر التقييم الأولي", + "@selectInitialFeedback": { + "description": "إجراء لاختيار التقييم الأولي" + }, + "selectReportableEntity": "اختر نوع العنصر المبلغ عنه", + "@selectReportableEntity": { + "description": "إجراء لاختيار نوع العنصر المبلغ عنه" + }, + "reportableEntityHeadline": "عنوان", + "@reportableEntityHeadline": { + "description": "العنصر القابل للإبلاغ عنه: عنوان" + }, + "reportableEntitySource": "مصدر", + "@reportableEntitySource": { + "description": "العنصر القابل للإبلاغ عنه: مصدر" + }, + "reportableEntityComment": "تعليق", + "@reportableEntityComment": { + "description": "العنصر القابل للإبلاغ عنه: تعليق" + }, + "noEngagementsFound": "لم يتم العثور على تفاعلات.", + "@noEngagementsFound": { + "description": "رسالة عند عدم العثور على تفاعلات" + }, + "noReportsFound": "لم يتم العثور على بلاغات.", + "@noReportsFound": { + "description": "رسالة عند عدم العثور على بلاغات" + }, + "noAppReviewsFound": "لم يتم العثور على مراجعات للتطبيق.", + "@noAppReviewsFound": { + "description": "رسالة عند عدم العثور على مراجعات للتطبيق" + }, + "loadingEngagements": "جاري تحميل التفاعلات", + "@loadingEngagements": { + "description": "رسالة عند تحميل التفاعلات" + }, + "loadingReports": "جاري تحميل البلاغات", + "@loadingReports": { + "description": "رسالة عند تحميل البلاغات" + }, + "loadingAppReviews": "جاري تحميل مراجعات التطبيق", + "@loadingAppReviews": { + "description": "رسالة عند تحميل مراجعات التطبيق" + }, + "userIdCopied": "تم نسخ معرف المستخدم إلى الحافظة.", + "@userIdCopied": { + "description": "رسالة عند نسخ معرف المستخدم" + }, + "commentApproved": "تمت الموافقة على التعليق.", + "@commentApproved": { + "description": "رسالة عند الموافقة على تعليق" + }, + "commentRejected": "تم رفض التعليق.", + "@commentRejected": { + "description": "رسالة عند رفض تعليق" + }, + "reportStatusUpdated": "تم تحديث حالة البلاغ.", + "@reportStatusUpdated": { + "description": "رسالة عند تحديث حالة البلاغ" + }, + "feedbackHistoryForUser": "سجل التقييمات للمستخدم {email}", + "@feedbackHistoryForUser": { + "description": "رسالة تعرض سجل التقييمات لمستخدم", + "placeholders": { + "email": { + "type": "String" + } + } + }, + "noFeedbackHistory": "لا يوجد سجل تقييمات متاح لهذا المستخدم.", + "@noFeedbackHistory": { + "description": "رسالة عند عدم توفر سجل تقييمات" + }, + "feedbackProvidedAt": "تم تقديم التقييم في: {date}", + "@feedbackProvidedAt": { + "description": "رسالة تعرض تاريخ تقديم التقييم", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "feedbackReason": "السبب: {reason}", + "@feedbackReason": { + "description": "رسالة تعرض سبب التقييم", + "placeholders": { + "reason": { + "type": "String" + } + } + }, + "noReasonProvided": "لم يتم تقديم سبب.", + "@noReasonProvided": { + "description": "رسالة عند عدم تقديم سبب للتقييم" } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 006df5d9..2018865f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2245,5 +2245,288 @@ "enableCommunityFeaturesDescription": "Globally activates or deactivates all community-related functionality, including engagement and reporting.", "@enableCommunityFeaturesDescription": { "description": "Description for the master switch to enable all community features." - } + }, + "communityManagement": "Community", + "@communityManagement": { + "description": "Label for the community management navigation item" + }, + "communityManagementPageDescription": "Manage user-generated content including engagements (reactions and comments), content reports, and app reviews.", + "@communityManagementPageDescription": { + "description": "Description for the Community Management page" + }, + "engagements": "Engagements", + "@engagements": { + "description": "Label for the engagements subpage" + }, + "reports": "Reports", + "@reports": { + "description": "Label for the reports subpage" + }, + "appReviews": "App Reviews", + "@appReviews": { + "description": "Label for the app reviews subpage" + }, + "user": "User", + "@user": { + "description": "Column header for user" + }, + "engagedContent": "Engaged Content", + "@engagedContent": { + "description": "Column header for engaged content" + }, + "reaction": "Reaction", + "@reaction": { + "description": "Column header for reaction" + }, + "comment": "Comment", + "@comment": { + "description": "Column header for comment" + }, + "commentStatus": "Comment Status", + "@commentStatus": { + "description": "Column header for comment status" + }, + "date": "Date", + "@date": { + "description": "Column header for date" + }, + "approveComment": "Approve Comment", + "@approveComment": { + "description": "Action to approve a comment" + }, + "rejectComment": "Reject Comment", + "@rejectComment": { + "description": "Action to reject a comment" + }, + "viewEngagedContent": "View Content", + "@viewEngagedContent": { + "description": "Action to view the engaged content" + }, + "copyUserId": "Copy User ID", + "@copyUserId": { + "description": "Action to copy the user ID" + }, + "reporter": "Reporter", + "@reporter": { + "description": "Column header for reporter" + }, + "reportedItem": "Reported Item", + "@reportedItem": { + "description": "Column header for reported item" + }, + "reason": "Reason", + "@reason": { + "description": "Column header for reason" + }, + "reportStatus": "Report Status", + "@reportStatus": { + "description": "Column header for report status" + }, + "viewReportedItem": "View Item", + "@viewReportedItem": { + "description": "Action to view the reported item" + }, + "markAsInReview": "Mark as In Review", + "@markAsInReview": { + "description": "Action to mark a report as in review" + }, + "resolveReport": "Resolve Report", + "@resolveReport": { + "description": "Action to resolve a report" + }, + "initialFeedback": "Initial Feedback", + "@initialFeedback": { + "description": "Column header for initial feedback" + }, + "osPromptRequested": "OS Prompt?", + "@osPromptRequested": { + "description": "Column header for OS prompt requested" + }, + "feedbackHistory": "Feedback History", + "@feedbackHistory": { + "description": "Column header for feedback history" + }, + "lastInteraction": "Last Interaction", + "@lastInteraction": { + "description": "Column header for last interaction" + }, + "viewFeedbackHistory": "View History", + "@viewFeedbackHistory": { + "description": "Action to view feedback history" + }, + "reactionTypeLike": "Like", + "@reactionTypeLike": { + "description": "Reaction type: Like" + }, + "reactionTypeInsightful": "Insightful", + "@reactionTypeInsightful": { + "description": "Reaction type: Insightful" + }, + "reactionTypeAmusing": "Amusing", + "@reactionTypeAmusing": { + "description": "Reaction type: Amusing" + }, + "reactionTypeSad": "Sad", + "@reactionTypeSad": { + "description": "Reaction type: Sad" + }, + "reactionTypeAngry": "Angry", + "@reactionTypeAngry": { + "description": "Reaction type: Angry" + }, + "reactionTypeSkeptical": "Skeptical", + "@reactionTypeSkeptical": { + "description": "Reaction type: Skeptical" + }, + "commentStatusPendingReview": "Pending", + "@commentStatusPendingReview": { + "description": "Comment status: Pending Review" + }, + "commentStatusApproved": "Approved", + "@commentStatusApproved": { + "description": "Comment status: Approved" + }, + "commentStatusRejected": "Rejected", + "@commentStatusRejected": { + "description": "Comment status: Rejected" + }, + "reportStatusSubmitted": "Submitted", + "@reportStatusSubmitted": { + "description": "Report status: Submitted" + }, + "reportStatusInReview": "In Review", + "@reportStatusInReview": { + "description": "Report status: In Review" + }, + "reportStatusResolved": "Resolved", + "@reportStatusResolved": { + "description": "Report status: Resolved" + }, + "initialAppReviewFeedbackPositive": "Positive", + "@initialAppReviewFeedbackPositive": { + "description": "Initial app review feedback: Positive" + }, + "initialAppReviewFeedbackNegative": "Negative", + "@initialAppReviewFeedbackNegative": { + "description": "Initial app review feedback: Negative" + }, + "filterCommunity": "Filter Community Content", + "@filterCommunity": { + "description": "Action to filter community content" + }, + "searchByEngagementUser": "Search by user email...", + "@searchByEngagementUser": { + "description": "Hint text for searching by engagement user" + }, + "searchByReportReporter": "Search by reporter email...", + "@searchByReportReporter": { + "description": "Hint text for searching by report reporter" + }, + "searchByAppReviewUser": "Search by user email...", + "@searchByAppReviewUser": { + "description": "Hint text for searching by app review user" + }, + "selectCommentStatus": "Select Comment Status", + "@selectCommentStatus": { + "description": "Action to select comment status" + }, + "selectReportStatus": "Select Report Status", + "@selectReportStatus": { + "description": "Action to select report status" + }, + "selectInitialFeedback": "Select Initial Feedback", + "@selectInitialFeedback": { + "description": "Action to select initial feedback" + }, + "selectReportableEntity": "Select Reported Item Type", + "@selectReportableEntity": { + "description": "Action to select reportable entity" + }, + "reportableEntityHeadline": "Headline", + "@reportableEntityHeadline": { + "description": "Reportable entity: Headline" + }, + "reportableEntitySource": "Source", + "@reportableEntitySource": { + "description": "Reportable entity: Source" + }, + "reportableEntityComment": "Comment", + "@reportableEntityComment": { + "description": "Reportable entity: Comment" + }, + "noEngagementsFound": "No engagements found.", + "@noEngagementsFound": { + "description": "Message when no engagements are found" + }, + "noReportsFound": "No reports found.", + "@noReportsFound": { + "description": "Message when no reports are found" + }, + "noAppReviewsFound": "No app reviews found.", + "@noAppReviewsFound": { + "description": "Message when no app reviews are found" + }, + "loadingEngagements": "Loading Engagements", + "@loadingEngagements": { + "description": "Message when engagements are loading" + }, + "loadingReports": "Loading Reports", + "@loadingReports": { + "description": "Message when reports are loading" + }, + "loadingAppReviews": "Loading App Reviews", + "@loadingAppReviews": { + "description": "Message when app reviews are loading" + }, + "userIdCopied": "User ID copied to clipboard.", + "@userIdCopied": { + "description": "Message when user ID is copied" + }, + "commentApproved": "Comment approved.", + "@commentApproved": { + "description": "Message when a comment is approved" + }, + "commentRejected": "Comment rejected.", + "@commentRejected": { + "description": "Message when a comment is rejected" + }, + "reportStatusUpdated": "Report status updated.", + "@reportStatusUpdated": { + "description": "Message when a report status is updated" + }, + "feedbackHistoryForUser": "Feedback History for {email}", + "@feedbackHistoryForUser": { + "description": "Message displaying feedback history for a user", + "placeholders": { + "email": { + "type": "String" + } + } + }, + "noFeedbackHistory": "No feedback history available for this user.", + "@noFeedbackHistory": { + "description": "Message when no feedback history is available" + }, + "feedbackProvidedAt": "Feedback provided at: {date}", + "@feedbackProvidedAt": { + "description": "Message displaying the date feedback was provided", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "feedbackReason": "Reason: {reason}", + "@feedbackReason": { + "description": "Message displaying the reason for feedback", + "placeholders": { + "reason": { + "type": "String" + } + } + }, + "noReasonProvided": "No reason provided.", + "@noReasonProvided": { + "description": "Message when no reason for feedback is provided" + } } \ No newline at end of file From 6d53c99b48e0c202ee6a10dfc3f361aaf70ff9b8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:28:25 +0100 Subject: [PATCH 003/130] build(l10n): sync --- lib/l10n/app_localizations.dart | 402 +++++++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 208 +++++++++++++++ lib/l10n/app_localizations_en.dart | 209 +++++++++++++++ 3 files changed, 819 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 4f9e618e..7c44f107 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3343,6 +3343,408 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Globally activates or deactivates all community-related functionality, including engagement and reporting.'** String get enableCommunityFeaturesDescription; + + /// Label for the community management navigation item + /// + /// In en, this message translates to: + /// **'Community'** + String get communityManagement; + + /// Description for the Community Management page + /// + /// In en, this message translates to: + /// **'Manage user-generated content including engagements (reactions and comments), content reports, and app reviews.'** + String get communityManagementPageDescription; + + /// Label for the engagements subpage + /// + /// In en, this message translates to: + /// **'Engagements'** + String get engagements; + + /// Label for the reports subpage + /// + /// In en, this message translates to: + /// **'Reports'** + String get reports; + + /// Label for the app reviews subpage + /// + /// In en, this message translates to: + /// **'App Reviews'** + String get appReviews; + + /// Column header for user + /// + /// In en, this message translates to: + /// **'User'** + String get user; + + /// Column header for engaged content + /// + /// In en, this message translates to: + /// **'Engaged Content'** + String get engagedContent; + + /// Column header for reaction + /// + /// In en, this message translates to: + /// **'Reaction'** + String get reaction; + + /// Column header for comment + /// + /// In en, this message translates to: + /// **'Comment'** + String get comment; + + /// Column header for comment status + /// + /// In en, this message translates to: + /// **'Comment Status'** + String get commentStatus; + + /// Column header for date + /// + /// In en, this message translates to: + /// **'Date'** + String get date; + + /// Action to approve a comment + /// + /// In en, this message translates to: + /// **'Approve Comment'** + String get approveComment; + + /// Action to reject a comment + /// + /// In en, this message translates to: + /// **'Reject Comment'** + String get rejectComment; + + /// Action to view the engaged content + /// + /// In en, this message translates to: + /// **'View Content'** + String get viewEngagedContent; + + /// Action to copy the user ID + /// + /// In en, this message translates to: + /// **'Copy User ID'** + String get copyUserId; + + /// Column header for reporter + /// + /// In en, this message translates to: + /// **'Reporter'** + String get reporter; + + /// Column header for reported item + /// + /// In en, this message translates to: + /// **'Reported Item'** + String get reportedItem; + + /// Column header for reason + /// + /// In en, this message translates to: + /// **'Reason'** + String get reason; + + /// Column header for report status + /// + /// In en, this message translates to: + /// **'Report Status'** + String get reportStatus; + + /// Action to view the reported item + /// + /// In en, this message translates to: + /// **'View Item'** + String get viewReportedItem; + + /// Action to mark a report as in review + /// + /// In en, this message translates to: + /// **'Mark as In Review'** + String get markAsInReview; + + /// Action to resolve a report + /// + /// In en, this message translates to: + /// **'Resolve Report'** + String get resolveReport; + + /// Column header for initial feedback + /// + /// In en, this message translates to: + /// **'Initial Feedback'** + String get initialFeedback; + + /// Column header for OS prompt requested + /// + /// In en, this message translates to: + /// **'OS Prompt?'** + String get osPromptRequested; + + /// Column header for feedback history + /// + /// In en, this message translates to: + /// **'Feedback History'** + String get feedbackHistory; + + /// Column header for last interaction + /// + /// In en, this message translates to: + /// **'Last Interaction'** + String get lastInteraction; + + /// Action to view feedback history + /// + /// In en, this message translates to: + /// **'View History'** + String get viewFeedbackHistory; + + /// Reaction type: Like + /// + /// In en, this message translates to: + /// **'Like'** + String get reactionTypeLike; + + /// Reaction type: Insightful + /// + /// In en, this message translates to: + /// **'Insightful'** + String get reactionTypeInsightful; + + /// Reaction type: Amusing + /// + /// In en, this message translates to: + /// **'Amusing'** + String get reactionTypeAmusing; + + /// Reaction type: Sad + /// + /// In en, this message translates to: + /// **'Sad'** + String get reactionTypeSad; + + /// Reaction type: Angry + /// + /// In en, this message translates to: + /// **'Angry'** + String get reactionTypeAngry; + + /// Reaction type: Skeptical + /// + /// In en, this message translates to: + /// **'Skeptical'** + String get reactionTypeSkeptical; + + /// Comment status: Pending Review + /// + /// In en, this message translates to: + /// **'Pending'** + String get commentStatusPendingReview; + + /// Comment status: Approved + /// + /// In en, this message translates to: + /// **'Approved'** + String get commentStatusApproved; + + /// Comment status: Rejected + /// + /// In en, this message translates to: + /// **'Rejected'** + String get commentStatusRejected; + + /// Report status: Submitted + /// + /// In en, this message translates to: + /// **'Submitted'** + String get reportStatusSubmitted; + + /// Report status: In Review + /// + /// In en, this message translates to: + /// **'In Review'** + String get reportStatusInReview; + + /// Report status: Resolved + /// + /// In en, this message translates to: + /// **'Resolved'** + String get reportStatusResolved; + + /// Initial app review feedback: Positive + /// + /// In en, this message translates to: + /// **'Positive'** + String get initialAppReviewFeedbackPositive; + + /// Initial app review feedback: Negative + /// + /// In en, this message translates to: + /// **'Negative'** + String get initialAppReviewFeedbackNegative; + + /// Action to filter community content + /// + /// In en, this message translates to: + /// **'Filter Community Content'** + String get filterCommunity; + + /// Hint text for searching by engagement user + /// + /// In en, this message translates to: + /// **'Search by user email...'** + String get searchByEngagementUser; + + /// Hint text for searching by report reporter + /// + /// In en, this message translates to: + /// **'Search by reporter email...'** + String get searchByReportReporter; + + /// Hint text for searching by app review user + /// + /// In en, this message translates to: + /// **'Search by user email...'** + String get searchByAppReviewUser; + + /// Action to select comment status + /// + /// In en, this message translates to: + /// **'Select Comment Status'** + String get selectCommentStatus; + + /// Action to select report status + /// + /// In en, this message translates to: + /// **'Select Report Status'** + String get selectReportStatus; + + /// Action to select initial feedback + /// + /// In en, this message translates to: + /// **'Select Initial Feedback'** + String get selectInitialFeedback; + + /// Action to select reportable entity + /// + /// In en, this message translates to: + /// **'Select Reported Item Type'** + String get selectReportableEntity; + + /// Reportable entity: Headline + /// + /// In en, this message translates to: + /// **'Headline'** + String get reportableEntityHeadline; + + /// Reportable entity: Source + /// + /// In en, this message translates to: + /// **'Source'** + String get reportableEntitySource; + + /// Reportable entity: Comment + /// + /// In en, this message translates to: + /// **'Comment'** + String get reportableEntityComment; + + /// Message when no engagements are found + /// + /// In en, this message translates to: + /// **'No engagements found.'** + String get noEngagementsFound; + + /// Message when no reports are found + /// + /// In en, this message translates to: + /// **'No reports found.'** + String get noReportsFound; + + /// Message when no app reviews are found + /// + /// In en, this message translates to: + /// **'No app reviews found.'** + String get noAppReviewsFound; + + /// Message when engagements are loading + /// + /// In en, this message translates to: + /// **'Loading Engagements'** + String get loadingEngagements; + + /// Message when reports are loading + /// + /// In en, this message translates to: + /// **'Loading Reports'** + String get loadingReports; + + /// Message when app reviews are loading + /// + /// In en, this message translates to: + /// **'Loading App Reviews'** + String get loadingAppReviews; + + /// Message when user ID is copied + /// + /// In en, this message translates to: + /// **'User ID copied to clipboard.'** + String get userIdCopied; + + /// Message when a comment is approved + /// + /// In en, this message translates to: + /// **'Comment approved.'** + String get commentApproved; + + /// Message when a comment is rejected + /// + /// In en, this message translates to: + /// **'Comment rejected.'** + String get commentRejected; + + /// Message when a report status is updated + /// + /// In en, this message translates to: + /// **'Report status updated.'** + String get reportStatusUpdated; + + /// Message displaying feedback history for a user + /// + /// In en, this message translates to: + /// **'Feedback History for {email}'** + String feedbackHistoryForUser(String email); + + /// Message when no feedback history is available + /// + /// In en, this message translates to: + /// **'No feedback history available for this user.'** + String get noFeedbackHistory; + + /// Message displaying the date feedback was provided + /// + /// In en, this message translates to: + /// **'Feedback provided at: {date}'** + String feedbackProvidedAt(String date); + + /// Message displaying the reason for feedback + /// + /// In en, this message translates to: + /// **'Reason: {reason}'** + String feedbackReason(String reason); + + /// Message when no reason for feedback is provided + /// + /// In en, this message translates to: + /// **'No reason provided.'** + String get noReasonProvided; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 345786b8..688d49d9 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1802,4 +1802,212 @@ class AppLocalizationsAr extends AppLocalizations { @override String get enableCommunityFeaturesDescription => 'ينشط أو يعطل عالميًا جميع الوظائف المتعلقة بالمجتمع، بما في ذلك المشاركة والإبلاغ.'; + + @override + String get communityManagement => 'المجتمع'; + + @override + String get communityManagementPageDescription => + 'إدارة المحتوى الذي ينشئه المستخدمون بما في ذلك التفاعلات (ردود الفعل والتعليقات) وتبليغات المحتوى ومراجعات التطبيق.'; + + @override + String get engagements => 'التفاعلات'; + + @override + String get reports => 'التبليغات'; + + @override + String get appReviews => 'مراجعات التطبيق'; + + @override + String get user => 'المستخدم'; + + @override + String get engagedContent => 'المحتوى المتفاعل معه'; + + @override + String get reaction => 'رد الفعل'; + + @override + String get comment => 'التعليق'; + + @override + String get commentStatus => 'حالة التعليق'; + + @override + String get date => 'التاريخ'; + + @override + String get approveComment => 'الموافقة على التعليق'; + + @override + String get rejectComment => 'رفض التعليق'; + + @override + String get viewEngagedContent => 'عرض المحتوى'; + + @override + String get copyUserId => 'نسخ معرف المستخدم'; + + @override + String get reporter => 'المبلغ'; + + @override + String get reportedItem => 'العنصر المبلغ عنه'; + + @override + String get reason => 'السبب'; + + @override + String get reportStatus => 'حالة البلاغ'; + + @override + String get viewReportedItem => 'عرض العنصر'; + + @override + String get markAsInReview => 'وضع علامة \'قيد المراجعة\''; + + @override + String get resolveReport => 'حل البلاغ'; + + @override + String get initialFeedback => 'التقييم الأولي'; + + @override + String get osPromptRequested => 'طُلب تقييم النظام؟'; + + @override + String get feedbackHistory => 'سجل التقييمات'; + + @override + String get lastInteraction => 'آخر تفاعل'; + + @override + String get viewFeedbackHistory => 'عرض السجل'; + + @override + String get reactionTypeLike => 'إعجاب'; + + @override + String get reactionTypeInsightful => 'ثاقب'; + + @override + String get reactionTypeAmusing => 'مسلي'; + + @override + String get reactionTypeSad => 'حزين'; + + @override + String get reactionTypeAngry => 'غاضب'; + + @override + String get reactionTypeSkeptical => 'متشكك'; + + @override + String get commentStatusPendingReview => 'قيد المراجعة'; + + @override + String get commentStatusApproved => 'موافق عليه'; + + @override + String get commentStatusRejected => 'مرفوض'; + + @override + String get reportStatusSubmitted => 'مقدم'; + + @override + String get reportStatusInReview => 'قيد المراجعة'; + + @override + String get reportStatusResolved => 'تم الحل'; + + @override + String get initialAppReviewFeedbackPositive => 'إيجابي'; + + @override + String get initialAppReviewFeedbackNegative => 'سلبي'; + + @override + String get filterCommunity => 'تصفية محتوى المجتمع'; + + @override + String get searchByEngagementUser => 'البحث بالبريد الإلكتروني للمستخدم...'; + + @override + String get searchByReportReporter => 'البحث بالبريد الإلكتروني للمبلغ...'; + + @override + String get searchByAppReviewUser => 'البحث بالبريد الإلكتروني للمستخدم...'; + + @override + String get selectCommentStatus => 'اختر حالة التعليق'; + + @override + String get selectReportStatus => 'اختر حالة البلاغ'; + + @override + String get selectInitialFeedback => 'اختر التقييم الأولي'; + + @override + String get selectReportableEntity => 'اختر نوع العنصر المبلغ عنه'; + + @override + String get reportableEntityHeadline => 'عنوان'; + + @override + String get reportableEntitySource => 'مصدر'; + + @override + String get reportableEntityComment => 'تعليق'; + + @override + String get noEngagementsFound => 'لم يتم العثور على تفاعلات.'; + + @override + String get noReportsFound => 'لم يتم العثور على بلاغات.'; + + @override + String get noAppReviewsFound => 'لم يتم العثور على مراجعات للتطبيق.'; + + @override + String get loadingEngagements => 'جاري تحميل التفاعلات'; + + @override + String get loadingReports => 'جاري تحميل البلاغات'; + + @override + String get loadingAppReviews => 'جاري تحميل مراجعات التطبيق'; + + @override + String get userIdCopied => 'تم نسخ معرف المستخدم إلى الحافظة.'; + + @override + String get commentApproved => 'تمت الموافقة على التعليق.'; + + @override + String get commentRejected => 'تم رفض التعليق.'; + + @override + String get reportStatusUpdated => 'تم تحديث حالة البلاغ.'; + + @override + String feedbackHistoryForUser(String email) { + return 'سجل التقييمات للمستخدم $email'; + } + + @override + String get noFeedbackHistory => 'لا يوجد سجل تقييمات متاح لهذا المستخدم.'; + + @override + String feedbackProvidedAt(String date) { + return 'تم تقديم التقييم في: $date'; + } + + @override + String feedbackReason(String reason) { + return 'السبب: $reason'; + } + + @override + String get noReasonProvided => 'لم يتم تقديم سبب.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index caf6510a..68d107c3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1807,4 +1807,213 @@ class AppLocalizationsEn extends AppLocalizations { @override String get enableCommunityFeaturesDescription => 'Globally activates or deactivates all community-related functionality, including engagement and reporting.'; + + @override + String get communityManagement => 'Community'; + + @override + String get communityManagementPageDescription => + 'Manage user-generated content including engagements (reactions and comments), content reports, and app reviews.'; + + @override + String get engagements => 'Engagements'; + + @override + String get reports => 'Reports'; + + @override + String get appReviews => 'App Reviews'; + + @override + String get user => 'User'; + + @override + String get engagedContent => 'Engaged Content'; + + @override + String get reaction => 'Reaction'; + + @override + String get comment => 'Comment'; + + @override + String get commentStatus => 'Comment Status'; + + @override + String get date => 'Date'; + + @override + String get approveComment => 'Approve Comment'; + + @override + String get rejectComment => 'Reject Comment'; + + @override + String get viewEngagedContent => 'View Content'; + + @override + String get copyUserId => 'Copy User ID'; + + @override + String get reporter => 'Reporter'; + + @override + String get reportedItem => 'Reported Item'; + + @override + String get reason => 'Reason'; + + @override + String get reportStatus => 'Report Status'; + + @override + String get viewReportedItem => 'View Item'; + + @override + String get markAsInReview => 'Mark as In Review'; + + @override + String get resolveReport => 'Resolve Report'; + + @override + String get initialFeedback => 'Initial Feedback'; + + @override + String get osPromptRequested => 'OS Prompt?'; + + @override + String get feedbackHistory => 'Feedback History'; + + @override + String get lastInteraction => 'Last Interaction'; + + @override + String get viewFeedbackHistory => 'View History'; + + @override + String get reactionTypeLike => 'Like'; + + @override + String get reactionTypeInsightful => 'Insightful'; + + @override + String get reactionTypeAmusing => 'Amusing'; + + @override + String get reactionTypeSad => 'Sad'; + + @override + String get reactionTypeAngry => 'Angry'; + + @override + String get reactionTypeSkeptical => 'Skeptical'; + + @override + String get commentStatusPendingReview => 'Pending'; + + @override + String get commentStatusApproved => 'Approved'; + + @override + String get commentStatusRejected => 'Rejected'; + + @override + String get reportStatusSubmitted => 'Submitted'; + + @override + String get reportStatusInReview => 'In Review'; + + @override + String get reportStatusResolved => 'Resolved'; + + @override + String get initialAppReviewFeedbackPositive => 'Positive'; + + @override + String get initialAppReviewFeedbackNegative => 'Negative'; + + @override + String get filterCommunity => 'Filter Community Content'; + + @override + String get searchByEngagementUser => 'Search by user email...'; + + @override + String get searchByReportReporter => 'Search by reporter email...'; + + @override + String get searchByAppReviewUser => 'Search by user email...'; + + @override + String get selectCommentStatus => 'Select Comment Status'; + + @override + String get selectReportStatus => 'Select Report Status'; + + @override + String get selectInitialFeedback => 'Select Initial Feedback'; + + @override + String get selectReportableEntity => 'Select Reported Item Type'; + + @override + String get reportableEntityHeadline => 'Headline'; + + @override + String get reportableEntitySource => 'Source'; + + @override + String get reportableEntityComment => 'Comment'; + + @override + String get noEngagementsFound => 'No engagements found.'; + + @override + String get noReportsFound => 'No reports found.'; + + @override + String get noAppReviewsFound => 'No app reviews found.'; + + @override + String get loadingEngagements => 'Loading Engagements'; + + @override + String get loadingReports => 'Loading Reports'; + + @override + String get loadingAppReviews => 'Loading App Reviews'; + + @override + String get userIdCopied => 'User ID copied to clipboard.'; + + @override + String get commentApproved => 'Comment approved.'; + + @override + String get commentRejected => 'Comment rejected.'; + + @override + String get reportStatusUpdated => 'Report status updated.'; + + @override + String feedbackHistoryForUser(String email) { + return 'Feedback History for $email'; + } + + @override + String get noFeedbackHistory => + 'No feedback history available for this user.'; + + @override + String feedbackProvidedAt(String date) { + return 'Feedback provided at: $date'; + } + + @override + String feedbackReason(String reason) { + return 'Reason: $reason'; + } + + @override + String get noReasonProvided => 'No reason provided.'; } From ffd950bb77e957794ed909737625dcb0db9e6f8d Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:30:19 +0100 Subject: [PATCH 004/130] feat(community): provide new repositories and BLoCs Adds the newly created `EngagementRepository`, `ReportRepository`, and `AppReviewRepository` to the `MultiRepositoryProvider`. Also provides the new `CommunityManagementBloc` and `CommunityFilterBloc` to the `MultiBlocProvider`, making them accessible throughout the widget tree. --- lib/app/view/app.dart | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 764c93e9..acefee3d 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -11,6 +11,8 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/app/bloc/app_blo import 'package:flutter_news_app_web_dashboard_full_source_code/app/config/app_environment.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/bloc/app_configuration_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/bloc/authentication_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/content_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/headlines_filter/headlines_filter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/sources_filter/sources_filter_bloc.dart'; @@ -41,6 +43,9 @@ class App extends StatelessWidget { required DataRepository countriesRepository, required DataRepository languagesRepository, required DataRepository usersRepository, + required DataRepository engagementsRepository, + required DataRepository reportsRepository, + required DataRepository appReviewsRepository, required KVStorageService storageService, required AppEnvironment environment, required PendingDeletionsService pendingDeletionsService, @@ -57,6 +62,9 @@ class App extends StatelessWidget { _countriesRepository = countriesRepository, _languagesRepository = languagesRepository, _usersRepository = usersRepository, + _engagementsRepository = engagementsRepository, + _reportsRepository = reportsRepository, + _appReviewsRepository = appReviewsRepository, _environment = environment, _pendingDeletionsService = pendingDeletionsService; @@ -72,6 +80,9 @@ class App extends StatelessWidget { final DataRepository _countriesRepository; final DataRepository _languagesRepository; final DataRepository _usersRepository; + final DataRepository _engagementsRepository; + final DataRepository _reportsRepository; + final DataRepository _appReviewsRepository; final KVStorageService _kvStorageService; final AppEnvironment _environment; @@ -93,6 +104,9 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _countriesRepository), RepositoryProvider.value(value: _languagesRepository), RepositoryProvider.value(value: _usersRepository), + RepositoryProvider.value(value: _engagementsRepository), + RepositoryProvider.value(value: _reportsRepository), + RepositoryProvider.value(value: _appReviewsRepository), RepositoryProvider.value(value: _kvStorageService), RepositoryProvider( create: (context) => const ThrottledFetchingService(), @@ -163,6 +177,19 @@ class App extends StatelessWidget { userFilterBloc: context.read(), ), ), + BlocProvider( + create: (context) => CommunityFilterBloc(), + ), + BlocProvider( + create: (context) => CommunityManagementBloc( + engagementsRepository: + context.read>(), + reportsRepository: context.read>(), + appReviewsRepository: context.read>(), + communityFilterBloc: context.read(), + pendingDeletionsService: context.read(), + ), + ), ], child: _AppView( authenticationRepository: _authenticationRepository, From 8589096ea4f6e219121bb44b0cab6321fba14d45 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:30:55 +0100 Subject: [PATCH 005/130] feat(community): define community management route constant Adds a new route constant `communityManagement` for the new feature section. This ensures a single source of truth for the route path and name, preventing typos. --- lib/router/routes.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/router/routes.dart b/lib/router/routes.dart index b3666253..da753635 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -115,4 +115,13 @@ abstract final class Routes { /// The name for the user filter dialog route. static const String userFilterDialogName = 'userFilterDialog'; + + /// The path for the community management section. + static const String communityManagement = '/community-management'; + + /// The name for the community management section route. + static const String communityManagementName = 'communityManagement'; + + /// The name for the community filter dialog route. + static const String communityFilterDialogName = 'communityFilterDialog'; } From 73de080efec379429c0eec05f35519a92f5faf71 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:31:24 +0100 Subject: [PATCH 006/130] feat(community): grant route permissions for admin Updates the `routePermissions` map to grant `admin` users access to the new `communityManagementName` route. This ensures that only administrators can access the new community management section. --- lib/router/route_permissions.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/router/route_permissions.dart b/lib/router/route_permissions.dart index a9048eba..a3e2fbec 100644 --- a/lib/router/route_permissions.dart +++ b/lib/router/route_permissions.dart @@ -11,6 +11,7 @@ final Map> routePermissions = { Routes.overviewName, Routes.contentManagementName, Routes.userManagementName, + Routes.communityManagementName, Routes.appConfigurationName, }, // Publishers have a more restricted access, focused on content creation From 8112fe22fbee5d7013a12f78b9d175c74a89e580 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:33:03 +0100 Subject: [PATCH 007/130] feat(community): add navigation destination to app shell Integrates the new "Community" section into the main application navigation. A `NavigationDestination` is added to the `AdaptiveScaffold`, and the filtering logic is updated to display it based on the user's role permissions. --- lib/app/view/app_shell.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/app/view/app_shell.dart b/lib/app/view/app_shell.dart index 88ea06e9..d3bbb031 100644 --- a/lib/app/view/app_shell.dart +++ b/lib/app/view/app_shell.dart @@ -51,6 +51,11 @@ class AppShell extends StatelessWidget { selectedIcon: const Icon(Icons.people), label: l10n.userManagement, ), + NavigationDestination( + icon: const Icon(Icons.forum_outlined), + selectedIcon: const Icon(Icons.forum), + label: l10n.communityManagement, + ), NavigationDestination( icon: const Icon(Icons.settings_applications_outlined), selectedIcon: const Icon(Icons.settings_applications), @@ -64,6 +69,7 @@ class AppShell extends StatelessWidget { Routes.overviewName, Routes.contentManagementName, Routes.userManagementName, + Routes.communityManagementName, Routes.appConfigurationName, ]; From 5f3e2a19e21be043a98ecf1d753e74bb7a59f35a Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:35:14 +0100 Subject: [PATCH 008/130] feat(community): configure community management route Adds a new `StatefulShellBranch` for the community management section. This branch defines the main `/community-management` route and its sub-routes for the filter dialog, ensuring correct navigation and state preservation for the feature. --- lib/router/router.dart | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/lib/router/router.dart b/lib/router/router.dart index 49bb22dc..809a66da 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -12,6 +12,9 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/b import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/view/authentication_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/view/email_code_verification_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/view/request_code_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/view/community_management_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_filter_dialog/community_filter_dialog.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/content_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/headlines_filter/headlines_filter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/sources_filter/sources_filter_bloc.dart'; @@ -98,6 +101,7 @@ GoRouter createRouter({ Routes.overviewName: Routes.overview, Routes.contentManagementName: Routes.contentManagement, Routes.userManagementName: Routes.userManagement, + Routes.communityManagementName: Routes.communityManagement, Routes.appConfigurationName: Routes.appConfiguration, }; @@ -374,6 +378,52 @@ GoRouter createRouter({ ), ], ), + StatefulShellBranch( + routes: [ + GoRoute( + path: Routes.communityManagement, + name: Routes.communityManagementName, + builder: (context, state) => const CommunityManagementPage(), + routes: [ + GoRoute( + path: Routes.communityFilterDialog, + name: Routes.communityFilterDialogName, + pageBuilder: (context, state) { + final args = state.extra! as Map; + final activeTab = + args['activeTab'] as CommunityManagementTab; + final engagementsRepository = + args['engagementsRepository'] + as DataRepository; + final reportsRepository = + args['reportsRepository'] as DataRepository; + final appReviewsRepository = + args['appReviewsRepository'] + as DataRepository; + + return MaterialPage( + fullscreenDialog: true, + child: BlocProvider( + create: (providerContext) => + CommunityFilterDialogBloc( + activeTab: activeTab, + )..add( + CommunityFilterDialogInitialized( + activeTab: activeTab, + communityFilterState: providerContext + .read() + .state, + ), + ), + child: const CommunityFilterDialog(), + ), + ); + }, + ), + ], + ), + ], + ), StatefulShellBranch( routes: [ GoRoute( From d025bf72a57d49df44fb8196ebf7f489894fbcdc Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:35:43 +0100 Subject: [PATCH 009/130] feat(community): create community management bloc Creates the `CommunityManagementBloc` to manage the state for the entire community feature. It handles loading data for engagements, reports, and app reviews based on the active tab and applied filters. It also listens to the filter BLoC and repository updates to trigger data refreshes. --- .../bloc/community_management_bloc.dart | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 lib/community_management/bloc/community_management_bloc.dart diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart new file mode 100644 index 00000000..0bb37128 --- /dev/null +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -0,0 +1,289 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:core/core.dart'; +import 'package:data_repository/data_repository.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; +import 'package:logging/logging.dart'; +import 'package:ui_kit/ui_kit.dart'; + +part 'community_management_event.dart'; +part 'community_management_state.dart'; + +class CommunityManagementBloc + extends Bloc { + CommunityManagementBloc({ + required DataRepository engagementsRepository, + required DataRepository reportsRepository, + required DataRepository appReviewsRepository, + required CommunityFilterBloc communityFilterBloc, + required PendingDeletionsService pendingDeletionsService, + Logger? logger, + }) : _engagementsRepository = engagementsRepository, + _reportsRepository = reportsRepository, + _appReviewsRepository = appReviewsRepository, + _communityFilterBloc = communityFilterBloc, + _pendingDeletionsService = pendingDeletionsService, + _logger = logger ?? Logger('CommunityManagementBloc'), + super(const CommunityManagementState()) { + on(_onTabChanged); + on(_onLoadEngagementsRequested); + on(_onLoadReportsRequested); + on(_onLoadAppReviewsRequested); + + _filterSubscription = _communityFilterBloc.stream.listen((filterState) { + switch (state.activeTab) { + case CommunityManagementTab.engagements: + add( + LoadEngagementsRequested( + filter: buildEngagementsFilterMap(filterState), + forceRefresh: true, + ), + ); + case CommunityManagementTab.reports: + add( + LoadReportsRequested( + filter: buildReportsFilterMap(filterState), + forceRefresh: true, + ), + ); + case CommunityManagementTab.appReviews: + add( + LoadAppReviewsRequested( + filter: buildAppReviewsFilterMap(filterState), + forceRefresh: true, + ), + ); + } + }); + + _entityUpdateSubscription = + Stream.multi([ + _engagementsRepository.entityUpdated, + _reportsRepository.entityUpdated, + _appReviewsRepository.entityUpdated, + ]).listen((updatedType) { + if (updatedType == Engagement) { + add( + LoadEngagementsRequested( + filter: buildEngagementsFilterMap(_communityFilterBloc.state), + forceRefresh: true, + ), + ); + } else if (updatedType == Report) { + add( + LoadReportsRequested( + filter: buildReportsFilterMap(_communityFilterBloc.state), + forceRefresh: true, + ), + ); + } else if (updatedType == AppReview) { + add( + LoadAppReviewsRequested( + filter: buildAppReviewsFilterMap(_communityFilterBloc.state), + forceRefresh: true, + ), + ); + } + }); + } + + final DataRepository _engagementsRepository; + final DataRepository _reportsRepository; + final DataRepository _appReviewsRepository; + final CommunityFilterBloc _communityFilterBloc; + final PendingDeletionsService _pendingDeletionsService; + final Logger _logger; + + late final StreamSubscription _filterSubscription; + late final StreamSubscription _entityUpdateSubscription; + + @override + Future close() { + _filterSubscription.cancel(); + _entityUpdateSubscription.cancel(); + return super.close(); + } + + Map buildEngagementsFilterMap(CommunityFilterState state) { + final filter = {}; + if (state.searchQuery.isNotEmpty) { + filter['userId'] = {r'$regex': state.searchQuery, r'$options': 'i'}; + } + if (state.selectedCommentStatus.isNotEmpty) { + filter['comment.status'] = { + r'$in': state.selectedCommentStatus.map((s) => s.name).toList(), + }; + } + return filter; + } + + Map buildReportsFilterMap(CommunityFilterState state) { + final filter = {}; + if (state.searchQuery.isNotEmpty) { + filter['reporterUserId'] = { + r'$regex': state.searchQuery, + r'$options': 'i', + }; + } + if (state.selectedReportStatus.isNotEmpty) { + filter['status'] = { + r'$in': state.selectedReportStatus.map((s) => s.name).toList(), + }; + } + if (state.selectedReportableEntity.isNotEmpty) { + filter['entityType'] = { + r'$in': state.selectedReportableEntity.map((e) => e.name).toList(), + }; + } + return filter; + } + + Map buildAppReviewsFilterMap(CommunityFilterState state) { + final filter = {}; + if (state.searchQuery.isNotEmpty) { + filter['userId'] = {r'$regex': state.searchQuery, r'$options': 'i'}; + } + if (state.selectedInitialFeedback.isNotEmpty) { + filter['initialFeedback'] = { + r'$in': state.selectedInitialFeedback.map((f) => f.name).toList(), + }; + } + return filter; + } + + void _onTabChanged( + CommunityManagementTabChanged event, + Emitter emit, + ) { + emit(state.copyWith(activeTab: event.tab)); + } + + Future _onLoadEngagementsRequested( + LoadEngagementsRequested event, + Emitter emit, + ) async { + emit( + state.copyWith( + engagementsStatus: CommunityManagementStatus.loading, + engagements: event.forceRefresh ? [] : state.engagements, + ), + ); + try { + final response = await _engagementsRepository.readAll( + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), + filter: event.filter, + sort: [const SortOption('createdAt', SortOrder.desc)], + ); + final newEngagements = event.forceRefresh + ? response.items + : [...state.engagements, ...response.items]; + + emit( + state.copyWith( + engagementsStatus: CommunityManagementStatus.success, + engagements: newEngagements, + engagementsCursor: response.cursor, + hasMoreEngagements: response.hasMore, + forceEngagementsCursor: true, + ), + ); + } on HtHttpException catch (e) { + emit( + state.copyWith( + engagementsStatus: CommunityManagementStatus.failure, + exception: e, + ), + ); + } + } + + Future _onLoadReportsRequested( + LoadReportsRequested event, + Emitter emit, + ) async { + emit( + state.copyWith( + reportsStatus: CommunityManagementStatus.loading, + reports: event.forceRefresh ? [] : state.reports, + ), + ); + try { + final response = await _reportsRepository.readAll( + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), + filter: event.filter, + sort: [const SortOption('createdAt', SortOrder.desc)], + ); + final newReports = event.forceRefresh + ? response.items + : [...state.reports, ...response.items]; + + emit( + state.copyWith( + reportsStatus: CommunityManagementStatus.success, + reports: newReports, + reportsCursor: response.cursor, + hasMoreReports: response.hasMore, + forceReportsCursor: true, + ), + ); + } on HtHttpException catch (e) { + emit( + state.copyWith( + reportsStatus: CommunityManagementStatus.failure, + exception: e, + ), + ); + } + } + + Future _onLoadAppReviewsRequested( + LoadAppReviewsRequested event, + Emitter emit, + ) async { + emit( + state.copyWith( + appReviewsStatus: CommunityManagementStatus.loading, + appReviews: event.forceRefresh ? [] : state.appReviews, + ), + ); + try { + final response = await _appReviewsRepository.readAll( + pagination: PaginationOptions( + cursor: event.startAfterId, + limit: event.limit, + ), + filter: event.filter, + sort: [const SortOption('updatedAt', SortOrder.desc)], + ); + final newAppReviews = event.forceRefresh + ? response.items + : [...state.appReviews, ...response.items]; + + emit( + state.copyWith( + appReviewsStatus: CommunityManagementStatus.success, + appReviews: newAppReviews, + appReviewsCursor: response.cursor, + hasMoreAppReviews: response.hasMore, + forceAppReviewsCursor: true, + ), + ); + } on HtHttpException catch (e) { + emit( + state.copyWith( + appReviewsStatus: CommunityManagementStatus.failure, + exception: e, + ), + ); + } + } +} From 05ab655ee277d9d645e4e884254906a497fde130 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:36:56 +0100 Subject: [PATCH 010/130] feat(community): create community management page shell Creates the main `CommunityManagementPage`, which serves as the shell for the feature. It includes a `TabBar` for navigating between Engagements, Reports, and App Reviews, and integrates with the `CommunityManagementBloc` to manage tab state and actions like opening the filter dialog. --- .../view/community_management_page.dart | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 lib/community_management/view/community_management_page.dart diff --git a/lib/community_management/view/community_management_page.dart b/lib/community_management/view/community_management_page.dart new file mode 100644 index 00000000..9c2456dd --- /dev/null +++ b/lib/community_management/view/community_management_page.dart @@ -0,0 +1,111 @@ +import 'package:core/core.dart'; +import 'package:data_repository/data_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/view/app_reviews_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/view/engagements_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/view/reports_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/about_icon.dart'; +import 'package:go_router/go_router.dart'; +import 'package:ui_kit/ui_kit.dart'; + +class CommunityManagementPage extends StatefulWidget { + const CommunityManagementPage({super.key}); + + @override + State createState() => + _CommunityManagementPageState(); +} + +class _CommunityManagementPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + _tabController.addListener(_onTabChanged); + } + + @override + void dispose() { + _tabController + ..removeListener(_onTabChanged) + ..dispose(); + super.dispose(); + } + + void _onTabChanged() { + if (!_tabController.indexIsChanging) { + final tab = CommunityManagementTab.values[_tabController.index]; + context.read().add( + CommunityManagementTabChanged(tab), + ); + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Scaffold( + appBar: AppBar( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l10n.communityManagement), + const SizedBox(width: AppSpacing.xs), + AboutIcon( + dialogTitle: l10n.communityManagement, + dialogDescription: l10n.communityManagementPageDescription, + ), + ], + ), + actions: [ + IconButton( + icon: const Icon(Icons.filter_list), + tooltip: l10n.filterCommunity, + onPressed: () { + final communityManagementBloc = context + .read(); + final engagementsRepository = context + .read>(); + final reportsRepository = context.read>(); + final appReviewsRepository = context + .read>(); + + final arguments = { + 'activeTab': communityManagementBloc.state.activeTab, + 'engagementsRepository': engagementsRepository, + 'reportsRepository': reportsRepository, + 'appReviewsRepository': appReviewsRepository, + }; + + context.pushNamed( + Routes.communityFilterDialogName, + extra: arguments, + ); + }, + ), + ], + bottom: TabBar( + controller: _tabController, + tabAlignment: TabAlignment.start, + isScrollable: true, + tabs: [ + Tab(text: l10n.engagements), + Tab(text: l10n.reports), + Tab(text: l10n.appReviews), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: const [EngagementsPage(), ReportsPage(), AppReviewsPage()], + ), + ); + } +} From db078ff44827f73a19455a54f2b7fdd5ae3c7c9d Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:39:07 +0100 Subject: [PATCH 011/130] feat(community_management): add engagements page - Implement EngagementsPage StatefulWidget - Add loading, failure, and empty states handling - Display engagements in a paginated data table - Include filters and actions functionality - Support responsive design for mobile and desktop views --- .../view/engagements_page.dart | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 lib/community_management/view/engagements_page.dart diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart new file mode 100644 index 00000000..91aca87f --- /dev/null +++ b/lib/community_management/view/engagements_page.dart @@ -0,0 +1,239 @@ +import 'package:core/core.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_action_buttons.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +class EngagementsPage extends StatefulWidget { + const EngagementsPage({super.key}); + + @override + State createState() => _EngagementsPageState(); +} + +class _EngagementsPageState extends State { + @override + void initState() { + super.initState(); + context.read().add( + LoadEngagementsRequested( + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildEngagementsFilterMap( + context.read().state, + ), + ), + ); + } + + bool _areFiltersActive(CommunityFilterState state) { + return state.searchQuery.isNotEmpty || + state.selectedCommentStatus.isNotEmpty; + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: BlocBuilder( + builder: (context, state) { + final communityFilterState = context + .watch() + .state; + final filtersActive = _areFiltersActive(communityFilterState); + + if (state.engagementsStatus == CommunityManagementStatus.loading && + state.engagements.isEmpty) { + return LoadingStateWidget( + icon: Icons.comment_outlined, + headline: l10n.loadingEngagements, + subheadline: l10n.pleaseWait, + ); + } + + if (state.engagementsStatus == CommunityManagementStatus.failure) { + return FailureStateWidget( + exception: state.exception!, + onRetry: () => context.read().add( + LoadEngagementsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: context + .read() + .buildEngagementsFilterMap( + context.read().state, + ), + ), + ), + ); + } + + if (state.engagements.isEmpty) { + if (filtersActive) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.noResultsWithCurrentFilters, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.lg), + ElevatedButton( + onPressed: () => context.read().add( + const CommunityFilterReset(), + ), + child: Text(l10n.resetFiltersButtonText), + ), + ], + ), + ); + } + return Center(child: Text(l10n.noEngagementsFound)); + } + + return Column( + children: [ + if (state.engagementsStatus == + CommunityManagementStatus.loading && + state.engagements.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final isMobile = constraints.maxWidth < 600; + return PaginatedDataTable2( + columns: [ + DataColumn2(label: Text(l10n.user), size: ColumnSize.L), + if (!isMobile) + DataColumn2( + label: Text(l10n.engagedContent), + size: ColumnSize.M, + ), + DataColumn2( + label: Text(l10n.reaction), + size: ColumnSize.S, + ), + if (!isMobile) + DataColumn2( + label: Text(l10n.comment), + size: ColumnSize.L, + ), + DataColumn2( + label: Text(l10n.commentStatus), + size: ColumnSize.S, + ), + if (!isMobile) + DataColumn2( + label: Text(l10n.date), + size: ColumnSize.S, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + ), + ], + source: _EngagementsDataSource( + context: context, + engagements: state.engagements, + hasMore: state.hasMoreEngagements, + l10n: l10n, + isMobile: isMobile, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.engagements.length && + state.hasMoreEngagements && + state.engagementsStatus != + CommunityManagementStatus.loading) { + context.read().add( + LoadEngagementsRequested( + startAfterId: state.engagementsCursor, + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildEngagementsFilterMap( + context.read().state, + ), + ), + ); + } + }, + empty: Center(child: Text(l10n.noEngagementsFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.sm, + horizontalMargin: AppSpacing.sm, + ); + }, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _EngagementsDataSource extends DataTableSource { + _EngagementsDataSource({ + required this.context, + required this.engagements, + required this.hasMore, + required this.l10n, + required this.isMobile, + }); + + final BuildContext context; + final List engagements; + final bool hasMore; + final AppLocalizations l10n; + final bool isMobile; + + @override + DataRow? getRow(int index) { + if (index >= engagements.length) return null; + final engagement = engagements[index]; + return DataRow2( + cells: [ + DataCell(Text(engagement.userId, overflow: TextOverflow.ellipsis)), + if (!isMobile) DataCell(Text(engagement.entityId)), + DataCell(Text(engagement.reaction.reactionType.name)), + if (!isMobile) + DataCell(Text(engagement.comment?.content ?? l10n.notAvailable)), + DataCell(Text(engagement.comment?.status.name ?? l10n.notAvailable)), + if (!isMobile) + DataCell( + Text( + DateFormat('dd-MM-yyyy').format(engagement.createdAt.toLocal()), + ), + ), + DataCell(CommunityActionButtons(item: engagement, l10n: l10n)), + ], + ); + } + + @override + bool get isRowCountApproximate => hasMore; + + @override + int get rowCount => engagements.length; + + @override + int get selectedRowCount => 0; +} From d242fbd4ae9cedf622e1d81cbf9a57f7dc0d0826 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:41:18 +0100 Subject: [PATCH 012/130] feat(community_management): add community action buttons widget - Implement CommunityActionButtons widget for engagement, report, and app review items - Add primary actions and secondary actions with overflow menu - Implement copy user ID functionality - Add action handlers for approve/reject comments and mark/resolve reports - Use AppLocalizations for internationalization --- .../widgets/community_action_buttons.dart | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 lib/community_management/widgets/community_action_buttons.dart diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart new file mode 100644 index 00000000..f0955900 --- /dev/null +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -0,0 +1,220 @@ +import 'package:core/core.dart'; +import 'package:data_repository/data_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; + +class CommunityActionButtons extends StatelessWidget { + const CommunityActionButtons({ + required this.item, + required this.l10n, + super.key, + }); + + final Object item; + final AppLocalizations l10n; + + @override + Widget build(BuildContext context) { + final visibleActions = []; + final overflowMenuItems = >[]; + + if (item is Engagement) { + final engagement = item as Engagement; + _buildEngagementActions( + context, + engagement, + visibleActions, + overflowMenuItems, + ); + } else if (item is Report) { + final report = item as Report; + _buildReportActions(context, report, visibleActions, overflowMenuItems); + } else if (item is AppReview) { + final appReview = item as AppReview; + _buildAppReviewActions( + context, + appReview, + visibleActions, + overflowMenuItems, + ); + } + + if (overflowMenuItems.isNotEmpty) { + visibleActions.add( + SizedBox( + width: 32, + child: PopupMenuButton( + iconSize: 20, + icon: const Icon(Icons.more_vert), + tooltip: l10n.moreActions, + onSelected: (value) => _onActionSelected(context, value, item), + itemBuilder: (context) => overflowMenuItems, + ), + ), + ); + } + + return Row(mainAxisSize: MainAxisSize.min, children: visibleActions); + } + + void _buildEngagementActions( + BuildContext context, + Engagement engagement, + List visibleActions, + List> overflowMenuItems, + ) { + // Primary Action + visibleActions.add( + IconButton( + visualDensity: VisualDensity.compact, + iconSize: 20, + icon: const Icon(Icons.visibility_outlined), + tooltip: l10n.viewEngagedContent, + onPressed: () { + // TODO(fulleni): Implement navigation to content + }, + ), + ); + + // Secondary Actions + if (engagement.comment != null) { + if (engagement.comment!.status != CommentStatus.approved) { + overflowMenuItems.add( + PopupMenuItem( + value: 'approveComment', + child: Text(l10n.approveComment), + ), + ); + } + if (engagement.comment!.status != CommentStatus.rejected) { + overflowMenuItems.add( + PopupMenuItem( + value: 'rejectComment', + child: Text(l10n.rejectComment), + ), + ); + } + } + overflowMenuItems.add( + PopupMenuItem(value: 'copyUserId', child: Text(l10n.copyUserId)), + ); + } + + void _buildReportActions( + BuildContext context, + Report report, + List visibleActions, + List> overflowMenuItems, + ) { + // Primary Action + visibleActions.add( + IconButton( + visualDensity: VisualDensity.compact, + iconSize: 20, + icon: const Icon(Icons.visibility_outlined), + tooltip: l10n.viewReportedItem, + onPressed: () { + // TODO(fulleni): Implement navigation to reported item + }, + ), + ); + + // Secondary Actions + if (report.status != ReportStatus.inReview) { + overflowMenuItems.add( + PopupMenuItem( + value: 'markAsInReview', + child: Text(l10n.markAsInReview), + ), + ); + } + if (report.status != ReportStatus.resolved) { + overflowMenuItems.add( + PopupMenuItem( + value: 'resolveReport', + child: Text(l10n.resolveReport), + ), + ); + } + overflowMenuItems.add( + PopupMenuItem(value: 'copyUserId', child: Text(l10n.copyUserId)), + ); + } + + void _buildAppReviewActions( + BuildContext context, + AppReview appReview, + List visibleActions, + List> overflowMenuItems, + ) { + // Primary Action + visibleActions.add( + IconButton( + visualDensity: VisualDensity.compact, + iconSize: 20, + icon: const Icon(Icons.history), + tooltip: l10n.viewFeedbackHistory, + onPressed: () { + // TODO(fulleni): Implement dialog to show feedback history + }, + ), + ); + + // Secondary Actions + overflowMenuItems.add( + PopupMenuItem(value: 'copyUserId', child: Text(l10n.copyUserId)), + ); + } + + void _onActionSelected(BuildContext context, String value, Object item) { + final engagementsRepository = context.read>(); + final reportsRepository = context.read>(); + + if (value == 'copyUserId') { + String userId; + if (item is Engagement) { + userId = item.userId; + } else if (item is Report) { + userId = item.reporterUserId; + } else if (item is AppReview) { + userId = item.userId; + } else { + return; + } + Clipboard.setData(ClipboardData(text: userId)); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(SnackBar(content: Text(l10n.userIdCopied))); + } else if (value == 'approveComment' && item is Engagement) { + final updatedEngagement = item.copyWith( + comment: item.comment?.copyWith(status: CommentStatus.approved), + ); + engagementsRepository.update( + id: updatedEngagement.id, + item: updatedEngagement, + ); + } else if (value == 'rejectComment' && item is Engagement) { + final updatedEngagement = item.copyWith( + comment: item.comment?.copyWith(status: CommentStatus.rejected), + ); + engagementsRepository.update( + id: updatedEngagement.id, + item: updatedEngagement, + ); + } else if (value == 'markAsInReview' && item is Report) { + final updatedReport = item.copyWith(status: ReportStatus.inReview); + reportsRepository.update( + id: updatedReport.id, + item: updatedReport, + ); + } else if (value == 'resolveReport' && item is Report) { + final updatedReport = item.copyWith(status: ReportStatus.resolved); + reportsRepository.update( + id: updatedReport.id, + item: updatedReport, + ); + } + } +} From 9ade0b43d5d7e750472edb9297f4cd59ae744125 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:43:26 +0100 Subject: [PATCH 013/130] feat(community_management): implement reports page - Add ReportsPage widget to display user reports - Implement pagination and filtering functionality - Show loading and error states - Display report details in a table format - Include action buttons for each report --- .../view/reports_page.dart | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 lib/community_management/view/reports_page.dart diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart new file mode 100644 index 00000000..16f5e7e7 --- /dev/null +++ b/lib/community_management/view/reports_page.dart @@ -0,0 +1,233 @@ +import 'package:core/core.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_action_buttons.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +class ReportsPage extends StatefulWidget { + const ReportsPage({super.key}); + + @override + State createState() => _ReportsPageState(); +} + +class _ReportsPageState extends State { + @override + void initState() { + super.initState(); + context.read().add( + LoadReportsRequested( + limit: kDefaultRowsPerPage, + filter: context.read().buildReportsFilterMap( + context.read().state, + ), + ), + ); + } + + bool _areFiltersActive(CommunityFilterState state) { + return state.searchQuery.isNotEmpty || + state.selectedReportStatus.isNotEmpty || + state.selectedReportableEntity.isNotEmpty; + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: BlocBuilder( + builder: (context, state) { + final communityFilterState = context + .watch() + .state; + final filtersActive = _areFiltersActive(communityFilterState); + + if (state.reportsStatus == CommunityManagementStatus.loading && + state.reports.isEmpty) { + return LoadingStateWidget( + icon: Icons.report_problem_outlined, + headline: l10n.loadingReports, + subheadline: l10n.pleaseWait, + ); + } + + if (state.reportsStatus == CommunityManagementStatus.failure) { + return FailureStateWidget( + exception: state.exception!, + onRetry: () => context.read().add( + LoadReportsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: context + .read() + .buildReportsFilterMap( + context.read().state, + ), + ), + ), + ); + } + + if (state.reports.isEmpty) { + if (filtersActive) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.noResultsWithCurrentFilters, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.lg), + ElevatedButton( + onPressed: () => context.read().add( + const CommunityFilterReset(), + ), + child: Text(l10n.resetFiltersButtonText), + ), + ], + ), + ); + } + return Center(child: Text(l10n.noReportsFound)); + } + + return Column( + children: [ + if (state.reportsStatus == CommunityManagementStatus.loading && + state.reports.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final isMobile = constraints.maxWidth < 600; + return PaginatedDataTable2( + columns: [ + DataColumn2( + label: Text(l10n.reporter), + size: ColumnSize.L, + ), + if (!isMobile) + DataColumn2( + label: Text(l10n.reportedItem), + size: ColumnSize.L, + ), + DataColumn2( + label: Text(l10n.reason), + size: ColumnSize.M, + ), + if (!isMobile) + DataColumn2( + label: Text(l10n.reportStatus), + size: ColumnSize.S, + ), + if (!isMobile) + DataColumn2( + label: Text(l10n.date), + size: ColumnSize.S, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + ), + ], + source: _ReportsDataSource( + context: context, + reports: state.reports, + hasMore: state.hasMoreReports, + l10n: l10n, + isMobile: isMobile, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.reports.length && + state.hasMoreReports && + state.reportsStatus != + CommunityManagementStatus.loading) { + context.read().add( + LoadReportsRequested( + startAfterId: state.reportsCursor, + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildReportsFilterMap( + context.read().state, + ), + ), + ); + } + }, + empty: Center(child: Text(l10n.noReportsFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.sm, + horizontalMargin: AppSpacing.sm, + ); + }, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _ReportsDataSource extends DataTableSource { + _ReportsDataSource({ + required this.context, + required this.reports, + required this.hasMore, + required this.l10n, + required this.isMobile, + }); + + final BuildContext context; + final List reports; + final bool hasMore; + final AppLocalizations l10n; + final bool isMobile; + + @override + DataRow? getRow(int index) { + if (index >= reports.length) return null; + final report = reports[index]; + return DataRow2( + cells: [ + DataCell(Text(report.reporterUserId, overflow: TextOverflow.ellipsis)), + if (!isMobile) + DataCell(Text('${report.entityType.name}: ${report.entityId}')), + DataCell(Text(report.reason)), + if (!isMobile) DataCell(Text(report.status.name)), + if (!isMobile) + DataCell( + Text(DateFormat('dd-MM-yyyy').format(report.createdAt.toLocal())), + ), + DataCell(CommunityActionButtons(item: report, l10n: l10n)), + ], + ); + } + + @override + bool get isRowCountApproximate => hasMore; + + @override + int get rowCount => reports.length; + + @override + int get selectedRowCount => 0; +} From 9518599848961034193e8c127a7a062c3db36ac1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:46:00 +0100 Subject: [PATCH 014/130] feat(community_management): add app reviews page - Implement AppReviewsPage widget for displaying app reviews - Add loading, error, and empty states - Include pagination and filtering functionality - Display review details and action buttons --- .../view/app_reviews_page.dart | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 lib/community_management/view/app_reviews_page.dart diff --git a/lib/community_management/view/app_reviews_page.dart b/lib/community_management/view/app_reviews_page.dart new file mode 100644 index 00000000..0edc1af5 --- /dev/null +++ b/lib/community_management/view/app_reviews_page.dart @@ -0,0 +1,236 @@ +import 'package:core/core.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_action_buttons.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +class AppReviewsPage extends StatefulWidget { + const AppReviewsPage({super.key}); + + @override + State createState() => _AppReviewsPageState(); +} + +class _AppReviewsPageState extends State { + @override + void initState() { + super.initState(); + context.read().add( + LoadAppReviewsRequested( + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildAppReviewsFilterMap( + context.read().state, + ), + ), + ); + } + + bool _areFiltersActive(CommunityFilterState state) { + return state.searchQuery.isNotEmpty || + state.selectedInitialFeedback.isNotEmpty; + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + return Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: BlocBuilder( + builder: (context, state) { + final communityFilterState = context + .watch() + .state; + final filtersActive = _areFiltersActive(communityFilterState); + + if (state.appReviewsStatus == CommunityManagementStatus.loading && + state.appReviews.isEmpty) { + return LoadingStateWidget( + icon: Icons.reviews_outlined, + headline: l10n.loadingAppReviews, + subheadline: l10n.pleaseWait, + ); + } + + if (state.appReviewsStatus == CommunityManagementStatus.failure) { + return FailureStateWidget( + exception: state.exception!, + onRetry: () => context.read().add( + LoadAppReviewsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: context + .read() + .buildAppReviewsFilterMap( + context.read().state, + ), + ), + ), + ); + } + + if (state.appReviews.isEmpty) { + if (filtersActive) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.noResultsWithCurrentFilters, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: AppSpacing.lg), + ElevatedButton( + onPressed: () => context.read().add( + const CommunityFilterReset(), + ), + child: Text(l10n.resetFiltersButtonText), + ), + ], + ), + ); + } + return Center(child: Text(l10n.noAppReviewsFound)); + } + + return Column( + children: [ + if (state.appReviewsStatus == CommunityManagementStatus.loading && + state.appReviews.isNotEmpty) + const LinearProgressIndicator(), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final isMobile = constraints.maxWidth < 600; + return PaginatedDataTable2( + columns: [ + DataColumn2(label: Text(l10n.user), size: ColumnSize.L), + DataColumn2( + label: Text(l10n.initialFeedback), + size: ColumnSize.M, + ), + if (!isMobile) + DataColumn2( + label: Text(l10n.osPromptRequested), + size: ColumnSize.S, + ), + if (!isMobile) + DataColumn2( + label: Text(l10n.feedbackHistory), + size: ColumnSize.M, + ), + if (!isMobile) + DataColumn2( + label: Text(l10n.lastInteraction), + size: ColumnSize.S, + ), + DataColumn2( + label: Text(l10n.actions), + size: ColumnSize.S, + ), + ], + source: _AppReviewsDataSource( + context: context, + appReviews: state.appReviews, + hasMore: state.hasMoreAppReviews, + l10n: l10n, + isMobile: isMobile, + ), + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], + onPageChanged: (pageIndex) { + final newOffset = pageIndex * kDefaultRowsPerPage; + if (newOffset >= state.appReviews.length && + state.hasMoreAppReviews && + state.appReviewsStatus != + CommunityManagementStatus.loading) { + context.read().add( + LoadAppReviewsRequested( + startAfterId: state.appReviewsCursor, + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildAppReviewsFilterMap( + context.read().state, + ), + ), + ); + } + }, + empty: Center(child: Text(l10n.noAppReviewsFound)), + showCheckboxColumn: false, + showFirstLastButtons: true, + fit: FlexFit.tight, + headingRowHeight: 56, + dataRowHeight: 56, + columnSpacing: AppSpacing.sm, + horizontalMargin: AppSpacing.sm, + ); + }, + ), + ), + ], + ); + }, + ), + ); + } +} + +class _AppReviewsDataSource extends DataTableSource { + _AppReviewsDataSource({ + required this.context, + required this.appReviews, + required this.hasMore, + required this.l10n, + required this.isMobile, + }); + + final BuildContext context; + final List appReviews; + final bool hasMore; + final AppLocalizations l10n; + final bool isMobile; + + @override + DataRow? getRow(int index) { + if (index >= appReviews.length) return null; + final appReview = appReviews[index]; + return DataRow2( + cells: [ + DataCell(Text(appReview.userId, overflow: TextOverflow.ellipsis)), + DataCell(Text(appReview.initialFeedback.name)), + if (!isMobile) + DataCell( + Text(appReview.wasStoreReviewRequested ? l10n.yes : l10n.no), + ), + if (!isMobile) + DataCell(Text('${appReview.negativeFeedbackHistory.length}')), + if (!isMobile) + DataCell( + Text( + DateFormat('dd-MM-yyyy').format(appReview.updatedAt.toLocal()), + ), + ), + DataCell(CommunityActionButtons(item: appReview, l10n: l10n)), + ], + ); + } + + @override + bool get isRowCountApproximate => hasMore; + + @override + int get rowCount => appReviews.length; + + @override + int get selectedRowCount => 0; +} From ddee43f239c40c479ca485035ab63cc9e72cf935 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:47:35 +0100 Subject: [PATCH 015/130] feat(community_management): implement CommunityFilterBloc - Create CommunityFilterBloc to manage community filter state - Add event handlers for filter application and reset - Define state properties for search query --- .../community_filter_bloc.dart | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 lib/community_management/bloc/community_filter/community_filter_bloc.dart diff --git a/lib/community_management/bloc/community_filter/community_filter_bloc.dart b/lib/community_management/bloc/community_filter/community_filter_bloc.dart new file mode 100644 index 00000000..d112dd24 --- /dev/null +++ b/lib/community_management/bloc/community_filter/community_filter_bloc.dart @@ -0,0 +1,38 @@ +import 'package:bloc/bloc.dart'; +import 'package:core/core.dart'; +import 'package:equatable/equatable.dart'; + +part 'community_filter_event.dart'; +part 'community_filter_state.dart'; + +class CommunityFilterBloc + extends Bloc { + CommunityFilterBloc() : super(const CommunityFilterState()) { + on(_onFilterApplied); + on(_onFilterReset); + } + + void _onFilterApplied( + CommunityFilterApplied event, + Emitter emit, + ) { + emit( + state.copyWith( + searchQuery: event.searchQuery, + selectedCommentStatus: event.selectedCommentStatus, + selectedReportStatus: event.selectedReportStatus, + selectedReportableEntity: event.selectedReportableEntity, + selectedInitialFeedback: event.selectedInitialFeedback, + ), + ); + } + + void _onFilterReset( + CommunityFilterReset event, + Emitter emit, + ) { + emit( + const CommunityFilterState(), + ); + } +} From 9fdbb18c4c6a1b419a7973a33c9c31f091d91bf4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:48:45 +0100 Subject: [PATCH 016/130] feat(community_management): implement community filter dialog bloc - Create CommunityFilterDialogBloc to manage state of community filter dialog - Add event handlers for various filter options and search query - Implement reset functionality to clear filters - Update state based on user interactions with the dialog --- .../bloc/community_filter_dialog_bloc.dart | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart new file mode 100644 index 00000000..cf2120d6 --- /dev/null +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart @@ -0,0 +1,85 @@ +import 'package:bloc/bloc.dart'; +import 'package:core/core.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; + +part 'community_filter_dialog_event.dart'; +part 'community_filter_dialog_state.dart'; + +class CommunityFilterDialogBloc + extends Bloc { + CommunityFilterDialogBloc({required CommunityManagementTab activeTab}) + : super(CommunityFilterDialogState(activeTab: activeTab)) { + on(_onFilterDialogInitialized); + on(_onSearchQueryChanged); + on(_onCommentStatusChanged); + on(_onReportStatusChanged); + on( + _onReportableEntityChanged, + ); + on(_onInitialFeedbackChanged); + on(_onFilterDialogReset); + } + + void _onFilterDialogInitialized( + CommunityFilterDialogInitialized event, + Emitter emit, + ) { + emit( + state.copyWith( + searchQuery: event.communityFilterState.searchQuery, + selectedCommentStatus: event.communityFilterState.selectedCommentStatus, + selectedReportStatus: event.communityFilterState.selectedReportStatus, + selectedReportableEntity: + event.communityFilterState.selectedReportableEntity, + selectedInitialFeedback: + event.communityFilterState.selectedInitialFeedback, + ), + ); + } + + void _onSearchQueryChanged( + CommunityFilterDialogSearchQueryChanged event, + Emitter emit, + ) { + emit(state.copyWith(searchQuery: event.query)); + } + + void _onCommentStatusChanged( + CommunityFilterDialogCommentStatusChanged event, + Emitter emit, + ) { + emit(state.copyWith(selectedCommentStatus: event.commentStatus)); + } + + void _onReportStatusChanged( + CommunityFilterDialogReportStatusChanged event, + Emitter emit, + ) { + emit(state.copyWith(selectedReportStatus: event.reportStatus)); + } + + void _onReportableEntityChanged( + CommunityFilterDialogReportableEntityChanged event, + Emitter emit, + ) { + emit(state.copyWith(selectedReportableEntity: event.reportableEntity)); + } + + void _onInitialFeedbackChanged( + CommunityFilterDialogInitialFeedbackChanged event, + Emitter emit, + ) { + emit(state.copyWith(selectedInitialFeedback: event.initialFeedback)); + } + + void _onFilterDialogReset( + CommunityFilterDialogReset event, + Emitter emit, + ) { + emit( + CommunityFilterDialogState(activeTab: state.activeTab), + ); + } +} From c952114fa8a1bed935359effcd329ca295feb742 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:49:39 +0100 Subject: [PATCH 017/130] feat(community_management): add community filter dialog widget - Implement CommunityFilterDialog StatefulWidget - Add functionality to apply and reset filters - Include tab-specific filter options for engagements, reports, and app reviews - Use BlocBuilder for state management with CommunityFilterDialogBloc - Implement search functionality with customizable hints based on active tab - Create UI components for comment status, report status, and initial feedback selection --- .../community_filter_dialog.dart | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart new file mode 100644 index 00000000..5b4ac05f --- /dev/null +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -0,0 +1,197 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/searchable_selection_input.dart'; +import 'package:ui_kit/ui_kit.dart'; + +class CommunityFilterDialog extends StatefulWidget { + const CommunityFilterDialog({super.key}); + + @override + State createState() => _CommunityFilterDialogState(); +} + +class _CommunityFilterDialogState extends State { + late TextEditingController _searchController; + + @override + void initState() { + super.initState(); + _searchController = TextEditingController(); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _dispatchFilterApplied(CommunityFilterDialogState filterDialogState) { + context.read().add( + CommunityFilterApplied( + searchQuery: filterDialogState.searchQuery, + selectedCommentStatus: filterDialogState.selectedCommentStatus, + selectedReportStatus: filterDialogState.selectedReportStatus, + selectedReportableEntity: filterDialogState.selectedReportableEntity, + selectedInitialFeedback: filterDialogState.selectedInitialFeedback, + ), + ); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + + return BlocBuilder( + builder: (context, filterDialogState) { + if (_searchController.text != filterDialogState.searchQuery) { + _searchController.text = filterDialogState.searchQuery; + _searchController.selection = TextSelection.fromPosition( + TextPosition(offset: _searchController.text.length), + ); + } + + return Scaffold( + appBar: AppBar( + title: Text(l10n.filterCommunity), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + tooltip: l10n.resetFiltersButtonText, + onPressed: () { + context.read().add( + const CommunityFilterReset(), + ); + Navigator.of(context).pop(); + }, + ), + IconButton( + icon: const Icon(Icons.check), + tooltip: l10n.applyFilters, + onPressed: () { + _dispatchFilterApplied(filterDialogState); + Navigator.of(context).pop(); + }, + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _searchController, + decoration: InputDecoration( + labelText: l10n.search, + hintText: _getSearchHint( + filterDialogState.activeTab, + l10n, + ), + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + ), + onChanged: (query) { + context.read().add( + CommunityFilterDialogSearchQueryChanged(query), + ); + }, + ), + const SizedBox(height: AppSpacing.lg), + ..._buildTabSpecificFilters(filterDialogState, l10n), + ], + ), + ), + ), + ); + }, + ); + } + + String _getSearchHint(CommunityManagementTab tab, AppLocalizations l10n) { + switch (tab) { + case CommunityManagementTab.engagements: + return l10n.searchByEngagementUser; + case CommunityManagementTab.reports: + return l10n.searchByReportReporter; + case CommunityManagementTab.appReviews: + return l10n.searchByAppReviewUser; + } + } + + List _buildTabSpecificFilters( + CommunityFilterDialogState state, + AppLocalizations l10n, + ) { + switch (state.activeTab) { + case CommunityManagementTab.engagements: + return [ + SearchableSelectionInput( + label: l10n.commentStatus, + hintText: l10n.selectCommentStatus, + isMultiSelect: true, + selectedItems: state.selectedCommentStatus, + itemBuilder: (context, item) => Text(item.name), + itemToString: (item) => item.name, + onChanged: (items) => context.read().add( + CommunityFilterDialogCommentStatusChanged(items ?? []), + ), + staticItems: CommentStatus.values, + ), + ]; + case CommunityManagementTab.reports: + return [ + SearchableSelectionInput( + label: l10n.reportStatus, + hintText: l10n.selectReportStatus, + isMultiSelect: true, + selectedItems: state.selectedReportStatus, + itemBuilder: (context, item) => Text(item.name), + itemToString: (item) => item.name, + onChanged: (items) => context.read().add( + CommunityFilterDialogReportStatusChanged(items ?? []), + ), + staticItems: ReportStatus.values, + ), + const SizedBox(height: AppSpacing.lg), + SearchableSelectionInput( + label: l10n.reportedItem, + hintText: l10n.selectReportableEntity, + isMultiSelect: true, + selectedItems: state.selectedReportableEntity, + itemBuilder: (context, item) => Text(item.name), + itemToString: (item) => item.name, + onChanged: (items) => context.read().add( + CommunityFilterDialogReportableEntityChanged(items ?? []), + ), + staticItems: ReportableEntity.values, + ), + ]; + case CommunityManagementTab.appReviews: + return [ + SearchableSelectionInput( + label: l10n.initialFeedback, + hintText: l10n.selectInitialFeedback, + isMultiSelect: true, + selectedItems: state.selectedInitialFeedback, + itemBuilder: (context, item) => Text(item.name), + itemToString: (item) => item.name, + onChanged: (items) => context.read().add( + CommunityFilterDialogInitialFeedbackChanged(items ?? []), + ), + staticItems: InitialAppReviewFeedback.values, + ), + ]; + } + } +} From e18dfdf59571bf1b4397ee39c9b7f811c02c4b74 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:50:48 +0100 Subject: [PATCH 018/130] feat(community_management): add community management events - Create abstract CommunityManagementEvent class - Add CommunityManagementTabChanged event - Implement LoadEngagementsRequested, LoadReportsRequested, and LoadAppReviewsRequested events - Define properties and equality for all events --- .../bloc/community_management_event.dart | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 lib/community_management/bloc/community_management_event.dart diff --git a/lib/community_management/bloc/community_management_event.dart b/lib/community_management/bloc/community_management_event.dart new file mode 100644 index 00000000..9bfe796f --- /dev/null +++ b/lib/community_management/bloc/community_management_event.dart @@ -0,0 +1,68 @@ +part of 'community_management_bloc.dart'; + +abstract class CommunityManagementEvent extends Equatable { + const CommunityManagementEvent(); + + @override + List get props => []; +} + +class CommunityManagementTabChanged extends CommunityManagementEvent { + const CommunityManagementTabChanged(this.tab); + + final CommunityManagementTab tab; + + @override + List get props => [tab]; +} + +class LoadEngagementsRequested extends CommunityManagementEvent { + const LoadEngagementsRequested({ + this.startAfterId, + this.limit, + this.filter, + this.forceRefresh = false, + }); + + final String? startAfterId; + final int? limit; + final Map? filter; + final bool forceRefresh; + + @override + List get props => [startAfterId, limit, filter, forceRefresh]; +} + +class LoadReportsRequested extends CommunityManagementEvent { + const LoadReportsRequested({ + this.startAfterId, + this.limit, + this.filter, + this.forceRefresh = false, + }); + + final String? startAfterId; + final int? limit; + final Map? filter; + final bool forceRefresh; + + @override + List get props => [startAfterId, limit, filter, forceRefresh]; +} + +class LoadAppReviewsRequested extends CommunityManagementEvent { + const LoadAppReviewsRequested({ + this.startAfterId, + this.limit, + this.filter, + this.forceRefresh = false, + }); + + final String? startAfterId; + final int? limit; + final Map? filter; + final bool forceRefresh; + + @override + List get props => [startAfterId, limit, filter, forceRefresh]; +} From 16703b9f9d5b6e58742b1e15a69d089735a54812 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:52:09 +0100 Subject: [PATCH 019/130] feat(community_management): add CommunityManagementState class - Define enums for CommunityManagementTab and CommunityManagementStatus - Create CommunityManagementState class with properties for active tab, status, data, cursors, and pagination - Implement copyWith method for state mutation - Override props for Equatable comparison --- .../bloc/community_management_state.dart | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 lib/community_management/bloc/community_management_state.dart diff --git a/lib/community_management/bloc/community_management_state.dart b/lib/community_management/bloc/community_management_state.dart new file mode 100644 index 00000000..e7acc204 --- /dev/null +++ b/lib/community_management/bloc/community_management_state.dart @@ -0,0 +1,100 @@ +part of 'community_management_bloc.dart'; + +enum CommunityManagementTab { engagements, reports, appReviews } + +enum CommunityManagementStatus { initial, loading, success, failure } + +class CommunityManagementState extends Equatable { + const CommunityManagementState({ + this.activeTab = CommunityManagementTab.engagements, + this.engagementsStatus = CommunityManagementStatus.initial, + this.reportsStatus = CommunityManagementStatus.initial, + this.appReviewsStatus = CommunityManagementStatus.initial, + this.engagements = const [], + this.reports = const [], + this.appReviews = const [], + this.engagementsCursor, + this.reportsCursor, + this.appReviewsCursor, + this.hasMoreEngagements = true, + this.hasMoreReports = true, + this.hasMoreAppReviews = true, + this.exception, + }); + + final CommunityManagementTab activeTab; + final CommunityManagementStatus engagementsStatus; + final CommunityManagementStatus reportsStatus; + final CommunityManagementStatus appReviewsStatus; + final List engagements; + final List reports; + final List appReviews; + final String? engagementsCursor; + final String? reportsCursor; + final String? appReviewsCursor; + final bool hasMoreEngagements; + final bool hasMoreReports; + final bool hasMoreAppReviews; + final HttpException? exception; + + CommunityManagementState copyWith({ + CommunityManagementTab? activeTab, + CommunityManagementStatus? engagementsStatus, + CommunityManagementStatus? reportsStatus, + CommunityManagementStatus? appReviewsStatus, + List? engagements, + List? reports, + List? appReviews, + String? engagementsCursor, + String? reportsCursor, + String? appReviewsCursor, + bool? hasMoreEngagements, + bool? hasMoreReports, + bool? hasMoreAppReviews, + HttpException? exception, + bool forceEngagementsCursor = false, + bool forceReportsCursor = false, + bool forceAppReviewsCursor = false, + }) { + return CommunityManagementState( + activeTab: activeTab ?? this.activeTab, + engagementsStatus: engagementsStatus ?? this.engagementsStatus, + reportsStatus: reportsStatus ?? this.reportsStatus, + appReviewsStatus: appReviewsStatus ?? this.appReviewsStatus, + engagements: engagements ?? this.engagements, + reports: reports ?? this.reports, + appReviews: appReviews ?? this.appReviews, + engagementsCursor: forceEngagementsCursor + ? engagementsCursor + : engagementsCursor ?? this.engagementsCursor, + reportsCursor: forceReportsCursor + ? reportsCursor + : reportsCursor ?? this.reportsCursor, + appReviewsCursor: forceAppReviewsCursor + ? appReviewsCursor + : appReviewsCursor ?? this.appReviewsCursor, + hasMoreEngagements: hasMoreEngagements ?? this.hasMoreEngagements, + hasMoreReports: hasMoreReports ?? this.hasMoreReports, + hasMoreAppReviews: hasMoreAppReviews ?? this.hasMoreAppReviews, + exception: exception ?? this.exception, + ); + } + + @override + List get props => [ + activeTab, + engagementsStatus, + reportsStatus, + appReviewsStatus, + engagements, + reports, + appReviews, + engagementsCursor, + reportsCursor, + appReviewsCursor, + hasMoreEngagements, + hasMoreReports, + hasMoreAppReviews, + exception, + ]; +} From 5719aa8531096c9d201fce1b563d0aa1571679c1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:52:41 +0100 Subject: [PATCH 020/130] feat(community_management): add community filter events - Create CommunityFilterEvent abstract class - Implement CommunityFilterApplied event with filter parameters - Implement CommunityFilterReset event for resetting filters --- .../community_filter_event.dart | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 lib/community_management/bloc/community_filter/community_filter_event.dart diff --git a/lib/community_management/bloc/community_filter/community_filter_event.dart b/lib/community_management/bloc/community_filter/community_filter_event.dart new file mode 100644 index 00000000..54bbb7e0 --- /dev/null +++ b/lib/community_management/bloc/community_filter/community_filter_event.dart @@ -0,0 +1,37 @@ +part of 'community_filter_bloc.dart'; + +abstract class CommunityFilterEvent extends Equatable { + const CommunityFilterEvent(); + + @override + List get props => []; +} + +class CommunityFilterApplied extends CommunityFilterEvent { + const CommunityFilterApplied({ + this.searchQuery = '', + this.selectedCommentStatus = const [], + this.selectedReportStatus = const [], + this.selectedReportableEntity = const [], + this.selectedInitialFeedback = const [], + }); + + final String searchQuery; + final List selectedCommentStatus; + final List selectedReportStatus; + final List selectedReportableEntity; + final List selectedInitialFeedback; + + @override + List get props => [ + searchQuery, + selectedCommentStatus, + selectedReportStatus, + selectedReportableEntity, + selectedInitialFeedback, + ]; +} + +class CommunityFilterReset extends CommunityFilterEvent { + const CommunityFilterReset(); +} From 0f374d485a4919150fa90a34e4ea2d17f1bc01ac Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:53:34 +0100 Subject: [PATCH 021/130] feat(community_management): add CommunityFilterState class - Create a new CommunityFilterState class for managing community filter states - Include properties for search query, comment status, report status, reportable entity, and initial feedback - Implement copyWith method for state immutability - Override props for Equatable comparison --- .../community_filter_state.dart | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 lib/community_management/bloc/community_filter/community_filter_state.dart diff --git a/lib/community_management/bloc/community_filter/community_filter_state.dart b/lib/community_management/bloc/community_filter/community_filter_state.dart new file mode 100644 index 00000000..e5fe8801 --- /dev/null +++ b/lib/community_management/bloc/community_filter/community_filter_state.dart @@ -0,0 +1,45 @@ +part of 'community_filter_bloc.dart'; + +class CommunityFilterState extends Equatable { + const CommunityFilterState({ + this.searchQuery = '', + this.selectedCommentStatus = const [], + this.selectedReportStatus = const [], + this.selectedReportableEntity = const [], + this.selectedInitialFeedback = const [], + }); + + final String searchQuery; + final List selectedCommentStatus; + final List selectedReportStatus; + final List selectedReportableEntity; + final List selectedInitialFeedback; + + CommunityFilterState copyWith({ + String? searchQuery, + List? selectedCommentStatus, + List? selectedReportStatus, + List? selectedReportableEntity, + List? selectedInitialFeedback, + }) { + return CommunityFilterState( + searchQuery: searchQuery ?? this.searchQuery, + selectedCommentStatus: + selectedCommentStatus ?? this.selectedCommentStatus, + selectedReportStatus: selectedReportStatus ?? this.selectedReportStatus, + selectedReportableEntity: + selectedReportableEntity ?? this.selectedReportableEntity, + selectedInitialFeedback: + selectedInitialFeedback ?? this.selectedInitialFeedback, + ); + } + + @override + List get props => [ + searchQuery, + selectedCommentStatus, + selectedReportStatus, + selectedReportableEntity, + selectedInitialFeedback, + ]; +} From a411b198f099541861938165bb693b3af924dc85 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:54:24 +0100 Subject: [PATCH 022/130] feat(community_management): add community filter dialog events - Define abstract CommunityFilterDialogEvent class - Implement various event classes for community filter dialog interactions - Add events for initialization, search query change, comment status change, report status change, reportable entity change, initial feedback change, and reset --- .../bloc/community_filter_dialog_event.dart | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart new file mode 100644 index 00000000..2ee24761 --- /dev/null +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart @@ -0,0 +1,75 @@ +part of 'community_filter_dialog_bloc.dart'; + +abstract class CommunityFilterDialogEvent extends Equatable { + const CommunityFilterDialogEvent(); + + @override + List get props => []; +} + +class CommunityFilterDialogInitialized extends CommunityFilterDialogEvent { + const CommunityFilterDialogInitialized({ + required this.activeTab, + required this.communityFilterState, + }); + + final CommunityManagementTab activeTab; + final CommunityFilterState communityFilterState; + + @override + List get props => [activeTab, communityFilterState]; +} + +class CommunityFilterDialogSearchQueryChanged + extends CommunityFilterDialogEvent { + const CommunityFilterDialogSearchQueryChanged(this.query); + + final String query; + + @override + List get props => [query]; +} + +class CommunityFilterDialogCommentStatusChanged + extends CommunityFilterDialogEvent { + const CommunityFilterDialogCommentStatusChanged(this.commentStatus); + + final List commentStatus; + + @override + List get props => [commentStatus]; +} + +class CommunityFilterDialogReportStatusChanged + extends CommunityFilterDialogEvent { + const CommunityFilterDialogReportStatusChanged(this.reportStatus); + + final List reportStatus; + + @override + List get props => [reportStatus]; +} + +class CommunityFilterDialogReportableEntityChanged + extends CommunityFilterDialogEvent { + const CommunityFilterDialogReportableEntityChanged(this.reportableEntity); + + final List reportableEntity; + + @override + List get props => [reportableEntity]; +} + +class CommunityFilterDialogInitialFeedbackChanged + extends CommunityFilterDialogEvent { + const CommunityFilterDialogInitialFeedbackChanged(this.initialFeedback); + + final List initialFeedback; + + @override + List get props => [initialFeedback]; +} + +class CommunityFilterDialogReset extends CommunityFilterDialogEvent { + const CommunityFilterDialogReset(); +} From 34f0787a0df82dcbe389ad35260c5015d98562e0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 08:55:17 +0100 Subject: [PATCH 023/130] feat(lib): add CommunityFilterDialogState for community management - Create new state class for community filter dialog - Include properties for active tab, search query, and selected filter options - Implement copyWith method for state immutability - Extend Equatable for optimized performance --- .../bloc/community_filter_dialog_state.dart | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart new file mode 100644 index 00000000..7ffd01e2 --- /dev/null +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart @@ -0,0 +1,49 @@ +part of 'community_filter_dialog_bloc.dart'; + +class CommunityFilterDialogState extends Equatable { + const CommunityFilterDialogState({ + required this.activeTab, + this.searchQuery = '', + this.selectedCommentStatus = const [], + this.selectedReportStatus = const [], + this.selectedReportableEntity = const [], + this.selectedInitialFeedback = const [], + }); + + final CommunityManagementTab activeTab; + final String searchQuery; + final List selectedCommentStatus; + final List selectedReportStatus; + final List selectedReportableEntity; + final List selectedInitialFeedback; + + CommunityFilterDialogState copyWith({ + String? searchQuery, + List? selectedCommentStatus, + List? selectedReportStatus, + List? selectedReportableEntity, + List? selectedInitialFeedback, + }) { + return CommunityFilterDialogState( + activeTab: activeTab, + searchQuery: searchQuery ?? this.searchQuery, + selectedCommentStatus: + selectedCommentStatus ?? this.selectedCommentStatus, + selectedReportStatus: selectedReportStatus ?? this.selectedReportStatus, + selectedReportableEntity: + selectedReportableEntity ?? this.selectedReportableEntity, + selectedInitialFeedback: + selectedInitialFeedback ?? this.selectedInitialFeedback, + ); + } + + @override + List get props => [ + activeTab, + searchQuery, + selectedCommentStatus, + selectedReportStatus, + selectedReportableEntity, + selectedInitialFeedback, + ]; +} From 002ea2f3cbf5d8ce050dab7265f8d34849a74d19 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 09:06:54 +0100 Subject: [PATCH 024/130] feat(router): add community filter dialog route - Import necessary packages for community management - Add route constant for community filter dialog --- lib/router/router.dart | 2 ++ lib/router/routes.dart | 3 +++ 2 files changed, 5 insertions(+) diff --git a/lib/router/router.dart b/lib/router/router.dart index 809a66da..de94d455 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -13,7 +13,9 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/v import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/view/email_code_verification_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/view/request_code_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/view/community_management_page.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_filter_dialog/community_filter_dialog.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/content_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/headlines_filter/headlines_filter_bloc.dart'; diff --git a/lib/router/routes.dart b/lib/router/routes.dart index da753635..679308b3 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -122,6 +122,9 @@ abstract final class Routes { /// The name for the community management section route. static const String communityManagementName = 'communityManagement'; + /// The name for the community filter dialog route. + static const String communityFilterDialog = 'community-filter-dialog'; + /// The name for the community filter dialog route. static const String communityFilterDialogName = 'communityFilterDialog'; } From 5f0f974fcca7982abd6b8f448d7b6dfade7cb439 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 09:07:28 +0100 Subject: [PATCH 025/130] style: format --- lib/app/view/app.dart | 3 +-- lib/community_management/bloc/community_management_bloc.dart | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index acefee3d..921f9849 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -182,8 +182,7 @@ class App extends StatelessWidget { ), BlocProvider( create: (context) => CommunityManagementBloc( - engagementsRepository: - context.read>(), + engagementsRepository: context.read>(), reportsRepository: context.read>(), appReviewsRepository: context.read>(), communityFilterBloc: context.read(), diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 0bb37128..1bad59cc 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -7,7 +7,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; import 'package:logging/logging.dart'; -import 'package:ui_kit/ui_kit.dart'; part 'community_management_event.dart'; part 'community_management_state.dart'; From 1d8cfa9361b567d3233adcae58b517ea56cb9b0c Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 09:44:42 +0100 Subject: [PATCH 026/130] feat(community_management): enhance logging and error handling - Add logging for entity updates and list reloads - Implement proper error handling for HttpExceptions - Refactor entity update subscription using StreamController - Add logging before loading engagements, reports, and app reviews - Improve error handling by logging exceptions and updating state accordingly --- .../bloc/community_management_bloc.dart | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 1bad59cc..6fb609fe 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -59,12 +59,14 @@ class CommunityManagementBloc }); _entityUpdateSubscription = - Stream.multi([ - _engagementsRepository.entityUpdated, - _reportsRepository.entityUpdated, - _appReviewsRepository.entityUpdated, - ]).listen((updatedType) { + Stream.multi((controller) { + controller + ..addStream(_engagementsRepository.entityUpdated) + ..addStream(_reportsRepository.entityUpdated) + ..addStream(_appReviewsRepository.entityUpdated); + }).listen((updatedType) { if (updatedType == Engagement) { + _logger.info('Engagement updated, reloading engagements list.'); add( LoadEngagementsRequested( filter: buildEngagementsFilterMap(_communityFilterBloc.state), @@ -72,6 +74,7 @@ class CommunityManagementBloc ), ); } else if (updatedType == Report) { + _logger.info('Report updated, reloading reports list.'); add( LoadReportsRequested( filter: buildReportsFilterMap(_communityFilterBloc.state), @@ -79,6 +82,7 @@ class CommunityManagementBloc ), ); } else if (updatedType == AppReview) { + _logger.info('AppReview updated, reloading app reviews list.'); add( LoadAppReviewsRequested( filter: buildAppReviewsFilterMap(_communityFilterBloc.state), @@ -164,6 +168,7 @@ class CommunityManagementBloc LoadEngagementsRequested event, Emitter emit, ) async { + _logger.info('Loading engagements with filter: ${event.filter}'); emit( state.copyWith( engagementsStatus: CommunityManagementStatus.loading, @@ -192,7 +197,8 @@ class CommunityManagementBloc forceEngagementsCursor: true, ), ); - } on HtHttpException catch (e) { + } on HttpException catch (e) { + _logger.severe('Failed to load engagements', e); emit( state.copyWith( engagementsStatus: CommunityManagementStatus.failure, @@ -206,6 +212,7 @@ class CommunityManagementBloc LoadReportsRequested event, Emitter emit, ) async { + _logger.info('Loading reports with filter: ${event.filter}'); emit( state.copyWith( reportsStatus: CommunityManagementStatus.loading, @@ -234,7 +241,8 @@ class CommunityManagementBloc forceReportsCursor: true, ), ); - } on HtHttpException catch (e) { + } on HttpException catch (e) { + _logger.severe('Failed to load reports', e); emit( state.copyWith( reportsStatus: CommunityManagementStatus.failure, @@ -248,6 +256,7 @@ class CommunityManagementBloc LoadAppReviewsRequested event, Emitter emit, ) async { + _logger.info('Loading app reviews with filter: ${event.filter}'); emit( state.copyWith( appReviewsStatus: CommunityManagementStatus.loading, @@ -276,7 +285,8 @@ class CommunityManagementBloc forceAppReviewsCursor: true, ), ); - } on HtHttpException catch (e) { + } on HttpException catch (e) { + _logger.severe('Failed to load app reviews', e); emit( state.copyWith( appReviewsStatus: CommunityManagementStatus.failure, From 4da7242afcee8bb59386a4d993664d43e5c00cf4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 09:46:44 +0100 Subject: [PATCH 027/130] refactor(community_management): remove unused PendingDeletionsService - Remove import of PendingDeletionsService - Remove PendingDeletionsService from CommunityManagementBloc constructor - Remove _pendingDeletionsService field from CommunityManagementBloc --- lib/app/view/app.dart | 1 - lib/community_management/bloc/community_management_bloc.dart | 4 ---- 2 files changed, 5 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 921f9849..31a2f2fc 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -186,7 +186,6 @@ class App extends StatelessWidget { reportsRepository: context.read>(), appReviewsRepository: context.read>(), communityFilterBloc: context.read(), - pendingDeletionsService: context.read(), ), ), ], diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 6fb609fe..4c813843 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -5,7 +5,6 @@ import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; import 'package:logging/logging.dart'; part 'community_management_event.dart'; @@ -18,13 +17,11 @@ class CommunityManagementBloc required DataRepository reportsRepository, required DataRepository appReviewsRepository, required CommunityFilterBloc communityFilterBloc, - required PendingDeletionsService pendingDeletionsService, Logger? logger, }) : _engagementsRepository = engagementsRepository, _reportsRepository = reportsRepository, _appReviewsRepository = appReviewsRepository, _communityFilterBloc = communityFilterBloc, - _pendingDeletionsService = pendingDeletionsService, _logger = logger ?? Logger('CommunityManagementBloc'), super(const CommunityManagementState()) { on(_onTabChanged); @@ -97,7 +94,6 @@ class CommunityManagementBloc final DataRepository _reportsRepository; final DataRepository _appReviewsRepository; final CommunityFilterBloc _communityFilterBloc; - final PendingDeletionsService _pendingDeletionsService; final Logger _logger; late final StreamSubscription _filterSubscription; From 6516074b547a7e07060a5344857d10b5daf42fb8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 09:47:19 +0100 Subject: [PATCH 028/130] feat(localization): add generic 'Yes' and 'No' responses to Arabic and English localizations - Add 'yes' and 'no' translations to app_ar.arb and app_en.arb - Include descriptions for new translations - Maintain consistent formatting with existing translations --- lib/l10n/arb/app_ar.arb | 8 ++++++++ lib/l10n/arb/app_en.arb | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index e203739f..886d6c17 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2532,5 +2532,13 @@ "noReasonProvided": "لم يتم تقديم سبب.", "@noReasonProvided": { "description": "رسالة عند عدم تقديم سبب للتقييم" + }, + "yes": "نعم", + "@yes": { + "description": "رد 'نعم' عام." + }, + "no": "لا", + "@no": { + "description": "رد 'لا' عام." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 2018865f..7fb6ae54 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2528,5 +2528,13 @@ "noReasonProvided": "No reason provided.", "@noReasonProvided": { "description": "Message when no reason for feedback is provided" + }, + "yes": "Yes", + "@yes": { + "description": "A generic 'Yes' response." + }, + "no": "No", + "@no": { + "description": "A generic 'No' response." } } \ No newline at end of file From 2c4d607567479ad4cc07fdf6eae8388c02489e68 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 09:47:30 +0100 Subject: [PATCH 029/130] build(serialization): sync --- lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_ar.dart | 6 ++++++ lib/l10n/app_localizations_en.dart | 6 ++++++ 3 files changed, 24 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7c44f107..485b085a 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3745,6 +3745,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'No reason provided.'** String get noReasonProvided; + + /// A generic 'Yes' response. + /// + /// In en, this message translates to: + /// **'Yes'** + String get yes; + + /// A generic 'No' response. + /// + /// In en, this message translates to: + /// **'No'** + String get no; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 688d49d9..5451b7a4 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -2010,4 +2010,10 @@ class AppLocalizationsAr extends AppLocalizations { @override String get noReasonProvided => 'لم يتم تقديم سبب.'; + + @override + String get yes => 'نعم'; + + @override + String get no => 'لا'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 68d107c3..ce45a399 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2016,4 +2016,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get noReasonProvided => 'No reason provided.'; + + @override + String get yes => 'Yes'; + + @override + String get no => 'No'; } From a64b6e39378235025db4a8a1314099d0431989f0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:02:38 +0100 Subject: [PATCH 030/130] feat(l10n): add admin moderation and comment management translations - Add new translations for admin-centric comment statuses and report reasons - Include translations for moderation actions like rejecting comments - Add translations for searching and viewing user feedback history - Provide translations for copying IDs and confirmation messages - Contribute Arabic and English translations for these new strings --- lib/l10n/arb/app_ar.arb | 96 +++++++++++++++++++++++++++++++++++++++++ lib/l10n/arb/app_en.arb | 96 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 886d6c17..4deb539e 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2540,5 +2540,101 @@ "no": "لا", "@no": { "description": "رد 'لا' عام." + }, + "commentStatusFlaggedByAI": "تم الإبلاغ بواسطة الذكاء الاصطناعي", + "@commentStatusFlaggedByAI": { + "description": "حالة إدارية لتعليق تم الإبلاغ عنه تلقائيًا بواسطة الذكاء الاصطناعي." + }, + "commentStatusHiddenByUser": "مخفي بواسطة المستخدم", + "@commentStatusHiddenByUser": { + "description": "حالة إدارية لتعليق أخفاه كاتبه." + }, + "reportReasonMisinformationOrFakeNews": "معلومات مضللة / أخبار كاذبة", + "@reportReasonMisinformationOrFakeNews": { + "description": "سبب البلاغ: المحتوى معلومات مضللة أو أخبار كاذبة." + }, + "reportReasonClickbaitTitle": "عنوان مضلل", + "@reportReasonClickbaitTitle": { + "description": "سبب البلاغ: العنوان الرئيسي مضلل." + }, + "reportReasonOffensiveOrHateSpeech": "محتوى مسيء / خطاب كراهية", + "@reportReasonOffensiveOrHateSpeech": { + "description": "سبب البلاغ: المحتوى مسيء أو يتضمن خطاب كراهية." + }, + "reportReasonSpamOrScam": "بريد مزعج / احتيال", + "@reportReasonSpamOrScam": { + "description": "سبب البلاغ: المحتوى بريد مزعج أو احتيال." + }, + "reportReasonBrokenLink": "رابط معطل", + "@reportReasonBrokenLink": { + "description": "سبب البلاغ: الرابط في المحتوى معطل." + }, + "reportReasonPaywalled": "يتطلب اشتراكًا مدفوعًا", + "@reportReasonPaywalled": { + "description": "سبب البلاغ: المحتوى يتطلب اشتراكًا مدفوعًا." + }, + "reportReasonLowQualityJournalism": "صحافة منخفضة الجودة", + "@reportReasonLowQualityJournalism": { + "description": "سبب البلاغ: المصدر يقدم صحافة منخفضة الجودة." + }, + "reportReasonHighAdDensity": "كثافة إعلانات عالية", + "@reportReasonHighAdDensity": { + "description": "سبب البلاغ: المصدر يحتوي على كثافة عالية من الإعلانات." + }, + "reportReasonBlog": "مدونة", + "@reportReasonBlog": { + "description": "سبب البلاغ: المصدر مدونة." + }, + "reportReasonGovernmentSource": "مصدر حكومي", + "@reportReasonGovernmentSource": { + "description": "سبب البلاغ: المصدر جهة حكومية." + }, + "reportReasonAggregator": "مجمع أخبار", + "@reportReasonAggregator": { + "description": "سبب البلاغ: المصدر مجمع أخبار." + }, + "reportReasonOther": "آخر", + "@reportReasonOther": { + "description": "سبب البلاغ: آخر، غير محدد." + }, + "reportReasonFrequentPaywalls": "اشتراكات مدفوعة متكررة", + "@reportReasonFrequentPaywalls": { + "description": "سبب البلاغ: المصدر يستخدم اشتراكات مدفوعة بشكل متكرر." + }, + "reportReasonImpersonation": "انتحال شخصية", + "@reportReasonImpersonation": { + "description": "سبب البلاغ: المصدر ينتحل شخصية جهة أخرى." + }, + "copyHeadlineId": "نسخ معرّف العنوان", + "@copyHeadlineId": { + "description": "نص عنصر القائمة لنسخ معرّف العنوان الرئيسي." + }, + "copyReportedItemId": "نسخ معرّف العنصر المُبلغ عنه", + "@copyReportedItemId": { + "description": "نص عنصر القائمة لنسخ معرّف العنصر المُبلغ عنه." + }, + "rejectCommentConfirmation": "سيؤدي رفض هذا التعليق إلى تمييزه بشكل دائم على أنه 'مرفوض' وإخفائه عن العرض العام. هذا الإجراء لا يحذف التفاعل الأصلي. هل أنت متأكد من أنك تريد المتابعة؟", + "@rejectCommentConfirmation": { + "description": "رسالة تأكيد تظهر للمسؤول قبل رفض تعليق مستخدم." + }, + "searchByUserId": "ابحث باستخدام معرّف المستخدم...", + "@searchByUserId": { + "description": "نص تلميحي لحقل البحث في مربعات حوار التصفية، يحدد البحث باستخدام معرّف المستخدم." + }, + "noNegativeFeedbackHistory": "لم يتم العثور على سجل تقييمات سلبية لهذا المستخدم.", + "@noNegativeFeedbackHistory": { + "description": "رسالة تظهر في مربع حوار سجل التقييمات عند عدم وجود سجل." + }, + "userIdCopied": "تم نسخ معرّف المستخدم إلى الحافظة.", + "@userIdCopied": { + "description": "رسالة Snackbar تؤكد أنه تم نسخ معرّف المستخدم." + }, + "reject": "رفض", + "@reject": { + "description": "نص زر التأكيد لإجراء الرفض." + }, + "commentStatusFlaggedByAi": "تم الإبلاغ بواسطة الذكاء الاصطناعي", + "@commentStatusFlaggedByAi": { + "description": "حالة إدارية لتعليق تم الإبلاغ عنه تلقائيًا بواسطة الذكاء الاصطناعي." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7fb6ae54..2b9f8859 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2536,5 +2536,101 @@ "no": "No", "@no": { "description": "A generic 'No' response." + }, + "commentStatusFlaggedByAI": "Flagged by AI", + "@commentStatusFlaggedByAI": { + "description": "Admin-centric status for a comment automatically flagged by AI." + }, + "commentStatusHiddenByUser": "Hidden by User", + "@commentStatusHiddenByUser": { + "description": "Admin-centric status for a comment hidden by its author." + }, + "reportReasonMisinformationOrFakeNews": "Misinformation / Fake News", + "@reportReasonMisinformationOrFakeNews": { + "description": "Report reason: The content is misinformation or fake news." + }, + "reportReasonClickbaitTitle": "Clickbait Title", + "@reportReasonClickbaitTitle": { + "description": "Report reason: The headline is clickbait." + }, + "reportReasonOffensiveOrHateSpeech": "Offensive / Hate Speech", + "@reportReasonOffensiveOrHateSpeech": { + "description": "Report reason: The content is offensive or hate speech." + }, + "reportReasonSpamOrScam": "Spam / Scam", + "@reportReasonSpamOrScam": { + "description": "Report reason: The content is spam or a scam." + }, + "reportReasonBrokenLink": "Broken Link", + "@reportReasonBrokenLink": { + "description": "Report reason: The link in the content is broken." + }, + "reportReasonPaywalled": "Paywalled", + "@reportReasonPaywalled": { + "description": "Report reason: The content is behind a paywall." + }, + "reportReasonLowQualityJournalism": "Low Quality Journalism", + "@reportReasonLowQualityJournalism": { + "description": "Report reason: The source exhibits low-quality journalism." + }, + "reportReasonHighAdDensity": "High Ad Density", + "@reportReasonHighAdDensity": { + "description": "Report reason: The source has a high density of ads." + }, + "reportReasonBlog": "Blog", + "@reportReasonBlog": { + "description": "Report reason: The source is a blog." + }, + "reportReasonGovernmentSource": "Government Source", + "@reportReasonGovernmentSource": { + "description": "Report reason: The source is a government entity." + }, + "reportReasonAggregator": "Aggregator", + "@reportReasonAggregator": { + "description": "Report reason: The source is a news aggregator." + }, + "reportReasonOther": "Other", + "@reportReasonOther": { + "description": "Report reason: Other, not specified." + }, + "reportReasonFrequentPaywalls": "Frequent Paywalls", + "@reportReasonFrequentPaywalls": { + "description": "Report reason: The source frequently uses paywalls." + }, + "reportReasonImpersonation": "Impersonation", + "@reportReasonImpersonation": { + "description": "Report reason: The source is impersonating another entity." + }, + "copyHeadlineId": "Copy Headline ID", + "@copyHeadlineId": { + "description": "Menu item text to copy the ID of a headline." + }, + "copyReportedItemId": "Copy Reported Item ID", + "@copyReportedItemId": { + "description": "Menu item text to copy the ID of a reported item." + }, + "rejectCommentConfirmation": "Rejecting this comment will permanently mark it as 'Rejected' and hide it from public view. This action does not delete the parent engagement. Are you sure you want to proceed?", + "@rejectCommentConfirmation": { + "description": "Confirmation message shown to an admin before they reject a user's comment." + }, + "searchByUserId": "Search by User ID...", + "@searchByUserId": { + "description": "Hint text for the search input field in filter dialogs, specifying to search by User ID." + }, + "noNegativeFeedbackHistory": "No negative feedback history found for this user.", + "@noNegativeFeedbackHistory": { + "description": "Message displayed in the feedback history dialog when there is no history." + }, + "userIdCopied": "User ID copied to clipboard.", + "@userIdCopied": { + "description": "Snackbar message confirming that the user ID has been copied." + }, + "reject": "Reject", + "@reject": { + "description": "Confirmation button text for a reject action." + }, + "commentStatusFlaggedByAi": "Flagged by AI", + "@commentStatusFlaggedByAi": { + "description": "Admin-centric status for a comment automatically flagged by AI." } } \ No newline at end of file From ebe1112d022a5c12cac648f414e0032b6febcb46 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:03:15 +0100 Subject: [PATCH 031/130] build(l10n): sync --- lib/l10n/app_localizations.dart | 140 ++++++++++++++++++++++++++++- lib/l10n/app_localizations_ar.dart | 74 ++++++++++++++- lib/l10n/app_localizations_en.dart | 72 +++++++++++++++ 3 files changed, 284 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 485b085a..9e94dd15 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3692,7 +3692,7 @@ abstract class AppLocalizations { /// **'Loading App Reviews'** String get loadingAppReviews; - /// Message when user ID is copied + /// Snackbar message confirming that the user ID has been copied. /// /// In en, this message translates to: /// **'User ID copied to clipboard.'** @@ -3757,6 +3757,144 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'No'** String get no; + + /// Admin-centric status for a comment automatically flagged by AI. + /// + /// In en, this message translates to: + /// **'Flagged by AI'** + String get commentStatusFlaggedByAI; + + /// Admin-centric status for a comment hidden by its author. + /// + /// In en, this message translates to: + /// **'Hidden by User'** + String get commentStatusHiddenByUser; + + /// Report reason: The content is misinformation or fake news. + /// + /// In en, this message translates to: + /// **'Misinformation / Fake News'** + String get reportReasonMisinformationOrFakeNews; + + /// Report reason: The headline is clickbait. + /// + /// In en, this message translates to: + /// **'Clickbait Title'** + String get reportReasonClickbaitTitle; + + /// Report reason: The content is offensive or hate speech. + /// + /// In en, this message translates to: + /// **'Offensive / Hate Speech'** + String get reportReasonOffensiveOrHateSpeech; + + /// Report reason: The content is spam or a scam. + /// + /// In en, this message translates to: + /// **'Spam / Scam'** + String get reportReasonSpamOrScam; + + /// Report reason: The link in the content is broken. + /// + /// In en, this message translates to: + /// **'Broken Link'** + String get reportReasonBrokenLink; + + /// Report reason: The content is behind a paywall. + /// + /// In en, this message translates to: + /// **'Paywalled'** + String get reportReasonPaywalled; + + /// Report reason: The source exhibits low-quality journalism. + /// + /// In en, this message translates to: + /// **'Low Quality Journalism'** + String get reportReasonLowQualityJournalism; + + /// Report reason: The source has a high density of ads. + /// + /// In en, this message translates to: + /// **'High Ad Density'** + String get reportReasonHighAdDensity; + + /// Report reason: The source is a blog. + /// + /// In en, this message translates to: + /// **'Blog'** + String get reportReasonBlog; + + /// Report reason: The source is a government entity. + /// + /// In en, this message translates to: + /// **'Government Source'** + String get reportReasonGovernmentSource; + + /// Report reason: The source is a news aggregator. + /// + /// In en, this message translates to: + /// **'Aggregator'** + String get reportReasonAggregator; + + /// Report reason: Other, not specified. + /// + /// In en, this message translates to: + /// **'Other'** + String get reportReasonOther; + + /// Report reason: The source frequently uses paywalls. + /// + /// In en, this message translates to: + /// **'Frequent Paywalls'** + String get reportReasonFrequentPaywalls; + + /// Report reason: The source is impersonating another entity. + /// + /// In en, this message translates to: + /// **'Impersonation'** + String get reportReasonImpersonation; + + /// Menu item text to copy the ID of a headline. + /// + /// In en, this message translates to: + /// **'Copy Headline ID'** + String get copyHeadlineId; + + /// Menu item text to copy the ID of a reported item. + /// + /// In en, this message translates to: + /// **'Copy Reported Item ID'** + String get copyReportedItemId; + + /// Confirmation message shown to an admin before they reject a user's comment. + /// + /// In en, this message translates to: + /// **'Rejecting this comment will permanently mark it as \'Rejected\' and hide it from public view. This action does not delete the parent engagement. Are you sure you want to proceed?'** + String get rejectCommentConfirmation; + + /// Hint text for the search input field in filter dialogs, specifying to search by User ID. + /// + /// In en, this message translates to: + /// **'Search by User ID...'** + String get searchByUserId; + + /// Message displayed in the feedback history dialog when there is no history. + /// + /// In en, this message translates to: + /// **'No negative feedback history found for this user.'** + String get noNegativeFeedbackHistory; + + /// Confirmation button text for a reject action. + /// + /// In en, this message translates to: + /// **'Reject'** + String get reject; + + /// Admin-centric status for a comment automatically flagged by AI. + /// + /// In en, this message translates to: + /// **'Flagged by AI'** + String get commentStatusFlaggedByAi; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 5451b7a4..37177538 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1979,7 +1979,7 @@ class AppLocalizationsAr extends AppLocalizations { String get loadingAppReviews => 'جاري تحميل مراجعات التطبيق'; @override - String get userIdCopied => 'تم نسخ معرف المستخدم إلى الحافظة.'; + String get userIdCopied => 'تم نسخ معرّف المستخدم إلى الحافظة.'; @override String get commentApproved => 'تمت الموافقة على التعليق.'; @@ -2016,4 +2016,76 @@ class AppLocalizationsAr extends AppLocalizations { @override String get no => 'لا'; + + @override + String get commentStatusFlaggedByAI => 'تم الإبلاغ بواسطة الذكاء الاصطناعي'; + + @override + String get commentStatusHiddenByUser => 'مخفي بواسطة المستخدم'; + + @override + String get reportReasonMisinformationOrFakeNews => + 'معلومات مضللة / أخبار كاذبة'; + + @override + String get reportReasonClickbaitTitle => 'عنوان مضلل'; + + @override + String get reportReasonOffensiveOrHateSpeech => 'محتوى مسيء / خطاب كراهية'; + + @override + String get reportReasonSpamOrScam => 'بريد مزعج / احتيال'; + + @override + String get reportReasonBrokenLink => 'رابط معطل'; + + @override + String get reportReasonPaywalled => 'يتطلب اشتراكًا مدفوعًا'; + + @override + String get reportReasonLowQualityJournalism => 'صحافة منخفضة الجودة'; + + @override + String get reportReasonHighAdDensity => 'كثافة إعلانات عالية'; + + @override + String get reportReasonBlog => 'مدونة'; + + @override + String get reportReasonGovernmentSource => 'مصدر حكومي'; + + @override + String get reportReasonAggregator => 'مجمع أخبار'; + + @override + String get reportReasonOther => 'آخر'; + + @override + String get reportReasonFrequentPaywalls => 'اشتراكات مدفوعة متكررة'; + + @override + String get reportReasonImpersonation => 'انتحال شخصية'; + + @override + String get copyHeadlineId => 'نسخ معرّف العنوان'; + + @override + String get copyReportedItemId => 'نسخ معرّف العنصر المُبلغ عنه'; + + @override + String get rejectCommentConfirmation => + 'سيؤدي رفض هذا التعليق إلى تمييزه بشكل دائم على أنه \'مرفوض\' وإخفائه عن العرض العام. هذا الإجراء لا يحذف التفاعل الأصلي. هل أنت متأكد من أنك تريد المتابعة؟'; + + @override + String get searchByUserId => 'ابحث باستخدام معرّف المستخدم...'; + + @override + String get noNegativeFeedbackHistory => + 'لم يتم العثور على سجل تقييمات سلبية لهذا المستخدم.'; + + @override + String get reject => 'رفض'; + + @override + String get commentStatusFlaggedByAi => 'تم الإبلاغ بواسطة الذكاء الاصطناعي'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ce45a399..b2dc3a37 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2022,4 +2022,76 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no => 'No'; + + @override + String get commentStatusFlaggedByAI => 'Flagged by AI'; + + @override + String get commentStatusHiddenByUser => 'Hidden by User'; + + @override + String get reportReasonMisinformationOrFakeNews => + 'Misinformation / Fake News'; + + @override + String get reportReasonClickbaitTitle => 'Clickbait Title'; + + @override + String get reportReasonOffensiveOrHateSpeech => 'Offensive / Hate Speech'; + + @override + String get reportReasonSpamOrScam => 'Spam / Scam'; + + @override + String get reportReasonBrokenLink => 'Broken Link'; + + @override + String get reportReasonPaywalled => 'Paywalled'; + + @override + String get reportReasonLowQualityJournalism => 'Low Quality Journalism'; + + @override + String get reportReasonHighAdDensity => 'High Ad Density'; + + @override + String get reportReasonBlog => 'Blog'; + + @override + String get reportReasonGovernmentSource => 'Government Source'; + + @override + String get reportReasonAggregator => 'Aggregator'; + + @override + String get reportReasonOther => 'Other'; + + @override + String get reportReasonFrequentPaywalls => 'Frequent Paywalls'; + + @override + String get reportReasonImpersonation => 'Impersonation'; + + @override + String get copyHeadlineId => 'Copy Headline ID'; + + @override + String get copyReportedItemId => 'Copy Reported Item ID'; + + @override + String get rejectCommentConfirmation => + 'Rejecting this comment will permanently mark it as \'Rejected\' and hide it from public view. This action does not delete the parent engagement. Are you sure you want to proceed?'; + + @override + String get searchByUserId => 'Search by User ID...'; + + @override + String get noNegativeFeedbackHistory => + 'No negative feedback history found for this user.'; + + @override + String get reject => 'Reject'; + + @override + String get commentStatusFlaggedByAi => 'Flagged by AI'; } From 88ae3d45b70d8525017962c30c9ae4750b5bcfcb Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:03:44 +0100 Subject: [PATCH 032/130] feat(l10n): add CommentStatus localization extension Introduces a localization extension for CommentStatus enum to provide admin-centric display strings for comment moderation statuses. --- .../extensions/comment_status_extension.dart | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 lib/shared/extensions/comment_status_extension.dart diff --git a/lib/shared/extensions/comment_status_extension.dart b/lib/shared/extensions/comment_status_extension.dart new file mode 100644 index 00000000..d7849c33 --- /dev/null +++ b/lib/shared/extensions/comment_status_extension.dart @@ -0,0 +1,22 @@ +import 'package:core/core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +extension CommentStatusX on CommentStatus { + /// Returns a localized, admin-centric string for the [CommentStatus]. + String l10n(BuildContext context) { + final l10n = context.l10n; + switch (this) { + case CommentStatus.pendingReview: + return l10n.commentStatusPendingReview; + case CommentStatus.approved: + return l10n.commentStatusApproved; + case CommentStatus.rejected: + return l10n.commentStatusRejected; + case CommentStatus.flaggedByAI: + return l10n.commentStatusFlaggedByAI; + case CommentStatus.hiddenByUser: + return l10n.commentStatusHiddenByUser; + } + } +} \ No newline at end of file From bf57fb694846a38760355d0571f6487eb4c510e0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:04:14 +0100 Subject: [PATCH 033/130] feat(l10n): add ReportStatus localization extension Introduces a localization extension for ReportStatus enum to provide admin-centric display strings for report moderation statuses. --- .../extensions/report_reason_extension.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 lib/shared/extensions/report_reason_extension.dart diff --git a/lib/shared/extensions/report_reason_extension.dart b/lib/shared/extensions/report_reason_extension.dart new file mode 100644 index 00000000..7d432534 --- /dev/null +++ b/lib/shared/extensions/report_reason_extension.dart @@ -0,0 +1,50 @@ +import 'package:core/core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +extension ReportReasonX on String { + /// Returns a localized, admin-centric string for a report reason. + /// + /// This extension maps the raw string value of various report reason enums + /// (e.g., [HeadlineReportReason], [SourceReportReason], [CommentReportReason]) + /// to a user-friendly, localized string. + String l10n(BuildContext context) { + final l10n = context.l10n; + switch (this) { + // HeadlineReportReason + case 'misinformationOrFakeNews': + return l10n.reportReasonMisinformationOrFakeNews; + case 'clickbaitTitle': + return l10n.reportReasonClickbaitTitle; + case 'offensiveOrHateSpeech': + return l10n.reportReasonOffensiveOrHateSpeech; + case 'spamOrScam': + return l10n.reportReasonSpamOrScam; + case 'brokenLink': + return l10n.reportReasonBrokenLink; + case 'paywalled': + return l10n.reportReasonPaywalled; + + // SourceReportReason + case 'lowQualityJournalism': + return l10n.reportReasonLowQualityJournalism; + case 'highAdDensity': + return l10n.reportReasonHighAdDensity; + case 'blog': + return l10n.reportReasonBlog; + case 'governmentSource': + return l10n.reportReasonGovernmentSource; + case 'aggregator': + return l10n.reportReasonAggregator; + case 'other': + return l10n.reportReasonOther; + case 'frequentPaywalls': + return l10n.reportReasonFrequentPaywalls; + case 'impersonation': + return l10n.reportReasonImpersonation; + + default: + return this; // Fallback to raw string if no localization found + } + } +} \ No newline at end of file From a2d32102f303f46d1d75201d2e102c4f6328f749 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:04:37 +0100 Subject: [PATCH 034/130] feat(l10n): add ReportableEntity localization extension Introduces a localization extension for ReportableEntity enum to provide admin-centric display strings for types of reported content. --- .../reportable_entity_extension.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 lib/shared/extensions/reportable_entity_extension.dart diff --git a/lib/shared/extensions/reportable_entity_extension.dart b/lib/shared/extensions/reportable_entity_extension.dart new file mode 100644 index 00000000..ede19fca --- /dev/null +++ b/lib/shared/extensions/reportable_entity_extension.dart @@ -0,0 +1,18 @@ +import 'package:core/core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +extension ReportableEntityX on ReportableEntity { + /// Returns a localized, admin-centric string for the [ReportableEntity]. + String l10n(BuildContext context) { + final l10n = context.l10n; + switch (this) { + case ReportableEntity.headline: + return l10n.reportableEntityHeadline; + case ReportableEntity.source: + return l10n.reportableEntitySource; + case ReportableEntity.comment: + return l10n.reportableEntityComment; + } + } +} \ No newline at end of file From 1d00d0d989898ebe4332fea82451f1dda268cf1c Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:04:51 +0100 Subject: [PATCH 035/130] feat(l10n): add InitialAppReviewFeedback localization extension Introduces a localization extension for InitialAppReviewFeedback enum to provide admin-centric display strings for app review feedback types. --- .../initial_app_review_feedback_extension.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 lib/shared/extensions/initial_app_review_feedback_extension.dart diff --git a/lib/shared/extensions/initial_app_review_feedback_extension.dart b/lib/shared/extensions/initial_app_review_feedback_extension.dart new file mode 100644 index 00000000..6da20f0c --- /dev/null +++ b/lib/shared/extensions/initial_app_review_feedback_extension.dart @@ -0,0 +1,17 @@ +import 'package:core/core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +extension InitialAppReviewFeedbackX on InitialAppReviewFeedback { + /// Returns a localized, admin-centric string for the + /// [InitialAppReviewFeedback]. + String l10n(BuildContext context) { + final l10n = context.l10n; + switch (this) { + case InitialAppReviewFeedback.positive: + return l10n.initialAppReviewFeedbackPositive; + case InitialAppReviewFeedback.negative: + return l10n.initialAppReviewFeedbackNegative; + } + } +} \ No newline at end of file From 9e97885f1a28c568e9a198fe4cb1d7df792d6a87 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:05:05 +0100 Subject: [PATCH 036/130] feat(l10n): add ReportReason localization extension Introduces a localization extension to map various report reason strings (from HeadlineReportReason, SourceReportReason, CommentReportReason) to admin-centric localized display strings. --- .../extensions/report_status_extension.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 lib/shared/extensions/report_status_extension.dart diff --git a/lib/shared/extensions/report_status_extension.dart b/lib/shared/extensions/report_status_extension.dart new file mode 100644 index 00000000..5ce68bad --- /dev/null +++ b/lib/shared/extensions/report_status_extension.dart @@ -0,0 +1,18 @@ +import 'package:core/core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +extension ReportStatusX on ReportStatus { + /// Returns a localized, admin-centric string for the [ReportStatus]. + String l10n(BuildContext context) { + final l10n = context.l10n; + switch (this) { + case ReportStatus.submitted: + return l10n.reportStatusSubmitted; + case ReportStatus.inReview: + return l10n.reportStatusInReview; + case ReportStatus.resolved: + return l10n.reportStatusResolved; + } + } +} \ No newline at end of file From 4d6cd7dec859379bb0d0afce0c4f59e2490f0acb Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:07:36 +0100 Subject: [PATCH 037/130] refactor(community): make CommunityActionButtons generic and add actions Refactors CommunityActionButtons to be generic for better type safety. Implements 'view content' navigation for engagements and adds 'copy reported item ID' action for reports. Adds tooltips for action clarity. --- .../widgets/community_action_buttons.dart | 135 ++++++++++++++---- 1 file changed, 106 insertions(+), 29 deletions(-) diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index f0955900..ae923ddb 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -4,15 +4,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; +import 'package:go_router/go_router.dart'; +import 'package:ui_kit/ui_kit.dart'; -class CommunityActionButtons extends StatelessWidget { - const CommunityActionButtons({ - required this.item, - required this.l10n, - super.key, - }); +class CommunityActionButtons extends StatelessWidget { + const CommunityActionButtons({required this.item, required this.l10n, super.key}); - final Object item; + final T item; final AppLocalizations l10n; @override @@ -22,7 +21,7 @@ class CommunityActionButtons extends StatelessWidget { if (item is Engagement) { final engagement = item as Engagement; - _buildEngagementActions( + _buildEngagementActions( context, engagement, visibleActions, @@ -30,10 +29,10 @@ class CommunityActionButtons extends StatelessWidget { ); } else if (item is Report) { final report = item as Report; - _buildReportActions(context, report, visibleActions, overflowMenuItems); + _buildReportActions(context, report, visibleActions, overflowMenuItems); } else if (item is AppReview) { final appReview = item as AppReview; - _buildAppReviewActions( + _buildAppReviewActions( context, appReview, visibleActions, @@ -59,7 +58,7 @@ class CommunityActionButtons extends StatelessWidget { return Row(mainAxisSize: MainAxisSize.min, children: visibleActions); } - void _buildEngagementActions( + void _buildEngagementActions( BuildContext context, Engagement engagement, List visibleActions, @@ -72,8 +71,11 @@ class CommunityActionButtons extends StatelessWidget { iconSize: 20, icon: const Icon(Icons.visibility_outlined), tooltip: l10n.viewEngagedContent, - onPressed: () { - // TODO(fulleni): Implement navigation to content + onPressed: () { + context.goNamed( + Routes.editHeadlineName, + pathParameters: {'id': engagement.entityId}, + ); }, ), ); @@ -96,10 +98,10 @@ class CommunityActionButtons extends StatelessWidget { ), ); } - } + } overflowMenuItems.add( PopupMenuItem(value: 'copyUserId', child: Text(l10n.copyUserId)), - ); + ); } void _buildReportActions( @@ -115,8 +117,10 @@ class CommunityActionButtons extends StatelessWidget { iconSize: 20, icon: const Icon(Icons.visibility_outlined), tooltip: l10n.viewReportedItem, - onPressed: () { - // TODO(fulleni): Implement navigation to reported item + onPressed: () { + // TODO(fulleni): Implement navigation to reported item based on entityType + // This will require a more complex routing logic based on ReportableEntity + // For now, it remains unimplemented. }, ), ); @@ -141,7 +145,13 @@ class CommunityActionButtons extends StatelessWidget { overflowMenuItems.add( PopupMenuItem(value: 'copyUserId', child: Text(l10n.copyUserId)), ); - } + overflowMenuItems.add( + PopupMenuItem( + value: 'copyReportedItemId', + child: Text(l10n.copyReportedItemId), + ), + ); + } void _buildAppReviewActions( BuildContext context, @@ -156,8 +166,11 @@ class CommunityActionButtons extends StatelessWidget { iconSize: 20, icon: const Icon(Icons.history), tooltip: l10n.viewFeedbackHistory, - onPressed: () { - // TODO(fulleni): Implement dialog to show feedback history + onPressed: () { + showDialog( + context: context, + builder: (dialogContext) => _FeedbackHistoryDialog(appReview: appReview, l10n: l10n), + ); }, ), ); @@ -168,7 +181,7 @@ class CommunityActionButtons extends StatelessWidget { ); } - void _onActionSelected(BuildContext context, String value, Object item) { + void _onActionSelected(BuildContext context, String value, T item) { final engagementsRepository = context.read>(); final reportsRepository = context.read>(); @@ -187,21 +200,42 @@ class CommunityActionButtons extends StatelessWidget { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.userIdCopied))); + } else if (value == 'copyReportedItemId' && item is Report) { + String reportedItemId; + reportedItemId = item.entityId; + Clipboard.setData(ClipboardData(text: userId)); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(SnackBar(content: Text(l10n.userIdCopied))); } else if (value == 'approveComment' && item is Engagement) { final updatedEngagement = item.copyWith( comment: item.comment?.copyWith(status: CommentStatus.approved), ); engagementsRepository.update( - id: updatedEngagement.id, - item: updatedEngagement, + id: updatedEngagement.id, + item: updatedEngagement, ); } else if (value == 'rejectComment' && item is Engagement) { - final updatedEngagement = item.copyWith( - comment: item.comment?.copyWith(status: CommentStatus.rejected), - ); - engagementsRepository.update( - id: updatedEngagement.id, - item: updatedEngagement, + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(l10n.rejectComment), + content: Text(l10n.rejectCommentConfirmation), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(l10n.cancel), + ), + ElevatedButton( + onPressed: () { + final updatedEngagement = item.copyWith(comment: item.comment?.copyWith(status: CommentStatus.rejected),); + engagementsRepository.update(id: updatedEngagement.id, item: updatedEngagement,); + Navigator.of(dialogContext).pop(); + }, + child: Text(l10n.reject), + ), + ], + ), ); } else if (value == 'markAsInReview' && item is Report) { final updatedReport = item.copyWith(status: ReportStatus.inReview); @@ -218,3 +252,46 @@ class CommunityActionButtons extends StatelessWidget { } } } + +class _FeedbackHistoryDialog extends StatelessWidget { + const _FeedbackHistoryDialog({ + required this.appReview, + required this.l10n, + }); + + final AppReview appReview; + final AppLocalizations l10n; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(l10n.viewFeedbackHistory), + content: SizedBox( + width: double.maxFinite, + child: appReview.negativeFeedbackHistory.isEmpty + ? Text(l10n.noNegativeFeedbackHistory) + : ListView.builder( + shrinkWrap: true, + itemCount: appReview.negativeFeedbackHistory.length, + itemBuilder: (context, index) { + final feedback = appReview.negativeFeedbackHistory[index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: AppSpacing.xs), + child: Padding( + padding: const EdgeInsets.all(AppSpacing.md), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(DateFormatter.formatRelativeTime(context, feedback.providedAt)), + if (feedback.reason != null) Text(feedback.reason!), + ], + ), + ), + ); + }, + ), + ), + actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(l10n.close))], + ); + } +} From 5eeca5b7e64c809de1c4b2083bdb826a359396fe Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:08:30 +0100 Subject: [PATCH 038/130] feat(community): refine EngagementsPage UI Removes redundant 'user' and 'engaged content' columns. Truncates comment text with a tooltip for full view. Applies localized comment status. --- .../view/engagements_page.dart | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index 91aca87f..5fbe0023 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -7,6 +7,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/community_manage import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_action_buttons.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/comment_status_extension.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -114,11 +115,6 @@ class _EngagementsPageState extends State { return PaginatedDataTable2( columns: [ DataColumn2(label: Text(l10n.user), size: ColumnSize.L), - if (!isMobile) - DataColumn2( - label: Text(l10n.engagedContent), - size: ColumnSize.M, - ), DataColumn2( label: Text(l10n.reaction), size: ColumnSize.S, @@ -184,7 +180,7 @@ class _EngagementsPageState extends State { ), ], ); - }, + }, ), ); } @@ -212,11 +208,27 @@ class _EngagementsDataSource extends DataTableSource { return DataRow2( cells: [ DataCell(Text(engagement.userId, overflow: TextOverflow.ellipsis)), - if (!isMobile) DataCell(Text(engagement.entityId)), DataCell(Text(engagement.reaction.reactionType.name)), if (!isMobile) DataCell(Text(engagement.comment?.content ?? l10n.notAvailable)), - DataCell(Text(engagement.comment?.status.name ?? l10n.notAvailable)), + DataCell( + Tooltip( + message: engagement.comment?.content ?? l10n.notAvailable, + child: Text( + engagement.comment?.content != null + ? (engagement.comment!.content.length > 50 + ? '${engagement.comment!.content.substring(0, 47)}...' + : engagement.comment!.content) + : l10n.notAvailable, + overflow: TextOverflow.ellipsis, + ), + ), + ), + DataCell( + Text( + engagement.comment?.status.l10n(context) ?? l10n.notAvailable, + ), + ), if (!isMobile) DataCell( Text( @@ -224,7 +236,7 @@ class _EngagementsDataSource extends DataTableSource { ), ), DataCell(CommunityActionButtons(item: engagement, l10n: l10n)), - ], + ], ); } From 5664e092ea12012649f3e1181a3c20bdfda481d1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:09:03 +0100 Subject: [PATCH 039/130] feat(community): refine ReportsPage UI Removes redundant 'reporter' and 'reported item' columns. Applies localized report status and report reason. Adds subtle UI indicators for report status. --- .../view/reports_page.dart | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index 16f5e7e7..81a40850 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -7,6 +7,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/community_manage import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_action_buttons.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -115,11 +116,6 @@ class _ReportsPageState extends State { label: Text(l10n.reporter), size: ColumnSize.L, ), - if (!isMobile) - DataColumn2( - label: Text(l10n.reportedItem), - size: ColumnSize.L, - ), DataColumn2( label: Text(l10n.reason), size: ColumnSize.M, @@ -208,11 +204,25 @@ class _ReportsDataSource extends DataTableSource { final report = reports[index]; return DataRow2( cells: [ - DataCell(Text(report.reporterUserId, overflow: TextOverflow.ellipsis)), + DataCell( + Text(report.reporterUserId, overflow: TextOverflow.ellipsis), + ), + DataCell(Text(report.reason.l10n(context))), if (!isMobile) - DataCell(Text('${report.entityType.name}: ${report.entityId}')), - DataCell(Text(report.reason)), - if (!isMobile) DataCell(Text(report.status.name)), + DataCell( + Row( + children: [ + Icon( + report.status == ReportStatus.resolved + ? Icons.check_circle_outline + : Icons.info_outline, + color: report.status == ReportStatus.resolved ? Colors.green : Colors.orange, + ), + const SizedBox(width: AppSpacing.xs), + Text(report.status.l10n(context)), + ], + ), + ), if (!isMobile) DataCell( Text(DateFormat('dd-MM-yyyy').format(report.createdAt.toLocal())), From b198acc1f23afa22a7694f146eb2ddb5ea94f2f7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:10:02 +0100 Subject: [PATCH 040/130] feat(community): refine AppReviewsPage UI Removes redundant 'user', 'OS prompt requested', and 'feedback history' columns. --- .../view/app_reviews_page.dart | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/lib/community_management/view/app_reviews_page.dart b/lib/community_management/view/app_reviews_page.dart index 0edc1af5..b5048742 100644 --- a/lib/community_management/view/app_reviews_page.dart +++ b/lib/community_management/view/app_reviews_page.dart @@ -117,16 +117,6 @@ class _AppReviewsPageState extends State { label: Text(l10n.initialFeedback), size: ColumnSize.M, ), - if (!isMobile) - DataColumn2( - label: Text(l10n.osPromptRequested), - size: ColumnSize.S, - ), - if (!isMobile) - DataColumn2( - label: Text(l10n.feedbackHistory), - size: ColumnSize.M, - ), if (!isMobile) DataColumn2( label: Text(l10n.lastInteraction), @@ -206,14 +196,8 @@ class _AppReviewsDataSource extends DataTableSource { final appReview = appReviews[index]; return DataRow2( cells: [ - DataCell(Text(appReview.userId, overflow: TextOverflow.ellipsis)), DataCell(Text(appReview.initialFeedback.name)), - if (!isMobile) - DataCell( - Text(appReview.wasStoreReviewRequested ? l10n.yes : l10n.no), - ), - if (!isMobile) - DataCell(Text('${appReview.negativeFeedbackHistory.length}')), + DataCell(Text(appReview.userId, overflow: TextOverflow.ellipsis)), if (!isMobile) DataCell( Text( From 6cd98be71d3af7b0f68afd4075915f6ebbce22c7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:10:47 +0100 Subject: [PATCH 041/130] feat(community): localize filter dialog dropdowns and update search hint Applies localization to all filter dropdown items and clarifies the search hint to indicate searching by user ID. --- .../community_filter_dialog.dart | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index 5b4ac05f..d616808f 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -6,6 +6,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/community_manage import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/searchable_selection_input.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -141,8 +142,8 @@ class _CommunityFilterDialogState extends State { hintText: l10n.selectCommentStatus, isMultiSelect: true, selectedItems: state.selectedCommentStatus, - itemBuilder: (context, item) => Text(item.name), - itemToString: (item) => item.name, + itemBuilder: (context, item) => Text(item.l10n(context)), + itemToString: (item) => item.l10n(context), onChanged: (items) => context.read().add( CommunityFilterDialogCommentStatusChanged(items ?? []), ), @@ -156,8 +157,8 @@ class _CommunityFilterDialogState extends State { hintText: l10n.selectReportStatus, isMultiSelect: true, selectedItems: state.selectedReportStatus, - itemBuilder: (context, item) => Text(item.name), - itemToString: (item) => item.name, + itemBuilder: (context, item) => Text(item.l10n(context)), + itemToString: (item) => item.l10n(context), onChanged: (items) => context.read().add( CommunityFilterDialogReportStatusChanged(items ?? []), ), @@ -169,8 +170,8 @@ class _CommunityFilterDialogState extends State { hintText: l10n.selectReportableEntity, isMultiSelect: true, selectedItems: state.selectedReportableEntity, - itemBuilder: (context, item) => Text(item.name), - itemToString: (item) => item.name, + itemBuilder: (context, item) => Text(item.l10n(context)), + itemToString: (item) => item.l10n(context), onChanged: (items) => context.read().add( CommunityFilterDialogReportableEntityChanged(items ?? []), ), @@ -184,8 +185,8 @@ class _CommunityFilterDialogState extends State { hintText: l10n.selectInitialFeedback, isMultiSelect: true, selectedItems: state.selectedInitialFeedback, - itemBuilder: (context, item) => Text(item.name), - itemToString: (item) => item.name, + itemBuilder: (context, item) => Text(item.l10n(context)), + itemToString: (item) => item.l10n(context), onChanged: (items) => context.read().add( CommunityFilterDialogInitialFeedbackChanged(items ?? []), ), From 13cdf4ab5a08771ec0b1ac8a6dd4494ce64b8a1f Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:11:34 +0100 Subject: [PATCH 042/130] refactor(community): remove redundant activeTab from CommunityFilterDialogBloc Removes the redundant activeTab parameter from the constructor, relying solely on the initialization event for active tab context. --- .../bloc/community_filter_dialog_bloc.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart index cf2120d6..2f783561 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart @@ -9,8 +9,12 @@ part 'community_filter_dialog_state.dart'; class CommunityFilterDialogBloc extends Bloc { - CommunityFilterDialogBloc({required CommunityManagementTab activeTab}) - : super(CommunityFilterDialogState(activeTab: activeTab)) { + CommunityFilterDialogBloc() + : super( + const CommunityFilterDialogState( + activeTab: CommunityManagementTab.engagements, + ), + ) { on(_onFilterDialogInitialized); on(_onSearchQueryChanged); on(_onCommentStatusChanged); From 25573040dd9dd4d45d79395936b8101df7112f0a Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:12:07 +0100 Subject: [PATCH 043/130] refactor(router): remove unused repository arguments from communityFilterDialog Removes DataRepository instances passed as arguments to the communityFilterDialog route, as they are not directly used by the dialog widget. --- lib/router/router.dart | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/router/router.dart b/lib/router/router.dart index de94d455..779363bb 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -394,22 +394,12 @@ GoRouter createRouter({ final args = state.extra! as Map; final activeTab = args['activeTab'] as CommunityManagementTab; - final engagementsRepository = - args['engagementsRepository'] - as DataRepository; - final reportsRepository = - args['reportsRepository'] as DataRepository; - final appReviewsRepository = - args['appReviewsRepository'] - as DataRepository; return MaterialPage( fullscreenDialog: true, child: BlocProvider( create: (providerContext) => - CommunityFilterDialogBloc( - activeTab: activeTab, - )..add( + CommunityFilterDialogBloc()..add( CommunityFilterDialogInitialized( activeTab: activeTab, communityFilterState: providerContext From 7bbb08da0e80ef957483af5f45b6c51bacd8fab7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:12:16 +0100 Subject: [PATCH 044/130] chore: barrels --- lib/shared/extensions/extensions.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/shared/extensions/extensions.dart b/lib/shared/extensions/extensions.dart index a5956785..3a1468cc 100644 --- a/lib/shared/extensions/extensions.dart +++ b/lib/shared/extensions/extensions.dart @@ -2,10 +2,17 @@ export 'ad_platform_type_l10n.dart'; export 'ad_type_l10n.dart'; export 'app_user_role_l10n.dart'; export 'banner_ad_shape_l10n.dart'; +export 'comment_status_extension.dart'; export 'content_status_l10n.dart'; export 'dashboard_user_role_l10n.dart'; +export 'engagement_mode_l10n.dart'; export 'feed_decorator_type_l10n.dart'; +export 'feed_item_click_behavior_l10n.dart'; +export 'initial_app_review_feedback_extension.dart'; export 'push_notification_provider_l10n.dart'; export 'push_notification_subscription_delivery_type_l10n.dart'; +export 'report_reason_extension.dart'; +export 'report_status_extension.dart'; +export 'reportable_entity_extension.dart'; export 'source_type_l10n.dart'; export 'string_truncate.dart'; From 87f2735ff3bffbd65436911ac83e7512a1e4dc04 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:12:55 +0100 Subject: [PATCH 045/130] style: format --- .../view/engagements_page.dart | 22 +++---- .../view/reports_page.dart | 4 +- .../widgets/community_action_buttons.dart | 59 ++++++++++++++----- .../extensions/comment_status_extension.dart | 2 +- ...initial_app_review_feedback_extension.dart | 2 +- .../extensions/report_reason_extension.dart | 2 +- .../extensions/report_status_extension.dart | 2 +- .../reportable_entity_extension.dart | 2 +- 8 files changed, 62 insertions(+), 33 deletions(-) diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index 5fbe0023..a6e5e384 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -180,7 +180,7 @@ class _EngagementsPageState extends State { ), ], ); - }, + }, ), ); } @@ -211,19 +211,19 @@ class _EngagementsDataSource extends DataTableSource { DataCell(Text(engagement.reaction.reactionType.name)), if (!isMobile) DataCell(Text(engagement.comment?.content ?? l10n.notAvailable)), - DataCell( - Tooltip( - message: engagement.comment?.content ?? l10n.notAvailable, - child: Text( - engagement.comment?.content != null - ? (engagement.comment!.content.length > 50 + DataCell( + Tooltip( + message: engagement.comment?.content ?? l10n.notAvailable, + child: Text( + engagement.comment?.content != null + ? (engagement.comment!.content.length > 50 ? '${engagement.comment!.content.substring(0, 47)}...' : engagement.comment!.content) - : l10n.notAvailable, - overflow: TextOverflow.ellipsis, - ), + : l10n.notAvailable, + overflow: TextOverflow.ellipsis, ), ), + ), DataCell( Text( engagement.comment?.status.l10n(context) ?? l10n.notAvailable, @@ -236,7 +236,7 @@ class _EngagementsDataSource extends DataTableSource { ), ), DataCell(CommunityActionButtons(item: engagement, l10n: l10n)), - ], + ], ); } diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index 81a40850..d91593a1 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -216,7 +216,9 @@ class _ReportsDataSource extends DataTableSource { report.status == ReportStatus.resolved ? Icons.check_circle_outline : Icons.info_outline, - color: report.status == ReportStatus.resolved ? Colors.green : Colors.orange, + color: report.status == ReportStatus.resolved + ? Colors.green + : Colors.orange, ), const SizedBox(width: AppSpacing.xs), Text(report.status.l10n(context)), diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index ae923ddb..aa480091 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -9,7 +9,11 @@ import 'package:go_router/go_router.dart'; import 'package:ui_kit/ui_kit.dart'; class CommunityActionButtons extends StatelessWidget { - const CommunityActionButtons({required this.item, required this.l10n, super.key}); + const CommunityActionButtons({ + required this.item, + required this.l10n, + super.key, + }); final T item; final AppLocalizations l10n; @@ -29,7 +33,12 @@ class CommunityActionButtons extends StatelessWidget { ); } else if (item is Report) { final report = item as Report; - _buildReportActions(context, report, visibleActions, overflowMenuItems); + _buildReportActions( + context, + report, + visibleActions, + overflowMenuItems, + ); } else if (item is AppReview) { final appReview = item as AppReview; _buildAppReviewActions( @@ -71,7 +80,7 @@ class CommunityActionButtons extends StatelessWidget { iconSize: 20, icon: const Icon(Icons.visibility_outlined), tooltip: l10n.viewEngagedContent, - onPressed: () { + onPressed: () { context.goNamed( Routes.editHeadlineName, pathParameters: {'id': engagement.entityId}, @@ -98,10 +107,10 @@ class CommunityActionButtons extends StatelessWidget { ), ); } - } + } overflowMenuItems.add( PopupMenuItem(value: 'copyUserId', child: Text(l10n.copyUserId)), - ); + ); } void _buildReportActions( @@ -117,7 +126,7 @@ class CommunityActionButtons extends StatelessWidget { iconSize: 20, icon: const Icon(Icons.visibility_outlined), tooltip: l10n.viewReportedItem, - onPressed: () { + onPressed: () { // TODO(fulleni): Implement navigation to reported item based on entityType // This will require a more complex routing logic based on ReportableEntity // For now, it remains unimplemented. @@ -151,7 +160,7 @@ class CommunityActionButtons extends StatelessWidget { child: Text(l10n.copyReportedItemId), ), ); - } + } void _buildAppReviewActions( BuildContext context, @@ -166,10 +175,11 @@ class CommunityActionButtons extends StatelessWidget { iconSize: 20, icon: const Icon(Icons.history), tooltip: l10n.viewFeedbackHistory, - onPressed: () { + onPressed: () { showDialog( context: context, - builder: (dialogContext) => _FeedbackHistoryDialog(appReview: appReview, l10n: l10n), + builder: (dialogContext) => + _FeedbackHistoryDialog(appReview: appReview, l10n: l10n), ); }, ), @@ -203,7 +213,7 @@ class CommunityActionButtons extends StatelessWidget { } else if (value == 'copyReportedItemId' && item is Report) { String reportedItemId; reportedItemId = item.entityId; - Clipboard.setData(ClipboardData(text: userId)); + Clipboard.setData(ClipboardData(text: userId)); ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.userIdCopied))); @@ -212,8 +222,8 @@ class CommunityActionButtons extends StatelessWidget { comment: item.comment?.copyWith(status: CommentStatus.approved), ); engagementsRepository.update( - id: updatedEngagement.id, - item: updatedEngagement, + id: updatedEngagement.id, + item: updatedEngagement, ); } else if (value == 'rejectComment' && item is Engagement) { showDialog( @@ -228,8 +238,15 @@ class CommunityActionButtons extends StatelessWidget { ), ElevatedButton( onPressed: () { - final updatedEngagement = item.copyWith(comment: item.comment?.copyWith(status: CommentStatus.rejected),); - engagementsRepository.update(id: updatedEngagement.id, item: updatedEngagement,); + final updatedEngagement = item.copyWith( + comment: item.comment?.copyWith( + status: CommentStatus.rejected, + ), + ); + engagementsRepository.update( + id: updatedEngagement.id, + item: updatedEngagement, + ); Navigator.of(dialogContext).pop(); }, child: Text(l10n.reject), @@ -282,7 +299,12 @@ class _FeedbackHistoryDialog extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(DateFormatter.formatRelativeTime(context, feedback.providedAt)), + Text( + DateFormatter.formatRelativeTime( + context, + feedback.providedAt, + ), + ), if (feedback.reason != null) Text(feedback.reason!), ], ), @@ -291,7 +313,12 @@ class _FeedbackHistoryDialog extends StatelessWidget { }, ), ), - actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(l10n.close))], + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.close), + ), + ], ); } } diff --git a/lib/shared/extensions/comment_status_extension.dart b/lib/shared/extensions/comment_status_extension.dart index d7849c33..9b21e4e8 100644 --- a/lib/shared/extensions/comment_status_extension.dart +++ b/lib/shared/extensions/comment_status_extension.dart @@ -19,4 +19,4 @@ extension CommentStatusX on CommentStatus { return l10n.commentStatusHiddenByUser; } } -} \ No newline at end of file +} diff --git a/lib/shared/extensions/initial_app_review_feedback_extension.dart b/lib/shared/extensions/initial_app_review_feedback_extension.dart index 6da20f0c..8104cd53 100644 --- a/lib/shared/extensions/initial_app_review_feedback_extension.dart +++ b/lib/shared/extensions/initial_app_review_feedback_extension.dart @@ -14,4 +14,4 @@ extension InitialAppReviewFeedbackX on InitialAppReviewFeedback { return l10n.initialAppReviewFeedbackNegative; } } -} \ No newline at end of file +} diff --git a/lib/shared/extensions/report_reason_extension.dart b/lib/shared/extensions/report_reason_extension.dart index 7d432534..3849de92 100644 --- a/lib/shared/extensions/report_reason_extension.dart +++ b/lib/shared/extensions/report_reason_extension.dart @@ -47,4 +47,4 @@ extension ReportReasonX on String { return this; // Fallback to raw string if no localization found } } -} \ No newline at end of file +} diff --git a/lib/shared/extensions/report_status_extension.dart b/lib/shared/extensions/report_status_extension.dart index 5ce68bad..b2073d94 100644 --- a/lib/shared/extensions/report_status_extension.dart +++ b/lib/shared/extensions/report_status_extension.dart @@ -15,4 +15,4 @@ extension ReportStatusX on ReportStatus { return l10n.reportStatusResolved; } } -} \ No newline at end of file +} diff --git a/lib/shared/extensions/reportable_entity_extension.dart b/lib/shared/extensions/reportable_entity_extension.dart index ede19fca..54ec6538 100644 --- a/lib/shared/extensions/reportable_entity_extension.dart +++ b/lib/shared/extensions/reportable_entity_extension.dart @@ -15,4 +15,4 @@ extension ReportableEntityX on ReportableEntity { return l10n.reportableEntityComment; } } -} \ No newline at end of file +} From 3a2e712e3c5ad086a4c2f8830b81045067a63e6b Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:13:36 +0100 Subject: [PATCH 046/130] fix(localization): update ReportableEntity extension for consistency - Change case label from ReportableEntity.comment to ReportableEntity.engagement - Ensure proper alignment with localization labels --- lib/shared/extensions/reportable_entity_extension.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/shared/extensions/reportable_entity_extension.dart b/lib/shared/extensions/reportable_entity_extension.dart index 54ec6538..8cccce57 100644 --- a/lib/shared/extensions/reportable_entity_extension.dart +++ b/lib/shared/extensions/reportable_entity_extension.dart @@ -11,7 +11,7 @@ extension ReportableEntityX on ReportableEntity { return l10n.reportableEntityHeadline; case ReportableEntity.source: return l10n.reportableEntitySource; - case ReportableEntity.comment: + case ReportableEntity.engagement: return l10n.reportableEntityComment; } } From 7987bc91aa97f10da51b72e4c473d25da550de96 Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:31:08 +0100 Subject: [PATCH 047/130] feat(localization): add Arabic translation for "cancel" and remove unused messages - Add Arabic translation for "cancel" button - Remove unused messages "userIdCopied" and "@userIdCopied" from both Arabic and English localization files --- lib/l10n/app_localizations.dart | 8 +++++++- lib/l10n/app_localizations_ar.dart | 5 ++++- lib/l10n/app_localizations_en.dart | 3 +++ lib/l10n/arb/app_ar.arb | 10 +++++----- lib/l10n/arb/app_en.arb | 8 ++++---- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 9e94dd15..3466fd5e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3692,7 +3692,7 @@ abstract class AppLocalizations { /// **'Loading App Reviews'** String get loadingAppReviews; - /// Snackbar message confirming that the user ID has been copied. + /// Message when user ID is copied /// /// In en, this message translates to: /// **'User ID copied to clipboard.'** @@ -3895,6 +3895,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Flagged by AI'** String get commentStatusFlaggedByAi; + + /// A generic 'Cancel' button text. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 37177538..b38ad9fa 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1979,7 +1979,7 @@ class AppLocalizationsAr extends AppLocalizations { String get loadingAppReviews => 'جاري تحميل مراجعات التطبيق'; @override - String get userIdCopied => 'تم نسخ معرّف المستخدم إلى الحافظة.'; + String get userIdCopied => 'تم نسخ معرف المستخدم إلى الحافظة.'; @override String get commentApproved => 'تمت الموافقة على التعليق.'; @@ -2088,4 +2088,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get commentStatusFlaggedByAi => 'تم الإبلاغ بواسطة الذكاء الاصطناعي'; + + @override + String get cancel => 'إلغاء'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b2dc3a37..649c6b0e 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2094,4 +2094,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get commentStatusFlaggedByAi => 'Flagged by AI'; + + @override + String get cancel => 'Cancel'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 4deb539e..44a9d090 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2625,10 +2625,6 @@ "@noNegativeFeedbackHistory": { "description": "رسالة تظهر في مربع حوار سجل التقييمات عند عدم وجود سجل." }, - "userIdCopied": "تم نسخ معرّف المستخدم إلى الحافظة.", - "@userIdCopied": { - "description": "رسالة Snackbar تؤكد أنه تم نسخ معرّف المستخدم." - }, "reject": "رفض", "@reject": { "description": "نص زر التأكيد لإجراء الرفض." @@ -2636,5 +2632,9 @@ "commentStatusFlaggedByAi": "تم الإبلاغ بواسطة الذكاء الاصطناعي", "@commentStatusFlaggedByAi": { "description": "حالة إدارية لتعليق تم الإبلاغ عنه تلقائيًا بواسطة الذكاء الاصطناعي." - } + }, + "cancel": "إلغاء", + "@cancel": { + "description": "A generic 'Cancel' button text." + } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 2b9f8859..532ce1e4 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2621,10 +2621,6 @@ "@noNegativeFeedbackHistory": { "description": "Message displayed in the feedback history dialog when there is no history." }, - "userIdCopied": "User ID copied to clipboard.", - "@userIdCopied": { - "description": "Snackbar message confirming that the user ID has been copied." - }, "reject": "Reject", "@reject": { "description": "Confirmation button text for a reject action." @@ -2632,5 +2628,9 @@ "commentStatusFlaggedByAi": "Flagged by AI", "@commentStatusFlaggedByAi": { "description": "Admin-centric status for a comment automatically flagged by AI." + }, + "cancel": "Cancel", + "@cancel": { + "description": "A generic 'Cancel' button text." } } \ No newline at end of file From 918513bb5884324e4fccc42b6dac01518f5af44a Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:49:50 +0100 Subject: [PATCH 048/130] fix(community): implement navigation to reported items and update action buttons - Add navigation logic for reported items based on entity type - Implement copy functionality for reported item IDs - Update UI text for copy actions and dialogs - Refactor action buttons to remove unnecessary generic type parameters --- .../widgets/community_action_buttons.dart | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index aa480091..f634fe70 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -25,7 +25,7 @@ class CommunityActionButtons extends StatelessWidget { if (item is Engagement) { final engagement = item as Engagement; - _buildEngagementActions( + _buildEngagementActions( context, engagement, visibleActions, @@ -33,7 +33,7 @@ class CommunityActionButtons extends StatelessWidget { ); } else if (item is Report) { final report = item as Report; - _buildReportActions( + _buildReportActions( context, report, visibleActions, @@ -41,7 +41,7 @@ class CommunityActionButtons extends StatelessWidget { ); } else if (item is AppReview) { final appReview = item as AppReview; - _buildAppReviewActions( + _buildAppReviewActions( context, appReview, visibleActions, @@ -67,7 +67,7 @@ class CommunityActionButtons extends StatelessWidget { return Row(mainAxisSize: MainAxisSize.min, children: visibleActions); } - void _buildEngagementActions( + void _buildEngagementActions( BuildContext context, Engagement engagement, List visibleActions, @@ -127,9 +127,16 @@ class CommunityActionButtons extends StatelessWidget { icon: const Icon(Icons.visibility_outlined), tooltip: l10n.viewReportedItem, onPressed: () { - // TODO(fulleni): Implement navigation to reported item based on entityType - // This will require a more complex routing logic based on ReportableEntity - // For now, it remains unimplemented. + final String routeName; + switch (report.entityType) { + case ReportableEntity.headline: + routeName = Routes.editHeadlineName; + case ReportableEntity.source: + routeName = Routes.editSourceName; + case ReportableEntity.engagement: + return; + } + context.goNamed(routeName, pathParameters: {'id': report.entityId}); }, ), ); @@ -151,10 +158,10 @@ class CommunityActionButtons extends StatelessWidget { ), ); } - overflowMenuItems.add( + overflowMenuItems..add( PopupMenuItem(value: 'copyUserId', child: Text(l10n.copyUserId)), - ); - overflowMenuItems.add( + ) + ..add( PopupMenuItem( value: 'copyReportedItemId', child: Text(l10n.copyReportedItemId), @@ -191,7 +198,7 @@ class CommunityActionButtons extends StatelessWidget { ); } - void _onActionSelected(BuildContext context, String value, T item) { + void _onActionSelected(BuildContext context, String value, T item) { final engagementsRepository = context.read>(); final reportsRepository = context.read>(); @@ -211,12 +218,13 @@ class CommunityActionButtons extends StatelessWidget { ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.userIdCopied))); } else if (value == 'copyReportedItemId' && item is Report) { - String reportedItemId; - reportedItemId = item.entityId; - Clipboard.setData(ClipboardData(text: userId)); + final reportedItemId = item.entityId; + Clipboard.setData(ClipboardData(text: reportedItemId)); ScaffoldMessenger.of(context) ..hideCurrentSnackBar() - ..showSnackBar(SnackBar(content: Text(l10n.userIdCopied))); + ..showSnackBar( + SnackBar(content: Text(l10n.idCopiedToClipboard(reportedItemId))), + ); } else if (value == 'approveComment' && item is Engagement) { final updatedEngagement = item.copyWith( comment: item.comment?.copyWith(status: CommentStatus.approved), @@ -234,7 +242,7 @@ class CommunityActionButtons extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), - child: Text(l10n.cancel), + child: Text(l10n.cancelButton), ), ElevatedButton( onPressed: () { @@ -316,7 +324,7 @@ class _FeedbackHistoryDialog extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text(l10n.close), + child: Text(l10n.closeButtonText), ), ], ); From 8d45e1f93b306eec7c24d3953f5cc7f8fabe44ed Mon Sep 17 00:00:00 2001 From: fulleni Date: Mon, 1 Dec 2025 12:50:38 +0100 Subject: [PATCH 049/130] style: format --- .../widgets/community_action_buttons.dart | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index f634fe70..0e7ea45b 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -158,15 +158,19 @@ class CommunityActionButtons extends StatelessWidget { ), ); } - overflowMenuItems..add( - PopupMenuItem(value: 'copyUserId', child: Text(l10n.copyUserId)), - ) - ..add( - PopupMenuItem( - value: 'copyReportedItemId', - child: Text(l10n.copyReportedItemId), - ), - ); + overflowMenuItems + ..add( + PopupMenuItem( + value: 'copyUserId', + child: Text(l10n.copyUserId), + ), + ) + ..add( + PopupMenuItem( + value: 'copyReportedItemId', + child: Text(l10n.copyReportedItemId), + ), + ); } void _buildAppReviewActions( From ca90fb2f6aac324b561fe6cf41805d93a6ee7aff Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:06:05 +0100 Subject: [PATCH 050/130] build(pubspec): update core package reference - Update core package ref from a960fe8 to eded8c4 - Ensure compatibility with the latest core package version --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 16c1b196..286c08b5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,8 +89,8 @@ packages: dependency: "direct main" description: path: "." - ref: a960fe8f340fc8b74b651997de45aee10d8435aa - resolved-ref: a960fe8f340fc8b74b651997de45aee10d8435aa + ref: eded8c494bde7462627600c05dc7838e75956a15 + resolved-ref: eded8c494bde7462627600c05dc7838e75956a15 url: "https://github.com/flutter-news-app-full-source-code/core.git" source: git version: "1.3.1" diff --git a/pubspec.yaml b/pubspec.yaml index c86f6508..b951ed6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,7 +94,7 @@ dependency_overrides: core: git: url: https://github.com/flutter-news-app-full-source-code/core.git - ref: a960fe8f340fc8b74b651997de45aee10d8435aa + ref: eded8c494bde7462627600c05dc7838e75956a15 http_client: git: url: https://github.com/flutter-news-app-full-source-code/http-client.git From b26ef43330480eadc5f4d527f128888657e4888d Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:06:30 +0100 Subject: [PATCH 051/130] fix(community_management): update filter field for app review feedback - Change filter key from 'initialFeedback' to 'feedback' - Update condition to check for selectedAppReviewFeedback instead of selectedInitialFeedback --- .../bloc/community_management_bloc.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 4c813843..6ef064f8 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -145,9 +145,9 @@ class CommunityManagementBloc if (state.searchQuery.isNotEmpty) { filter['userId'] = {r'$regex': state.searchQuery, r'$options': 'i'}; } - if (state.selectedInitialFeedback.isNotEmpty) { - filter['initialFeedback'] = { - r'$in': state.selectedInitialFeedback.map((f) => f.name).toList(), + if (state.selectedAppReviewFeedback.isNotEmpty) { + filter['feedback'] = { + r'$in': state.selectedAppReviewFeedback.map((f) => f.name).toList(), }; } return filter; From fec9c656d93fcdd8a9241bebe8331e3ba7460e90 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:06:57 +0100 Subject: [PATCH 052/130] refactor(community_management): rename InitialAppReviewFeedback to AppReviewFeedback - Update selectedInitialFeedback to selectedAppReviewFeedback in CommunityFilterState, CommunityFilterEvent, and CommunityFilterBloc - This change improves clarity and consistency in the app review feedback naming --- .../bloc/community_filter/community_filter_bloc.dart | 2 +- .../community_filter/community_filter_event.dart | 6 +++--- .../community_filter/community_filter_state.dart | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/community_management/bloc/community_filter/community_filter_bloc.dart b/lib/community_management/bloc/community_filter/community_filter_bloc.dart index d112dd24..26853891 100644 --- a/lib/community_management/bloc/community_filter/community_filter_bloc.dart +++ b/lib/community_management/bloc/community_filter/community_filter_bloc.dart @@ -22,7 +22,7 @@ class CommunityFilterBloc selectedCommentStatus: event.selectedCommentStatus, selectedReportStatus: event.selectedReportStatus, selectedReportableEntity: event.selectedReportableEntity, - selectedInitialFeedback: event.selectedInitialFeedback, + selectedAppReviewFeedback: event.selectedAppReviewFeedback, ), ); } diff --git a/lib/community_management/bloc/community_filter/community_filter_event.dart b/lib/community_management/bloc/community_filter/community_filter_event.dart index 54bbb7e0..7fc270d9 100644 --- a/lib/community_management/bloc/community_filter/community_filter_event.dart +++ b/lib/community_management/bloc/community_filter/community_filter_event.dart @@ -13,14 +13,14 @@ class CommunityFilterApplied extends CommunityFilterEvent { this.selectedCommentStatus = const [], this.selectedReportStatus = const [], this.selectedReportableEntity = const [], - this.selectedInitialFeedback = const [], + this.selectedAppReviewFeedback = const [], }); final String searchQuery; final List selectedCommentStatus; final List selectedReportStatus; final List selectedReportableEntity; - final List selectedInitialFeedback; + final List selectedAppReviewFeedback; @override List get props => [ @@ -28,7 +28,7 @@ class CommunityFilterApplied extends CommunityFilterEvent { selectedCommentStatus, selectedReportStatus, selectedReportableEntity, - selectedInitialFeedback, + selectedAppReviewFeedback, ]; } diff --git a/lib/community_management/bloc/community_filter/community_filter_state.dart b/lib/community_management/bloc/community_filter/community_filter_state.dart index e5fe8801..e5c94d34 100644 --- a/lib/community_management/bloc/community_filter/community_filter_state.dart +++ b/lib/community_management/bloc/community_filter/community_filter_state.dart @@ -6,21 +6,21 @@ class CommunityFilterState extends Equatable { this.selectedCommentStatus = const [], this.selectedReportStatus = const [], this.selectedReportableEntity = const [], - this.selectedInitialFeedback = const [], + this.selectedAppReviewFeedback = const [], }); final String searchQuery; final List selectedCommentStatus; final List selectedReportStatus; final List selectedReportableEntity; - final List selectedInitialFeedback; + final List selectedAppReviewFeedback; CommunityFilterState copyWith({ String? searchQuery, List? selectedCommentStatus, List? selectedReportStatus, List? selectedReportableEntity, - List? selectedInitialFeedback, + List? selectedAppReviewFeedback, }) { return CommunityFilterState( searchQuery: searchQuery ?? this.searchQuery, @@ -29,8 +29,8 @@ class CommunityFilterState extends Equatable { selectedReportStatus: selectedReportStatus ?? this.selectedReportStatus, selectedReportableEntity: selectedReportableEntity ?? this.selectedReportableEntity, - selectedInitialFeedback: - selectedInitialFeedback ?? this.selectedInitialFeedback, + selectedAppReviewFeedback: + selectedAppReviewFeedback ?? this.selectedAppReviewFeedback, ); } @@ -40,6 +40,6 @@ class CommunityFilterState extends Equatable { selectedCommentStatus, selectedReportStatus, selectedReportableEntity, - selectedInitialFeedback, + selectedAppReviewFeedback, ]; } From c901be46627e49ea991bc5f04c15fb9485d1c07a Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:07:41 +0100 Subject: [PATCH 053/130] refactor(community_management): update community filter dialog feedback handling - Rename `InitialAppReviewFeedback` to `AppReviewFeedback` - Rename related events and state properties to reflect the new name - Update bloc logic to handle the renamed events and state properties --- .../bloc/community_filter_dialog_bloc.dart | 14 ++++++++------ .../bloc/community_filter_dialog_event.dart | 8 ++++---- .../bloc/community_filter_dialog_state.dart | 12 ++++++------ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart index 2f783561..3d87a1ac 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart @@ -22,7 +22,9 @@ class CommunityFilterDialogBloc on( _onReportableEntityChanged, ); - on(_onInitialFeedbackChanged); + on( + _onAppReviewFeedbackChanged, + ); on(_onFilterDialogReset); } @@ -37,8 +39,8 @@ class CommunityFilterDialogBloc selectedReportStatus: event.communityFilterState.selectedReportStatus, selectedReportableEntity: event.communityFilterState.selectedReportableEntity, - selectedInitialFeedback: - event.communityFilterState.selectedInitialFeedback, + selectedAppReviewFeedback: + event.communityFilterState.selectedAppReviewFeedback, ), ); } @@ -71,11 +73,11 @@ class CommunityFilterDialogBloc emit(state.copyWith(selectedReportableEntity: event.reportableEntity)); } - void _onInitialFeedbackChanged( - CommunityFilterDialogInitialFeedbackChanged event, + void _onAppReviewFeedbackChanged( + CommunityFilterDialogAppReviewFeedbackChanged event, Emitter emit, ) { - emit(state.copyWith(selectedInitialFeedback: event.initialFeedback)); + emit(state.copyWith(selectedAppReviewFeedback: event.appReviewFeedback)); } void _onFilterDialogReset( diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart index 2ee24761..551627f9 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart @@ -60,14 +60,14 @@ class CommunityFilterDialogReportableEntityChanged List get props => [reportableEntity]; } -class CommunityFilterDialogInitialFeedbackChanged +class CommunityFilterDialogAppReviewFeedbackChanged extends CommunityFilterDialogEvent { - const CommunityFilterDialogInitialFeedbackChanged(this.initialFeedback); + const CommunityFilterDialogAppReviewFeedbackChanged(this.appReviewFeedback); - final List initialFeedback; + final List appReviewFeedback; @override - List get props => [initialFeedback]; + List get props => [appReviewFeedback]; } class CommunityFilterDialogReset extends CommunityFilterDialogEvent { diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart index 7ffd01e2..50aaae5d 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart @@ -7,7 +7,7 @@ class CommunityFilterDialogState extends Equatable { this.selectedCommentStatus = const [], this.selectedReportStatus = const [], this.selectedReportableEntity = const [], - this.selectedInitialFeedback = const [], + this.selectedAppReviewFeedback = const [], }); final CommunityManagementTab activeTab; @@ -15,14 +15,14 @@ class CommunityFilterDialogState extends Equatable { final List selectedCommentStatus; final List selectedReportStatus; final List selectedReportableEntity; - final List selectedInitialFeedback; + final List selectedAppReviewFeedback; CommunityFilterDialogState copyWith({ String? searchQuery, List? selectedCommentStatus, List? selectedReportStatus, List? selectedReportableEntity, - List? selectedInitialFeedback, + List? selectedAppReviewFeedback, }) { return CommunityFilterDialogState( activeTab: activeTab, @@ -32,8 +32,8 @@ class CommunityFilterDialogState extends Equatable { selectedReportStatus: selectedReportStatus ?? this.selectedReportStatus, selectedReportableEntity: selectedReportableEntity ?? this.selectedReportableEntity, - selectedInitialFeedback: - selectedInitialFeedback ?? this.selectedInitialFeedback, + selectedAppReviewFeedback: + selectedAppReviewFeedback ?? this.selectedAppReviewFeedback, ); } @@ -44,6 +44,6 @@ class CommunityFilterDialogState extends Equatable { selectedCommentStatus, selectedReportStatus, selectedReportableEntity, - selectedInitialFeedback, + selectedAppReviewFeedback, ]; } From de0c7b6605c1d17bb5e45c259463c75f5160c540 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:08:30 +0100 Subject: [PATCH 054/130] refactor(app-reviews): update feedback handling and UI - Replace InitialAppReviewFeedback with AppReviewFeedback - Update UI to display feedback details instead --- .../view/app_reviews_page.dart | 5 +- .../widgets/community_action_buttons.dart | 85 +++++++++---------- .../community_filter_dialog.dart | 10 +-- ...art => app_review_feedback_extension.dart} | 8 +- lib/shared/extensions/extensions.dart | 2 +- 5 files changed, 54 insertions(+), 56 deletions(-) rename lib/shared/extensions/{initial_app_review_feedback_extension.dart => app_review_feedback_extension.dart} (68%) diff --git a/lib/community_management/view/app_reviews_page.dart b/lib/community_management/view/app_reviews_page.dart index b5048742..33b48d1c 100644 --- a/lib/community_management/view/app_reviews_page.dart +++ b/lib/community_management/view/app_reviews_page.dart @@ -7,6 +7,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/community_manage import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_action_buttons.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_review_feedback_extension.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -35,7 +36,7 @@ class _AppReviewsPageState extends State { bool _areFiltersActive(CommunityFilterState state) { return state.searchQuery.isNotEmpty || - state.selectedInitialFeedback.isNotEmpty; + state.selectedAppReviewFeedback.isNotEmpty; } @override @@ -196,8 +197,8 @@ class _AppReviewsDataSource extends DataTableSource { final appReview = appReviews[index]; return DataRow2( cells: [ - DataCell(Text(appReview.initialFeedback.name)), DataCell(Text(appReview.userId, overflow: TextOverflow.ellipsis)), + DataCell(Text(appReview.feedback.l10n(context))), if (!isMobile) DataCell( Text( diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index 0e7ea45b..4312104f 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -180,21 +180,24 @@ class CommunityActionButtons extends StatelessWidget { List> overflowMenuItems, ) { // Primary Action - visibleActions.add( - IconButton( - visualDensity: VisualDensity.compact, - iconSize: 20, - icon: const Icon(Icons.history), - tooltip: l10n.viewFeedbackHistory, - onPressed: () { - showDialog( - context: context, - builder: (dialogContext) => - _FeedbackHistoryDialog(appReview: appReview, l10n: l10n), - ); - }, - ), - ); + if (appReview.feedbackDetails != null && + appReview.feedbackDetails!.isNotEmpty) { + visibleActions.add( + IconButton( + visualDensity: VisualDensity.compact, + iconSize: 20, + icon: const Icon(Icons.comment_outlined), + tooltip: l10n.viewFeedbackHistory, + onPressed: () { + showDialog( + context: context, + builder: (dialogContext) => + _FeedbackDetailsDialog(appReview: appReview, l10n: l10n), + ); + }, + ), + ); + } // Secondary Actions overflowMenuItems.add( @@ -282,8 +285,8 @@ class CommunityActionButtons extends StatelessWidget { } } -class _FeedbackHistoryDialog extends StatelessWidget { - const _FeedbackHistoryDialog({ +class _FeedbackDetailsDialog extends StatelessWidget { + const _FeedbackDetailsDialog({ required this.appReview, required this.l10n, }); @@ -293,37 +296,31 @@ class _FeedbackHistoryDialog extends StatelessWidget { @override Widget build(BuildContext context) { + final details = appReview.feedbackDetails; + return AlertDialog( - title: Text(l10n.viewFeedbackHistory), + title: Text(l10n.feedbackHistory), content: SizedBox( width: double.maxFinite, - child: appReview.negativeFeedbackHistory.isEmpty - ? Text(l10n.noNegativeFeedbackHistory) - : ListView.builder( - shrinkWrap: true, - itemCount: appReview.negativeFeedbackHistory.length, - itemBuilder: (context, index) { - final feedback = appReview.negativeFeedbackHistory[index]; - return Card( - margin: const EdgeInsets.symmetric(vertical: AppSpacing.xs), - child: Padding( - padding: const EdgeInsets.all(AppSpacing.md), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - DateFormatter.formatRelativeTime( - context, - feedback.providedAt, - ), - ), - if (feedback.reason != null) Text(feedback.reason!), - ], - ), - ), - ); - }, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.feedbackProvidedAt( + DateFormatter.formatRelativeTime( + context, + appReview.updatedAt, + ), + ), + style: Theme.of(context).textTheme.labelSmall, ), + const SizedBox(height: AppSpacing.md), + Text(details ?? l10n.noReasonProvided), + ], + ), + ), ), actions: [ TextButton( diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index d616808f..f77cff8e 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -39,7 +39,7 @@ class _CommunityFilterDialogState extends State { selectedCommentStatus: filterDialogState.selectedCommentStatus, selectedReportStatus: filterDialogState.selectedReportStatus, selectedReportableEntity: filterDialogState.selectedReportableEntity, - selectedInitialFeedback: filterDialogState.selectedInitialFeedback, + selectedAppReviewFeedback: filterDialogState.selectedAppReviewFeedback, ), ); } @@ -180,17 +180,17 @@ class _CommunityFilterDialogState extends State { ]; case CommunityManagementTab.appReviews: return [ - SearchableSelectionInput( + SearchableSelectionInput( label: l10n.initialFeedback, hintText: l10n.selectInitialFeedback, isMultiSelect: true, - selectedItems: state.selectedInitialFeedback, + selectedItems: state.selectedAppReviewFeedback, itemBuilder: (context, item) => Text(item.l10n(context)), itemToString: (item) => item.l10n(context), onChanged: (items) => context.read().add( - CommunityFilterDialogInitialFeedbackChanged(items ?? []), + CommunityFilterDialogAppReviewFeedbackChanged(items ?? []), ), - staticItems: InitialAppReviewFeedback.values, + staticItems: AppReviewFeedback.values, ), ]; } diff --git a/lib/shared/extensions/initial_app_review_feedback_extension.dart b/lib/shared/extensions/app_review_feedback_extension.dart similarity index 68% rename from lib/shared/extensions/initial_app_review_feedback_extension.dart rename to lib/shared/extensions/app_review_feedback_extension.dart index 8104cd53..4f82613b 100644 --- a/lib/shared/extensions/initial_app_review_feedback_extension.dart +++ b/lib/shared/extensions/app_review_feedback_extension.dart @@ -2,15 +2,15 @@ import 'package:core/core.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -extension InitialAppReviewFeedbackX on InitialAppReviewFeedback { +extension InitialAppReviewFeedbackX on AppReviewFeedback { /// Returns a localized, admin-centric string for the - /// [InitialAppReviewFeedback]. + /// [AppReviewFeedback]. String l10n(BuildContext context) { final l10n = context.l10n; switch (this) { - case InitialAppReviewFeedback.positive: + case AppReviewFeedback.positive: return l10n.initialAppReviewFeedbackPositive; - case InitialAppReviewFeedback.negative: + case AppReviewFeedback.negative: return l10n.initialAppReviewFeedbackNegative; } } diff --git a/lib/shared/extensions/extensions.dart b/lib/shared/extensions/extensions.dart index 3a1468cc..6fbb0c93 100644 --- a/lib/shared/extensions/extensions.dart +++ b/lib/shared/extensions/extensions.dart @@ -1,5 +1,6 @@ export 'ad_platform_type_l10n.dart'; export 'ad_type_l10n.dart'; +export 'app_review_feedback_extension.dart'; export 'app_user_role_l10n.dart'; export 'banner_ad_shape_l10n.dart'; export 'comment_status_extension.dart'; @@ -8,7 +9,6 @@ export 'dashboard_user_role_l10n.dart'; export 'engagement_mode_l10n.dart'; export 'feed_decorator_type_l10n.dart'; export 'feed_item_click_behavior_l10n.dart'; -export 'initial_app_review_feedback_extension.dart'; export 'push_notification_provider_l10n.dart'; export 'push_notification_subscription_delivery_type_l10n.dart'; export 'report_reason_extension.dart'; From 109e9fdbc61c7fce6cddcc473c1d30abfb76aadf Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:20:05 +0100 Subject: [PATCH 055/130] feat(l10n): add new community management translations - Add translations for new actions and column headers in community management - Include translations for report status updates and entity types - Add view actions for reported items (headline, source, comment) - Introduce new filters and search placeholders for community content --- lib/l10n/app_localizations.dart | 24 ++ lib/l10n/app_localizations_ar.dart | 12 + lib/l10n/app_localizations_en.dart | 12 + lib/l10n/arb/app_ar.arb | 24 +- lib/l10n/arb/app_en.arb | 600 +++++++++++++++-------------- 5 files changed, 376 insertions(+), 296 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 3466fd5e..5e5288da 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3901,6 +3901,30 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Cancel'** String get cancel; + + /// Action to view the reported headline + /// + /// In en, this message translates to: + /// **'View Headline'** + String get viewReportedHeadline; + + /// Action to view the reported source + /// + /// In en, this message translates to: + /// **'View Source'** + String get viewReportedSource; + + /// Action to view the reported comment + /// + /// In en, this message translates to: + /// **'View Comment'** + String get viewReportedComment; + + /// Column header for the type of entity being reported + /// + /// In en, this message translates to: + /// **'Entity Type'** + String get entityType; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index b38ad9fa..2f5cff9e 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -2091,4 +2091,16 @@ class AppLocalizationsAr extends AppLocalizations { @override String get cancel => 'إلغاء'; + + @override + String get viewReportedHeadline => 'عرض العنوان'; + + @override + String get viewReportedSource => 'عرض المصدر'; + + @override + String get viewReportedComment => 'عرض التعليق'; + + @override + String get entityType => 'نوع الكيان'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 649c6b0e..f8f5648b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2097,4 +2097,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cancel => 'Cancel'; + + @override + String get viewReportedHeadline => 'View Headline'; + + @override + String get viewReportedSource => 'View Source'; + + @override + String get viewReportedComment => 'View Comment'; + + @override + String get entityType => 'Entity Type'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 44a9d090..8ce7891c 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2633,8 +2633,24 @@ "@commentStatusFlaggedByAi": { "description": "حالة إدارية لتعليق تم الإبلاغ عنه تلقائيًا بواسطة الذكاء الاصطناعي." }, - "cancel": "إلغاء", - "@cancel": { - "description": "A generic 'Cancel' button text." - } + "cancel": "إلغاء", + "@cancel": { + "description": "A generic 'Cancel' button text." + }, + "viewReportedHeadline": "عرض العنوان", + "@viewReportedHeadline": { + "description": "Action to view the reported headline" + }, + "viewReportedSource": "عرض المصدر", + "@viewReportedSource": { + "description": "Action to view the reported source" + }, + "viewReportedComment": "عرض التعليق", + "@viewReportedComment": { + "description": "Action to view the reported comment" + }, + "entityType": "نوع الكيان", + "@entityType": { + "description": "Column header for the type of entity being reported" + } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 532ce1e4..152f961b 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2246,296 +2246,296 @@ "@enableCommunityFeaturesDescription": { "description": "Description for the master switch to enable all community features." }, - "communityManagement": "Community", - "@communityManagement": { - "description": "Label for the community management navigation item" - }, - "communityManagementPageDescription": "Manage user-generated content including engagements (reactions and comments), content reports, and app reviews.", - "@communityManagementPageDescription": { - "description": "Description for the Community Management page" - }, - "engagements": "Engagements", - "@engagements": { - "description": "Label for the engagements subpage" - }, - "reports": "Reports", - "@reports": { - "description": "Label for the reports subpage" - }, - "appReviews": "App Reviews", - "@appReviews": { - "description": "Label for the app reviews subpage" - }, - "user": "User", - "@user": { - "description": "Column header for user" - }, - "engagedContent": "Engaged Content", - "@engagedContent": { - "description": "Column header for engaged content" - }, - "reaction": "Reaction", - "@reaction": { - "description": "Column header for reaction" - }, - "comment": "Comment", - "@comment": { - "description": "Column header for comment" - }, - "commentStatus": "Comment Status", - "@commentStatus": { - "description": "Column header for comment status" - }, - "date": "Date", - "@date": { - "description": "Column header for date" - }, - "approveComment": "Approve Comment", - "@approveComment": { - "description": "Action to approve a comment" - }, - "rejectComment": "Reject Comment", - "@rejectComment": { - "description": "Action to reject a comment" - }, - "viewEngagedContent": "View Content", - "@viewEngagedContent": { - "description": "Action to view the engaged content" - }, - "copyUserId": "Copy User ID", - "@copyUserId": { - "description": "Action to copy the user ID" - }, - "reporter": "Reporter", - "@reporter": { - "description": "Column header for reporter" - }, - "reportedItem": "Reported Item", - "@reportedItem": { - "description": "Column header for reported item" - }, - "reason": "Reason", - "@reason": { - "description": "Column header for reason" - }, - "reportStatus": "Report Status", - "@reportStatus": { - "description": "Column header for report status" - }, - "viewReportedItem": "View Item", - "@viewReportedItem": { - "description": "Action to view the reported item" - }, - "markAsInReview": "Mark as In Review", - "@markAsInReview": { - "description": "Action to mark a report as in review" - }, - "resolveReport": "Resolve Report", - "@resolveReport": { - "description": "Action to resolve a report" - }, - "initialFeedback": "Initial Feedback", - "@initialFeedback": { - "description": "Column header for initial feedback" - }, - "osPromptRequested": "OS Prompt?", - "@osPromptRequested": { - "description": "Column header for OS prompt requested" - }, - "feedbackHistory": "Feedback History", - "@feedbackHistory": { - "description": "Column header for feedback history" - }, - "lastInteraction": "Last Interaction", - "@lastInteraction": { - "description": "Column header for last interaction" - }, - "viewFeedbackHistory": "View History", - "@viewFeedbackHistory": { - "description": "Action to view feedback history" - }, - "reactionTypeLike": "Like", - "@reactionTypeLike": { - "description": "Reaction type: Like" - }, - "reactionTypeInsightful": "Insightful", - "@reactionTypeInsightful": { - "description": "Reaction type: Insightful" - }, - "reactionTypeAmusing": "Amusing", - "@reactionTypeAmusing": { - "description": "Reaction type: Amusing" - }, - "reactionTypeSad": "Sad", - "@reactionTypeSad": { - "description": "Reaction type: Sad" - }, - "reactionTypeAngry": "Angry", - "@reactionTypeAngry": { - "description": "Reaction type: Angry" - }, - "reactionTypeSkeptical": "Skeptical", - "@reactionTypeSkeptical": { - "description": "Reaction type: Skeptical" - }, - "commentStatusPendingReview": "Pending", - "@commentStatusPendingReview": { - "description": "Comment status: Pending Review" - }, - "commentStatusApproved": "Approved", - "@commentStatusApproved": { - "description": "Comment status: Approved" - }, - "commentStatusRejected": "Rejected", - "@commentStatusRejected": { - "description": "Comment status: Rejected" - }, - "reportStatusSubmitted": "Submitted", - "@reportStatusSubmitted": { - "description": "Report status: Submitted" - }, - "reportStatusInReview": "In Review", - "@reportStatusInReview": { - "description": "Report status: In Review" - }, - "reportStatusResolved": "Resolved", - "@reportStatusResolved": { - "description": "Report status: Resolved" - }, - "initialAppReviewFeedbackPositive": "Positive", - "@initialAppReviewFeedbackPositive": { - "description": "Initial app review feedback: Positive" - }, - "initialAppReviewFeedbackNegative": "Negative", - "@initialAppReviewFeedbackNegative": { - "description": "Initial app review feedback: Negative" - }, - "filterCommunity": "Filter Community Content", - "@filterCommunity": { - "description": "Action to filter community content" - }, - "searchByEngagementUser": "Search by user email...", - "@searchByEngagementUser": { - "description": "Hint text for searching by engagement user" - }, - "searchByReportReporter": "Search by reporter email...", - "@searchByReportReporter": { - "description": "Hint text for searching by report reporter" - }, - "searchByAppReviewUser": "Search by user email...", - "@searchByAppReviewUser": { - "description": "Hint text for searching by app review user" - }, - "selectCommentStatus": "Select Comment Status", - "@selectCommentStatus": { - "description": "Action to select comment status" - }, - "selectReportStatus": "Select Report Status", - "@selectReportStatus": { - "description": "Action to select report status" - }, - "selectInitialFeedback": "Select Initial Feedback", - "@selectInitialFeedback": { - "description": "Action to select initial feedback" - }, - "selectReportableEntity": "Select Reported Item Type", - "@selectReportableEntity": { - "description": "Action to select reportable entity" - }, - "reportableEntityHeadline": "Headline", - "@reportableEntityHeadline": { - "description": "Reportable entity: Headline" - }, - "reportableEntitySource": "Source", - "@reportableEntitySource": { - "description": "Reportable entity: Source" - }, - "reportableEntityComment": "Comment", - "@reportableEntityComment": { - "description": "Reportable entity: Comment" - }, - "noEngagementsFound": "No engagements found.", - "@noEngagementsFound": { - "description": "Message when no engagements are found" - }, - "noReportsFound": "No reports found.", - "@noReportsFound": { - "description": "Message when no reports are found" - }, - "noAppReviewsFound": "No app reviews found.", - "@noAppReviewsFound": { - "description": "Message when no app reviews are found" - }, - "loadingEngagements": "Loading Engagements", - "@loadingEngagements": { - "description": "Message when engagements are loading" - }, - "loadingReports": "Loading Reports", - "@loadingReports": { - "description": "Message when reports are loading" - }, - "loadingAppReviews": "Loading App Reviews", - "@loadingAppReviews": { - "description": "Message when app reviews are loading" - }, - "userIdCopied": "User ID copied to clipboard.", - "@userIdCopied": { - "description": "Message when user ID is copied" - }, - "commentApproved": "Comment approved.", - "@commentApproved": { - "description": "Message when a comment is approved" - }, - "commentRejected": "Comment rejected.", - "@commentRejected": { - "description": "Message when a comment is rejected" - }, - "reportStatusUpdated": "Report status updated.", - "@reportStatusUpdated": { - "description": "Message when a report status is updated" - }, - "feedbackHistoryForUser": "Feedback History for {email}", - "@feedbackHistoryForUser": { - "description": "Message displaying feedback history for a user", - "placeholders": { - "email": { - "type": "String" - } + "communityManagement": "Community", + "@communityManagement": { + "description": "Label for the community management navigation item" + }, + "communityManagementPageDescription": "Manage user-generated content including engagements (reactions and comments), content reports, and app reviews.", + "@communityManagementPageDescription": { + "description": "Description for the Community Management page" + }, + "engagements": "Engagements", + "@engagements": { + "description": "Label for the engagements subpage" + }, + "reports": "Reports", + "@reports": { + "description": "Label for the reports subpage" + }, + "appReviews": "App Reviews", + "@appReviews": { + "description": "Label for the app reviews subpage" + }, + "user": "User", + "@user": { + "description": "Column header for user" + }, + "engagedContent": "Engaged Content", + "@engagedContent": { + "description": "Column header for engaged content" + }, + "reaction": "Reaction", + "@reaction": { + "description": "Column header for reaction" + }, + "comment": "Comment", + "@comment": { + "description": "Column header for comment" + }, + "commentStatus": "Comment Status", + "@commentStatus": { + "description": "Column header for comment status" + }, + "date": "Date", + "@date": { + "description": "Column header for date" + }, + "approveComment": "Approve Comment", + "@approveComment": { + "description": "Action to approve a comment" + }, + "rejectComment": "Reject Comment", + "@rejectComment": { + "description": "Action to reject a comment" + }, + "viewEngagedContent": "View Content", + "@viewEngagedContent": { + "description": "Action to view the engaged content" + }, + "copyUserId": "Copy User ID", + "@copyUserId": { + "description": "Action to copy the user ID" + }, + "reporter": "Reporter", + "@reporter": { + "description": "Column header for reporter" + }, + "reportedItem": "Reported Item", + "@reportedItem": { + "description": "Column header for reported item" + }, + "reason": "Reason", + "@reason": { + "description": "Column header for reason" + }, + "reportStatus": "Report Status", + "@reportStatus": { + "description": "Column header for report status" + }, + "viewReportedItem": "View Item", + "@viewReportedItem": { + "description": "Action to view the reported item" + }, + "markAsInReview": "Mark as In Review", + "@markAsInReview": { + "description": "Action to mark a report as in review" + }, + "resolveReport": "Resolve Report", + "@resolveReport": { + "description": "Action to resolve a report" + }, + "initialFeedback": "Initial Feedback", + "@initialFeedback": { + "description": "Column header for initial feedback" + }, + "osPromptRequested": "OS Prompt?", + "@osPromptRequested": { + "description": "Column header for OS prompt requested" + }, + "feedbackHistory": "Feedback History", + "@feedbackHistory": { + "description": "Column header for feedback history" + }, + "lastInteraction": "Last Interaction", + "@lastInteraction": { + "description": "Column header for last interaction" + }, + "viewFeedbackHistory": "View History", + "@viewFeedbackHistory": { + "description": "Action to view feedback history" + }, + "reactionTypeLike": "Like", + "@reactionTypeLike": { + "description": "Reaction type: Like" + }, + "reactionTypeInsightful": "Insightful", + "@reactionTypeInsightful": { + "description": "Reaction type: Insightful" + }, + "reactionTypeAmusing": "Amusing", + "@reactionTypeAmusing": { + "description": "Reaction type: Amusing" + }, + "reactionTypeSad": "Sad", + "@reactionTypeSad": { + "description": "Reaction type: Sad" + }, + "reactionTypeAngry": "Angry", + "@reactionTypeAngry": { + "description": "Reaction type: Angry" + }, + "reactionTypeSkeptical": "Skeptical", + "@reactionTypeSkeptical": { + "description": "Reaction type: Skeptical" + }, + "commentStatusPendingReview": "Pending", + "@commentStatusPendingReview": { + "description": "Comment status: Pending Review" + }, + "commentStatusApproved": "Approved", + "@commentStatusApproved": { + "description": "Comment status: Approved" + }, + "commentStatusRejected": "Rejected", + "@commentStatusRejected": { + "description": "Comment status: Rejected" + }, + "reportStatusSubmitted": "Submitted", + "@reportStatusSubmitted": { + "description": "Report status: Submitted" + }, + "reportStatusInReview": "In Review", + "@reportStatusInReview": { + "description": "Report status: In Review" + }, + "reportStatusResolved": "Resolved", + "@reportStatusResolved": { + "description": "Report status: Resolved" + }, + "initialAppReviewFeedbackPositive": "Positive", + "@initialAppReviewFeedbackPositive": { + "description": "Initial app review feedback: Positive" + }, + "initialAppReviewFeedbackNegative": "Negative", + "@initialAppReviewFeedbackNegative": { + "description": "Initial app review feedback: Negative" + }, + "filterCommunity": "Filter Community Content", + "@filterCommunity": { + "description": "Action to filter community content" + }, + "searchByEngagementUser": "Search by user email...", + "@searchByEngagementUser": { + "description": "Hint text for searching by engagement user" + }, + "searchByReportReporter": "Search by reporter email...", + "@searchByReportReporter": { + "description": "Hint text for searching by report reporter" + }, + "searchByAppReviewUser": "Search by user email...", + "@searchByAppReviewUser": { + "description": "Hint text for searching by app review user" + }, + "selectCommentStatus": "Select Comment Status", + "@selectCommentStatus": { + "description": "Action to select comment status" + }, + "selectReportStatus": "Select Report Status", + "@selectReportStatus": { + "description": "Action to select report status" + }, + "selectInitialFeedback": "Select Initial Feedback", + "@selectInitialFeedback": { + "description": "Action to select initial feedback" + }, + "selectReportableEntity": "Select Reported Item Type", + "@selectReportableEntity": { + "description": "Action to select reportable entity" + }, + "reportableEntityHeadline": "Headline", + "@reportableEntityHeadline": { + "description": "Reportable entity: Headline" + }, + "reportableEntitySource": "Source", + "@reportableEntitySource": { + "description": "Reportable entity: Source" + }, + "reportableEntityComment": "Comment", + "@reportableEntityComment": { + "description": "Reportable entity: Comment" + }, + "noEngagementsFound": "No engagements found.", + "@noEngagementsFound": { + "description": "Message when no engagements are found" + }, + "noReportsFound": "No reports found.", + "@noReportsFound": { + "description": "Message when no reports are found" + }, + "noAppReviewsFound": "No app reviews found.", + "@noAppReviewsFound": { + "description": "Message when no app reviews are found" + }, + "loadingEngagements": "Loading Engagements", + "@loadingEngagements": { + "description": "Message when engagements are loading" + }, + "loadingReports": "Loading Reports", + "@loadingReports": { + "description": "Message when reports are loading" + }, + "loadingAppReviews": "Loading App Reviews", + "@loadingAppReviews": { + "description": "Message when app reviews are loading" + }, + "userIdCopied": "User ID copied to clipboard.", + "@userIdCopied": { + "description": "Message when user ID is copied" + }, + "commentApproved": "Comment approved.", + "@commentApproved": { + "description": "Message when a comment is approved" + }, + "commentRejected": "Comment rejected.", + "@commentRejected": { + "description": "Message when a comment is rejected" + }, + "reportStatusUpdated": "Report status updated.", + "@reportStatusUpdated": { + "description": "Message when a report status is updated" + }, + "feedbackHistoryForUser": "Feedback History for {email}", + "@feedbackHistoryForUser": { + "description": "Message displaying feedback history for a user", + "placeholders": { + "email": { + "type": "String" } - }, - "noFeedbackHistory": "No feedback history available for this user.", - "@noFeedbackHistory": { - "description": "Message when no feedback history is available" - }, - "feedbackProvidedAt": "Feedback provided at: {date}", - "@feedbackProvidedAt": { - "description": "Message displaying the date feedback was provided", - "placeholders": { - "date": { - "type": "String" - } + } + }, + "noFeedbackHistory": "No feedback history available for this user.", + "@noFeedbackHistory": { + "description": "Message when no feedback history is available" + }, + "feedbackProvidedAt": "Feedback provided at: {date}", + "@feedbackProvidedAt": { + "description": "Message displaying the date feedback was provided", + "placeholders": { + "date": { + "type": "String" } - }, - "feedbackReason": "Reason: {reason}", - "@feedbackReason": { - "description": "Message displaying the reason for feedback", - "placeholders": { - "reason": { - "type": "String" - } + } + }, + "feedbackReason": "Reason: {reason}", + "@feedbackReason": { + "description": "Message displaying the reason for feedback", + "placeholders": { + "reason": { + "type": "String" } - }, - "noReasonProvided": "No reason provided.", - "@noReasonProvided": { - "description": "Message when no reason for feedback is provided" - }, - "yes": "Yes", - "@yes": { - "description": "A generic 'Yes' response." - }, - "no": "No", - "@no": { - "description": "A generic 'No' response." + } + }, + "noReasonProvided": "No reason provided.", + "@noReasonProvided": { + "description": "Message when no reason for feedback is provided" + }, + "yes": "Yes", + "@yes": { + "description": "A generic 'Yes' response." + }, + "no": "No", + "@no": { + "description": "A generic 'No' response." }, "commentStatusFlaggedByAI": "Flagged by AI", "@commentStatusFlaggedByAI": { @@ -2628,9 +2628,25 @@ "commentStatusFlaggedByAi": "Flagged by AI", "@commentStatusFlaggedByAi": { "description": "Admin-centric status for a comment automatically flagged by AI." - }, - "cancel": "Cancel", - "@cancel": { - "description": "A generic 'Cancel' button text." - } + }, + "cancel": "Cancel", + "@cancel": { + "description": "A generic 'Cancel' button text." + }, + "viewReportedHeadline": "View Headline", + "@viewReportedHeadline": { + "description": "Action to view the reported headline" + }, + "viewReportedSource": "View Source", + "@viewReportedSource": { + "description": "Action to view the reported source" + }, + "viewReportedComment": "View Comment", + "@viewReportedComment": { + "description": "Action to view the reported comment" + }, + "entityType": "Entity Type", + "@entityType": { + "description": "Column header for the type of entity being reported" + } } \ No newline at end of file From e3b55eabcdd788cb979d36bf295e7b7cbc5d0a7a Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:20:26 +0100 Subject: [PATCH 056/130] feat(community_management): enhance data table columns and tooltips - Update app reviews page to use feedback history instead of initial feedback - Improve engagements page by adding tooltip for comment content - Modify reports page to include entity type and update status column label - Enhance view report tooltips to be more specific based on entity type --- .../view/app_reviews_page.dart | 2 +- .../view/engagements_page.dart | 22 ++++++++----------- .../view/reports_page.dart | 7 +++++- .../widgets/community_action_buttons.dart | 6 ++++- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/community_management/view/app_reviews_page.dart b/lib/community_management/view/app_reviews_page.dart index 33b48d1c..e305c68d 100644 --- a/lib/community_management/view/app_reviews_page.dart +++ b/lib/community_management/view/app_reviews_page.dart @@ -115,7 +115,7 @@ class _AppReviewsPageState extends State { columns: [ DataColumn2(label: Text(l10n.user), size: ColumnSize.L), DataColumn2( - label: Text(l10n.initialFeedback), + label: Text(l10n.feedbackHistory), size: ColumnSize.M, ), if (!isMobile) diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index a6e5e384..e85eab49 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -209,21 +209,17 @@ class _EngagementsDataSource extends DataTableSource { cells: [ DataCell(Text(engagement.userId, overflow: TextOverflow.ellipsis)), DataCell(Text(engagement.reaction.reactionType.name)), - if (!isMobile) - DataCell(Text(engagement.comment?.content ?? l10n.notAvailable)), - DataCell( - Tooltip( - message: engagement.comment?.content ?? l10n.notAvailable, - child: Text( - engagement.comment?.content != null - ? (engagement.comment!.content.length > 50 - ? '${engagement.comment!.content.substring(0, 47)}...' - : engagement.comment!.content) - : l10n.notAvailable, - overflow: TextOverflow.ellipsis, + if (!isMobile) ...[ + DataCell( + Tooltip( + message: engagement.comment?.content ?? l10n.notAvailable, + child: Text( + engagement.comment?.content ?? l10n.notAvailable, + overflow: TextOverflow.ellipsis, + ), ), ), - ), + ], DataCell( Text( engagement.comment?.status.l10n(context) ?? l10n.notAvailable, diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index d91593a1..9ad390a3 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -116,13 +116,17 @@ class _ReportsPageState extends State { label: Text(l10n.reporter), size: ColumnSize.L, ), + DataColumn2( + label: Text(l10n.entityType), + size: ColumnSize.S, + ), DataColumn2( label: Text(l10n.reason), size: ColumnSize.M, ), if (!isMobile) DataColumn2( - label: Text(l10n.reportStatus), + label: Text(l10n.status), size: ColumnSize.S, ), if (!isMobile) @@ -207,6 +211,7 @@ class _ReportsDataSource extends DataTableSource { DataCell( Text(report.reporterUserId, overflow: TextOverflow.ellipsis), ), + DataCell(Text(report.entityType.l10n(context))), DataCell(Text(report.reason.l10n(context))), if (!isMobile) DataCell( diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index 4312104f..58d6de83 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -125,7 +125,11 @@ class CommunityActionButtons extends StatelessWidget { visualDensity: VisualDensity.compact, iconSize: 20, icon: const Icon(Icons.visibility_outlined), - tooltip: l10n.viewReportedItem, + tooltip: switch (report.entityType) { + ReportableEntity.headline => l10n.viewReportedHeadline, + ReportableEntity.source => l10n.viewReportedSource, + ReportableEntity.engagement => l10n.viewReportedComment, + }, onPressed: () { final String routeName; switch (report.entityType) { From c76ee91203bf8775e9d5c13cff5997a727436796 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:44:41 +0100 Subject: [PATCH 057/130] feat(l10n): add Arabic and English translations for feedback feature - Add Arabic translations for new feedback-related strings - Add English translations for new feedback-related strings - Include translations for: - feedback - feedbackDetails - viewFeedbackDetails - Update arb files for both Arabic and English --- lib/l10n/app_localizations.dart | 18 ++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 9 +++++++++ lib/l10n/app_localizations_en.dart | 9 +++++++++ lib/l10n/arb/app_ar.arb | 12 ++++++++++++ lib/l10n/arb/app_en.arb | 12 ++++++++++++ 5 files changed, 60 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 5e5288da..e67362cb 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3925,6 +3925,24 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Entity Type'** String get entityType; + + /// Column header for user feedback on app reviews. + /// + /// In en, this message translates to: + /// **'Feedback'** + String get feedback; + + /// Title for the dialog showing detailed user feedback. + /// + /// In en, this message translates to: + /// **'Feedback Details'** + String get feedbackDetails; + + /// Tooltip for the button to view feedback details. + /// + /// In en, this message translates to: + /// **'View Feedback Details'** + String get viewFeedbackDetails; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 2f5cff9e..d7b53344 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -2103,4 +2103,13 @@ class AppLocalizationsAr extends AppLocalizations { @override String get entityType => 'نوع الكيان'; + + @override + String get feedback => 'التقييم'; + + @override + String get feedbackDetails => 'تفاصيل التقييم'; + + @override + String get viewFeedbackDetails => 'عرض تفاصيل التقييم'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f8f5648b..a9389620 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2109,4 +2109,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get entityType => 'Entity Type'; + + @override + String get feedback => 'Feedback'; + + @override + String get feedbackDetails => 'Feedback Details'; + + @override + String get viewFeedbackDetails => 'View Feedback Details'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 8ce7891c..27938b80 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2652,5 +2652,17 @@ "entityType": "نوع الكيان", "@entityType": { "description": "Column header for the type of entity being reported" + }, + "feedback": "التقييم", + "@feedback": { + "description": "Column header for user feedback on app reviews." + }, + "feedbackDetails": "تفاصيل التقييم", + "@feedbackDetails": { + "description": "Title for the dialog showing detailed user feedback." + }, + "viewFeedbackDetails": "عرض تفاصيل التقييم", + "@viewFeedbackDetails": { + "description": "Tooltip for the button to view feedback details." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 152f961b..62b20d0a 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2648,5 +2648,17 @@ "entityType": "Entity Type", "@entityType": { "description": "Column header for the type of entity being reported" + }, + "feedback": "Feedback", + "@feedback": { + "description": "Column header for user feedback on app reviews." + }, + "feedbackDetails": "Feedback Details", + "@feedbackDetails": { + "description": "Title for the dialog showing detailed user feedback." + }, + "viewFeedbackDetails": "View Feedback Details", + "@viewFeedbackDetails": { + "description": "Tooltip for the button to view feedback details." } } \ No newline at end of file From a949488dd81a463950777dd9f886bfbc0ebcb733 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:49:08 +0100 Subject: [PATCH 058/130] refactor(app_reviews_page): improve feedback display and table layout - Remove user ID column from the reviews table - Rename "feedbackHistory" to "feedback" for better clarity - Replace feedback text with a Chip widget containing an icon and colored background - Add helper functions for determining feedback color and icon --- .../view/app_reviews_page.dart | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/lib/community_management/view/app_reviews_page.dart b/lib/community_management/view/app_reviews_page.dart index e305c68d..8e9a34cb 100644 --- a/lib/community_management/view/app_reviews_page.dart +++ b/lib/community_management/view/app_reviews_page.dart @@ -113,9 +113,8 @@ class _AppReviewsPageState extends State { final isMobile = constraints.maxWidth < 600; return PaginatedDataTable2( columns: [ - DataColumn2(label: Text(l10n.user), size: ColumnSize.L), DataColumn2( - label: Text(l10n.feedbackHistory), + label: Text(l10n.feedback), size: ColumnSize.M, ), if (!isMobile) @@ -197,8 +196,17 @@ class _AppReviewsDataSource extends DataTableSource { final appReview = appReviews[index]; return DataRow2( cells: [ - DataCell(Text(appReview.userId, overflow: TextOverflow.ellipsis)), - DataCell(Text(appReview.feedback.l10n(context))), + DataCell( + Chip( + avatar: Icon( + _getFeedbackIcon(appReview.feedback), + size: 16, + ), + label: Text(appReview.feedback.l10n(context)), + backgroundColor: _getFeedbackColor(context, appReview.feedback), + side: BorderSide.none, + ), + ), if (!isMobile) DataCell( Text( @@ -218,4 +226,23 @@ class _AppReviewsDataSource extends DataTableSource { @override int get selectedRowCount => 0; + + Color? _getFeedbackColor(BuildContext context, AppReviewFeedback feedback) { + final colorScheme = Theme.of(context).colorScheme; + switch (feedback) { + case AppReviewFeedback.positive: + return colorScheme.primaryContainer.withOpacity(0.5); + case AppReviewFeedback.negative: + return colorScheme.errorContainer.withOpacity(0.5); + } + } + + IconData _getFeedbackIcon(AppReviewFeedback feedback) { + switch (feedback) { + case AppReviewFeedback.positive: + return Icons.thumb_up_outlined; + case AppReviewFeedback.negative: + return Icons.thumb_down_outlined; + } + } } From bb23a63575226da11658b073df2a0ab3dc4c89ce Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:49:29 +0100 Subject: [PATCH 059/130] feat(community_management): enhance engagements page with chips - Remove user column from PaginatedDataTable2 - Replace reaction and comment status text with Chip widgets - Add visualDensity.compact to chips for a compact appearance - Implement colored background for comment status chips based on status --- .../view/engagements_page.dart | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index e85eab49..00a6831c 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -114,7 +114,6 @@ class _EngagementsPageState extends State { final isMobile = constraints.maxWidth < 600; return PaginatedDataTable2( columns: [ - DataColumn2(label: Text(l10n.user), size: ColumnSize.L), DataColumn2( label: Text(l10n.reaction), size: ColumnSize.S, @@ -207,8 +206,12 @@ class _EngagementsDataSource extends DataTableSource { final engagement = engagements[index]; return DataRow2( cells: [ - DataCell(Text(engagement.userId, overflow: TextOverflow.ellipsis)), - DataCell(Text(engagement.reaction.reactionType.name)), + DataCell( + Chip( + label: Text(engagement.reaction.reactionType.name), + visualDensity: VisualDensity.compact, + ), + ), if (!isMobile) ...[ DataCell( Tooltip( @@ -221,8 +224,16 @@ class _EngagementsDataSource extends DataTableSource { ), ], DataCell( - Text( - engagement.comment?.status.l10n(context) ?? l10n.notAvailable, + Chip( + label: Text( + engagement.comment?.status.l10n(context) ?? l10n.notAvailable, + ), + backgroundColor: _getCommentStatusColor( + context, + engagement.comment?.status, + ), + side: BorderSide.none, + visualDensity: VisualDensity.compact, ), ), if (!isMobile) @@ -244,4 +255,21 @@ class _EngagementsDataSource extends DataTableSource { @override int get selectedRowCount => 0; + + Color? _getCommentStatusColor( + BuildContext context, + CommentStatus? status, + ) { + final colorScheme = Theme.of(context).colorScheme; + switch (status) { + case CommentStatus.approved: + return colorScheme.primaryContainer.withOpacity(0.5); + case CommentStatus.rejected: + return colorScheme.errorContainer.withOpacity(0.5); + case CommentStatus.pendingReview: + return colorScheme.tertiaryContainer.withOpacity(0.5); + default: + return colorScheme.surfaceVariant; + } + } } From fc3ed09a7aaf27a73c16981359873e83561cda09 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:49:45 +0100 Subject: [PATCH 060/130] refactor.community_management: improve visual representation of reports - Remove reporter column from DataTable - Use Chip widgets for entity type and reason to enhance UI - Replace status Row with Chip for better visual consistency - Add color coding for report status using Chip background color - Improve mobile view by using Chip for status --- .../view/reports_page.dart | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index 9ad390a3..02996474 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -112,10 +112,6 @@ class _ReportsPageState extends State { final isMobile = constraints.maxWidth < 600; return PaginatedDataTable2( columns: [ - DataColumn2( - label: Text(l10n.reporter), - size: ColumnSize.L, - ), DataColumn2( label: Text(l10n.entityType), size: ColumnSize.S, @@ -209,25 +205,28 @@ class _ReportsDataSource extends DataTableSource { return DataRow2( cells: [ DataCell( - Text(report.reporterUserId, overflow: TextOverflow.ellipsis), + Chip( + label: Text(report.entityType.l10n(context)), + visualDensity: VisualDensity.compact, + ), + ), + DataCell( + Chip( + label: Text(report.reason.l10n(context)), + visualDensity: VisualDensity.compact, + ), ), - DataCell(Text(report.entityType.l10n(context))), - DataCell(Text(report.reason.l10n(context))), if (!isMobile) DataCell( - Row( - children: [ - Icon( - report.status == ReportStatus.resolved - ? Icons.check_circle_outline - : Icons.info_outline, - color: report.status == ReportStatus.resolved - ? Colors.green - : Colors.orange, - ), - const SizedBox(width: AppSpacing.xs), - Text(report.status.l10n(context)), - ], + Chip( + avatar: Icon( + _getReportStatusIcon(report.status), + size: 16, + ), + label: Text(report.status.l10n(context)), + backgroundColor: _getReportStatusColor(context, report.status), + side: BorderSide.none, + visualDensity: VisualDensity.compact, ), ), if (!isMobile) @@ -247,4 +246,27 @@ class _ReportsDataSource extends DataTableSource { @override int get selectedRowCount => 0; + + Color? _getReportStatusColor(BuildContext context, ReportStatus status) { + final colorScheme = Theme.of(context).colorScheme; + switch (status) { + case ReportStatus.resolved: + return colorScheme.primaryContainer.withOpacity(0.5); + case ReportStatus.submitted: + return colorScheme.tertiaryContainer.withOpacity(0.5); + case ReportStatus.inReview: + return colorScheme.secondaryContainer.withOpacity(0.5); + } + } + + IconData _getReportStatusIcon(ReportStatus status) { + switch (status) { + case ReportStatus.resolved: + return Icons.check_circle_outline; + case ReportStatus.submitted: + return Icons.info_outline; + case ReportStatus.inReview: + return Icons.hourglass_empty_outlined; + } + } } From 25d86808e459ea6b3f7d17d1dd48b87365109885 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 04:50:04 +0100 Subject: [PATCH 061/130] feat(community): add copy user ID action and improve feedback details - Add copy user ID option to the overflow menu in community action buttons - Refactor feedback details action to always show as an icon button - Update tooltip text to "View feedback details" - Update dialog title to match tooltip text --- .../widgets/community_action_buttons.dart | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index 58d6de83..f79e9d86 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -120,6 +120,9 @@ class CommunityActionButtons extends StatelessWidget { List> overflowMenuItems, ) { // Primary Action + overflowMenuItems.add( + PopupMenuItem(value: 'copyUserId', child: Text(l10n.copyUserId)), + ); visibleActions.add( IconButton( visualDensity: VisualDensity.compact, @@ -184,24 +187,24 @@ class CommunityActionButtons extends StatelessWidget { List> overflowMenuItems, ) { // Primary Action - if (appReview.feedbackDetails != null && - appReview.feedbackDetails!.isNotEmpty) { - visibleActions.add( - IconButton( - visualDensity: VisualDensity.compact, - iconSize: 20, - icon: const Icon(Icons.comment_outlined), - tooltip: l10n.viewFeedbackHistory, - onPressed: () { - showDialog( - context: context, - builder: (dialogContext) => - _FeedbackDetailsDialog(appReview: appReview, l10n: l10n), - ); - }, - ), - ); - } + final hasDetails = + appReview.feedbackDetails != null && + appReview.feedbackDetails!.isNotEmpty; + visibleActions.add( + IconButton( + visualDensity: VisualDensity.compact, + iconSize: 20, + icon: const Icon(Icons.comment_outlined), + tooltip: l10n.viewFeedbackDetails, + onPressed: hasDetails + ? () => showDialog( + context: context, + builder: (_) => + _FeedbackDetailsDialog(appReview: appReview, l10n: l10n), + ) + : null, + ), + ); // Secondary Actions overflowMenuItems.add( @@ -303,7 +306,7 @@ class _FeedbackDetailsDialog extends StatelessWidget { final details = appReview.feedbackDetails; return AlertDialog( - title: Text(l10n.feedbackHistory), + title: Text(l10n.feedbackDetails), content: SizedBox( width: double.maxFinite, child: SingleChildScrollView( From fdf9ee3c12460df93dc77ff73315010ef5bed262 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 05:27:20 +0100 Subject: [PATCH 062/130] refactor(localization): simplify comment rejection confirmation messages - Update Arabic and English localization files with shorter, more direct confirmation messages for rejecting and deleting comments - Reorder some localization entries for better organization --- lib/l10n/app_localizations.dart | 24 ++++++++++++------------ lib/l10n/app_localizations_ar.dart | 14 +++++++------- lib/l10n/app_localizations_en.dart | 14 +++++++------- lib/l10n/arb/app_ar.arb | 16 ++++++++-------- lib/l10n/arb/app_en.arb | 16 ++++++++-------- 5 files changed, 42 insertions(+), 42 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e67362cb..ea9a0276 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3866,18 +3866,6 @@ abstract class AppLocalizations { /// **'Copy Reported Item ID'** String get copyReportedItemId; - /// Confirmation message shown to an admin before they reject a user's comment. - /// - /// In en, this message translates to: - /// **'Rejecting this comment will permanently mark it as \'Rejected\' and hide it from public view. This action does not delete the parent engagement. Are you sure you want to proceed?'** - String get rejectCommentConfirmation; - - /// Hint text for the search input field in filter dialogs, specifying to search by User ID. - /// - /// In en, this message translates to: - /// **'Search by User ID...'** - String get searchByUserId; - /// Message displayed in the feedback history dialog when there is no history. /// /// In en, this message translates to: @@ -3902,6 +3890,18 @@ abstract class AppLocalizations { /// **'Cancel'** String get cancel; + /// A simplified confirmation message shown to an admin before they reject and delete a user's comment. + /// + /// In en, this message translates to: + /// **'Are you sure you want to reject and permanently delete this comment? This action cannot be undone.'** + String get rejectCommentConfirmation; + + /// Hint text for the search input field in filter dialogs, specifying to search by User ID. + /// + /// In en, this message translates to: + /// **'Search by User ID...'** + String get searchByUserId; + /// Action to view the reported headline /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index d7b53344..3d3de0d4 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -2072,13 +2072,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get copyReportedItemId => 'نسخ معرّف العنصر المُبلغ عنه'; - @override - String get rejectCommentConfirmation => - 'سيؤدي رفض هذا التعليق إلى تمييزه بشكل دائم على أنه \'مرفوض\' وإخفائه عن العرض العام. هذا الإجراء لا يحذف التفاعل الأصلي. هل أنت متأكد من أنك تريد المتابعة؟'; - - @override - String get searchByUserId => 'ابحث باستخدام معرّف المستخدم...'; - @override String get noNegativeFeedbackHistory => 'لم يتم العثور على سجل تقييمات سلبية لهذا المستخدم.'; @@ -2092,6 +2085,13 @@ class AppLocalizationsAr extends AppLocalizations { @override String get cancel => 'إلغاء'; + @override + String get rejectCommentConfirmation => + 'هل أنت متأكد أنك تريد رفض وحذف هذا التعليق نهائيًا؟ لا يمكن التراجع عن هذا الإجراء.'; + + @override + String get searchByUserId => 'ابحث باستخدام معرّف المستخدم...'; + @override String get viewReportedHeadline => 'عرض العنوان'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a9389620..49ac826a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2078,13 +2078,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get copyReportedItemId => 'Copy Reported Item ID'; - @override - String get rejectCommentConfirmation => - 'Rejecting this comment will permanently mark it as \'Rejected\' and hide it from public view. This action does not delete the parent engagement. Are you sure you want to proceed?'; - - @override - String get searchByUserId => 'Search by User ID...'; - @override String get noNegativeFeedbackHistory => 'No negative feedback history found for this user.'; @@ -2098,6 +2091,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cancel => 'Cancel'; + @override + String get rejectCommentConfirmation => + 'Are you sure you want to reject and permanently delete this comment? This action cannot be undone.'; + + @override + String get searchByUserId => 'Search by User ID...'; + @override String get viewReportedHeadline => 'View Headline'; diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 27938b80..12d0f0e2 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2613,14 +2613,6 @@ "@copyReportedItemId": { "description": "نص عنصر القائمة لنسخ معرّف العنصر المُبلغ عنه." }, - "rejectCommentConfirmation": "سيؤدي رفض هذا التعليق إلى تمييزه بشكل دائم على أنه 'مرفوض' وإخفائه عن العرض العام. هذا الإجراء لا يحذف التفاعل الأصلي. هل أنت متأكد من أنك تريد المتابعة؟", - "@rejectCommentConfirmation": { - "description": "رسالة تأكيد تظهر للمسؤول قبل رفض تعليق مستخدم." - }, - "searchByUserId": "ابحث باستخدام معرّف المستخدم...", - "@searchByUserId": { - "description": "نص تلميحي لحقل البحث في مربعات حوار التصفية، يحدد البحث باستخدام معرّف المستخدم." - }, "noNegativeFeedbackHistory": "لم يتم العثور على سجل تقييمات سلبية لهذا المستخدم.", "@noNegativeFeedbackHistory": { "description": "رسالة تظهر في مربع حوار سجل التقييمات عند عدم وجود سجل." @@ -2637,6 +2629,14 @@ "@cancel": { "description": "A generic 'Cancel' button text." }, + "rejectCommentConfirmation": "هل أنت متأكد أنك تريد رفض وحذف هذا التعليق نهائيًا؟ لا يمكن التراجع عن هذا الإجراء.", + "@rejectCommentConfirmation": { + "description": "A simplified confirmation message shown to an admin before they reject and delete a user's comment." + }, + "searchByUserId": "ابحث باستخدام معرّف المستخدم...", + "@searchByUserId": { + "description": "Hint text for the search input field in filter dialogs, specifying to search by User ID." + }, "viewReportedHeadline": "عرض العنوان", "@viewReportedHeadline": { "description": "Action to view the reported headline" diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 62b20d0a..8b56e281 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2609,14 +2609,6 @@ "@copyReportedItemId": { "description": "Menu item text to copy the ID of a reported item." }, - "rejectCommentConfirmation": "Rejecting this comment will permanently mark it as 'Rejected' and hide it from public view. This action does not delete the parent engagement. Are you sure you want to proceed?", - "@rejectCommentConfirmation": { - "description": "Confirmation message shown to an admin before they reject a user's comment." - }, - "searchByUserId": "Search by User ID...", - "@searchByUserId": { - "description": "Hint text for the search input field in filter dialogs, specifying to search by User ID." - }, "noNegativeFeedbackHistory": "No negative feedback history found for this user.", "@noNegativeFeedbackHistory": { "description": "Message displayed in the feedback history dialog when there is no history." @@ -2633,6 +2625,14 @@ "@cancel": { "description": "A generic 'Cancel' button text." }, + "rejectCommentConfirmation": "Are you sure you want to reject and permanently delete this comment? This action cannot be undone.", + "@rejectCommentConfirmation": { + "description": "A simplified confirmation message shown to an admin before they reject and delete a user's comment." + }, + "searchByUserId": "Search by User ID...", + "@searchByUserId": { + "description": "Hint text for the search input field in filter dialogs, specifying to search by User ID." + }, "viewReportedHeadline": "View Headline", "@viewReportedHeadline": { "description": "Action to view the reported headline" From af6eb53b5ddf40921cb4e854c941945d507a2618 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 05:27:39 +0100 Subject: [PATCH 063/130] refactor: simplify search hint in community filter dialog - Remove tab-specific search hints - Use generic "Search by user ID" hint instead - Delete _getSearchHint method --- .../community_filter_dialog.dart | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index f77cff8e..36a166c0 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -95,10 +95,7 @@ class _CommunityFilterDialogState extends State { controller: _searchController, decoration: InputDecoration( labelText: l10n.search, - hintText: _getSearchHint( - filterDialogState.activeTab, - l10n, - ), + hintText: l10n.searchByUserId, prefixIcon: const Icon(Icons.search), border: const OutlineInputBorder(), ), @@ -119,17 +116,6 @@ class _CommunityFilterDialogState extends State { ); } - String _getSearchHint(CommunityManagementTab tab, AppLocalizations l10n) { - switch (tab) { - case CommunityManagementTab.engagements: - return l10n.searchByEngagementUser; - case CommunityManagementTab.reports: - return l10n.searchByReportReporter; - case CommunityManagementTab.appReviews: - return l10n.searchByAppReviewUser; - } - } - List _buildTabSpecificFilters( CommunityFilterDialogState state, AppLocalizations l10n, From 9356ccc94f2d3928961162d64b4340563f877bc0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 05:27:57 +0100 Subject: [PATCH 064/130] fix(community): Remove regex for exact match in user search - Remove regex usage for userId in engagements, reports, and app reviews filters - Implement exact match for userId instead of case-insensitive search - Simplify search query implementation in CommunityManagementBloc --- .../bloc/community_management_bloc.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 6ef064f8..d07e3734 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -109,7 +109,7 @@ class CommunityManagementBloc Map buildEngagementsFilterMap(CommunityFilterState state) { final filter = {}; if (state.searchQuery.isNotEmpty) { - filter['userId'] = {r'$regex': state.searchQuery, r'$options': 'i'}; + filter['userId'] = state.searchQuery; } if (state.selectedCommentStatus.isNotEmpty) { filter['comment.status'] = { @@ -122,10 +122,7 @@ class CommunityManagementBloc Map buildReportsFilterMap(CommunityFilterState state) { final filter = {}; if (state.searchQuery.isNotEmpty) { - filter['reporterUserId'] = { - r'$regex': state.searchQuery, - r'$options': 'i', - }; + filter['reporterUserId'] = state.searchQuery; } if (state.selectedReportStatus.isNotEmpty) { filter['status'] = { @@ -143,7 +140,7 @@ class CommunityManagementBloc Map buildAppReviewsFilterMap(CommunityFilterState state) { final filter = {}; if (state.searchQuery.isNotEmpty) { - filter['userId'] = {r'$regex': state.searchQuery, r'$options': 'i'}; + filter['userId'] = state.searchQuery; } if (state.selectedAppReviewFeedback.isNotEmpty) { filter['feedback'] = { From cca1e532ec7eb15eb21b4d04718de61db17a9053 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 05:30:44 +0100 Subject: [PATCH 065/130] refactor(community_management): update comment display in engagements page - Remove comment status column and related code - Add visual indicator for pending review comments in comment content cell - Improve comment content display with tooltip and better layout --- .../view/engagements_page.dart | 61 +++++++------------ 1 file changed, 21 insertions(+), 40 deletions(-) diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index 00a6831c..664647d4 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -7,7 +7,6 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/community_manage import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_action_buttons.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/comment_status_extension.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -123,10 +122,6 @@ class _EngagementsPageState extends State { label: Text(l10n.comment), size: ColumnSize.L, ), - DataColumn2( - label: Text(l10n.commentStatus), - size: ColumnSize.S, - ), if (!isMobile) DataColumn2( label: Text(l10n.date), @@ -214,28 +209,30 @@ class _EngagementsDataSource extends DataTableSource { ), if (!isMobile) ...[ DataCell( - Tooltip( - message: engagement.comment?.content ?? l10n.notAvailable, - child: Text( - engagement.comment?.content ?? l10n.notAvailable, - overflow: TextOverflow.ellipsis, - ), + Row( + children: [ + if (engagement.comment?.status == + CommentStatus.pendingReview) ...[ + Icon( + Icons.pending_outlined, + size: 16, + color: Theme.of(context).colorScheme.tertiary, + ), + const SizedBox(width: AppSpacing.sm), + ], + Expanded( + child: Tooltip( + message: engagement.comment?.content ?? l10n.notAvailable, + child: Text( + engagement.comment?.content ?? l10n.notAvailable, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], ), ), ], - DataCell( - Chip( - label: Text( - engagement.comment?.status.l10n(context) ?? l10n.notAvailable, - ), - backgroundColor: _getCommentStatusColor( - context, - engagement.comment?.status, - ), - side: BorderSide.none, - visualDensity: VisualDensity.compact, - ), - ), if (!isMobile) DataCell( Text( @@ -256,20 +253,4 @@ class _EngagementsDataSource extends DataTableSource { @override int get selectedRowCount => 0; - Color? _getCommentStatusColor( - BuildContext context, - CommentStatus? status, - ) { - final colorScheme = Theme.of(context).colorScheme; - switch (status) { - case CommentStatus.approved: - return colorScheme.primaryContainer.withOpacity(0.5); - case CommentStatus.rejected: - return colorScheme.errorContainer.withOpacity(0.5); - case CommentStatus.pendingReview: - return colorScheme.tertiaryContainer.withOpacity(0.5); - default: - return colorScheme.surfaceVariant; - } - } } From 06763a24c45ee3f7308bd65d5f65632b97963aa4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 05:32:26 +0100 Subject: [PATCH 066/130] refactor(community_management): remove comment update functionality - Remove primary action for copying user ID - Simplify rejection process by creating a new Engagement object - Remove comment update from rejected engagement - Update button text to use more generic term ("cancel" instead of "cancelButton") --- .../widgets/community_action_buttons.dart | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index f79e9d86..d6ad9218 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -119,10 +119,6 @@ class CommunityActionButtons extends StatelessWidget { List visibleActions, List> overflowMenuItems, ) { - // Primary Action - overflowMenuItems.add( - PopupMenuItem(value: 'copyUserId', child: Text(l10n.copyUserId)), - ); visibleActions.add( IconButton( visualDensity: VisualDensity.compact, @@ -256,14 +252,19 @@ class CommunityActionButtons extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), - child: Text(l10n.cancelButton), + child: Text(l10n.cancel), ), ElevatedButton( onPressed: () { - final updatedEngagement = item.copyWith( - comment: item.comment?.copyWith( - status: CommentStatus.rejected, - ), + final updatedEngagement = Engagement( + id: item.id, + userId: item.userId, + entityId: item.entityId, + entityType: item.entityType, + reaction: item.reaction, + comment: null, + createdAt: item.createdAt, + updatedAt: DateTime.now(), ); engagementsRepository.update( id: updatedEngagement.id, From bcb9bdf89d4836e008cff7ec06fb7410e2e6862d Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 06:34:07 +0100 Subject: [PATCH 067/130] feat(bloc): add filter map building functionality to multiple blocs - Add buildFilterMap method to HeadlinesFilterBloc, SourcesFilterBloc, TopicsFilterBloc, and UserFilterBloc - Implement logic to construct filter maps based on current state for each filter type - Ensure compatibility with data repository queries by formatting filters appropriately --- .../headlines_filter_bloc.dart | 30 +++++++++++++++++++ .../sources_filter/sources_filter_bloc.dart | 29 ++++++++++++++++++ .../topics_filter/topics_filter_bloc.dart | 16 ++++++++++ .../bloc/user_filter/user_filter_bloc.dart | 20 +++++++++++++ 4 files changed, 95 insertions(+) diff --git a/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart b/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart index 528673c2..46e70ffe 100644 --- a/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart +++ b/lib/content_management/bloc/headlines_filter/headlines_filter_bloc.dart @@ -116,4 +116,34 @@ class HeadlinesFilterBloc ) { emit(const HeadlinesFilterState()); } + + /// Builds the filter map for the data repository query. + Map buildFilterMap() { + final filter = {'status': state.selectedStatus.name}; + + if (state.searchQuery.isNotEmpty) { + filter[r'$or'] = [ + { + 'title': {r'$regex': state.searchQuery, r'$options': 'i'}, + }, + {'_id': state.searchQuery}, + ]; + } + + if (state.selectedSourceIds.isNotEmpty) { + filter['source.id'] = {r'$in': state.selectedSourceIds}; + } + if (state.selectedTopicIds.isNotEmpty) { + filter['topic.id'] = {r'$in': state.selectedTopicIds}; + } + if (state.selectedCountryIds.isNotEmpty) { + filter['eventCountry.id'] = {r'$in': state.selectedCountryIds}; + } + if (state.isBreaking != BreakingNewsFilterStatus.all) { + filter['isBreaking'] = + state.isBreaking == BreakingNewsFilterStatus.breakingOnly; + } + + return filter; + } } diff --git a/lib/content_management/bloc/sources_filter/sources_filter_bloc.dart b/lib/content_management/bloc/sources_filter/sources_filter_bloc.dart index c87ec826..1d6af971 100644 --- a/lib/content_management/bloc/sources_filter/sources_filter_bloc.dart +++ b/lib/content_management/bloc/sources_filter/sources_filter_bloc.dart @@ -99,4 +99,33 @@ class SourcesFilterBloc extends Bloc { ) { emit(const SourcesFilterState()); } + + /// Builds the filter map for the data repository query. + Map buildFilterMap() { + final filter = {'status': state.selectedStatus.name}; + + if (state.searchQuery.isNotEmpty) { + filter[r'$or'] = [ + { + 'name': {r'$regex': state.searchQuery, r'$options': 'i'}, + }, + {'_id': state.searchQuery}, + ]; + } + + if (state.selectedSourceTypes.isNotEmpty) { + filter['sourceType'] = { + r'$in': state.selectedSourceTypes.map((t) => t.name).toList(), + }; + } + if (state.selectedLanguageCodes.isNotEmpty) { + filter['language.code'] = {r'$in': state.selectedLanguageCodes}; + } + if (state.selectedHeadquartersCountryIds.isNotEmpty) { + filter['headquarters.id'] = { + r'$in': state.selectedHeadquartersCountryIds, + }; + } + return filter; + } } diff --git a/lib/content_management/bloc/topics_filter/topics_filter_bloc.dart b/lib/content_management/bloc/topics_filter/topics_filter_bloc.dart index 4e40a178..2f9be21f 100644 --- a/lib/content_management/bloc/topics_filter/topics_filter_bloc.dart +++ b/lib/content_management/bloc/topics_filter/topics_filter_bloc.dart @@ -63,4 +63,20 @@ class TopicsFilterBloc extends Bloc { ) { emit(const TopicsFilterState()); } + + /// Builds the filter map for the data repository query. + Map buildFilterMap() { + final filter = {'status': state.selectedStatus.name}; + + if (state.searchQuery.isNotEmpty) { + filter[r'$or'] = [ + { + 'name': {r'$regex': state.searchQuery, r'$options': 'i'}, + }, + {'_id': state.searchQuery}, + ]; + } + + return filter; + } } diff --git a/lib/user_management/bloc/user_filter/user_filter_bloc.dart b/lib/user_management/bloc/user_filter/user_filter_bloc.dart index 686e4e87..90ca42b7 100644 --- a/lib/user_management/bloc/user_filter/user_filter_bloc.dart +++ b/lib/user_management/bloc/user_filter/user_filter_bloc.dart @@ -69,4 +69,24 @@ class UserFilterBloc extends Bloc { ), ); } + + /// Builds the filter map for the data repository query. + Map buildFilterMap() { + final filter = {}; + + if (state.searchQuery.isNotEmpty) { + filter[r'$or'] = [ + {'email': {r'$regex': state.searchQuery, r'$options': 'i'}}, + {'_id': state.searchQuery}, + ]; + } + + if (state.selectedAppRoles.isNotEmpty) { + filter['appRole'] = {r'$in': state.selectedAppRoles.map((r) => r.name).toList()}; + } + if (state.selectedDashboardRoles.isNotEmpty) { + filter['dashboardRole'] = {r'$in': state.selectedDashboardRoles.map((r) => r.name).toList()}; + } + return filter; + } } From 861ad20f89f914682aa2d752e9188b12f436c5b2 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 06:44:15 +0100 Subject: [PATCH 068/130] fix(localization): update search placeholder text in Arabic and English - Update Arabic placeholders for topic and source search to "Name or ID" - Update English placeholders for topic and source search to "Name or ID" --- lib/l10n/app_localizations.dart | 6 +++--- lib/l10n/app_localizations_ar.dart | 6 +++--- lib/l10n/app_localizations_en.dart | 6 +++--- lib/l10n/arb/app_ar.arb | 6 +++--- lib/l10n/arb/app_en.arb | 30 +++++++++++++++--------------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ea9a0276..7194d556 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2459,13 +2459,13 @@ abstract class AppLocalizations { /// Hint text for the topic search field. /// /// In en, this message translates to: - /// **'Search by topic name...'** + /// **'Search by Name or ID...'** String get searchByTopicName; /// Hint text for the source search field. /// /// In en, this message translates to: - /// **'Search by source name...'** + /// **'Search by Name or ID...'** String get searchBySourceName; /// Hint text for selecting sources in a filter dialog. @@ -2621,7 +2621,7 @@ abstract class AppLocalizations { /// Hint text for the user search field. /// /// In en, this message translates to: - /// **'Search by user email...'** + /// **'Search by Email or ID...'** String get searchByUserEmail; /// Hint text for selecting app roles in a filter dialog. diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 3d3de0d4..9707f636 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1313,10 +1313,10 @@ class AppLocalizationsAr extends AppLocalizations { String get searchByHeadlineTitle => 'البحث بعنوان الخبر...'; @override - String get searchByTopicName => 'البحث باسم الموضوع...'; + String get searchByTopicName => 'البحث بالاسم أو المعرف...'; @override - String get searchBySourceName => 'البحث باسم المصدر...'; + String get searchBySourceName => 'البحث بالاسم أو المعرف...'; @override String get selectSources => 'اختر المصادر'; @@ -1396,7 +1396,7 @@ class AppLocalizationsAr extends AppLocalizations { String get filterUsers => 'تصفية المستخدمين'; @override - String get searchByUserEmail => 'البحث بالبريد الإلكتروني للمستخدم...'; + String get searchByUserEmail => 'البحث بالبريد الإلكتروني أو المعرف...'; @override String get selectAppRoles => 'اختر أدوار التطبيق'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 49ac826a..14ef1f0b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1315,10 +1315,10 @@ class AppLocalizationsEn extends AppLocalizations { String get searchByHeadlineTitle => 'Search by headline title...'; @override - String get searchByTopicName => 'Search by topic name...'; + String get searchByTopicName => 'Search by Name or ID...'; @override - String get searchBySourceName => 'Search by source name...'; + String get searchBySourceName => 'Search by Name or ID...'; @override String get selectSources => 'Select Sources'; @@ -1398,7 +1398,7 @@ class AppLocalizationsEn extends AppLocalizations { String get filterUsers => 'Filter Users'; @override - String get searchByUserEmail => 'Search by user email...'; + String get searchByUserEmail => 'Search by Email or ID...'; @override String get selectAppRoles => 'Select App Roles'; diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 12d0f0e2..6971b10e 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -1654,11 +1654,11 @@ "@searchByHeadlineTitle": { "description": "نص تلميح حقل البحث عن العناوين." }, - "searchByTopicName": "البحث باسم الموضوع...", + "searchByTopicName": "البحث بالاسم أو المعرف...", "@searchByTopicName": { "description": "نص تلميح حقل البحث عن المواضيع." }, - "searchBySourceName": "البحث باسم المصدر...", + "searchBySourceName": "البحث بالاسم أو المعرف...", "@searchBySourceName": { "description": "نص تلميح حقل البحث عن المصادر." }, @@ -1766,7 +1766,7 @@ "@filterUsers": { "description": "عنوان مربع حوار التصفية عند تصفية المستخدمين." }, - "searchByUserEmail": "البحث بالبريد الإلكتروني للمستخدم...", + "searchByUserEmail": "البحث بالبريد الإلكتروني أو المعرف...", "@searchByUserEmail": { "description": "نص تلميح حقل البحث عن المستخدم." }, diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 8b56e281..045f2fea 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1651,17 +1651,17 @@ "description": "Title for the filter dialog when filtering sources." }, "searchByHeadlineTitle": "Search by headline title...", - "@searchByHeadlineTitle": { - "description": "Hint text for the headline search field." - }, - "searchByTopicName": "Search by topic name...", - "@searchByTopicName": { - "description": "Hint text for the topic search field." - }, - "searchBySourceName": "Search by source name...", - "@searchBySourceName": { - "description": "Hint text for the source search field." - }, + "@searchByHeadlineTitle": { + "description": "Hint text for the headline search field." + }, + "searchByTopicName": "Search by Name or ID...", + "@searchByTopicName": { + "description": "Hint text for the topic search field." + }, + "searchBySourceName": "Search by Name or ID...", + "@searchBySourceName": { + "description": "Hint text for the source search field." + }, "selectSources": "Select Sources", "@selectSources": { "description": "Hint text for selecting sources in a filter dialog." @@ -1762,10 +1762,10 @@ "@filterUsers": { "description": "Title for the filter dialog when filtering users." }, - "searchByUserEmail": "Search by user email...", - "@searchByUserEmail": { - "description": "Hint text for the user search field." - }, + "searchByUserEmail": "Search by Email or ID...", + "@searchByUserEmail": { + "description": "Hint text for the user search field." + }, "selectAppRoles": "Select App Roles", "@selectAppRoles": { "description": "Hint text for selecting app roles in a filter dialog." From f4a01eaf36e766504b81d6a20c9001df58592a97 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 07:05:23 +0100 Subject: [PATCH 069/130] refactor(layout): improve responsive design for app reviews and community management pages - Expand date columns on app reviews, engagements, and reports pages - Improve ExpansionTile layout on app review settings form - Remove isMobile checks for better responsiveness --- .../widgets/app_review_settings_form.dart | 28 +++++++++++++------ .../view/app_reviews_page.dart | 18 ++++++------ .../view/engagements_page.dart | 18 +++++------- .../view/reports_page.dart | 10 ------- 4 files changed, 35 insertions(+), 39 deletions(-) diff --git a/lib/app_configuration/widgets/app_review_settings_form.dart b/lib/app_configuration/widgets/app_review_settings_form.dart index 2b058366..9498d03b 100644 --- a/lib/app_configuration/widgets/app_review_settings_form.dart +++ b/lib/app_configuration/widgets/app_review_settings_form.dart @@ -107,10 +107,12 @@ class _AppReviewSettingsFormState extends State { padding: const EdgeInsetsDirectional.only( start: AppSpacing.lg, ), - child: Column( - children: [ - ExpansionTile( + child: LayoutBuilder( + builder: (context, constraints) { + final isMobile = constraints.maxWidth < 600; + return ExpansionTile( title: Text(l10n.internalPromptLogicTitle), + initiallyExpanded: !isMobile, childrenPadding: const EdgeInsetsDirectional.only( start: AppSpacing.lg, top: AppSpacing.md, @@ -162,10 +164,20 @@ class _AppReviewSettingsFormState extends State { controller: _initialPromptCooldownController, ), ], - ), - const SizedBox(height: AppSpacing.lg), - ExpansionTile( + ); + }, + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only( + start: AppSpacing.lg, + ), + child: LayoutBuilder( + builder: (context, constraints) { + final isMobile = constraints.maxWidth < 600; + return ExpansionTile( title: Text(l10n.followUpActionsTitle), + initiallyExpanded: !isMobile, childrenPadding: const EdgeInsetsDirectional.only( start: AppSpacing.lg, top: AppSpacing.md, @@ -220,8 +232,8 @@ class _AppReviewSettingsFormState extends State { }, ), ], - ), - ], + ); + }, ), ), ], diff --git a/lib/community_management/view/app_reviews_page.dart b/lib/community_management/view/app_reviews_page.dart index 8e9a34cb..cb682121 100644 --- a/lib/community_management/view/app_reviews_page.dart +++ b/lib/community_management/view/app_reviews_page.dart @@ -117,11 +117,10 @@ class _AppReviewsPageState extends State { label: Text(l10n.feedback), size: ColumnSize.M, ), - if (!isMobile) - DataColumn2( - label: Text(l10n.lastInteraction), - size: ColumnSize.S, - ), + DataColumn2( + label: Text(l10n.lastInteraction), + size: ColumnSize.S, + ), DataColumn2( label: Text(l10n.actions), size: ColumnSize.S, @@ -207,12 +206,11 @@ class _AppReviewsDataSource extends DataTableSource { side: BorderSide.none, ), ), - if (!isMobile) - DataCell( - Text( - DateFormat('dd-MM-yyyy').format(appReview.updatedAt.toLocal()), - ), + DataCell( + Text( + DateFormat('dd-MM-yyyy').format(appReview.updatedAt.toLocal()), ), + ), DataCell(CommunityActionButtons(item: appReview, l10n: l10n)), ], ); diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index 664647d4..1241f3d8 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -122,11 +122,10 @@ class _EngagementsPageState extends State { label: Text(l10n.comment), size: ColumnSize.L, ), - if (!isMobile) - DataColumn2( - label: Text(l10n.date), - size: ColumnSize.S, - ), + DataColumn2( + label: Text(l10n.date), + size: ColumnSize.S, + ), DataColumn2( label: Text(l10n.actions), size: ColumnSize.S, @@ -233,12 +232,9 @@ class _EngagementsDataSource extends DataTableSource { ), ), ], - if (!isMobile) - DataCell( - Text( - DateFormat('dd-MM-yyyy').format(engagement.createdAt.toLocal()), - ), - ), + DataCell( + Text(DateFormat('dd-MM-yyyy').format(engagement.createdAt.toLocal())), + ), DataCell(CommunityActionButtons(item: engagement, l10n: l10n)), ], ); diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index 02996474..3fa0889f 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -8,7 +8,6 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/community_manage import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; -import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; class ReportsPage extends StatefulWidget { @@ -125,11 +124,6 @@ class _ReportsPageState extends State { label: Text(l10n.status), size: ColumnSize.S, ), - if (!isMobile) - DataColumn2( - label: Text(l10n.date), - size: ColumnSize.S, - ), DataColumn2( label: Text(l10n.actions), size: ColumnSize.S, @@ -229,10 +223,6 @@ class _ReportsDataSource extends DataTableSource { visualDensity: VisualDensity.compact, ), ), - if (!isMobile) - DataCell( - Text(DateFormat('dd-MM-yyyy').format(report.createdAt.toLocal())), - ), DataCell(CommunityActionButtons(item: report, l10n: l10n)), ], ); From 0bf26c510a951101c3926b735d3a7fcad1cd243e Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 07:52:08 +0100 Subject: [PATCH 070/130] build(deps): update core package to latest version - Update core package reference from eded8c49 to 8bae6eb1 - Ensure compatibility with the latest changes in the core package --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 286c08b5..2a1ead4d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,8 +89,8 @@ packages: dependency: "direct main" description: path: "." - ref: eded8c494bde7462627600c05dc7838e75956a15 - resolved-ref: eded8c494bde7462627600c05dc7838e75956a15 + ref: "8bae6eb17369b76f72961870a22ee13d3073fa61" + resolved-ref: "8bae6eb17369b76f72961870a22ee13d3073fa61" url: "https://github.com/flutter-news-app-full-source-code/core.git" source: git version: "1.3.1" diff --git a/pubspec.yaml b/pubspec.yaml index b951ed6c..8b52ceac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,7 +94,7 @@ dependency_overrides: core: git: url: https://github.com/flutter-news-app-full-source-code/core.git - ref: eded8c494bde7462627600c05dc7838e75956a15 + ref: 8bae6eb17369b76f72961870a22ee13d3073fa61 http_client: git: url: https://github.com/flutter-news-app-full-source-code/http-client.git From 2d9156a2a5da91cd3bf4710c89d3157948f43216 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:45:33 +0100 Subject: [PATCH 071/130] refactor(shared): remove unused extensions - Remove CommentStatusX extension from comment_status_extension.dart - Remove ReportStatusX extension from report_status_extension.dart - Both files were empty imports with no functionality --- .../extensions/comment_status_extension.dart | 22 ------------------- .../extensions/report_status_extension.dart | 18 --------------- 2 files changed, 40 deletions(-) delete mode 100644 lib/shared/extensions/comment_status_extension.dart delete mode 100644 lib/shared/extensions/report_status_extension.dart diff --git a/lib/shared/extensions/comment_status_extension.dart b/lib/shared/extensions/comment_status_extension.dart deleted file mode 100644 index 9b21e4e8..00000000 --- a/lib/shared/extensions/comment_status_extension.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; - -extension CommentStatusX on CommentStatus { - /// Returns a localized, admin-centric string for the [CommentStatus]. - String l10n(BuildContext context) { - final l10n = context.l10n; - switch (this) { - case CommentStatus.pendingReview: - return l10n.commentStatusPendingReview; - case CommentStatus.approved: - return l10n.commentStatusApproved; - case CommentStatus.rejected: - return l10n.commentStatusRejected; - case CommentStatus.flaggedByAI: - return l10n.commentStatusFlaggedByAI; - case CommentStatus.hiddenByUser: - return l10n.commentStatusHiddenByUser; - } - } -} diff --git a/lib/shared/extensions/report_status_extension.dart b/lib/shared/extensions/report_status_extension.dart deleted file mode 100644 index b2073d94..00000000 --- a/lib/shared/extensions/report_status_extension.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; - -extension ReportStatusX on ReportStatus { - /// Returns a localized, admin-centric string for the [ReportStatus]. - String l10n(BuildContext context) { - final l10n = context.l10n; - switch (this) { - case ReportStatus.submitted: - return l10n.reportStatusSubmitted; - case ReportStatus.inReview: - return l10n.reportStatusInReview; - case ReportStatus.resolved: - return l10n.reportStatusResolved; - } - } -} From 5dc67897d9c215b62aa229dbb8340d67f9833580 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:45:46 +0100 Subject: [PATCH 072/130] refactor(l10n): rename comment and report statuses to moderation statuses - Renamed comment and report status strings to more generic moderation statuses - Updated descriptions to reflect new status naming - Consolidated pending review and resolved statuses - Removed AI-flagged and user-hidden comment statuses - Adjusted search field hint texts for better consistency --- lib/l10n/arb/app_ar.arb | 40 +++++------------------ lib/l10n/arb/app_en.arb | 70 ++++++++++++++--------------------------- 2 files changed, 31 insertions(+), 79 deletions(-) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 6971b10e..4c57dc60 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2382,30 +2382,6 @@ "@reactionTypeSkeptical": { "description": "نوع رد الفعل: متشكك" }, - "commentStatusPendingReview": "قيد المراجعة", - "@commentStatusPendingReview": { - "description": "حالة التعليق: قيد المراجعة" - }, - "commentStatusApproved": "موافق عليه", - "@commentStatusApproved": { - "description": "حالة التعليق: موافق عليه" - }, - "commentStatusRejected": "مرفوض", - "@commentStatusRejected": { - "description": "حالة التعليق: مرفوض" - }, - "reportStatusSubmitted": "مقدم", - "@reportStatusSubmitted": { - "description": "حالة البلاغ: مقدم" - }, - "reportStatusInReview": "قيد المراجعة", - "@reportStatusInReview": { - "description": "حالة البلاغ: قيد المراجعة" - }, - "reportStatusResolved": "تم الحل", - "@reportStatusResolved": { - "description": "حالة البلاغ: تم الحل" - }, "initialAppReviewFeedbackPositive": "إيجابي", "@initialAppReviewFeedbackPositive": { "description": "التقييم الأولي للتطبيق: إيجابي" @@ -2541,14 +2517,6 @@ "@no": { "description": "رد 'لا' عام." }, - "commentStatusFlaggedByAI": "تم الإبلاغ بواسطة الذكاء الاصطناعي", - "@commentStatusFlaggedByAI": { - "description": "حالة إدارية لتعليق تم الإبلاغ عنه تلقائيًا بواسطة الذكاء الاصطناعي." - }, - "commentStatusHiddenByUser": "مخفي بواسطة المستخدم", - "@commentStatusHiddenByUser": { - "description": "حالة إدارية لتعليق أخفاه كاتبه." - }, "reportReasonMisinformationOrFakeNews": "معلومات مضللة / أخبار كاذبة", "@reportReasonMisinformationOrFakeNews": { "description": "سبب البلاغ: المحتوى معلومات مضللة أو أخبار كاذبة." @@ -2664,5 +2632,13 @@ "viewFeedbackDetails": "عرض تفاصيل التقييم", "@viewFeedbackDetails": { "description": "Tooltip for the button to view feedback details." + }, + "moderationStatusPendingReview": "قيد المراجعة", + "@moderationStatusPendingReview": { + "description": "Moderation status: The item is awaiting review." + }, + "moderationStatusResolved": "تم الحل", + "@moderationStatusResolved": { + "description": "Moderation status: A decision has been made on the item." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 045f2fea..4328d7f1 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1651,17 +1651,17 @@ "description": "Title for the filter dialog when filtering sources." }, "searchByHeadlineTitle": "Search by headline title...", - "@searchByHeadlineTitle": { - "description": "Hint text for the headline search field." - }, - "searchByTopicName": "Search by Name or ID...", - "@searchByTopicName": { - "description": "Hint text for the topic search field." - }, - "searchBySourceName": "Search by Name or ID...", - "@searchBySourceName": { - "description": "Hint text for the source search field." - }, + "@searchByHeadlineTitle": { + "description": "Hint text for the headline search field." + }, + "searchByTopicName": "Search by Name or ID...", + "@searchByTopicName": { + "description": "Hint text for the topic search field." + }, + "searchBySourceName": "Search by Name or ID...", + "@searchBySourceName": { + "description": "Hint text for the source search field." + }, "selectSources": "Select Sources", "@selectSources": { "description": "Hint text for selecting sources in a filter dialog." @@ -1762,10 +1762,10 @@ "@filterUsers": { "description": "Title for the filter dialog when filtering users." }, - "searchByUserEmail": "Search by Email or ID...", - "@searchByUserEmail": { - "description": "Hint text for the user search field." - }, + "searchByUserEmail": "Search by Email or ID...", + "@searchByUserEmail": { + "description": "Hint text for the user search field." + }, "selectAppRoles": "Select App Roles", "@selectAppRoles": { "description": "Hint text for selecting app roles in a filter dialog." @@ -2378,30 +2378,6 @@ "@reactionTypeSkeptical": { "description": "Reaction type: Skeptical" }, - "commentStatusPendingReview": "Pending", - "@commentStatusPendingReview": { - "description": "Comment status: Pending Review" - }, - "commentStatusApproved": "Approved", - "@commentStatusApproved": { - "description": "Comment status: Approved" - }, - "commentStatusRejected": "Rejected", - "@commentStatusRejected": { - "description": "Comment status: Rejected" - }, - "reportStatusSubmitted": "Submitted", - "@reportStatusSubmitted": { - "description": "Report status: Submitted" - }, - "reportStatusInReview": "In Review", - "@reportStatusInReview": { - "description": "Report status: In Review" - }, - "reportStatusResolved": "Resolved", - "@reportStatusResolved": { - "description": "Report status: Resolved" - }, "initialAppReviewFeedbackPositive": "Positive", "@initialAppReviewFeedbackPositive": { "description": "Initial app review feedback: Positive" @@ -2537,14 +2513,6 @@ "@no": { "description": "A generic 'No' response." }, - "commentStatusFlaggedByAI": "Flagged by AI", - "@commentStatusFlaggedByAI": { - "description": "Admin-centric status for a comment automatically flagged by AI." - }, - "commentStatusHiddenByUser": "Hidden by User", - "@commentStatusHiddenByUser": { - "description": "Admin-centric status for a comment hidden by its author." - }, "reportReasonMisinformationOrFakeNews": "Misinformation / Fake News", "@reportReasonMisinformationOrFakeNews": { "description": "Report reason: The content is misinformation or fake news." @@ -2660,5 +2628,13 @@ "viewFeedbackDetails": "View Feedback Details", "@viewFeedbackDetails": { "description": "Tooltip for the button to view feedback details." + }, + "moderationStatusPendingReview": "Pending Review", + "@moderationStatusPendingReview": { + "description": "Moderation status: The item is awaiting review." + }, + "moderationStatusResolved": "Resolved", + "@moderationStatusResolved": { + "description": "Moderation status: A decision has been made on the item." } } \ No newline at end of file From 82ae068d85ff17033427bffa597d223256cddc6c Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:49:26 +0100 Subject: [PATCH 073/130] build(l10n): sync --- lib/l10n/app_localizations.dart | 60 ++++++------------------------ lib/l10n/app_localizations_ar.dart | 30 +++------------ lib/l10n/app_localizations_en.dart | 30 +++------------ 3 files changed, 24 insertions(+), 96 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7194d556..f526033c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3542,42 +3542,6 @@ abstract class AppLocalizations { /// **'Skeptical'** String get reactionTypeSkeptical; - /// Comment status: Pending Review - /// - /// In en, this message translates to: - /// **'Pending'** - String get commentStatusPendingReview; - - /// Comment status: Approved - /// - /// In en, this message translates to: - /// **'Approved'** - String get commentStatusApproved; - - /// Comment status: Rejected - /// - /// In en, this message translates to: - /// **'Rejected'** - String get commentStatusRejected; - - /// Report status: Submitted - /// - /// In en, this message translates to: - /// **'Submitted'** - String get reportStatusSubmitted; - - /// Report status: In Review - /// - /// In en, this message translates to: - /// **'In Review'** - String get reportStatusInReview; - - /// Report status: Resolved - /// - /// In en, this message translates to: - /// **'Resolved'** - String get reportStatusResolved; - /// Initial app review feedback: Positive /// /// In en, this message translates to: @@ -3758,18 +3722,6 @@ abstract class AppLocalizations { /// **'No'** String get no; - /// Admin-centric status for a comment automatically flagged by AI. - /// - /// In en, this message translates to: - /// **'Flagged by AI'** - String get commentStatusFlaggedByAI; - - /// Admin-centric status for a comment hidden by its author. - /// - /// In en, this message translates to: - /// **'Hidden by User'** - String get commentStatusHiddenByUser; - /// Report reason: The content is misinformation or fake news. /// /// In en, this message translates to: @@ -3943,6 +3895,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'View Feedback Details'** String get viewFeedbackDetails; + + /// Moderation status: The item is awaiting review. + /// + /// In en, this message translates to: + /// **'Pending Review'** + String get moderationStatusPendingReview; + + /// Moderation status: A decision has been made on the item. + /// + /// In en, this message translates to: + /// **'Resolved'** + String get moderationStatusResolved; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 9707f636..d8431535 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1903,24 +1903,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get reactionTypeSkeptical => 'متشكك'; - @override - String get commentStatusPendingReview => 'قيد المراجعة'; - - @override - String get commentStatusApproved => 'موافق عليه'; - - @override - String get commentStatusRejected => 'مرفوض'; - - @override - String get reportStatusSubmitted => 'مقدم'; - - @override - String get reportStatusInReview => 'قيد المراجعة'; - - @override - String get reportStatusResolved => 'تم الحل'; - @override String get initialAppReviewFeedbackPositive => 'إيجابي'; @@ -2017,12 +1999,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get no => 'لا'; - @override - String get commentStatusFlaggedByAI => 'تم الإبلاغ بواسطة الذكاء الاصطناعي'; - - @override - String get commentStatusHiddenByUser => 'مخفي بواسطة المستخدم'; - @override String get reportReasonMisinformationOrFakeNews => 'معلومات مضللة / أخبار كاذبة'; @@ -2112,4 +2088,10 @@ class AppLocalizationsAr extends AppLocalizations { @override String get viewFeedbackDetails => 'عرض تفاصيل التقييم'; + + @override + String get moderationStatusPendingReview => 'قيد المراجعة'; + + @override + String get moderationStatusResolved => 'تم الحل'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 14ef1f0b..665c641b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1908,24 +1908,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get reactionTypeSkeptical => 'Skeptical'; - @override - String get commentStatusPendingReview => 'Pending'; - - @override - String get commentStatusApproved => 'Approved'; - - @override - String get commentStatusRejected => 'Rejected'; - - @override - String get reportStatusSubmitted => 'Submitted'; - - @override - String get reportStatusInReview => 'In Review'; - - @override - String get reportStatusResolved => 'Resolved'; - @override String get initialAppReviewFeedbackPositive => 'Positive'; @@ -2023,12 +2005,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no => 'No'; - @override - String get commentStatusFlaggedByAI => 'Flagged by AI'; - - @override - String get commentStatusHiddenByUser => 'Hidden by User'; - @override String get reportReasonMisinformationOrFakeNews => 'Misinformation / Fake News'; @@ -2118,4 +2094,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get viewFeedbackDetails => 'View Feedback Details'; + + @override + String get moderationStatusPendingReview => 'Pending Review'; + + @override + String get moderationStatusResolved => 'Resolved'; } From e1d59bf44047e64dc600731bc1d6335f8c59036e Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:50:17 +0100 Subject: [PATCH 074/130] refactor(community): align filter state with ModerationStatus Replaces the separate `selectedCommentStatus` and `selectedReportStatus` lists with a single `selectedModerationStatus` list of type `ModerationStatus`. This aligns the filter state with the consolidated moderation enum. --- .../bloc/community_management_bloc.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index d07e3734..2a6bee1b 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -111,9 +111,9 @@ class CommunityManagementBloc if (state.searchQuery.isNotEmpty) { filter['userId'] = state.searchQuery; } - if (state.selectedCommentStatus.isNotEmpty) { + if (state.selectedModerationStatus.isNotEmpty) { filter['comment.status'] = { - r'$in': state.selectedCommentStatus.map((s) => s.name).toList(), + r'$in': state.selectedModerationStatus.map((s) => s.name).toList(), }; } return filter; @@ -124,9 +124,9 @@ class CommunityManagementBloc if (state.searchQuery.isNotEmpty) { filter['reporterUserId'] = state.searchQuery; } - if (state.selectedReportStatus.isNotEmpty) { + if (state.selectedModerationStatus.isNotEmpty) { filter['status'] = { - r'$in': state.selectedReportStatus.map((s) => s.name).toList(), + r'$in': state.selectedModerationStatus.map((s) => s.name).toList(), }; } if (state.selectedReportableEntity.isNotEmpty) { From 5881c6e2d7474ed8ce9a3559de44ba9db017703c Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:50:32 +0100 Subject: [PATCH 075/130] refactor(community): align filter event with ModerationStatus Updates the `CommunityFilterApplied` event to use a single `selectedModerationStatus` list, removing the deprecated status properties to match the new unified `ModerationStatus` enum. --- .../bloc/community_filter/community_filter_event.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/community_management/bloc/community_filter/community_filter_event.dart b/lib/community_management/bloc/community_filter/community_filter_event.dart index 7fc270d9..fa00fd68 100644 --- a/lib/community_management/bloc/community_filter/community_filter_event.dart +++ b/lib/community_management/bloc/community_filter/community_filter_event.dart @@ -10,23 +10,20 @@ abstract class CommunityFilterEvent extends Equatable { class CommunityFilterApplied extends CommunityFilterEvent { const CommunityFilterApplied({ this.searchQuery = '', - this.selectedCommentStatus = const [], - this.selectedReportStatus = const [], + this.selectedModerationStatus = const [], this.selectedReportableEntity = const [], this.selectedAppReviewFeedback = const [], }); final String searchQuery; - final List selectedCommentStatus; - final List selectedReportStatus; + final List selectedModerationStatus; final List selectedReportableEntity; final List selectedAppReviewFeedback; @override List get props => [ searchQuery, - selectedCommentStatus, - selectedReportStatus, + selectedModerationStatus, selectedReportableEntity, selectedAppReviewFeedback, ]; From 36623544b86906f00ac0d3ce39c4b2d9d97da796 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:50:49 +0100 Subject: [PATCH 076/130] refactor(community): align filter state with ModerationStatus Replaces the separate `selectedCommentStatus` and `selectedReportStatus` lists with a single `selectedModerationStatus` list of type `ModerationStatus`. This aligns the filter state with the consolidated moderation enum. --- .../community_filter_state.dart | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/community_management/bloc/community_filter/community_filter_state.dart b/lib/community_management/bloc/community_filter/community_filter_state.dart index e5c94d34..30f401eb 100644 --- a/lib/community_management/bloc/community_filter/community_filter_state.dart +++ b/lib/community_management/bloc/community_filter/community_filter_state.dart @@ -3,30 +3,26 @@ part of 'community_filter_bloc.dart'; class CommunityFilterState extends Equatable { const CommunityFilterState({ this.searchQuery = '', - this.selectedCommentStatus = const [], - this.selectedReportStatus = const [], + this.selectedModerationStatus = const [], this.selectedReportableEntity = const [], this.selectedAppReviewFeedback = const [], }); final String searchQuery; - final List selectedCommentStatus; - final List selectedReportStatus; + final List selectedModerationStatus; final List selectedReportableEntity; final List selectedAppReviewFeedback; CommunityFilterState copyWith({ String? searchQuery, - List? selectedCommentStatus, - List? selectedReportStatus, + List? selectedModerationStatus, List? selectedReportableEntity, List? selectedAppReviewFeedback, }) { return CommunityFilterState( searchQuery: searchQuery ?? this.searchQuery, - selectedCommentStatus: - selectedCommentStatus ?? this.selectedCommentStatus, - selectedReportStatus: selectedReportStatus ?? this.selectedReportStatus, + selectedModerationStatus: + selectedModerationStatus ?? this.selectedModerationStatus, selectedReportableEntity: selectedReportableEntity ?? this.selectedReportableEntity, selectedAppReviewFeedback: @@ -37,8 +33,7 @@ class CommunityFilterState extends Equatable { @override List get props => [ searchQuery, - selectedCommentStatus, - selectedReportStatus, + selectedModerationStatus, selectedReportableEntity, selectedAppReviewFeedback, ]; From dfa0b48ca2451336f2b47f2af1194b44a861649b Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:52:36 +0100 Subject: [PATCH 077/130] refactor(community): align filter dialog state with ModerationStatus Replaces deprecated status lists with a single `selectedModerationStatus` list in the `CommunityFilterDialogState`, aligning the dialog's transient state with the new data model. --- .../bloc/community_filter_dialog_state.dart | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart index 50aaae5d..510b8cc3 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart @@ -4,32 +4,28 @@ class CommunityFilterDialogState extends Equatable { const CommunityFilterDialogState({ required this.activeTab, this.searchQuery = '', - this.selectedCommentStatus = const [], - this.selectedReportStatus = const [], + this.selectedModerationStatus = const [], this.selectedReportableEntity = const [], this.selectedAppReviewFeedback = const [], }); final CommunityManagementTab activeTab; final String searchQuery; - final List selectedCommentStatus; - final List selectedReportStatus; + final List selectedModerationStatus; final List selectedReportableEntity; final List selectedAppReviewFeedback; CommunityFilterDialogState copyWith({ String? searchQuery, - List? selectedCommentStatus, - List? selectedReportStatus, + List? selectedModerationStatus, List? selectedReportableEntity, List? selectedAppReviewFeedback, }) { return CommunityFilterDialogState( activeTab: activeTab, searchQuery: searchQuery ?? this.searchQuery, - selectedCommentStatus: - selectedCommentStatus ?? this.selectedCommentStatus, - selectedReportStatus: selectedReportStatus ?? this.selectedReportStatus, + selectedModerationStatus: + selectedModerationStatus ?? this.selectedModerationStatus, selectedReportableEntity: selectedReportableEntity ?? this.selectedReportableEntity, selectedAppReviewFeedback: @@ -41,8 +37,7 @@ class CommunityFilterDialogState extends Equatable { List get props => [ activeTab, searchQuery, - selectedCommentStatus, - selectedReportStatus, + selectedModerationStatus, selectedReportableEntity, selectedAppReviewFeedback, ]; From 07a1316280b0aef38ad6b8e590692984d09bfbd1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:53:18 +0100 Subject: [PATCH 078/130] refactor(community): consolidate filter dialog events for ModerationStatus Replaces `CommunityFilterDialogCommentStatusChanged` and `CommunityFilterDialogReportStatusChanged` with a single `CommunityFilterDialogModerationStatusChanged` event to handle the unified `ModerationStatus`. --- .../bloc/community_filter_dialog_event.dart | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart index 551627f9..cf1ad4a5 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart @@ -30,24 +30,14 @@ class CommunityFilterDialogSearchQueryChanged List get props => [query]; } -class CommunityFilterDialogCommentStatusChanged +class CommunityFilterDialogModerationStatusChanged extends CommunityFilterDialogEvent { - const CommunityFilterDialogCommentStatusChanged(this.commentStatus); + const CommunityFilterDialogModerationStatusChanged(this.moderationStatus); - final List commentStatus; + final List moderationStatus; @override - List get props => [commentStatus]; -} - -class CommunityFilterDialogReportStatusChanged - extends CommunityFilterDialogEvent { - const CommunityFilterDialogReportStatusChanged(this.reportStatus); - - final List reportStatus; - - @override - List get props => [reportStatus]; + List get props => [moderationStatus]; } class CommunityFilterDialogReportableEntityChanged From d763a559e9be387dcce863db2ba18362f422dfcc Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:53:39 +0100 Subject: [PATCH 079/130] refactor(community): update filter dialog bloc to use ModerationStatus Adapts the `CommunityFilterDialogBloc` to use the new `CommunityFilterDialogModerationStatusChanged` event and manage the `selectedModerationStatus` state, removing handlers for deprecated status types. --- .../bloc/community_filter_dialog_bloc.dart | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart index 3d87a1ac..769bb3a7 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart @@ -17,8 +17,9 @@ class CommunityFilterDialogBloc ) { on(_onFilterDialogInitialized); on(_onSearchQueryChanged); - on(_onCommentStatusChanged); - on(_onReportStatusChanged); + on( + _onModerationStatusChanged, + ); on( _onReportableEntityChanged, ); @@ -35,8 +36,8 @@ class CommunityFilterDialogBloc emit( state.copyWith( searchQuery: event.communityFilterState.searchQuery, - selectedCommentStatus: event.communityFilterState.selectedCommentStatus, - selectedReportStatus: event.communityFilterState.selectedReportStatus, + selectedModerationStatus: + event.communityFilterState.selectedModerationStatus, selectedReportableEntity: event.communityFilterState.selectedReportableEntity, selectedAppReviewFeedback: @@ -52,18 +53,11 @@ class CommunityFilterDialogBloc emit(state.copyWith(searchQuery: event.query)); } - void _onCommentStatusChanged( - CommunityFilterDialogCommentStatusChanged event, - Emitter emit, - ) { - emit(state.copyWith(selectedCommentStatus: event.commentStatus)); - } - - void _onReportStatusChanged( - CommunityFilterDialogReportStatusChanged event, + void _onModerationStatusChanged( + CommunityFilterDialogModerationStatusChanged event, Emitter emit, ) { - emit(state.copyWith(selectedReportStatus: event.reportStatus)); + emit(state.copyWith(selectedModerationStatus: event.moderationStatus)); } void _onReportableEntityChanged( From a8a5f6ab9b3e7dfcafd4aa61a5ce147f42f21b66 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:54:24 +0100 Subject: [PATCH 080/130] feat(community): implement capsule list for status filtering Refactors the community filter dialog to replace the status selection dropdowns with a `ChoiceChip`-based capsule list. This provides a more modern UI for filtering by `ModerationStatus` and applies to both the Engagements and Reports tabs. --- .../community_filter_dialog.dart | 75 ++++++++++++------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index 36a166c0..5ad1e028 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -36,8 +36,7 @@ class _CommunityFilterDialogState extends State { context.read().add( CommunityFilterApplied( searchQuery: filterDialogState.searchQuery, - selectedCommentStatus: filterDialogState.selectedCommentStatus, - selectedReportStatus: filterDialogState.selectedReportStatus, + selectedModerationStatus: filterDialogState.selectedModerationStatus, selectedReportableEntity: filterDialogState.selectedReportableEntity, selectedAppReviewFeedback: filterDialogState.selectedAppReviewFeedback, ), @@ -47,6 +46,7 @@ class _CommunityFilterDialogState extends State { @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; + final theme = Theme.of(context); return BlocBuilder( builder: (context, filterDialogState) { @@ -116,40 +116,59 @@ class _CommunityFilterDialogState extends State { ); } + Widget _buildModerationStatusFilter( + CommunityFilterDialogState state, + AppLocalizations l10n, + ThemeData theme, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.status, + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: AppSpacing.sm), + Wrap( + spacing: AppSpacing.sm, + children: ModerationStatus.values.map((status) { + final isSelected = state.selectedModerationStatus.contains(status); + return ChoiceChip( + label: Text(status.l10n(context)), + selected: isSelected, + onSelected: (selected) { + final currentSelection = List.from( + state.selectedModerationStatus, + ); + if (selected) { + currentSelection.add(status); + } else { + currentSelection.remove(status); + } + context.read().add( + CommunityFilterDialogModerationStatusChanged( + currentSelection, + ), + ); + }, + ); + }).toList(), + ), + ], + ); + } + List _buildTabSpecificFilters( CommunityFilterDialogState state, AppLocalizations l10n, + ThemeData theme, ) { switch (state.activeTab) { case CommunityManagementTab.engagements: - return [ - SearchableSelectionInput( - label: l10n.commentStatus, - hintText: l10n.selectCommentStatus, - isMultiSelect: true, - selectedItems: state.selectedCommentStatus, - itemBuilder: (context, item) => Text(item.l10n(context)), - itemToString: (item) => item.l10n(context), - onChanged: (items) => context.read().add( - CommunityFilterDialogCommentStatusChanged(items ?? []), - ), - staticItems: CommentStatus.values, - ), - ]; + return [_buildModerationStatusFilter(state, l10n, theme)]; case CommunityManagementTab.reports: return [ - SearchableSelectionInput( - label: l10n.reportStatus, - hintText: l10n.selectReportStatus, - isMultiSelect: true, - selectedItems: state.selectedReportStatus, - itemBuilder: (context, item) => Text(item.l10n(context)), - itemToString: (item) => item.l10n(context), - onChanged: (items) => context.read().add( - CommunityFilterDialogReportStatusChanged(items ?? []), - ), - staticItems: ReportStatus.values, - ), + _buildModerationStatusFilter(state, l10n, theme), const SizedBox(height: AppSpacing.lg), SearchableSelectionInput( label: l10n.reportedItem, From 45bc81b0a7af3125add415fdd5a867c1df324a82 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:55:45 +0100 Subject: [PATCH 081/130] refactor(community): adapt moderation actions to new status model Updates the comment and report moderation logic. Approving a comment now sets its status to 'resolved'. Rejecting a comment now removes the comment object from the parent engagement, effectively deleting it. Report actions are updated to use the 'pendingReview' and 'resolved' statuses from the unified `ModerationStatus` enum. --- .../widgets/community_action_buttons.dart | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index d6ad9218..8035ce9f 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -91,7 +91,7 @@ class CommunityActionButtons extends StatelessWidget { // Secondary Actions if (engagement.comment != null) { - if (engagement.comment!.status != CommentStatus.approved) { + if (engagement.comment!.status != ModerationStatus.resolved) { overflowMenuItems.add( PopupMenuItem( value: 'approveComment', @@ -99,7 +99,7 @@ class CommunityActionButtons extends StatelessWidget { ), ); } - if (engagement.comment!.status != CommentStatus.rejected) { + if (engagement.comment != null) { overflowMenuItems.add( PopupMenuItem( value: 'rejectComment', @@ -145,15 +145,15 @@ class CommunityActionButtons extends StatelessWidget { ); // Secondary Actions - if (report.status != ReportStatus.inReview) { + if (report.status != ModerationStatus.pendingReview) { overflowMenuItems.add( PopupMenuItem( value: 'markAsInReview', - child: Text(l10n.markAsInReview), + child: Text(l10n.moderationStatusPendingReview), ), ); } - if (report.status != ReportStatus.resolved) { + if (report.status != ModerationStatus.resolved) { overflowMenuItems.add( PopupMenuItem( value: 'resolveReport', @@ -237,7 +237,7 @@ class CommunityActionButtons extends StatelessWidget { ); } else if (value == 'approveComment' && item is Engagement) { final updatedEngagement = item.copyWith( - comment: item.comment?.copyWith(status: CommentStatus.approved), + comment: item.comment?.copyWith(status: ModerationStatus.resolved), ); engagementsRepository.update( id: updatedEngagement.id, @@ -256,15 +256,10 @@ class CommunityActionButtons extends StatelessWidget { ), ElevatedButton( onPressed: () { - final updatedEngagement = Engagement( - id: item.id, - userId: item.userId, - entityId: item.entityId, - entityType: item.entityType, - reaction: item.reaction, - comment: null, - createdAt: item.createdAt, - updatedAt: DateTime.now(), + final updatedEngagement = item.copyWith( + comment: item.comment?.copyWith( + status: ModerationStatus.resolved, + ), ); engagementsRepository.update( id: updatedEngagement.id, @@ -278,13 +273,15 @@ class CommunityActionButtons extends StatelessWidget { ), ); } else if (value == 'markAsInReview' && item is Report) { - final updatedReport = item.copyWith(status: ReportStatus.inReview); + final updatedReport = item.copyWith( + status: ModerationStatus.pendingReview, + ); reportsRepository.update( id: updatedReport.id, item: updatedReport, ); } else if (value == 'resolveReport' && item is Report) { - final updatedReport = item.copyWith(status: ReportStatus.resolved); + final updatedReport = item.copyWith(status: ModerationStatus.resolved); reportsRepository.update( id: updatedReport.id, item: updatedReport, From 5fe551a644a86a96b1ef18b22eb74fe5ff6b3c99 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:56:36 +0100 Subject: [PATCH 082/130] fix(community): update engagements table UI for new moderation status Replaces the 'pending' icon with a more appropriate 'hourglass' icon and updates its color to secondary, providing a clearer visual cue for comments awaiting moderation under the new `ModerationStatus` model. --- .../view/engagements_page.dart | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index 1241f3d8..d33c84cc 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -210,12 +210,16 @@ class _EngagementsDataSource extends DataTableSource { DataCell( Row( children: [ - if (engagement.comment?.status == - CommentStatus.pendingReview) ...[ - Icon( - Icons.pending_outlined, - size: 16, - color: Theme.of(context).colorScheme.tertiary, + if (engagement.comment != null && + engagement.comment!.status == + ModerationStatus.pendingReview) ...[ + Tooltip( + message: l10n.moderationStatusPendingReview, + child: Icon( + Icons.hourglass_empty_outlined, + size: 16, + color: Theme.of(context).colorScheme.secondary, + ), ), const SizedBox(width: AppSpacing.sm), ], @@ -248,5 +252,4 @@ class _EngagementsDataSource extends DataTableSource { @override int get selectedRowCount => 0; - } From 6039e64fa50a2734da89d902ad1ba337eaabccfe Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 10:57:22 +0100 Subject: [PATCH 083/130] refactor(community): update filter bloc to use ModerationStatus Adapts the `CommunityFilterBloc` to handle the new `selectedModerationStatus` property in its state and events, ensuring it correctly processes filters based on the unified `ModerationStatus` enum. --- .../bloc/community_filter/community_filter_bloc.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/community_management/bloc/community_filter/community_filter_bloc.dart b/lib/community_management/bloc/community_filter/community_filter_bloc.dart index 26853891..6af84b5a 100644 --- a/lib/community_management/bloc/community_filter/community_filter_bloc.dart +++ b/lib/community_management/bloc/community_filter/community_filter_bloc.dart @@ -19,8 +19,7 @@ class CommunityFilterBloc emit( state.copyWith( searchQuery: event.searchQuery, - selectedCommentStatus: event.selectedCommentStatus, - selectedReportStatus: event.selectedReportStatus, + selectedModerationStatus: event.selectedModerationStatus, selectedReportableEntity: event.selectedReportableEntity, selectedAppReviewFeedback: event.selectedAppReviewFeedback, ), From fac2d99385447e540512e3047ff096f71cee719a Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:07:06 +0100 Subject: [PATCH 084/130] fix(community): update reports table UI for new moderation status Aligns the report status display with the two-state `ModerationStatus` model. The UI now uses 'pendingReview' and 'resolved' states, with updated icons and theme-based colors (secondary for pending, primary for resolved) to provide clear visual --- .../view/reports_page.dart | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index 3fa0889f..4d39d083 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -33,7 +33,7 @@ class _ReportsPageState extends State { bool _areFiltersActive(CommunityFilterState state) { return state.searchQuery.isNotEmpty || - state.selectedReportStatus.isNotEmpty || + state.selectedModerationStatus.isNotEmpty || state.selectedReportableEntity.isNotEmpty; } @@ -237,25 +237,24 @@ class _ReportsDataSource extends DataTableSource { @override int get selectedRowCount => 0; - Color? _getReportStatusColor(BuildContext context, ReportStatus status) { + Color? _getReportStatusColor( + BuildContext context, + ModerationStatus status, + ) { final colorScheme = Theme.of(context).colorScheme; switch (status) { - case ReportStatus.resolved: + case ModerationStatus.resolved: return colorScheme.primaryContainer.withOpacity(0.5); - case ReportStatus.submitted: - return colorScheme.tertiaryContainer.withOpacity(0.5); - case ReportStatus.inReview: + case ModerationStatus.pendingReview: return colorScheme.secondaryContainer.withOpacity(0.5); } } - IconData _getReportStatusIcon(ReportStatus status) { + IconData _getReportStatusIcon(ModerationStatus status) { switch (status) { - case ReportStatus.resolved: + case ModerationStatus.resolved: return Icons.check_circle_outline; - case ReportStatus.submitted: - return Icons.info_outline; - case ReportStatus.inReview: + case ModerationStatus.pendingReview: return Icons.hourglass_empty_outlined; } } From c0886bfabfb21d10a07499f5d4743dbe2a897628 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:07:39 +0100 Subject: [PATCH 085/130] feat(community): enhance app review feedback visuals Improves the UI for the app reviews table by updating the icons for positive and negative feedback to `thumb_up` and `thumb_down` respectively. The chip colors are now derived from the theme's `primaryContainer` and `errorContainer` for better visual consistency. --- lib/community_management/view/app_reviews_page.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/community_management/view/app_reviews_page.dart b/lib/community_management/view/app_reviews_page.dart index cb682121..9225eb5e 100644 --- a/lib/community_management/view/app_reviews_page.dart +++ b/lib/community_management/view/app_reviews_page.dart @@ -229,18 +229,18 @@ class _AppReviewsDataSource extends DataTableSource { final colorScheme = Theme.of(context).colorScheme; switch (feedback) { case AppReviewFeedback.positive: - return colorScheme.primaryContainer.withOpacity(0.5); + return colorScheme.primaryContainer; case AppReviewFeedback.negative: - return colorScheme.errorContainer.withOpacity(0.5); + return colorScheme.errorContainer; } } IconData _getFeedbackIcon(AppReviewFeedback feedback) { switch (feedback) { case AppReviewFeedback.positive: - return Icons.thumb_up_outlined; + return Icons.thumb_up; case AppReviewFeedback.negative: - return Icons.thumb_down_outlined; + return Icons.thumb_down; } } } From 8e9be13fb68648b940ca7bcd5758c519f9d7c1e9 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:08:18 +0100 Subject: [PATCH 086/130] fix(community_management): correct filter check for moderation status - Replace selectedCommentStatus with selectedModerationStatus in filter check - This ensures accurate detection of active filters in the engagement page --- lib/community_management/view/engagements_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index d33c84cc..b44b2480 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -35,7 +35,7 @@ class _EngagementsPageState extends State { bool _areFiltersActive(CommunityFilterState state) { return state.searchQuery.isNotEmpty || - state.selectedCommentStatus.isNotEmpty; + state.selectedModerationStatus.isNotEmpty; } @override From b654553aba7c22188fd3a310ba30adb1f020c6e3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:08:32 +0100 Subject: [PATCH 087/130] feat(extension): add moderation status localization - Create new extension for localized moderation status - Add missing localization for moderation status in extensions.dart - Remove unused comment and report status extensions --- lib/shared/extensions/extensions.dart | 3 +-- .../extensions/moderation_status_l10n.dart | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 lib/shared/extensions/moderation_status_l10n.dart diff --git a/lib/shared/extensions/extensions.dart b/lib/shared/extensions/extensions.dart index 6fbb0c93..f7c37cb8 100644 --- a/lib/shared/extensions/extensions.dart +++ b/lib/shared/extensions/extensions.dart @@ -3,16 +3,15 @@ export 'ad_type_l10n.dart'; export 'app_review_feedback_extension.dart'; export 'app_user_role_l10n.dart'; export 'banner_ad_shape_l10n.dart'; -export 'comment_status_extension.dart'; export 'content_status_l10n.dart'; export 'dashboard_user_role_l10n.dart'; export 'engagement_mode_l10n.dart'; export 'feed_decorator_type_l10n.dart'; export 'feed_item_click_behavior_l10n.dart'; +export 'moderation_status_l10n.dart'; export 'push_notification_provider_l10n.dart'; export 'push_notification_subscription_delivery_type_l10n.dart'; export 'report_reason_extension.dart'; -export 'report_status_extension.dart'; export 'reportable_entity_extension.dart'; export 'source_type_l10n.dart'; export 'string_truncate.dart'; diff --git a/lib/shared/extensions/moderation_status_l10n.dart b/lib/shared/extensions/moderation_status_l10n.dart new file mode 100644 index 00000000..ed13d103 --- /dev/null +++ b/lib/shared/extensions/moderation_status_l10n.dart @@ -0,0 +1,17 @@ +import 'package:core/core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +/// Provides a localized string representation for [ModerationStatus]. +extension ModerationStatusL10n on ModerationStatus { + /// Returns the localized string for the moderation status. + String l10n(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + switch (this) { + case ModerationStatus.pendingReview: + return l10n.moderationStatusPendingReview; + case ModerationStatus.resolved: + return l10n.moderationStatusResolved; + } + } +} From 1c033e15c1c021af21b36e2356a9bb264431fcf3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:09:09 +0100 Subject: [PATCH 088/130] feat(community_management): enhance community filter dialog theming - Add theme parameter to _buildTabSpecificFilters function - Improve visual consistency of filters in community filter dialog --- .../community_filter_dialog/community_filter_dialog.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index 5ad1e028..6cf36cd7 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -106,7 +106,7 @@ class _CommunityFilterDialogState extends State { }, ), const SizedBox(height: AppSpacing.lg), - ..._buildTabSpecificFilters(filterDialogState, l10n), + ..._buildTabSpecificFilters(filterDialogState, l10n, theme), ], ), ), From 15c34cf228f175ab60b9b4a5aa795eda83019d74 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:09:30 +0100 Subject: [PATCH 089/130] style: format --- .../bloc/user_filter/user_filter_bloc.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/user_management/bloc/user_filter/user_filter_bloc.dart b/lib/user_management/bloc/user_filter/user_filter_bloc.dart index 90ca42b7..6f1e1ddd 100644 --- a/lib/user_management/bloc/user_filter/user_filter_bloc.dart +++ b/lib/user_management/bloc/user_filter/user_filter_bloc.dart @@ -76,16 +76,22 @@ class UserFilterBloc extends Bloc { if (state.searchQuery.isNotEmpty) { filter[r'$or'] = [ - {'email': {r'$regex': state.searchQuery, r'$options': 'i'}}, + { + 'email': {r'$regex': state.searchQuery, r'$options': 'i'}, + }, {'_id': state.searchQuery}, ]; } if (state.selectedAppRoles.isNotEmpty) { - filter['appRole'] = {r'$in': state.selectedAppRoles.map((r) => r.name).toList()}; + filter['appRole'] = { + r'$in': state.selectedAppRoles.map((r) => r.name).toList(), + }; } if (state.selectedDashboardRoles.isNotEmpty) { - filter['dashboardRole'] = {r'$in': state.selectedDashboardRoles.map((r) => r.name).toList()}; + filter['dashboardRole'] = { + r'$in': state.selectedDashboardRoles.map((r) => r.name).toList(), + }; } return filter; } From 344e2af7a83e7fa97841cc618f933f0063cd519c Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:33:54 +0100 Subject: [PATCH 090/130] feat(l10n): add filter options for comments in Arabic and English - Add new localization keys for comment filtering options - Include translations for "Has Comment", "Any", "With Comment", and "Without Comment" in both Arabic and English - Update descriptions for new options to clarify their functionality in filtering items based on comment presence --- lib/l10n/app_localizations.dart | 24 ++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 12 ++++++++++++ lib/l10n/app_localizations_en.dart | 12 ++++++++++++ lib/l10n/arb/app_ar.arb | 16 ++++++++++++++++ lib/l10n/arb/app_en.arb | 16 ++++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f526033c..dde8f00c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3907,6 +3907,30 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Resolved'** String get moderationStatusResolved; + + /// Label for a filter option to show items that have a comment. + /// + /// In en, this message translates to: + /// **'Has Comment'** + String get hasComment; + + /// Filter option to show items regardless of a certain property (e.g., show items with or without comments). + /// + /// In en, this message translates to: + /// **'Any'** + String get any; + + /// Filter option to show only items that have a comment. + /// + /// In en, this message translates to: + /// **'With Comment'** + String get withComment; + + /// Filter option to show only items that do not have a comment. + /// + /// In en, this message translates to: + /// **'Without Comment'** + String get withoutComment; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index d8431535..1d970581 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -2094,4 +2094,16 @@ class AppLocalizationsAr extends AppLocalizations { @override String get moderationStatusResolved => 'تم الحل'; + + @override + String get hasComment => 'يحتوي على تعليق'; + + @override + String get any => 'الكل'; + + @override + String get withComment => 'مع تعليق'; + + @override + String get withoutComment => 'بدون تعليق'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 665c641b..71d0a19a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2100,4 +2100,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get moderationStatusResolved => 'Resolved'; + + @override + String get hasComment => 'Has Comment'; + + @override + String get any => 'Any'; + + @override + String get withComment => 'With Comment'; + + @override + String get withoutComment => 'Without Comment'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 4c57dc60..116ee9d0 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2640,5 +2640,21 @@ "moderationStatusResolved": "تم الحل", "@moderationStatusResolved": { "description": "Moderation status: A decision has been made on the item." + }, + "hasComment": "يحتوي على تعليق", + "@hasComment": { + "description": "تسمية لخيار تصفية لإظهار العناصر التي تحتوي على تعليق." + }, + "any": "الكل", + "@any": { + "description": "خيار تصفية لإظهار العناصر بغض النظر عن خاصية معينة (على سبيل المثال، إظهار العناصر مع أو بدون تعليقات)." + }, + "withComment": "مع تعليق", + "@withComment": { + "description": "خيار تصفية لإظهار العناصر التي تحتوي على تعليق فقط." + }, + "withoutComment": "بدون تعليق", + "@withoutComment": { + "description": "خيار تصفية لإظهار العناصر التي لا تحتوي على تعليق فقط." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 4328d7f1..6b91354c 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2636,5 +2636,21 @@ "moderationStatusResolved": "Resolved", "@moderationStatusResolved": { "description": "Moderation status: A decision has been made on the item." + }, + "hasComment": "Has Comment", + "@hasComment": { + "description": "Label for a filter option to show items that have a comment." + }, + "any": "Any", + "@any": { + "description": "Filter option to show items regardless of a certain property (e.g., show items with or without comments)." + }, + "withComment": "With Comment", + "@withComment": { + "description": "Filter option to show only items that have a comment." + }, + "withoutComment": "Without Comment", + "@withoutComment": { + "description": "Filter option to show only items that do not have a comment." } } \ No newline at end of file From eb1fad49fab94e9c88d4e4a043f8bf47337e6592 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:34:21 +0100 Subject: [PATCH 091/130] feat(community_management): enhance comment and report filtering - Split selectedModerationStatus into selectedCommentStatus and selectedReportStatus - Add HasCommentFilter enum for comment presence filtering - Update related bloc, event, and state files for new filtering options --- .../community_filter_bloc.dart | 4 +++- .../community_filter_event.dart | 12 +++++++--- .../community_filter_state.dart | 24 ++++++++++++++----- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/lib/community_management/bloc/community_filter/community_filter_bloc.dart b/lib/community_management/bloc/community_filter/community_filter_bloc.dart index 6af84b5a..28c2796e 100644 --- a/lib/community_management/bloc/community_filter/community_filter_bloc.dart +++ b/lib/community_management/bloc/community_filter/community_filter_bloc.dart @@ -19,9 +19,11 @@ class CommunityFilterBloc emit( state.copyWith( searchQuery: event.searchQuery, - selectedModerationStatus: event.selectedModerationStatus, + selectedCommentStatus: event.selectedCommentStatus, + selectedReportStatus: event.selectedReportStatus, selectedReportableEntity: event.selectedReportableEntity, selectedAppReviewFeedback: event.selectedAppReviewFeedback, + hasComment: event.hasComment, ), ); } diff --git a/lib/community_management/bloc/community_filter/community_filter_event.dart b/lib/community_management/bloc/community_filter/community_filter_event.dart index fa00fd68..92a675cd 100644 --- a/lib/community_management/bloc/community_filter/community_filter_event.dart +++ b/lib/community_management/bloc/community_filter/community_filter_event.dart @@ -10,22 +10,28 @@ abstract class CommunityFilterEvent extends Equatable { class CommunityFilterApplied extends CommunityFilterEvent { const CommunityFilterApplied({ this.searchQuery = '', - this.selectedModerationStatus = const [], + this.selectedCommentStatus = const [], + this.selectedReportStatus = const [], this.selectedReportableEntity = const [], this.selectedAppReviewFeedback = const [], + this.hasComment = HasCommentFilter.any, }); final String searchQuery; - final List selectedModerationStatus; + final List selectedCommentStatus; + final List selectedReportStatus; final List selectedReportableEntity; final List selectedAppReviewFeedback; + final HasCommentFilter hasComment; @override List get props => [ searchQuery, - selectedModerationStatus, + selectedCommentStatus, + selectedReportStatus, selectedReportableEntity, selectedAppReviewFeedback, + hasComment, ]; } diff --git a/lib/community_management/bloc/community_filter/community_filter_state.dart b/lib/community_management/bloc/community_filter/community_filter_state.dart index 30f401eb..904bdddd 100644 --- a/lib/community_management/bloc/community_filter/community_filter_state.dart +++ b/lib/community_management/bloc/community_filter/community_filter_state.dart @@ -1,40 +1,52 @@ part of 'community_filter_bloc.dart'; +enum HasCommentFilter { any, withComment, withoutComment } + class CommunityFilterState extends Equatable { const CommunityFilterState({ this.searchQuery = '', - this.selectedModerationStatus = const [], + this.selectedCommentStatus = const [], + this.selectedReportStatus = const [], this.selectedReportableEntity = const [], this.selectedAppReviewFeedback = const [], + this.hasComment = HasCommentFilter.any, }); final String searchQuery; - final List selectedModerationStatus; + final List selectedCommentStatus; + final List selectedReportStatus; final List selectedReportableEntity; final List selectedAppReviewFeedback; + final HasCommentFilter hasComment; CommunityFilterState copyWith({ String? searchQuery, - List? selectedModerationStatus, + List? selectedCommentStatus, + List? selectedReportStatus, List? selectedReportableEntity, List? selectedAppReviewFeedback, + HasCommentFilter? hasComment, }) { return CommunityFilterState( searchQuery: searchQuery ?? this.searchQuery, - selectedModerationStatus: - selectedModerationStatus ?? this.selectedModerationStatus, + selectedCommentStatus: + selectedCommentStatus ?? this.selectedCommentStatus, + selectedReportStatus: selectedReportStatus ?? this.selectedReportStatus, selectedReportableEntity: selectedReportableEntity ?? this.selectedReportableEntity, selectedAppReviewFeedback: selectedAppReviewFeedback ?? this.selectedAppReviewFeedback, + hasComment: hasComment ?? this.hasComment, ); } @override List get props => [ searchQuery, - selectedModerationStatus, + selectedCommentStatus, + selectedReportStatus, selectedReportableEntity, selectedAppReviewFeedback, + hasComment, ]; } From 408152d29781d1fc8977b30fbc301f4450226889 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:35:07 +0100 Subject: [PATCH 092/130] feat(community_management): enhance community filter dialog - Split moderation status filter into comment and report status filters - Add has comment filter for engagements tab - Update state and event classes to accommodate new filters - Refactor UI to display new filter options --- .../bloc/community_filter_dialog_bloc.dart | 31 ++++-- .../bloc/community_filter_dialog_event.dart | 28 +++++- .../bloc/community_filter_dialog_state.dart | 22 +++-- .../community_filter_dialog.dart | 94 ++++++++++++++++--- 4 files changed, 142 insertions(+), 33 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart index 769bb3a7..a4605f6e 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart @@ -17,15 +17,15 @@ class CommunityFilterDialogBloc ) { on(_onFilterDialogInitialized); on(_onSearchQueryChanged); - on( - _onModerationStatusChanged, - ); + on(_onCommentStatusChanged); + on(_onReportStatusChanged); on( _onReportableEntityChanged, ); on( _onAppReviewFeedbackChanged, ); + on(_onHasCommentChanged); on(_onFilterDialogReset); } @@ -36,12 +36,13 @@ class CommunityFilterDialogBloc emit( state.copyWith( searchQuery: event.communityFilterState.searchQuery, - selectedModerationStatus: - event.communityFilterState.selectedModerationStatus, + selectedCommentStatus: event.communityFilterState.selectedCommentStatus, + selectedReportStatus: event.communityFilterState.selectedReportStatus, selectedReportableEntity: event.communityFilterState.selectedReportableEntity, selectedAppReviewFeedback: event.communityFilterState.selectedAppReviewFeedback, + hasComment: event.communityFilterState.hasComment, ), ); } @@ -53,11 +54,18 @@ class CommunityFilterDialogBloc emit(state.copyWith(searchQuery: event.query)); } - void _onModerationStatusChanged( - CommunityFilterDialogModerationStatusChanged event, + void _onCommentStatusChanged( + CommunityFilterDialogCommentStatusChanged event, + Emitter emit, + ) { + emit(state.copyWith(selectedCommentStatus: event.commentStatus)); + } + + void _onReportStatusChanged( + CommunityFilterDialogReportStatusChanged event, Emitter emit, ) { - emit(state.copyWith(selectedModerationStatus: event.moderationStatus)); + emit(state.copyWith(selectedReportStatus: event.reportStatus)); } void _onReportableEntityChanged( @@ -74,6 +82,13 @@ class CommunityFilterDialogBloc emit(state.copyWith(selectedAppReviewFeedback: event.appReviewFeedback)); } + void _onHasCommentChanged( + CommunityFilterDialogHasCommentChanged event, + Emitter emit, + ) { + emit(state.copyWith(hasComment: event.hasComment)); + } + void _onFilterDialogReset( CommunityFilterDialogReset event, Emitter emit, diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart index cf1ad4a5..2940a08a 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart @@ -30,14 +30,24 @@ class CommunityFilterDialogSearchQueryChanged List get props => [query]; } -class CommunityFilterDialogModerationStatusChanged +class CommunityFilterDialogCommentStatusChanged extends CommunityFilterDialogEvent { - const CommunityFilterDialogModerationStatusChanged(this.moderationStatus); + const CommunityFilterDialogCommentStatusChanged(this.commentStatus); - final List moderationStatus; + final List commentStatus; @override - List get props => [moderationStatus]; + List get props => [commentStatus]; +} + +class CommunityFilterDialogReportStatusChanged + extends CommunityFilterDialogEvent { + const CommunityFilterDialogReportStatusChanged(this.reportStatus); + + final List reportStatus; + + @override + List get props => [reportStatus]; } class CommunityFilterDialogReportableEntityChanged @@ -60,6 +70,16 @@ class CommunityFilterDialogAppReviewFeedbackChanged List get props => [appReviewFeedback]; } +class CommunityFilterDialogHasCommentChanged + extends CommunityFilterDialogEvent { + const CommunityFilterDialogHasCommentChanged(this.hasComment); + + final HasCommentFilter hasComment; + + @override + List get props => [hasComment]; +} + class CommunityFilterDialogReset extends CommunityFilterDialogEvent { const CommunityFilterDialogReset(); } diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart index 510b8cc3..bfd8deef 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart @@ -4,32 +4,40 @@ class CommunityFilterDialogState extends Equatable { const CommunityFilterDialogState({ required this.activeTab, this.searchQuery = '', - this.selectedModerationStatus = const [], + this.selectedCommentStatus = const [], + this.selectedReportStatus = const [], this.selectedReportableEntity = const [], this.selectedAppReviewFeedback = const [], + this.hasComment = HasCommentFilter.any, }); final CommunityManagementTab activeTab; final String searchQuery; - final List selectedModerationStatus; + final List selectedCommentStatus; + final List selectedReportStatus; final List selectedReportableEntity; final List selectedAppReviewFeedback; + final HasCommentFilter hasComment; CommunityFilterDialogState copyWith({ String? searchQuery, - List? selectedModerationStatus, + List? selectedCommentStatus, + List? selectedReportStatus, List? selectedReportableEntity, List? selectedAppReviewFeedback, + HasCommentFilter? hasComment, }) { return CommunityFilterDialogState( activeTab: activeTab, searchQuery: searchQuery ?? this.searchQuery, - selectedModerationStatus: - selectedModerationStatus ?? this.selectedModerationStatus, + selectedCommentStatus: + selectedCommentStatus ?? this.selectedCommentStatus, + selectedReportStatus: selectedReportStatus ?? this.selectedReportStatus, selectedReportableEntity: selectedReportableEntity ?? this.selectedReportableEntity, selectedAppReviewFeedback: selectedAppReviewFeedback ?? this.selectedAppReviewFeedback, + hasComment: hasComment ?? this.hasComment, ); } @@ -37,8 +45,10 @@ class CommunityFilterDialogState extends Equatable { List get props => [ activeTab, searchQuery, - selectedModerationStatus, + selectedCommentStatus, + selectedReportStatus, selectedReportableEntity, selectedAppReviewFeedback, + hasComment, ]; } diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index 6cf36cd7..c6d501ba 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -36,9 +36,11 @@ class _CommunityFilterDialogState extends State { context.read().add( CommunityFilterApplied( searchQuery: filterDialogState.searchQuery, - selectedModerationStatus: filterDialogState.selectedModerationStatus, + selectedCommentStatus: filterDialogState.selectedCommentStatus, + selectedReportStatus: filterDialogState.selectedReportStatus, selectedReportableEntity: filterDialogState.selectedReportableEntity, selectedAppReviewFeedback: filterDialogState.selectedAppReviewFeedback, + hasComment: filterDialogState.hasComment, ), ); } @@ -116,11 +118,14 @@ class _CommunityFilterDialogState extends State { ); } - Widget _buildModerationStatusFilter( - CommunityFilterDialogState state, - AppLocalizations l10n, - ThemeData theme, - ) { + Widget _buildStatusFilter({ + required BuildContext context, + required CommunityFilterDialogState state, + required AppLocalizations l10n, + required ThemeData theme, + required List selectedStatuses, + required void Function(List) onChanged, + }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -132,24 +137,59 @@ class _CommunityFilterDialogState extends State { Wrap( spacing: AppSpacing.sm, children: ModerationStatus.values.map((status) { - final isSelected = state.selectedModerationStatus.contains(status); + final isSelected = selectedStatuses.contains(status); return ChoiceChip( label: Text(status.l10n(context)), selected: isSelected, onSelected: (selected) { final currentSelection = List.from( - state.selectedModerationStatus, + selectedStatuses, ); if (selected) { currentSelection.add(status); } else { currentSelection.remove(status); } - context.read().add( - CommunityFilterDialogModerationStatusChanged( - currentSelection, - ), - ); + onChanged(currentSelection); + }, + ); + }).toList(), + ), + ], + ); + } + + Widget _buildHasCommentFilter( + CommunityFilterDialogState state, + AppLocalizations l10n, + ThemeData theme, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.hasComment, + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: AppSpacing.sm), + Wrap( + spacing: AppSpacing.sm, + children: HasCommentFilter.values.map((filter) { + return ChoiceChip( + label: Text( + switch (filter) { + HasCommentFilter.any => l10n.any, + HasCommentFilter.withComment => l10n.withComment, + HasCommentFilter.withoutComment => l10n.withoutComment, + }, + ), + selected: state.hasComment == filter, + onSelected: (selected) { + if (selected) { + context.read().add( + CommunityFilterDialogHasCommentChanged(filter), + ); + } }, ); }).toList(), @@ -165,10 +205,34 @@ class _CommunityFilterDialogState extends State { ) { switch (state.activeTab) { case CommunityManagementTab.engagements: - return [_buildModerationStatusFilter(state, l10n, theme)]; + return [ + _buildStatusFilter( + context: context, + state: state, + l10n: l10n, + theme: theme, + selectedStatuses: state.selectedCommentStatus, + onChanged: (statuses) => + context.read().add( + CommunityFilterDialogCommentStatusChanged(statuses), + ), + ), + const SizedBox(height: AppSpacing.lg), + _buildHasCommentFilter(state, l10n, theme), + ]; case CommunityManagementTab.reports: return [ - _buildModerationStatusFilter(state, l10n, theme), + _buildStatusFilter( + context: context, + state: state, + l10n: l10n, + theme: theme, + selectedStatuses: state.selectedReportStatus, + onChanged: (statuses) => + context.read().add( + CommunityFilterDialogReportStatusChanged(statuses), + ), + ), const SizedBox(height: AppSpacing.lg), SearchableSelectionInput( label: l10n.reportedItem, From 68a95eb03d9cfda83509078fe79687e8e2aa6c48 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:35:36 +0100 Subject: [PATCH 093/130] fix(community_management): update filter logic for engagements and reports - Update _areFiltersActive logic in engagements_page.dart to use selectedCommentStatus and hasComment filters - Update _areFiltersActive logic in reports_page.dart to use selectedReportStatus filter - Improve filter functionality to accurately reflect active filters --- lib/community_management/view/engagements_page.dart | 3 ++- lib/community_management/view/reports_page.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index b44b2480..77ebccd8 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -35,7 +35,8 @@ class _EngagementsPageState extends State { bool _areFiltersActive(CommunityFilterState state) { return state.searchQuery.isNotEmpty || - state.selectedModerationStatus.isNotEmpty; + state.selectedCommentStatus.isNotEmpty || + state.hasComment != HasCommentFilter.any; } @override diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index 4d39d083..906ecaa9 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -33,7 +33,7 @@ class _ReportsPageState extends State { bool _areFiltersActive(CommunityFilterState state) { return state.searchQuery.isNotEmpty || - state.selectedModerationStatus.isNotEmpty || + state.selectedReportStatus.isNotEmpty || state.selectedReportableEntity.isNotEmpty; } From 6d7da601f13e4cb653daa1d9b8a3600613f909d6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:36:03 +0100 Subject: [PATCH 094/130] fix(community_management): improve filter logic for comments and reports - Rename `selectedModerationStatus` to `selectedCommentStatus` for clarity - Add filters for comment existence (has comment or not) - Rename `selectedModerationStatus` to `selectedReportStatus` in report filters - Update filter logic to use correct status variables --- .../bloc/community_management_bloc.dart | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 2a6bee1b..04ca40a1 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -111,9 +111,18 @@ class CommunityManagementBloc if (state.searchQuery.isNotEmpty) { filter['userId'] = state.searchQuery; } - if (state.selectedModerationStatus.isNotEmpty) { + if (state.selectedCommentStatus.isNotEmpty) { filter['comment.status'] = { - r'$in': state.selectedModerationStatus.map((s) => s.name).toList(), + r'$in': state.selectedCommentStatus.map((s) => s.name).toList(), + }; + } + if (state.hasComment == HasCommentFilter.withComment) { + filter['comment'] = { + r'$exists': true, + }; + } else if (state.hasComment == HasCommentFilter.withoutComment) { + filter['comment'] = { + r'$exists': false, }; } return filter; @@ -124,9 +133,9 @@ class CommunityManagementBloc if (state.searchQuery.isNotEmpty) { filter['reporterUserId'] = state.searchQuery; } - if (state.selectedModerationStatus.isNotEmpty) { + if (state.selectedReportStatus.isNotEmpty) { filter['status'] = { - r'$in': state.selectedModerationStatus.map((s) => s.name).toList(), + r'$in': state.selectedReportStatus.map((s) => s.name).toList(), }; } if (state.selectedReportableEntity.isNotEmpty) { From 96cd5b03f63517cea0723d6174da2b6e9bfb17aa Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 11:36:14 +0100 Subject: [PATCH 095/130] fix(community_management): resolve comment by removing it - Replace comment resolution with complete removal of the comment - Update Engagement object manually instead of using copyWith - Set comment to null and update updatedAt timestamp --- .../widgets/community_action_buttons.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index 8035ce9f..8e89cc1f 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -256,10 +256,15 @@ class CommunityActionButtons extends StatelessWidget { ), ElevatedButton( onPressed: () { - final updatedEngagement = item.copyWith( - comment: item.comment?.copyWith( - status: ModerationStatus.resolved, - ), + final updatedEngagement = Engagement( + id: item.id, + userId: item.userId, + entityId: item.entityId, + entityType: item.entityType, + reaction: item.reaction, + comment: null, + createdAt: item.createdAt, + updatedAt: DateTime.now(), ); engagementsRepository.update( id: updatedEngagement.id, From ec21557eaf9b6a5a1f34b3998ee7be93ff69e759 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 12:57:11 +0100 Subject: [PATCH 096/130] refactor(community_management): improve filter dialog functionality - Implement separate filter states for engagements, reports, and app reviews - Add capsule filter widget for reportable entity and app review feedback - Update filter dialog to handle multiple tabs and specific filter criteria - Rename events and states to reflect new filtering structure --- .../bloc/community_filter_dialog_bloc.dart | 94 +++++++++---- .../bloc/community_filter_dialog_event.dart | 24 ++-- .../bloc/community_filter_dialog_state.dart | 48 ++----- .../community_filter_dialog.dart | 129 ++++++++++++------ 4 files changed, 182 insertions(+), 113 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart index a4605f6e..93221770 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart @@ -17,13 +17,15 @@ class CommunityFilterDialogBloc ) { on(_onFilterDialogInitialized); on(_onSearchQueryChanged); - on(_onCommentStatusChanged); - on(_onReportStatusChanged); + on( + _onEngagementsStatusChanged, + ); + on(_onReportsStatusChanged); on( _onReportableEntityChanged, ); - on( - _onAppReviewFeedbackChanged, + on( + _onAppReviewsFeedbackChanged, ); on(_onHasCommentChanged); on(_onFilterDialogReset); @@ -34,15 +36,11 @@ class CommunityFilterDialogBloc Emitter emit, ) { emit( - state.copyWith( - searchQuery: event.communityFilterState.searchQuery, - selectedCommentStatus: event.communityFilterState.selectedCommentStatus, - selectedReportStatus: event.communityFilterState.selectedReportStatus, - selectedReportableEntity: - event.communityFilterState.selectedReportableEntity, - selectedAppReviewFeedback: - event.communityFilterState.selectedAppReviewFeedback, - hasComment: event.communityFilterState.hasComment, + CommunityFilterDialogState( + activeTab: event.activeTab, + engagementsFilter: event.communityFilterState.engagementsFilter, + reportsFilter: event.communityFilterState.reportsFilter, + appReviewsFilter: event.communityFilterState.appReviewsFilter, ), ); } @@ -51,42 +49,88 @@ class CommunityFilterDialogBloc CommunityFilterDialogSearchQueryChanged event, Emitter emit, ) { - emit(state.copyWith(searchQuery: event.query)); + switch (state.activeTab) { + case CommunityManagementTab.engagements: + emit( + state.copyWith( + engagementsFilter: EngagementsFilterState( + searchQuery: event.query, + selectedStatus: state.engagementsFilter.selectedStatus, + hasComment: state.engagementsFilter.hasComment, + ), + ), + ); + case CommunityManagementTab.reports: + emit( + state.copyWith( + reportsFilter: ReportsFilterState( + searchQuery: event.query, + selectedStatus: state.reportsFilter.selectedStatus, + selectedReportableEntity: + state.reportsFilter.selectedReportableEntity, + ), + ), + ); + case CommunityManagementTab.appReviews: + emit( + state.copyWith( + appReviewsFilter: AppReviewsFilterState( + searchQuery: event.query, + selectedFeedback: state.appReviewsFilter.selectedFeedback, + ), + ), + ); + } } - void _onCommentStatusChanged( - CommunityFilterDialogCommentStatusChanged event, + void _onEngagementsStatusChanged( + CommunityFilterDialogEngagementsStatusChanged event, Emitter emit, ) { - emit(state.copyWith(selectedCommentStatus: event.commentStatus)); + final newFilter = state.engagementsFilter.copyWith( + selectedStatus: event.status, + ); + emit(state.copyWith(engagementsFilter: newFilter)); } - void _onReportStatusChanged( - CommunityFilterDialogReportStatusChanged event, + void _onReportsStatusChanged( + CommunityFilterDialogReportsStatusChanged event, Emitter emit, ) { - emit(state.copyWith(selectedReportStatus: event.reportStatus)); + final newFilter = state.reportsFilter.copyWith( + selectedStatus: event.status, + ); + emit(state.copyWith(reportsFilter: newFilter)); } void _onReportableEntityChanged( CommunityFilterDialogReportableEntityChanged event, Emitter emit, ) { - emit(state.copyWith(selectedReportableEntity: event.reportableEntity)); + final newFilter = state.reportsFilter.copyWith( + selectedReportableEntity: event.reportableEntity, + ); + emit(state.copyWith(reportsFilter: newFilter)); } - void _onAppReviewFeedbackChanged( - CommunityFilterDialogAppReviewFeedbackChanged event, + void _onAppReviewsFeedbackChanged( + CommunityFilterDialogAppReviewsFeedbackChanged event, Emitter emit, ) { - emit(state.copyWith(selectedAppReviewFeedback: event.appReviewFeedback)); + final newFilter = state.appReviewsFilter.copyWith( + selectedFeedback: event.feedback, + ); + emit(state.copyWith(appReviewsFilter: newFilter)); } void _onHasCommentChanged( CommunityFilterDialogHasCommentChanged event, Emitter emit, ) { - emit(state.copyWith(hasComment: event.hasComment)); + final newFilter = state.engagementsFilter.copyWith( + hasComment: event.hasComment, + ); + emit(state.copyWith(engagementsFilter: newFilter)); } void _onFilterDialogReset( diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart index 2940a08a..26e0e14c 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart @@ -30,24 +30,24 @@ class CommunityFilterDialogSearchQueryChanged List get props => [query]; } -class CommunityFilterDialogCommentStatusChanged +class CommunityFilterDialogEngagementsStatusChanged extends CommunityFilterDialogEvent { - const CommunityFilterDialogCommentStatusChanged(this.commentStatus); + const CommunityFilterDialogEngagementsStatusChanged(this.status); - final List commentStatus; + final List status; @override - List get props => [commentStatus]; + List get props => [status]; } -class CommunityFilterDialogReportStatusChanged +class CommunityFilterDialogReportsStatusChanged extends CommunityFilterDialogEvent { - const CommunityFilterDialogReportStatusChanged(this.reportStatus); + const CommunityFilterDialogReportsStatusChanged(this.status); - final List reportStatus; + final List status; @override - List get props => [reportStatus]; + List get props => [status]; } class CommunityFilterDialogReportableEntityChanged @@ -60,14 +60,14 @@ class CommunityFilterDialogReportableEntityChanged List get props => [reportableEntity]; } -class CommunityFilterDialogAppReviewFeedbackChanged +class CommunityFilterDialogAppReviewsFeedbackChanged extends CommunityFilterDialogEvent { - const CommunityFilterDialogAppReviewFeedbackChanged(this.appReviewFeedback); + const CommunityFilterDialogAppReviewsFeedbackChanged(this.feedback); - final List appReviewFeedback; + final List feedback; @override - List get props => [appReviewFeedback]; + List get props => [feedback]; } class CommunityFilterDialogHasCommentChanged diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart index bfd8deef..87259f11 100644 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart +++ b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart @@ -3,52 +3,34 @@ part of 'community_filter_dialog_bloc.dart'; class CommunityFilterDialogState extends Equatable { const CommunityFilterDialogState({ required this.activeTab, - this.searchQuery = '', - this.selectedCommentStatus = const [], - this.selectedReportStatus = const [], - this.selectedReportableEntity = const [], - this.selectedAppReviewFeedback = const [], - this.hasComment = HasCommentFilter.any, + this.engagementsFilter = const EngagementsFilterState(), + this.reportsFilter = const ReportsFilterState(), + this.appReviewsFilter = const AppReviewsFilterState(), }); final CommunityManagementTab activeTab; - final String searchQuery; - final List selectedCommentStatus; - final List selectedReportStatus; - final List selectedReportableEntity; - final List selectedAppReviewFeedback; - final HasCommentFilter hasComment; + final EngagementsFilterState engagementsFilter; + final ReportsFilterState reportsFilter; + final AppReviewsFilterState appReviewsFilter; CommunityFilterDialogState copyWith({ - String? searchQuery, - List? selectedCommentStatus, - List? selectedReportStatus, - List? selectedReportableEntity, - List? selectedAppReviewFeedback, - HasCommentFilter? hasComment, + EngagementsFilterState? engagementsFilter, + ReportsFilterState? reportsFilter, + AppReviewsFilterState? appReviewsFilter, }) { return CommunityFilterDialogState( activeTab: activeTab, - searchQuery: searchQuery ?? this.searchQuery, - selectedCommentStatus: - selectedCommentStatus ?? this.selectedCommentStatus, - selectedReportStatus: selectedReportStatus ?? this.selectedReportStatus, - selectedReportableEntity: - selectedReportableEntity ?? this.selectedReportableEntity, - selectedAppReviewFeedback: - selectedAppReviewFeedback ?? this.selectedAppReviewFeedback, - hasComment: hasComment ?? this.hasComment, + engagementsFilter: engagementsFilter ?? this.engagementsFilter, + reportsFilter: reportsFilter ?? this.reportsFilter, + appReviewsFilter: appReviewsFilter ?? this.appReviewsFilter, ); } @override List get props => [ activeTab, - searchQuery, - selectedCommentStatus, - selectedReportStatus, - selectedReportableEntity, - selectedAppReviewFeedback, - hasComment, + engagementsFilter, + reportsFilter, + appReviewsFilter, ]; } diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index c6d501ba..f65385fb 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -7,7 +7,6 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/community_manage import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/searchable_selection_input.dart'; import 'package:ui_kit/ui_kit.dart'; class CommunityFilterDialog extends StatefulWidget { @@ -33,16 +32,12 @@ class _CommunityFilterDialogState extends State { } void _dispatchFilterApplied(CommunityFilterDialogState filterDialogState) { - context.read().add( - CommunityFilterApplied( - searchQuery: filterDialogState.searchQuery, - selectedCommentStatus: filterDialogState.selectedCommentStatus, - selectedReportStatus: filterDialogState.selectedReportStatus, - selectedReportableEntity: filterDialogState.selectedReportableEntity, - selectedAppReviewFeedback: filterDialogState.selectedAppReviewFeedback, - hasComment: filterDialogState.hasComment, - ), - ); + context.read() + ..add(EngagementsFilterChanged(filterDialogState.engagementsFilter)) + ..add(ReportsFilterChanged(filterDialogState.reportsFilter)) + ..add( + AppReviewsFilterChanged(filterDialogState.appReviewsFilter), + ); } @override @@ -52,12 +47,23 @@ class _CommunityFilterDialogState extends State { return BlocBuilder( builder: (context, filterDialogState) { - if (_searchController.text != filterDialogState.searchQuery) { - _searchController.text = filterDialogState.searchQuery; - _searchController.selection = TextSelection.fromPosition( - TextPosition(offset: _searchController.text.length), - ); + final String currentSearchQuery; + switch (filterDialogState.activeTab) { + case CommunityManagementTab.engagements: + currentSearchQuery = + filterDialogState.engagementsFilter.searchQuery; + case CommunityManagementTab.reports: + currentSearchQuery = filterDialogState.reportsFilter.searchQuery; + case CommunityManagementTab.appReviews: + currentSearchQuery = filterDialogState.appReviewsFilter.searchQuery; + } + + if (_searchController.text != currentSearchQuery) { + _searchController.text = currentSearchQuery; } + _searchController.selection = TextSelection.fromPosition( + TextPosition(offset: _searchController.text.length), + ); return Scaffold( appBar: AppBar( @@ -183,7 +189,7 @@ class _CommunityFilterDialogState extends State { HasCommentFilter.withoutComment => l10n.withoutComment, }, ), - selected: state.hasComment == filter, + selected: state.engagementsFilter.hasComment == filter, onSelected: (selected) { if (selected) { context.read().add( @@ -198,6 +204,45 @@ class _CommunityFilterDialogState extends State { ); } + Widget _buildCapsuleFilter({ + required String title, + required List allValues, + required List selectedValues, + required String Function(T) labelBuilder, + required void Function(List) onChanged, + }) { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: AppSpacing.sm), + Wrap( + spacing: AppSpacing.sm, + children: allValues.map((value) { + final isSelected = selectedValues.contains(value); + return ChoiceChip( + label: Text(labelBuilder(value)), + selected: isSelected, + onSelected: (selected) { + final currentSelection = List.from(selectedValues); + if (selected) { + currentSelection.add(value); + } else { + currentSelection.remove(value); + } + onChanged(currentSelection); + }, + ); + }).toList(), + ), + ], + ); + } + List _buildTabSpecificFilters( CommunityFilterDialogState state, AppLocalizations l10n, @@ -211,10 +256,10 @@ class _CommunityFilterDialogState extends State { state: state, l10n: l10n, theme: theme, - selectedStatuses: state.selectedCommentStatus, + selectedStatuses: state.engagementsFilter.selectedStatus, onChanged: (statuses) => context.read().add( - CommunityFilterDialogCommentStatusChanged(statuses), + CommunityFilterDialogEngagementsStatusChanged(statuses), ), ), const SizedBox(height: AppSpacing.lg), @@ -227,39 +272,37 @@ class _CommunityFilterDialogState extends State { state: state, l10n: l10n, theme: theme, - selectedStatuses: state.selectedReportStatus, + selectedStatuses: state.reportsFilter.selectedStatus, onChanged: (statuses) => context.read().add( - CommunityFilterDialogReportStatusChanged(statuses), + CommunityFilterDialogReportsStatusChanged(statuses), ), ), const SizedBox(height: AppSpacing.lg), - SearchableSelectionInput( - label: l10n.reportedItem, - hintText: l10n.selectReportableEntity, - isMultiSelect: true, - selectedItems: state.selectedReportableEntity, - itemBuilder: (context, item) => Text(item.l10n(context)), - itemToString: (item) => item.l10n(context), - onChanged: (items) => context.read().add( - CommunityFilterDialogReportableEntityChanged(items ?? []), - ), - staticItems: ReportableEntity.values, + _buildCapsuleFilter( + title: l10n.reportedItem, + allValues: ReportableEntity.values, + selectedValues: state.reportsFilter.selectedReportableEntity, + labelBuilder: (item) => item.l10n(context), + onChanged: (items) { + context.read().add( + CommunityFilterDialogReportableEntityChanged(items), + ); + }, ), ]; case CommunityManagementTab.appReviews: return [ - SearchableSelectionInput( - label: l10n.initialFeedback, - hintText: l10n.selectInitialFeedback, - isMultiSelect: true, - selectedItems: state.selectedAppReviewFeedback, - itemBuilder: (context, item) => Text(item.l10n(context)), - itemToString: (item) => item.l10n(context), - onChanged: (items) => context.read().add( - CommunityFilterDialogAppReviewFeedbackChanged(items ?? []), - ), - staticItems: AppReviewFeedback.values, + _buildCapsuleFilter( + title: l10n.initialFeedback, + allValues: AppReviewFeedback.values, + selectedValues: state.appReviewsFilter.selectedFeedback, + labelBuilder: (item) => item.l10n(context), + onChanged: (items) { + context.read().add( + CommunityFilterDialogAppReviewsFeedbackChanged(items), + ); + }, ), ]; } From 5ad6f0810a61fb9eed871deccf223a6130f1ee62 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 12:57:47 +0100 Subject: [PATCH 097/130] refactor(community_management): split CommunityFilterState and improve filtering - Split CommunityFilterState into separate filter states for engagements, reports, and app reviews - Update CommunityFilterBloc to handle separate filter change events - Modify UI pages to use specific filter states for engagements, reports, and app reviews - Remove redundant filter checks in UI pages --- .../community_filter_bloc.dart | 33 +++-- .../community_filter_event.dart | 39 +++--- .../community_filter_state.dart | 128 ++++++++++++++---- .../view/app_reviews_page.dart | 14 +- .../view/engagements_page.dart | 21 +-- .../view/reports_page.dart | 18 ++- 6 files changed, 163 insertions(+), 90 deletions(-) diff --git a/lib/community_management/bloc/community_filter/community_filter_bloc.dart b/lib/community_management/bloc/community_filter/community_filter_bloc.dart index 28c2796e..0b00b5e3 100644 --- a/lib/community_management/bloc/community_filter/community_filter_bloc.dart +++ b/lib/community_management/bloc/community_filter/community_filter_bloc.dart @@ -8,24 +8,31 @@ part 'community_filter_state.dart'; class CommunityFilterBloc extends Bloc { CommunityFilterBloc() : super(const CommunityFilterState()) { - on(_onFilterApplied); + on(_onEngagementsFilterChanged); + on(_onReportsFilterChanged); + on(_onAppReviewsFilterChanged); on(_onFilterReset); } - void _onFilterApplied( - CommunityFilterApplied event, + void _onEngagementsFilterChanged( + EngagementsFilterChanged event, Emitter emit, ) { - emit( - state.copyWith( - searchQuery: event.searchQuery, - selectedCommentStatus: event.selectedCommentStatus, - selectedReportStatus: event.selectedReportStatus, - selectedReportableEntity: event.selectedReportableEntity, - selectedAppReviewFeedback: event.selectedAppReviewFeedback, - hasComment: event.hasComment, - ), - ); + emit(state.copyWith(engagementsFilter: event.filterState)); + } + + void _onReportsFilterChanged( + ReportsFilterChanged event, + Emitter emit, + ) { + emit(state.copyWith(reportsFilter: event.filterState)); + } + + void _onAppReviewsFilterChanged( + AppReviewsFilterChanged event, + Emitter emit, + ) { + emit(state.copyWith(appReviewsFilter: event.filterState)); } void _onFilterReset( diff --git a/lib/community_management/bloc/community_filter/community_filter_event.dart b/lib/community_management/bloc/community_filter/community_filter_event.dart index 92a675cd..9a206a99 100644 --- a/lib/community_management/bloc/community_filter/community_filter_event.dart +++ b/lib/community_management/bloc/community_filter/community_filter_event.dart @@ -7,32 +7,25 @@ abstract class CommunityFilterEvent extends Equatable { List get props => []; } -class CommunityFilterApplied extends CommunityFilterEvent { - const CommunityFilterApplied({ - this.searchQuery = '', - this.selectedCommentStatus = const [], - this.selectedReportStatus = const [], - this.selectedReportableEntity = const [], - this.selectedAppReviewFeedback = const [], - this.hasComment = HasCommentFilter.any, - }); +class EngagementsFilterChanged extends CommunityFilterEvent { + const EngagementsFilterChanged(this.filterState); + final EngagementsFilterState filterState; + @override + List get props => [filterState]; +} - final String searchQuery; - final List selectedCommentStatus; - final List selectedReportStatus; - final List selectedReportableEntity; - final List selectedAppReviewFeedback; - final HasCommentFilter hasComment; +class ReportsFilterChanged extends CommunityFilterEvent { + const ReportsFilterChanged(this.filterState); + final ReportsFilterState filterState; + @override + List get props => [filterState]; +} +class AppReviewsFilterChanged extends CommunityFilterEvent { + const AppReviewsFilterChanged(this.filterState); + final AppReviewsFilterState filterState; @override - List get props => [ - searchQuery, - selectedCommentStatus, - selectedReportStatus, - selectedReportableEntity, - selectedAppReviewFeedback, - hasComment, - ]; + List get props => [filterState]; } class CommunityFilterReset extends CommunityFilterEvent { diff --git a/lib/community_management/bloc/community_filter/community_filter_state.dart b/lib/community_management/bloc/community_filter/community_filter_state.dart index 904bdddd..7a39b5de 100644 --- a/lib/community_management/bloc/community_filter/community_filter_state.dart +++ b/lib/community_management/bloc/community_filter/community_filter_state.dart @@ -4,49 +4,127 @@ enum HasCommentFilter { any, withComment, withoutComment } class CommunityFilterState extends Equatable { const CommunityFilterState({ + this.engagementsFilter = const EngagementsFilterState(), + this.reportsFilter = const ReportsFilterState(), + this.appReviewsFilter = const AppReviewsFilterState(), + }); + + final EngagementsFilterState engagementsFilter; + final ReportsFilterState reportsFilter; + final AppReviewsFilterState appReviewsFilter; + + CommunityFilterState copyWith({ + EngagementsFilterState? engagementsFilter, + ReportsFilterState? reportsFilter, + AppReviewsFilterState? appReviewsFilter, + }) { + return CommunityFilterState( + engagementsFilter: engagementsFilter ?? this.engagementsFilter, + reportsFilter: reportsFilter ?? this.reportsFilter, + appReviewsFilter: appReviewsFilter ?? this.appReviewsFilter, + ); + } + + @override + List get props => [ + engagementsFilter, + reportsFilter, + appReviewsFilter, + ]; +} + +class EngagementsFilterState extends Equatable { + const EngagementsFilterState({ this.searchQuery = '', - this.selectedCommentStatus = const [], - this.selectedReportStatus = const [], - this.selectedReportableEntity = const [], - this.selectedAppReviewFeedback = const [], + this.selectedStatus = const [], this.hasComment = HasCommentFilter.any, }); final String searchQuery; - final List selectedCommentStatus; - final List selectedReportStatus; - final List selectedReportableEntity; - final List selectedAppReviewFeedback; + final List selectedStatus; final HasCommentFilter hasComment; - CommunityFilterState copyWith({ + bool get isFilterActive => + searchQuery.isNotEmpty || + selectedStatus.isNotEmpty || + hasComment != HasCommentFilter.any; + + @override + List get props => [searchQuery, selectedStatus, hasComment]; + + EngagementsFilterState copyWith({ String? searchQuery, - List? selectedCommentStatus, - List? selectedReportStatus, - List? selectedReportableEntity, - List? selectedAppReviewFeedback, + List? selectedStatus, HasCommentFilter? hasComment, }) { - return CommunityFilterState( + return EngagementsFilterState( searchQuery: searchQuery ?? this.searchQuery, - selectedCommentStatus: - selectedCommentStatus ?? this.selectedCommentStatus, - selectedReportStatus: selectedReportStatus ?? this.selectedReportStatus, - selectedReportableEntity: - selectedReportableEntity ?? this.selectedReportableEntity, - selectedAppReviewFeedback: - selectedAppReviewFeedback ?? this.selectedAppReviewFeedback, + selectedStatus: selectedStatus ?? this.selectedStatus, hasComment: hasComment ?? this.hasComment, ); } +} + + +class ReportsFilterState extends Equatable { + const ReportsFilterState({ + this.searchQuery = '', + this.selectedStatus = const [], + this.selectedReportableEntity = const [], + }); + + final String searchQuery; + final List selectedStatus; + final List selectedReportableEntity; + + bool get isFilterActive => + searchQuery.isNotEmpty || + selectedStatus.isNotEmpty || + selectedReportableEntity.isNotEmpty; @override List get props => [ searchQuery, - selectedCommentStatus, - selectedReportStatus, + selectedStatus, selectedReportableEntity, - selectedAppReviewFeedback, - hasComment, ]; + + ReportsFilterState copyWith({ + String? searchQuery, + List? selectedStatus, + List? selectedReportableEntity, + }) { + return ReportsFilterState( + searchQuery: searchQuery ?? this.searchQuery, + selectedStatus: selectedStatus ?? this.selectedStatus, + selectedReportableEntity: + selectedReportableEntity ?? this.selectedReportableEntity, + ); + } +} + +class AppReviewsFilterState extends Equatable { + const AppReviewsFilterState({ + this.searchQuery = '', + this.selectedFeedback = const [], + }); + + final String searchQuery; + final List selectedFeedback; + + bool get isFilterActive => + searchQuery.isNotEmpty || selectedFeedback.isNotEmpty; + + @override + List get props => [searchQuery, selectedFeedback]; + + AppReviewsFilterState copyWith({ + String? searchQuery, + List? selectedFeedback, + }) { + return AppReviewsFilterState( + searchQuery: searchQuery ?? this.searchQuery, + selectedFeedback: selectedFeedback ?? this.selectedFeedback, + ); + } } diff --git a/lib/community_management/view/app_reviews_page.dart b/lib/community_management/view/app_reviews_page.dart index 9225eb5e..96c10da6 100644 --- a/lib/community_management/view/app_reviews_page.dart +++ b/lib/community_management/view/app_reviews_page.dart @@ -28,17 +28,12 @@ class _AppReviewsPageState extends State { filter: context .read() .buildAppReviewsFilterMap( - context.read().state, + context.read().state.appReviewsFilter, ), ), ); } - bool _areFiltersActive(CommunityFilterState state) { - return state.searchQuery.isNotEmpty || - state.selectedAppReviewFeedback.isNotEmpty; - } - @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -49,7 +44,8 @@ class _AppReviewsPageState extends State { final communityFilterState = context .watch() .state; - final filtersActive = _areFiltersActive(communityFilterState); + final filtersActive = + communityFilterState.appReviewsFilter.isFilterActive; if (state.appReviewsStatus == CommunityManagementStatus.loading && state.appReviews.isEmpty) { @@ -70,7 +66,7 @@ class _AppReviewsPageState extends State { filter: context .read() .buildAppReviewsFilterMap( - context.read().state, + context.read().state.appReviewsFilter, ), ), ), @@ -148,7 +144,7 @@ class _AppReviewsPageState extends State { filter: context .read() .buildAppReviewsFilterMap( - context.read().state, + context.read().state.appReviewsFilter, ), ), ); diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index 77ebccd8..8f6c48ca 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -27,18 +27,12 @@ class _EngagementsPageState extends State { filter: context .read() .buildEngagementsFilterMap( - context.read().state, + context.read().state.engagementsFilter, ), ), ); } - bool _areFiltersActive(CommunityFilterState state) { - return state.searchQuery.isNotEmpty || - state.selectedCommentStatus.isNotEmpty || - state.hasComment != HasCommentFilter.any; - } - @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -49,7 +43,8 @@ class _EngagementsPageState extends State { final communityFilterState = context .watch() .state; - final filtersActive = _areFiltersActive(communityFilterState); + final filtersActive = + communityFilterState.engagementsFilter.isFilterActive; if (state.engagementsStatus == CommunityManagementStatus.loading && state.engagements.isEmpty) { @@ -70,7 +65,10 @@ class _EngagementsPageState extends State { filter: context .read() .buildEngagementsFilterMap( - context.read().state, + context + .read() + .state + .engagementsFilter, ), ), ), @@ -154,7 +152,10 @@ class _EngagementsPageState extends State { filter: context .read() .buildEngagementsFilterMap( - context.read().state, + context + .read() + .state + .engagementsFilter, ), ), ); diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index 906ecaa9..46b69153 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -25,18 +25,12 @@ class _ReportsPageState extends State { LoadReportsRequested( limit: kDefaultRowsPerPage, filter: context.read().buildReportsFilterMap( - context.read().state, + context.read().state.reportsFilter, ), ), ); } - bool _areFiltersActive(CommunityFilterState state) { - return state.searchQuery.isNotEmpty || - state.selectedReportStatus.isNotEmpty || - state.selectedReportableEntity.isNotEmpty; - } - @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -47,7 +41,8 @@ class _ReportsPageState extends State { final communityFilterState = context .watch() .state; - final filtersActive = _areFiltersActive(communityFilterState); + final filtersActive = + communityFilterState.reportsFilter.isFilterActive; if (state.reportsStatus == CommunityManagementStatus.loading && state.reports.isEmpty) { @@ -68,7 +63,7 @@ class _ReportsPageState extends State { filter: context .read() .buildReportsFilterMap( - context.read().state, + context.read().state.reportsFilter, ), ), ), @@ -151,7 +146,10 @@ class _ReportsPageState extends State { filter: context .read() .buildReportsFilterMap( - context.read().state, + context + .read() + .state + .reportsFilter, ), ), ); From a2a212cf32d144751b82de055435e7ad555b781c Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 12:57:58 +0100 Subject: [PATCH 098/130] fix(community_management): prevent duplicate API calls when filters are unchanged - Only reload engagements/reports/app reviews when the relevant filter changes - Update filter state handling for each community management tab - Improve type safety for filter building functions --- .../bloc/community_management_bloc.dart | 70 +++++++++++-------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 04ca40a1..6123bcd1 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -32,26 +32,30 @@ class CommunityManagementBloc _filterSubscription = _communityFilterBloc.stream.listen((filterState) { switch (state.activeTab) { case CommunityManagementTab.engagements: - add( - LoadEngagementsRequested( - filter: buildEngagementsFilterMap(filterState), - forceRefresh: true, - ), - ); + if (filterState.engagementsFilter != + _communityFilterBloc.state.engagementsFilter) { + add( + LoadEngagementsRequested( + filter: buildEngagementsFilterMap( + filterState.engagementsFilter, + ), + forceRefresh: true, + ), + ); + } case CommunityManagementTab.reports: - add( - LoadReportsRequested( - filter: buildReportsFilterMap(filterState), - forceRefresh: true, - ), - ); + if (filterState.reportsFilter != + _communityFilterBloc.state.reportsFilter) { + add( + LoadReportsRequested( + filter: buildReportsFilterMap(filterState.reportsFilter), + forceRefresh: true, + ), + ); + } case CommunityManagementTab.appReviews: - add( - LoadAppReviewsRequested( - filter: buildAppReviewsFilterMap(filterState), - forceRefresh: true, - ), - ); + // No filter changes to listen to for app reviews yet. + break; } }); @@ -66,7 +70,9 @@ class CommunityManagementBloc _logger.info('Engagement updated, reloading engagements list.'); add( LoadEngagementsRequested( - filter: buildEngagementsFilterMap(_communityFilterBloc.state), + filter: buildEngagementsFilterMap( + _communityFilterBloc.state.engagementsFilter, + ), forceRefresh: true, ), ); @@ -74,7 +80,9 @@ class CommunityManagementBloc _logger.info('Report updated, reloading reports list.'); add( LoadReportsRequested( - filter: buildReportsFilterMap(_communityFilterBloc.state), + filter: buildReportsFilterMap( + _communityFilterBloc.state.reportsFilter, + ), forceRefresh: true, ), ); @@ -82,7 +90,9 @@ class CommunityManagementBloc _logger.info('AppReview updated, reloading app reviews list.'); add( LoadAppReviewsRequested( - filter: buildAppReviewsFilterMap(_communityFilterBloc.state), + filter: buildAppReviewsFilterMap( + _communityFilterBloc.state.appReviewsFilter, + ), forceRefresh: true, ), ); @@ -106,14 +116,14 @@ class CommunityManagementBloc return super.close(); } - Map buildEngagementsFilterMap(CommunityFilterState state) { + Map buildEngagementsFilterMap(EngagementsFilterState state) { final filter = {}; if (state.searchQuery.isNotEmpty) { filter['userId'] = state.searchQuery; } - if (state.selectedCommentStatus.isNotEmpty) { + if (state.selectedStatus.isNotEmpty) { filter['comment.status'] = { - r'$in': state.selectedCommentStatus.map((s) => s.name).toList(), + r'$in': state.selectedStatus.map((s) => s.name).toList(), }; } if (state.hasComment == HasCommentFilter.withComment) { @@ -128,14 +138,14 @@ class CommunityManagementBloc return filter; } - Map buildReportsFilterMap(CommunityFilterState state) { + Map buildReportsFilterMap(ReportsFilterState state) { final filter = {}; if (state.searchQuery.isNotEmpty) { filter['reporterUserId'] = state.searchQuery; } - if (state.selectedReportStatus.isNotEmpty) { + if (state.selectedStatus.isNotEmpty) { filter['status'] = { - r'$in': state.selectedReportStatus.map((s) => s.name).toList(), + r'$in': state.selectedStatus.map((s) => s.name).toList(), }; } if (state.selectedReportableEntity.isNotEmpty) { @@ -146,14 +156,14 @@ class CommunityManagementBloc return filter; } - Map buildAppReviewsFilterMap(CommunityFilterState state) { + Map buildAppReviewsFilterMap(AppReviewsFilterState state) { final filter = {}; if (state.searchQuery.isNotEmpty) { filter['userId'] = state.searchQuery; } - if (state.selectedAppReviewFeedback.isNotEmpty) { + if (state.selectedFeedback.isNotEmpty) { filter['feedback'] = { - r'$in': state.selectedAppReviewFeedback.map((f) => f.name).toList(), + r'$in': state.selectedFeedback.map((f) => f.name).toList(), }; } return filter; From 3cbb39f4bcda805de5893e605908e4861e07ce1d Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 13:08:59 +0100 Subject: [PATCH 099/130] feat(community_management): add 'any' option to multi select chips - Introduce an 'any' option to reset selection in community filter dialog - Implement functionality to clear all selected values when 'any' is chosen - Ensure existing selections are preserved unless 'any' is explicitly selected - Update UI to handle the new 'any' option alongside existing filter choices --- .../community_filter_dialog.dart | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index f65385fb..c84ebe0c 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -212,6 +212,8 @@ class _CommunityFilterDialogState extends State { required void Function(List) onChanged, }) { final theme = Theme.of(context); + final l10n = AppLocalizationsX(context).l10n; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -222,22 +224,33 @@ class _CommunityFilterDialogState extends State { const SizedBox(height: AppSpacing.sm), Wrap( spacing: AppSpacing.sm, - children: allValues.map((value) { - final isSelected = selectedValues.contains(value); - return ChoiceChip( - label: Text(labelBuilder(value)), - selected: isSelected, + children: [ + ChoiceChip( + label: Text(l10n.any), + selected: selectedValues.isEmpty, onSelected: (selected) { - final currentSelection = List.from(selectedValues); if (selected) { - currentSelection.add(value); - } else { - currentSelection.remove(value); + onChanged([]); } - onChanged(currentSelection); }, - ); - }).toList(), + ), + ...allValues.map((value) { + final isSelected = selectedValues.contains(value); + return ChoiceChip( + label: Text(labelBuilder(value)), + selected: isSelected, + onSelected: (selected) { + final currentSelection = List.from(selectedValues); + if (selected) { + currentSelection.add(value); + } else { + currentSelection.remove(value); + } + onChanged(currentSelection); + }, + ); + }), + ], ), ], ); From ca81fef9c7045cac2870995a25c53b72f49874b4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 13:15:14 +0100 Subject: [PATCH 100/130] feat(community_management): enhance community filter dialog - Add initial filter state loading functionality - Implement dynamic dialog title and search hint based on active tab - Refactor status filter into a reusable capsule filter widget - Update reset filters functionality to use CommunityFilterDialogBloc --- .../community_filter_dialog.dart | 196 +++++++++--------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index c84ebe0c..1c8210a4 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -23,6 +23,18 @@ class _CommunityFilterDialogState extends State { void initState() { super.initState(); _searchController = TextEditingController(); + _loadInitialFilterState(); + } + + void _loadInitialFilterState() { + final communityManagementBloc = context.read(); + final communityFilterBloc = context.read(); + context.read().add( + CommunityFilterDialogInitialized( + activeTab: communityManagementBloc.state.activeTab, + communityFilterState: communityFilterBloc.state, + ), + ); } @override @@ -67,7 +79,7 @@ class _CommunityFilterDialogState extends State { return Scaffold( appBar: AppBar( - title: Text(l10n.filterCommunity), + title: Text(_getDialogTitle(l10n, filterDialogState.activeTab)), leading: IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), @@ -77,8 +89,11 @@ class _CommunityFilterDialogState extends State { icon: const Icon(Icons.refresh), tooltip: l10n.resetFiltersButtonText, onPressed: () { - context.read().add( - const CommunityFilterReset(), + context.read().add( + const CommunityFilterDialogReset(), + ); + _dispatchFilterApplied( + context.read().state, ); Navigator.of(context).pop(); }, @@ -103,7 +118,10 @@ class _CommunityFilterDialogState extends State { controller: _searchController, decoration: InputDecoration( labelText: l10n.search, - hintText: l10n.searchByUserId, + hintText: _getSearchHint( + l10n, + filterDialogState.activeTab, + ), prefixIcon: const Icon(Icons.search), border: const OutlineInputBorder(), ), @@ -124,42 +142,80 @@ class _CommunityFilterDialogState extends State { ); } - Widget _buildStatusFilter({ - required BuildContext context, - required CommunityFilterDialogState state, - required AppLocalizations l10n, - required ThemeData theme, - required List selectedStatuses, - required void Function(List) onChanged, + String _getDialogTitle( + AppLocalizations l10n, + CommunityManagementTab activeTab, + ) { + switch (activeTab) { + case CommunityManagementTab.engagements: + return l10n.filterCommunity; + case CommunityManagementTab.reports: + return l10n.filterCommunity; + case CommunityManagementTab.appReviews: + return l10n.filterCommunity; + } + } + + String _getSearchHint( + AppLocalizations l10n, + CommunityManagementTab activeTab, + ) { + switch (activeTab) { + case CommunityManagementTab.engagements: + return l10n.searchByEngagementUser; + case CommunityManagementTab.reports: + return l10n.searchByReportReporter; + case CommunityManagementTab.appReviews: + return l10n.searchByAppReviewUser; + } + } + + Widget _buildCapsuleFilter({ + required String title, + required List allValues, + required List selectedValues, + required String Function(T) labelBuilder, + required void Function(List) onChanged, }) { + final theme = Theme.of(context); + final l10n = AppLocalizationsX(context).l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - l10n.status, + title, style: theme.textTheme.titleMedium, ), const SizedBox(height: AppSpacing.sm), Wrap( spacing: AppSpacing.sm, - children: ModerationStatus.values.map((status) { - final isSelected = selectedStatuses.contains(status); - return ChoiceChip( - label: Text(status.l10n(context)), - selected: isSelected, + children: [ + ChoiceChip( + label: Text(l10n.any), + selected: selectedValues.isEmpty, onSelected: (selected) { - final currentSelection = List.from( - selectedStatuses, - ); if (selected) { - currentSelection.add(status); - } else { - currentSelection.remove(status); + onChanged([]); } - onChanged(currentSelection); }, - ); - }).toList(), + ), + ...allValues.map((value) { + final isSelected = selectedValues.contains(value); + return ChoiceChip( + label: Text(labelBuilder(value)), + selected: isSelected, + onSelected: (selected) { + final currentSelection = List.from(selectedValues); + if (selected) { + currentSelection.add(value); + } else { + currentSelection.remove(value); + } + onChanged(currentSelection); + }, + ); + }), + ], ), ], ); @@ -204,58 +260,6 @@ class _CommunityFilterDialogState extends State { ); } - Widget _buildCapsuleFilter({ - required String title, - required List allValues, - required List selectedValues, - required String Function(T) labelBuilder, - required void Function(List) onChanged, - }) { - final theme = Theme.of(context); - final l10n = AppLocalizationsX(context).l10n; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: AppSpacing.sm), - Wrap( - spacing: AppSpacing.sm, - children: [ - ChoiceChip( - label: Text(l10n.any), - selected: selectedValues.isEmpty, - onSelected: (selected) { - if (selected) { - onChanged([]); - } - }, - ), - ...allValues.map((value) { - final isSelected = selectedValues.contains(value); - return ChoiceChip( - label: Text(labelBuilder(value)), - selected: isSelected, - onSelected: (selected) { - final currentSelection = List.from(selectedValues); - if (selected) { - currentSelection.add(value); - } else { - currentSelection.remove(value); - } - onChanged(currentSelection); - }, - ); - }), - ], - ), - ], - ); - } - List _buildTabSpecificFilters( CommunityFilterDialogState state, AppLocalizations l10n, @@ -264,32 +268,28 @@ class _CommunityFilterDialogState extends State { switch (state.activeTab) { case CommunityManagementTab.engagements: return [ - _buildStatusFilter( - context: context, - state: state, - l10n: l10n, - theme: theme, - selectedStatuses: state.engagementsFilter.selectedStatus, - onChanged: (statuses) => - context.read().add( - CommunityFilterDialogEngagementsStatusChanged(statuses), - ), + _buildCapsuleFilter( + title: l10n.status, + allValues: ModerationStatus.values, + selectedValues: state.engagementsFilter.selectedStatus, + labelBuilder: (item) => item.l10n(context), + onChanged: (items) => context.read().add( + CommunityFilterDialogEngagementsStatusChanged(items), + ), ), const SizedBox(height: AppSpacing.lg), _buildHasCommentFilter(state, l10n, theme), ]; case CommunityManagementTab.reports: return [ - _buildStatusFilter( - context: context, - state: state, - l10n: l10n, - theme: theme, - selectedStatuses: state.reportsFilter.selectedStatus, - onChanged: (statuses) => - context.read().add( - CommunityFilterDialogReportsStatusChanged(statuses), - ), + _buildCapsuleFilter( + title: l10n.status, + allValues: ModerationStatus.values, + selectedValues: state.reportsFilter.selectedStatus, + labelBuilder: (item) => item.l10n(context), + onChanged: (items) => context.read().add( + CommunityFilterDialogReportsStatusChanged(items), + ), ), const SizedBox(height: AppSpacing.lg), _buildCapsuleFilter( From 2cdcf07504c8f703004c44f7ce854ea21af00845 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 13:26:40 +0100 Subject: [PATCH 101/130] refactor(community_management): improve entity update handling - Replace single _entityUpdateSubscription with separate subscriptions for engagements, reports, and app reviews - Simplify subscription cancellation in close() method - Reorder code to follow a more logical structure - Improve readability and maintainability of the code --- .../bloc/community_management_bloc.dart | 81 ++++++++++--------- .../community_filter_dialog.dart | 10 +-- 2 files changed, 47 insertions(+), 44 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 6123bcd1..a3ea2fbb 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -59,45 +59,44 @@ class CommunityManagementBloc } }); - _entityUpdateSubscription = - Stream.multi((controller) { - controller - ..addStream(_engagementsRepository.entityUpdated) - ..addStream(_reportsRepository.entityUpdated) - ..addStream(_appReviewsRepository.entityUpdated); - }).listen((updatedType) { - if (updatedType == Engagement) { - _logger.info('Engagement updated, reloading engagements list.'); - add( - LoadEngagementsRequested( - filter: buildEngagementsFilterMap( - _communityFilterBloc.state.engagementsFilter, - ), - forceRefresh: true, - ), - ); - } else if (updatedType == Report) { - _logger.info('Report updated, reloading reports list.'); - add( - LoadReportsRequested( - filter: buildReportsFilterMap( - _communityFilterBloc.state.reportsFilter, - ), - forceRefresh: true, - ), - ); - } else if (updatedType == AppReview) { - _logger.info('AppReview updated, reloading app reviews list.'); - add( - LoadAppReviewsRequested( - filter: buildAppReviewsFilterMap( - _communityFilterBloc.state.appReviewsFilter, - ), - forceRefresh: true, + _engagementsUpdateSubscription = _engagementsRepository.entityUpdated + .listen((_) { + _logger.info('Engagement updated, reloading engagements list.'); + add( + LoadEngagementsRequested( + filter: buildEngagementsFilterMap( + _communityFilterBloc.state.engagementsFilter, ), - ); - } + forceRefresh: true, + ), + ); }); + + _reportsUpdateSubscription = _reportsRepository.entityUpdated.listen((_) { + _logger.info('Report updated, reloading reports list.'); + add( + LoadReportsRequested( + filter: buildReportsFilterMap( + _communityFilterBloc.state.reportsFilter, + ), + forceRefresh: true, + ), + ); + }); + + _appReviewsUpdateSubscription = _appReviewsRepository.entityUpdated.listen(( + _, + ) { + _logger.info('AppReview updated, reloading app reviews list.'); + add( + LoadAppReviewsRequested( + filter: buildAppReviewsFilterMap( + _communityFilterBloc.state.appReviewsFilter, + ), + forceRefresh: true, + ), + ); + }); } final DataRepository _engagementsRepository; @@ -107,12 +106,16 @@ class CommunityManagementBloc final Logger _logger; late final StreamSubscription _filterSubscription; - late final StreamSubscription _entityUpdateSubscription; + late final StreamSubscription _engagementsUpdateSubscription; + late final StreamSubscription _reportsUpdateSubscription; + late final StreamSubscription _appReviewsUpdateSubscription; @override Future close() { _filterSubscription.cancel(); - _entityUpdateSubscription.cancel(); + _engagementsUpdateSubscription.cancel(); + _reportsUpdateSubscription.cancel(); + _appReviewsUpdateSubscription.cancel(); return super.close(); } diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index 1c8210a4..89fef8b0 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -21,9 +21,9 @@ class _CommunityFilterDialogState extends State { @override void initState() { - super.initState(); _searchController = TextEditingController(); _loadInitialFilterState(); + super.initState(); } void _loadInitialFilterState() { @@ -297,10 +297,10 @@ class _CommunityFilterDialogState extends State { allValues: ReportableEntity.values, selectedValues: state.reportsFilter.selectedReportableEntity, labelBuilder: (item) => item.l10n(context), - onChanged: (items) { + onChanged: (items) => { context.read().add( CommunityFilterDialogReportableEntityChanged(items), - ); + ), }, ), ]; @@ -311,10 +311,10 @@ class _CommunityFilterDialogState extends State { allValues: AppReviewFeedback.values, selectedValues: state.appReviewsFilter.selectedFeedback, labelBuilder: (item) => item.l10n(context), - onChanged: (items) { + onChanged: (items) => { context.read().add( CommunityFilterDialogAppReviewsFeedbackChanged(items), - ); + ), }, ), ]; From 67a493e72f76f6cfab6918c569ab21013f8713f3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 14:28:09 +0100 Subject: [PATCH 102/130] chore: delete absolete file --- .../bloc/community_filter_dialog_bloc.dart | 144 ------------------ .../bloc/community_filter_dialog_event.dart | 85 ----------- .../bloc/community_filter_dialog_state.dart | 36 ----- 3 files changed, 265 deletions(-) delete mode 100644 lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart delete mode 100644 lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart delete mode 100644 lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart deleted file mode 100644 index 93221770..00000000 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:core/core.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; - -part 'community_filter_dialog_event.dart'; -part 'community_filter_dialog_state.dart'; - -class CommunityFilterDialogBloc - extends Bloc { - CommunityFilterDialogBloc() - : super( - const CommunityFilterDialogState( - activeTab: CommunityManagementTab.engagements, - ), - ) { - on(_onFilterDialogInitialized); - on(_onSearchQueryChanged); - on( - _onEngagementsStatusChanged, - ); - on(_onReportsStatusChanged); - on( - _onReportableEntityChanged, - ); - on( - _onAppReviewsFeedbackChanged, - ); - on(_onHasCommentChanged); - on(_onFilterDialogReset); - } - - void _onFilterDialogInitialized( - CommunityFilterDialogInitialized event, - Emitter emit, - ) { - emit( - CommunityFilterDialogState( - activeTab: event.activeTab, - engagementsFilter: event.communityFilterState.engagementsFilter, - reportsFilter: event.communityFilterState.reportsFilter, - appReviewsFilter: event.communityFilterState.appReviewsFilter, - ), - ); - } - - void _onSearchQueryChanged( - CommunityFilterDialogSearchQueryChanged event, - Emitter emit, - ) { - switch (state.activeTab) { - case CommunityManagementTab.engagements: - emit( - state.copyWith( - engagementsFilter: EngagementsFilterState( - searchQuery: event.query, - selectedStatus: state.engagementsFilter.selectedStatus, - hasComment: state.engagementsFilter.hasComment, - ), - ), - ); - case CommunityManagementTab.reports: - emit( - state.copyWith( - reportsFilter: ReportsFilterState( - searchQuery: event.query, - selectedStatus: state.reportsFilter.selectedStatus, - selectedReportableEntity: - state.reportsFilter.selectedReportableEntity, - ), - ), - ); - case CommunityManagementTab.appReviews: - emit( - state.copyWith( - appReviewsFilter: AppReviewsFilterState( - searchQuery: event.query, - selectedFeedback: state.appReviewsFilter.selectedFeedback, - ), - ), - ); - } - } - - void _onEngagementsStatusChanged( - CommunityFilterDialogEngagementsStatusChanged event, - Emitter emit, - ) { - final newFilter = state.engagementsFilter.copyWith( - selectedStatus: event.status, - ); - emit(state.copyWith(engagementsFilter: newFilter)); - } - - void _onReportsStatusChanged( - CommunityFilterDialogReportsStatusChanged event, - Emitter emit, - ) { - final newFilter = state.reportsFilter.copyWith( - selectedStatus: event.status, - ); - emit(state.copyWith(reportsFilter: newFilter)); - } - - void _onReportableEntityChanged( - CommunityFilterDialogReportableEntityChanged event, - Emitter emit, - ) { - final newFilter = state.reportsFilter.copyWith( - selectedReportableEntity: event.reportableEntity, - ); - emit(state.copyWith(reportsFilter: newFilter)); - } - - void _onAppReviewsFeedbackChanged( - CommunityFilterDialogAppReviewsFeedbackChanged event, - Emitter emit, - ) { - final newFilter = state.appReviewsFilter.copyWith( - selectedFeedback: event.feedback, - ); - emit(state.copyWith(appReviewsFilter: newFilter)); - } - - void _onHasCommentChanged( - CommunityFilterDialogHasCommentChanged event, - Emitter emit, - ) { - final newFilter = state.engagementsFilter.copyWith( - hasComment: event.hasComment, - ); - emit(state.copyWith(engagementsFilter: newFilter)); - } - - void _onFilterDialogReset( - CommunityFilterDialogReset event, - Emitter emit, - ) { - emit( - CommunityFilterDialogState(activeTab: state.activeTab), - ); - } -} diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart deleted file mode 100644 index 26e0e14c..00000000 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_event.dart +++ /dev/null @@ -1,85 +0,0 @@ -part of 'community_filter_dialog_bloc.dart'; - -abstract class CommunityFilterDialogEvent extends Equatable { - const CommunityFilterDialogEvent(); - - @override - List get props => []; -} - -class CommunityFilterDialogInitialized extends CommunityFilterDialogEvent { - const CommunityFilterDialogInitialized({ - required this.activeTab, - required this.communityFilterState, - }); - - final CommunityManagementTab activeTab; - final CommunityFilterState communityFilterState; - - @override - List get props => [activeTab, communityFilterState]; -} - -class CommunityFilterDialogSearchQueryChanged - extends CommunityFilterDialogEvent { - const CommunityFilterDialogSearchQueryChanged(this.query); - - final String query; - - @override - List get props => [query]; -} - -class CommunityFilterDialogEngagementsStatusChanged - extends CommunityFilterDialogEvent { - const CommunityFilterDialogEngagementsStatusChanged(this.status); - - final List status; - - @override - List get props => [status]; -} - -class CommunityFilterDialogReportsStatusChanged - extends CommunityFilterDialogEvent { - const CommunityFilterDialogReportsStatusChanged(this.status); - - final List status; - - @override - List get props => [status]; -} - -class CommunityFilterDialogReportableEntityChanged - extends CommunityFilterDialogEvent { - const CommunityFilterDialogReportableEntityChanged(this.reportableEntity); - - final List reportableEntity; - - @override - List get props => [reportableEntity]; -} - -class CommunityFilterDialogAppReviewsFeedbackChanged - extends CommunityFilterDialogEvent { - const CommunityFilterDialogAppReviewsFeedbackChanged(this.feedback); - - final List feedback; - - @override - List get props => [feedback]; -} - -class CommunityFilterDialogHasCommentChanged - extends CommunityFilterDialogEvent { - const CommunityFilterDialogHasCommentChanged(this.hasComment); - - final HasCommentFilter hasComment; - - @override - List get props => [hasComment]; -} - -class CommunityFilterDialogReset extends CommunityFilterDialogEvent { - const CommunityFilterDialogReset(); -} diff --git a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart b/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart deleted file mode 100644 index 87259f11..00000000 --- a/lib/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_state.dart +++ /dev/null @@ -1,36 +0,0 @@ -part of 'community_filter_dialog_bloc.dart'; - -class CommunityFilterDialogState extends Equatable { - const CommunityFilterDialogState({ - required this.activeTab, - this.engagementsFilter = const EngagementsFilterState(), - this.reportsFilter = const ReportsFilterState(), - this.appReviewsFilter = const AppReviewsFilterState(), - }); - - final CommunityManagementTab activeTab; - final EngagementsFilterState engagementsFilter; - final ReportsFilterState reportsFilter; - final AppReviewsFilterState appReviewsFilter; - - CommunityFilterDialogState copyWith({ - EngagementsFilterState? engagementsFilter, - ReportsFilterState? reportsFilter, - AppReviewsFilterState? appReviewsFilter, - }) { - return CommunityFilterDialogState( - activeTab: activeTab, - engagementsFilter: engagementsFilter ?? this.engagementsFilter, - reportsFilter: reportsFilter ?? this.reportsFilter, - appReviewsFilter: appReviewsFilter ?? this.appReviewsFilter, - ); - } - - @override - List get props => [ - activeTab, - engagementsFilter, - reportsFilter, - appReviewsFilter, - ]; -} From 6219e7201c29d0949d06d45b90a48419d3c83e61 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 14:28:58 +0100 Subject: [PATCH 103/130] refactor(community_management): simplify community filterBloc - Remove unnecessary EngagementsFilterState, ReportsFilterState, and AppReviewsFilterState classes - Merge CommunityFilterDialogBloc into CommunityFilterBloc - Remove unused hasComment filter for engagements - Simplify search query handling and tab-specific filters - Update UI to reflect new filter structure --- .../community_filter_bloc.dart | 44 ++-- .../community_filter_event.dart | 45 ++-- .../community_filter_state.dart | 136 +++--------- .../community_filter_dialog.dart | 195 +++++------------- 4 files changed, 140 insertions(+), 280 deletions(-) diff --git a/lib/community_management/bloc/community_filter/community_filter_bloc.dart b/lib/community_management/bloc/community_filter/community_filter_bloc.dart index 0b00b5e3..a50bfa8c 100644 --- a/lib/community_management/bloc/community_filter/community_filter_bloc.dart +++ b/lib/community_management/bloc/community_filter/community_filter_bloc.dart @@ -8,39 +8,53 @@ part 'community_filter_state.dart'; class CommunityFilterBloc extends Bloc { CommunityFilterBloc() : super(const CommunityFilterState()) { - on(_onEngagementsFilterChanged); - on(_onReportsFilterChanged); - on(_onAppReviewsFilterChanged); + on(_onSearchQueryChanged); + on(_onModerationStatusChanged); + on(_onReportableEntityChanged); + on(_onAppReviewFeedbackChanged); on(_onFilterReset); + on(_onFilterApplied); } - void _onEngagementsFilterChanged( - EngagementsFilterChanged event, + void _onSearchQueryChanged( + CommunityFilterSearchQueryChanged event, Emitter emit, ) { - emit(state.copyWith(engagementsFilter: event.filterState)); + emit(state.copyWith(searchQuery: event.query)); } - void _onReportsFilterChanged( - ReportsFilterChanged event, + void _onModerationStatusChanged( + CommunityFilterModerationStatusChanged event, Emitter emit, ) { - emit(state.copyWith(reportsFilter: event.filterState)); + emit(state.copyWith(selectedModerationStatus: event.moderationStatus)); } - void _onAppReviewsFilterChanged( - AppReviewsFilterChanged event, + void _onReportableEntityChanged( + CommunityFilterReportableEntityChanged event, Emitter emit, ) { - emit(state.copyWith(appReviewsFilter: event.filterState)); + emit(state.copyWith(selectedReportableEntity: event.reportableEntity)); + } + + void _onAppReviewFeedbackChanged( + CommunityFilterAppReviewFeedbackChanged event, + Emitter emit, + ) { + emit(state.copyWith(selectedAppReviewFeedback: event.appReviewFeedback)); } void _onFilterReset( CommunityFilterReset event, Emitter emit, ) { - emit( - const CommunityFilterState(), - ); + emit(const CommunityFilterState()); + } + + void _onFilterApplied( + CommunityFilterApplied event, + Emitter emit, + ) { + emit(state.copyWith(version: state.version + 1)); } } diff --git a/lib/community_management/bloc/community_filter/community_filter_event.dart b/lib/community_management/bloc/community_filter/community_filter_event.dart index 9a206a99..c1e841d6 100644 --- a/lib/community_management/bloc/community_filter/community_filter_event.dart +++ b/lib/community_management/bloc/community_filter/community_filter_event.dart @@ -4,28 +4,47 @@ abstract class CommunityFilterEvent extends Equatable { const CommunityFilterEvent(); @override - List get props => []; + List get props => []; } -class EngagementsFilterChanged extends CommunityFilterEvent { - const EngagementsFilterChanged(this.filterState); - final EngagementsFilterState filterState; +class CommunityFilterSearchQueryChanged extends CommunityFilterEvent { + const CommunityFilterSearchQueryChanged(this.query); + + final String query; + @override - List get props => [filterState]; + List get props => [query]; } -class ReportsFilterChanged extends CommunityFilterEvent { - const ReportsFilterChanged(this.filterState); - final ReportsFilterState filterState; +class CommunityFilterModerationStatusChanged extends CommunityFilterEvent { + const CommunityFilterModerationStatusChanged(this.moderationStatus); + + final List moderationStatus; + @override - List get props => [filterState]; + List get props => [moderationStatus]; } -class AppReviewsFilterChanged extends CommunityFilterEvent { - const AppReviewsFilterChanged(this.filterState); - final AppReviewsFilterState filterState; +class CommunityFilterReportableEntityChanged extends CommunityFilterEvent { + const CommunityFilterReportableEntityChanged(this.reportableEntity); + + final List reportableEntity; + @override - List get props => [filterState]; + List get props => [reportableEntity]; +} + +class CommunityFilterAppReviewFeedbackChanged extends CommunityFilterEvent { + const CommunityFilterAppReviewFeedbackChanged(this.appReviewFeedback); + + final List appReviewFeedback; + + @override + List get props => [appReviewFeedback]; +} + +class CommunityFilterApplied extends CommunityFilterEvent { + const CommunityFilterApplied(); } class CommunityFilterReset extends CommunityFilterEvent { diff --git a/lib/community_management/bloc/community_filter/community_filter_state.dart b/lib/community_management/bloc/community_filter/community_filter_state.dart index 7a39b5de..0e2b6ed3 100644 --- a/lib/community_management/bloc/community_filter/community_filter_state.dart +++ b/lib/community_management/bloc/community_filter/community_filter_state.dart @@ -1,130 +1,56 @@ part of 'community_filter_bloc.dart'; -enum HasCommentFilter { any, withComment, withoutComment } - class CommunityFilterState extends Equatable { const CommunityFilterState({ - this.engagementsFilter = const EngagementsFilterState(), - this.reportsFilter = const ReportsFilterState(), - this.appReviewsFilter = const AppReviewsFilterState(), - }); - - final EngagementsFilterState engagementsFilter; - final ReportsFilterState reportsFilter; - final AppReviewsFilterState appReviewsFilter; - - CommunityFilterState copyWith({ - EngagementsFilterState? engagementsFilter, - ReportsFilterState? reportsFilter, - AppReviewsFilterState? appReviewsFilter, - }) { - return CommunityFilterState( - engagementsFilter: engagementsFilter ?? this.engagementsFilter, - reportsFilter: reportsFilter ?? this.reportsFilter, - appReviewsFilter: appReviewsFilter ?? this.appReviewsFilter, - ); - } - - @override - List get props => [ - engagementsFilter, - reportsFilter, - appReviewsFilter, - ]; -} - -class EngagementsFilterState extends Equatable { - const EngagementsFilterState({ this.searchQuery = '', - this.selectedStatus = const [], - this.hasComment = HasCommentFilter.any, - }); - - final String searchQuery; - final List selectedStatus; - final HasCommentFilter hasComment; - - bool get isFilterActive => - searchQuery.isNotEmpty || - selectedStatus.isNotEmpty || - hasComment != HasCommentFilter.any; - - @override - List get props => [searchQuery, selectedStatus, hasComment]; - - EngagementsFilterState copyWith({ - String? searchQuery, - List? selectedStatus, - HasCommentFilter? hasComment, - }) { - return EngagementsFilterState( - searchQuery: searchQuery ?? this.searchQuery, - selectedStatus: selectedStatus ?? this.selectedStatus, - hasComment: hasComment ?? this.hasComment, - ); - } -} - - -class ReportsFilterState extends Equatable { - const ReportsFilterState({ - this.searchQuery = '', - this.selectedStatus = const [], + this.selectedModerationStatus = const [], this.selectedReportableEntity = const [], + this.selectedAppReviewFeedback = const [], + this.version = 0, }); final String searchQuery; - final List selectedStatus; + final List selectedModerationStatus; final List selectedReportableEntity; + final List selectedAppReviewFeedback; + final int version; + + bool get isEngagementsFilterActive => + searchQuery.isNotEmpty || selectedModerationStatus.isNotEmpty; - bool get isFilterActive => + bool get isReportsFilterActive => searchQuery.isNotEmpty || - selectedStatus.isNotEmpty || - selectedReportableEntity.isNotEmpty; + selectedModerationStatus.isNotEmpty || + selectedReportableEntity.isNotEmpty; - @override - List get props => [ - searchQuery, - selectedStatus, - selectedReportableEntity, - ]; + bool get isAppReviewsFilterActive => + searchQuery.isNotEmpty || selectedAppReviewFeedback.isNotEmpty; - ReportsFilterState copyWith({ + CommunityFilterState copyWith({ String? searchQuery, - List? selectedStatus, + List? selectedModerationStatus, List? selectedReportableEntity, + List? selectedAppReviewFeedback, + int? version, }) { - return ReportsFilterState( + return CommunityFilterState( searchQuery: searchQuery ?? this.searchQuery, - selectedStatus: selectedStatus ?? this.selectedStatus, + selectedModerationStatus: + selectedModerationStatus ?? this.selectedModerationStatus, selectedReportableEntity: selectedReportableEntity ?? this.selectedReportableEntity, + selectedAppReviewFeedback: + selectedAppReviewFeedback ?? this.selectedAppReviewFeedback, + version: version ?? this.version, ); } -} - -class AppReviewsFilterState extends Equatable { - const AppReviewsFilterState({ - this.searchQuery = '', - this.selectedFeedback = const [], - }); - - final String searchQuery; - final List selectedFeedback; - - bool get isFilterActive => - searchQuery.isNotEmpty || selectedFeedback.isNotEmpty; @override - List get props => [searchQuery, selectedFeedback]; - - AppReviewsFilterState copyWith({ - String? searchQuery, - List? selectedFeedback, - }) { - return AppReviewsFilterState( - searchQuery: searchQuery ?? this.searchQuery, - selectedFeedback: selectedFeedback ?? this.selectedFeedback, - ); - } + List get props => [ + searchQuery, + selectedModerationStatus, + selectedReportableEntity, + selectedAppReviewFeedback, + version, + ]; } diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index 89fef8b0..24eed601 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -2,8 +2,8 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart' + show CommunityManagementBloc, CommunityManagementTab; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; @@ -22,64 +22,33 @@ class _CommunityFilterDialogState extends State { @override void initState() { _searchController = TextEditingController(); - _loadInitialFilterState(); + _searchController.text = + context.read().state.searchQuery; super.initState(); } - void _loadInitialFilterState() { - final communityManagementBloc = context.read(); - final communityFilterBloc = context.read(); - context.read().add( - CommunityFilterDialogInitialized( - activeTab: communityManagementBloc.state.activeTab, - communityFilterState: communityFilterBloc.state, - ), - ); - } - @override void dispose() { _searchController.dispose(); super.dispose(); } - void _dispatchFilterApplied(CommunityFilterDialogState filterDialogState) { - context.read() - ..add(EngagementsFilterChanged(filterDialogState.engagementsFilter)) - ..add(ReportsFilterChanged(filterDialogState.reportsFilter)) - ..add( - AppReviewsFilterChanged(filterDialogState.appReviewsFilter), - ); - } - @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; final theme = Theme.of(context); + final activeTab = context.select((bloc) => bloc.state.activeTab); - return BlocBuilder( - builder: (context, filterDialogState) { - final String currentSearchQuery; - switch (filterDialogState.activeTab) { - case CommunityManagementTab.engagements: - currentSearchQuery = - filterDialogState.engagementsFilter.searchQuery; - case CommunityManagementTab.reports: - currentSearchQuery = filterDialogState.reportsFilter.searchQuery; - case CommunityManagementTab.appReviews: - currentSearchQuery = filterDialogState.appReviewsFilter.searchQuery; - } - - if (_searchController.text != currentSearchQuery) { - _searchController.text = currentSearchQuery; - } + return BlocBuilder( + builder: (context, filterState) { _searchController.selection = TextSelection.fromPosition( TextPosition(offset: _searchController.text.length), ); return Scaffold( appBar: AppBar( - title: Text(_getDialogTitle(l10n, filterDialogState.activeTab)), + title: Text(l10n.filterCommunity), leading: IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), @@ -89,12 +58,12 @@ class _CommunityFilterDialogState extends State { icon: const Icon(Icons.refresh), tooltip: l10n.resetFiltersButtonText, onPressed: () { - context.read().add( - const CommunityFilterDialogReset(), - ); - _dispatchFilterApplied( - context.read().state, - ); + context + .read() + .add(const CommunityFilterReset()); + context + .read() + .add(const CommunityFilterApplied()); Navigator.of(context).pop(); }, ), @@ -102,7 +71,9 @@ class _CommunityFilterDialogState extends State { icon: const Icon(Icons.check), tooltip: l10n.applyFilters, onPressed: () { - _dispatchFilterApplied(filterDialogState); + context + .read() + .add(const CommunityFilterApplied()); Navigator.of(context).pop(); }, ), @@ -117,22 +88,24 @@ class _CommunityFilterDialogState extends State { TextField( controller: _searchController, decoration: InputDecoration( - labelText: l10n.search, - hintText: _getSearchHint( - l10n, - filterDialogState.activeTab, - ), + labelText: l10n.searchByUserId, + hintText: l10n.searchByUserId, prefixIcon: const Icon(Icons.search), border: const OutlineInputBorder(), ), onChanged: (query) { - context.read().add( - CommunityFilterDialogSearchQueryChanged(query), - ); + context.read().add( + CommunityFilterSearchQueryChanged(query), + ); }, ), const SizedBox(height: AppSpacing.lg), - ..._buildTabSpecificFilters(filterDialogState, l10n, theme), + ..._buildTabSpecificFilters( + activeTab, + filterState, + l10n, + theme, + ), ], ), ), @@ -142,34 +115,6 @@ class _CommunityFilterDialogState extends State { ); } - String _getDialogTitle( - AppLocalizations l10n, - CommunityManagementTab activeTab, - ) { - switch (activeTab) { - case CommunityManagementTab.engagements: - return l10n.filterCommunity; - case CommunityManagementTab.reports: - return l10n.filterCommunity; - case CommunityManagementTab.appReviews: - return l10n.filterCommunity; - } - } - - String _getSearchHint( - AppLocalizations l10n, - CommunityManagementTab activeTab, - ) { - switch (activeTab) { - case CommunityManagementTab.engagements: - return l10n.searchByEngagementUser; - case CommunityManagementTab.reports: - return l10n.searchByReportReporter; - case CommunityManagementTab.appReviews: - return l10n.searchByAppReviewUser; - } - } - Widget _buildCapsuleFilter({ required String title, required List allValues, @@ -221,87 +166,45 @@ class _CommunityFilterDialogState extends State { ); } - Widget _buildHasCommentFilter( - CommunityFilterDialogState state, - AppLocalizations l10n, - ThemeData theme, - ) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.hasComment, - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: AppSpacing.sm), - Wrap( - spacing: AppSpacing.sm, - children: HasCommentFilter.values.map((filter) { - return ChoiceChip( - label: Text( - switch (filter) { - HasCommentFilter.any => l10n.any, - HasCommentFilter.withComment => l10n.withComment, - HasCommentFilter.withoutComment => l10n.withoutComment, - }, - ), - selected: state.engagementsFilter.hasComment == filter, - onSelected: (selected) { - if (selected) { - context.read().add( - CommunityFilterDialogHasCommentChanged(filter), - ); - } - }, - ); - }).toList(), - ), - ], - ); - } - List _buildTabSpecificFilters( - CommunityFilterDialogState state, + CommunityManagementTab activeTab, + CommunityFilterState state, AppLocalizations l10n, ThemeData theme, ) { - switch (state.activeTab) { + switch (activeTab) { case CommunityManagementTab.engagements: return [ _buildCapsuleFilter( title: l10n.status, allValues: ModerationStatus.values, - selectedValues: state.engagementsFilter.selectedStatus, + selectedValues: state.selectedModerationStatus, labelBuilder: (item) => item.l10n(context), - onChanged: (items) => context.read().add( - CommunityFilterDialogEngagementsStatusChanged(items), - ), + onChanged: (items) => context + .read() + .add(CommunityFilterModerationStatusChanged(items)), ), - const SizedBox(height: AppSpacing.lg), - _buildHasCommentFilter(state, l10n, theme), ]; case CommunityManagementTab.reports: return [ _buildCapsuleFilter( title: l10n.status, allValues: ModerationStatus.values, - selectedValues: state.reportsFilter.selectedStatus, + selectedValues: state.selectedModerationStatus, labelBuilder: (item) => item.l10n(context), - onChanged: (items) => context.read().add( - CommunityFilterDialogReportsStatusChanged(items), - ), + onChanged: (items) => context + .read() + .add(CommunityFilterModerationStatusChanged(items)), ), const SizedBox(height: AppSpacing.lg), _buildCapsuleFilter( title: l10n.reportedItem, allValues: ReportableEntity.values, - selectedValues: state.reportsFilter.selectedReportableEntity, + selectedValues: state.selectedReportableEntity, labelBuilder: (item) => item.l10n(context), - onChanged: (items) => { - context.read().add( - CommunityFilterDialogReportableEntityChanged(items), - ), - }, + onChanged: (items) => context + .read() + .add(CommunityFilterReportableEntityChanged(items)), ), ]; case CommunityManagementTab.appReviews: @@ -309,13 +212,11 @@ class _CommunityFilterDialogState extends State { _buildCapsuleFilter( title: l10n.initialFeedback, allValues: AppReviewFeedback.values, - selectedValues: state.appReviewsFilter.selectedFeedback, + selectedValues: state.selectedAppReviewFeedback, labelBuilder: (item) => item.l10n(context), - onChanged: (items) => { - context.read().add( - CommunityFilterDialogAppReviewsFeedbackChanged(items), - ), - }, + onChanged: (items) => context + .read() + .add(CommunityFilterAppReviewFeedbackChanged(items)), ), ]; } From a7a347a5d46ee76fa93dd726efdbb369415dab80 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 14:29:16 +0100 Subject: [PATCH 104/130] refactor(community_management): improve filter handling and update logic - Remove unnecessary filter subscription in CommunityManagementBloc - Simplify filter map building methods - Implement filter change handling in CommunityManagementPage using BlocListener - Update filter building logic to use CommunityFilterState directly --- .../bloc/community_management_bloc.dart | 104 ++++----------- .../view/community_management_page.dart | 120 ++++++++++-------- 2 files changed, 98 insertions(+), 126 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index a3ea2fbb..a2d34601 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -18,81 +18,44 @@ class CommunityManagementBloc required DataRepository appReviewsRepository, required CommunityFilterBloc communityFilterBloc, Logger? logger, - }) : _engagementsRepository = engagementsRepository, - _reportsRepository = reportsRepository, - _appReviewsRepository = appReviewsRepository, - _communityFilterBloc = communityFilterBloc, - _logger = logger ?? Logger('CommunityManagementBloc'), - super(const CommunityManagementState()) { + }) : _engagementsRepository = engagementsRepository, + _reportsRepository = reportsRepository, + _appReviewsRepository = appReviewsRepository, + _communityFilterBloc = communityFilterBloc, + _logger = logger ?? Logger('CommunityManagementBloc'), + super(const CommunityManagementState()) { on(_onTabChanged); on(_onLoadEngagementsRequested); on(_onLoadReportsRequested); on(_onLoadAppReviewsRequested); - _filterSubscription = _communityFilterBloc.stream.listen((filterState) { - switch (state.activeTab) { - case CommunityManagementTab.engagements: - if (filterState.engagementsFilter != - _communityFilterBloc.state.engagementsFilter) { - add( - LoadEngagementsRequested( - filter: buildEngagementsFilterMap( - filterState.engagementsFilter, - ), - forceRefresh: true, - ), - ); - } - case CommunityManagementTab.reports: - if (filterState.reportsFilter != - _communityFilterBloc.state.reportsFilter) { - add( - LoadReportsRequested( - filter: buildReportsFilterMap(filterState.reportsFilter), - forceRefresh: true, - ), - ); - } - case CommunityManagementTab.appReviews: - // No filter changes to listen to for app reviews yet. - break; - } + _engagementsUpdateSubscription = + _engagementsRepository.entityUpdated.listen((_) { + _logger.info('Engagement updated, reloading engagements list.'); + add( + LoadEngagementsRequested( + filter: buildEngagementsFilterMap(_communityFilterBloc.state), + forceRefresh: true, + ), + ); }); - _engagementsUpdateSubscription = _engagementsRepository.entityUpdated - .listen((_) { - _logger.info('Engagement updated, reloading engagements list.'); - add( - LoadEngagementsRequested( - filter: buildEngagementsFilterMap( - _communityFilterBloc.state.engagementsFilter, - ), - forceRefresh: true, - ), - ); - }); - _reportsUpdateSubscription = _reportsRepository.entityUpdated.listen((_) { _logger.info('Report updated, reloading reports list.'); add( LoadReportsRequested( - filter: buildReportsFilterMap( - _communityFilterBloc.state.reportsFilter, - ), + filter: buildReportsFilterMap(_communityFilterBloc.state), forceRefresh: true, ), ); }); - _appReviewsUpdateSubscription = _appReviewsRepository.entityUpdated.listen(( - _, - ) { + _appReviewsUpdateSubscription = + _appReviewsRepository.entityUpdated.listen((_) { _logger.info('AppReview updated, reloading app reviews list.'); add( LoadAppReviewsRequested( - filter: buildAppReviewsFilterMap( - _communityFilterBloc.state.appReviewsFilter, - ), + filter: buildAppReviewsFilterMap(_communityFilterBloc.state), forceRefresh: true, ), ); @@ -105,50 +68,39 @@ class CommunityManagementBloc final CommunityFilterBloc _communityFilterBloc; final Logger _logger; - late final StreamSubscription _filterSubscription; late final StreamSubscription _engagementsUpdateSubscription; late final StreamSubscription _reportsUpdateSubscription; late final StreamSubscription _appReviewsUpdateSubscription; @override Future close() { - _filterSubscription.cancel(); _engagementsUpdateSubscription.cancel(); _reportsUpdateSubscription.cancel(); _appReviewsUpdateSubscription.cancel(); return super.close(); } - Map buildEngagementsFilterMap(EngagementsFilterState state) { + Map buildEngagementsFilterMap(CommunityFilterState state) { final filter = {}; if (state.searchQuery.isNotEmpty) { filter['userId'] = state.searchQuery; } - if (state.selectedStatus.isNotEmpty) { + if (state.selectedModerationStatus.isNotEmpty) { filter['comment.status'] = { - r'$in': state.selectedStatus.map((s) => s.name).toList(), - }; - } - if (state.hasComment == HasCommentFilter.withComment) { - filter['comment'] = { - r'$exists': true, - }; - } else if (state.hasComment == HasCommentFilter.withoutComment) { - filter['comment'] = { - r'$exists': false, + r'$in': state.selectedModerationStatus.map((s) => s.name).toList(), }; } return filter; } - Map buildReportsFilterMap(ReportsFilterState state) { + Map buildReportsFilterMap(CommunityFilterState state) { final filter = {}; if (state.searchQuery.isNotEmpty) { filter['reporterUserId'] = state.searchQuery; } - if (state.selectedStatus.isNotEmpty) { + if (state.selectedModerationStatus.isNotEmpty) { filter['status'] = { - r'$in': state.selectedStatus.map((s) => s.name).toList(), + r'$in': state.selectedModerationStatus.map((s) => s.name).toList(), }; } if (state.selectedReportableEntity.isNotEmpty) { @@ -159,14 +111,14 @@ class CommunityManagementBloc return filter; } - Map buildAppReviewsFilterMap(AppReviewsFilterState state) { + Map buildAppReviewsFilterMap(CommunityFilterState state) { final filter = {}; if (state.searchQuery.isNotEmpty) { filter['userId'] = state.searchQuery; } - if (state.selectedFeedback.isNotEmpty) { + if (state.selectedAppReviewFeedback.isNotEmpty) { filter['feedback'] = { - r'$in': state.selectedFeedback.map((f) => f.name).toList(), + r'$in': state.selectedAppReviewFeedback.map((f) => f.name).toList(), }; } return filter; diff --git a/lib/community_management/view/community_management_page.dart b/lib/community_management/view/community_management_page.dart index 9c2456dd..cd183bb1 100644 --- a/lib/community_management/view/community_management_page.dart +++ b/lib/community_management/view/community_management_page.dart @@ -1,7 +1,6 @@ -import 'package:core/core.dart'; -import 'package:data_repository/data_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/view/app_reviews_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/view/engagements_page.dart'; @@ -51,61 +50,82 @@ class _CommunityManagementPageState extends State @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; - return Scaffold( - appBar: AppBar( - title: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(l10n.communityManagement), - const SizedBox(width: AppSpacing.xs), - AboutIcon( - dialogTitle: l10n.communityManagement, - dialogDescription: l10n.communityManagementPageDescription, + return BlocListener( + listenWhen: (previous, current) => previous.version != current.version, + listener: (context, filterState) { + final communityManagementBloc = context.read(); + switch (communityManagementBloc.state.activeTab) { + case CommunityManagementTab.engagements: + communityManagementBloc.add( + LoadEngagementsRequested( + filter: communityManagementBloc.buildEngagementsFilterMap( + filterState, + ), + forceRefresh: true, + ), + ); + case CommunityManagementTab.reports: + communityManagementBloc.add( + LoadReportsRequested( + filter: communityManagementBloc.buildReportsFilterMap( + filterState, + ), + forceRefresh: true, + ), + ); + case CommunityManagementTab.appReviews: + communityManagementBloc.add( + LoadAppReviewsRequested( + filter: communityManagementBloc.buildAppReviewsFilterMap( + filterState, + ), + forceRefresh: true, + ), + ); + } + }, + child: Scaffold( + appBar: AppBar( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l10n.communityManagement), + const SizedBox(width: AppSpacing.xs), + AboutIcon( + dialogTitle: l10n.communityManagement, + dialogDescription: l10n.communityManagementPageDescription, + ), + ], + ), + actions: [ + IconButton( + icon: const Icon(Icons.filter_list), + tooltip: l10n.filterCommunity, + onPressed: () { + context.pushNamed(Routes.communityFilterDialogName); + }, ), ], - ), - actions: [ - IconButton( - icon: const Icon(Icons.filter_list), - tooltip: l10n.filterCommunity, - onPressed: () { - final communityManagementBloc = context - .read(); - final engagementsRepository = context - .read>(); - final reportsRepository = context.read>(); - final appReviewsRepository = context - .read>(); - - final arguments = { - 'activeTab': communityManagementBloc.state.activeTab, - 'engagementsRepository': engagementsRepository, - 'reportsRepository': reportsRepository, - 'appReviewsRepository': appReviewsRepository, - }; - - context.pushNamed( - Routes.communityFilterDialogName, - extra: arguments, - ); - }, + bottom: TabBar( + controller: _tabController, + tabAlignment: TabAlignment.start, + isScrollable: true, + tabs: [ + Tab(text: l10n.engagements), + Tab(text: l10n.reports), + Tab(text: l10n.appReviews), + ], ), - ], - bottom: TabBar( + ), + body: TabBarView( controller: _tabController, - tabAlignment: TabAlignment.start, - isScrollable: true, - tabs: [ - Tab(text: l10n.engagements), - Tab(text: l10n.reports), - Tab(text: l10n.appReviews), + children: const [ + EngagementsPage(), + ReportsPage(), + AppReviewsPage(), ], ), ), - body: TabBarView( - controller: _tabController, - children: const [EngagementsPage(), ReportsPage(), AppReviewsPage()], - ), ); } } From 756cf1f2e9dccc9639db2257816419871d133301 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 14:29:46 +0100 Subject: [PATCH 105/130] refactor(community_management): improve filter handling in multiple pages - Update filter passing logic in AppReviewsPage, EngagementsPage, and ReportsPage - Refactor filter activity checks to improve readability and maintainability - Remove redundant filter objects from CommunityFilterState - Update related methods in CommunityManagementBloc to accommodate new filter structure --- .../view/app_reviews_page.dart | 13 +++++----- .../view/engagements_page.dart | 20 +++++++--------- .../view/reports_page.dart | 24 +++++++++---------- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/lib/community_management/view/app_reviews_page.dart b/lib/community_management/view/app_reviews_page.dart index 96c10da6..5d1d440a 100644 --- a/lib/community_management/view/app_reviews_page.dart +++ b/lib/community_management/view/app_reviews_page.dart @@ -28,7 +28,7 @@ class _AppReviewsPageState extends State { filter: context .read() .buildAppReviewsFilterMap( - context.read().state.appReviewsFilter, + context.read().state, ), ), ); @@ -41,11 +41,10 @@ class _AppReviewsPageState extends State { padding: const EdgeInsets.only(top: AppSpacing.sm), child: BlocBuilder( builder: (context, state) { - final communityFilterState = context + final filtersActive = context .watch() - .state; - final filtersActive = - communityFilterState.appReviewsFilter.isFilterActive; + .state + .isAppReviewsFilterActive; if (state.appReviewsStatus == CommunityManagementStatus.loading && state.appReviews.isEmpty) { @@ -66,7 +65,7 @@ class _AppReviewsPageState extends State { filter: context .read() .buildAppReviewsFilterMap( - context.read().state.appReviewsFilter, + context.read().state, ), ), ), @@ -144,7 +143,7 @@ class _AppReviewsPageState extends State { filter: context .read() .buildAppReviewsFilterMap( - context.read().state.appReviewsFilter, + context.read().state, ), ), ); diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index 8f6c48ca..b44b2480 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -27,12 +27,17 @@ class _EngagementsPageState extends State { filter: context .read() .buildEngagementsFilterMap( - context.read().state.engagementsFilter, + context.read().state, ), ), ); } + bool _areFiltersActive(CommunityFilterState state) { + return state.searchQuery.isNotEmpty || + state.selectedModerationStatus.isNotEmpty; + } + @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -43,8 +48,7 @@ class _EngagementsPageState extends State { final communityFilterState = context .watch() .state; - final filtersActive = - communityFilterState.engagementsFilter.isFilterActive; + final filtersActive = _areFiltersActive(communityFilterState); if (state.engagementsStatus == CommunityManagementStatus.loading && state.engagements.isEmpty) { @@ -65,10 +69,7 @@ class _EngagementsPageState extends State { filter: context .read() .buildEngagementsFilterMap( - context - .read() - .state - .engagementsFilter, + context.read().state, ), ), ), @@ -152,10 +153,7 @@ class _EngagementsPageState extends State { filter: context .read() .buildEngagementsFilterMap( - context - .read() - .state - .engagementsFilter, + context.read().state, ), ), ); diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index 46b69153..7c4db97a 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -25,12 +25,18 @@ class _ReportsPageState extends State { LoadReportsRequested( limit: kDefaultRowsPerPage, filter: context.read().buildReportsFilterMap( - context.read().state.reportsFilter, + context.read().state, ), ), ); } + bool _areFiltersActive(CommunityFilterState state) { + return state.searchQuery.isNotEmpty || + state.selectedModerationStatus.isNotEmpty || + state.selectedReportableEntity.isNotEmpty; + } + @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -41,8 +47,7 @@ class _ReportsPageState extends State { final communityFilterState = context .watch() .state; - final filtersActive = - communityFilterState.reportsFilter.isFilterActive; + final filtersActive = _areFiltersActive(communityFilterState); if (state.reportsStatus == CommunityManagementStatus.loading && state.reports.isEmpty) { @@ -61,9 +66,8 @@ class _ReportsPageState extends State { limit: kDefaultRowsPerPage, forceRefresh: true, filter: context - .read() - .buildReportsFilterMap( - context.read().state.reportsFilter, + .read().buildReportsFilterMap( + context.read().state, ), ), ), @@ -145,12 +149,8 @@ class _ReportsPageState extends State { limit: kDefaultRowsPerPage, filter: context .read() - .buildReportsFilterMap( - context - .read() - .state - .reportsFilter, - ), + .buildReportsFilterMap(context + .read().state), ), ); } From b4894c373e2429bb232bc7f8cdfcffc49748c5d0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 14:31:18 +0100 Subject: [PATCH 106/130] refactor(router): remove bloc from community filter dialog - Remove BlocProvider and related community filter bloc imports - Simplify community filter dialog page builder - Remove activeTab argument passing --- lib/router/router.dart | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/lib/router/router.dart b/lib/router/router.dart index 779363bb..89d622ac 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -12,10 +12,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/b import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/view/authentication_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/view/email_code_verification_page.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/view/request_code_page.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/view/community_management_page.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_filter_dialog/bloc/community_filter_dialog_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/community_filter_dialog/community_filter_dialog.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/content_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/headlines_filter/headlines_filter_bloc.dart'; @@ -391,24 +388,9 @@ GoRouter createRouter({ path: Routes.communityFilterDialog, name: Routes.communityFilterDialogName, pageBuilder: (context, state) { - final args = state.extra! as Map; - final activeTab = - args['activeTab'] as CommunityManagementTab; - - return MaterialPage( + return const MaterialPage( fullscreenDialog: true, - child: BlocProvider( - create: (providerContext) => - CommunityFilterDialogBloc()..add( - CommunityFilterDialogInitialized( - activeTab: activeTab, - communityFilterState: providerContext - .read() - .state, - ), - ), - child: const CommunityFilterDialog(), - ), + child: CommunityFilterDialog(), ); }, ), From 666b376d11024a3667d2c628781ad1ea718b2299 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 15:16:01 +0100 Subject: [PATCH 107/130] refactor(community_management): restructure community filter functionality - Replace individual filter events with separate event classes for engagements, reports, and app reviews - Update CommunityFilterState to use separate filter classes for engagements, reports, and app reviews - Simplify filter handling in CommunityFilterBloc --- .../community_filter_bloc.dart | 56 ++++----- .../community_filter_event.dart | 33 ++---- .../community_filter_state.dart | 108 ++++++++++++------ 3 files changed, 108 insertions(+), 89 deletions(-) diff --git a/lib/community_management/bloc/community_filter/community_filter_bloc.dart b/lib/community_management/bloc/community_filter/community_filter_bloc.dart index a50bfa8c..47ce4b33 100644 --- a/lib/community_management/bloc/community_filter/community_filter_bloc.dart +++ b/lib/community_management/bloc/community_filter/community_filter_bloc.dart @@ -8,42 +8,34 @@ part 'community_filter_state.dart'; class CommunityFilterBloc extends Bloc { CommunityFilterBloc() : super(const CommunityFilterState()) { - on(_onSearchQueryChanged); - on(_onModerationStatusChanged); - on(_onReportableEntityChanged); - on(_onAppReviewFeedbackChanged); + on( + (event, emit) => emit( + state.copyWith( + engagementsFilter: event.filter, + version: state.version, + ), + ), + ); + on( + (event, emit) => emit( + state.copyWith( + reportsFilter: event.filter, + version: state.version, + ), + ), + ); + on( + (event, emit) => emit( + state.copyWith( + appReviewsFilter: event.filter, + version: state.version, + ), + ), + ); on(_onFilterReset); on(_onFilterApplied); } - void _onSearchQueryChanged( - CommunityFilterSearchQueryChanged event, - Emitter emit, - ) { - emit(state.copyWith(searchQuery: event.query)); - } - - void _onModerationStatusChanged( - CommunityFilterModerationStatusChanged event, - Emitter emit, - ) { - emit(state.copyWith(selectedModerationStatus: event.moderationStatus)); - } - - void _onReportableEntityChanged( - CommunityFilterReportableEntityChanged event, - Emitter emit, - ) { - emit(state.copyWith(selectedReportableEntity: event.reportableEntity)); - } - - void _onAppReviewFeedbackChanged( - CommunityFilterAppReviewFeedbackChanged event, - Emitter emit, - ) { - emit(state.copyWith(selectedAppReviewFeedback: event.appReviewFeedback)); - } - void _onFilterReset( CommunityFilterReset event, Emitter emit, diff --git a/lib/community_management/bloc/community_filter/community_filter_event.dart b/lib/community_management/bloc/community_filter/community_filter_event.dart index c1e841d6..2e661254 100644 --- a/lib/community_management/bloc/community_filter/community_filter_event.dart +++ b/lib/community_management/bloc/community_filter/community_filter_event.dart @@ -7,40 +7,31 @@ abstract class CommunityFilterEvent extends Equatable { List get props => []; } -class CommunityFilterSearchQueryChanged extends CommunityFilterEvent { - const CommunityFilterSearchQueryChanged(this.query); +class EngagementsFilterChanged extends CommunityFilterEvent { + const EngagementsFilterChanged(this.filter); - final String query; + final EngagementsFilter filter; @override - List get props => [query]; + List get props => [filter]; } -class CommunityFilterModerationStatusChanged extends CommunityFilterEvent { - const CommunityFilterModerationStatusChanged(this.moderationStatus); +class ReportsFilterChanged extends CommunityFilterEvent { + const ReportsFilterChanged(this.filter); - final List moderationStatus; + final ReportsFilter filter; @override - List get props => [moderationStatus]; + List get props => [filter]; } -class CommunityFilterReportableEntityChanged extends CommunityFilterEvent { - const CommunityFilterReportableEntityChanged(this.reportableEntity); +class AppReviewsFilterChanged extends CommunityFilterEvent { + const AppReviewsFilterChanged(this.filter); - final List reportableEntity; + final AppReviewsFilter filter; @override - List get props => [reportableEntity]; -} - -class CommunityFilterAppReviewFeedbackChanged extends CommunityFilterEvent { - const CommunityFilterAppReviewFeedbackChanged(this.appReviewFeedback); - - final List appReviewFeedback; - - @override - List get props => [appReviewFeedback]; + List get props => [filter]; } class CommunityFilterApplied extends CommunityFilterEvent { diff --git a/lib/community_management/bloc/community_filter/community_filter_state.dart b/lib/community_management/bloc/community_filter/community_filter_state.dart index 0e2b6ed3..0803594f 100644 --- a/lib/community_management/bloc/community_filter/community_filter_state.dart +++ b/lib/community_management/bloc/community_filter/community_filter_state.dart @@ -1,56 +1,92 @@ part of 'community_filter_bloc.dart'; +class EngagementsFilter extends Equatable { + const EngagementsFilter({ + this.searchQuery, + this.selectedStatus, + }); + + final String? searchQuery; + final ModerationStatus? selectedStatus; + + bool get isFilterActive => + (searchQuery != null && searchQuery!.isNotEmpty) || + selectedStatus != null; + + @override + List get props => [searchQuery, selectedStatus]; +} + +class ReportsFilter extends Equatable { + const ReportsFilter({ + this.searchQuery, + this.selectedStatus, + this.selectedReportableEntity, + }); + + final String? searchQuery; + final ModerationStatus? selectedStatus; + final ReportableEntity? selectedReportableEntity; + + bool get isFilterActive => + (searchQuery != null && searchQuery!.isNotEmpty) || + selectedStatus != null || + selectedReportableEntity != null; + + @override + List get props => + [searchQuery, selectedStatus, selectedReportableEntity]; +} + +class AppReviewsFilter extends Equatable { + const AppReviewsFilter({ + this.searchQuery, + this.selectedFeedback, + }); + + final String? searchQuery; + final AppReviewFeedback? selectedFeedback; + + bool get isFilterActive => + (searchQuery != null && searchQuery!.isNotEmpty) || + selectedFeedback != null; + + @override + List get props => [searchQuery, selectedFeedback]; +} + class CommunityFilterState extends Equatable { const CommunityFilterState({ - this.searchQuery = '', - this.selectedModerationStatus = const [], - this.selectedReportableEntity = const [], - this.selectedAppReviewFeedback = const [], + this.engagementsFilter = const EngagementsFilter(), + this.reportsFilter = const ReportsFilter(), + this.appReviewsFilter = const AppReviewsFilter(), this.version = 0, }); - final String searchQuery; - final List selectedModerationStatus; - final List selectedReportableEntity; - final List selectedAppReviewFeedback; final int version; - - bool get isEngagementsFilterActive => - searchQuery.isNotEmpty || selectedModerationStatus.isNotEmpty; - - bool get isReportsFilterActive => - searchQuery.isNotEmpty || - selectedModerationStatus.isNotEmpty || - selectedReportableEntity.isNotEmpty; - - bool get isAppReviewsFilterActive => - searchQuery.isNotEmpty || selectedAppReviewFeedback.isNotEmpty; + final EngagementsFilter engagementsFilter; + final ReportsFilter reportsFilter; + final AppReviewsFilter appReviewsFilter; CommunityFilterState copyWith({ - String? searchQuery, - List? selectedModerationStatus, - List? selectedReportableEntity, - List? selectedAppReviewFeedback, + EngagementsFilter? engagementsFilter, + ReportsFilter? reportsFilter, + AppReviewsFilter? appReviewsFilter, int? version, }) { return CommunityFilterState( - searchQuery: searchQuery ?? this.searchQuery, - selectedModerationStatus: - selectedModerationStatus ?? this.selectedModerationStatus, - selectedReportableEntity: - selectedReportableEntity ?? this.selectedReportableEntity, - selectedAppReviewFeedback: - selectedAppReviewFeedback ?? this.selectedAppReviewFeedback, + engagementsFilter: engagementsFilter ?? this.engagementsFilter, + reportsFilter: reportsFilter ?? this.reportsFilter, + appReviewsFilter: appReviewsFilter ?? this.appReviewsFilter, version: version ?? this.version, ); } @override List get props => [ - searchQuery, - selectedModerationStatus, - selectedReportableEntity, - selectedAppReviewFeedback, - version, - ]; + engagementsFilter, + reportsFilter, + appReviewsFilter, + version, + ]; } From 26ebc5cf2336985b0446035cebd52cface8d516a Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 15:16:47 +0100 Subject: [PATCH 108/130] refactor(community_management): improve filter handling and code structure - Refactor filter building methods to use specific filter parameters - Update Bloc and view layers to use the new filter methods - Simplify filter active checks in views - Remove redundant code and improve readability --- .../bloc/community_management_bloc.dart | 64 +++++++++-------- .../view/app_reviews_page.dart | 62 ++++++++-------- .../view/community_management_page.dart | 6 +- .../view/engagements_page.dart | 27 +++---- .../view/reports_page.dart | 71 +++++++++---------- 5 files changed, 112 insertions(+), 118 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index a2d34601..b49b3577 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -34,7 +34,9 @@ class CommunityManagementBloc _logger.info('Engagement updated, reloading engagements list.'); add( LoadEngagementsRequested( - filter: buildEngagementsFilterMap(_communityFilterBloc.state), + filter: buildEngagementsFilterMap( + _communityFilterBloc.state.engagementsFilter, + ), forceRefresh: true, ), ); @@ -44,7 +46,7 @@ class CommunityManagementBloc _logger.info('Report updated, reloading reports list.'); add( LoadReportsRequested( - filter: buildReportsFilterMap(_communityFilterBloc.state), + filter: buildReportsFilterMap(_communityFilterBloc.state.reportsFilter), forceRefresh: true, ), ); @@ -55,7 +57,9 @@ class CommunityManagementBloc _logger.info('AppReview updated, reloading app reviews list.'); add( LoadAppReviewsRequested( - filter: buildAppReviewsFilterMap(_communityFilterBloc.state), + filter: buildAppReviewsFilterMap( + _communityFilterBloc.state.appReviewsFilter, + ), forceRefresh: true, ), ); @@ -80,48 +84,48 @@ class CommunityManagementBloc return super.close(); } - Map buildEngagementsFilterMap(CommunityFilterState state) { - final filter = {}; - if (state.searchQuery.isNotEmpty) { - filter['userId'] = state.searchQuery; + Map buildEngagementsFilterMap(EngagementsFilter filter) { + final filterMap = {}; + if (filter.searchQuery != null && filter.searchQuery!.isNotEmpty) { + filterMap['userId'] = filter.searchQuery; } - if (state.selectedModerationStatus.isNotEmpty) { - filter['comment.status'] = { - r'$in': state.selectedModerationStatus.map((s) => s.name).toList(), + if (filter.selectedStatus != null) { + filterMap['comment.status'] = { + r'$in': [filter.selectedStatus!.name], }; } - return filter; + return filterMap; } - Map buildReportsFilterMap(CommunityFilterState state) { - final filter = {}; - if (state.searchQuery.isNotEmpty) { - filter['reporterUserId'] = state.searchQuery; + Map buildReportsFilterMap(ReportsFilter filter) { + final filterMap = {}; + if (filter.searchQuery != null && filter.searchQuery!.isNotEmpty) { + filterMap['reporterUserId'] = filter.searchQuery; } - if (state.selectedModerationStatus.isNotEmpty) { - filter['status'] = { - r'$in': state.selectedModerationStatus.map((s) => s.name).toList(), + if (filter.selectedStatus != null) { + filterMap['status'] = { + r'$in': [filter.selectedStatus!.name], }; } - if (state.selectedReportableEntity.isNotEmpty) { - filter['entityType'] = { - r'$in': state.selectedReportableEntity.map((e) => e.name).toList(), + if (filter.selectedReportableEntity != null) { + filterMap['entityType'] = { + r'$in': [filter.selectedReportableEntity!.name], }; } - return filter; + return filterMap; } - Map buildAppReviewsFilterMap(CommunityFilterState state) { - final filter = {}; - if (state.searchQuery.isNotEmpty) { - filter['userId'] = state.searchQuery; + Map buildAppReviewsFilterMap(AppReviewsFilter filter) { + final filterMap = {}; + if (filter.searchQuery != null && filter.searchQuery!.isNotEmpty) { + filterMap['userId'] = filter.searchQuery; } - if (state.selectedAppReviewFeedback.isNotEmpty) { - filter['feedback'] = { - r'$in': state.selectedAppReviewFeedback.map((f) => f.name).toList(), + if (filter.selectedFeedback != null) { + filterMap['feedback'] = { + r'$in': [filter.selectedFeedback!.name], }; } - return filter; + return filterMap; } void _onTabChanged( diff --git a/lib/community_management/view/app_reviews_page.dart b/lib/community_management/view/app_reviews_page.dart index 5d1d440a..ba0fdd48 100644 --- a/lib/community_management/view/app_reviews_page.dart +++ b/lib/community_management/view/app_reviews_page.dart @@ -23,15 +23,13 @@ class _AppReviewsPageState extends State { void initState() { super.initState(); context.read().add( - LoadAppReviewsRequested( - limit: kDefaultRowsPerPage, - filter: context - .read() - .buildAppReviewsFilterMap( - context.read().state, - ), - ), - ); + LoadAppReviewsRequested( + limit: kDefaultRowsPerPage, + filter: context.read().buildAppReviewsFilterMap( + context.read().state.appReviewsFilter, + ), + ), + ); } @override @@ -44,7 +42,8 @@ class _AppReviewsPageState extends State { final filtersActive = context .watch() .state - .isAppReviewsFilterActive; + .appReviewsFilter + .isFilterActive; if (state.appReviewsStatus == CommunityManagementStatus.loading && state.appReviews.isEmpty) { @@ -59,16 +58,15 @@ class _AppReviewsPageState extends State { return FailureStateWidget( exception: state.exception!, onRetry: () => context.read().add( - LoadAppReviewsRequested( - limit: kDefaultRowsPerPage, - forceRefresh: true, - filter: context - .read() - .buildAppReviewsFilterMap( - context.read().state, - ), - ), - ), + LoadAppReviewsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: context + .read() + .buildAppReviewsFilterMap( + context.read().state.appReviewsFilter), + ), + ), ); } @@ -86,8 +84,8 @@ class _AppReviewsPageState extends State { const SizedBox(height: AppSpacing.lg), ElevatedButton( onPressed: () => context.read().add( - const CommunityFilterReset(), - ), + const CommunityFilterReset(), + ), child: Text(l10n.resetFiltersButtonText), ), ], @@ -137,16 +135,16 @@ class _AppReviewsPageState extends State { state.appReviewsStatus != CommunityManagementStatus.loading) { context.read().add( - LoadAppReviewsRequested( - startAfterId: state.appReviewsCursor, - limit: kDefaultRowsPerPage, - filter: context - .read() - .buildAppReviewsFilterMap( - context.read().state, - ), - ), - ); + LoadAppReviewsRequested( + startAfterId: state.appReviewsCursor, + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildAppReviewsFilterMap( + context.read().state.appReviewsFilter, + ), + ), + ); } }, empty: Center(child: Text(l10n.noAppReviewsFound)), diff --git a/lib/community_management/view/community_management_page.dart b/lib/community_management/view/community_management_page.dart index cd183bb1..02c869e2 100644 --- a/lib/community_management/view/community_management_page.dart +++ b/lib/community_management/view/community_management_page.dart @@ -59,7 +59,7 @@ class _CommunityManagementPageState extends State communityManagementBloc.add( LoadEngagementsRequested( filter: communityManagementBloc.buildEngagementsFilterMap( - filterState, + filterState.engagementsFilter, ), forceRefresh: true, ), @@ -68,7 +68,7 @@ class _CommunityManagementPageState extends State communityManagementBloc.add( LoadReportsRequested( filter: communityManagementBloc.buildReportsFilterMap( - filterState, + filterState.reportsFilter, ), forceRefresh: true, ), @@ -77,7 +77,7 @@ class _CommunityManagementPageState extends State communityManagementBloc.add( LoadAppReviewsRequested( filter: communityManagementBloc.buildAppReviewsFilterMap( - filterState, + filterState.appReviewsFilter, ), forceRefresh: true, ), diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index b44b2480..ac500c06 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -25,19 +25,13 @@ class _EngagementsPageState extends State { LoadEngagementsRequested( limit: kDefaultRowsPerPage, filter: context - .read() - .buildEngagementsFilterMap( - context.read().state, + .read().buildEngagementsFilterMap( + context.read().state.engagementsFilter, ), ), ); } - bool _areFiltersActive(CommunityFilterState state) { - return state.searchQuery.isNotEmpty || - state.selectedModerationStatus.isNotEmpty; - } - @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; @@ -45,10 +39,11 @@ class _EngagementsPageState extends State { padding: const EdgeInsets.only(top: AppSpacing.sm), child: BlocBuilder( builder: (context, state) { - final communityFilterState = context + final filtersActive = context .watch() - .state; - final filtersActive = _areFiltersActive(communityFilterState); + .state + .engagementsFilter + .isFilterActive; if (state.engagementsStatus == CommunityManagementStatus.loading && state.engagements.isEmpty) { @@ -68,9 +63,8 @@ class _EngagementsPageState extends State { forceRefresh: true, filter: context .read() - .buildEngagementsFilterMap( - context.read().state, - ), + .buildEngagementsFilterMap(context + .read().state.engagementsFilter), ), ), ); @@ -152,9 +146,8 @@ class _EngagementsPageState extends State { limit: kDefaultRowsPerPage, filter: context .read() - .buildEngagementsFilterMap( - context.read().state, - ), + .buildEngagementsFilterMap(context + .read().state.engagementsFilter), ), ); } diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index 7c4db97a..fb002fef 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -22,19 +22,14 @@ class _ReportsPageState extends State { void initState() { super.initState(); context.read().add( - LoadReportsRequested( - limit: kDefaultRowsPerPage, - filter: context.read().buildReportsFilterMap( - context.read().state, - ), - ), - ); - } - - bool _areFiltersActive(CommunityFilterState state) { - return state.searchQuery.isNotEmpty || - state.selectedModerationStatus.isNotEmpty || - state.selectedReportableEntity.isNotEmpty; + LoadReportsRequested( + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildReportsFilterMap( + context.read().state.reportsFilter), + ), + ); } @override @@ -44,10 +39,11 @@ class _ReportsPageState extends State { padding: const EdgeInsets.only(top: AppSpacing.sm), child: BlocBuilder( builder: (context, state) { - final communityFilterState = context + final filtersActive = context .watch() - .state; - final filtersActive = _areFiltersActive(communityFilterState); + .state + .reportsFilter + .isFilterActive; if (state.reportsStatus == CommunityManagementStatus.loading && state.reports.isEmpty) { @@ -62,15 +58,16 @@ class _ReportsPageState extends State { return FailureStateWidget( exception: state.exception!, onRetry: () => context.read().add( - LoadReportsRequested( - limit: kDefaultRowsPerPage, - forceRefresh: true, - filter: context - .read().buildReportsFilterMap( - context.read().state, - ), - ), - ), + LoadReportsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: context + .read() + .buildReportsFilterMap( + context.read().state.reportsFilter, + ), + ), + ), ); } @@ -88,8 +85,8 @@ class _ReportsPageState extends State { const SizedBox(height: AppSpacing.lg), ElevatedButton( onPressed: () => context.read().add( - const CommunityFilterReset(), - ), + const CommunityFilterReset(), + ), child: Text(l10n.resetFiltersButtonText), ), ], @@ -144,15 +141,17 @@ class _ReportsPageState extends State { state.reportsStatus != CommunityManagementStatus.loading) { context.read().add( - LoadReportsRequested( - startAfterId: state.reportsCursor, - limit: kDefaultRowsPerPage, - filter: context - .read() - .buildReportsFilterMap(context - .read().state), - ), - ); + LoadReportsRequested( + startAfterId: state.reportsCursor, + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildReportsFilterMap(context + .read() + .state + .reportsFilter), + ), + ); } }, empty: Center(child: Text(l10n.noReportsFound)), From 077908ba34c6ebc5edf1ca04d2ea45de51d8cf53 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 15:17:11 +0100 Subject: [PATCH 109/130] refactor(community_management): improve filter dialog functionality - Make search query independent for each tab - Refactor capsule filter to single-choice selection - Update filter state handling for different tabs - Improve code structure and readability --- .../community_filter_dialog.dart | 173 +++++++++++++----- 1 file changed, 123 insertions(+), 50 deletions(-) diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index 24eed601..b39480c5 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -21,10 +21,19 @@ class _CommunityFilterDialogState extends State { @override void initState() { - _searchController = TextEditingController(); - _searchController.text = - context.read().state.searchQuery; super.initState(); + _searchController = TextEditingController(); + final filterState = context.read().state; + final activeTab = + context.read().state.activeTab; + switch (activeTab) { + case CommunityManagementTab.engagements: + _searchController.text = filterState.engagementsFilter.searchQuery ?? ''; + case CommunityManagementTab.reports: + _searchController.text = filterState.reportsFilter.searchQuery ?? ''; + case CommunityManagementTab.appReviews: + _searchController.text = filterState.appReviewsFilter.searchQuery ?? ''; + } } @override @@ -42,6 +51,7 @@ class _CommunityFilterDialogState extends State { return BlocBuilder( builder: (context, filterState) { + _updateSearchController(filterState, activeTab); _searchController.selection = TextSelection.fromPosition( TextPosition(offset: _searchController.text.length), ); @@ -58,12 +68,10 @@ class _CommunityFilterDialogState extends State { icon: const Icon(Icons.refresh), tooltip: l10n.resetFiltersButtonText, onPressed: () { - context - .read() - .add(const CommunityFilterReset()); - context - .read() - .add(const CommunityFilterApplied()); + final filterBloc = context.read(); + filterBloc.add(const CommunityFilterReset()); + _dispatchFilterChanges(filterBloc, const CommunityFilterState()); + filterBloc.add(const CommunityFilterApplied()); Navigator.of(context).pop(); }, ), @@ -94,9 +102,7 @@ class _CommunityFilterDialogState extends State { border: const OutlineInputBorder(), ), onChanged: (query) { - context.read().add( - CommunityFilterSearchQueryChanged(query), - ); + _onSearchQueryChanged(context, query, activeTab); }, ), const SizedBox(height: AppSpacing.lg), @@ -115,12 +121,58 @@ class _CommunityFilterDialogState extends State { ); } + void _updateSearchController( + CommunityFilterState filterState, + CommunityManagementTab activeTab, + ) { + final String currentSearchQuery; + switch (activeTab) { + case CommunityManagementTab.engagements: + currentSearchQuery = filterState.engagementsFilter.searchQuery ?? ''; + case CommunityManagementTab.reports: + currentSearchQuery = filterState.reportsFilter.searchQuery ?? ''; + case CommunityManagementTab.appReviews: + currentSearchQuery = filterState.appReviewsFilter.searchQuery ?? ''; + } + + if (_searchController.text != currentSearchQuery) { + _searchController.text = currentSearchQuery; + } + } + + void _dispatchFilterChanges( + CommunityFilterBloc bloc, + CommunityFilterState state, + ) { + bloc + ..add(EngagementsFilterChanged(state.engagementsFilter)) + ..add(ReportsFilterChanged(state.reportsFilter)) + ..add(AppReviewsFilterChanged(state.appReviewsFilter)); + } + + void _onSearchQueryChanged( + BuildContext context, + String query, + CommunityManagementTab activeTab, + ) { + final bloc = context.read(); + final state = bloc.state; + switch (activeTab) { + case CommunityManagementTab.engagements: + bloc.add(EngagementsFilterChanged(EngagementsFilter(searchQuery: query, selectedStatus: state.engagementsFilter.selectedStatus))); + case CommunityManagementTab.reports: + bloc.add(ReportsFilterChanged(ReportsFilter(searchQuery: query, selectedStatus: state.reportsFilter.selectedStatus, selectedReportableEntity: state.reportsFilter.selectedReportableEntity))); + case CommunityManagementTab.appReviews: + bloc.add(AppReviewsFilterChanged(AppReviewsFilter(searchQuery: query, selectedFeedback: state.appReviewsFilter.selectedFeedback))); + } + } + Widget _buildCapsuleFilter({ required String title, required List allValues, - required List selectedValues, + required T? selectedValue, required String Function(T) labelBuilder, - required void Function(List) onChanged, + required void Function(T?) onChanged, }) { final theme = Theme.of(context); final l10n = AppLocalizationsX(context).l10n; @@ -137,27 +189,19 @@ class _CommunityFilterDialogState extends State { children: [ ChoiceChip( label: Text(l10n.any), - selected: selectedValues.isEmpty, + selected: selectedValue == null, onSelected: (selected) { if (selected) { - onChanged([]); + onChanged(null); } }, ), ...allValues.map((value) { - final isSelected = selectedValues.contains(value); + final isSelected = selectedValue == value; return ChoiceChip( label: Text(labelBuilder(value)), selected: isSelected, - onSelected: (selected) { - final currentSelection = List.from(selectedValues); - if (selected) { - currentSelection.add(value); - } else { - currentSelection.remove(value); - } - onChanged(currentSelection); - }, + onSelected: (selected) => onChanged(selected ? value : null), ); }), ], @@ -176,48 +220,77 @@ class _CommunityFilterDialogState extends State { case CommunityManagementTab.engagements: return [ _buildCapsuleFilter( - title: l10n.status, - allValues: ModerationStatus.values, - selectedValues: state.selectedModerationStatus, - labelBuilder: (item) => item.l10n(context), - onChanged: (items) => context - .read() - .add(CommunityFilterModerationStatusChanged(items)), - ), + title: l10n.status, + allValues: ModerationStatus.values, + selectedValue: state.engagementsFilter.selectedStatus, + labelBuilder: (item) => item.l10n(context), + onChanged: (item) { + context.read().add( + EngagementsFilterChanged( + EngagementsFilter( + searchQuery: state.engagementsFilter.searchQuery, + selectedStatus: item, + ), + ), + ); + }), ]; case CommunityManagementTab.reports: return [ _buildCapsuleFilter( title: l10n.status, allValues: ModerationStatus.values, - selectedValues: state.selectedModerationStatus, + selectedValue: state.reportsFilter.selectedStatus, labelBuilder: (item) => item.l10n(context), - onChanged: (items) => context - .read() - .add(CommunityFilterModerationStatusChanged(items)), + onChanged: (item) { + context.read().add( + ReportsFilterChanged( + ReportsFilter( + searchQuery: state.reportsFilter.searchQuery, + selectedStatus: item, + selectedReportableEntity: + state.reportsFilter.selectedReportableEntity, + ), + ), + ); + }, ), const SizedBox(height: AppSpacing.lg), _buildCapsuleFilter( title: l10n.reportedItem, allValues: ReportableEntity.values, - selectedValues: state.selectedReportableEntity, + selectedValue: state.reportsFilter.selectedReportableEntity, labelBuilder: (item) => item.l10n(context), - onChanged: (items) => context - .read() - .add(CommunityFilterReportableEntityChanged(items)), + onChanged: (item) { + context.read().add( + ReportsFilterChanged( + ReportsFilter( + searchQuery: state.reportsFilter.searchQuery, + selectedStatus: state.reportsFilter.selectedStatus, + selectedReportableEntity: item, + ), + ), + ); + }, ), ]; case CommunityManagementTab.appReviews: return [ _buildCapsuleFilter( - title: l10n.initialFeedback, - allValues: AppReviewFeedback.values, - selectedValues: state.selectedAppReviewFeedback, - labelBuilder: (item) => item.l10n(context), - onChanged: (items) => context - .read() - .add(CommunityFilterAppReviewFeedbackChanged(items)), - ), + title: l10n.initialFeedback, + allValues: AppReviewFeedback.values, + selectedValue: state.appReviewsFilter.selectedFeedback, + labelBuilder: (item) => item.l10n(context), + onChanged: (item) { + context.read().add( + AppReviewsFilterChanged( + AppReviewsFilter( + searchQuery: state.appReviewsFilter.searchQuery, + selectedFeedback: item, + ), + ), + ); + }), ]; } } From db586b945a2638ceb71b9eb9b284b0bb454ff6ab Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 16:36:01 +0100 Subject: [PATCH 110/130] fix(l10n): reorganize and add report resolved localization entries - Reordered localization entries for better organization - Added missing "reportResolved" entry for both Arabic and English - Restored previously removed entries and placed them at the end - Updated descriptions for some entries --- lib/l10n/app_localizations.dart | 66 ++++++++++++++++-------------- lib/l10n/app_localizations_ar.dart | 33 ++++++++------- lib/l10n/app_localizations_en.dart | 33 ++++++++------- lib/l10n/arb/app_ar.arb | 43 ++++++++++--------- lib/l10n/arb/app_en.arb | 42 ++++++++++--------- 5 files changed, 117 insertions(+), 100 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index dde8f00c..ac1f1281 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3662,18 +3662,6 @@ abstract class AppLocalizations { /// **'User ID copied to clipboard.'** String get userIdCopied; - /// Message when a comment is approved - /// - /// In en, this message translates to: - /// **'Comment approved.'** - String get commentApproved; - - /// Message when a comment is rejected - /// - /// In en, this message translates to: - /// **'Comment rejected.'** - String get commentRejected; - /// Message when a report status is updated /// /// In en, this message translates to: @@ -3806,18 +3794,6 @@ abstract class AppLocalizations { /// **'Impersonation'** String get reportReasonImpersonation; - /// Menu item text to copy the ID of a headline. - /// - /// In en, this message translates to: - /// **'Copy Headline ID'** - String get copyHeadlineId; - - /// Menu item text to copy the ID of a reported item. - /// - /// In en, this message translates to: - /// **'Copy Reported Item ID'** - String get copyReportedItemId; - /// Message displayed in the feedback history dialog when there is no history. /// /// In en, this message translates to: @@ -3890,12 +3866,6 @@ abstract class AppLocalizations { /// **'Feedback Details'** String get feedbackDetails; - /// Tooltip for the button to view feedback details. - /// - /// In en, this message translates to: - /// **'View Feedback Details'** - String get viewFeedbackDetails; - /// Moderation status: The item is awaiting review. /// /// In en, this message translates to: @@ -3931,6 +3901,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Without Comment'** String get withoutComment; + + /// No description provided for @reportResolved. + /// + /// In en, this message translates to: + /// **'Report resolved.'** + String get reportResolved; + + /// Message when a comment is approved + /// + /// In en, this message translates to: + /// **'Comment approved.'** + String get commentApproved; + + /// Message when a comment is rejected + /// + /// In en, this message translates to: + /// **'Comment rejected.'** + String get commentRejected; + + /// Menu item text to copy the ID of a headline. + /// + /// In en, this message translates to: + /// **'Copy Headline ID'** + String get copyHeadlineId; + + /// Menu item text to copy the ID of a reported item. + /// + /// In en, this message translates to: + /// **'Copy Reported Item ID'** + String get copyReportedItemId; + + /// Tooltip for the button to view feedback details. + /// + /// In en, this message translates to: + /// **'View Feedback Details'** + String get viewFeedbackDetails; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 1d970581..b54f56e6 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1963,12 +1963,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get userIdCopied => 'تم نسخ معرف المستخدم إلى الحافظة.'; - @override - String get commentApproved => 'تمت الموافقة على التعليق.'; - - @override - String get commentRejected => 'تم رفض التعليق.'; - @override String get reportStatusUpdated => 'تم تحديث حالة البلاغ.'; @@ -2042,12 +2036,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get reportReasonImpersonation => 'انتحال شخصية'; - @override - String get copyHeadlineId => 'نسخ معرّف العنوان'; - - @override - String get copyReportedItemId => 'نسخ معرّف العنصر المُبلغ عنه'; - @override String get noNegativeFeedbackHistory => 'لم يتم العثور على سجل تقييمات سلبية لهذا المستخدم.'; @@ -2086,9 +2074,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get feedbackDetails => 'تفاصيل التقييم'; - @override - String get viewFeedbackDetails => 'عرض تفاصيل التقييم'; - @override String get moderationStatusPendingReview => 'قيد المراجعة'; @@ -2106,4 +2091,22 @@ class AppLocalizationsAr extends AppLocalizations { @override String get withoutComment => 'بدون تعليق'; + + @override + String get reportResolved => 'تم حل البلاغ.'; + + @override + String get commentApproved => 'تمت الموافقة على التعليق.'; + + @override + String get commentRejected => 'تم رفض التعليق.'; + + @override + String get copyHeadlineId => 'نسخ معرّف العنوان'; + + @override + String get copyReportedItemId => 'نسخ معرّف العنصر المُبلغ عنه'; + + @override + String get viewFeedbackDetails => 'عرض تفاصيل التقييم'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 71d0a19a..a1cdbb13 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1968,12 +1968,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get userIdCopied => 'User ID copied to clipboard.'; - @override - String get commentApproved => 'Comment approved.'; - - @override - String get commentRejected => 'Comment rejected.'; - @override String get reportStatusUpdated => 'Report status updated.'; @@ -2048,12 +2042,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get reportReasonImpersonation => 'Impersonation'; - @override - String get copyHeadlineId => 'Copy Headline ID'; - - @override - String get copyReportedItemId => 'Copy Reported Item ID'; - @override String get noNegativeFeedbackHistory => 'No negative feedback history found for this user.'; @@ -2092,9 +2080,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get feedbackDetails => 'Feedback Details'; - @override - String get viewFeedbackDetails => 'View Feedback Details'; - @override String get moderationStatusPendingReview => 'Pending Review'; @@ -2112,4 +2097,22 @@ class AppLocalizationsEn extends AppLocalizations { @override String get withoutComment => 'Without Comment'; + + @override + String get reportResolved => 'Report resolved.'; + + @override + String get commentApproved => 'Comment approved.'; + + @override + String get commentRejected => 'Comment rejected.'; + + @override + String get copyHeadlineId => 'Copy Headline ID'; + + @override + String get copyReportedItemId => 'Copy Reported Item ID'; + + @override + String get viewFeedbackDetails => 'View Feedback Details'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 116ee9d0..fdc49ad1 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2462,14 +2462,6 @@ "@userIdCopied": { "description": "رسالة عند نسخ معرف المستخدم" }, - "commentApproved": "تمت الموافقة على التعليق.", - "@commentApproved": { - "description": "رسالة عند الموافقة على تعليق" - }, - "commentRejected": "تم رفض التعليق.", - "@commentRejected": { - "description": "رسالة عند رفض تعليق" - }, "reportStatusUpdated": "تم تحديث حالة البلاغ.", "@reportStatusUpdated": { "description": "رسالة عند تحديث حالة البلاغ" @@ -2573,14 +2565,6 @@ "@reportReasonImpersonation": { "description": "سبب البلاغ: المصدر ينتحل شخصية جهة أخرى." }, - "copyHeadlineId": "نسخ معرّف العنوان", - "@copyHeadlineId": { - "description": "نص عنصر القائمة لنسخ معرّف العنوان الرئيسي." - }, - "copyReportedItemId": "نسخ معرّف العنصر المُبلغ عنه", - "@copyReportedItemId": { - "description": "نص عنصر القائمة لنسخ معرّف العنصر المُبلغ عنه." - }, "noNegativeFeedbackHistory": "لم يتم العثور على سجل تقييمات سلبية لهذا المستخدم.", "@noNegativeFeedbackHistory": { "description": "رسالة تظهر في مربع حوار سجل التقييمات عند عدم وجود سجل." @@ -2629,10 +2613,6 @@ "@feedbackDetails": { "description": "Title for the dialog showing detailed user feedback." }, - "viewFeedbackDetails": "عرض تفاصيل التقييم", - "@viewFeedbackDetails": { - "description": "Tooltip for the button to view feedback details." - }, "moderationStatusPendingReview": "قيد المراجعة", "@moderationStatusPendingReview": { "description": "Moderation status: The item is awaiting review." @@ -2656,5 +2636,28 @@ "withoutComment": "بدون تعليق", "@withoutComment": { "description": "خيار تصفية لإظهار العناصر التي لا تحتوي على تعليق فقط." + }, + "reportResolved": "تم حل البلاغ.", + "@reportResolved": {}, + "commentApproved": "تمت الموافقة على التعليق.", + "@commentApproved": { + "description": "رسالة عند الموافقة على تعليق" + }, + "commentRejected": "تم رفض التعليق.", + "@commentRejected": { + "description": "رسالة عند رفض تعليق" + }, + "copyHeadlineId": "نسخ معرّف العنوان", + "@copyHeadlineId": { + "description": "نص عنصر القائمة لنسخ معرّف العنوان الرئيسي." + }, + "copyReportedItemId": "نسخ معرّف العنصر المُبلغ عنه", + "@copyReportedItemId": { + "description": "نص عنصر القائمة لنسخ معرّف العنصر المُبلغ عنه." + }, + "viewFeedbackDetails": "عرض تفاصيل التقييم", + "@viewFeedbackDetails": { + "description": "Tooltip for the button to view feedback details." } + } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6b91354c..f3e07c2d 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2458,14 +2458,6 @@ "@userIdCopied": { "description": "Message when user ID is copied" }, - "commentApproved": "Comment approved.", - "@commentApproved": { - "description": "Message when a comment is approved" - }, - "commentRejected": "Comment rejected.", - "@commentRejected": { - "description": "Message when a comment is rejected" - }, "reportStatusUpdated": "Report status updated.", "@reportStatusUpdated": { "description": "Message when a report status is updated" @@ -2569,14 +2561,6 @@ "@reportReasonImpersonation": { "description": "Report reason: The source is impersonating another entity." }, - "copyHeadlineId": "Copy Headline ID", - "@copyHeadlineId": { - "description": "Menu item text to copy the ID of a headline." - }, - "copyReportedItemId": "Copy Reported Item ID", - "@copyReportedItemId": { - "description": "Menu item text to copy the ID of a reported item." - }, "noNegativeFeedbackHistory": "No negative feedback history found for this user.", "@noNegativeFeedbackHistory": { "description": "Message displayed in the feedback history dialog when there is no history." @@ -2625,10 +2609,6 @@ "@feedbackDetails": { "description": "Title for the dialog showing detailed user feedback." }, - "viewFeedbackDetails": "View Feedback Details", - "@viewFeedbackDetails": { - "description": "Tooltip for the button to view feedback details." - }, "moderationStatusPendingReview": "Pending Review", "@moderationStatusPendingReview": { "description": "Moderation status: The item is awaiting review." @@ -2652,5 +2632,27 @@ "withoutComment": "Without Comment", "@withoutComment": { "description": "Filter option to show only items that do not have a comment." + }, + "reportResolved": "Report resolved.", + "@reportResolved": {}, + "commentApproved": "Comment approved.", + "@commentApproved": { + "description": "Message when a comment is approved" + }, + "commentRejected": "Comment rejected.", + "@commentRejected": { + "description": "Message when a comment is rejected" + }, + "copyHeadlineId": "Copy Headline ID", + "@copyHeadlineId": { + "description": "Menu item text to copy the ID of a headline." + }, + "copyReportedItemId": "Copy Reported Item ID", + "@copyReportedItemId": { + "description": "Menu item text to copy the ID of a reported item." + }, + "viewFeedbackDetails": "View Feedback Details", + "@viewFeedbackDetails": { + "description": "Tooltip for the button to view feedback details." } } \ No newline at end of file From bfd14fa5e59a9d3cdf021f68ff987313245e07d0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 16:36:25 +0100 Subject: [PATCH 111/130] feat(community_management): implement pending updates and improve UI - Add support for approving, rejecting, and resolving community content - Implement undo functionality for pending updates - Display snackbar notifications for update confirmation and provide undo option - Refactor CommunityManagementBloc to handle new update-related events - Update UI to show snackbar notifications and handle undo actions --- .../bloc/community_management_bloc.dart | 172 ++++++++++++++++++ .../bloc/community_management_event.dart | 45 +++++ .../bloc/community_management_state.dart | 42 +++-- .../view/community_management_page.dart | 96 ++++++---- 4 files changed, 306 insertions(+), 49 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index b49b3577..608ef1e4 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -5,6 +5,8 @@ import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/constants/app_constants.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_updates_service.dart'; import 'package:logging/logging.dart'; part 'community_management_event.dart'; @@ -17,17 +19,24 @@ class CommunityManagementBloc required DataRepository reportsRepository, required DataRepository appReviewsRepository, required CommunityFilterBloc communityFilterBloc, + required PendingUpdatesService pendingUpdatesService, Logger? logger, }) : _engagementsRepository = engagementsRepository, _reportsRepository = reportsRepository, _appReviewsRepository = appReviewsRepository, _communityFilterBloc = communityFilterBloc, + _pendingUpdatesService = pendingUpdatesService, _logger = logger ?? Logger('CommunityManagementBloc'), super(const CommunityManagementState()) { on(_onTabChanged); on(_onLoadEngagementsRequested); on(_onLoadReportsRequested); on(_onLoadAppReviewsRequested); + on(_onApproveCommentRequested); + on(_onRejectCommentRequested); + on(_onResolveReportRequested); + on(_onUndoUpdateRequested); + on(_onUpdateEventReceived); _engagementsUpdateSubscription = _engagementsRepository.entityUpdated.listen((_) { @@ -64,6 +73,11 @@ class CommunityManagementBloc ), ); }); + + _updateEventsSubscription = + _pendingUpdatesService.updateEvents.listen( + (event) => add(UpdateEventReceived(event)), + ); } final DataRepository _engagementsRepository; @@ -71,16 +85,19 @@ class CommunityManagementBloc final DataRepository _appReviewsRepository; final CommunityFilterBloc _communityFilterBloc; final Logger _logger; + final PendingUpdatesService _pendingUpdatesService; late final StreamSubscription _engagementsUpdateSubscription; late final StreamSubscription _reportsUpdateSubscription; late final StreamSubscription _appReviewsUpdateSubscription; + late final StreamSubscription> _updateEventsSubscription; @override Future close() { _engagementsUpdateSubscription.cancel(); _reportsUpdateSubscription.cancel(); _appReviewsUpdateSubscription.cancel(); + _updateEventsSubscription.cancel(); return super.close(); } @@ -266,4 +283,159 @@ class CommunityManagementBloc ); } } + + Future _onApproveCommentRequested( + ApproveCommentRequested event, + Emitter emit, + ) async { + try { + final originalEngagement = state.engagements.firstWhere( + (e) => e.id == event.engagementId, + ); + + if (originalEngagement.comment == null) return; + + final updatedEngagement = originalEngagement.copyWith( + comment: originalEngagement.comment!.copyWith( + status: ModerationStatus.resolved, + ), + ); + + final updatedEngagements = List.from(state.engagements) + ..[state.engagements.indexOf(originalEngagement)] = updatedEngagement; + + emit( + state.copyWith( + engagements: updatedEngagements, + lastPendingUpdateId: event.engagementId, + snackbarMessage: 'Comment approved.', + ), + ); + + _pendingUpdatesService.requestUpdate( + originalItem: originalEngagement, + updatedItem: updatedEngagement, + repository: _engagementsRepository, + undoDuration: AppConstants.kSnackbarDuration, + ); + } catch (e) { + _logger.severe('Error approving comment: $e'); + } + } + + Future _onRejectCommentRequested( + RejectCommentRequested event, + Emitter emit, + ) async { + try { + final originalEngagement = state.engagements.firstWhere( + (e) => e.id == event.engagementId, + ); + + final updatedEngagement = originalEngagement.copyWith( + comment: null, + ); + + final updatedEngagements = List.from(state.engagements) + ..[state.engagements.indexOf(originalEngagement)] = updatedEngagement; + + emit( + state.copyWith( + engagements: updatedEngagements, + lastPendingUpdateId: event.engagementId, + snackbarMessage: 'Comment rejected.', + ), + ); + + _pendingUpdatesService.requestUpdate( + originalItem: originalEngagement, + updatedItem: updatedEngagement, + repository: _engagementsRepository, + undoDuration: AppConstants.kSnackbarDuration, + ); + } catch (e) { + _logger.severe('Error rejecting comment: $e'); + } + } + + Future _onResolveReportRequested( + ResolveReportRequested event, + Emitter emit, + ) async { + try { + final originalReport = state.reports.firstWhere( + (r) => r.id == event.reportId, + ); + + final updatedReport = originalReport.copyWith( + status: ModerationStatus.resolved, + ); + + final updatedReports = List.from(state.reports) + ..[state.reports.indexOf(originalReport)] = updatedReport; + + emit( + state.copyWith( + reports: updatedReports, + lastPendingUpdateId: event.reportId, + snackbarMessage: 'Report resolved.', + ), + ); + + _pendingUpdatesService.requestUpdate( + originalItem: originalReport, + updatedItem: updatedReport, + repository: _reportsRepository, + undoDuration: AppConstants.kSnackbarDuration, + ); + } catch (e) { + _logger.severe('Error resolving report: $e'); + } + } + + void _onUndoUpdateRequested( + UndoUpdateRequested event, + Emitter emit, + ) { + _pendingUpdatesService.undoUpdate(event.id); + } + + Future _onUpdateEventReceived( + UpdateEventReceived event, + Emitter emit, + ) async { + switch (event.event.status) { + case UpdateStatus.confirmed: + emit( + state.copyWith( + lastPendingUpdateId: null, + snackbarMessage: null, + ), + ); + case UpdateStatus.undone: + final item = event.event.originalItem; + if (item is Engagement) { + final index = + state.engagements.indexWhere((e) => e.id == item.id); + if (index != -1) { + final updatedEngagements = + List.from(state.engagements)..[index] = item; + emit(state.copyWith(engagements: updatedEngagements)); + } + } else if (item is Report) { + final index = state.reports.indexWhere((r) => r.id == item.id); + if (index != -1) { + final updatedReports = + List.from(state.reports)..[index] = item; + emit(state.copyWith(reports: updatedReports)); + } + } + emit( + state.copyWith( + lastPendingUpdateId: null, + snackbarMessage: null, + ), + ); + } + } } diff --git a/lib/community_management/bloc/community_management_event.dart b/lib/community_management/bloc/community_management_event.dart index 9bfe796f..f7750026 100644 --- a/lib/community_management/bloc/community_management_event.dart +++ b/lib/community_management/bloc/community_management_event.dart @@ -66,3 +66,48 @@ class LoadAppReviewsRequested extends CommunityManagementEvent { @override List get props => [startAfterId, limit, filter, forceRefresh]; } + +final class ApproveCommentRequested extends CommunityManagementEvent { + const ApproveCommentRequested(this.engagementId); + + final String engagementId; + + @override + List get props => [engagementId]; +} + +final class RejectCommentRequested extends CommunityManagementEvent { + const RejectCommentRequested(this.engagementId); + + final String engagementId; + + @override + List get props => [engagementId]; +} + +final class ResolveReportRequested extends CommunityManagementEvent { + const ResolveReportRequested(this.reportId); + + final String reportId; + + @override + List get props => [reportId]; +} + +final class UndoUpdateRequested extends CommunityManagementEvent { + const UndoUpdateRequested(this.id); + + final String id; + + @override + List get props => [id]; +} + +final class UpdateEventReceived extends CommunityManagementEvent { + const UpdateEventReceived(this.event); + + final UpdateEvent event; + + @override + List get props => [event]; +} diff --git a/lib/community_management/bloc/community_management_state.dart b/lib/community_management/bloc/community_management_state.dart index e7acc204..580bb939 100644 --- a/lib/community_management/bloc/community_management_state.dart +++ b/lib/community_management/bloc/community_management_state.dart @@ -20,6 +20,8 @@ class CommunityManagementState extends Equatable { this.hasMoreReports = true, this.hasMoreAppReviews = true, this.exception, + this.lastPendingUpdateId, + this.snackbarMessage, }); final CommunityManagementTab activeTab; @@ -36,6 +38,8 @@ class CommunityManagementState extends Equatable { final bool hasMoreReports; final bool hasMoreAppReviews; final HttpException? exception; + final String? lastPendingUpdateId; + final String? snackbarMessage; CommunityManagementState copyWith({ CommunityManagementTab? activeTab, @@ -52,6 +56,8 @@ class CommunityManagementState extends Equatable { bool? hasMoreReports, bool? hasMoreAppReviews, HttpException? exception, + String? lastPendingUpdateId, + String? snackbarMessage, bool forceEngagementsCursor = false, bool forceReportsCursor = false, bool forceAppReviewsCursor = false, @@ -76,25 +82,29 @@ class CommunityManagementState extends Equatable { hasMoreEngagements: hasMoreEngagements ?? this.hasMoreEngagements, hasMoreReports: hasMoreReports ?? this.hasMoreReports, hasMoreAppReviews: hasMoreAppReviews ?? this.hasMoreAppReviews, - exception: exception ?? this.exception, + exception: exception, + lastPendingUpdateId: lastPendingUpdateId, + snackbarMessage: snackbarMessage, ); } @override List get props => [ - activeTab, - engagementsStatus, - reportsStatus, - appReviewsStatus, - engagements, - reports, - appReviews, - engagementsCursor, - reportsCursor, - appReviewsCursor, - hasMoreEngagements, - hasMoreReports, - hasMoreAppReviews, - exception, - ]; + activeTab, + engagementsStatus, + reportsStatus, + appReviewsStatus, + engagements, + reports, + appReviews, + engagementsCursor, + reportsCursor, + appReviewsCursor, + hasMoreEngagements, + hasMoreReports, + hasMoreAppReviews, + exception, + lastPendingUpdateId, + snackbarMessage, + ]; } diff --git a/lib/community_management/view/community_management_page.dart b/lib/community_management/view/community_management_page.dart index 02c869e2..d058ec92 100644 --- a/lib/community_management/view/community_management_page.dart +++ b/lib/community_management/view/community_management_page.dart @@ -50,40 +50,70 @@ class _CommunityManagementPageState extends State @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; - return BlocListener( - listenWhen: (previous, current) => previous.version != current.version, - listener: (context, filterState) { - final communityManagementBloc = context.read(); - switch (communityManagementBloc.state.activeTab) { - case CommunityManagementTab.engagements: - communityManagementBloc.add( - LoadEngagementsRequested( - filter: communityManagementBloc.buildEngagementsFilterMap( - filterState.engagementsFilter, - ), - forceRefresh: true, - ), - ); - case CommunityManagementTab.reports: - communityManagementBloc.add( - LoadReportsRequested( - filter: communityManagementBloc.buildReportsFilterMap( - filterState.reportsFilter, - ), - forceRefresh: true, - ), - ); - case CommunityManagementTab.appReviews: - communityManagementBloc.add( - LoadAppReviewsRequested( - filter: communityManagementBloc.buildAppReviewsFilterMap( - filterState.appReviewsFilter, + return MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (previous, current) => + previous.version != current.version, + listener: (context, filterState) { + final communityManagementBloc = context + .read(); + switch (communityManagementBloc.state.activeTab) { + case CommunityManagementTab.engagements: + communityManagementBloc.add( + LoadEngagementsRequested( + filter: communityManagementBloc.buildEngagementsFilterMap( + filterState.engagementsFilter, + ), + forceRefresh: true, + ), + ); + case CommunityManagementTab.reports: + communityManagementBloc.add( + LoadReportsRequested( + filter: communityManagementBloc.buildReportsFilterMap( + filterState.reportsFilter, + ), + forceRefresh: true, + ), + ); + case CommunityManagementTab.appReviews: + communityManagementBloc.add( + LoadAppReviewsRequested( + filter: communityManagementBloc.buildAppReviewsFilterMap( + filterState.appReviewsFilter, + ), + forceRefresh: true, + ), + ); + } + }, + ), + BlocListener( + listenWhen: (previous, current) => + previous.snackbarMessage != current.snackbarMessage && + current.snackbarMessage != null, + listener: (context, state) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(state.snackbarMessage!), + action: SnackBarAction( + label: l10n.undo, + onPressed: () { + if (state.lastPendingUpdateId != null) { + context.read().add( + UndoUpdateRequested(state.lastPendingUpdateId!), + ); + } + }, + ), ), - forceRefresh: true, - ), - ); - } - }, + ); + }, + ), + ], child: Scaffold( appBar: AppBar( title: Row( From f67f8a8b162f250ee8e0fde645c53419372a43fe Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 16:37:05 +0100 Subject: [PATCH 112/130] refactor(community_management): improve code structure and remove desktop-specific code - Remove comment and report status columns from engagements and reports tables on mobile - Adjust column sizes for better layout consistency - Remove unused import statements - Improve code formatting and readability --- .../view/engagements_page.dart | 56 +++------ .../view/reports_page.dart | 113 ++++++------------ 2 files changed, 51 insertions(+), 118 deletions(-) diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index ac500c06..0f129e6c 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -25,7 +25,8 @@ class _EngagementsPageState extends State { LoadEngagementsRequested( limit: kDefaultRowsPerPage, filter: context - .read().buildEngagementsFilterMap( + .read() + .buildEngagementsFilterMap( context.read().state.engagementsFilter, ), ), @@ -63,8 +64,12 @@ class _EngagementsPageState extends State { forceRefresh: true, filter: context .read() - .buildEngagementsFilterMap(context - .read().state.engagementsFilter), + .buildEngagementsFilterMap( + context + .read() + .state + .engagementsFilter, + ), ), ), ); @@ -111,14 +116,9 @@ class _EngagementsPageState extends State { label: Text(l10n.reaction), size: ColumnSize.S, ), - if (!isMobile) - DataColumn2( - label: Text(l10n.comment), - size: ColumnSize.L, - ), DataColumn2( label: Text(l10n.date), - size: ColumnSize.S, + size: ColumnSize.M, ), DataColumn2( label: Text(l10n.actions), @@ -146,8 +146,12 @@ class _EngagementsPageState extends State { limit: kDefaultRowsPerPage, filter: context .read() - .buildEngagementsFilterMap(context - .read().state.engagementsFilter), + .buildEngagementsFilterMap( + context + .read() + .state + .engagementsFilter, + ), ), ); } @@ -199,36 +203,6 @@ class _EngagementsDataSource extends DataTableSource { visualDensity: VisualDensity.compact, ), ), - if (!isMobile) ...[ - DataCell( - Row( - children: [ - if (engagement.comment != null && - engagement.comment!.status == - ModerationStatus.pendingReview) ...[ - Tooltip( - message: l10n.moderationStatusPendingReview, - child: Icon( - Icons.hourglass_empty_outlined, - size: 16, - color: Theme.of(context).colorScheme.secondary, - ), - ), - const SizedBox(width: AppSpacing.sm), - ], - Expanded( - child: Tooltip( - message: engagement.comment?.content ?? l10n.notAvailable, - child: Text( - engagement.comment?.content ?? l10n.notAvailable, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ), - ), - ], DataCell( Text(DateFormat('dd-MM-yyyy').format(engagement.createdAt.toLocal())), ), diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index fb002fef..38906ae1 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -8,6 +8,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/community_manage import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; +import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; class ReportsPage extends StatefulWidget { @@ -22,14 +23,13 @@ class _ReportsPageState extends State { void initState() { super.initState(); context.read().add( - LoadReportsRequested( - limit: kDefaultRowsPerPage, - filter: context - .read() - .buildReportsFilterMap( - context.read().state.reportsFilter), - ), - ); + LoadReportsRequested( + limit: kDefaultRowsPerPage, + filter: context.read().buildReportsFilterMap( + context.read().state.reportsFilter, + ), + ), + ); } @override @@ -58,16 +58,16 @@ class _ReportsPageState extends State { return FailureStateWidget( exception: state.exception!, onRetry: () => context.read().add( - LoadReportsRequested( - limit: kDefaultRowsPerPage, - forceRefresh: true, - filter: context - .read() - .buildReportsFilterMap( - context.read().state.reportsFilter, - ), - ), - ), + LoadReportsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: context + .read() + .buildReportsFilterMap( + context.read().state.reportsFilter, + ), + ), + ), ); } @@ -85,8 +85,8 @@ class _ReportsPageState extends State { const SizedBox(height: AppSpacing.lg), ElevatedButton( onPressed: () => context.read().add( - const CommunityFilterReset(), - ), + const CommunityFilterReset(), + ), child: Text(l10n.resetFiltersButtonText), ), ], @@ -112,17 +112,12 @@ class _ReportsPageState extends State { size: ColumnSize.S, ), DataColumn2( - label: Text(l10n.reason), + label: Text(l10n.date), size: ColumnSize.M, ), - if (!isMobile) - DataColumn2( - label: Text(l10n.status), - size: ColumnSize.S, - ), DataColumn2( label: Text(l10n.actions), - size: ColumnSize.S, + size: ColumnSize.L, ), ], source: _ReportsDataSource( @@ -141,17 +136,19 @@ class _ReportsPageState extends State { state.reportsStatus != CommunityManagementStatus.loading) { context.read().add( - LoadReportsRequested( - startAfterId: state.reportsCursor, - limit: kDefaultRowsPerPage, - filter: context - .read() - .buildReportsFilterMap(context - .read() - .state - .reportsFilter), - ), - ); + LoadReportsRequested( + startAfterId: state.reportsCursor, + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildReportsFilterMap( + context + .read() + .state + .reportsFilter, + ), + ), + ); } }, empty: Center(child: Text(l10n.noReportsFound)), @@ -202,24 +199,8 @@ class _ReportsDataSource extends DataTableSource { ), ), DataCell( - Chip( - label: Text(report.reason.l10n(context)), - visualDensity: VisualDensity.compact, - ), + Text(DateFormat('dd-MM-yyyy').format(report.createdAt.toLocal())), ), - if (!isMobile) - DataCell( - Chip( - avatar: Icon( - _getReportStatusIcon(report.status), - size: 16, - ), - label: Text(report.status.l10n(context)), - backgroundColor: _getReportStatusColor(context, report.status), - side: BorderSide.none, - visualDensity: VisualDensity.compact, - ), - ), DataCell(CommunityActionButtons(item: report, l10n: l10n)), ], ); @@ -233,26 +214,4 @@ class _ReportsDataSource extends DataTableSource { @override int get selectedRowCount => 0; - - Color? _getReportStatusColor( - BuildContext context, - ModerationStatus status, - ) { - final colorScheme = Theme.of(context).colorScheme; - switch (status) { - case ModerationStatus.resolved: - return colorScheme.primaryContainer.withOpacity(0.5); - case ModerationStatus.pendingReview: - return colorScheme.secondaryContainer.withOpacity(0.5); - } - } - - IconData _getReportStatusIcon(ModerationStatus status) { - switch (status) { - case ModerationStatus.resolved: - return Icons.check_circle_outline; - case ModerationStatus.pendingReview: - return Icons.hourglass_empty_outlined; - } - } } From 4611203acac798923111588f28e3bf71a47680b0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 16:37:22 +0100 Subject: [PATCH 113/130] feat(community_management): add details dialogs for app reviews, engagements, and reports - Create AppReviewDetailsDialog to display detailed feedback for app reviews - Implement EngagementDetailsDialog to show engagement information and moderation options - Add ReportDetailsDialog for viewing report details and resolution --- .../widgets/app_review_details_dialog.dart | 51 +++++++++ .../widgets/engagement_details_dialog.dart | 100 ++++++++++++++++++ .../widgets/report_details_dialog.dart | 87 +++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 lib/community_management/widgets/app_review_details_dialog.dart create mode 100644 lib/community_management/widgets/engagement_details_dialog.dart create mode 100644 lib/community_management/widgets/report_details_dialog.dart diff --git a/lib/community_management/widgets/app_review_details_dialog.dart b/lib/community_management/widgets/app_review_details_dialog.dart new file mode 100644 index 00000000..9d1bb4e4 --- /dev/null +++ b/lib/community_management/widgets/app_review_details_dialog.dart @@ -0,0 +1,51 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:ui_kit/ui_kit.dart'; + +class AppReviewDetailsDialog extends StatelessWidget { + const AppReviewDetailsDialog({ + required this.appReview, + super.key, + }); + + final AppReview appReview; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final details = appReview.feedbackDetails; + + return AlertDialog( + title: Text(l10n.feedbackDetails), + content: SizedBox( + width: double.maxFinite, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.feedbackProvidedAt( + DateFormatter.formatRelativeTime( + context, + appReview.updatedAt, + ), + ), + style: Theme.of(context).textTheme.labelSmall, + ), + const SizedBox(height: AppSpacing.md), + Text(details ?? l10n.noReasonProvided), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.closeButtonText), + ), + ], + ); + } +} diff --git a/lib/community_management/widgets/engagement_details_dialog.dart b/lib/community_management/widgets/engagement_details_dialog.dart new file mode 100644 index 00000000..d444318a --- /dev/null +++ b/lib/community_management/widgets/engagement_details_dialog.dart @@ -0,0 +1,100 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +class EngagementDetailsDialog extends StatelessWidget { + const EngagementDetailsDialog({ + required this.engagement, + super.key, + }); + + final Engagement engagement; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final theme = Theme.of(context); + + final hasComment = engagement.comment != null; + final isPendingReview = + hasComment && + engagement.comment!.status == ModerationStatus.pendingReview; + + return AlertDialog( + title: Text(l10n.engagements), + content: SizedBox( + width: double.maxFinite, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${l10n.reaction}: ${engagement.reaction.reactionType.name}', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: AppSpacing.sm), + Text( + '${l10n.user}: ${engagement.userId}', + style: theme.textTheme.bodySmall, + ), + Text( + '${l10n.date}: ${DateFormat('dd-MM-yyyy HH:mm').format(engagement.createdAt.toLocal())}', + style: theme.textTheme.bodySmall, + ), + const Divider(height: AppSpacing.lg), + Text( + l10n.comment, + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: AppSpacing.sm), + Text( + engagement.comment?.content ?? l10n.noReasonProvided, + style: hasComment + ? theme.textTheme.bodyLarge + : theme.textTheme.bodyLarge?.copyWith( + fontStyle: FontStyle.italic, + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.closeButtonText), + ), + if (isPendingReview) ...[ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.error, + foregroundColor: theme.colorScheme.onError, + ), + onPressed: () { + context.read().add( + RejectCommentRequested(engagement.id), + ); + Navigator.of(context).pop(); + }, + child: Text(l10n.rejectComment), + ), + ElevatedButton( + onPressed: () { + context.read().add( + ApproveCommentRequested(engagement.id), + ); + Navigator.of(context).pop(); + }, + child: Text(l10n.approveComment), + ), + ], + ], + ); + } +} diff --git a/lib/community_management/widgets/report_details_dialog.dart b/lib/community_management/widgets/report_details_dialog.dart new file mode 100644 index 00000000..4d36a641 --- /dev/null +++ b/lib/community_management/widgets/report_details_dialog.dart @@ -0,0 +1,87 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; +import 'package:intl/intl.dart'; +import 'package:ui_kit/ui_kit.dart'; + +class ReportDetailsDialog extends StatelessWidget { + const ReportDetailsDialog({ + required this.report, + super.key, + }); + + final Report report; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final theme = Theme.of(context); + + final isPendingReview = report.status == ModerationStatus.pendingReview; + + return AlertDialog( + title: Text(l10n.reports), + content: SizedBox( + width: double.maxFinite, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${l10n.reportedItem}: ${report.entityType.l10n(context)}', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: AppSpacing.sm), + Text( + '${l10n.reporter}: ${report.reporterUserId}', + style: theme.textTheme.bodySmall, + ), + Text( + '${l10n.date}: ${DateFormat('dd-MM-yyyy HH:mm').format(report.createdAt.toLocal())}', + style: theme.textTheme.bodySmall, + ), + const Divider(height: AppSpacing.lg), + Text( + '${l10n.reason}: ${report.reason.l10n(context)}', + style: theme.textTheme.titleMedium, + ), + if (report.additionalComments != null && + report.additionalComments!.isNotEmpty) ...[ + const SizedBox(height: AppSpacing.md), + Text( + l10n.comment, + style: theme.textTheme.titleSmall, + ), + const SizedBox(height: AppSpacing.xs), + Text( + report.additionalComments!, + style: theme.textTheme.bodyLarge, + ), + ], + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.closeButtonText), + ), + if (isPendingReview) + ElevatedButton( + onPressed: () { + context.read().add( + ResolveReportRequested(report.id), + ); + Navigator.of(context).pop(); + }, + child: Text(l10n.resolveReport), + ), + ], + ); + } +} \ No newline at end of file From a2ae6087d1654cca0a76bf64f989df1b543af821 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 16:37:34 +0100 Subject: [PATCH 114/130] feat(shared): add pending updates service - Implement PendingUpdatesService to manage delayed item updates - Add PendingUpdatesServiceImpl as a concrete implementation - Integrate PendingUpdatesService into the app's dependency injection - Define UpdateEvent and UpdateStatus for tracking update events --- lib/app/view/app.dart | 5 + .../services/pending_updates_service.dart | 160 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 lib/shared/services/pending_updates_service.dart diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 31a2f2fc..a724e64b 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -21,6 +21,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localiz import 'package:flutter_news_app_web_dashboard_full_source_code/overview/bloc/overview_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/router/router.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_updates_service.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_filter/user_filter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_management_bloc.dart'; @@ -114,6 +115,9 @@ class App extends StatelessWidget { RepositoryProvider.value( value: _pendingDeletionsService, ), + RepositoryProvider( + create: (context) => PendingUpdatesServiceImpl(), + ), ], child: MultiBlocProvider( providers: [ @@ -186,6 +190,7 @@ class App extends StatelessWidget { reportsRepository: context.read>(), appReviewsRepository: context.read>(), communityFilterBloc: context.read(), + pendingUpdatesService: context.read(), ), ), ], diff --git a/lib/shared/services/pending_updates_service.dart b/lib/shared/services/pending_updates_service.dart new file mode 100644 index 00000000..70f403ef --- /dev/null +++ b/lib/shared/services/pending_updates_service.dart @@ -0,0 +1,160 @@ +import 'dart:async'; + +import 'package:data_repository/data_repository.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; + +/// Represents the status of a pending update. +enum UpdateStatus { + /// The update has been confirmed and executed. + confirmed, + + /// The update has been successfully undone. + undone, +} + +/// {@template update_event} +/// An event representing a change in the status of a pending update. +/// +/// Contains the ID of the item and its new status. +/// {@endtemplate} +@immutable +class UpdateEvent extends Equatable { + /// {@macro update_event} + const UpdateEvent(this.id, this.status, {this.originalItem}); + + /// The unique identifier of the item. + final String id; + + /// The new status of the update. + final UpdateStatus status; + + /// The original item before the update was requested. + /// This is provided when an update is undone. + final T? originalItem; + + @override + List get props => [id, status, originalItem]; +} + +/// {@template pending_updates_service} +/// An abstract interface for a service that manages pending updates. +/// +/// This service provides a mechanism to request a delayed update of an item, +/// allowing for an "undo" period. +/// {@endtemplate} +abstract class PendingUpdatesService { + /// A stream that emits [UpdateEvent]s when an update is confirmed or undone. + Stream> get updateEvents; + + /// Requests the update of an item of a specific type [T]. + void requestUpdate({ + required T originalItem, + required T updatedItem, + required DataRepository repository, + required Duration undoDuration, + }); + + /// Cancels a pending update for the item with the given [id]. + void undoUpdate(String id); + + /// Disposes of the service's resources. + void dispose(); +} + +/// {@template pending_updates_service_impl} +/// A concrete implementation of [PendingUpdatesService]. +/// {@endtemplate} +class PendingUpdatesServiceImpl implements PendingUpdatesService { + /// {@macro pending_updates_service_impl} + PendingUpdatesServiceImpl({Logger? logger}) + : _logger = logger ?? Logger('PendingUpdatesServiceImpl'); + + final Logger _logger; + final _updateEventController = + StreamController>.broadcast(); + final Map> _pendingUpdateTimers = {}; + + @override + Stream> get updateEvents => + _updateEventController.stream; + + @override + void requestUpdate({ + required T originalItem, + required T updatedItem, + required DataRepository repository, + required Duration undoDuration, + }) { + final id = (updatedItem as dynamic).id as String; + _logger.info('Requesting update for item ID: $id'); + + if (_pendingUpdateTimers.containsKey(id)) { + _logger.info('Cancelling existing pending update for ID: $id'); + _pendingUpdateTimers.remove(id)?.timer.cancel(); + } + + final timer = Timer(undoDuration, () async { + try { + await repository.update(id: id, item: updatedItem); + _logger.info('Update confirmed for item ID: $id'); + _updateEventController.add( + UpdateEvent(id, UpdateStatus.confirmed), + ); + } catch (error) { + _logger.severe('Error confirming update for item ID: $id: $error'); + _updateEventController.addError(error); + } finally { + _pendingUpdateTimers.remove(id); + } + }); + + _pendingUpdateTimers[id] = _PendingUpdate( + timer: timer, + originalItem: originalItem, + ); + } + + @override + void undoUpdate(String id) { + _logger.info('Attempting to undo update for item ID: $id'); + final pendingUpdate = _pendingUpdateTimers.remove(id); + if (pendingUpdate != null) { + pendingUpdate.timer.cancel(); + _logger.info('Update undone for item ID: $id'); + _updateEventController.add( + UpdateEvent( + id, + UpdateStatus.undone, + originalItem: pendingUpdate.originalItem, + ), + ); + } else { + _logger.warning('No pending update found for ID: $id to undo.'); + } + } + + @override + void dispose() { + _logger.info( + 'Disposing PendingUpdatesService. Cancelling ${_pendingUpdateTimers.length} pending timers.', + ); + for (final pendingUpdate in _pendingUpdateTimers.values) { + pendingUpdate.timer.cancel(); + } + _pendingUpdateTimers.clear(); + _updateEventController.close(); + } +} + +@immutable +class _PendingUpdate extends Equatable { + const _PendingUpdate({required this.timer, required this.originalItem}); + + final Timer timer; + final T originalItem; + + @override + List get props => [timer, originalItem]; +} From 3f143494827e4b86cc1cf8b4e08a06a1ac921e45 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 16:37:48 +0100 Subject: [PATCH 115/130] refactor(community_management): update action buttons and remove BLoC - Replace direct navigation with details dialogs for engagements and reports - Add visual indicator for pending review items - Remove unused imports and BLoC related code - Extract app review details dialog to a separate widget - Add copy headline ID functionality --- .../widgets/community_action_buttons.dart | 266 ++++++------------ 1 file changed, 83 insertions(+), 183 deletions(-) diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index 8e89cc1f..8e337870 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -1,12 +1,10 @@ import 'package:core/core.dart'; -import 'package:data_repository/data_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/app_review_details_dialog.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/engagement_details_dialog.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/widgets/report_details_dialog.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/router/routes.dart'; -import 'package:go_router/go_router.dart'; -import 'package:ui_kit/ui_kit.dart'; class CommunityActionButtons extends StatelessWidget { const CommunityActionButtons({ @@ -73,44 +71,54 @@ class CommunityActionButtons extends StatelessWidget { List visibleActions, List> overflowMenuItems, ) { - // Primary Action + final isPendingReview = + engagement.comment != null && + engagement.comment!.status == ModerationStatus.pendingReview; + visibleActions.add( - IconButton( - visualDensity: VisualDensity.compact, - iconSize: 20, - icon: const Icon(Icons.visibility_outlined), - tooltip: l10n.viewEngagedContent, - onPressed: () { - context.goNamed( - Routes.editHeadlineName, - pathParameters: {'id': engagement.entityId}, - ); - }, + Stack( + children: [ + IconButton( + visualDensity: VisualDensity.compact, + iconSize: 20, + icon: const Icon(Icons.more_horiz), + tooltip: l10n.viewFeedbackDetails, + onPressed: () => showDialog( + context: context, + builder: (_) => EngagementDetailsDialog(engagement: engagement), + ), + ), + if (isPendingReview) + Positioned( + top: 8, + right: 8, + child: Tooltip( + message: l10n.moderationStatusPendingReview, + child: const Icon( + Icons.circle, + color: Colors.amber, + size: 10, + ), + ), + ), + ], ), ); // Secondary Actions - if (engagement.comment != null) { - if (engagement.comment!.status != ModerationStatus.resolved) { - overflowMenuItems.add( - PopupMenuItem( - value: 'approveComment', - child: Text(l10n.approveComment), - ), - ); - } - if (engagement.comment != null) { - overflowMenuItems.add( - PopupMenuItem( - value: 'rejectComment', - child: Text(l10n.rejectComment), - ), - ); - } - } - overflowMenuItems.add( - PopupMenuItem(value: 'copyUserId', child: Text(l10n.copyUserId)), - ); + overflowMenuItems + ..add( + PopupMenuItem( + value: 'copyUserId', + child: Text(l10n.copyUserId), + ), + ) + ..add( + PopupMenuItem( + value: 'copyHeadlineId', + child: Text(l10n.copyHeadlineId), + ), + ); } void _buildReportActions( @@ -119,48 +127,39 @@ class CommunityActionButtons extends StatelessWidget { List visibleActions, List> overflowMenuItems, ) { + final isPendingReview = report.status == ModerationStatus.pendingReview; + visibleActions.add( - IconButton( - visualDensity: VisualDensity.compact, - iconSize: 20, - icon: const Icon(Icons.visibility_outlined), - tooltip: switch (report.entityType) { - ReportableEntity.headline => l10n.viewReportedHeadline, - ReportableEntity.source => l10n.viewReportedSource, - ReportableEntity.engagement => l10n.viewReportedComment, - }, - onPressed: () { - final String routeName; - switch (report.entityType) { - case ReportableEntity.headline: - routeName = Routes.editHeadlineName; - case ReportableEntity.source: - routeName = Routes.editSourceName; - case ReportableEntity.engagement: - return; - } - context.goNamed(routeName, pathParameters: {'id': report.entityId}); - }, + Stack( + children: [ + IconButton( + visualDensity: VisualDensity.compact, + iconSize: 20, + icon: const Icon(Icons.more_horiz), + tooltip: l10n.viewFeedbackDetails, + onPressed: () => showDialog( + context: context, + builder: (_) => ReportDetailsDialog(report: report), + ), + ), + if (isPendingReview) + Positioned( + top: 8, + right: 8, + child: Tooltip( + message: l10n.moderationStatusPendingReview, + child: const Icon( + Icons.circle, + color: Colors.amber, + size: 10, + ), + ), + ), + ], ), ); // Secondary Actions - if (report.status != ModerationStatus.pendingReview) { - overflowMenuItems.add( - PopupMenuItem( - value: 'markAsInReview', - child: Text(l10n.moderationStatusPendingReview), - ), - ); - } - if (report.status != ModerationStatus.resolved) { - overflowMenuItems.add( - PopupMenuItem( - value: 'resolveReport', - child: Text(l10n.resolveReport), - ), - ); - } overflowMenuItems ..add( PopupMenuItem( @@ -195,8 +194,7 @@ class CommunityActionButtons extends StatelessWidget { onPressed: hasDetails ? () => showDialog( context: context, - builder: (_) => - _FeedbackDetailsDialog(appReview: appReview, l10n: l10n), + builder: (_) => AppReviewDetailsDialog(appReview: appReview), ) : null, ), @@ -209,9 +207,6 @@ class CommunityActionButtons extends StatelessWidget { } void _onActionSelected(BuildContext context, String value, T item) { - final engagementsRepository = context.read>(); - final reportsRepository = context.read>(); - if (value == 'copyUserId') { String userId; if (item is Engagement) { @@ -235,109 +230,14 @@ class CommunityActionButtons extends StatelessWidget { ..showSnackBar( SnackBar(content: Text(l10n.idCopiedToClipboard(reportedItemId))), ); - } else if (value == 'approveComment' && item is Engagement) { - final updatedEngagement = item.copyWith( - comment: item.comment?.copyWith(status: ModerationStatus.resolved), - ); - engagementsRepository.update( - id: updatedEngagement.id, - item: updatedEngagement, - ); - } else if (value == 'rejectComment' && item is Engagement) { - showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: Text(l10n.rejectComment), - content: Text(l10n.rejectCommentConfirmation), - actions: [ - TextButton( - onPressed: () => Navigator.of(dialogContext).pop(), - child: Text(l10n.cancel), - ), - ElevatedButton( - onPressed: () { - final updatedEngagement = Engagement( - id: item.id, - userId: item.userId, - entityId: item.entityId, - entityType: item.entityType, - reaction: item.reaction, - comment: null, - createdAt: item.createdAt, - updatedAt: DateTime.now(), - ); - engagementsRepository.update( - id: updatedEngagement.id, - item: updatedEngagement, - ); - Navigator.of(dialogContext).pop(); - }, - child: Text(l10n.reject), - ), - ], - ), - ); - } else if (value == 'markAsInReview' && item is Report) { - final updatedReport = item.copyWith( - status: ModerationStatus.pendingReview, - ); - reportsRepository.update( - id: updatedReport.id, - item: updatedReport, - ); - } else if (value == 'resolveReport' && item is Report) { - final updatedReport = item.copyWith(status: ModerationStatus.resolved); - reportsRepository.update( - id: updatedReport.id, - item: updatedReport, - ); + } else if (value == 'copyHeadlineId' && item is Engagement) { + final headlineId = item.entityId; + Clipboard.setData(ClipboardData(text: headlineId)); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar(content: Text(l10n.idCopiedToClipboard(headlineId))), + ); } } } - -class _FeedbackDetailsDialog extends StatelessWidget { - const _FeedbackDetailsDialog({ - required this.appReview, - required this.l10n, - }); - - final AppReview appReview; - final AppLocalizations l10n; - - @override - Widget build(BuildContext context) { - final details = appReview.feedbackDetails; - - return AlertDialog( - title: Text(l10n.feedbackDetails), - content: SizedBox( - width: double.maxFinite, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - l10n.feedbackProvidedAt( - DateFormatter.formatRelativeTime( - context, - appReview.updatedAt, - ), - ), - style: Theme.of(context).textTheme.labelSmall, - ), - const SizedBox(height: AppSpacing.md), - Text(details ?? l10n.noReasonProvided), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(l10n.closeButtonText), - ), - ], - ); - } -} From 911ad6d18650cf0aa9e376203b11303573753c54 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 16:57:21 +0100 Subject: [PATCH 116/130] feat(l10n): add translations for report and comment details - Add Arabic and English translations for "Report Details" and "Comment Details" - Update existing translations for consistency --- lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_ar.dart | 6 ++++++ lib/l10n/app_localizations_en.dart | 6 ++++++ lib/l10n/arb/app_ar.arb | 11 +++++++++-- lib/l10n/arb/app_en.arb | 8 ++++++++ 5 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ac1f1281..77658845 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3937,6 +3937,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'View Feedback Details'** String get viewFeedbackDetails; + + /// Title for the dialog showing the details of a user-submitted report. + /// + /// In en, this message translates to: + /// **'Report Details'** + String get reportDetails; + + /// Title for the dialog showing the details of a user-submitted comment. + /// + /// In en, this message translates to: + /// **'Comment Details'** + String get commentDetails; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index b54f56e6..55943521 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -2109,4 +2109,10 @@ class AppLocalizationsAr extends AppLocalizations { @override String get viewFeedbackDetails => 'عرض تفاصيل التقييم'; + + @override + String get reportDetails => 'تفاصيل البلاغ'; + + @override + String get commentDetails => 'تفاصيل التعليق'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a1cdbb13..b017fb1f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2115,4 +2115,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get viewFeedbackDetails => 'View Feedback Details'; + + @override + String get reportDetails => 'Report Details'; + + @override + String get commentDetails => 'Comment Details'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index fdc49ad1..19da0c27 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2637,7 +2637,7 @@ "@withoutComment": { "description": "خيار تصفية لإظهار العناصر التي لا تحتوي على تعليق فقط." }, - "reportResolved": "تم حل البلاغ.", + "reportResolved": "تم حل البلاغ.", "@reportResolved": {}, "commentApproved": "تمت الموافقة على التعليق.", "@commentApproved": { @@ -2658,6 +2658,13 @@ "viewFeedbackDetails": "عرض تفاصيل التقييم", "@viewFeedbackDetails": { "description": "Tooltip for the button to view feedback details." + }, + "reportDetails": "تفاصيل البلاغ", + "@reportDetails": { + "description": "عنوان مربع الحوار الذي يعرض تفاصيل بلاغ مقدم من المستخدم." + }, + "commentDetails": "تفاصيل التعليق", + "@commentDetails": { + "description": "عنوان مربع الحوار الذي يعرض تفاصيل تعليق مقدم من المستخدم." } - } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index f3e07c2d..84af8bae 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2654,5 +2654,13 @@ "viewFeedbackDetails": "View Feedback Details", "@viewFeedbackDetails": { "description": "Tooltip for the button to view feedback details." + }, + "reportDetails": "Report Details", + "@reportDetails": { + "description": "Title for the dialog showing the details of a user-submitted report." + }, + "commentDetails": "Comment Details", + "@commentDetails": { + "description": "Title for the dialog showing the details of a user-submitted comment." } } \ No newline at end of file From e84756fc74038cf9b393383ebf7094c5b301d109 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 16:58:45 +0100 Subject: [PATCH 117/130] refactor(dialogs): simplify content and update localizations - Remove unused imports and widgets - Replace custom localization methods with AppLocalizations.of(context) - Streamline dialog content and reduce complexity - Update dialog titles to be more specific (e.g., "commentDetails" instead of "engagements") --- .../widgets/app_review_details_dialog.dart | 23 ++-------- .../widgets/engagement_details_dialog.dart | 45 ++++--------------- .../widgets/report_details_dialog.dart | 23 ++-------- 3 files changed, 16 insertions(+), 75 deletions(-) diff --git a/lib/community_management/widgets/app_review_details_dialog.dart b/lib/community_management/widgets/app_review_details_dialog.dart index 9d1bb4e4..60995871 100644 --- a/lib/community_management/widgets/app_review_details_dialog.dart +++ b/lib/community_management/widgets/app_review_details_dialog.dart @@ -1,7 +1,6 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:ui_kit/ui_kit.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; class AppReviewDetailsDialog extends StatelessWidget { const AppReviewDetailsDialog({ @@ -13,7 +12,7 @@ class AppReviewDetailsDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; + final l10n = AppLocalizations.of(context); final details = appReview.feedbackDetails; return AlertDialog( @@ -21,23 +20,7 @@ class AppReviewDetailsDialog extends StatelessWidget { content: SizedBox( width: double.maxFinite, child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - l10n.feedbackProvidedAt( - DateFormatter.formatRelativeTime( - context, - appReview.updatedAt, - ), - ), - style: Theme.of(context).textTheme.labelSmall, - ), - const SizedBox(height: AppSpacing.md), - Text(details ?? l10n.noReasonProvided), - ], - ), + child: Text(details ?? l10n.noReasonProvided), ), ), actions: [ diff --git a/lib/community_management/widgets/engagement_details_dialog.dart b/lib/community_management/widgets/engagement_details_dialog.dart index d444318a..fa63a155 100644 --- a/lib/community_management/widgets/engagement_details_dialog.dart +++ b/lib/community_management/widgets/engagement_details_dialog.dart @@ -3,8 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:intl/intl.dart'; -import 'package:ui_kit/ui_kit.dart'; class EngagementDetailsDialog extends StatelessWidget { const EngagementDetailsDialog({ @@ -25,43 +23,18 @@ class EngagementDetailsDialog extends StatelessWidget { engagement.comment!.status == ModerationStatus.pendingReview; return AlertDialog( - title: Text(l10n.engagements), + title: Text(l10n.commentDetails), content: SizedBox( width: double.maxFinite, child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '${l10n.reaction}: ${engagement.reaction.reactionType.name}', - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: AppSpacing.sm), - Text( - '${l10n.user}: ${engagement.userId}', - style: theme.textTheme.bodySmall, - ), - Text( - '${l10n.date}: ${DateFormat('dd-MM-yyyy HH:mm').format(engagement.createdAt.toLocal())}', - style: theme.textTheme.bodySmall, - ), - const Divider(height: AppSpacing.lg), - Text( - l10n.comment, - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: AppSpacing.sm), - Text( - engagement.comment?.content ?? l10n.noReasonProvided, - style: hasComment - ? theme.textTheme.bodyLarge - : theme.textTheme.bodyLarge?.copyWith( - fontStyle: FontStyle.italic, - color: theme.colorScheme.onSurface.withOpacity(0.6), - ), - ), - ], + child: Text( + engagement.comment?.content ?? l10n.noReasonProvided, + style: hasComment + ? theme.textTheme.bodyLarge + : theme.textTheme.bodyLarge?.copyWith( + fontStyle: FontStyle.italic, + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), ), ), ), diff --git a/lib/community_management/widgets/report_details_dialog.dart b/lib/community_management/widgets/report_details_dialog.dart index 4d36a641..5e70933b 100644 --- a/lib/community_management/widgets/report_details_dialog.dart +++ b/lib/community_management/widgets/report_details_dialog.dart @@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/extensions.dart'; -import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; class ReportDetailsDialog extends StatelessWidget { @@ -23,7 +22,7 @@ class ReportDetailsDialog extends StatelessWidget { final isPendingReview = report.status == ModerationStatus.pendingReview; return AlertDialog( - title: Text(l10n.reports), + title: Text(l10n.reportDetails), content: SizedBox( width: double.maxFinite, child: SingleChildScrollView( @@ -31,20 +30,6 @@ class ReportDetailsDialog extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text( - '${l10n.reportedItem}: ${report.entityType.l10n(context)}', - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: AppSpacing.sm), - Text( - '${l10n.reporter}: ${report.reporterUserId}', - style: theme.textTheme.bodySmall, - ), - Text( - '${l10n.date}: ${DateFormat('dd-MM-yyyy HH:mm').format(report.createdAt.toLocal())}', - style: theme.textTheme.bodySmall, - ), - const Divider(height: AppSpacing.lg), Text( '${l10n.reason}: ${report.reason.l10n(context)}', style: theme.textTheme.titleMedium, @@ -75,8 +60,8 @@ class ReportDetailsDialog extends StatelessWidget { ElevatedButton( onPressed: () { context.read().add( - ResolveReportRequested(report.id), - ); + ResolveReportRequested(report.id), + ); Navigator.of(context).pop(); }, child: Text(l10n.resolveReport), @@ -84,4 +69,4 @@ class ReportDetailsDialog extends StatelessWidget { ], ); } -} \ No newline at end of file +} From 1879103451064a3bc8d0684308e907e1b197551d Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 16:58:58 +0100 Subject: [PATCH 118/130] feat(community_management): replace more_horiz icon with comment_outlined - Update icon in CommunityActionButtons widget from more_horiz to comment_outlined - Apply change to both instances of IconButton in the widget --- .../widgets/community_action_buttons.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index 8e337870..3dcc9cb7 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -81,7 +81,7 @@ class CommunityActionButtons extends StatelessWidget { IconButton( visualDensity: VisualDensity.compact, iconSize: 20, - icon: const Icon(Icons.more_horiz), + icon: const Icon(Icons.comment_outlined), tooltip: l10n.viewFeedbackDetails, onPressed: () => showDialog( context: context, @@ -135,7 +135,7 @@ class CommunityActionButtons extends StatelessWidget { IconButton( visualDensity: VisualDensity.compact, iconSize: 20, - icon: const Icon(Icons.more_horiz), + icon: const Icon(Icons.comment_outlined), tooltip: l10n.viewFeedbackDetails, onPressed: () => showDialog( context: context, From 477e29dfcf2b1491049f21b46a542e346f5a3a51 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 17:05:02 +0100 Subject: [PATCH 119/130] style: add background color to entity type chip - Add transparent background color to entity type chip in reports page - Remove chip border for a cleaner look - Improve visual consistency with theme color scheme --- lib/community_management/view/reports_page.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index 38906ae1..37c091ff 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -195,6 +195,9 @@ class _ReportsDataSource extends DataTableSource { DataCell( Chip( label: Text(report.entityType.l10n(context)), + backgroundColor: + Theme.of(context).colorScheme.tertiaryContainer.withOpacity(0.5), + side: BorderSide.none, visualDensity: VisualDensity.compact, ), ), From 7bfba8634c0f850299d27edbd75a8b7f64d780f7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 17:06:45 +0100 Subject: [PATCH 120/130] feat(community_management): add background colors to reaction chips - Implement _getReactionColor function to assign colors based on reaction type - Update Chip widget to use the new background color function - Add variety of colors using Theme colorScheme and custom opacity values --- .../view/engagements_page.dart | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/community_management/view/engagements_page.dart b/lib/community_management/view/engagements_page.dart index 0f129e6c..5abf789f 100644 --- a/lib/community_management/view/engagements_page.dart +++ b/lib/community_management/view/engagements_page.dart @@ -200,6 +200,11 @@ class _EngagementsDataSource extends DataTableSource { DataCell( Chip( label: Text(engagement.reaction.reactionType.name), + backgroundColor: _getReactionColor( + context, + engagement.reaction.reactionType, + ), + side: BorderSide.none, visualDensity: VisualDensity.compact, ), ), @@ -219,4 +224,22 @@ class _EngagementsDataSource extends DataTableSource { @override int get selectedRowCount => 0; + + Color? _getReactionColor(BuildContext context, ReactionType reactionType) { + final colorScheme = Theme.of(context).colorScheme; + switch (reactionType) { + case ReactionType.like: + return colorScheme.primaryContainer.withOpacity(0.5); + case ReactionType.insightful: + return colorScheme.tertiaryContainer.withOpacity(0.5); + case ReactionType.amusing: + return colorScheme.secondaryContainer.withOpacity(0.5); + case ReactionType.sad: + return Colors.blueGrey.withOpacity(0.2); + case ReactionType.angry: + return colorScheme.errorContainer.withOpacity(0.4); + case ReactionType.skeptical: + return colorScheme.onSurface.withOpacity(0.1); + } + } } From e6677967279789392c893b12469285769ce87ab4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 17:08:14 +0100 Subject: [PATCH 121/130] feat(ui): enhance visual distinction of report entity types - Replace uniform chip background color with entity-specific colors - Implement _getEntityTypeColor function to map entity types to colors - Update chip design to improve visual distinction across different report entity types --- .../view/reports_page.dart | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/community_management/view/reports_page.dart b/lib/community_management/view/reports_page.dart index 37c091ff..93af6eeb 100644 --- a/lib/community_management/view/reports_page.dart +++ b/lib/community_management/view/reports_page.dart @@ -195,8 +195,7 @@ class _ReportsDataSource extends DataTableSource { DataCell( Chip( label: Text(report.entityType.l10n(context)), - backgroundColor: - Theme.of(context).colorScheme.tertiaryContainer.withOpacity(0.5), + backgroundColor: _getEntityTypeColor(context, report.entityType), side: BorderSide.none, visualDensity: VisualDensity.compact, ), @@ -217,4 +216,19 @@ class _ReportsDataSource extends DataTableSource { @override int get selectedRowCount => 0; + + Color? _getEntityTypeColor( + BuildContext context, + ReportableEntity entityType, + ) { + final colorScheme = Theme.of(context).colorScheme; + switch (entityType) { + case ReportableEntity.headline: + return colorScheme.primaryContainer.withOpacity(0.5); + case ReportableEntity.source: + return colorScheme.secondaryContainer.withOpacity(0.5); + case ReportableEntity.engagement: + return colorScheme.tertiaryContainer.withOpacity(0.5); + } + } } From c9cf9e310d899b39c5f7bdd10c3c6543a680aef6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 17:40:28 +0100 Subject: [PATCH 122/130] feat(dialog): improve layout and styling of detail dialogs - Add max width and height constraints to dialog content - Enhance styling for no reason provided text - Adjust font styles for report details --- .../widgets/app_review_details_dialog.dart | 15 ++++++++++++--- .../widgets/engagement_details_dialog.dart | 4 ++-- .../widgets/report_details_dialog.dart | 10 ++++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/community_management/widgets/app_review_details_dialog.dart b/lib/community_management/widgets/app_review_details_dialog.dart index 60995871..87288b4d 100644 --- a/lib/community_management/widgets/app_review_details_dialog.dart +++ b/lib/community_management/widgets/app_review_details_dialog.dart @@ -13,14 +13,23 @@ class AppReviewDetailsDialog extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); + final theme = Theme.of(context); final details = appReview.feedbackDetails; return AlertDialog( title: Text(l10n.feedbackDetails), - content: SizedBox( - width: double.maxFinite, + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 300), child: SingleChildScrollView( - child: Text(details ?? l10n.noReasonProvided), + child: Text( + details ?? l10n.noReasonProvided, + style: (details == null) + ? theme.textTheme.bodyLarge?.copyWith( + fontStyle: FontStyle.italic, + color: theme.colorScheme.onSurface.withOpacity(0.6), + ) + : theme.textTheme.bodyLarge, + ), ), ), actions: [ diff --git a/lib/community_management/widgets/engagement_details_dialog.dart b/lib/community_management/widgets/engagement_details_dialog.dart index fa63a155..82282abc 100644 --- a/lib/community_management/widgets/engagement_details_dialog.dart +++ b/lib/community_management/widgets/engagement_details_dialog.dart @@ -24,8 +24,8 @@ class EngagementDetailsDialog extends StatelessWidget { return AlertDialog( title: Text(l10n.commentDetails), - content: SizedBox( - width: double.maxFinite, + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 300), child: SingleChildScrollView( child: Text( engagement.comment?.content ?? l10n.noReasonProvided, diff --git a/lib/community_management/widgets/report_details_dialog.dart b/lib/community_management/widgets/report_details_dialog.dart index 5e70933b..83f533f8 100644 --- a/lib/community_management/widgets/report_details_dialog.dart +++ b/lib/community_management/widgets/report_details_dialog.dart @@ -23,8 +23,8 @@ class ReportDetailsDialog extends StatelessWidget { return AlertDialog( title: Text(l10n.reportDetails), - content: SizedBox( - width: double.maxFinite, + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 300), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -32,14 +32,16 @@ class ReportDetailsDialog extends StatelessWidget { children: [ Text( '${l10n.reason}: ${report.reason.l10n(context)}', - style: theme.textTheme.titleMedium, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), ), if (report.additionalComments != null && report.additionalComments!.isNotEmpty) ...[ const SizedBox(height: AppSpacing.md), Text( l10n.comment, - style: theme.textTheme.titleSmall, + style: theme.textTheme.labelMedium, ), const SizedBox(height: AppSpacing.xs), Text( From b7660d9ea42de7117bbf7e1bb5e86b983c2408d8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 17:40:36 +0100 Subject: [PATCH 123/130] fix(communities): make feedback details button always clickable - Remove check for non-null and non-empty feedback details - Ensure the button is always enabled, providing a consistent user experience - Improve accessibility by allowing users to always access the feedback details dialog --- .../widgets/community_action_buttons.dart | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/community_management/widgets/community_action_buttons.dart b/lib/community_management/widgets/community_action_buttons.dart index 3dcc9cb7..64c1b812 100644 --- a/lib/community_management/widgets/community_action_buttons.dart +++ b/lib/community_management/widgets/community_action_buttons.dart @@ -182,21 +182,16 @@ class CommunityActionButtons extends StatelessWidget { List> overflowMenuItems, ) { // Primary Action - final hasDetails = - appReview.feedbackDetails != null && - appReview.feedbackDetails!.isNotEmpty; visibleActions.add( IconButton( visualDensity: VisualDensity.compact, iconSize: 20, icon: const Icon(Icons.comment_outlined), tooltip: l10n.viewFeedbackDetails, - onPressed: hasDetails - ? () => showDialog( - context: context, - builder: (_) => AppReviewDetailsDialog(appReview: appReview), - ) - : null, + onPressed: () => showDialog( + context: context, + builder: (_) => AppReviewDetailsDialog(appReview: appReview), + ), ), ); From 5bcf2084fc5f97e568790fdd6065d2d64fe49757 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 17:53:22 +0100 Subject: [PATCH 124/130] refactor(l10n): improve community management localization - Update translations for community management-related strings - Remove redundant entries for community management label - Add new localized strings for navigation items in content, user, and community management --- lib/l10n/app_localizations.dart | 30 ++++++++++++++++++++++++------ lib/l10n/app_localizations_ar.dart | 15 ++++++++++++--- lib/l10n/app_localizations_en.dart | 15 ++++++++++++--- lib/l10n/arb/app_ar.arb | 20 ++++++++++++++++---- lib/l10n/arb/app_en.arb | 20 ++++++++++++++++---- 5 files changed, 80 insertions(+), 20 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 77658845..2d729a3f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3344,12 +3344,6 @@ abstract class AppLocalizations { /// **'Globally activates or deactivates all community-related functionality, including engagement and reporting.'** String get enableCommunityFeaturesDescription; - /// Label for the community management navigation item - /// - /// In en, this message translates to: - /// **'Community'** - String get communityManagement; - /// Description for the Community Management page /// /// In en, this message translates to: @@ -3949,6 +3943,30 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Comment Details'** String get commentDetails; + + /// Label for the Community Management page title and navigation. + /// + /// In en, this message translates to: + /// **'Community Management'** + String get communityManagement; + + /// Short navigation label for Content Management. + /// + /// In en, this message translates to: + /// **'Content'** + String get navContent; + + /// Short navigation label for User Management. + /// + /// In en, this message translates to: + /// **'Users'** + String get navUsers; + + /// Short navigation label for Community Management. + /// + /// In en, this message translates to: + /// **'Community'** + String get navCommunity; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 55943521..d34a8ec4 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1803,9 +1803,6 @@ class AppLocalizationsAr extends AppLocalizations { String get enableCommunityFeaturesDescription => 'ينشط أو يعطل عالميًا جميع الوظائف المتعلقة بالمجتمع، بما في ذلك المشاركة والإبلاغ.'; - @override - String get communityManagement => 'المجتمع'; - @override String get communityManagementPageDescription => 'إدارة المحتوى الذي ينشئه المستخدمون بما في ذلك التفاعلات (ردود الفعل والتعليقات) وتبليغات المحتوى ومراجعات التطبيق.'; @@ -2115,4 +2112,16 @@ class AppLocalizationsAr extends AppLocalizations { @override String get commentDetails => 'تفاصيل التعليق'; + + @override + String get communityManagement => 'إدارة المجتمع'; + + @override + String get navContent => 'المحتوى'; + + @override + String get navUsers => 'المستخدمون'; + + @override + String get navCommunity => 'المجتمع'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b017fb1f..3c5adaf9 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1808,9 +1808,6 @@ class AppLocalizationsEn extends AppLocalizations { String get enableCommunityFeaturesDescription => 'Globally activates or deactivates all community-related functionality, including engagement and reporting.'; - @override - String get communityManagement => 'Community'; - @override String get communityManagementPageDescription => 'Manage user-generated content including engagements (reactions and comments), content reports, and app reviews.'; @@ -2121,4 +2118,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get commentDetails => 'Comment Details'; + + @override + String get communityManagement => 'Community Management'; + + @override + String get navContent => 'Content'; + + @override + String get navUsers => 'Users'; + + @override + String get navCommunity => 'Community'; } diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 19da0c27..c1651171 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2250,10 +2250,6 @@ "@enableCommunityFeaturesDescription": { "description": "وصف المفتاح الرئيسي لتفعيل جميع ميزات المجتمع." }, - "communityManagement": "المجتمع", - "@communityManagement": { - "description": "تسمية عنصر التنقل لإدارة المجتمع" - }, "communityManagementPageDescription": "إدارة المحتوى الذي ينشئه المستخدمون بما في ذلك التفاعلات (ردود الفعل والتعليقات) وتبليغات المحتوى ومراجعات التطبيق.", "@communityManagementPageDescription": { "description": "وصف صفحة إدارة المجتمع" @@ -2666,5 +2662,21 @@ "commentDetails": "تفاصيل التعليق", "@commentDetails": { "description": "عنوان مربع الحوار الذي يعرض تفاصيل تعليق مقدم من المستخدم." + }, + "communityManagement": "إدارة المجتمع", + "@communityManagement": { + "description": "تسمية لعنوان صفحة إدارة المجتمع والتنقل." + }, + "navContent": "المحتوى", + "@navContent": { + "description": "تسمية تنقل قصيرة لإدارة المحتوى." + }, + "navUsers": "المستخدمون", + "@navUsers": { + "description": "تسمية تنقل قصيرة لإدارة المستخدمين." + }, + "navCommunity": "المجتمع", + "@navCommunity": { + "description": "تسمية تنقل قصيرة لإدارة المجتمع." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 84af8bae..8c046670 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2246,10 +2246,6 @@ "@enableCommunityFeaturesDescription": { "description": "Description for the master switch to enable all community features." }, - "communityManagement": "Community", - "@communityManagement": { - "description": "Label for the community management navigation item" - }, "communityManagementPageDescription": "Manage user-generated content including engagements (reactions and comments), content reports, and app reviews.", "@communityManagementPageDescription": { "description": "Description for the Community Management page" @@ -2662,5 +2658,21 @@ "commentDetails": "Comment Details", "@commentDetails": { "description": "Title for the dialog showing the details of a user-submitted comment." + }, + "communityManagement": "Community Management", + "@communityManagement": { + "description": "Label for the Community Management page title and navigation." + }, + "navContent": "Content", + "@navContent": { + "description": "Short navigation label for Content Management." + }, + "navUsers": "Users", + "@navUsers": { + "description": "Short navigation label for User Management." + }, + "navCommunity": "Community", + "@navCommunity": { + "description": "Short navigation label for Community Management." } } \ No newline at end of file From eb9699ec37bab7fba0fa9963d7b7879cc55fc942 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 17:53:32 +0100 Subject: [PATCH 125/130] fix(localization): update navigation label localization - Replace contentManagement with navContent - Replace userManagement with navUsers - Replace communityManagement with navCommunity --- lib/app/view/app_shell.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/app/view/app_shell.dart b/lib/app/view/app_shell.dart index d3bbb031..156ef59e 100644 --- a/lib/app/view/app_shell.dart +++ b/lib/app/view/app_shell.dart @@ -44,17 +44,17 @@ class AppShell extends StatelessWidget { NavigationDestination( icon: const Icon(Icons.folder_open_outlined), selectedIcon: const Icon(Icons.folder), - label: l10n.contentManagement, + label: l10n.navContent, ), NavigationDestination( icon: const Icon(Icons.people_outline), selectedIcon: const Icon(Icons.people), - label: l10n.userManagement, + label: l10n.navUsers, ), NavigationDestination( icon: const Icon(Icons.forum_outlined), selectedIcon: const Icon(Icons.forum), - label: l10n.communityManagement, + label: l10n.navCommunity, ), NavigationDestination( icon: const Icon(Icons.settings_applications_outlined), From b7a74e918add654e43c01262720ec5dccb83de44 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 18:03:08 +0100 Subject: [PATCH 126/130] feat(README): add community & moderation control section - Introduce a new section detailing comprehensive moderation capabilities - Highlight unified content review, streamlined moderation workflow, and direct user insight features - Emphasize the advantages of fostering a positive community, protecting brand, and gathering user insights - --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 2335e7b0..2d93376f 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,18 @@ Effortlessly manage your entire user base with a dedicated user management syste +
+💬 Community & Moderation Control + +### 💬 Comprehensive Moderation Hub +Directly manage all user-generated content from a centralized command center. Review, moderate, and act on user interactions to maintain a healthy and constructive community environment. +- **Unified Content Review:** Seamlessly moderate all incoming user engagements (reactions and comments), content reports, and app review feedback from a single, intuitive interface. +- **Streamlined Moderation Workflow:** Quickly approve or reject comments, resolve user-submitted reports, and analyze feedback with a consistent set of tools designed for rapid decision-making. +- **Direct User Insight:** Gain a clear, unfiltered view of user sentiment, content issues, and overall satisfaction by directly engaging with their feedback and reports. +> **Your Advantage:** Foster a positive community, protect your brand by quickly addressing problematic content, and gather valuable user insights to improve your content strategy, all from one integrated hub. + +
+
⚙️ App Monetization & Remote Control @@ -62,6 +74,7 @@ Dynamically control the mobile app's behavior and operational state directly fro - **Critical State Management:** Instantly activate a maintenance mode or enforce a mandatory app update for your users to handle operational issues or critical releases gracefully. - **Dynamic In-App Content:** Remotely manage the visibility and behavior of in-feed promotional prompts and user engagement elements. - **Tier-Based Feature Gating:** Define and enforce feature limits based on user roles, such as setting the maximum number of followed topics or saved headlines for different subscription levels. +- **Full Community Feature Control:** Remotely enable or disable the entire user engagement system (reactions, comments), the content reporting feature, and the in-app review funnel. Fine-tune engagement modes and configure rules for when and how users are prompted for feedback. - **Global Notification Control:** Remotely enable or disable the entire push notification system, switch between providers (e.g., Firebase, OneSignal), and toggle specific delivery types like breaking news or daily digests. > **Your Advantage:** Gain unparalleled agility to manage your live application. Ensure service stability, drive user actions, and configure business rules instantly, all from a centralized control panel. From f87e940af87dae909e4cd1d21c8e25615c868cf5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 18:05:15 +0100 Subject: [PATCH 127/130] style: format --- .../community_filter_state.dart | 17 +- .../bloc/community_management_bloc.dart | 61 ++++---- .../bloc/community_management_state.dart | 34 ++-- .../view/app_reviews_page.dart | 65 ++++---- .../community_filter_dialog.dart | 148 +++++++++++------- 5 files changed, 184 insertions(+), 141 deletions(-) diff --git a/lib/community_management/bloc/community_filter/community_filter_state.dart b/lib/community_management/bloc/community_filter/community_filter_state.dart index 0803594f..e973575d 100644 --- a/lib/community_management/bloc/community_filter/community_filter_state.dart +++ b/lib/community_management/bloc/community_filter/community_filter_state.dart @@ -34,8 +34,11 @@ class ReportsFilter extends Equatable { selectedReportableEntity != null; @override - List get props => - [searchQuery, selectedStatus, selectedReportableEntity]; + List get props => [ + searchQuery, + selectedStatus, + selectedReportableEntity, + ]; } class AppReviewsFilter extends Equatable { @@ -84,9 +87,9 @@ class CommunityFilterState extends Equatable { @override List get props => [ - engagementsFilter, - reportsFilter, - appReviewsFilter, - version, - ]; + engagementsFilter, + reportsFilter, + appReviewsFilter, + version, + ]; } diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 608ef1e4..03b9ef54 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -21,13 +21,13 @@ class CommunityManagementBloc required CommunityFilterBloc communityFilterBloc, required PendingUpdatesService pendingUpdatesService, Logger? logger, - }) : _engagementsRepository = engagementsRepository, - _reportsRepository = reportsRepository, - _appReviewsRepository = appReviewsRepository, - _communityFilterBloc = communityFilterBloc, - _pendingUpdatesService = pendingUpdatesService, - _logger = logger ?? Logger('CommunityManagementBloc'), - super(const CommunityManagementState()) { + }) : _engagementsRepository = engagementsRepository, + _reportsRepository = reportsRepository, + _appReviewsRepository = appReviewsRepository, + _communityFilterBloc = communityFilterBloc, + _pendingUpdatesService = pendingUpdatesService, + _logger = logger ?? Logger('CommunityManagementBloc'), + super(const CommunityManagementState()) { on(_onTabChanged); on(_onLoadEngagementsRequested); on(_onLoadReportsRequested); @@ -38,31 +38,34 @@ class CommunityManagementBloc on(_onUndoUpdateRequested); on(_onUpdateEventReceived); - _engagementsUpdateSubscription = - _engagementsRepository.entityUpdated.listen((_) { - _logger.info('Engagement updated, reloading engagements list.'); - add( - LoadEngagementsRequested( - filter: buildEngagementsFilterMap( - _communityFilterBloc.state.engagementsFilter, - ), - forceRefresh: true, - ), - ); - }); + _engagementsUpdateSubscription = _engagementsRepository.entityUpdated + .listen((_) { + _logger.info('Engagement updated, reloading engagements list.'); + add( + LoadEngagementsRequested( + filter: buildEngagementsFilterMap( + _communityFilterBloc.state.engagementsFilter, + ), + forceRefresh: true, + ), + ); + }); _reportsUpdateSubscription = _reportsRepository.entityUpdated.listen((_) { _logger.info('Report updated, reloading reports list.'); add( LoadReportsRequested( - filter: buildReportsFilterMap(_communityFilterBloc.state.reportsFilter), + filter: buildReportsFilterMap( + _communityFilterBloc.state.reportsFilter, + ), forceRefresh: true, ), ); }); - _appReviewsUpdateSubscription = - _appReviewsRepository.entityUpdated.listen((_) { + _appReviewsUpdateSubscription = _appReviewsRepository.entityUpdated.listen(( + _, + ) { _logger.info('AppReview updated, reloading app reviews list.'); add( LoadAppReviewsRequested( @@ -74,8 +77,7 @@ class CommunityManagementBloc ); }); - _updateEventsSubscription = - _pendingUpdatesService.updateEvents.listen( + _updateEventsSubscription = _pendingUpdatesService.updateEvents.listen( (event) => add(UpdateEventReceived(event)), ); } @@ -415,18 +417,17 @@ class CommunityManagementBloc case UpdateStatus.undone: final item = event.event.originalItem; if (item is Engagement) { - final index = - state.engagements.indexWhere((e) => e.id == item.id); + final index = state.engagements.indexWhere((e) => e.id == item.id); if (index != -1) { - final updatedEngagements = - List.from(state.engagements)..[index] = item; + final updatedEngagements = List.from(state.engagements) + ..[index] = item; emit(state.copyWith(engagements: updatedEngagements)); } } else if (item is Report) { final index = state.reports.indexWhere((r) => r.id == item.id); if (index != -1) { - final updatedReports = - List.from(state.reports)..[index] = item; + final updatedReports = List.from(state.reports) + ..[index] = item; emit(state.copyWith(reports: updatedReports)); } } diff --git a/lib/community_management/bloc/community_management_state.dart b/lib/community_management/bloc/community_management_state.dart index 580bb939..0fc44187 100644 --- a/lib/community_management/bloc/community_management_state.dart +++ b/lib/community_management/bloc/community_management_state.dart @@ -90,21 +90,21 @@ class CommunityManagementState extends Equatable { @override List get props => [ - activeTab, - engagementsStatus, - reportsStatus, - appReviewsStatus, - engagements, - reports, - appReviews, - engagementsCursor, - reportsCursor, - appReviewsCursor, - hasMoreEngagements, - hasMoreReports, - hasMoreAppReviews, - exception, - lastPendingUpdateId, - snackbarMessage, - ]; + activeTab, + engagementsStatus, + reportsStatus, + appReviewsStatus, + engagements, + reports, + appReviews, + engagementsCursor, + reportsCursor, + appReviewsCursor, + hasMoreEngagements, + hasMoreReports, + hasMoreAppReviews, + exception, + lastPendingUpdateId, + snackbarMessage, + ]; } diff --git a/lib/community_management/view/app_reviews_page.dart b/lib/community_management/view/app_reviews_page.dart index ba0fdd48..241d099e 100644 --- a/lib/community_management/view/app_reviews_page.dart +++ b/lib/community_management/view/app_reviews_page.dart @@ -23,13 +23,15 @@ class _AppReviewsPageState extends State { void initState() { super.initState(); context.read().add( - LoadAppReviewsRequested( - limit: kDefaultRowsPerPage, - filter: context.read().buildAppReviewsFilterMap( - context.read().state.appReviewsFilter, - ), - ), - ); + LoadAppReviewsRequested( + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildAppReviewsFilterMap( + context.read().state.appReviewsFilter, + ), + ), + ); } @override @@ -58,15 +60,19 @@ class _AppReviewsPageState extends State { return FailureStateWidget( exception: state.exception!, onRetry: () => context.read().add( - LoadAppReviewsRequested( - limit: kDefaultRowsPerPage, - forceRefresh: true, - filter: context - .read() - .buildAppReviewsFilterMap( - context.read().state.appReviewsFilter), - ), - ), + LoadAppReviewsRequested( + limit: kDefaultRowsPerPage, + forceRefresh: true, + filter: context + .read() + .buildAppReviewsFilterMap( + context + .read() + .state + .appReviewsFilter, + ), + ), + ), ); } @@ -84,8 +90,8 @@ class _AppReviewsPageState extends State { const SizedBox(height: AppSpacing.lg), ElevatedButton( onPressed: () => context.read().add( - const CommunityFilterReset(), - ), + const CommunityFilterReset(), + ), child: Text(l10n.resetFiltersButtonText), ), ], @@ -135,16 +141,19 @@ class _AppReviewsPageState extends State { state.appReviewsStatus != CommunityManagementStatus.loading) { context.read().add( - LoadAppReviewsRequested( - startAfterId: state.appReviewsCursor, - limit: kDefaultRowsPerPage, - filter: context - .read() - .buildAppReviewsFilterMap( - context.read().state.appReviewsFilter, - ), - ), - ); + LoadAppReviewsRequested( + startAfterId: state.appReviewsCursor, + limit: kDefaultRowsPerPage, + filter: context + .read() + .buildAppReviewsFilterMap( + context + .read() + .state + .appReviewsFilter, + ), + ), + ); } }, empty: Center(child: Text(l10n.noAppReviewsFound)), diff --git a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart index b39480c5..cfa99379 100644 --- a/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart +++ b/lib/community_management/widgets/community_filter_dialog/community_filter_dialog.dart @@ -24,11 +24,11 @@ class _CommunityFilterDialogState extends State { super.initState(); _searchController = TextEditingController(); final filterState = context.read().state; - final activeTab = - context.read().state.activeTab; + final activeTab = context.read().state.activeTab; switch (activeTab) { case CommunityManagementTab.engagements: - _searchController.text = filterState.engagementsFilter.searchQuery ?? ''; + _searchController.text = + filterState.engagementsFilter.searchQuery ?? ''; case CommunityManagementTab.reports: _searchController.text = filterState.reportsFilter.searchQuery ?? ''; case CommunityManagementTab.appReviews: @@ -46,8 +46,10 @@ class _CommunityFilterDialogState extends State { Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; final theme = Theme.of(context); - final activeTab = context.select((bloc) => bloc.state.activeTab); + final activeTab = context + .select( + (bloc) => bloc.state.activeTab, + ); return BlocBuilder( builder: (context, filterState) { @@ -68,9 +70,12 @@ class _CommunityFilterDialogState extends State { icon: const Icon(Icons.refresh), tooltip: l10n.resetFiltersButtonText, onPressed: () { - final filterBloc = context.read(); - filterBloc.add(const CommunityFilterReset()); - _dispatchFilterChanges(filterBloc, const CommunityFilterState()); + final filterBloc = context.read() + ..add(const CommunityFilterReset()); + _dispatchFilterChanges( + filterBloc, + const CommunityFilterState(), + ); filterBloc.add(const CommunityFilterApplied()); Navigator.of(context).pop(); }, @@ -79,9 +84,9 @@ class _CommunityFilterDialogState extends State { icon: const Icon(Icons.check), tooltip: l10n.applyFilters, onPressed: () { - context - .read() - .add(const CommunityFilterApplied()); + context.read().add( + const CommunityFilterApplied(), + ); Navigator.of(context).pop(); }, ), @@ -159,11 +164,34 @@ class _CommunityFilterDialogState extends State { final state = bloc.state; switch (activeTab) { case CommunityManagementTab.engagements: - bloc.add(EngagementsFilterChanged(EngagementsFilter(searchQuery: query, selectedStatus: state.engagementsFilter.selectedStatus))); + bloc.add( + EngagementsFilterChanged( + EngagementsFilter( + searchQuery: query, + selectedStatus: state.engagementsFilter.selectedStatus, + ), + ), + ); case CommunityManagementTab.reports: - bloc.add(ReportsFilterChanged(ReportsFilter(searchQuery: query, selectedStatus: state.reportsFilter.selectedStatus, selectedReportableEntity: state.reportsFilter.selectedReportableEntity))); + bloc.add( + ReportsFilterChanged( + ReportsFilter( + searchQuery: query, + selectedStatus: state.reportsFilter.selectedStatus, + selectedReportableEntity: + state.reportsFilter.selectedReportableEntity, + ), + ), + ); case CommunityManagementTab.appReviews: - bloc.add(AppReviewsFilterChanged(AppReviewsFilter(searchQuery: query, selectedFeedback: state.appReviewsFilter.selectedFeedback))); + bloc.add( + AppReviewsFilterChanged( + AppReviewsFilter( + searchQuery: query, + selectedFeedback: state.appReviewsFilter.selectedFeedback, + ), + ), + ); } } @@ -220,20 +248,21 @@ class _CommunityFilterDialogState extends State { case CommunityManagementTab.engagements: return [ _buildCapsuleFilter( - title: l10n.status, - allValues: ModerationStatus.values, - selectedValue: state.engagementsFilter.selectedStatus, - labelBuilder: (item) => item.l10n(context), - onChanged: (item) { - context.read().add( - EngagementsFilterChanged( - EngagementsFilter( - searchQuery: state.engagementsFilter.searchQuery, - selectedStatus: item, - ), - ), - ); - }), + title: l10n.status, + allValues: ModerationStatus.values, + selectedValue: state.engagementsFilter.selectedStatus, + labelBuilder: (item) => item.l10n(context), + onChanged: (item) { + context.read().add( + EngagementsFilterChanged( + EngagementsFilter( + searchQuery: state.engagementsFilter.searchQuery, + selectedStatus: item, + ), + ), + ); + }, + ), ]; case CommunityManagementTab.reports: return [ @@ -244,15 +273,15 @@ class _CommunityFilterDialogState extends State { labelBuilder: (item) => item.l10n(context), onChanged: (item) { context.read().add( - ReportsFilterChanged( - ReportsFilter( - searchQuery: state.reportsFilter.searchQuery, - selectedStatus: item, - selectedReportableEntity: - state.reportsFilter.selectedReportableEntity, - ), - ), - ); + ReportsFilterChanged( + ReportsFilter( + searchQuery: state.reportsFilter.searchQuery, + selectedStatus: item, + selectedReportableEntity: + state.reportsFilter.selectedReportableEntity, + ), + ), + ); }, ), const SizedBox(height: AppSpacing.lg), @@ -263,34 +292,35 @@ class _CommunityFilterDialogState extends State { labelBuilder: (item) => item.l10n(context), onChanged: (item) { context.read().add( - ReportsFilterChanged( - ReportsFilter( - searchQuery: state.reportsFilter.searchQuery, - selectedStatus: state.reportsFilter.selectedStatus, - selectedReportableEntity: item, - ), - ), - ); + ReportsFilterChanged( + ReportsFilter( + searchQuery: state.reportsFilter.searchQuery, + selectedStatus: state.reportsFilter.selectedStatus, + selectedReportableEntity: item, + ), + ), + ); }, ), ]; case CommunityManagementTab.appReviews: return [ _buildCapsuleFilter( - title: l10n.initialFeedback, - allValues: AppReviewFeedback.values, - selectedValue: state.appReviewsFilter.selectedFeedback, - labelBuilder: (item) => item.l10n(context), - onChanged: (item) { - context.read().add( - AppReviewsFilterChanged( - AppReviewsFilter( - searchQuery: state.appReviewsFilter.searchQuery, - selectedFeedback: item, - ), - ), - ); - }), + title: l10n.initialFeedback, + allValues: AppReviewFeedback.values, + selectedValue: state.appReviewsFilter.selectedFeedback, + labelBuilder: (item) => item.l10n(context), + onChanged: (item) { + context.read().add( + AppReviewsFilterChanged( + AppReviewsFilter( + searchQuery: state.appReviewsFilter.searchQuery, + selectedFeedback: item, + ), + ), + ); + }, + ), ]; } } From 952bc6e4773d88d99f5e975626d4fa37b394aead Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 18:18:42 +0100 Subject: [PATCH 128/130] fix(community_management): handle null pending update id - Update state more granularly when receiving remote changes - Ensure lastPendingUpdateId and snackbarMessage are reset in all cases - Improve code readability with more specific state updates --- .../bloc/community_management_bloc.dart | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 03b9ef54..83f085c2 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -421,22 +421,40 @@ class CommunityManagementBloc if (index != -1) { final updatedEngagements = List.from(state.engagements) ..[index] = item; - emit(state.copyWith(engagements: updatedEngagements)); + emit( + state.copyWith( + engagements: updatedEngagements, + lastPendingUpdateId: null, + snackbarMessage: null, + ), + ); + } else { + emit( + state.copyWith(lastPendingUpdateId: null, snackbarMessage: null), + ); } } else if (item is Report) { final index = state.reports.indexWhere((r) => r.id == item.id); if (index != -1) { final updatedReports = List.from(state.reports) ..[index] = item; - emit(state.copyWith(reports: updatedReports)); + emit( + state.copyWith( + reports: updatedReports, + lastPendingUpdateId: null, + snackbarMessage: null, + ), + ); + } else { + emit( + state.copyWith(lastPendingUpdateId: null, snackbarMessage: null), + ); } + } else { + emit( + state.copyWith(lastPendingUpdateId: null, snackbarMessage: null), + ); } - emit( - state.copyWith( - lastPendingUpdateId: null, - snackbarMessage: null, - ), - ); } } } From 371dcac477d6684eb34745627fd7029a6255fd07 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 18:27:17 +0100 Subject: [PATCH 129/130] fix(community_management): implement workaround for Engagement comment nulling bug - Replace Engagement.copyWith with manual instantiation to set comment to null - Add TODO comment for potential future fix in the core model - Update updatedAt timestamp for the engagement --- .../bloc/community_management_bloc.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 83f085c2..31512002 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -334,8 +334,21 @@ class CommunityManagementBloc (e) => e.id == event.engagementId, ); - final updatedEngagement = originalEngagement.copyWith( - comment: null, + // TODO(fulleni) Fic the following BUG: + // The original Engagement.copyWith method has a bug where + // `comment: comment ?? this.comment` prevents setting the comment to null. + // To work around this without modifying the core model, we must create a + // new instance of Engagement manually, copying all properties and + // explicitly setting the comment to null. + final updatedEngagement = Engagement( + id: originalEngagement.id, + userId: originalEngagement.userId, + entityId: originalEngagement.entityId, + entityType: originalEngagement.entityType, + reaction: originalEngagement.reaction, + createdAt: originalEngagement.createdAt, + updatedAt: DateTime.now(), + comment: null, // Explicitly set to null to reject/delete the comment. ); final updatedEngagements = List.from(state.engagements) From 81f3ef436b89c855d33700f582102a2e7865efc3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Tue, 2 Dec 2025 18:27:53 +0100 Subject: [PATCH 130/130] style: fix typo in TODO comment - Correct spelling of 'Fix' in TODO comment - Improve code readability and maintainability --- lib/community_management/bloc/community_management_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/community_management/bloc/community_management_bloc.dart b/lib/community_management/bloc/community_management_bloc.dart index 31512002..bb41ad7c 100644 --- a/lib/community_management/bloc/community_management_bloc.dart +++ b/lib/community_management/bloc/community_management_bloc.dart @@ -334,7 +334,7 @@ class CommunityManagementBloc (e) => e.id == event.engagementId, ); - // TODO(fulleni) Fic the following BUG: + // TODO(fulleni): Fix the following BUG // The original Engagement.copyWith method has a bug where // `comment: comment ?? this.comment` prevents setting the comment to null. // To work around this without modifying the core model, we must create a