-
Notifications
You must be signed in to change notification settings - Fork 306
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 ios: Handle opening of conversation on tap; take 2 #1379
base: main
Are you sure you want to change the base?
Conversation
463b9ee
to
8478226
Compare
0142dbd
to
89df63b
Compare
89df63b
to
4f3224a
Compare
80a34eb
to
9c07740
Compare
Ah this has gathered a conflict in lib/widgets/app.dart; could you resolve it please? (I see you did a few days ago, but looks like it's happened again; thanks. 🙂) |
3b50218
to
2178fe6
Compare
(Rebased to main, Thanks!) |
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.
Thanks! There's a lot here, and I haven't gotten around to it all today. But here are some comments from an initial review.
lib/widgets/app.dart
Outdated
GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context); | ||
return child!; | ||
}, | ||
return DeferrredBuilderWidget( |
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: check spelling (here and in commit message)
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.
(also spelling of _ZulipAppState.initState
in the commit message)
lib/widgets/store.dart
Outdated
/// Provides access to the app's data. | ||
/// | ||
/// There should be one of this widget, near the root of the tree. | ||
/// | ||
/// See also: | ||
/// * [GlobalStoreWidget.of], to get access to the data. | ||
/// * [PerAccountStoreWidget], for the user's data associated with a | ||
/// particular Zulip account. | ||
class GlobalStoreWidget extends StatefulWidget { | ||
// This is separate from [GlobalStoreWidget] only because we need | ||
// a [StatefulWidget] to get hold of the store, and an [InheritedWidget] to | ||
// provide it to descendants, and one widget can't be both of those. | ||
class GlobalStoreWidget extends InheritedNotifier<GlobalStore> { |
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.
Did you mean to move this implementation comment here and delete the dartdoc? The implementation comment doesn't make sense in this context—saying GlobalStoreWidget
is "separate from" GlobalStoreWidget
.
test/widgets/content_test.dart
Outdated
child: PerAccountStoreWidget(accountId: eg.selfAccount.id, | ||
child: RealmContentNetworkImage(src)))); | ||
await tester.pumpWidget(DeferrredBuilderWidget( | ||
future: ZulipBinding.instance.getGlobalStoreUniquely(), |
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.
We normally do this as testBinding.getGlobalStoreUniquely
, right?
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.
Also, for these tests that aren't specifically about GlobalStoreWidget
, it would be simpler to use TestZulipApp
instead, I think.
test/widgets/store_test.dart
Outdated
store: store, | ||
child: PerAccountStoreWidget( | ||
accountId: accountId, | ||
child: MyWidgetWithMixin(key: widgetWithMixinKey))); |
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.
Maybe we can use TestZulipApp
for this test?
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.
Used TestZulipApp
in tests where possible, but kept this unchanged because I was getting the same extraneous dep changes mentioned in the above TODO.
lib/notifications/open.dart
Outdated
case TargetPlatform.android: | ||
case TargetPlatform.fuchsia: | ||
case TargetPlatform.linux: | ||
case TargetPlatform.macOS: | ||
case TargetPlatform.windows: | ||
// Do nothing; we don't offer notifications on these platforms. | ||
break; |
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.
The comment is wrong about Android; we do offer notifications on Android.
A lot of the code added in this commit is implemented just for iOS, but named/documented as though it's cross-platform, because it doesn't say it's just for iOS.
I don't know if we plan to align the implementation with the names/docs or vice versa. The answer might be in the later commits (I haven't read them yet), but it would be helpful to comment on this in the commit message, I think.
lib/widgets/app.dart
Outdated
List<Route<dynamic>> _handleGenerateInitialRoutesIos(_) { | ||
// 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. |
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: "its"
test/notifications/open_test.dart
Outdated
await tester.pump(); | ||
takeStartingRoutes(); | ||
matchesNavigation(check(pushedRoutes).single, account, message); | ||
debugDefaultTargetPlatformOverride = null; |
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.
Hmm, awkward to have this teardown line so far away from its corresponding setup line.
How about instead passing variant: const TargetPlatformVariant({TargetPlatform.iOS}))
to testWidgets
?
final route = _routeForNotification(context, payload); | ||
if (route == null) return; // TODO(log) | ||
|
||
// TODO(nav): Better interact with existing nav stack on notif open |
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.
Maybe we should have an iOS counterpart to
for this? Or make that a both-platforms issue? That issue says:
(The iOS counterpart is covered by #1147, for navigating at all when a notification is tapped.)
but it seems reasonable to postpone this part of it; we'd just want to keep track of it.
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.
After this PR is merged the potential fix for that issue would work on both platforms, because this PR consolidates the notification routing implementation on both iOS and Android.
pigeon/notifications.dart
Outdated
@EventChannelApi() | ||
abstract class NotificationHostEvents { | ||
/// An event stream that emits a notification payload when | ||
/// app encounters a notification tap, while the app is runnning. |
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: "the app encounters a notification tap, while the app is running."
test/notifications/open_test.dart
Outdated
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); | ||
await prepare(tester); | ||
await checkOpenNotification(tester, eg.selfAccount, eg.streamMessage()); | ||
debugDefaultTargetPlatformOverride = null; |
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.
(same comment about maybe using variant: const TargetPlatformVariant({TargetPlatform.iOS})
)
2178fe6
to
616defe
Compare
Thanks for the review @chrisbobbe! Pushed a new revision, PTAL. |
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.
Thanks! Here's a more thorough review of the first four commits:
d1f648c app: Use DeferredBuilderWidget while loading GlobalStore
a5a0fff pigeon [nfc]: Rename pigeon file to notification
-> android_notifications
ab3ff84 notif ios: Navigate when app launched from notification
4b2ade0 notif ios: Navigate when app running but in background
That leaves the last two commits:
28ea77f notif android: Migrate to cross-platform Pigeon API for navigation
616defe docs: Document testing push notifications on iOS Simulator
Actually for your next revision, could you send a new PR with everything except the "Migrate to cross-platform" commit? That one's pretty large, so makes sense to review separately.
ios/Runner/Notifications.g.swift
Outdated
@@ -0,0 +1,167 @@ | |||
// Autogenerated from Pigeon (v24.2.1), do not edit directly. |
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.
This needs an update for the Pigeon 25 upgrade e2aac35.
lib/widgets/app.dart
Outdated
/// The widget to build when [future] completes, with it's result | ||
/// passed as `result`. |
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: "its"
@@ -381,7 +381,7 @@ data class StoredNotificationSound ( | |||
) | |||
} | |||
} | |||
private open class NotificationsPigeonCodec : StandardMessageCodec() { | |||
private open class AndroidNotificationsPigeonCodec : StandardMessageCodec() { |
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.
Commit-message nit:
pigeon [nfc]: Rename pigeon file to `notification` -> `android_notifications`
I think the "to" should be deleted? Or moved to replace the "->"?
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.
pigeon [nfc]: Rename pigeon file `notifications.dart` to `android_notifications.dart`
commit-message nit: limit summary line length to 76 (this is 85).
pigeon/notifications.dart
Outdated
/// On iOS, this checks and returns value for the `remoteNotification` key | ||
/// in the `launchOptions` map. The value could be either the raw APNs data | ||
/// dictionary, if the launch of the app was triggered by a notification tap, | ||
/// otherwise it will be null. | ||
/// | ||
/// See: https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification |
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.
Can be a bit more concise I think:
/// Returns `launchOptions.remoteNotification`,
/// which is the raw APNs data dictionary
/// if the app launch was opened by a notification tap,
/// else null. See Apple doc:
/// https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey/remotenotification
And this is only used on iOS at this commit, right; the "On iOS" can be added in the later commit where it also starts being used on Android.
test/model/binding.dart
Outdated
FakeNotificationPigeonApi? _notificationPigeonApi; | ||
|
||
@override | ||
FakeNotificationPigeonApi get notificationPigeonApi { | ||
return (_notificationPigeonApi ??= FakeNotificationPigeonApi()); | ||
} |
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.
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.
Also, can use fewer lines:
@override
FakeNotificationPigeonApi get notificationPigeonApi =>
(_notificationPigeonApi ??= FakeNotificationPigeonApi());
ios/Runner/AppDelegate.swift
Outdated
func onNotificationTapEvent(data: NotificationPayloadForOpen) { | ||
if let eventSink = eventSink { | ||
eventSink.success(data) | ||
} | ||
} |
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.
Instead of the one class NotificationPayloadForOpen
, could we have two separate classes NotificationDataFromLaunch
and NotificationTapEvent
? Perhaps with a Payload
typedef helper:
typedef Payload = Map<Object?, Object?>;
class NotificationDataFromLaunch {
const NotificationDataFromLaunch({required this.payload});
/// The raw payload that is attached to the notification,
/// holding the information required to carry out the navigation.
final Payload payload;
}
class NotificationTapEvent {
const NotificationTapEvent({required this.payload});
/// The raw payload that is attached to the notification,
/// holding the information required to carry out the navigation.
final Payload payload;
}
I think the current naming makes the event-channel code harder to read than it needs to be. When reading the Pigeon example code, I see "event" used pretty consistently in the names of things. Here, we have both "data" (onNotificationTapEvent
's param) and "payload", and "payload" is used ambiguously for a payload (NotificationPayloadForOpen.payload
) and something that contains a payload (NotificationPayloadForOpen
itself).
That would mean, for this method:
func onNotificationTapEvent(event: NotificationTapEvent) {
if let eventSink = eventSink {
eventSink.success(event)
}
}
pigeon/notifications.dart
Outdated
@EventChannelApi() | ||
abstract class NotificationHostEvents { |
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.
Would NotificationEventChannelApi
be a better name for this, to make it clearer what kind of thing it is?
lib/model/binding.dart
Outdated
// in global scope of the generated file. This is a helper class to | ||
// namespace the notification related Pigeon API under a single class. | ||
class NotificationPigeonApi { | ||
final _notifInteractionHost = notif_pigeon.NotificationHostApi(); |
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.
Is _notifInteractionHost
the best name for this? How about _hostApi
? We might add more to NotificationHostApi
that's not about interacting with notifications (such as a method to query the current notification permissions). Also, NotificationHostApi
isn't the only code that's about interacting with notifications; notif_pigeon.notificationTapEvents
is too.
lib/notifications/open.dart
Outdated
/// Navigates to the [MessageListPage] of the specific conversation | ||
/// for the provided payload that was attached while creating the | ||
/// notification. | ||
Future<void> _navigateForNotification(NotificationPayloadForOpen payload) async { |
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.
Another reason the NotificationPayloadForOpen
rename would be helpful: currently, it's pretty unclear that this private method is only about notifications that come while the app is open. It'll be helpful for debugging if it's easier to see what code is about the launch notification vs. not.
test/notifications/open_test.dart
Outdated
|
||
testWidgets('(iOS) stream message', (tester) async { | ||
addTearDown(testBinding.reset); | ||
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); |
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.
This add-self-account line looks boring; can we put it in prepare
?
173f11f
to
9813a4d
Compare
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.
Thanks! Just small comments below.
@@ -381,7 +381,7 @@ data class StoredNotificationSound ( | |||
) | |||
} | |||
} | |||
private open class NotificationsPigeonCodec : StandardMessageCodec() { | |||
private open class AndroidNotificationsPigeonCodec : StandardMessageCodec() { |
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.
pigeon [nfc]: Rename pigeon file `notifications.dart` to `android_notifications.dart`
commit-message nit: limit summary line length to 76 (this is 85).
test/model/binding.dart
Outdated
@override | ||
FakeNotificationPigeonApi get notificationPigeonApi => | ||
(_notificationPigeonApi ??= FakeNotificationPigeonApi()); | ||
|
||
FakeNotificationPigeonApi? _notificationPigeonApi; |
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: no blank line between getter and private field, I think
ios/Runner/AppDelegate.swift
Outdated
func onNotificationTapEvent(data: NotificationTapEvent) { | ||
if let eventSink = eventSink { | ||
eventSink.success(data) | ||
} | ||
} |
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.
bump on part of #1379 (comment):
func onNotificationTapEvent(data: NotificationTapEvent) { | |
if let eventSink = eventSink { | |
eventSink.success(data) | |
} | |
} | |
func onNotificationTapEvent(event: NotificationTapEvent) { | |
if let eventSink = eventSink { | |
eventSink.success(event) | |
} | |
} |
0e9a281
to
4e70f64
Compare
Thanks for the review @chrisbobbe! Pushed an update, PTAL. |
Thanks! LGTM; marking for Greg's review. |
4e70f64
to
7cda0d0
Compare
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.
Thanks @rajveermalviya for building this, and @chrisbobbe for the previous reviews!
Here's a review just on the first part, as a start:
5eb2d1b app: Use DeferredBuilderWidget while loading GlobalStore
0639411 pigeon [nfc]: Rename notifications.dart
to android_notifications.dart
5a462cb dialog [nfc]: Document required ancestors for BuildContext in showErrorDialog
be90183 notif: Fix error message when account not found in store
e9ce0d8 binding test [nfc]: Reorder androidNotificationHost getter
(i.e. mainly on the first commit). I'll look closer at the subsequent, main commits in a next round.
assets/l10n/app_en.arb
Outdated
"errorNotificationOpenAccountMissing": "The account associated with this notification no longer exists.", | ||
"@errorNotificationOpenAccountMissing": { | ||
"description": "Error message when the account associated with the notification is not found" | ||
"errorNotificationOpenAccountLoggedOut": "The account associated with this notification was logged out.", |
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.
Hmm, definitely good to change it away from "account … no longer exists" — that sounds like it's saying something about the account itself disappearing, from the server where it lives.
I think "was logged out" is a bit too specific, though. We should only be getting a notification for an unknown account if the client was previously logged into it, hence has been logged out; but it's possible to imagine a server bug that would make that not the case.
Instead, how about:
"errorNotificationOpenAccountNotFound": "The account associated with this notification could not be found.",
@@ -58,43 +58,17 @@ extension MyWidgetWithMixinStateChecks on Subject<MyWidgetWithMixinState> { | |||
void main() { | |||
TestZulipBinding.ensureInitialized(); | |||
|
|||
testWidgets('GlobalStoreWidget loads data while showing placeholder', (tester) async { |
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.
This functionality should continue to get tested, though the test will now be about ZulipApp (and live in the corresponding test file) instead.
lib/widgets/app.dart
Outdated
/// A widget that defers the builder until the provided [future] completes. | ||
/// | ||
/// It shows a placeholder widget while it waits for the [future] | ||
/// to complete. | ||
class DeferredBuilderWidget<T> extends StatefulWidget { |
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.
Seeing this widget class with this quite generic name and description makes me think of the widget Flutter provides for this upstream, FutureBuilder, and wonder about what the differences are and why that's not used.
The FutureBuilder API is somewhat more complicated, partly in order to handle errors, whereas we ignore errors here. I think that simplification would be straightforward to package up as a DeferredBuilderWidget that is implemented via FutureBuilder, though.
Then looking at FutureBuilder causes me to notice one subtlety: if the DeferredBuilderWidget
's parent gets rebuilt and provides a new future, the DeferredBuilderWidget
never actually looks at the new future. That seems buggy. It doesn't affect our use case because the new future is always substantively the same; but on a nice generic interface like this, it's a bug waiting to happen with a future use of this class.
We could fix that with more effort, but I think the cleanest thing may be to just make this logic specialized again, without the generality. As is, all the call sites of the DeferredBuilderWidget constructor are wrapped around a GlobalStoreWidget, and vice versa.
I'll describe a way to do that in the next comment.
lib/widgets/app.dart
Outdated
@override | ||
void initState() { | ||
super.initState(); | ||
WidgetsBinding.instance.addObserver(this); | ||
_globalStoreFuture = ZulipBinding.instance.getGlobalStoreUniquely(); |
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.
This can do the job of both DeferredBuilderWidget and the Future.wait, in just a few lines of code because specialized to our use case:
_globalStoreFuture = ZulipBinding.instance.getGlobalStoreUniquely(); | |
() async { | |
final globalStore = await ZulipBinding.instance.getGlobalStoreUniquely(); | |
final notifFuture = NotificationOpenManager.instance.initializationFuture; | |
if (notifFuture != null) await notifFuture; | |
if (!mounted) return; | |
setState(() { | |
this.globalStore = globalStore; | |
}); | |
}(); |
Relative to the status quo in main (or to a version after a refactor like the one in your first commit, moving the loading up to ZulipApp), this differs only in adding the two lines for notifFuture
.
Because the underlying asynchronous work behind initializationFuture
is already started by main
before reaching this point, the only thing this needs to do to make the loading happen concurrently is to be sure to call getGlobalStoreUniquely()
before awaiting initializationFuture
.
test/widgets/store_test.dart
Outdated
child: DeferredBuilderWidget( | ||
future: testBinding.getGlobalStoreUniquely(), | ||
builder: (context, store) { | ||
return GlobalStoreWidget( | ||
store: store, |
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.
For these tests that aren't about GlobalStoreWidget and how it would formerly load data, I think they don't need DeferredBuilderWidget or an analogue; they can just use (the new) GlobalStoreWidget directly, and have the caller do its own await to get the actual global store to pass in.
// This is separate from [GlobalStoreWidget] only because we need | ||
// a [StatefulWidget] to get hold of the store, and an [InheritedWidget] to | ||
// provide it to descendants, and one widget can't be both of those. | ||
class _GlobalStoreInheritedWidget extends InheritedNotifier<GlobalStore> { |
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.
There's a comment below which refers to this class, so needs to be updated 🙂
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.
Here's a review on the handy new page of docs.
Using the canned payloads, I successfully tested this PR, and opening a notification works!
I didn't get to testing it more end-to-end, with a real (dev) server; see questions below.
user `[email protected]`:_ | ||
|
||
<details> | ||
<summary>Preforged payload: dm.json</summary> |
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.
These canned payloads should be very helpful, thanks.
Let's include next to them a mention of what server version they were generated from — commit ID for complete unambiguity, feature level for consulting the API changelog to scan for likely changes since then, and a calendar time like "early 2025" for a quick sense of age without having to look anything up. That will help a reader a year or two from now be able to confidently make use of these.
Apple's Push Notification service to show notifications on iOS | ||
Simulator. | ||
|
||
## 1. Setup dev server |
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.
This is basically optional in the same way as steps 2–5, right?
I guess if the reader uses some other realm like chat.zulip.org, they'll need to edit the canned payloads accordingly. Should be pretty doable, though less reliable (easy to miss something).
But even if they're using a dev server, the part about making it use the production bouncer is only needed if doing steps 2–5, right? Otherwise, one only needs this sub-list of instructions:
https://github.com/zulip/zulip-mobile/blob/main/docs/howto/dev-server.md
[Follow the steps in this section to setup a development server, and | ||
register the development server to the production bouncer](https://github.com/zulip/zulip-mobile/blob/main/docs/howto/push-notifications.md#server). |
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.
Did this work for you verbatim? I think it needs some updating.
First I got this error:
$ python manage.py register_server --agree_to_terms_of_service
usage: manage.py register_server [-h] [--version] [-v {0,1,2,3}]
[--settings SETTINGS] [--pythonpath PYTHONPATH]
[--traceback] [--no-color] [--force-color]
[--skip-checks] [--agree-to-terms-of-service]
[--rotate-key | --registration-transfer | --deactivate]
[--automated | --no-automated]
manage.py register_server: error: unrecognized arguments: --agree_to_terms_of_service
After fixing the spelling of the option, I got this error:
$ python manage.py register_server --agree-to-terms-of-service
This command registers your server for the Mobile Push Notifications Service.
Doing so will share basic metadata with the service's maintainers:
* This server's configured hostname: zulipdev.com:9991
* This server's configured contact email address: [email protected]
* Metadata about each organization hosted by the server; see:
<https://zulip.com/doc-permalinks/basic-metadata>
Use of this service is governed by the Zulip Terms of Service:
<https://zulip.com/policies/terms>
Network error connecting to push notifications service (http://push.zulipdev.com:9991)
at which point I guess further debugging is needed to work out why it's looking at that busted URL and how to point it at the right one.
Do you have a record of what you did differently from the existing instructions?
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.
It looks like after the commit zulip/zulip@4a93149 there is no need to do the push notifications registration manually, just running the dev server should work fine. Tested with fresh dev environment and verified that it works.
device ID, you can get the device ID by running the following command: | ||
|
||
```shell-session | ||
$ xcrun simctl list 'devices' 'booted' |
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: these quotes have no effect; cleaner to leave them out, since these arguments are in the style of identifiers
$ xcrun simctl list 'devices' 'booted' | |
$ xcrun simctl list devices booted |
_If you skipped steps 2-5, you'll need pre-forged APNs payloads for | ||
existing messages in a default development server messages for the | ||
user `[email protected]`:_ |
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.
These worked great for me (because I haven't gotten the dev server working with the bouncer — see question above), with one change:
I replaced the string localhost
throughout with the IP address my dev server was using in its EXTERNAL_HOST, per the instructions at https://github.com/zulip/zulip-mobile/blob/main/docs/howto/dev-server.md .
(If my dev server were located on the Mac where the simulator is running, then localhost
would work fine, as per https://github.com/zulip/zulip-mobile/blob/main/docs/howto/dev-server.md#ios-simulator-macos-only . But instead I have it running on a different machine, my Linux desktop, so it needs to use the address of that machine.)
For me that looked like:
$ perl -i -0pe s/localhost/192.168.0.113/g tmp/*.json
to make the strings say "http://192.168.0.113:9991"
instead of "http://localhost:9991"
.
Oh also let's put the doc commit before the two main "notif ios:" commits. It's basically describing how to manually test those commits and the functionality they're changing; and the instructions apply just as well before those changes as they do after them. |
7cda0d0
to
3db5b54
Compare
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.
I started reviewing the new revision, but then realized you hadn't actually said it was ready for review yet. 🙂 Anyway, here's the comments I wrote down so far, all on the first commit:
e6e19e1 app: Move initialization of GlobalStore from GlobalStoreWidget to ZulipApp
But I'll leave further re-review until you say it's ready.
test/widgets/store_test.dart
Outdated
GlobalStoreWidget( | ||
TestZulipApp( |
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.
This test's name no longer seems accurate:
GlobalStoreWidget loads data while showing placeholder
as GlobalStoreWidget no longer does either of those things.
Probably just move this test to app_test.dart. (cf #1379 (comment) )
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.
Oh hmm and this version of the test no longer really tests the app's code — instead it's testing the replica in TestZulipApp of that code.
Can that be fixed by just saying ZulipApp
here?
test/widgets/test_app.dart
Outdated
this.skipAssertAccountExists = false, | ||
this.navigatorObservers, | ||
this.child = const Placeholder(), | ||
}) : assert(!skipAssertAccountExists || accountId != null); | ||
|
||
final int? accountId; | ||
|
||
final Key? perAccountStoreWidgetKey; |
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.
This is kind of a weird feature for this to have. It really only makes sense for that one PerAccountStoreWidget test case that wants to be able to say this:
// But then if we mount a separate PerAccountStoreWidget...
// […]
// (... even one that really is separate, with its own fresh state node ...)
So I'd rather avoid adding it here to the interface of TestZulipApp, which lots and lots of tests are using.
I think there's actually an easy solution for that test case without this: just don't pass accountId
, and instead have the test continue supplying its own PerAccountStoreWidget
objects.
test/widgets/test_app.dart
Outdated
this.skipAssertAccountExists = false, | ||
this.navigatorObservers, | ||
this.child = const Placeholder(), | ||
}) : assert(!skipAssertAccountExists || accountId != null); | ||
|
||
final int? accountId; | ||
|
||
final Key? perAccountStoreWidgetKey; | ||
|
||
final ThemeData? theme; |
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.
This isn't quite as weird a feature, but it's still quite boutique: just one test case uses it. So I'd again rather avoid adding it to this widely-shared helper class.
If you want to convert that test case to use TestZulipApp, I think the simplest solution is to have the test just insert its own Theme
widget, so that it effectively ignores the theme that TestZulipApp put on the MaterialApp widget.
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.
OK, and I've now sat down and spent some time studying the Swift code (and reading docs to understand how it should work). Here's a round of review on that.
ios/Runner/AppDelegate.swift
Outdated
} | ||
} | ||
|
||
func onEventsDone() { |
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.
This method never gets called, right?
In particular I notice it doesn't have override
like onListen
does — so it's not going to get called by any generic Pigeon code. And I think we never would call it, because there's never a point when we now know that there won't be any future notifications that get opened.
Let's leave it out, then. I see it's there in the Pigeon example doc, but I think that's just to illustrate how one would implement such a method if one needed it.
ios/Runner/AppDelegate.swift
Outdated
if let listener = notificationTapEventListener { | ||
let userInfo = response.notification.request.content.userInfo | ||
listener.onNotificationTapEvent(event: NotificationTapEvent(payload: userInfo)) | ||
completionHandler() |
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.
We should be calling the completion handler unconditionally, I think, from my reading of the doc for the method we're implementing here:
https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate/usernotificationcenter(_:didreceive:withcompletionhandler:)
ios/Runner/AppDelegate.swift
Outdated
if let listener = notificationTapEventListener { | ||
let userInfo = response.notification.request.content.userInfo | ||
listener.onNotificationTapEvent(event: NotificationTapEvent(payload: userInfo)) |
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.
What if the user dismisses a notification, instead of tapping it to open?
I believe that will also cause this method to be called. So we should be looking at response.actionIdentifier
to determine what to do. See docs:
https://developer.apple.com/documentation/usernotifications/unnotificationresponse
This guide doc also seems helpful:
https://developer.apple.com/documentation/usernotifications/handling-notifications-and-notification-related-actions#Handle-user-selected-actions
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.
Hmm, I tested this and found that dismissing a notification doesn't cause that method to be called. (Even with the small func implemented to support foreground notifications on iOS)
The the docs for UNNotificationDismissActionIdentifier say:
The system delivers this action only if your app configured the notification’s category object with the customDismissAction option.
…
And from UNNotificationDefaultActionIdentifier:
The delivery of this action doesn’t require any special configuration of notification categories. Use the userNotificationCenter(_:didReceive:withCompletionHandler:) method of your delegate object to receive this action.
So, since we do not setup a notification category using setNotificationCategories, we don't need to handle UNNotificationDismissActionIdentifier
currently.
But adding a check anyway (response.actionIdentifier == UNNotificationDefaultActionIdentifier
) in case that changes in future.
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.
Ah I see, good to know. Sounds good, though.
ios/Runner/AppDelegate.swift
Outdated
didReceive response: UNNotificationResponse, | ||
withCompletionHandler completionHandler: @escaping () -> Void | ||
) { | ||
if let listener = notificationTapEventListener { |
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.
Can this property actually be nil here?
I think it can't. We set it (to a non-nil value) before setting the delegate, so before this method could possibly get called.
So we can simplify by force-unwrapping, as listener!
.
ios/Runner/AppDelegate.swift
Outdated
if let eventSink = eventSink { | ||
eventSink.success(event) | ||
} |
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.
This would do the same thing, right?
if let eventSink = eventSink { | |
eventSink.success(event) | |
} | |
eventSink?.success(event) |
ios/Runner/AppDelegate.swift
Outdated
) { | ||
if let listener = notificationTapEventListener { | ||
let userInfo = response.notification.request.content.userInfo | ||
listener.onNotificationTapEvent(event: NotificationTapEvent(payload: userInfo)) |
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.
I think this API would be a bit cleaner if we adjust it like so:
listener.onNotificationTapEvent(event: NotificationTapEvent(payload: userInfo)) | |
listener.onNotificationTapEvent(payload: userInfo) |
Then the NotificationTapEvent gets constructed by the method implementation.
That's also consistent with the Pigeon docs' example:
func onIntEvent(event: Int64) {
if let eventSink = eventSink {
eventSink.success(IntEvent(data: event))
}
}
func onStringEvent(event: String) {
if let eventSink = eventSink {
eventSink.success(StringEvent(data: event))
}
}
ios/Runner/AppDelegate.swift
Outdated
guard let controller = window?.rootViewController as? FlutterViewController else { | ||
fatalError("rootViewController is not type FlutterViewController") | ||
} |
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.
What did you look at to determine this was the way to set up the Pigeon channels?
(I'm not finding a clear answer as I look now in the Pigeon docs.)
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.
I had adapted this implementation by refering to Flutter's PlatformChannel examples and Pigeon's example.
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.
Excellent, thanks. This logic looks good, then.
For the style of it, let's use the terser as!
form, rather than throwing with a custom error message. It should be an invariant of our app that there'll be a FlutterViewController here, and I think that's a boring enough fact that we don't need to call extra attention to it.
418b205
to
b2bd272
Compare
Thanks for the review @gnprice! Pushed an update, PTAL. |
…art` This makes it clear that these bindings are for Android only.
…orDialog And fix a typo.
Introduces a new Pigeon API file, and adds the corresponding bindings in Swift. Unlike the `pigeon/android_notifications.dart` API this doesn't use the ZulipPlugin hack, as that is only needed when we want the Pigeon functions to be available inside a background isolate (see doc in `zulip_plugin/pubspec.yaml`). Since the notification tap will trigger an app launch first (if not running already) anyway, we can be sure that these new functions won't be running on a Dart background isolate, thus not needing the ZulipPlugin hack.
b2bd272
to
994a641
Compare
Fixes #1147.
2nd attempt, first attempt was #1261. This one uses pigeon to move most of the notification payload handling to dart side (and doesn't use/rely on
zulip://notification
URL).