Skip to content

Commit 3887218

Browse files
committed
feat(account): enhance in-app notification center
- Add loading more indicator and functionality - Improve empty state handling - Optimize tab switching and notification loading logic - Refactor notification list to support infinite scrolling
1 parent a1deefa commit 3887218

File tree

1 file changed

+129
-65
lines changed

1 file changed

+129
-65
lines changed

lib/account/view/in_app_notification_center_page.dart

Lines changed: 129 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ class _InAppNotificationCenterPageState
3636
..addListener(() {
3737
if (!_tabController.indexIsChanging) {
3838
context.read<InAppNotificationCenterBloc>().add(
39-
InAppNotificationCenterTabChanged(_tabController.index),
40-
);
39+
InAppNotificationCenterTabChanged(_tabController.index),
40+
);
4141
}
4242
});
4343
}
@@ -56,18 +56,16 @@ class _InAppNotificationCenterPageState
5656
appBar: AppBar(
5757
title: Text(l10n.notificationCenterPageTitle),
5858
actions: [
59-
BlocBuilder<
60-
InAppNotificationCenterBloc,
61-
InAppNotificationCenterState
62-
>(
59+
BlocBuilder<InAppNotificationCenterBloc,
60+
InAppNotificationCenterState>(
6361
builder: (context, state) {
6462
final hasUnread = state.notifications.any((n) => !n.isRead);
6563
return IconButton(
6664
onPressed: hasUnread
6765
? () {
6866
context.read<InAppNotificationCenterBloc>().add(
69-
const InAppNotificationCenterMarkAllAsRead(),
70-
);
67+
const InAppNotificationCenterMarkAllAsRead(),
68+
);
7169
}
7270
: null,
7371
icon: const Icon(Icons.done_all),
@@ -83,73 +81,124 @@ class _InAppNotificationCenterPageState
8381
],
8482
),
8583
),
86-
body:
87-
BlocConsumer<
88-
InAppNotificationCenterBloc,
89-
InAppNotificationCenterState
90-
>(
91-
listener: (context, state) {
92-
if (state.status == InAppNotificationCenterStatus.failure &&
93-
state.error != null) {
94-
ScaffoldMessenger.of(context)
95-
..hideCurrentSnackBar()
96-
..showSnackBar(
97-
SnackBar(
98-
content: Text(state.error!.message),
99-
backgroundColor: Theme.of(context).colorScheme.error,
100-
),
101-
);
102-
}
103-
},
104-
builder: (context, state) {
105-
if (state.status == InAppNotificationCenterStatus.loading) {
106-
return LoadingStateWidget(
107-
icon: Icons.notifications_none_outlined,
108-
headline: l10n.notificationCenterLoadingHeadline,
109-
subheadline: l10n.notificationCenterLoadingSubheadline,
110-
);
111-
}
112-
113-
if (state.status == InAppNotificationCenterStatus.failure) {
114-
return FailureStateWidget(
115-
exception:
116-
state.error ??
117-
OperationFailedException(
118-
l10n.notificationCenterFailureHeadline,
119-
),
120-
onRetry: () {
121-
context.read<InAppNotificationCenterBloc>().add(
84+
body: BlocConsumer<InAppNotificationCenterBloc,
85+
InAppNotificationCenterState>(
86+
listener: (context, state) {
87+
if (state.status == InAppNotificationCenterStatus.failure &&
88+
state.error != null) {
89+
ScaffoldMessenger.of(context)
90+
..hideCurrentSnackBar()
91+
..showSnackBar(
92+
SnackBar(
93+
content: Text(state.error!.message),
94+
backgroundColor: Theme.of(context).colorScheme.error,
95+
),
96+
);
97+
}
98+
},
99+
builder: (context, state) {
100+
if (state.status == InAppNotificationCenterStatus.loading &&
101+
state.breakingNewsNotifications.isEmpty &&
102+
state.digestNotifications.isEmpty) {
103+
return LoadingStateWidget(
104+
icon: Icons.notifications_none_outlined,
105+
headline: l10n.notificationCenterLoadingHeadline,
106+
subheadline: l10n.notificationCenterLoadingSubheadline,
107+
);
108+
}
109+
110+
if (state.status == InAppNotificationCenterStatus.failure &&
111+
state.breakingNewsNotifications.isEmpty &&
112+
state.digestNotifications.isEmpty) {
113+
return FailureStateWidget(
114+
exception: state.error ??
115+
OperationFailedException(
116+
l10n.notificationCenterFailureHeadline,
117+
),
118+
onRetry: () {
119+
context.read<InAppNotificationCenterBloc>().add(
122120
const InAppNotificationCenterSubscriptionRequested(),
123121
);
124-
},
125-
);
126-
}
122+
},
123+
);
124+
}
127125

128-
return TabBarView(
129-
controller: _tabController,
130-
children: [
131-
_NotificationList(
132-
notifications: state.breakingNewsNotifications,
133-
),
134-
_NotificationList(notifications: state.digestNotifications),
135-
],
136-
);
137-
},
138-
),
126+
return TabBarView(
127+
controller: _tabController,
128+
children: [
129+
_NotificationList(
130+
status: state.status,
131+
notifications: state.breakingNewsNotifications,
132+
hasMore: state.breakingNewsHasMore,
133+
),
134+
_NotificationList(
135+
status: state.status,
136+
notifications: state.digestNotifications,
137+
hasMore: state.digestHasMore,
138+
),
139+
],
140+
);
141+
},
142+
),
139143
);
140144
}
141145
}
142146

143-
class _NotificationList extends StatelessWidget {
144-
const _NotificationList({required this.notifications});
147+
class _NotificationList extends StatefulWidget {
148+
const _NotificationList({
149+
required this.notifications,
150+
required this.hasMore,
151+
required this.status,
152+
});
145153

154+
final InAppNotificationCenterStatus status;
146155
final List<InAppNotification> notifications;
156+
final bool hasMore;
157+
158+
@override
159+
State<_NotificationList> createState() => _NotificationListState();
160+
}
161+
162+
class _NotificationListState extends State<_NotificationList> {
163+
final _scrollController = ScrollController();
164+
165+
@override
166+
void initState() {
167+
super.initState();
168+
_scrollController.addListener(_onScroll);
169+
}
170+
171+
@override
172+
void dispose() {
173+
_scrollController
174+
..removeListener(_onScroll)
175+
..dispose();
176+
super.dispose();
177+
}
178+
179+
void _onScroll() {
180+
final bloc = context.read<InAppNotificationCenterBloc>();
181+
if (_isBottom &&
182+
widget.hasMore &&
183+
bloc.state.status != InAppNotificationCenterStatus.loadingMore) {
184+
bloc.add(const InAppNotificationCenterFetchMoreRequested());
185+
}
186+
}
187+
188+
bool get _isBottom {
189+
if (!_scrollController.hasClients) return false;
190+
final maxScroll = _scrollController.position.maxScrollExtent;
191+
final currentScroll = _scrollController.offset;
192+
return currentScroll >= (maxScroll * 0.98);
193+
}
147194

148195
@override
149196
Widget build(BuildContext context) {
150197
final l10n = AppLocalizationsX(context).l10n;
151198

152-
if (notifications.isEmpty) {
199+
// Show empty state only if not in the middle of an initial load.
200+
if (widget.notifications.isEmpty &&
201+
widget.status != InAppNotificationCenterStatus.loading) {
153202
return InitialStateWidget(
154203
icon: Icons.notifications_off_outlined,
155204
headline: l10n.notificationCenterEmptyHeadline,
@@ -158,16 +207,31 @@ class _NotificationList extends StatelessWidget {
158207
}
159208

160209
return ListView.separated(
161-
itemCount: notifications.length,
210+
controller: _scrollController,
211+
itemCount: widget.hasMore
212+
? widget.notifications.length + 1
213+
: widget.notifications.length,
162214
separatorBuilder: (context, index) => const Divider(height: 1),
163215
itemBuilder: (context, index) {
164-
final notification = notifications[index];
216+
if (index >= widget.notifications.length) {
217+
return widget.status == InAppNotificationCenterStatus.loadingMore
218+
? const Padding(
219+
padding: EdgeInsets.symmetric(
220+
vertical: AppSpacing.lg,
221+
),
222+
child: Center(
223+
child: CircularProgressIndicator(),
224+
),
225+
)
226+
: const SizedBox.shrink();
227+
}
228+
final notification = widget.notifications[index];
165229
return InAppNotificationListItem(
166230
notification: notification,
167231
onTap: () async {
168232
context.read<InAppNotificationCenterBloc>().add(
169-
InAppNotificationCenterMarkedAsRead(notification.id),
170-
);
233+
InAppNotificationCenterMarkedAsRead(notification.id),
234+
);
171235

172236
final payload = notification.payload;
173237
final contentType = payload.data['contentType'] as String?;

0 commit comments

Comments
 (0)