diff --git a/lib/api/notifications.dart b/lib/api/notifications.dart index 95068f7834..1a5bc0c54c 100644 --- a/lib/api/notifications.dart +++ b/lib/api/notifications.dart @@ -74,9 +74,13 @@ sealed class FcmMessageWithIdentity extends FcmMessage { @JsonKey(readValue: _readIntOrString) // TODO(server-12) final int userId; + /// The realm's name. + final String? realmName; // TODO(server-8) + FcmMessageWithIdentity({ required this.realmUrl, required this.userId, + required this.realmName, }); // TODO(server-9): FL 257 deprecated 'realm_uri' in favor of 'realm_url'. @@ -121,6 +125,7 @@ class MessageFcmMessage extends FcmMessageWithIdentity { MessageFcmMessage({ required super.realmUrl, required super.userId, + required super.realmName, required this.senderId, required this.senderAvatarUrl, required this.senderFullName, @@ -251,6 +256,7 @@ class RemoveFcmMessage extends FcmMessageWithIdentity { RemoveFcmMessage({ required super.realmUrl, required super.userId, + required super.realmName, required this.messageIds, }); diff --git a/lib/api/notifications.g.dart b/lib/api/notifications.g.dart index 7f01b3c420..0864ac450f 100644 --- a/lib/api/notifications.g.dart +++ b/lib/api/notifications.g.dart @@ -14,6 +14,7 @@ MessageFcmMessage _$MessageFcmMessageFromJson(Map json) => FcmMessageWithIdentity._readRealmUrl(json, 'realm_url') as String, ), userId: (_readIntOrString(json, 'user_id') as num).toInt(), + realmName: json['realm_name'] as String?, senderId: (_readIntOrString(json, 'sender_id') as num).toInt(), senderAvatarUrl: Uri.parse(json['sender_avatar_url'] as String), senderFullName: json['sender_full_name'] as String, @@ -30,6 +31,7 @@ Map _$MessageFcmMessageToJson(MessageFcmMessage instance) => { 'realm_url': instance.realmUrl.toString(), 'user_id': instance.userId, + 'realm_name': instance.realmName, 'type': instance.type, 'sender_id': instance.senderId, 'sender_avatar_url': instance.senderAvatarUrl.toString(), @@ -57,6 +59,7 @@ RemoveFcmMessage _$RemoveFcmMessageFromJson(Map json) => FcmMessageWithIdentity._readRealmUrl(json, 'realm_url') as String, ), userId: (_readIntOrString(json, 'user_id') as num).toInt(), + realmName: json['realm_name'] as String?, messageIds: (RemoveFcmMessage._readMessageIds(json, 'message_ids') as List) @@ -68,6 +71,7 @@ Map _$RemoveFcmMessageToJson(RemoveFcmMessage instance) => { 'realm_url': instance.realmUrl.toString(), 'user_id': instance.userId, + 'realm_name': instance.realmName, 'type': instance.type, 'message_ids': instance.messageIds, }; diff --git a/lib/notifications/display.dart b/lib/notifications/display.dart index e78e89f0cf..1625fbbb5e 100644 --- a/lib/notifications/display.dart +++ b/lib/notifications/display.dart @@ -353,8 +353,9 @@ class NotificationDisplayManager { // 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 inboxStyle: InboxStyle( - // TODO(#570) Show organization name, not URL - summaryText: data.realmUrl.toString()), + summaryText: account.realmName + ?? data.realmName + ?? data.realmUrl.toString()), // On Android 11 and lower, if autoCancel is not specified, // the summary notification may linger even after all child diff --git a/test/api/notifications_test.dart b/test/api/notifications_test.dart index 373e3eabc4..550c2c587b 100644 --- a/test/api/notifications_test.dart +++ b/test/api/notifications_test.dart @@ -9,6 +9,7 @@ void main() { final baseBaseJson = { "realm_url": "https://zulip.example.com/", "user_id": 234, + "realm_name": "Example Organization", }; // Before E2EE notifications, the data comes directly as FCM payloads, @@ -20,6 +21,7 @@ void main() { "realm_uri": "https://zulip.example.com/", // TODO(server-9) "realm_url": "https://zulip.example.com/", "user_id": "234", + "realm_name": "Example Organization", }; void checkParseFails(Map data) { @@ -116,6 +118,7 @@ void main() { ..realmUrl.equals(Uri.parse(baseJson['realm_url'] as String)) ..realmUrl.equals(Uri.parse(baseJsonPreE2ee['realm_uri'] as String)) // TODO(server-9) ..userId.equals(234) + ..realmName.equals(baseBaseJson['realm_name'] as String) ..senderId.equals(123) ..senderAvatarUrl.equals(Uri.parse(streamJson['sender_avatar_url'] as String)) ..senderFullName.equals(streamJson['sender_full_name'] as String) @@ -146,6 +149,9 @@ void main() { .recipient.isA().which((it) => it ..channelId.equals(42) ..channelName.isNull()); + + check(parse({ ...streamJson }..remove('realm_name'))) + .realmName.isNull(); }); test('toJson round-trips', () { @@ -319,6 +325,7 @@ extension UnexpectedFcmMessageChecks on Subject { extension FcmMessageWithIdentityChecks on Subject { Subject get realmUrl => has((x) => x.realmUrl, 'realmUrl'); Subject get userId => has((x) => x.userId, 'userId'); + Subject get realmName => has((x) => x.realmName, 'realmName'); } extension MessageFcmMessageChecks on Subject { diff --git a/test/notifications/display_test.dart b/test/notifications/display_test.dart index 29319d8eaf..a813020836 100644 --- a/test/notifications/display_test.dart +++ b/test/notifications/display_test.dart @@ -24,14 +24,18 @@ import 'package:zulip/widgets/theme.dart'; import '../example_data.dart' as eg; import '../fake_async.dart'; import '../model/binding.dart'; +import '../model/store_checks.dart'; import '../test_images.dart'; +import '../api/notifications_test.dart'; MessageFcmMessage messageFcmMessage( Message zulipMessage, { String? streamName, + String? realmName, Account? account, }) { account ??= eg.selfAccount; + realmName ??= account.realmName; final narrow = SendableNarrow.ofMessage(zulipMessage, selfUserId: account.userId); return FcmMessage.fromJson({ "event": "message", @@ -40,6 +44,7 @@ MessageFcmMessage messageFcmMessage( "realm_id": "4", "realm_uri": account.realmUrl.toString(), "user_id": account.userId.toString(), + if (realmName != null) "realm_name": realmName, "zulip_message_id": zulipMessage.id.toString(), "time": zulipMessage.timestamp.toString(), @@ -343,13 +348,19 @@ void main() { }); group('NotificationDisplayManager show', () { - void checkNotification(MessageFcmMessage data, { + void checkNotification( + MessageFcmMessage data, { + Account? account, required List messageStyleMessages, required String expectedTitle, required String expectedTagComponent, required bool expectedIsGroupConversation, List? expectedIconBitmap = kSolidBlueAvatar, + String? expectedSummaryText, }) { + account ??= eg.selfAccount; + assert(account.userId == data.userId + && account.realmUrl == data.realmUrl); assert(messageStyleMessages.every((e) => e.userId == data.userId)); assert(messageStyleMessages.every((e) => e.realmUrl == data.realmUrl)); @@ -367,6 +378,9 @@ void main() { FcmMessageDmRecipient(:var allRecipientIds) => DmNarrow(allRecipientIds: allRecipientIds, selfUserId: data.userId), }).buildAndroidNotificationUrl(); + expectedSummaryText ??= account.realmName + ?? data.realmName + ?? data.realmUrl.toString(); final messageStyleMessagesChecks = messageStyleMessages.mapIndexed((i, messageData) { @@ -428,14 +442,18 @@ void main() { ..extras.isNull() ..groupKey.equals(expectedGroupKey) ..isGroupSummary.equals(true) - ..inboxStyle.which((it) => it.isNotNull() - ..summaryText.equals(data.realmUrl.toString())) + ..inboxStyle.which( + (it) => it.isNotNull() + ..summaryText.equals(expectedSummaryText!)) ..autoCancel.equals(true) ..contentIntent.isNull(), ]); } - Future checkNotifications(FakeAsync async, MessageFcmMessage data, { + Future checkNotifications( + FakeAsync async, + MessageFcmMessage data, { + Account? account, required String expectedTitle, required String expectedTagComponent, required bool expectedIsGroupConversation, @@ -448,6 +466,7 @@ void main() { RemoteMessage(data: data.toJson())); async.flushMicrotasks(); checkNotification(data, + account: account, messageStyleMessages: [data], expectedIsGroupConversation: expectedIsGroupConversation, expectedTitle: expectedTitle, @@ -458,6 +477,7 @@ void main() { RemoteMessage(data: data.toJson())); async.flushMicrotasks(); checkNotification(data, + account: account, messageStyleMessages: [data], expectedIsGroupConversation: expectedIsGroupConversation, expectedTitle: expectedTitle, @@ -634,6 +654,112 @@ void main() { expectedTagComponent: 'stream:${message.streamId}:${message.topic}'); }))); + test('stream message: different realms', () => runWithHttpClient(() => awaitFakeAsync((async) async { + await init(addSelfAccount: false); + + final account1 = eg.account( + id: 1001, + user: eg.user(), + realmUrl: Uri.parse('http://realm1.example'), + realmName: 'Realm 1'); + await testBinding.globalStore.add(account1, eg.initialSnapshot()); + final account2 = eg.account( + id: 1002, + user: eg.user(), + realmUrl: Uri.parse('http://realm2.example'), + realmName: 'Realm 2'); + await testBinding.globalStore.add(account2, eg.initialSnapshot()); + + final stream = eg.stream(); + final topic = 'test topic'; + final data1 = messageFcmMessage( + eg.streamMessage(stream: stream, topic: topic), + account: account1, streamName: stream.name); + final data2 = messageFcmMessage( + eg.streamMessage(stream: stream, topic: topic), + account: account2, streamName: stream.name); + + receiveFcmMessage(async, data1); + checkNotification(data1, + account: account1, + messageStyleMessages: [data1], + expectedIsGroupConversation: true, + expectedTitle: '#${stream.name} > $topic', + expectedTagComponent: 'stream:${stream.streamId}:$topic', + expectedSummaryText: account1.realmName); + + receiveFcmMessage(async, data2); + checkNotification(data2, + account: account2, + messageStyleMessages: [data2], + expectedIsGroupConversation: true, + expectedTitle: '#${stream.name} > $topic', + expectedTagComponent: 'stream:${stream.streamId}:$topic', + expectedSummaryText: account2.realmName); + }))); + + test('stream message: realm name absent in account, falls back to notif data', () => runWithHttpClient(() => awaitFakeAsync((async) async { + await init(addSelfAccount: false); + + var account = eg.account( + id: 1001, + user: eg.user(), + realmUrl: Uri.parse('http://realm1.example')); + await testBinding.globalStore.add(account, eg.initialSnapshot()); + // Override the default realmName from eg.account(). + account = await testBinding.globalStore.updateAccount(account.id, + AccountsCompanion(realmName: const Value(null))); + check(account).realmName.isNull(); + + final stream = eg.stream(); + final topic = 'test topic'; + final data = messageFcmMessage( + eg.streamMessage(stream: stream, topic: topic), + account: account, + streamName: stream.name, + realmName: 'Notif realm name'); + check(data).realmName.equals('Notif realm name'); + + receiveFcmMessage(async, data); + checkNotification(data, + account: account, + messageStyleMessages: [data], + expectedIsGroupConversation: true, + expectedTitle: '#${stream.name} > $topic', + expectedTagComponent: 'stream:${stream.streamId}:$topic', + expectedSummaryText: 'Notif realm name'); + }))); + + test('stream message: realm name absent in account and notif data, falls back to realm URL', () => runWithHttpClient(() => awaitFakeAsync((async) async { + await init(addSelfAccount: false); + + var account = eg.account( + id: 1001, + user: eg.user(), + realmUrl: Uri.parse('http://realm1.example')); + await testBinding.globalStore.add(account, eg.initialSnapshot()); + // Override the default realmName from eg.account(). + account = await testBinding.globalStore.updateAccount(account.id, + AccountsCompanion(realmName: const Value(null))); + check(account).realmName.isNull(); + + final stream = eg.stream(); + final topic = 'test topic'; + final data = messageFcmMessage( + eg.streamMessage(stream: stream, topic: topic), + account: account, streamName: stream.name); + check(data).realmName.isNull(); + + receiveFcmMessage(async, data); + checkNotification(data, + account: account, + messageStyleMessages: [data], + expectedIsGroupConversation: true, + expectedTitle: '#${stream.name} > $topic', + expectedTagComponent: 'stream:${stream.streamId}:$topic', + expectedSummaryText: account.realmUrl.toString()); + }))); + test('group DM: 3 users', () => runWithHttpClient(() => awaitFakeAsync((async) async { await init(); final message = eg.dmMessage(from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]);