Skip to content

Commit

Permalink
notif: Support messaging-style notifications
Browse files Browse the repository at this point in the history
Fixes: zulip#128
  • Loading branch information
rajveermalviya committed Jun 3, 2024
1 parent a646972 commit 0e3d32c
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 10 deletions.
56 changes: 50 additions & 6 deletions lib/notifications/display.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'dart:convert';

import 'package:http/http.dart' as http;
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Person;

import '../api/notifications.dart';
import '../host/android_notifications.dart';
Expand Down Expand Up @@ -92,7 +93,32 @@ class NotificationDisplayManager {
static Future<void> _onMessageFcmMessage(MessageFcmMessage data, Map<String, dynamic> dataJson) async {
assert(debugLog('notif message content: ${data.content}'));
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
final title = switch (data.recipient) {
final groupKey = _groupKey(data);
final conversationKey = _conversationKey(data, groupKey);

final oldMessagingStyle = await ZulipBinding.instance.androidNotificationHost
.getActiveNotificationMessagingStyleByTag(conversationKey);

MessagingStyle messagingStyle;
if (oldMessagingStyle != null) {
messagingStyle = MessagingStyle(
user: oldMessagingStyle.user,
messages: oldMessagingStyle.messages?.toList() ?? [], // Clone a fixed-length list
isGroupConversation: oldMessagingStyle.isGroupConversation);
} else {
messagingStyle = MessagingStyle(
user: Person(
key: data.userId.toString(),
name: 'You'),
messages: [],
isGroupConversation: switch (data.recipient) {
FcmMessageStreamRecipient() => true,
FcmMessageDmRecipient(:var allRecipientIds) when allRecipientIds.length > 2 => true,
FcmMessageDmRecipient() => false,
});
}

messagingStyle.conversationTitle = switch (data.recipient) {
FcmMessageStreamRecipient(:var streamName?, :var topic) =>
'#$streamName > $topic',
FcmMessageStreamRecipient(:var topic) =>
Expand All @@ -103,8 +129,15 @@ class NotificationDisplayManager {
FcmMessageDmRecipient() =>
data.senderFullName,
};
final groupKey = _groupKey(data);
final conversationKey = _conversationKey(data, groupKey);

messagingStyle.messages?.add(MessagingStyleMessage(
text: data.content,
timestampMs: data.time * 1000,
person: Person(
key: data.senderId.toString(),
name: data.senderFullName,
iconData: await _fetchBitmap(data.senderAvatarUrl))),
);

await ZulipBinding.instance.androidNotificationHost.notify(
// TODO the notification ID can be constant, instead of matching requestCode
Expand All @@ -114,8 +147,9 @@ class NotificationDisplayManager {
channelId: NotificationChannelManager.kChannelId,
groupKey: groupKey,

contentTitle: title,
contentText: data.content,
messagingStyle: messagingStyle,
number: messagingStyle.messages?.length,

color: kZulipBrandColor.value,
// TODO vary notification icon for debug
smallIconResourceName: 'zulip_notification', // This name must appear in keep.xml too: https://github.com/zulip/zulip-flutter/issues/528
Expand Down Expand Up @@ -230,4 +264,14 @@ class NotificationDisplayManager {
page: MessageListPage(narrow: narrow)));
return;
}

static Future<Uint8List?> _fetchBitmap(Uri url) async {
try {
final resp = await http.get(url);
return resp.bodyBytes;
} catch (e) {
// TODO(log)
return null;
}
}
}
24 changes: 20 additions & 4 deletions test/notifications/display_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ void main() {
void checkNotification(MessageFcmMessage data, {
required String expectedTitle,
required String expectedTagComponent,
required bool expectedGroup,
}) {
final expectedTag = '${data.realmUri}|${data.userId}|$expectedTagComponent';
final expectedGroupKey = '${data.realmUri}|${data.userId}';
Expand All @@ -121,15 +122,19 @@ void main() {
..id.equals(expectedId)
..tag.equals(expectedTag)
..channelId.equals(NotificationChannelManager.kChannelId)
..contentTitle.equals(expectedTitle)
..contentText.equals(data.content)
..color.equals(kZulipBrandColor.value)
..smallIconResourceName.equals('zulip_notification')
..extras.isNull()
..groupKey.equals(expectedGroupKey)
..isGroupSummary.isNull()
..inboxStyle.isNull()
..autoCancel.equals(true)
..messagingStyle.which((it) => it.isNotNull()
..isGroupConversation.equals(expectedGroup)
..conversationTitle.equals(expectedTitle)
..messages.which((it) => it.isNotNull()
..last.which((it) => it.isNotNull()
..text.equals(data.content))))
..contentIntent.which((it) => it.isNotNull()
..requestCode.equals(expectedId)
..flags.equals(expectedIntentFlags)
Expand All @@ -156,6 +161,7 @@ void main() {
Future<void> checkNotifications(FakeAsync async, MessageFcmMessage data, {
required String expectedTitle,
required String expectedTagComponent,
required bool expectedGroup,
}) async {
// We could just call `NotificationDisplayManager.onFcmMessage`.
// But this way is cheap, and it provides our test coverage of
Expand All @@ -164,13 +170,17 @@ void main() {
testBinding.firebaseMessaging.onMessage.add(
RemoteMessage(data: data.toJson()));
async.flushMicrotasks();
checkNotification(data, expectedTitle: expectedTitle,
checkNotification(data,
expectedGroup: expectedGroup,
expectedTitle: expectedTitle,
expectedTagComponent: expectedTagComponent);

testBinding.firebaseMessaging.onBackgroundMessage.add(
RemoteMessage(data: data.toJson()));
async.flushMicrotasks();
checkNotification(data, expectedTitle: expectedTitle,
checkNotification(data,
expectedGroup: expectedGroup,
expectedTitle: expectedTitle,
expectedTagComponent: expectedTagComponent);
}

Expand All @@ -179,6 +189,7 @@ void main() {
final stream = eg.stream();
final message = eg.streamMessage(stream: stream);
await checkNotifications(async, messageFcmMessage(message, streamName: stream.name),
expectedGroup: true,
expectedTitle: '#${stream.name} > ${message.subject}',
expectedTagComponent: 'stream:${message.streamId}:${message.subject}');
}));
Expand All @@ -188,6 +199,7 @@ void main() {
final stream = eg.stream();
final message = eg.streamMessage(stream: stream);
await checkNotifications(async, messageFcmMessage(message, streamName: null),
expectedGroup: true,
expectedTitle: '#(unknown stream) > ${message.subject}',
expectedTagComponent: 'stream:${message.streamId}:${message.subject}');
}));
Expand All @@ -196,6 +208,7 @@ void main() {
await init();
final message = eg.dmMessage(from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]);
await checkNotifications(async, messageFcmMessage(message),
expectedGroup: true,
expectedTitle: "${eg.thirdUser.fullName} to you and 1 other",
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
}));
Expand All @@ -205,6 +218,7 @@ void main() {
final message = eg.dmMessage(from: eg.thirdUser,
to: [eg.otherUser, eg.selfUser, eg.fourthUser]);
await checkNotifications(async, messageFcmMessage(message),
expectedGroup: true,
expectedTitle: "${eg.thirdUser.fullName} to you and 2 others",
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
}));
Expand All @@ -213,6 +227,7 @@ void main() {
await init();
final message = eg.dmMessage(from: eg.otherUser, to: [eg.selfUser]);
await checkNotifications(async, messageFcmMessage(message),
expectedGroup: false,
expectedTitle: eg.otherUser.fullName,
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
}));
Expand All @@ -221,6 +236,7 @@ void main() {
await init();
final message = eg.dmMessage(from: eg.selfUser, to: []);
await checkNotifications(async, messageFcmMessage(message),
expectedGroup: false,
expectedTitle: eg.selfUser.fullName,
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
}));
Expand Down

0 comments on commit 0e3d32c

Please sign in to comment.