Skip to content

notif ios: Handle opening of conversation on tap; take 2 #1379

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ data class StoredNotificationSound (
)
}
}
private open class NotificationsPigeonCodec : StandardMessageCodec() {
private open class AndroidNotificationsPigeonCodec : StandardMessageCodec() {
Copy link
Collaborator

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 "->"?

Copy link
Collaborator

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).

override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
129.toByte() -> {
Expand Down Expand Up @@ -589,7 +589,7 @@ interface AndroidNotificationHostApi {
companion object {
/** The codec used by AndroidNotificationHostApi. */
val codec: MessageCodec<Any?> by lazy {
NotificationsPigeonCodec()
AndroidNotificationsPigeonCodec()
}
/** Sets up an instance of `AndroidNotificationHostApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
Expand Down
6 changes: 3 additions & 3 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -848,9 +848,9 @@
"@errorNotificationOpenTitle": {
"description": "Error title when notification opening fails"
},
"errorNotificationOpenAccountMissing": "The account associated with this notification no longer exists.",
"@errorNotificationOpenAccountMissing": {
"description": "Error message when the account associated with the notification is not found"
"errorNotificationOpenAccountNotFound": "The account associated with this notification could not be found.",
"@errorNotificationOpenAccountNotFound": {
"description": "Error message when the account associated with the notification could not be found"
},
"errorReactionAddingFailedTitle": "Adding reaction failed",
"@errorReactionAddingFailedTitle": {
Expand Down
4 changes: 0 additions & 4 deletions assets/l10n/app_pl.arb
Original file line number Diff line number Diff line change
Expand Up @@ -557,10 +557,6 @@
"@errorNotificationOpenTitle": {
"description": "Error title when notification opening fails"
},
"errorNotificationOpenAccountMissing": "Konto związane z tym powiadomieniem już nie istnieje.",
"@errorNotificationOpenAccountMissing": {
"description": "Error message when the account associated with the notification is not found"
},
"aboutPageOpenSourceLicenses": "Licencje otwartego źródła",
"@aboutPageOpenSourceLicenses": {
"description": "Item title in About Zulip page to navigate to Licenses page"
Expand Down
4 changes: 0 additions & 4 deletions assets/l10n/app_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,6 @@
"@errorNotificationOpenTitle": {
"description": "Error title when notification opening fails"
},
"errorNotificationOpenAccountMissing": "Учетной записи, связанной с этим оповещением, больше нет.",
"@errorNotificationOpenAccountMissing": {
"description": "Error message when the account associated with the notification is not found"
},
"switchAccountButton": "Сменить учетную запись",
"@switchAccountButton": {
"description": "Label for main-menu button leading to the choose-account page."
Expand Down
253 changes: 253 additions & 0 deletions docs/howto/push-notifications-ios-simulator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# Testing Push Notifications on iOS Simulator

For documentation on testing push notifications on Android or a real
iOS Device, see https://github.com/zulip/zulip-mobile/blob/main/docs/howto/push-notifications.md

This doc describes how to test client side changes on iOS Simulator.
It will demonstrate how to use APNs payloads the server sends to
Apple's Push Notification service to show notifications on iOS
Simulator.

## 1. (Optional) Setup dev server

_Skip to step 6 in which you will use canned notification payloads,
these intermediate steps records how to get those payloads, which may
be useful in future._

Follow
[this setup tutorial](https://zulip.readthedocs.io/en/latest/development/setup-recommended.html)
to setup and run the dev server on same the Mac machine that hosts
the iOS Simulator.

If you want to run the dev server on a different machine than the Mac
host, you'll need to follow extra steps
[documented here](https://github.com/zulip/zulip-mobile/blob/main/docs/howto/dev-server.md)
to make it possible for the app running on the iOS Simulator to
connect to the dev server.

## 2. (Optional) Setup the dev user to receive mobile notifications.

We'll use the devlogin user `[email protected]` to test notifications,
log in to that user by going to `/devlogin` on that server on Web.

And then follow the steps [here](https://zulip.com/help/mobile-notifications)
to enable Mobile Notifications for "Channels".

## 3. (Optional) Login to the dev user on zulip-flutter.

<!-- TODO(#405) Guide to use the new devlogin page instead -->

To login to this user in the Flutter app, you'll need the password
that was generated by the development server. You can print the
password by running this command inside your `vagrant ssh` shell:
```
$ ./manage.py print_initial_password [email protected]
```

Then run the app on the iOS Simulator, accept the permission to
receive push notifications, and then login to the dev user
(`[email protected]`).

## 4. (Optional) Edit the server code to log the notification payload.

We need to retrieve the APNs payload the server generates and sends
to the bouncer. To do that we can add a log statement after the
server completes generating the APNs in `zerver/lib/push_notifications.py`:

```diff
apns_payload = get_message_payload_apns(
user_profile,
message,
trigger,
mentioned_user_group_id,
mentioned_user_group_name,
can_access_sender,
)
gcm_payload, gcm_options = get_message_payload_gcm(
user_profile, message, mentioned_user_group_id, mentioned_user_group_name, can_access_sender
)
logger.info("Sending push notifications to mobile clients for user %s", user_profile_id)
+ logger.info("APNS payload %s", orjson.dumps(apns_payload))

android_devices = list(
PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.FCM).order_by("id")
```

## 5. (Optional) Send messages to the dev user

To generate notifications to the dev user `[email protected]` we need to
send messages from another user. For a variety of different types of
payloads try sending a message in a topic, a message in a group DM,
and one in one-one DM. Then look for the payloads in the server logs
by searching for "APNS payload".

The logged payload JSON will have different structure than what an
iOS device actually receives, to fix that, run the payload through
the following command:

```shell-session
$ echo '{"alert":{"title": ...' | jq '{aps: {alert: .alert, sound: .sound, badge: .badge}, zulip: .custom.zulip}'
```

## 6. Push APNs payload to iOS Simulator

_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]`:_
Comment on lines +94 to +96
Copy link
Member

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".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump — I'd like this information to not get lost for the next person.

Here's a version that could go in verbatim:

These canned payloads assume that EXTERNAL_HOST has its default value for the dev server. If you've set EXTERNAL_HOST to use an IP address in order to enable your device to connect to the dev server, you'll need to adjust the realm_url fields. You can do this by a find-and-replace for localhost; for example, perl -i -0pe s/localhost/10.0.2.2/g tmp/*.json after saving the canned payloads to files tmp/*.json.


These canned payloads were generated from Zulip Server 11.0-dev+git
8fd04b0f0, API Feature Level 377, in April 2025.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, thanks for adding the 9th digit here :-)


These canned payloads assume that EXTERNAL_HOST has its default value
for the dev server. If you've
[set EXTERNAL_HOST to use an IP address](https://github.com/zulip/zulip-mobile/blob/main/docs/howto/dev-server.md#4-set-external_host)
in order to enable your device to connect to the dev server, you'll
need to adjust the `realm_url` fields. You can do this by a
find-and-replace for `localhost`; for example,
`perl -i -0pe s/localhost/10.0.2.2/g tmp/*.json` after saving the
canned payloads to files `tmp/*.json`.

<details>
<summary>Payload: dm.json</summary>

```json
{
"aps": {
"alert": {
"title": "Zoe",
"subtitle": "",
"body": "But wouldn't that show you contextually who is in the audience before you have to open the compose box?"
},
"sound": "default",
"badge": 0,
},
"zulip": {
"server": "zulipdev.com:9991",
"realm_id": 2,
"realm_uri": "http://localhost:9991",
"realm_url": "http://localhost:9991",
"realm_name": "Zulip Dev",
"user_id": 11,
"sender_id": 7,
"sender_email": "[email protected]",
"time": 1740890583,
"recipient_type": "private",
"message_ids": [
87
]
}
}
```

</details>

<details>
<summary>Payload: group_dm.json</summary>

```json
{
"aps": {
"alert": {
"title": "Othello, the Moor of Venice, Polonius (guest), Iago",
"subtitle": "Othello, the Moor of Venice:",
"body": "Sit down awhile; And let us once again assail your ears, That are so fortified against our story What we have two nights seen."
},
"sound": "default",
"badge": 0,
},
"zulip": {
"server": "zulipdev.com:9991",
"realm_id": 2,
"realm_uri": "http://localhost:9991",
"realm_url": "http://localhost:9991",
"realm_name": "Zulip Dev",
"user_id": 11,
"sender_id": 12,
"sender_email": "[email protected]",
"time": 1740533641,
"recipient_type": "private",
"pm_users": "11,12,13",
"message_ids": [
17
]
}
}
```

</details>

<details>
<summary>Payload: stream.json</summary>

```json
{
"aps": {
"alert": {
"title": "#devel > plotter",
"subtitle": "Desdemona:",
"body": "Despite the fact that such a claim at first glance seems counterintuitive, it is derived from known results. Electrical engineering follows a cycle of four phases: location, refinement, visualization, and evaluation."
},
"sound": "default",
"badge": 0,
},
"zulip": {
"server": "zulipdev.com:9991",
"realm_id": 2,
"realm_uri": "http://localhost:9991",
"realm_url": "http://localhost:9991",
"realm_name": "Zulip Dev",
"user_id": 11,
"sender_id": 9,
"sender_email": "[email protected]",
"time": 1740558997,
"recipient_type": "stream",
"stream": "devel",
"stream_id": 11,
"topic": "plotter",
"message_ids": [
40
]
}
}
```

</details>

To receive a notification on the iOS Simulator, we need to push
the APNs payload to the specific running iOS Simulator by using it's
device ID, you can get the device ID by running the following command:

```shell-session
$ xcrun simctl list devices booted
```

<details>
<summary>Example output:</summary>

```shell-session
$ xcrun simctl list devices booted
== Devices ==
-- iOS 18.3 --
iPhone 16 Pro (90CC33B2-679B-4053-B380-7B986A29F28C) (Booted)
```

</details>

And then push the payload using the following command:

```shell-session
$ xcrun simctl push [device-id] com.zulip.flutter [payload json path]
```

<details>
<summary>Example output:</summary>

```shell-session
$ xcrun simctl push 90CC33B2-679B-4053-B380-7B986A29F28C com.zulip.flutter ./dm.json
Notification sent to 'com.zulip.flutter'
```

</details>

Now, on the iOS Simulator you should have a notification and tapping
on it should route to the specific conversation.
4 changes: 4 additions & 0 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B34E9F092D776BEB0009AED2 /* Notifications.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34E9F082D776BEB0009AED2 /* Notifications.g.swift */; };
F311C174AF9C005CE4AADD72 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3EAE3F3F518B95B7BFEB4FE7 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -48,6 +49,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B34E9F082D776BEB0009AED2 /* Notifications.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.g.swift; sourceTree = "<group>"; };
B3AF53A72CA20BD10039801D /* Zulip.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Zulip.xcconfig; path = Flutter/Zulip.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -115,6 +117,7 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
B34E9F082D776BEB0009AED2 /* Notifications.g.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
Expand Down Expand Up @@ -297,6 +300,7 @@
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
B34E9F092D776BEB0009AED2 /* Notifications.g.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Loading