@@ -2,10 +2,11 @@ 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' ;
@@ -107,25 +108,53 @@ void main() {
107
108
108
109
group ('NotificationDisplayManager show' , () {
109
110
void checkNotification (MessageFcmMessage data, {
111
+ required List <MessageFcmMessage > messageStyleMessages,
110
112
required String expectedTitle,
111
113
required String expectedTagComponent,
114
+ required bool expectedIsGroupConversation,
112
115
}) {
113
116
final expectedTag = '${data .realmUri }|${data .userId }|$expectedTagComponent ' ;
114
117
final expectedGroupKey = '${data .realmUri }|${data .userId }' ;
115
118
final expectedId =
116
119
NotificationDisplayManager .notificationIdAsHashOf (expectedTag);
117
120
const expectedIntentFlags =
118
121
PendingIntentFlag .immutable | PendingIntentFlag .updateCurrent;
122
+ final expectedSelfUserKey = '${data .realmUri }|${data .userId }' ;
123
+
124
+ final messageStyleMessagesChecks =
125
+ messageStyleMessages.mapIndexed ((i, messageData) {
126
+ assert (messageData.realmUri == data.realmUri);
127
+ assert (messageData.userId == data.userId);
128
+
129
+ final expectedSenderKey =
130
+ '${messageData .realmUri }|${messageData .senderId }' ;
131
+ final isLast = i == (messageStyleMessages.length - 1 );
132
+ return (Subject <Object ?> it) => it.isA <MessagingStyleMessage >()
133
+ ..text.equals (messageData.content)
134
+ ..timestampMs.equals (messageData.time * 1000 )
135
+ ..person.which ((it) => it.isNotNull ()
136
+ ..iconBitmap.which ((it) => isLast ? it.isNotNull () : it.isNull ())
137
+ ..key.equals (expectedSenderKey)
138
+ ..name.equals (messageData.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 ('You' )) // TODO(i18n)
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,14 @@ 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
158
187
Future <void > checkNotifications (FakeAsync async , MessageFcmMessage data, {
159
188
required String expectedTitle,
160
189
required String expectedTagComponent,
190
+ required bool expectedIsGroupConversation,
161
191
}) async {
162
192
// We could just call `NotificationDisplayManager.onFcmMessage`.
163
193
// But this way is cheap, and it provides our test coverage of
@@ -166,30 +196,81 @@ void main() {
166
196
testBinding.firebaseMessaging.onMessage.add (
167
197
RemoteMessage (data: data.toJson ()));
168
198
async .flushMicrotasks ();
169
- checkNotification (data, expectedTitle: expectedTitle,
199
+ checkNotification (data,
200
+ messageStyleMessages: [data],
201
+ expectedIsGroupConversation: expectedIsGroupConversation,
202
+ expectedTitle: expectedTitle,
170
203
expectedTagComponent: expectedTagComponent);
204
+ testBinding.androidNotificationHost.clearActiveNotifications ();
171
205
172
206
testBinding.firebaseMessaging.onBackgroundMessage.add (
173
207
RemoteMessage (data: data.toJson ()));
174
208
async .flushMicrotasks ();
175
- checkNotification (data, expectedTitle: expectedTitle,
209
+ checkNotification (data,
210
+ messageStyleMessages: [data],
211
+ expectedIsGroupConversation: expectedIsGroupConversation,
212
+ expectedTitle: expectedTitle,
176
213
expectedTagComponent: expectedTagComponent);
177
214
}
178
215
216
+ Future <void > receiveFcmMessage (FakeAsync async , MessageFcmMessage data) async {
217
+ testBinding.firebaseMessaging.onMessage.add (
218
+ RemoteMessage (data: data.toJson ()));
219
+ async .flushMicrotasks ();
220
+ }
221
+
179
222
test ('stream message' , () => awaitFakeAsync ((async ) async {
180
223
await init ();
181
224
final stream = eg.stream ();
182
225
final message = eg.streamMessage (stream: stream);
183
226
await checkNotifications (async , messageFcmMessage (message, streamName: stream.name),
227
+ expectedIsGroupConversation: true ,
184
228
expectedTitle: '#${stream .name } > ${message .topic }' ,
185
229
expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
186
230
}));
187
231
188
- test ('stream message, stream name omitted' , () => awaitFakeAsync ((async ) async {
232
+ test ('stream message: multiple messages, same topic' , () => awaitFakeAsync ((async ) async {
233
+ await init ();
234
+ final stream = eg.stream ();
235
+ const topic = 'topic 1' ;
236
+ final message1 = eg.streamMessage (topic: topic, stream: stream);
237
+ final data1 = messageFcmMessage (message1, streamName: stream.name);
238
+ final message2 = eg.streamMessage (topic: topic, stream: stream);
239
+ final data2 = messageFcmMessage (message2, streamName: stream.name);
240
+ final message3 = eg.streamMessage (topic: topic, stream: stream);
241
+ final data3 = messageFcmMessage (message3, streamName: stream.name);
242
+
243
+ final expectedTitle = '#${stream .name } > $topic ' ;
244
+ final expectedTagComponent = 'stream:${stream .streamId }:$topic ' ;
245
+
246
+ await receiveFcmMessage (async , data1);
247
+ checkNotification (data1,
248
+ messageStyleMessages: [data1],
249
+ expectedIsGroupConversation: true ,
250
+ expectedTitle: expectedTitle,
251
+ expectedTagComponent: expectedTagComponent);
252
+
253
+ await receiveFcmMessage (async , data2);
254
+ checkNotification (data2,
255
+ messageStyleMessages: [data1, data2],
256
+ expectedIsGroupConversation: true ,
257
+ expectedTitle: expectedTitle,
258
+ expectedTagComponent: expectedTagComponent);
259
+
260
+ await receiveFcmMessage (async , data3);
261
+ checkNotification (data3,
262
+ messageStyleMessages: [data1, data2, data3],
263
+ expectedIsGroupConversation: true ,
264
+ expectedTitle: expectedTitle,
265
+ expectedTagComponent: expectedTagComponent);
266
+ }));
267
+
268
+ test ('stream message: stream name omitted' , () => awaitFakeAsync ((async ) async {
189
269
await init ();
190
270
final stream = eg.stream ();
191
271
final message = eg.streamMessage (stream: stream);
192
272
await checkNotifications (async , messageFcmMessage (message, streamName: null ),
273
+ expectedIsGroupConversation: true ,
193
274
expectedTitle: '#(unknown channel) > ${message .topic }' ,
194
275
expectedTagComponent: 'stream:${message .streamId }:${message .topic }' );
195
276
}));
@@ -198,6 +279,7 @@ void main() {
198
279
await init ();
199
280
final message = eg.dmMessage (from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]);
200
281
await checkNotifications (async , messageFcmMessage (message),
282
+ expectedIsGroupConversation: true ,
201
283
expectedTitle: "${eg .thirdUser .fullName } to you and 1 other" ,
202
284
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
203
285
}));
@@ -207,6 +289,7 @@ void main() {
207
289
final message = eg.dmMessage (from: eg.thirdUser,
208
290
to: [eg.otherUser, eg.selfUser, eg.fourthUser]);
209
291
await checkNotifications (async , messageFcmMessage (message),
292
+ expectedIsGroupConversation: true ,
210
293
expectedTitle: "${eg .thirdUser .fullName } to you and 2 others" ,
211
294
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
212
295
}));
@@ -215,6 +298,7 @@ void main() {
215
298
await init ();
216
299
final message = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser]);
217
300
await checkNotifications (async , messageFcmMessage (message),
301
+ expectedIsGroupConversation: false ,
218
302
expectedTitle: eg.otherUser.fullName,
219
303
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
220
304
}));
@@ -223,6 +307,7 @@ void main() {
223
307
await init ();
224
308
final message = eg.dmMessage (from: eg.selfUser, to: []);
225
309
await checkNotifications (async , messageFcmMessage (message),
310
+ expectedIsGroupConversation: false ,
226
311
expectedTitle: eg.selfUser.fullName,
227
312
expectedTagComponent: 'dm:${message .allRecipientIds .join ("," )}' );
228
313
}));
@@ -403,6 +488,8 @@ extension on Subject<AndroidNotificationHostApiNotifyCall> {
403
488
Subject <String ?> get groupKey => has ((x) => x.groupKey, 'groupKey' );
404
489
Subject <InboxStyle ?> get inboxStyle => has ((x) => x.inboxStyle, 'inboxStyle' );
405
490
Subject <bool ?> get isGroupSummary => has ((x) => x.isGroupSummary, 'isGroupSummary' );
491
+ Subject <MessagingStyle ?> get messagingStyle => has ((x) => x.messagingStyle, 'messagingStyle' );
492
+ Subject <int ?> get number => has ((x) => x.number, 'number' );
406
493
Subject <String ?> get smallIconResourceName => has ((x) => x.smallIconResourceName, 'smallIconResourceName' );
407
494
}
408
495
@@ -415,3 +502,22 @@ extension on Subject<PendingIntent> {
415
502
extension on Subject <InboxStyle > {
416
503
Subject <String > get summaryText => has ((x) => x.summaryText, 'summaryText' );
417
504
}
505
+
506
+ extension on Subject <MessagingStyle > {
507
+ Subject <Person > get user => has ((x) => x.user, 'user' );
508
+ Subject <String ?> get conversationTitle => has ((x) => x.conversationTitle, 'conversationTitle' );
509
+ Subject <List <MessagingStyleMessage ?>> get messages => has ((x) => x.messages, 'messages' );
510
+ Subject <bool > get isGroupConversation => has ((x) => x.isGroupConversation, 'isGroupConversation' );
511
+ }
512
+
513
+ extension on Subject <Person > {
514
+ Subject <Uint8List ?> get iconBitmap => has ((x) => x.iconBitmap, 'iconBitmap' );
515
+ Subject <String > get key => has ((x) => x.key, 'key' );
516
+ Subject <String > get name => has ((x) => x.name, 'name' );
517
+ }
518
+
519
+ extension on Subject <MessagingStyleMessage > {
520
+ Subject <String > get text => has ((x) => x.text, 'text' );
521
+ Subject <int > get timestampMs => has ((x) => x.timestampMs, 'timestampMs' );
522
+ Subject <Person > get person => has ((x) => x.person, 'person' );
523
+ }
0 commit comments