Skip to content

Commit b5aabaf

Browse files
authored
Merge pull request #138 from flutter-news-app-full-source-code/feat/Community-Management
Feat/community management
2 parents 0190676 + 81f3ef4 commit b5aabaf

40 files changed

+5205
-28
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ Effortlessly manage your entire user base with a dedicated user management syste
4545
4646
</details>
4747

48+
<details>
49+
<summary><strong>💬 Community & Moderation Control</strong></summary>
50+
51+
### 💬 Comprehensive Moderation Hub
52+
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.
53+
- **Unified Content Review:** Seamlessly moderate all incoming user engagements (reactions and comments), content reports, and app review feedback from a single, intuitive interface.
54+
- **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.
55+
- **Direct User Insight:** Gain a clear, unfiltered view of user sentiment, content issues, and overall satisfaction by directly engaging with their feedback and reports.
56+
> **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.
57+
58+
</details>
59+
4860

4961
<details>
5062
<summary><strong>⚙️ App Monetization & Remote Control</strong></summary>
@@ -62,6 +74,7 @@ Dynamically control the mobile app's behavior and operational state directly fro
6274
- **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.
6375
- **Dynamic In-App Content:** Remotely manage the visibility and behavior of in-feed promotional prompts and user engagement elements.
6476
- **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.
77+
- **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.
6578
- **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.
6679
> **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.
6780

lib/app/view/app.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/app/bloc/app_blo
1111
import 'package:flutter_news_app_web_dashboard_full_source_code/app/config/app_environment.dart';
1212
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/bloc/app_configuration_bloc.dart';
1313
import 'package:flutter_news_app_web_dashboard_full_source_code/authentication/bloc/authentication_bloc.dart';
14+
import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_filter/community_filter_bloc.dart';
15+
import 'package:flutter_news_app_web_dashboard_full_source_code/community_management/bloc/community_management_bloc.dart';
1416
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/content_management_bloc.dart';
1517
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/headlines_filter/headlines_filter_bloc.dart';
1618
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
1921
import 'package:flutter_news_app_web_dashboard_full_source_code/overview/bloc/overview_bloc.dart';
2022
import 'package:flutter_news_app_web_dashboard_full_source_code/router/router.dart';
2123
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart';
24+
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_updates_service.dart';
2225
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart';
2326
import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_filter/user_filter_bloc.dart';
2427
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 {
4144
required DataRepository<Country> countriesRepository,
4245
required DataRepository<Language> languagesRepository,
4346
required DataRepository<User> usersRepository,
47+
required DataRepository<Engagement> engagementsRepository,
48+
required DataRepository<Report> reportsRepository,
49+
required DataRepository<AppReview> appReviewsRepository,
4450
required KVStorageService storageService,
4551
required AppEnvironment environment,
4652
required PendingDeletionsService pendingDeletionsService,
@@ -57,6 +63,9 @@ class App extends StatelessWidget {
5763
_countriesRepository = countriesRepository,
5864
_languagesRepository = languagesRepository,
5965
_usersRepository = usersRepository,
66+
_engagementsRepository = engagementsRepository,
67+
_reportsRepository = reportsRepository,
68+
_appReviewsRepository = appReviewsRepository,
6069
_environment = environment,
6170
_pendingDeletionsService = pendingDeletionsService;
6271

@@ -72,6 +81,9 @@ class App extends StatelessWidget {
7281
final DataRepository<Country> _countriesRepository;
7382
final DataRepository<Language> _languagesRepository;
7483
final DataRepository<User> _usersRepository;
84+
final DataRepository<Engagement> _engagementsRepository;
85+
final DataRepository<Report> _reportsRepository;
86+
final DataRepository<AppReview> _appReviewsRepository;
7587
final KVStorageService _kvStorageService;
7688
final AppEnvironment _environment;
7789

@@ -93,13 +105,19 @@ class App extends StatelessWidget {
93105
RepositoryProvider.value(value: _countriesRepository),
94106
RepositoryProvider.value(value: _languagesRepository),
95107
RepositoryProvider.value(value: _usersRepository),
108+
RepositoryProvider.value(value: _engagementsRepository),
109+
RepositoryProvider.value(value: _reportsRepository),
110+
RepositoryProvider.value(value: _appReviewsRepository),
96111
RepositoryProvider.value(value: _kvStorageService),
97112
RepositoryProvider(
98113
create: (context) => const ThrottledFetchingService(),
99114
),
100115
RepositoryProvider.value(
101116
value: _pendingDeletionsService,
102117
),
118+
RepositoryProvider<PendingUpdatesService>(
119+
create: (context) => PendingUpdatesServiceImpl(),
120+
),
103121
],
104122
child: MultiBlocProvider(
105123
providers: [
@@ -163,6 +181,18 @@ class App extends StatelessWidget {
163181
userFilterBloc: context.read<UserFilterBloc>(),
164182
),
165183
),
184+
BlocProvider(
185+
create: (context) => CommunityFilterBloc(),
186+
),
187+
BlocProvider(
188+
create: (context) => CommunityManagementBloc(
189+
engagementsRepository: context.read<DataRepository<Engagement>>(),
190+
reportsRepository: context.read<DataRepository<Report>>(),
191+
appReviewsRepository: context.read<DataRepository<AppReview>>(),
192+
communityFilterBloc: context.read<CommunityFilterBloc>(),
193+
pendingUpdatesService: context.read<PendingUpdatesService>(),
194+
),
195+
),
166196
],
167197
child: _AppView(
168198
authenticationRepository: _authenticationRepository,

lib/app/view/app_shell.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,17 @@ class AppShell extends StatelessWidget {
4444
NavigationDestination(
4545
icon: const Icon(Icons.folder_open_outlined),
4646
selectedIcon: const Icon(Icons.folder),
47-
label: l10n.contentManagement,
47+
label: l10n.navContent,
4848
),
4949
NavigationDestination(
5050
icon: const Icon(Icons.people_outline),
5151
selectedIcon: const Icon(Icons.people),
52-
label: l10n.userManagement,
52+
label: l10n.navUsers,
53+
),
54+
NavigationDestination(
55+
icon: const Icon(Icons.forum_outlined),
56+
selectedIcon: const Icon(Icons.forum),
57+
label: l10n.navCommunity,
5358
),
5459
NavigationDestination(
5560
icon: const Icon(Icons.settings_applications_outlined),
@@ -64,6 +69,7 @@ class AppShell extends StatelessWidget {
6469
Routes.overviewName,
6570
Routes.contentManagementName,
6671
Routes.userManagementName,
72+
Routes.communityManagementName,
6773
Routes.appConfigurationName,
6874
];
6975

lib/app_configuration/widgets/app_review_settings_form.dart

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,12 @@ class _AppReviewSettingsFormState extends State<AppReviewSettingsForm> {
107107
padding: const EdgeInsetsDirectional.only(
108108
start: AppSpacing.lg,
109109
),
110-
child: Column(
111-
children: [
112-
ExpansionTile(
110+
child: LayoutBuilder(
111+
builder: (context, constraints) {
112+
final isMobile = constraints.maxWidth < 600;
113+
return ExpansionTile(
113114
title: Text(l10n.internalPromptLogicTitle),
115+
initiallyExpanded: !isMobile,
114116
childrenPadding: const EdgeInsetsDirectional.only(
115117
start: AppSpacing.lg,
116118
top: AppSpacing.md,
@@ -162,10 +164,20 @@ class _AppReviewSettingsFormState extends State<AppReviewSettingsForm> {
162164
controller: _initialPromptCooldownController,
163165
),
164166
],
165-
),
166-
const SizedBox(height: AppSpacing.lg),
167-
ExpansionTile(
167+
);
168+
},
169+
),
170+
),
171+
Padding(
172+
padding: const EdgeInsetsDirectional.only(
173+
start: AppSpacing.lg,
174+
),
175+
child: LayoutBuilder(
176+
builder: (context, constraints) {
177+
final isMobile = constraints.maxWidth < 600;
178+
return ExpansionTile(
168179
title: Text(l10n.followUpActionsTitle),
180+
initiallyExpanded: !isMobile,
169181
childrenPadding: const EdgeInsetsDirectional.only(
170182
start: AppSpacing.lg,
171183
top: AppSpacing.md,
@@ -220,8 +232,8 @@ class _AppReviewSettingsFormState extends State<AppReviewSettingsForm> {
220232
},
221233
),
222234
],
223-
),
224-
],
235+
);
236+
},
225237
),
226238
),
227239
],

lib/bootstrap.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ Future<Widget> bootstrap(
6565
DataClient<Country> countriesClient;
6666
DataClient<Language> languagesClient;
6767
DataClient<User> usersClient;
68+
DataClient<Engagement> engagementsClient;
69+
DataClient<Report> reportsClient;
70+
DataClient<AppReview> appReviewsClient;
6871

6972
if (appConfig.environment == app_config.AppEnvironment.demo) {
7073
headlinesClient = DataInMemory<Headline>(
@@ -128,6 +131,24 @@ Future<Widget> bootstrap(
128131
initialData: usersFixturesData,
129132
logger: Logger('DataInMemory<User>'),
130133
);
134+
engagementsClient = DataInMemory<Engagement>(
135+
toJson: (i) => i.toJson(),
136+
getId: (i) => i.id,
137+
initialData: getEngagementsFixturesData(),
138+
logger: Logger('DataInMemory<Engagement>'),
139+
);
140+
reportsClient = DataInMemory<Report>(
141+
toJson: (i) => i.toJson(),
142+
getId: (i) => i.id,
143+
initialData: getReportsFixturesData(),
144+
logger: Logger('DataInMemory<Report>'),
145+
);
146+
appReviewsClient = DataInMemory<AppReview>(
147+
toJson: (i) => i.toJson(),
148+
getId: (i) => i.id,
149+
initialData: getAppReviewsFixturesData(),
150+
logger: Logger('DataInMemory<AppReview>'),
151+
);
131152
} else {
132153
headlinesClient = DataApi<Headline>(
133154
httpClient: httpClient!,
@@ -200,6 +221,27 @@ Future<Widget> bootstrap(
200221
toJson: (user) => user.toJson(),
201222
logger: Logger('DataApi<User>'),
202223
);
224+
engagementsClient = DataApi<Engagement>(
225+
httpClient: httpClient,
226+
modelName: 'engagement',
227+
fromJson: Engagement.fromJson,
228+
toJson: (engagement) => engagement.toJson(),
229+
logger: Logger('DataApi<Engagement>'),
230+
);
231+
reportsClient = DataApi<Report>(
232+
httpClient: httpClient,
233+
modelName: 'report',
234+
fromJson: Report.fromJson,
235+
toJson: (report) => report.toJson(),
236+
logger: Logger('DataApi<Report>'),
237+
);
238+
appReviewsClient = DataApi<AppReview>(
239+
httpClient: httpClient,
240+
modelName: 'app_review',
241+
fromJson: AppReview.fromJson,
242+
toJson: (appReview) => appReview.toJson(),
243+
logger: Logger('DataApi<AppReview>'),
244+
);
203245
}
204246

205247
pendingDeletionsService = PendingDeletionsServiceImpl(
@@ -231,6 +273,15 @@ Future<Widget> bootstrap(
231273
dataClient: languagesClient,
232274
);
233275
final usersRepository = DataRepository<User>(dataClient: usersClient);
276+
final engagementsRepository = DataRepository<Engagement>(
277+
dataClient: engagementsClient,
278+
);
279+
final reportsRepository = DataRepository<Report>(
280+
dataClient: reportsClient,
281+
);
282+
final appReviewsRepository = DataRepository<AppReview>(
283+
dataClient: appReviewsClient,
284+
);
234285

235286
return App(
236287
authenticationRepository: authenticationRepository,
@@ -244,6 +295,9 @@ Future<Widget> bootstrap(
244295
countriesRepository: countriesRepository,
245296
languagesRepository: languagesRepository,
246297
usersRepository: usersRepository,
298+
engagementsRepository: engagementsRepository,
299+
reportsRepository: reportsRepository,
300+
appReviewsRepository: appReviewsRepository,
247301
storageService: kvStorage,
248302
environment: environment,
249303
pendingDeletionsService: pendingDeletionsService,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import 'package:bloc/bloc.dart';
2+
import 'package:core/core.dart';
3+
import 'package:equatable/equatable.dart';
4+
5+
part 'community_filter_event.dart';
6+
part 'community_filter_state.dart';
7+
8+
class CommunityFilterBloc
9+
extends Bloc<CommunityFilterEvent, CommunityFilterState> {
10+
CommunityFilterBloc() : super(const CommunityFilterState()) {
11+
on<EngagementsFilterChanged>(
12+
(event, emit) => emit(
13+
state.copyWith(
14+
engagementsFilter: event.filter,
15+
version: state.version,
16+
),
17+
),
18+
);
19+
on<ReportsFilterChanged>(
20+
(event, emit) => emit(
21+
state.copyWith(
22+
reportsFilter: event.filter,
23+
version: state.version,
24+
),
25+
),
26+
);
27+
on<AppReviewsFilterChanged>(
28+
(event, emit) => emit(
29+
state.copyWith(
30+
appReviewsFilter: event.filter,
31+
version: state.version,
32+
),
33+
),
34+
);
35+
on<CommunityFilterReset>(_onFilterReset);
36+
on<CommunityFilterApplied>(_onFilterApplied);
37+
}
38+
39+
void _onFilterReset(
40+
CommunityFilterReset event,
41+
Emitter<CommunityFilterState> emit,
42+
) {
43+
emit(const CommunityFilterState());
44+
}
45+
46+
void _onFilterApplied(
47+
CommunityFilterApplied event,
48+
Emitter<CommunityFilterState> emit,
49+
) {
50+
emit(state.copyWith(version: state.version + 1));
51+
}
52+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
part of 'community_filter_bloc.dart';
2+
3+
abstract class CommunityFilterEvent extends Equatable {
4+
const CommunityFilterEvent();
5+
6+
@override
7+
List<Object> get props => [];
8+
}
9+
10+
class EngagementsFilterChanged extends CommunityFilterEvent {
11+
const EngagementsFilterChanged(this.filter);
12+
13+
final EngagementsFilter filter;
14+
15+
@override
16+
List<Object> get props => [filter];
17+
}
18+
19+
class ReportsFilterChanged extends CommunityFilterEvent {
20+
const ReportsFilterChanged(this.filter);
21+
22+
final ReportsFilter filter;
23+
24+
@override
25+
List<Object> get props => [filter];
26+
}
27+
28+
class AppReviewsFilterChanged extends CommunityFilterEvent {
29+
const AppReviewsFilterChanged(this.filter);
30+
31+
final AppReviewsFilter filter;
32+
33+
@override
34+
List<Object> get props => [filter];
35+
}
36+
37+
class CommunityFilterApplied extends CommunityFilterEvent {
38+
const CommunityFilterApplied();
39+
}
40+
41+
class CommunityFilterReset extends CommunityFilterEvent {
42+
const CommunityFilterReset();
43+
}

0 commit comments

Comments
 (0)