@@ -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