Skip to content

Commit d402a01

Browse files
notif ios: Handle opening of conversation on tap
1 parent a055486 commit d402a01

File tree

1 file changed

+125
-1
lines changed

1 file changed

+125
-1
lines changed

ios/Runner/AppDelegate.swift

+125-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import UIKit
22
import Flutter
3+
import UserNotifications
34

45
@main
56
@objc class AppDelegate: FlutterAppDelegate {
@@ -8,6 +9,129 @@ import Flutter
89
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
910
) -> Bool {
1011
GeneratedPluginRegistrant.register(with: self)
11-
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12+
UNUserNotificationCenter.current().delegate = self
13+
14+
// Handle launch of application from the notification.
15+
let controller = window.rootViewController as! FlutterViewController
16+
if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable : Any],
17+
let payload = ApnsPayload(fromJson: remoteNotification),
18+
let routeUrl = internalRouteUrlFromNotification(payload: payload) {
19+
// https://api.flutter.dev/flutter/dart-ui/PlatformDispatcher/defaultRouteName.html
20+
// TODO(?) FlutterViewController.setInitialRoute is deprecated (warning visible in Xcode)
21+
// but the above doc mentions using it.
22+
controller.setInitialRoute(routeUrl.absoluteString)
23+
}
24+
25+
return super.application(
26+
application,
27+
didFinishLaunchingWithOptions: launchOptions
28+
)
29+
}
30+
31+
// Handle notification tap while the app is running.
32+
override func userNotificationCenter(
33+
_ center: UNUserNotificationCenter,
34+
didReceive response: UNNotificationResponse,
35+
withCompletionHandler completionHandler: @escaping () -> Void
36+
) {
37+
if let payload = ApnsPayload(fromJson: response.notification.request.content.userInfo),
38+
let routeUrl = internalRouteUrlFromNotification(payload: payload) {
39+
let controller = window.rootViewController as! FlutterViewController
40+
controller.pushRoute(routeUrl.absoluteString)
41+
completionHandler()
42+
}
43+
}
44+
}
45+
46+
// https://github.com/zulip/zulip/blob/aa8f47774f08b6fc5d947ae97cafefcf7dfb8bef/zerver/lib/push_notifications.py#L1087
47+
struct ApnsPayload {
48+
enum Narrow {
49+
case topicNarrow(channelId: Int, topic: String)
50+
case dmNarrow(allRecipientIds: [Int])
51+
}
52+
53+
let realmUrl: String
54+
let userId: Int
55+
let narrow: Narrow
56+
57+
init?(fromJson payload: [AnyHashable : Any]) {
58+
guard let aps = payload["aps"] as? [String : Any],
59+
let customData = aps["custom"] as? [String : Any],
60+
let zulipData = customData["zulip"] as? [String : Any],
61+
let realmUrl = (
62+
zulipData["realm_url"] as? String ?? zulipData["realm_url"] as? String
63+
),
64+
let userId = zulipData["user_id"] as? Int,
65+
let senderId = zulipData["sender_id"] as? Int,
66+
let recipientType = zulipData["recipient_type"] as? String
67+
else {
68+
return nil
69+
}
70+
71+
var narrow: Narrow
72+
switch recipientType {
73+
case "stream":
74+
guard let streamId = zulipData["stream_id"] as? Int,
75+
let topic = zulipData["topic"] as? String else {
76+
return nil
77+
}
78+
79+
narrow = Narrow.topicNarrow(channelId: streamId, topic: topic)
80+
81+
case "private":
82+
var allRecipientIds = Set<Int>()
83+
84+
if let pmUsersStr = zulipData["pm_users"] as? String {
85+
for str in pmUsersStr.split(separator: ",") {
86+
guard let recipientId = Int(str, radix: 10) else {
87+
return nil
88+
}
89+
allRecipientIds.insert(recipientId)
90+
}
91+
} else {
92+
allRecipientIds.formUnion([senderId, userId])
93+
}
94+
95+
narrow = Narrow.dmNarrow(allRecipientIds: allRecipientIds.sorted(by: <))
96+
97+
default:
98+
return nil
99+
}
100+
101+
self.realmUrl = realmUrl
102+
self.userId = userId
103+
self.narrow = narrow
104+
}
105+
}
106+
107+
func internalRouteUrlFromNotification(payload: ApnsPayload) -> URL? {
108+
var components = URLComponents()
109+
components.scheme = "zulip"
110+
components.host = "notification"
111+
112+
var queryItems = [
113+
URLQueryItem(name: "realm_url", value: payload.realmUrl),
114+
URLQueryItem(name: "user_id", value: String(payload.userId)),
115+
]
116+
117+
switch payload.narrow {
118+
case .topicNarrow(channelId: let channelId, topic: let topic):
119+
queryItems.append(contentsOf: [
120+
URLQueryItem(name: "narrow_type", value: "topic"),
121+
URLQueryItem(name: "channel_id", value: String(channelId)),
122+
URLQueryItem(name: "topic", value: topic),
123+
])
124+
125+
case .dmNarrow(allRecipientIds: let allRecipientIds):
126+
queryItems.append(
127+
contentsOf: [
128+
URLQueryItem(name: "narrow_type", value: "dm"),
129+
URLQueryItem(
130+
name: "all_recipient_ids",
131+
value: allRecipientIds.map{ String($0) }.joined(separator: ",")),
132+
]
133+
)
12134
}
135+
components.queryItems = queryItems
136+
return components.url
13137
}

0 commit comments

Comments
 (0)