-
Notifications
You must be signed in to change notification settings - Fork 306
notif: Preserve target account context on back navigation after opening a notification #1373
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ import '../model/narrow.dart'; | |
import '../widgets/app.dart'; | ||
import '../widgets/color.dart'; | ||
import '../widgets/dialog.dart'; | ||
import '../widgets/home.dart'; | ||
import '../widgets/message_list.dart'; | ||
import '../widgets/page.dart'; | ||
import '../widgets/store.dart'; | ||
|
@@ -214,8 +215,34 @@ class NotificationChannelManager { | |
} | ||
} | ||
|
||
/// Tracks the currently active account in the navigation stack | ||
class AccountNavigationObserver extends NavigatorObserver { | ||
int? _activeAccountId; | ||
int? get activeAccountId => _activeAccountId; | ||
|
||
@override | ||
void didPush(Route<dynamic> route, Route<void>? previousRoute) { | ||
_updateActiveAccount(route); | ||
} | ||
|
||
@override | ||
void didReplace({Route<dynamic>? newRoute, Route<void>? oldRoute}) { | ||
if (newRoute != null) { | ||
_updateActiveAccount(newRoute); | ||
} | ||
} | ||
|
||
void _updateActiveAccount(Route<void> route) { | ||
if (route is AccountRoute) { | ||
_activeAccountId = route.accountId; | ||
} | ||
} | ||
} | ||
|
||
/// Service for managing the notifications shown to the user. | ||
class NotificationDisplayManager { | ||
static final accountObserver = AccountNavigationObserver(); | ||
|
||
static Future<void> init() async { | ||
await NotificationChannelManager.ensureChannel(); | ||
} | ||
|
@@ -502,7 +529,14 @@ class NotificationDisplayManager { | |
final route = routeForNotification(context: context, url: url); | ||
if (route == null) return; // TODO(log) | ||
|
||
// TODO(nav): Better interact with existing nav stack on notif open | ||
final currentAccountId = accountObserver.activeAccountId; | ||
|
||
if (currentAccountId != route.accountId) { | ||
navigator.popUntil((r) => r.isFirst); | ||
HomePage.navigate(context, accountId: route.accountId); | ||
navigator = await ZulipApp.navigator; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Either way, we should not pop all routes unconditionally, because it doesn't feel like great user experience when the existing routes already belong to the same account. #F1210 suggests that we
I think what we need here is just a way to keep track of the currently active account, or a way to inspect the navigator stack without actually popping the routes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See discussion here: #mobile > Beta: after open via notification, back opens old server @ 💬. Feel free to ask questions if you have any! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on my current implementation, I have addressed the following:
|
||
|
||
unawaited(navigator.push(route)); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -959,6 +959,8 @@ void main() { | |
|
||
group('NotificationDisplayManager open', () { | ||
late List<Route<void>> pushedRoutes; | ||
late List<Route<void>> poppedRoutes; | ||
late TestNavigatorObserver testNavObserver; | ||
|
||
void takeStartingRoutes({Account? account, bool withAccount = true}) { | ||
account ??= eg.selfAccount; | ||
|
@@ -978,8 +980,14 @@ void main() { | |
{bool early = false, bool withAccount = true}) async { | ||
await init(); | ||
pushedRoutes = []; | ||
final testNavObserver = TestNavigatorObserver() | ||
..onPushed = (route, prevRoute) => pushedRoutes.add(route); | ||
poppedRoutes = []; | ||
testNavObserver = TestNavigatorObserver(); | ||
testNavObserver.onPushed = (route, prevRoute) => pushedRoutes.add(route); | ||
testNavObserver.onPopped = (route, prevRoute) => poppedRoutes.add(route); | ||
testNavObserver.onReplaced = (route, prevRoute) { | ||
poppedRoutes.add(prevRoute!); | ||
pushedRoutes.add(route!); | ||
}; | ||
// This uses [ZulipApp] instead of [TestZulipApp] because notification | ||
// logic uses `await ZulipApp.navigator`. | ||
await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver])); | ||
|
@@ -1018,7 +1026,7 @@ void main() { | |
|
||
Future<void> checkOpenNotification(WidgetTester tester, Account account, Message message) async { | ||
await openNotification(tester, account, message); | ||
matchesNavigation(check(pushedRoutes).single, account, message); | ||
check(pushedRoutes).any((it) => matchesNavigation(it, account, message)); | ||
pushedRoutes.clear(); | ||
} | ||
|
||
|
@@ -1176,6 +1184,80 @@ void main() { | |
takeStartingRoutes(account: accountB); | ||
matchesNavigation(check(pushedRoutes).single, accountB, message); | ||
}); | ||
|
||
testWidgets('notification switches account only when from different account', (tester) async { | ||
addTearDown(testBinding.reset); | ||
|
||
final accountA = eg.selfAccount; | ||
final accountB = eg.otherAccount; | ||
final message = eg.streamMessage(); | ||
|
||
await testBinding.globalStore.add(accountA, eg.initialSnapshot()); | ||
await testBinding.globalStore.add(accountB, eg.initialSnapshot()); | ||
|
||
await prepare(tester, early: true); | ||
await tester.pump(); | ||
takeStartingRoutes(account: accountA); | ||
|
||
await openNotification(tester, accountB, message); | ||
check(poppedRoutes).any((it) => it.isA<MaterialAccountWidgetRoute>() | ||
.accountId.equals(accountA.id)); | ||
check(pushedRoutes.last).isA<MaterialAccountWidgetRoute>() | ||
..accountId.equals(accountB.id) | ||
..page.isA<MessageListPage>(); | ||
}); | ||
|
||
testWidgets('notification preserves navigation stack when in same account', (tester) async { | ||
addTearDown(testBinding.reset); | ||
|
||
final account = eg.selfAccount; | ||
final message = eg.streamMessage(); | ||
|
||
await testBinding.globalStore.add(account, eg.initialSnapshot()); | ||
|
||
await prepare(tester, early: true); | ||
await tester.pump(); | ||
takeStartingRoutes(account: account); | ||
|
||
await openNotification(tester, account, message); | ||
check(pushedRoutes.last).isA<MaterialAccountWidgetRoute>() | ||
..accountId.equals(account.id) | ||
..page.isA<MessageListPage>(); | ||
|
||
check(poppedRoutes).isEmpty(); | ||
}); | ||
|
||
testWidgets('notification keeps AccountRoute in stack when opened from non-AccountRoute', (tester) async { | ||
addTearDown(testBinding.reset); | ||
|
||
final accountA = eg.selfAccount; | ||
final accountB = eg.otherAccount; | ||
final message = eg.streamMessage(); | ||
|
||
await testBinding.globalStore.add(accountA, eg.initialSnapshot()); | ||
await testBinding.globalStore.add(accountB, eg.initialSnapshot()); | ||
|
||
await prepare(tester, early: true); | ||
await tester.pump(); | ||
takeStartingRoutes(account: accountA); | ||
|
||
final navigator = await ZulipApp.navigator; | ||
unawaited(navigator.push(MaterialWidgetRoute(page: const ChooseAccountPage()))); | ||
await tester.pumpAndSettle(); | ||
|
||
await openNotification(tester, accountA, message); | ||
check(poppedRoutes).isEmpty(); | ||
check(pushedRoutes.last).isA<MaterialAccountWidgetRoute>() | ||
..accountId.equals(accountA.id) | ||
..page.isA<MessageListPage>(); | ||
|
||
await openNotification(tester, accountB, message); | ||
check(poppedRoutes).any((it) => it.isA<MaterialAccountWidgetRoute>() | ||
.accountId.equals(accountA.id)); | ||
check(pushedRoutes.last).isA<MaterialAccountWidgetRoute>() | ||
..accountId.equals(accountB.id) | ||
..page.isA<MessageListPage>(); | ||
}); | ||
}); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also have a test where the top-most route is a non- |
||
group('NotificationOpenPayload', () { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
currentAccountId
(i.e., account ID associated with the top-most route) isnull
, routes below the top-most route can still belong to the same account.HomePage.navigate
will pop those routes even though they belong to same account that receives this notification.