Skip to content

Commit d0cf530

Browse files
notif ios: Navigate when app running but in background
1 parent 75ca11a commit d0cf530

File tree

7 files changed

+158
-0
lines changed

7 files changed

+158
-0
lines changed

ios/Runner/AppDelegate.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import Flutter
33

44
@main
55
@objc class AppDelegate: FlutterAppDelegate {
6+
private var notificationTapEventListener: NotificationTapEventListener?
7+
68
override func application(
79
_ application: UIApplication,
810
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
@@ -16,9 +18,24 @@ import Flutter
1618
let api = NotificationHostApiImpl(notificationData.map { NotificationPayloadForOpen(payload: $0) })
1719
NotificationHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: api)
1820

21+
notificationTapEventListener = NotificationTapEventListener()
22+
NotificationTapEventsStreamHandler.register(with: controller.binaryMessenger, streamHandler: notificationTapEventListener!)
23+
1924
UNUserNotificationCenter.current().delegate = self
2025
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
2126
}
27+
28+
override func userNotificationCenter(
29+
_ center: UNUserNotificationCenter,
30+
didReceive response: UNNotificationResponse,
31+
withCompletionHandler completionHandler: @escaping () -> Void
32+
) {
33+
if let listener = notificationTapEventListener {
34+
let userInfo = response.notification.request.content.userInfo
35+
listener.onNotificationTapEvent(data: NotificationPayloadForOpen(payload: userInfo))
36+
completionHandler()
37+
}
38+
}
2239
}
2340

2441
private class NotificationHostApiImpl: NotificationHostApi {
@@ -32,3 +49,22 @@ private class NotificationHostApiImpl: NotificationHostApi {
3249
maybeNotifPayload
3350
}
3451
}
52+
53+
class NotificationTapEventListener: NotificationTapEventsStreamHandler {
54+
var eventSink: PigeonEventSink<NotificationPayloadForOpen>?
55+
56+
override func onListen(withArguments arguments: Any?, sink: PigeonEventSink<NotificationPayloadForOpen>) {
57+
eventSink = sink
58+
}
59+
60+
func onNotificationTapEvent(data: NotificationPayloadForOpen) {
61+
if let eventSink = eventSink {
62+
eventSink.success(data)
63+
}
64+
}
65+
66+
func onEventsDone() {
67+
eventSink?.endOfStream()
68+
eventSink = nil
69+
}
70+
}

ios/Runner/Notifications.g.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ class NotificationsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
120120
static let shared = NotificationsPigeonCodec(readerWriter: NotificationsPigeonCodecReaderWriter())
121121
}
122122

123+
var notificationsPigeonMethodCodec = FlutterStandardMethodCodec(readerWriter: NotificationsPigeonCodecReaderWriter());
124+
123125
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
124126
protocol NotificationHostApi {
125127
func getNotificationDataFromLaunch() throws -> NotificationPayloadForOpen?
@@ -146,3 +148,67 @@ class NotificationHostApiSetup {
146148
}
147149
}
148150
}
151+
152+
private class PigeonStreamHandler<ReturnType>: NSObject, FlutterStreamHandler {
153+
private let wrapper: PigeonEventChannelWrapper<ReturnType>
154+
private var pigeonSink: PigeonEventSink<ReturnType>? = nil
155+
156+
init(wrapper: PigeonEventChannelWrapper<ReturnType>) {
157+
self.wrapper = wrapper
158+
}
159+
160+
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
161+
-> FlutterError?
162+
{
163+
pigeonSink = PigeonEventSink<ReturnType>(events)
164+
wrapper.onListen(withArguments: arguments, sink: pigeonSink!)
165+
return nil
166+
}
167+
168+
func onCancel(withArguments arguments: Any?) -> FlutterError? {
169+
pigeonSink = nil
170+
wrapper.onCancel(withArguments: arguments)
171+
return nil
172+
}
173+
}
174+
175+
class PigeonEventChannelWrapper<ReturnType> {
176+
func onListen(withArguments arguments: Any?, sink: PigeonEventSink<ReturnType>) {}
177+
func onCancel(withArguments arguments: Any?) {}
178+
}
179+
180+
class PigeonEventSink<ReturnType> {
181+
private let sink: FlutterEventSink
182+
183+
init(_ sink: @escaping FlutterEventSink) {
184+
self.sink = sink
185+
}
186+
187+
func success(_ value: ReturnType) {
188+
sink(value)
189+
}
190+
191+
func error(code: String, message: String?, details: Any?) {
192+
sink(FlutterError(code: code, message: message, details: details))
193+
}
194+
195+
func endOfStream() {
196+
sink(FlutterEndOfEventStream)
197+
}
198+
199+
}
200+
201+
class NotificationTapEventsStreamHandler: PigeonEventChannelWrapper<NotificationPayloadForOpen> {
202+
static func register(with messenger: FlutterBinaryMessenger,
203+
instanceName: String = "",
204+
streamHandler: NotificationTapEventsStreamHandler) {
205+
var channelName = "dev.flutter.pigeon.zulip.NotificationHostEvents.notificationTapEvents"
206+
if !instanceName.isEmpty {
207+
channelName += ".\(instanceName)"
208+
}
209+
let internalStreamHandler = PigeonStreamHandler<NotificationPayloadForOpen>(wrapper: streamHandler)
210+
let channel = FlutterEventChannel(name: channelName, binaryMessenger: messenger, codec: notificationsPigeonMethodCodec)
211+
channel.setStreamHandler(internalStreamHandler)
212+
}
213+
}
214+

lib/host/notifications.g.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class _PigeonCodec extends StandardMessageCodec {
6363
}
6464
}
6565

66+
const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec());
67+
6668
class NotificationHostApi {
6769
/// Constructor for [NotificationHostApi]. The [binaryMessenger] named argument is
6870
/// available for dependency injection. If it is left null, the default
@@ -99,3 +101,15 @@ class NotificationHostApi {
99101
}
100102
}
101103
}
104+
105+
Stream<NotificationPayloadForOpen> notificationTapEvents( {String instanceName = ''}) {
106+
if (instanceName.isNotEmpty) {
107+
instanceName = '.$instanceName';
108+
}
109+
final EventChannel notificationTapEventsChannel =
110+
EventChannel('dev.flutter.pigeon.zulip.NotificationHostEvents.notificationTapEvents$instanceName', pigeonMethodCodec);
111+
return notificationTapEventsChannel.receiveBroadcastStream().map((dynamic event) {
112+
return event as NotificationPayloadForOpen;
113+
});
114+
}
115+

lib/model/binding.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,9 @@ class NotificationPigeonApi {
318318

319319
Future<notif_pigeon.NotificationPayloadForOpen?> getNotificationDataFromLaunch() =>
320320
_notifInteractionHost.getNotificationDataFromLaunch();
321+
322+
Stream<notif_pigeon.NotificationPayloadForOpen> notificationTapEventsStream() =>
323+
notif_pigeon.notificationTapEvents();
321324
}
322325

323326
/// A concrete binding for use in the live application.

lib/notifications/open.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '../host/notifications.dart';
1212
import '../log.dart';
1313
import '../model/binding.dart';
1414
import '../model/narrow.dart';
15+
import '../widgets/app.dart';
1516
import '../widgets/dialog.dart';
1617
import '../widgets/message_list.dart';
1718
import '../widgets/page.dart';
@@ -31,6 +32,8 @@ class NotificationOpenManager {
3132
switch (defaultTargetPlatform) {
3233
case TargetPlatform.iOS:
3334
_notifLaunchData = await _notifPigeonApi.getNotificationDataFromLaunch();
35+
_notifPigeonApi.notificationTapEventsStream()
36+
.listen(_navigateForNotification);
3437

3538
case TargetPlatform.android:
3639
case TargetPlatform.fuchsia:
@@ -68,6 +71,21 @@ class NotificationOpenManager {
6871
// TODO(#82): Open at specific message, not just conversation
6972
narrow: openData.narrow);
7073
}
74+
75+
Future<void> _navigateForNotification(NotificationPayloadForOpen data) async {
76+
assert(debugLog('opened notif: ${jsonEncode(data.payload)}'));
77+
78+
NavigatorState navigator = await ZulipApp.navigator;
79+
final context = navigator.context;
80+
assert(context.mounted);
81+
if (!context.mounted) return; // TODO(linter): this is impossible as there's no actual async gap, but the use_build_context_synchronously lint doesn't see that
82+
83+
final route = _routeForNotification(context, data);
84+
if (route == null) return; // TODO(log)
85+
86+
// TODO(nav): Better interact with existing nav stack on notif open
87+
unawaited(navigator.push(route));
88+
}
7189
}
7290

7391
class NotificationDataForOpen {

pigeon/notifications.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ class NotificationPayloadForOpen {
1616
abstract class NotificationHostApi {
1717
NotificationPayloadForOpen? getNotificationDataFromLaunch();
1818
}
19+
20+
@EventChannelApi()
21+
abstract class NotificationHostEvents {
22+
NotificationPayloadForOpen notificationTapEvents();
23+
}

test/model/binding.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,22 @@ class FakeNotificationPigeonApi implements NotificationPigeonApi {
742742
@override
743743
Future<NotificationPayloadForOpen?> getNotificationDataFromLaunch() async =>
744744
_notificationDataFromLaunch;
745+
746+
StreamController<NotificationPayloadForOpen>? _notificationTapEventsStreamController;
747+
748+
void addNotificationTapEvent(NotificationPayloadForOpen data) {
749+
_notificationTapEventsStreamController!.add(data);
750+
}
751+
752+
void resetNotificationTapEventStream() {
753+
_notificationTapEventsStreamController?.close();
754+
}
755+
756+
@override
757+
Stream<NotificationPayloadForOpen> notificationTapEventsStream() {
758+
_notificationTapEventsStreamController ??= StreamController();
759+
return _notificationTapEventsStreamController!.stream;
760+
}
745761
}
746762

747763
typedef AndroidNotificationHostApiNotifyCall = ({

0 commit comments

Comments
 (0)