Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/api/notifications.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -251,6 +256,7 @@ class RemoveFcmMessage extends FcmMessageWithIdentity {
RemoveFcmMessage({
required super.realmUrl,
required super.userId,
required super.realmName,
required this.messageIds,
});

Expand Down
4 changes: 4 additions & 0 deletions lib/api/notifications.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions lib/notifications/display.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions test/api/notifications_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ void main() {
final baseBaseJson = <String, Object?>{
"realm_url": "https://zulip.example.com/",
"user_id": 234,
"realm_name": "Example Organization",
};

// Before E2EE notifications, the data comes directly as FCM payloads,
Expand All @@ -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<String, Object?> data) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -146,6 +149,9 @@ void main() {
.recipient.isA<FcmMessageChannelRecipient>().which((it) => it
..channelId.equals(42)
..channelName.isNull());

check(parse({ ...streamJson }..remove('realm_name')))
.realmName.isNull();
});

test('toJson round-trips', () {
Expand Down Expand Up @@ -319,6 +325,7 @@ extension UnexpectedFcmMessageChecks on Subject<UnexpectedFcmMessage> {
extension FcmMessageWithIdentityChecks on Subject<FcmMessageWithIdentity> {
Subject<Uri> get realmUrl => has((x) => x.realmUrl, 'realmUrl');
Subject<int> get userId => has((x) => x.userId, 'userId');
Subject<String?> get realmName => has((x) => x.realmName, 'realmName');
}

extension MessageFcmMessageChecks on Subject<MessageFcmMessage> {
Expand Down
134 changes: 130 additions & 4 deletions test/notifications/display_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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(),
Expand Down Expand Up @@ -343,13 +348,19 @@ void main() {
});

group('NotificationDisplayManager show', () {
void checkNotification(MessageFcmMessage data, {
void checkNotification(
MessageFcmMessage data, {
Account? account,
required List<MessageFcmMessage> messageStyleMessages,
required String expectedTitle,
required String expectedTagComponent,
required bool expectedIsGroupConversation,
List<int>? 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));

Expand All @@ -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) {
Expand Down Expand Up @@ -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<void> checkNotifications(FakeAsync async, MessageFcmMessage data, {
Future<void> checkNotifications(
FakeAsync async,
MessageFcmMessage data, {
Account? account,
required String expectedTitle,
required String expectedTagComponent,
required bool expectedIsGroupConversation,
Expand All @@ -448,6 +466,7 @@ void main() {
RemoteMessage(data: data.toJson()));
async.flushMicrotasks();
checkNotification(data,
account: account,
messageStyleMessages: [data],
expectedIsGroupConversation: expectedIsGroupConversation,
expectedTitle: expectedTitle,
Expand All @@ -458,6 +477,7 @@ void main() {
RemoteMessage(data: data.toJson()));
async.flushMicrotasks();
checkNotification(data,
account: account,
messageStyleMessages: [data],
expectedIsGroupConversation: expectedIsGroupConversation,
expectedTitle: expectedTitle,
Expand Down Expand Up @@ -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]);
Expand Down
Loading