Skip to content

Commit 4663cd4

Browse files
authored
Merge pull request #221 from flutter-news-app-full-source-code/feat/user-generated-content
Feat/user generated content
2 parents a3e1bca + d44e43d commit 4663cd4

File tree

48 files changed

+4798
-812
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+4798
-812
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ A robust, backend-driven notification system keeps users informed and brings the
5353
- **Integrated Notification Center:** Includes a full-featured in-app notification center where users can view their history. Foreground notifications are handled gracefully, appearing as an unread indicator that leads the user to this central hub, avoiding intrusive system alerts during active use.
5454
> **Your Advantage:** You get a highly flexible and scalable notification system that avoids vendor lock-in and is ready to re-engage users from day one.
5555
56+
---
57+
58+
### 💬 Community & Feedback Systems
59+
A complete suite of tools to build a vibrant user community and gather valuable feedback directly within the app.
60+
- **Configurable Headline Engagement:** Enable immediate user interaction directly on each headline within the feed. The entire engagement system is controlled via remote configuration, allowing you to dynamically adjust the depth of user interaction—from simple reactions to full comment threads—without an app update.
61+
- **Intelligent Review Funnel:** A sophisticated, multi-layered system that strategically prompts users for an app review. Its behavior is entirely driven by remote configuration, including cooldown periods and positive interaction thresholds. It first gauges user sentiment with a private, in-app prompt: positive responses trigger the native OS review dialog, while negative responses open a private feedback form, ensuring you only ask happy users for public reviews and capture valuable insights from others.
62+
- **Moderated Content Reporting:** Empower your community to maintain content quality with a built-in reporting system. Users can easily report headlines, sources, or individual comments through a guided process. All reports are submitted to the backend and are designed to be managed and actioned from the companion web dashboard.
63+
> **Your Advantage:** Deploy a full-featured community and feedback system from day one. Skip the complexity of building engagement UI, state management for reactions, and the nuanced logic of a best-practice app review funnel.
64+
5665
</details>
5766

5867
<details>

analysis_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ analyzer:
88
document_ignores: ignore
99
flutter_style_todos: ignore
1010
lines_longer_than_80_chars: ignore
11+
one_member_abstracts: ignore
1112
prefer_asserts_with_message: ignore
1213
use_build_context_synchronously: ignore
1314
use_if_null_to_convert_nulls_to_bools: ignore

lib/account/view/followed_contents/countries/add_country_to_follow_page.dart

Lines changed: 105 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,113 @@ import 'package:flutter/material.dart';
44
import 'package:flutter_bloc/flutter_bloc.dart';
55
import 'package:flutter_news_app_mobile_client_full_source_code/account/bloc/available_countries_bloc.dart';
66
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
7+
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart';
78
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
89
import 'package:flutter_news_app_mobile_client_full_source_code/shared/services/content_limitation_service.dart';
910
import 'package:flutter_news_app_mobile_client_full_source_code/shared/widgets/content_limitation_bottom_sheet.dart';
1011
import 'package:ui_kit/ui_kit.dart';
1112

13+
class _FollowButton extends StatefulWidget {
14+
const _FollowButton({required this.country, required this.isFollowed});
15+
16+
final Country country;
17+
final bool isFollowed;
18+
19+
@override
20+
State<_FollowButton> createState() => _FollowButtonState();
21+
}
22+
23+
class _FollowButtonState extends State<_FollowButton> {
24+
bool _isLoading = false;
25+
26+
Future<void> _onFollowToggled() async {
27+
setState(() => _isLoading = true);
28+
29+
final l10n = AppLocalizations.of(context);
30+
final appBloc = context.read<AppBloc>();
31+
final userContentPreferences = appBloc.state.userContentPreferences;
32+
33+
if (userContentPreferences == null) {
34+
setState(() => _isLoading = false);
35+
return;
36+
}
37+
38+
final updatedFollowedCountries = List<Country>.from(
39+
userContentPreferences.followedCountries,
40+
);
41+
42+
try {
43+
if (widget.isFollowed) {
44+
updatedFollowedCountries.removeWhere((c) => c.id == widget.country.id);
45+
} else {
46+
final limitationService = context.read<ContentLimitationService>();
47+
final status = await limitationService.checkAction(
48+
ContentAction.followCountry,
49+
);
50+
51+
if (status != LimitationStatus.allowed) {
52+
if (mounted) {
53+
showContentLimitationBottomSheet(
54+
context: context,
55+
status: status,
56+
action: ContentAction.followCountry,
57+
);
58+
}
59+
return;
60+
}
61+
updatedFollowedCountries.add(widget.country);
62+
}
63+
64+
final updatedPreferences = userContentPreferences.copyWith(
65+
followedCountries: updatedFollowedCountries,
66+
);
67+
68+
appBloc.add(
69+
AppUserContentPreferencesChanged(preferences: updatedPreferences),
70+
);
71+
} on ForbiddenException catch (e) {
72+
if (mounted) {
73+
await showModalBottomSheet<void>(
74+
context: context,
75+
builder: (_) => ContentLimitationBottomSheet(
76+
title: l10n.limitReachedTitle,
77+
body: e.message,
78+
buttonText: l10n.gotItButton,
79+
),
80+
);
81+
}
82+
} finally {
83+
if (mounted) {
84+
setState(() => _isLoading = false);
85+
}
86+
}
87+
}
88+
89+
@override
90+
Widget build(BuildContext context) {
91+
final l10n = AppLocalizations.of(context);
92+
final colorScheme = Theme.of(context).colorScheme;
93+
94+
if (_isLoading) {
95+
return const SizedBox(
96+
width: 24,
97+
height: 24,
98+
child: CircularProgressIndicator(strokeWidth: 2),
99+
);
100+
}
101+
102+
return IconButton(
103+
icon: widget.isFollowed
104+
? Icon(Icons.check_circle, color: colorScheme.primary)
105+
: const Icon(Icons.add_circle_outline),
106+
tooltip: widget.isFollowed
107+
? l10n.unfollowCountryTooltip(widget.country.name)
108+
: l10n.followCountryTooltip(widget.country.name),
109+
onPressed: _onFollowToggled,
110+
);
111+
}
112+
}
113+
12114
/// {@template add_country_to_follow_page}
13115
/// A page that allows users to browse and select countries to follow.
14116
/// {@endtemplate}
@@ -138,76 +240,9 @@ class AddCountryToFollowPage extends StatelessWidget {
138240
),
139241
),
140242
title: Text(country.name, style: textTheme.titleMedium),
141-
trailing: IconButton(
142-
icon: isFollowed
143-
? Icon(
144-
Icons.check_circle,
145-
color: colorScheme.primary,
146-
)
147-
: Icon(
148-
Icons.add_circle_outline,
149-
color: colorScheme.onSurfaceVariant,
150-
),
151-
tooltip: isFollowed
152-
? l10n.unfollowCountryTooltip(country.name)
153-
: l10n.followCountryTooltip(country.name),
154-
onPressed: () {
155-
// Ensure user preferences are available before
156-
// proceeding.
157-
if (userContentPreferences == null) return;
158-
159-
// Create a mutable copy of the followed countries list.
160-
final updatedFollowedCountries = List<Country>.from(
161-
followedCountries,
162-
);
163-
164-
// If the user is unfollowing, always allow it.
165-
if (isFollowed) {
166-
updatedFollowedCountries.removeWhere(
167-
(c) => c.id == country.id,
168-
);
169-
final updatedPreferences = userContentPreferences
170-
.copyWith(
171-
followedCountries: updatedFollowedCountries,
172-
);
173-
174-
context.read<AppBloc>().add(
175-
AppUserContentPreferencesChanged(
176-
preferences: updatedPreferences,
177-
),
178-
);
179-
} else {
180-
// If the user is following, check the limit first.
181-
final limitationService = context
182-
.read<ContentLimitationService>();
183-
final status = limitationService.checkAction(
184-
ContentAction.followCountry,
185-
);
186-
187-
if (status == LimitationStatus.allowed) {
188-
updatedFollowedCountries.add(country);
189-
final updatedPreferences =
190-
userContentPreferences.copyWith(
191-
followedCountries:
192-
updatedFollowedCountries,
193-
);
194-
195-
context.read<AppBloc>().add(
196-
AppUserContentPreferencesChanged(
197-
preferences: updatedPreferences,
198-
),
199-
);
200-
} else {
201-
// If the limit is reached, show the bottom sheet.
202-
showModalBottomSheet<void>(
203-
context: context,
204-
builder: (_) => ContentLimitationBottomSheet(
205-
status: status,
206-
),
207-
);
208-
}
209-
}
210-
},
243+
trailing: _FollowButton(
244+
country: country,
245+
isFollowed: isFollowed,
211246
),
212247
contentPadding: const EdgeInsets.symmetric(
213248
horizontal: AppSpacing.paddingMedium,

0 commit comments

Comments
 (0)