-
Notifications
You must be signed in to change notification settings - Fork 263
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
notif: Use associated account as initial account; if opened from background #1240
Changes from all commits
6765f66
05628ca
4ece284
6d7c751
bd70287
4b2f51e
fb1b97f
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 |
---|---|---|
|
@@ -139,6 +139,52 @@ class ZulipApp extends StatefulWidget { | |
} | ||
|
||
class _ZulipAppState extends State<ZulipApp> with WidgetsBindingObserver { | ||
@override | ||
void initState() { | ||
super.initState(); | ||
WidgetsBinding.instance.addObserver(this); | ||
} | ||
|
||
@override | ||
void dispose() { | ||
WidgetsBinding.instance.removeObserver(this); | ||
super.dispose(); | ||
} | ||
|
||
List<Route<dynamic>> _handleGenerateInitialRoutes(String initialRoute) { | ||
// The `_ZulipAppState.context` lacks the required ancestors. Instead | ||
// we use the Navigator which should be available when this callback is | ||
// called and it's context should have the required ancestors. | ||
final context = ZulipApp.navigatorKey.currentContext!; | ||
|
||
final initialRouteUrl = Uri.tryParse(initialRoute); | ||
if (initialRouteUrl case Uri(scheme: 'zulip', host: 'notification')) { | ||
final route = NotificationDisplayManager.routeForNotification( | ||
context: context, | ||
url: initialRouteUrl); | ||
|
||
if (route != null) { | ||
return [ | ||
HomePage.buildRoute(accountId: route.accountId), | ||
route, | ||
Comment on lines
+166
to
+169
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. nit:
semicolon ";" doesn't fit for this; use "," |
||
]; | ||
} else { | ||
// The account didn't match any existing accounts, | ||
// fall through to show the default route below. | ||
} | ||
} | ||
|
||
final globalStore = GlobalStoreWidget.of(context); | ||
// TODO(#524) choose initial account as last one used | ||
final initialAccountId = globalStore.accounts.firstOrNull?.id; | ||
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. How about doing this (checking the contents of Then in particular we don't have to think about what happens if the —ah, I see: its position outside the callback is the same in 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. Seems also nice if it's below the open-from-notification code in that callback, so it's easier to see that it's not part of that logic. |
||
return [ | ||
if (initialAccountId == null) | ||
MaterialWidgetRoute(page: const ChooseAccountPage()) | ||
else | ||
HomePage.buildRoute(accountId: initialAccountId), | ||
]; | ||
} | ||
|
||
@override | ||
Future<bool> didPushRouteInformation(routeInformation) async { | ||
switch (routeInformation.uri) { | ||
|
@@ -152,71 +198,39 @@ class _ZulipAppState extends State<ZulipApp> with WidgetsBindingObserver { | |
return super.didPushRouteInformation(routeInformation); | ||
} | ||
|
||
Future<void> _handleInitialRoute() async { | ||
final initialRouteUrl = Uri.parse(WidgetsBinding.instance.platformDispatcher.defaultRouteName); | ||
if (initialRouteUrl case Uri(scheme: 'zulip', host: 'notification')) { | ||
await NotificationDisplayManager.navigateForNotification(initialRouteUrl); | ||
} | ||
} | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
WidgetsBinding.instance.addObserver(this); | ||
_handleInitialRoute(); | ||
} | ||
|
||
@override | ||
void dispose() { | ||
WidgetsBinding.instance.removeObserver(this); | ||
super.dispose(); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final themeData = zulipThemeData(context); | ||
return GlobalStoreWidget( | ||
child: Builder(builder: (context) { | ||
final globalStore = GlobalStoreWidget.of(context); | ||
// TODO(#524) choose initial account as last one used | ||
final initialAccountId = globalStore.accounts.firstOrNull?.id; | ||
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.
Can that actually happen? I suspect it can't. Or rather: the actual scenario where this would make a non-NFC difference is if the account gets logged out between this builder callback (underneath Not worth spending time investigating that, if you don't already know of a way it can happen after all —
if that's an accurate description of what you know. 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.
Correct, I'm not aware of a way it can happen after all. |
||
return MaterialApp( | ||
onGenerateTitle: (BuildContext context) { | ||
return ZulipLocalizations.of(context).zulipAppTitle; | ||
}, | ||
localizationsDelegates: ZulipLocalizations.localizationsDelegates, | ||
supportedLocales: ZulipLocalizations.supportedLocales, | ||
theme: themeData, | ||
|
||
navigatorKey: ZulipApp.navigatorKey, | ||
navigatorObservers: widget.navigatorObservers ?? const [], | ||
builder: (BuildContext context, Widget? child) { | ||
if (!ZulipApp.ready.value) { | ||
SchedulerBinding.instance.addPostFrameCallback( | ||
(_) => widget._declareReady()); | ||
} | ||
GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context); | ||
return child!; | ||
}, | ||
|
||
// We use onGenerateInitialRoutes for the real work of specifying the | ||
// initial nav state. To do that we need [MaterialApp] to decide to | ||
// build a [Navigator]... which means specifying either `home`, `routes`, | ||
// `onGenerateRoute`, or `onUnknownRoute`. Make it `onGenerateRoute`. | ||
// It never actually gets called, though: `onGenerateInitialRoutes` | ||
// handles startup, and then we always push whole routes with methods | ||
// like [Navigator.push], never mere names as with [Navigator.pushNamed]. | ||
onGenerateRoute: (_) => null, | ||
|
||
onGenerateInitialRoutes: (_) { | ||
return [ | ||
if (initialAccountId == null) | ||
MaterialWidgetRoute(page: const ChooseAccountPage()) | ||
else | ||
HomePage.buildRoute(accountId: initialAccountId), | ||
]; | ||
}); | ||
})); | ||
child: MaterialApp( | ||
onGenerateTitle: (BuildContext context) { | ||
return ZulipLocalizations.of(context).zulipAppTitle; | ||
}, | ||
localizationsDelegates: ZulipLocalizations.localizationsDelegates, | ||
supportedLocales: ZulipLocalizations.supportedLocales, | ||
theme: themeData, | ||
|
||
navigatorKey: ZulipApp.navigatorKey, | ||
navigatorObservers: widget.navigatorObservers ?? const [], | ||
builder: (BuildContext context, Widget? child) { | ||
if (!ZulipApp.ready.value) { | ||
SchedulerBinding.instance.addPostFrameCallback( | ||
(_) => widget._declareReady()); | ||
} | ||
GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context); | ||
return child!; | ||
}, | ||
|
||
// We use onGenerateInitialRoutes for the real work of specifying the | ||
// initial nav state. To do that we need [MaterialApp] to decide to | ||
// build a [Navigator]... which means specifying either `home`, `routes`, | ||
// `onGenerateRoute`, or `onUnknownRoute`. Make it `onGenerateRoute`. | ||
// It never actually gets called, though: `onGenerateInitialRoutes` | ||
// handles startup, and then we always push whole routes with methods | ||
// like [Navigator.push], never mere names as with [Navigator.pushNamed]. | ||
onGenerateRoute: (_) => null, | ||
|
||
onGenerateInitialRoutes: _handleGenerateInitialRoutes)); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -960,11 +960,12 @@ void main() { | |
group('NotificationDisplayManager open', () { | ||
late List<Route<void>> pushedRoutes; | ||
|
||
void takeStartingRoutes({bool withAccount = true}) { | ||
void takeStartingRoutes({Account? account, bool withAccount = true}) { | ||
account ??= eg.selfAccount; | ||
final expected = <Condition<Object?>>[ | ||
if (withAccount) | ||
(it) => it.isA<MaterialAccountWidgetRoute>() | ||
..accountId.equals(eg.selfAccount.id) | ||
..accountId.equals(account!.id) | ||
..page.isA<HomePage>() | ||
else | ||
(it) => it.isA<WidgetRoute>().page.isA<ChooseAccountPage>(), | ||
|
@@ -1036,6 +1037,21 @@ void main() { | |
eg.dmMessage(from: eg.otherUser, to: [eg.selfUser])); | ||
}); | ||
|
||
testWidgets('account queried by realmUrl origin component', (tester) async { | ||
addTearDown(testBinding.reset); | ||
await testBinding.globalStore.add( | ||
eg.selfAccount.copyWith(realmUrl: Uri.parse('http://chat.example')), | ||
eg.initialSnapshot()); | ||
await prepare(tester); | ||
|
||
await checkOpenNotification(tester, | ||
eg.selfAccount.copyWith(realmUrl: Uri.parse('http://chat.example/')), | ||
eg.streamMessage()); | ||
await checkOpenNotification(tester, | ||
eg.selfAccount.copyWith(realmUrl: Uri.parse('http://chat.example')), | ||
eg.streamMessage()); | ||
}); | ||
|
||
testWidgets('no accounts', (tester) async { | ||
await prepare(tester, withAccount: false); | ||
await openNotification(tester, eg.selfAccount, eg.streamMessage()); | ||
|
@@ -1112,11 +1128,12 @@ void main() { | |
realmUrl: data.realmUrl, | ||
userId: data.userId, | ||
narrow: switch (data.recipient) { | ||
FcmMessageChannelRecipient(:var streamId, :var topic) => | ||
TopicNarrow(streamId, topic), | ||
FcmMessageDmRecipient(:var allRecipientIds) => | ||
DmNarrow(allRecipientIds: allRecipientIds, selfUserId: data.userId), | ||
}).buildUrl(); | ||
FcmMessageChannelRecipient(:var streamId, :var topic) => | ||
TopicNarrow(streamId, topic), | ||
FcmMessageDmRecipient(:var allRecipientIds) => | ||
DmNarrow(allRecipientIds: allRecipientIds, selfUserId: data.userId), | ||
}).buildUrl(); | ||
addTearDown(tester.binding.platformDispatcher.clearDefaultRouteNameTestValue); | ||
tester.binding.platformDispatcher.defaultRouteNameTestValue = intentDataUrl.toString(); | ||
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. nit: put the |
||
|
||
// Now start the app. | ||
|
@@ -1129,6 +1146,36 @@ void main() { | |
takeStartingRoutes(); | ||
matchesNavigation(check(pushedRoutes).single, account, message); | ||
}); | ||
|
||
testWidgets('uses associated account as initial account; if initial route', (tester) async { | ||
addTearDown(testBinding.reset); | ||
|
||
final accountA = eg.selfAccount; | ||
final accountB = eg.otherAccount; | ||
final message = eg.streamMessage(); | ||
final data = messageFcmMessage(message, account: accountB); | ||
await testBinding.globalStore.add(accountA, eg.initialSnapshot()); | ||
await testBinding.globalStore.add(accountB, eg.initialSnapshot()); | ||
|
||
final intentDataUrl = NotificationOpenPayload( | ||
realmUrl: data.realmUrl, | ||
userId: data.userId, | ||
narrow: switch (data.recipient) { | ||
FcmMessageChannelRecipient(:var streamId, :var topic) => | ||
TopicNarrow(streamId, topic), | ||
FcmMessageDmRecipient(:var allRecipientIds) => | ||
DmNarrow(allRecipientIds: allRecipientIds, selfUserId: data.userId), | ||
}).buildUrl(); | ||
addTearDown(tester.binding.platformDispatcher.clearDefaultRouteNameTestValue); | ||
tester.binding.platformDispatcher.defaultRouteNameTestValue = intentDataUrl.toString(); | ||
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. This looks like it needs a corresponding teardown, with |
||
|
||
await prepare(tester, early: true); | ||
check(pushedRoutes).isEmpty(); // GlobalStore hasn't loaded yet | ||
|
||
await tester.pump(); | ||
takeStartingRoutes(account: accountB); | ||
matchesNavigation(check(pushedRoutes).single, accountB, message); | ||
}); | ||
}); | ||
|
||
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.
nit: this change is now happening in the commit
6c35bea notif: Use associated account as initial account; if opened from background
instead of in the intended commit
9f68ba3 notif: Query account by realm URL origin, not full URL
Should move back to the latter commit.