@@ -2,14 +2,16 @@ import 'dart:convert';
2
2
import 'dart:typed_data' ;
3
3
4
4
import 'package:checks/checks.dart' ;
5
+ import 'package:collection/collection.dart' ;
5
6
import 'package:fake_async/fake_async.dart' ;
6
7
import 'package:firebase_messaging/firebase_messaging.dart' ;
7
8
import 'package:flutter/material.dart' ;
8
- import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Message;
9
+ import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Message, Person ;
9
10
import 'package:flutter_test/flutter_test.dart' ;
10
11
import 'package:zulip/api/model/model.dart' ;
11
12
import 'package:zulip/api/notifications.dart' ;
12
13
import 'package:zulip/host/android_notifications.dart' ;
14
+ import 'package:zulip/model/localizations.dart' ;
13
15
import 'package:zulip/model/narrow.dart' ;
14
16
import 'package:zulip/model/store.dart' ;
15
17
import 'package:zulip/notifications/display.dart' ;
@@ -75,6 +77,7 @@ MessageFcmMessage messageFcmMessage(
75
77
76
78
void main () {
77
79
TestZulipBinding .ensureInitialized ();
80
+ final zulipLocalizations = GlobalLocalizations .zulipLocalizations;
78
81
79
82
Future <void > init () async {
80
83
addTearDown (testBinding.reset);
@@ -107,25 +110,51 @@ void main() {
107
110
108
111
group ('NotificationDisplayManager show' , () {
109
112
void checkNotification (MessageFcmMessage data, {
113
+ required List <MessageFcmMessage > messageStyleMessages,
110
114
required String expectedTitle,
111
115
required String expectedTagComponent,
116
+ required bool expectedIsGroupConversation,
112
117
}) {
113
- final expectedTag = '${data .realmUri }|${data .userId }|$expectedTagComponent ' ;
118
+ const expectedIntentFlags =
119
+ PendingIntentFlag .immutable | PendingIntentFlag .updateCurrent;
120
+ final expectedTag =
121
+ '${data .realmUri }|${data .userId }|$expectedTagComponent ' ;
114
122
final expectedGroupKey = '${data .realmUri }|${data .userId }' ;
115
123
final expectedId =
116
124
NotificationDisplayManager .notificationIdAsHashOf (expectedTag);
117
- const expectedIntentFlags =
118
- PendingIntentFlag .immutable | PendingIntentFlag .updateCurrent;
125
+ final expectedSelfUserKey = '${data .realmUri }|${data .userId }' ;
126
+
127
+ // List of all the checks for messages on each notify calls.
128
+ final messageStyleMessagesChecks = messageStyleMessages.mapIndexed <Condition <Object ?>>((i, data) {
129
+ final expectedSenderKey = '${data .realmUri }|${data .senderId }' ;
130
+ return (it) => it.isA <MessagingStyleMessage >()
131
+ ..text.equals (data.content)
132
+ ..timestampMs.equals (data.time * 1000 )
133
+ ..person.which ((it) => it.isNotNull ()
134
+ ..iconBitmap.which ((it) => i == (messageStyleMessages.length - 1 )
135
+ ? it.isNotNull ()
136
+ : it.isNull ())
137
+ ..key.equals (expectedSenderKey)
138
+ ..name.equals (data.senderFullName));
139
+ });
119
140
120
141
check (testBinding.androidNotificationHost.takeNotifyCalls ())
121
- ..length.equals (2 )
122
- ..containsInOrder (< Condition <AndroidNotificationHostApiNotifyCall >> [
123
- (it) => it
142
+ .deepEquals (< Condition <Object ?>> [
143
+ (it) => it.isA <AndroidNotificationHostApiNotifyCall >()
124
144
..id.equals (expectedId)
125
145
..tag.equals (expectedTag)
126
146
..channelId.equals (NotificationChannelManager .kChannelId)
127
- ..contentTitle.equals (expectedTitle)
128
- ..contentText.equals (data.content)
147
+ ..contentTitle.isNull ()
148
+ ..contentText.isNull ()
149
+ ..messagingStyle.which ((it) => it.isNotNull ()
150
+ ..user.which ((it) => it
151
+ ..iconBitmap.isNull ()
152
+ ..key.equals (expectedSelfUserKey)
153
+ ..name.equals (zulipLocalizations.notifSelfUser))
154
+ ..isGroupConversation.equals (expectedIsGroupConversation)
155
+ ..conversationTitle.equals (expectedTitle)
156
+ ..messages.deepEquals (messageStyleMessagesChecks))
157
+ ..number.equals (messageStyleMessages.length)
129
158
..color.equals (kZulipBrandColor.value)
130
159
..smallIconResourceName.equals ('zulip_notification' )
131
160
..extras.isNull ()
@@ -137,7 +166,7 @@ void main() {
137
166
..requestCode.equals (expectedId)
138
167
..flags.equals (expectedIntentFlags)
139
168
..intentPayload.equals (jsonEncode (data.toJson ()))),
140
- (it) => it
169
+ (it) => it. isA < AndroidNotificationHostApiNotifyCall >()
141
170
..id.equals (NotificationDisplayManager .notificationIdAsHashOf (expectedGroupKey))
142
171
..tag.equals (expectedGroupKey)
143
172
..channelId.equals (NotificationChannelManager .kChannelId)
@@ -151,13 +180,15 @@ void main() {
151
180
..inboxStyle.which ((it) => it.isNotNull ()
152
181
..summaryText.equals (data.realmUri.toString ()))
153
182
..autoCancel.equals (true )
154
- ..contentIntent.isNull ()
183
+ ..contentIntent.isNull (),
155
184
]);
156
185
}
157
186
187
+
158
188
Future <void > checkNotifications (FakeAsync async , MessageFcmMessage data, {
159
189
required String expectedTitle,
160
190
required String expectedTagComponent,
191
+ required bool expectedIsGroupConversation,
161
192
}) async {
162
193
// We could just call `NotificationDisplayManager.onFcmMessage`.
163
194
// But this way is cheap, and it provides our test coverage of
@@ -166,30 +197,79 @@ void main() {
166
197
testBinding.firebaseMessaging.onMessage.add (
167
198
RemoteMessage (data: data.toJson ()));
168
199
async .flushMicrotasks ();
169
- checkNotification (data, expectedTitle: expectedTitle,
200
+ checkNotification (data,
201
+ messageStyleMessages: [data],
202
+ expectedIsGroupConversation: expectedIsGroupConversation,
203
+ expectedTitle: expectedTitle,
170
204
expectedTagComponent: expectedTagComponent);
205
+ testBinding.androidNotificationHost.clearActiveNotifications ();
171
206
172
207
testBinding.firebaseMessaging.onBackgroundMessage.add (
173
208
RemoteMessage (data: data.toJson ()));
174
209
async .flushMicrotasks ();
175
- checkNotification (data, expectedTitle: expectedTitle,
210
+ checkNotification (data,
211
+ messageStyleMessages: [data],
212
+ expectedIsGroupConversation: expectedIsGroupConversation,
213
+ expectedTitle: expectedTitle,
176
214
expectedTagComponent: expectedTagComponent);
215
+ testBinding.androidNotificationHost.clearActiveNotifications ();
216
+ }
217
+
218
+ Future <void > receiveFcmMessage (FakeAsync async , MessageFcmMessage data) async {
219
+ testBinding.firebaseMessaging.onMessage.add (
220
+ RemoteMessage (data: data.toJson ()));
221
+ async .flushMicrotasks ();
177
222
}
178
223
179
224
test ('stream message' , () => awaitFakeAsync ((async ) async {
180
225
await init ();
181
226
final stream = eg.stream ();
182
227
final message = eg.streamMessage (stream: stream);
183
228
await checkNotifications (async , messageFcmMessage (message, streamName: stream.name),
229
+ expectedIsGroupConversation: true ,
184
230
expectedTitle: '#${stream .name } > ${message .topic }' ,
185
231
expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
186
232
}));
187
233
234
+ test ('multiple stream messages, same topic' , () => awaitFakeAsync ((async ) async {
235
+ await init ();
236
+ final stream = eg.stream ();
237
+ final message1 = eg.streamMessage (topic: 'topic 1' , stream: stream, timestamp: 1 );
238
+ final data1 = messageFcmMessage (message1, streamName: stream.name);
239
+ final message2 = eg.streamMessage (topic: 'topic 1' , stream: stream, timestamp: 2 );
240
+ final data2 = messageFcmMessage (message2, streamName: stream.name);
241
+ final message3 = eg.streamMessage (topic: 'topic 1' , stream: stream, timestamp: 3 );
242
+ final data3 = messageFcmMessage (message3, streamName: stream.name);
243
+
244
+ await receiveFcmMessage (async , data1);
245
+ checkNotification (data1,
246
+ messageStyleMessages: [data1],
247
+ expectedIsGroupConversation: true ,
248
+ expectedTitle: '#${stream .name } > ${message1 .topic }' ,
249
+ expectedTagComponent: 'stream:${message1 .streamId }:${message1 .topic }' );
250
+
251
+ await receiveFcmMessage (async , data2);
252
+ checkNotification (data2,
253
+ messageStyleMessages: [data1, data2],
254
+ expectedIsGroupConversation: true ,
255
+ expectedTitle: '#${stream .name } > ${message2 .topic }' ,
256
+ expectedTagComponent: 'stream:${message2 .streamId }:${message2 .topic }' );
257
+
258
+ await receiveFcmMessage (async , data3);
259
+ checkNotification (data3,
260
+ messageStyleMessages: [data1, data2, data3],
261
+ expectedIsGroupConversation: true ,
262
+ expectedTitle: '#${stream .name } > ${message3 .topic }' ,
263
+ expectedTagComponent: 'stream:${message3 .streamId }:${message3 .topic }' );
264
+ testBinding.androidNotificationHost.clearActiveNotifications ();
265
+ }));
266
+
188
267
test ('stream message, stream name omitted' , () => awaitFakeAsync ((async ) async {
189
268
await init ();
190
269
final stream = eg.stream ();
191
270
final message = eg.streamMessage (stream: stream);
192
271
await checkNotifications (async , messageFcmMessage (message, streamName: null ),
272
+ expectedIsGroupConversation: true ,
193
273
expectedTitle: '#(unknown channel) > ${message .topic }' ,
194
274
expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
195
275
}));
@@ -198,6 +278,7 @@ void main() {
198
278
await init ();
199
279
final message = eg.dmMessage (from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]);
200
280
await checkNotifications (async , messageFcmMessage (message),
281
+ expectedIsGroupConversation: true ,
201
282
expectedTitle: "${eg .thirdUser .fullName } to you and 1 other" ,
202
283
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
203
284
}));
@@ -207,6 +288,7 @@ void main() {
207
288
final message = eg.dmMessage (from: eg.thirdUser,
208
289
to: [eg.otherUser, eg.selfUser, eg.fourthUser]);
209
290
await checkNotifications (async , messageFcmMessage (message),
291
+ expectedIsGroupConversation: true ,
210
292
expectedTitle: "${eg .thirdUser .fullName } to you and 2 others" ,
211
293
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
212
294
}));
@@ -215,6 +297,7 @@ void main() {
215
297
await init ();
216
298
final message = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser]);
217
299
await checkNotifications (async , messageFcmMessage (message),
300
+ expectedIsGroupConversation: false ,
218
301
expectedTitle: eg.otherUser.fullName,
219
302
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
220
303
}));
@@ -223,6 +306,7 @@ void main() {
223
306
await init ();
224
307
final message = eg.dmMessage (from: eg.selfUser, to: []);
225
308
await checkNotifications (async , messageFcmMessage (message),
309
+ expectedIsGroupConversation: false ,
226
310
expectedTitle: eg.selfUser.fullName,
227
311
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
228
312
}));
@@ -403,6 +487,8 @@ extension on Subject<AndroidNotificationHostApiNotifyCall> {
403
487
Subject <String ?> get groupKey => has ((x) => x.groupKey, 'groupKey' );
404
488
Subject <InboxStyle ?> get inboxStyle => has ((x) => x.inboxStyle, 'inboxStyle' );
405
489
Subject <bool ?> get isGroupSummary => has ((x) => x.isGroupSummary, 'isGroupSummary' );
490
+ Subject <MessagingStyle ?> get messagingStyle => has ((x) => x.messagingStyle, 'messagingStyle' );
491
+ Subject <int ?> get number => has ((x) => x.number, 'number' );
406
492
Subject <String ?> get smallIconResourceName => has ((x) => x.smallIconResourceName, 'smallIconResourceName' );
407
493
}
408
494
@@ -415,3 +501,22 @@ extension on Subject<PendingIntent> {
415
501
extension on Subject <InboxStyle > {
416
502
Subject <String > get summaryText => has ((x) => x.summaryText, 'summaryText' );
417
503
}
504
+
505
+ extension on Subject <MessagingStyle > {
506
+ Subject <Person > get user => has ((x) => x.user, 'user' );
507
+ Subject <String ?> get conversationTitle => has ((x) => x.conversationTitle, 'conversationTitle' );
508
+ Subject <List <MessagingStyleMessage ?>> get messages => has ((x) => x.messages, 'messages' );
509
+ Subject <bool > get isGroupConversation => has ((x) => x.isGroupConversation, 'isGroupConversation' );
510
+ }
511
+
512
+ extension on Subject <Person > {
513
+ Subject <Uint8List ?> get iconBitmap => has ((x) => x.iconBitmap, 'iconBitmap' );
514
+ Subject <String > get key => has ((x) => x.key, 'key' );
515
+ Subject <String > get name => has ((x) => x.name, 'name' );
516
+ }
517
+
518
+ extension on Subject <MessagingStyleMessage > {
519
+ Subject <String > get text => has ((x) => x.text, 'text' );
520
+ Subject <int > get timestampMs => has ((x) => x.timestampMs, 'timestampMs' );
521
+ Subject <Person > get person => has ((x) => x.person, 'person' );
522
+ }
0 commit comments