Skip to content

Commit 0190676

Browse files
authored
Merge pull request #134 from flutter-news-app-full-source-code/feat/build-user-generated-content-config-and-moderation-suite
Feat/build user generated content config and moderation suite
2 parents 6da0a5a + f891710 commit 0190676

14 files changed

+1296
-6
lines changed

lib/app_configuration/view/tabs/features_configuration_tab.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:core/core.dart';
22
import 'package:flutter/material.dart';
33
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/ad_config_form.dart';
44
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/ad_platform_config_form.dart';
5+
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/community_config_form.dart';
56
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_ad_settings_form.dart';
67
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/feed_decorator_form.dart';
78
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/navigation_ad_settings_form.dart';
@@ -244,6 +245,43 @@ class _FeaturesConfigurationTabState extends State<FeaturesConfigurationTab> {
244245
);
245246
},
246247
),
248+
const SizedBox(height: AppSpacing.lg),
249+
250+
// Community & Engagement
251+
ValueListenableBuilder<int?>(
252+
valueListenable: _expandedTileIndex,
253+
builder: (context, expandedIndex, child) {
254+
const tileIndex = 3;
255+
return ExpansionTile(
256+
key: ValueKey('communityTile_$expandedIndex'),
257+
title: Text(l10n.communityAndEngagementTitle),
258+
subtitle: Text(
259+
l10n.communityAndEngagementDescription,
260+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
261+
color: Theme.of(
262+
context,
263+
).colorScheme.onSurface.withOpacity(0.7),
264+
),
265+
),
266+
onExpansionChanged: (isExpanded) {
267+
_expandedTileIndex.value = isExpanded ? tileIndex : null;
268+
},
269+
initiallyExpanded: expandedIndex == tileIndex,
270+
childrenPadding: const EdgeInsetsDirectional.only(
271+
start: AppSpacing.lg,
272+
top: AppSpacing.md,
273+
bottom: AppSpacing.md,
274+
),
275+
expandedCrossAxisAlignment: CrossAxisAlignment.start,
276+
children: [
277+
CommunityConfigForm(
278+
remoteConfig: widget.remoteConfig,
279+
onConfigChanged: widget.onConfigChanged,
280+
),
281+
],
282+
);
283+
},
284+
),
247285
],
248286
);
249287
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import 'package:core/core.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_news_app_web_dashboard_full_source_code/app_configuration/widgets/app_config_form_fields.dart';
4+
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart';
5+
import 'package:ui_kit/ui_kit.dart';
6+
7+
/// {@template app_review_settings_form}
8+
/// A form widget for configuring app review funnel settings.
9+
/// {@endtemplate}
10+
class AppReviewSettingsForm extends StatefulWidget {
11+
/// {@macro app_review_settings_form}
12+
const AppReviewSettingsForm({
13+
required this.remoteConfig,
14+
required this.onConfigChanged,
15+
super.key,
16+
});
17+
18+
/// The current [RemoteConfig] object.
19+
final RemoteConfig remoteConfig;
20+
21+
/// Callback to notify parent of changes to the [RemoteConfig].
22+
final ValueChanged<RemoteConfig> onConfigChanged;
23+
24+
@override
25+
State<AppReviewSettingsForm> createState() => _AppReviewSettingsFormState();
26+
}
27+
28+
class _AppReviewSettingsFormState extends State<AppReviewSettingsForm> {
29+
late final TextEditingController _positiveInteractionThresholdController;
30+
late final TextEditingController _initialPromptCooldownController;
31+
32+
@override
33+
void initState() {
34+
super.initState();
35+
_initializeControllers();
36+
}
37+
38+
@override
39+
void didUpdateWidget(covariant AppReviewSettingsForm oldWidget) {
40+
super.didUpdateWidget(oldWidget);
41+
if (widget.remoteConfig.features.community.appReview !=
42+
oldWidget.remoteConfig.features.community.appReview) {
43+
_updateControllers();
44+
}
45+
}
46+
47+
void _initializeControllers() {
48+
final appReviewConfig = widget.remoteConfig.features.community.appReview;
49+
_positiveInteractionThresholdController = TextEditingController(
50+
text: appReviewConfig.positiveInteractionThreshold.toString(),
51+
);
52+
_initialPromptCooldownController = TextEditingController(
53+
text: appReviewConfig.initialPromptCooldownDays.toString(),
54+
);
55+
}
56+
57+
void _updateControllers() {
58+
final appReviewConfig = widget.remoteConfig.features.community.appReview;
59+
_positiveInteractionThresholdController.text = appReviewConfig
60+
.positiveInteractionThreshold
61+
.toString();
62+
_initialPromptCooldownController.text = appReviewConfig
63+
.initialPromptCooldownDays
64+
.toString();
65+
}
66+
67+
@override
68+
void dispose() {
69+
_positiveInteractionThresholdController.dispose();
70+
_initialPromptCooldownController.dispose();
71+
super.dispose();
72+
}
73+
74+
@override
75+
Widget build(BuildContext context) {
76+
final l10n = AppLocalizationsX(context).l10n;
77+
final communityConfig = widget.remoteConfig.features.community;
78+
final appReviewConfig = communityConfig.appReview;
79+
80+
return Column(
81+
children: [
82+
Padding(
83+
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
84+
child: Column(
85+
crossAxisAlignment: CrossAxisAlignment.start,
86+
children: [
87+
SwitchListTile(
88+
title: Text(l10n.enableAppFeedbackSystemLabel),
89+
subtitle: Text(l10n.enableAppFeedbackSystemDescription),
90+
value: appReviewConfig.enabled,
91+
onChanged: (value) {
92+
final newConfig = communityConfig.copyWith(
93+
appReview: appReviewConfig.copyWith(enabled: value),
94+
);
95+
widget.onConfigChanged(
96+
widget.remoteConfig.copyWith(
97+
features: widget.remoteConfig.features.copyWith(
98+
community: newConfig,
99+
),
100+
),
101+
);
102+
},
103+
),
104+
if (appReviewConfig.enabled) ...[
105+
const SizedBox(height: AppSpacing.lg),
106+
Padding(
107+
padding: const EdgeInsetsDirectional.only(
108+
start: AppSpacing.lg,
109+
),
110+
child: Column(
111+
children: [
112+
ExpansionTile(
113+
title: Text(l10n.internalPromptLogicTitle),
114+
childrenPadding: const EdgeInsetsDirectional.only(
115+
start: AppSpacing.lg,
116+
top: AppSpacing.md,
117+
bottom: AppSpacing.md,
118+
),
119+
expandedCrossAxisAlignment: CrossAxisAlignment.start,
120+
children: [
121+
AppConfigIntField(
122+
label: l10n.positiveInteractionThresholdLabel,
123+
description:
124+
l10n.positiveInteractionThresholdDescription,
125+
value: appReviewConfig.positiveInteractionThreshold,
126+
onChanged: (value) {
127+
final newConfig = communityConfig.copyWith(
128+
appReview: appReviewConfig.copyWith(
129+
positiveInteractionThreshold: value,
130+
),
131+
);
132+
widget.onConfigChanged(
133+
widget.remoteConfig.copyWith(
134+
features: widget.remoteConfig.features
135+
.copyWith(
136+
community: newConfig,
137+
),
138+
),
139+
);
140+
},
141+
controller: _positiveInteractionThresholdController,
142+
),
143+
AppConfigIntField(
144+
label: l10n.initialPromptCooldownLabel,
145+
description: l10n.initialPromptCooldownDescription,
146+
value: appReviewConfig.initialPromptCooldownDays,
147+
onChanged: (value) {
148+
final newConfig = communityConfig.copyWith(
149+
appReview: appReviewConfig.copyWith(
150+
initialPromptCooldownDays: value,
151+
),
152+
);
153+
widget.onConfigChanged(
154+
widget.remoteConfig.copyWith(
155+
features: widget.remoteConfig.features
156+
.copyWith(
157+
community: newConfig,
158+
),
159+
),
160+
);
161+
},
162+
controller: _initialPromptCooldownController,
163+
),
164+
],
165+
),
166+
const SizedBox(height: AppSpacing.lg),
167+
ExpansionTile(
168+
title: Text(l10n.followUpActionsTitle),
169+
childrenPadding: const EdgeInsetsDirectional.only(
170+
start: AppSpacing.lg,
171+
top: AppSpacing.md,
172+
bottom: AppSpacing.md,
173+
),
174+
expandedCrossAxisAlignment: CrossAxisAlignment.start,
175+
children: [
176+
SwitchListTile(
177+
title: Text(l10n.requestStoreReviewLabel),
178+
subtitle: Text(l10n.requestStoreReviewDescription),
179+
value: appReviewConfig
180+
.isPositiveFeedbackFollowUpEnabled,
181+
onChanged: (value) {
182+
final newAppReviewConfig = appReviewConfig
183+
.copyWith(
184+
isPositiveFeedbackFollowUpEnabled: value,
185+
);
186+
widget.onConfigChanged(
187+
widget.remoteConfig.copyWith(
188+
features: widget.remoteConfig.features
189+
.copyWith(
190+
community: communityConfig.copyWith(
191+
appReview: newAppReviewConfig,
192+
),
193+
),
194+
),
195+
);
196+
},
197+
),
198+
SwitchListTile(
199+
title: Text(l10n.requestWrittenFeedbackLabel),
200+
subtitle: Text(
201+
l10n.requestWrittenFeedbackDescription,
202+
),
203+
value: appReviewConfig
204+
.isNegativeFeedbackFollowUpEnabled,
205+
onChanged: (value) {
206+
final newAppReviewConfig = appReviewConfig
207+
.copyWith(
208+
isNegativeFeedbackFollowUpEnabled: value,
209+
);
210+
widget.onConfigChanged(
211+
widget.remoteConfig.copyWith(
212+
features: widget.remoteConfig.features
213+
.copyWith(
214+
community: communityConfig.copyWith(
215+
appReview: newAppReviewConfig,
216+
),
217+
),
218+
),
219+
);
220+
},
221+
),
222+
],
223+
),
224+
],
225+
),
226+
),
227+
],
228+
],
229+
),
230+
),
231+
],
232+
);
233+
}
234+
}

0 commit comments

Comments
 (0)