@@ -5,6 +5,7 @@ import 'package:catalyst_voices/notification/banner_close_button.dart';
55import 'package:catalyst_voices/notification/banner_content.dart' ;
66import 'package:catalyst_voices/notification/catalyst_notification.dart' ;
77import 'package:catalyst_voices/routes/app_router_factory.dart' ;
8+ import 'package:catalyst_voices/widgets/modals/voices_dialog.dart' ;
89import 'package:catalyst_voices_assets/catalyst_voices_assets.dart' ;
910import 'package:catalyst_voices_shared/catalyst_voices_shared.dart' ;
1011import 'package:flutter/material.dart' ;
@@ -37,9 +38,12 @@ class CatalystMessenger extends StatefulWidget {
3738}
3839
3940class CatalystMessengerState extends State <CatalystMessenger > {
40- final _pending = < CatalystNotification > [];
41+ final _pendingDialogs = < DialogNotification > [];
42+ final _pendingBanners = < BannerNotification > [];
4143 bool _isShowingBanner = false ;
44+ bool _isShowingDialog = false ;
4245 BannerNotification ? _activeBanner;
46+ DialogNotification ? _activeDialog;
4347
4448 GoRouter ? __router;
4549
@@ -50,18 +54,26 @@ class CatalystMessengerState extends State<CatalystMessenger> {
5054 /// Adds a notification to the queue if it is not already present.
5155 ///
5256 /// This method ensures that duplicate notifications are not added to the queue.
53- /// If the notification already exists in the `_pending` queue, it logs a message
54- /// and skips adding it. Otherwise, the notification is added to the queue in a
55- /// sorted order, and the queue is processed to display notifications.
57+ /// Notifications are routed to the appropriate queue (dialogs or banners)
58+ /// and sorted by priority within each queue.
5659 void add (CatalystNotification notification) {
57- if (_pending.contains (notification)) {
58- _logger.fine ('$notification already in queue, skipping add' );
59- return ;
60- }
61-
6260 _logger.finest ('Adding $notification to queue' );
6361
64- _addSorted (notification);
62+ switch (notification) {
63+ case DialogNotification ():
64+ if (_pendingDialogs.any ((n) => n.id == notification.id)) {
65+ _logger.fine ('$notification already in dialogs queue, skipping add' );
66+ return ;
67+ }
68+ _addToQueue (notification);
69+ case BannerNotification ():
70+ if (_pendingBanners.any ((n) => n.id == notification.id)) {
71+ _logger.fine ('$notification already in banners queue, skipping add' );
72+ return ;
73+ }
74+ _addToQueue (notification);
75+ }
76+
6577 _processQueue ();
6678 }
6779
@@ -71,7 +83,13 @@ class CatalystMessengerState extends State<CatalystMessenger> {
7183 }
7284
7385 void cancelWhere (CatalystNotificationPredicate test) {
74- _pending.removeWhere (test);
86+ _pendingDialogs.removeWhere (test);
87+ _pendingBanners.removeWhere (test);
88+
89+ final activeDialog = _activeDialog;
90+ if (activeDialog != null && test (activeDialog)) {
91+ _hideCurrentDialog ();
92+ }
7593
7694 final activeBanner = _activeBanner;
7795 if (activeBanner != null && test (activeBanner)) {
@@ -87,10 +105,18 @@ class CatalystMessengerState extends State<CatalystMessenger> {
87105 super .dispose ();
88106 }
89107
90- void _addSorted (CatalystNotification notification) {
91- _pending
92- ..add (notification)
93- ..sort ();
108+ void _addToQueue (CatalystNotification notification) {
109+ switch (notification) {
110+ case BannerNotification ():
111+ _pendingBanners
112+ ..add (notification)
113+ ..sort ();
114+ break ;
115+ case DialogNotification ():
116+ _pendingDialogs
117+ ..add (notification)
118+ ..sort ();
119+ }
94120 }
95121
96122 GoRouter _findRouter () {
@@ -103,16 +129,24 @@ class CatalystMessengerState extends State<CatalystMessenger> {
103129 void _handleRouterChange () {
104130 final routerState = _router.state;
105131
132+ // Handle active dialog
133+ final activeDialog = _activeDialog;
134+ if (activeDialog != null && ! activeDialog.routerPredicate (routerState)) {
135+ _logger.finer ('Hiding dialog(${activeDialog .id }). Not valid for router state' );
136+ _addToQueue (activeDialog);
137+ _hideCurrentDialog ();
138+ }
139+
106140 // Handle active banner
107141 final activeBanner = _activeBanner;
108142 if (activeBanner != null && ! activeBanner.routerPredicate (routerState)) {
109143 _logger.finer ('Hiding banner(${activeBanner .id }). Not valid for router state' );
110- _addSorted (activeBanner);
144+ _addToQueue (activeBanner);
111145 _hideCurrentBanner ();
112146 }
113147
114148 // Process queue if there are pending notifications
115- if (_pending .isNotEmpty) {
149+ if (_pendingDialogs.isNotEmpty || _pendingBanners .isNotEmpty) {
116150 _processQueue ();
117151 }
118152 }
@@ -126,6 +160,15 @@ class CatalystMessengerState extends State<CatalystMessenger> {
126160 messengerState.removeCurrentMaterialBanner (reason: MaterialBannerClosedReason .hide);
127161 }
128162
163+ /// Hiding current dialog will trigger _onDialogCompleted and process queue.
164+ void _hideCurrentDialog () {
165+ final navigatorContext = _router.routerDelegate.navigatorKey.currentContext;
166+ if (navigatorContext == null ) {
167+ return ;
168+ }
169+ Navigator .of (navigatorContext, rootNavigator: true ).pop ();
170+ }
171+
129172 void _onBannerCompleted () {
130173 assert (_activeBanner != null , 'Completed banner but active was null' );
131174 final activeBanner = _activeBanner! ;
@@ -138,29 +181,56 @@ class CatalystMessengerState extends State<CatalystMessenger> {
138181 _processQueue ();
139182 }
140183
184+ void _onDialogCompleted () {
185+ assert (_activeDialog != null , 'Completed dialog but active was null' );
186+ final activeDialog = _activeDialog! ;
187+
188+ _logger.finer ('Completed dialog $activeDialog ' );
189+
190+ _isShowingDialog = false ;
191+ _activeDialog = null ;
192+
193+ _processQueue ();
194+ }
195+
141196 void _processQueue () {
142197 final routerState = _router.state;
143- final allowed = _pending.where ((notification) => notification.routerPredicate (routerState));
144198
145- if (allowed.isEmpty) {
146- if (_pending.isNotEmpty) {
147- _logger.finest ('Found ${_pending .length } notification but none allow for router state' );
199+ // Filter notifications that are allowed for current router state
200+ final allowedDialogs = _pendingDialogs.where ((n) => n.routerPredicate (routerState));
201+ final allowedBanners = _pendingBanners.where ((n) => n.routerPredicate (routerState));
202+
203+ if (allowedDialogs.isEmpty && allowedBanners.isEmpty) {
204+ final totalPending = _pendingDialogs.length + _pendingBanners.length;
205+ if (totalPending > 0 ) {
206+ _logger.finest ('Found $totalPending notification(s) but none allow for router state' );
148207 }
149208 return ;
150209 }
151210
152- // Process banners
153- if (! _isShowingBanner) {
154- final banner = allowed.whereType <BannerNotification >().firstOrNull;
155- if (banner != null ) {
156- _pending.removeWhere ((element) => element.id == banner.id);
157- _activeBanner = banner;
158- _isShowingBanner = true ;
211+ // Get next notification respecting priority across both queues
212+ final nextDialog = allowedDialogs.firstOrNull;
213+ final nextBanner = allowedBanners.firstOrNull;
159214
160- _logger.finer ('Showing banner $banner ' );
215+ // Banners and dialog can be shown at the same time
216+ if (! _isShowingDialog && nextDialog != null ) {
217+ _pendingDialogs.removeWhere ((element) => element.id == nextDialog.id);
218+ _activeDialog = nextDialog;
219+ _isShowingDialog = true ;
161220
162- unawaited (_showBanner (banner).whenComplete (_onBannerCompleted));
163- }
221+ _logger.finer ('Showing dialog $nextDialog ' );
222+
223+ unawaited (_showDialog (nextDialog).whenComplete (_onDialogCompleted));
224+ }
225+
226+ if (! _isShowingBanner && nextBanner != null ) {
227+ _pendingBanners.removeWhere ((element) => element.id == nextBanner.id);
228+ _activeBanner = nextBanner;
229+ _isShowingBanner = true ;
230+
231+ _logger.finer ('Showing banner $nextBanner ' );
232+
233+ unawaited (_showBanner (nextBanner).whenComplete (_onBannerCompleted));
164234 }
165235 }
166236
@@ -190,4 +260,37 @@ class CatalystMessengerState extends State<CatalystMessenger> {
190260 },
191261 );
192262 }
263+
264+ Future <void > _showDialog (DialogNotification notification) async {
265+ // Wait for all pending frames to complete. Navigation (especially with
266+ // redirects) can take multiple frames. By waiting while Flutter has
267+ // scheduled frames, we ensure navigation has fully completed.
268+ await WidgetsBinding .instance.endOfFrame;
269+
270+ // Check if the dialog is still active after navigation
271+ if (_activeDialog? .id != notification.id) {
272+ return ;
273+ }
274+
275+ // Verify the dialog is still allowed for the current route
276+ if (! notification.routerPredicate (_router.state)) {
277+ _logger.finer ('Dialog ${notification .id } no longer valid for current route after navigation' );
278+ return ;
279+ }
280+
281+ final navigatorContext = _router.routerDelegate.navigatorKey.currentContext;
282+ if (navigatorContext == null || ! navigatorContext.mounted) {
283+ return ;
284+ }
285+
286+ final widget = notification.buildDialog (navigatorContext);
287+
288+ if (navigatorContext.mounted) {
289+ await VoicesDialog .show <void >(
290+ context: navigatorContext,
291+ routeSettings: RouteSettings (name: '/dialog-${notification .id }' ),
292+ builder: (context) => widget,
293+ );
294+ }
295+ }
193296}
0 commit comments