@@ -5,9 +5,9 @@ import 'dart:io';
5
5
import 'package:http/http.dart' as http;
6
6
import 'package:collection/collection.dart' ;
7
7
import 'package:crypto/crypto.dart' ;
8
+ import 'package:flutter_gen/gen_l10n/zulip_localizations.dart' ;
8
9
import 'package:flutter/foundation.dart' ;
9
10
import 'package:flutter/widgets.dart' hide Notification;
10
- import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Person;
11
11
12
12
import '../api/notifications.dart' ;
13
13
import '../host/android_notifications.dart' ;
@@ -17,6 +17,7 @@ import '../model/localizations.dart';
17
17
import '../model/narrow.dart' ;
18
18
import '../widgets/app.dart' ;
19
19
import '../widgets/color.dart' ;
20
+ import '../widgets/dialog.dart' ;
20
21
import '../widgets/message_list.dart' ;
21
22
import '../widgets/page.dart' ;
22
23
import '../widgets/store.dart' ;
@@ -95,16 +96,6 @@ class NotificationChannelManager {
95
96
/// Service for managing the notifications shown to the user.
96
97
class NotificationDisplayManager {
97
98
static Future <void > init () async {
98
- await ZulipBinding .instance.notifications.initialize (
99
- const InitializationSettings (
100
- android: AndroidInitializationSettings ('zulip_notification' ),
101
- ),
102
- onDidReceiveNotificationResponse: _onNotificationOpened,
103
- );
104
- final launchDetails = await ZulipBinding .instance.notifications.getNotificationAppLaunchDetails ();
105
- if (launchDetails? .didNotificationLaunchApp ?? false ) {
106
- _handleNotificationAppLaunch (launchDetails! .notificationResponse);
107
- }
108
99
await NotificationChannelManager .ensureChannel ();
109
100
}
110
101
@@ -168,6 +159,16 @@ class NotificationDisplayManager {
168
159
name: data.senderFullName,
169
160
iconBitmap: await _fetchBitmap (data.senderAvatarUrl))));
170
161
162
+ final intentDataUrl = NotificationOpenPayload (
163
+ realmUrl: data.realmUrl,
164
+ userId: data.userId,
165
+ narrow: switch (data.recipient) {
166
+ FcmMessageChannelRecipient (: var streamId, : var topic) =>
167
+ TopicNarrow (streamId, topic),
168
+ FcmMessageDmRecipient (: var allRecipientIds) =>
169
+ DmNarrow (allRecipientIds: allRecipientIds, selfUserId: data.userId),
170
+ }).buildUrl ();
171
+
171
172
await _androidHost.notify (
172
173
// TODO the notification ID can be constant, instead of matching requestCode
173
174
// (This is a legacy of `flutter_local_notifications`.)
@@ -205,13 +206,9 @@ class NotificationDisplayManager {
205
206
// (That's a legacy of `flutter_local_notifications`.)
206
207
flags: PendingIntentFlag .immutable | PendingIntentFlag .updateCurrent,
207
208
intent: AndroidIntent (
208
- // This action name and extra name are special to
209
- // FlutterLocalNotificationsPlugin, which handles receiving the Intent.
210
- // TODO take care of receiving the notification-opened Intent ourselves
211
- action: 'SELECT_NOTIFICATION' ,
212
- extras: < String , String > {
213
- 'payload' : jsonEncode (dataJson),
214
- }),
209
+ action: IntentAction .view,
210
+ dataUrl: intentDataUrl.toString (),
211
+ extras: {}),
215
212
// TODO this doesn't set the Intent flags we set in zulip-mobile; is that OK?
216
213
// (This is a legacy of `flutter_local_notifications`.)
217
214
),
@@ -348,46 +345,36 @@ class NotificationDisplayManager {
348
345
349
346
static String _personKey (Uri realmUrl, int userId) => "$realmUrl |$userId " ;
350
347
351
- static void _onNotificationOpened (NotificationResponse response) async {
352
- final payload = jsonDecode (response.payload! ) as Map <String , dynamic >;
353
- final data = MessageFcmMessage .fromJson (payload);
354
- assert (debugLog ('opened notif: message ${data .zulipMessageId }, content ${data .content }' ));
355
- _navigateForNotification (data);
356
- }
357
-
358
- static void _handleNotificationAppLaunch (NotificationResponse ? response) async {
359
- assert (response != null );
360
- if (response == null ) return ; // TODO(log) seems like a bug in flutter_local_notifications if this can happen
348
+ /// Navigates to the [MessageListPage] of the specific conversation
349
+ /// given the `zulip://notification/…` Android intent data URL,
350
+ /// generated with [NotificationOpenPayload.buildUrl] while creating
351
+ /// the notification.
352
+ static Future <void > navigateForNotification (Uri url) async {
353
+ assert (debugLog ('opened notif: url: $url ' ));
361
354
362
- final payload = jsonDecode (response.payload! ) as Map <String , dynamic >;
363
- final data = MessageFcmMessage .fromJson (payload);
364
- assert (debugLog ('launched from notif: message ${data .zulipMessageId }, content ${data .content }' ));
365
- _navigateForNotification (data);
366
- }
355
+ assert (url.scheme == 'zulip' && url.host == 'notification' );
356
+ final payload = NotificationOpenPayload .parseUrl (url);
367
357
368
- static void _navigateForNotification (MessageFcmMessage data) async {
369
358
NavigatorState navigator = await ZulipApp .navigator;
370
359
final context = navigator.context;
371
360
assert (context.mounted);
372
361
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
373
362
363
+ final zulipLocalizations = ZulipLocalizations .of (context);
374
364
final globalStore = GlobalStoreWidget .of (context);
375
365
final account = globalStore.accounts.firstWhereOrNull ((account) =>
376
- account.realmUrl == data.realmUrl && account.userId == data.userId);
377
- if (account == null ) return ; // TODO(log)
378
-
379
- final narrow = switch (data.recipient) {
380
- FcmMessageChannelRecipient (: var streamId, : var topic) =>
381
- TopicNarrow (streamId, topic),
382
- FcmMessageDmRecipient (: var allRecipientIds) =>
383
- DmNarrow (allRecipientIds: allRecipientIds, selfUserId: account.userId),
384
- };
366
+ account.realmUrl == payload.realmUrl && account.userId == payload.userId);
367
+ if (account == null ) { // TODO(log)
368
+ showErrorDialog (context: context,
369
+ title: zulipLocalizations.errorNotificationOpenTitle,
370
+ message: zulipLocalizations.errorNotificationOpenAccountMissing);
371
+ return ;
372
+ }
385
373
386
- assert (debugLog (' account: $account , narrow: $narrow ' ));
387
374
// TODO(nav): Better interact with existing nav stack on notif open
388
375
unawaited (navigator.push (MaterialAccountWidgetRoute <void >(accountId: account.id,
389
376
// TODO(#82): Open at specific message, not just conversation
390
- page: MessageListPage (initNarrow: narrow))));
377
+ page: MessageListPage (initNarrow: payload. narrow))));
391
378
}
392
379
393
380
static Future <Uint8List ?> _fetchBitmap (Uri url) async {
@@ -403,3 +390,86 @@ class NotificationDisplayManager {
403
390
return null ;
404
391
}
405
392
}
393
+
394
+ /// The information contained in 'zulip://notification/…' internal
395
+ /// Android intent data URL, used for notification-open flow.
396
+ class NotificationOpenPayload {
397
+ final Uri realmUrl;
398
+ final int userId;
399
+ final Narrow narrow;
400
+
401
+ NotificationOpenPayload ({
402
+ required this .realmUrl,
403
+ required this .userId,
404
+ required this .narrow,
405
+ });
406
+
407
+ factory NotificationOpenPayload .parseUrl (Uri url) {
408
+ if (url case Uri (
409
+ scheme: 'zulip' ,
410
+ host: 'notification' ,
411
+ queryParameters: {
412
+ 'realm_url' : var realmUrlStr,
413
+ 'user_id' : var userIdStr,
414
+ 'narrow_type' : var narrowType,
415
+ // In case of narrowType == 'topic':
416
+ // 'channel_id' and 'topic' handled below.
417
+
418
+ // In case of narrowType == 'dm':
419
+ // 'all_recipient_ids' handled below.
420
+ },
421
+ )) {
422
+ final realmUrl = Uri .parse (realmUrlStr);
423
+ final userId = int .parse (userIdStr, radix: 10 );
424
+
425
+ final Narrow narrow;
426
+ switch (narrowType) {
427
+ case 'topic' :
428
+ final channelIdStr = url.queryParameters['channel_id' ]! ;
429
+ final channelId = int .parse (channelIdStr, radix: 10 );
430
+ final topic = url.queryParameters['topic' ]! ;
431
+ narrow = TopicNarrow (channelId, topic);
432
+ case 'dm' :
433
+ final allRecipientIdsStr = url.queryParameters['all_recipient_ids' ]! ;
434
+ final allRecipientIds = allRecipientIdsStr.split (',' )
435
+ .map ((idStr) => int .parse (idStr, radix: 10 ))
436
+ .toList (growable: false );
437
+ narrow = DmNarrow (allRecipientIds: allRecipientIds, selfUserId: userId);
438
+ default :
439
+ throw const FormatException ();
440
+ }
441
+
442
+ return NotificationOpenPayload (
443
+ realmUrl: realmUrl,
444
+ userId: userId,
445
+ narrow: narrow,
446
+ );
447
+ } else {
448
+ // TODO(dart): simplify after https://github.com/dart-lang/language/issues/2537
449
+ throw const FormatException ();
450
+ }
451
+ }
452
+
453
+ Uri buildUrl () {
454
+ return Uri (
455
+ scheme: 'zulip' ,
456
+ host: 'notification' ,
457
+ queryParameters: < String , String > {
458
+ 'realm_url' : realmUrl.toString (),
459
+ 'user_id' : userId.toString (),
460
+ ...(switch (narrow) {
461
+ TopicNarrow (streamId: var channelId, : var topic) => {
462
+ 'narrow_type' : 'topic' ,
463
+ 'channel_id' : channelId.toString (),
464
+ 'topic' : topic,
465
+ },
466
+ DmNarrow (: var allRecipientIds) => {
467
+ 'narrow_type' : 'dm' ,
468
+ 'all_recipient_ids' : allRecipientIds.join (',' ),
469
+ },
470
+ _ => throw UnsupportedError ('Found an unexpected Narrow of type ${narrow .runtimeType }.' ),
471
+ })
472
+ },
473
+ );
474
+ }
475
+ }
0 commit comments