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.
diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart
index 764c93e9..a724e64b 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';
@@ -19,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';
@@ -41,6 +44,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 +63,9 @@ class App extends StatelessWidget {
_countriesRepository = countriesRepository,
_languagesRepository = languagesRepository,
_usersRepository = usersRepository,
+ _engagementsRepository = engagementsRepository,
+ _reportsRepository = reportsRepository,
+ _appReviewsRepository = appReviewsRepository,
_environment = environment,
_pendingDeletionsService = pendingDeletionsService;
@@ -72,6 +81,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 +105,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(),
@@ -100,6 +115,9 @@ class App extends StatelessWidget {
RepositoryProvider.value(
value: _pendingDeletionsService,
),
+ RepositoryProvider(
+ create: (context) => PendingUpdatesServiceImpl(),
+ ),
],
child: MultiBlocProvider(
providers: [
@@ -163,6 +181,18 @@ 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(),
+ pendingUpdatesService: context.read(),
+ ),
+ ),
],
child: _AppView(
authenticationRepository: _authenticationRepository,
diff --git a/lib/app/view/app_shell.dart b/lib/app/view/app_shell.dart
index 88ea06e9..156ef59e 100644
--- a/lib/app/view/app_shell.dart
+++ b/lib/app/view/app_shell.dart
@@ -44,12 +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.navCommunity,
),
NavigationDestination(
icon: const Icon(Icons.settings_applications_outlined),
@@ -64,6 +69,7 @@ class AppShell extends StatelessWidget {
Routes.overviewName,
Routes.contentManagementName,
Routes.userManagementName,
+ Routes.communityManagementName,
Routes.appConfigurationName,
];
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/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,
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..47ce4b33
--- /dev/null
+++ b/lib/community_management/bloc/community_filter/community_filter_bloc.dart
@@ -0,0 +1,52 @@
+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(
+ (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 _onFilterReset(
+ CommunityFilterReset event,
+ Emitter emit,
+ ) {
+ 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
new file mode 100644
index 00000000..2e661254
--- /dev/null
+++ b/lib/community_management/bloc/community_filter/community_filter_event.dart
@@ -0,0 +1,43 @@
+part of 'community_filter_bloc.dart';
+
+abstract class CommunityFilterEvent extends Equatable {
+ const CommunityFilterEvent();
+
+ @override
+ List