@@ -461,6 +461,183 @@ void main() async {
461
461
check (model).messages.length.equals (31 );
462
462
check (model.contents[0 ]).equalsNode (correctContent);
463
463
});
464
+
465
+ test ('recipient headers are maintained consistently' , () async {
466
+ // This tests the code that maintains the invariant that recipient headers
467
+ // are present just where [canShareRecipientHeader] requires them.
468
+ // In [checkInvariants] we check the current state against that invariant,
469
+ // so here we just need to exercise that code through all the relevant cases.
470
+ // Each [checkNotifiedOnce] call ensures there's been a [checkInvariants] call
471
+ // (in the listener that increments [notifiedCount]).
472
+ //
473
+ // A separate unit test covers [canShareRecipientHeader] itself.
474
+ // So this test just needs messages that can share, and messages that can't,
475
+ // and doesn't need to exercise the different reasons that messages can't.
476
+
477
+ const timestamp = 1693602618 ;
478
+ final stream = eg.stream ();
479
+ Message streamMessage (int id) =>
480
+ eg.streamMessage (id: id, stream: stream, topic: 'foo' , timestamp: timestamp);
481
+ Message dmMessage (int id) =>
482
+ eg.dmMessage (id: id, from: eg.selfUser, to: [], timestamp: timestamp);
483
+
484
+ // First, test fetchInitial, where some headers are needed and others not.
485
+ prepare ();
486
+ connection.prepare (json: newestResult (
487
+ foundOldest: false ,
488
+ messages: [streamMessage (10 ), streamMessage (11 ), dmMessage (12 )],
489
+ ).toJson ());
490
+ await model.fetchInitial ();
491
+ checkNotifiedOnce ();
492
+
493
+ // Then fetchOlder, where a header is needed in between…
494
+ connection.prepare (json: olderResult (
495
+ anchor: model.messages[0 ].id,
496
+ foundOldest: false ,
497
+ messages: [streamMessage (7 ), streamMessage (8 ), dmMessage (9 )],
498
+ ).toJson ());
499
+ await model.fetchOlder ();
500
+ checkNotified (count: 2 );
501
+
502
+ // … and fetchOlder where there's no header in between.
503
+ connection.prepare (json: olderResult (
504
+ anchor: model.messages[0 ].id,
505
+ foundOldest: false ,
506
+ messages: [streamMessage (6 )],
507
+ ).toJson ());
508
+ await model.fetchOlder ();
509
+ checkNotified (count: 2 );
510
+
511
+ // Then test maybeAddMessage, where a new header is needed…
512
+ model.maybeAddMessage (streamMessage (13 ));
513
+ checkNotifiedOnce ();
514
+
515
+ // … and where it's not.
516
+ model.maybeAddMessage (streamMessage (14 ));
517
+ checkNotifiedOnce ();
518
+
519
+ // Then test maybeUpdateMessage, where a header is and remains needed…
520
+ UpdateMessageEvent updateEvent (Message message) => UpdateMessageEvent (
521
+ id: 1 , messageId: message.id, messageIds: [message.id],
522
+ flags: message.flags,
523
+ renderedContent: '${message .content }<p>edited</p>' ,
524
+ );
525
+ model.maybeUpdateMessage (updateEvent (model.messages.first));
526
+ checkNotifiedOnce ();
527
+ model.maybeUpdateMessage (updateEvent (model.messages[model.messages.length - 2 ]));
528
+ checkNotifiedOnce ();
529
+
530
+ // … and where it's not.
531
+ model.maybeUpdateMessage (updateEvent (model.messages.last));
532
+ checkNotifiedOnce ();
533
+
534
+ // Then test reassemble.
535
+ model.reassemble ();
536
+ checkNotifiedOnce ();
537
+
538
+ // Have a new fetchOlder reach the oldest, so that a history-start marker appears…
539
+ connection.prepare (json: olderResult (
540
+ anchor: model.messages[0 ].id,
541
+ foundOldest: true ,
542
+ messages: [streamMessage (5 )],
543
+ ).toJson ());
544
+ await model.fetchOlder ();
545
+ checkNotified (count: 2 );
546
+
547
+ // … and then test reassemble again.
548
+ model.reassemble ();
549
+ checkNotifiedOnce ();
550
+ });
551
+
552
+ group ('canShareRecipientHeader' , () {
553
+ test ('stream messages vs DMs, no share' , () {
554
+ final dmMessage = eg.dmMessage (from: eg.selfUser, to: [eg.otherUser]);
555
+ final streamMessage = eg.streamMessage (timestamp: dmMessage.timestamp);
556
+ check (canShareRecipientHeader (streamMessage, dmMessage)).isFalse ();
557
+ check (canShareRecipientHeader (dmMessage, streamMessage)).isFalse ();
558
+ });
559
+
560
+ test ('stream messages of same day share just if same stream/topic' , () {
561
+ final stream0 = eg.stream (streamId: 123 );
562
+ final stream1 = eg.stream (streamId: 234 );
563
+ final messageAB = eg.streamMessage (stream: stream0, topic: 'foo' );
564
+ final messageXB = eg.streamMessage (stream: stream1, topic: 'foo' , timestamp: messageAB.timestamp);
565
+ final messageAX = eg.streamMessage (stream: stream0, topic: 'bar' , timestamp: messageAB.timestamp);
566
+ check (canShareRecipientHeader (messageAB, messageAB)).isTrue ();
567
+ check (canShareRecipientHeader (messageAB, messageXB)).isFalse ();
568
+ check (canShareRecipientHeader (messageXB, messageAB)).isFalse ();
569
+ check (canShareRecipientHeader (messageAB, messageAX)).isFalse ();
570
+ check (canShareRecipientHeader (messageAX, messageAB)).isFalse ();
571
+ check (canShareRecipientHeader (messageAX, messageXB)).isFalse ();
572
+ check (canShareRecipientHeader (messageXB, messageAX)).isFalse ();
573
+ });
574
+
575
+ test ('DMs of same day share just if same recipients' , () {
576
+ final message0 = eg.dmMessage (from: eg.selfUser, to: []);
577
+ final message01 = eg.dmMessage (from: eg.selfUser, to: [eg.otherUser], timestamp: message0.timestamp);
578
+ final message10 = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser], timestamp: message0.timestamp);
579
+ final message02 = eg.dmMessage (from: eg.selfUser, to: [eg.thirdUser], timestamp: message0.timestamp);
580
+ final message20 = eg.dmMessage (from: eg.thirdUser, to: [eg.selfUser], timestamp: message0.timestamp);
581
+ final message012 = eg.dmMessage (from: eg.selfUser, to: [eg.otherUser, eg.thirdUser], timestamp: message0.timestamp);
582
+ final message102 = eg.dmMessage (from: eg.otherUser, to: [eg.selfUser, eg.thirdUser], timestamp: message0.timestamp);
583
+ final message201 = eg.dmMessage (from: eg.thirdUser, to: [eg.selfUser, eg.otherUser], timestamp: message0.timestamp);
584
+ final groups = [[message0], [message01, message10],
585
+ [message02, message20], [message012, message102, message201]];
586
+ for (int i0 = 0 ; i0 < groups.length; i0++ ) {
587
+ for (int i1 = 0 ; i1 < groups.length; i1++ ) {
588
+ for (int j0 = 0 ; j0 < groups[i0].length; j0++ ) {
589
+ for (int j1 = 0 ; j1 < groups[i1].length; j1++ ) {
590
+ final message0 = groups[i0][j0];
591
+ final message1 = groups[i1][j1];
592
+ check (
593
+ because: 'recipients ${message0 .allRecipientIds } vs ${message1 .allRecipientIds }' ,
594
+ canShareRecipientHeader (message0, message1),
595
+ ).equals (i0 == i1);
596
+ }
597
+ }
598
+ }
599
+ }
600
+ });
601
+
602
+ test ('messages to same recipient share just if same day' , () {
603
+ // These timestamps will differ depending on the timezone of the
604
+ // environment where the tests are run, in order to give the same results
605
+ // in the code under test which is also based on the ambient timezone.
606
+ // TODO(dart): It'd be great if tests could control the ambient timezone,
607
+ // so as to exercise cases like where local time falls back across midnight.
608
+ int timestampFromLocalTime (String date) => DateTime .parse (date).millisecondsSinceEpoch ~ / 1000 ;
609
+
610
+ const t111a = '2021-01-01 00:00:00' ;
611
+ const t111b = '2021-01-01 12:00:00' ;
612
+ const t111c = '2021-01-01 23:59:58' ;
613
+ const t111d = '2021-01-01 23:59:59' ;
614
+ const t112a = '2021-01-02 00:00:00' ;
615
+ const t112b = '2021-01-02 00:00:01' ;
616
+ const t121 = '2021-02-01 00:00:00' ;
617
+ const t211 = '2022-01-01 00:00:00' ;
618
+ final groups = [[t111a, t111b, t111c, t111d], [t112a, t112b], [t121], [t211]];
619
+
620
+ final stream = eg.stream ();
621
+ for (int i0 = 0 ; i0 < groups.length; i0++ ) {
622
+ for (int i1 = i0; i1 < groups.length; i1++ ) {
623
+ for (int j0 = 0 ; j0 < groups[i0].length; j0++ ) {
624
+ for (int j1 = (i0 == i1) ? j0 : 0 ; j1 < groups[i1].length; j1++ ) {
625
+ final time0 = groups[i0][j0];
626
+ final time1 = groups[i1][j1];
627
+ check (because: 'times $time0 , $time1 ' , canShareRecipientHeader (
628
+ eg.streamMessage (stream: stream, topic: 'foo' , timestamp: timestampFromLocalTime (time0)),
629
+ eg.streamMessage (stream: stream, topic: 'foo' , timestamp: timestampFromLocalTime (time1)),
630
+ )).equals (i0 == i1);
631
+ check (because: 'times $time0 , $time1 ' , canShareRecipientHeader (
632
+ eg.dmMessage (from: eg.selfUser, to: [], timestamp: timestampFromLocalTime (time0)),
633
+ eg.dmMessage (from: eg.selfUser, to: [], timestamp: timestampFromLocalTime (time1)),
634
+ )).equals (i0 == i1);
635
+ }
636
+ }
637
+ }
638
+ }
639
+ });
640
+ });
464
641
}
465
642
466
643
void checkInvariants (MessageListView model) {
@@ -484,9 +661,6 @@ void checkInvariants(MessageListView model) {
484
661
.equalsNode (parseContent (model.messages[i].content));
485
662
}
486
663
487
- check (model).items.length.equals (
488
- ((model.haveOldest || model.fetchingOlder) ? 1 : 0 )
489
- + 2 * model.messages.length);
490
664
int i = 0 ;
491
665
if (model.haveOldest) {
492
666
check (model.items[i++ ]).isA <MessageListHistoryStartItem >();
@@ -495,13 +669,18 @@ void checkInvariants(MessageListView model) {
495
669
check (model.items[i++ ]).isA <MessageListLoadingItem >();
496
670
}
497
671
for (int j = 0 ; j < model.messages.length; j++ ) {
498
- check (model.items[i++ ]).isA <MessageListRecipientHeaderItem >()
499
- .message.identicalTo (model.messages[j]);
672
+ if (j == 0
673
+ || ! canShareRecipientHeader (model.messages[j- 1 ], model.messages[j])) {
674
+ check (model.items[i++ ]).isA <MessageListRecipientHeaderItem >()
675
+ .message.identicalTo (model.messages[j]);
676
+ }
500
677
check (model.items[i++ ]).isA <MessageListMessageItem >()
501
678
..message.identicalTo (model.messages[j])
502
679
..content.identicalTo (model.contents[j])
503
- ..isLastInBlock.isTrue ();
680
+ ..isLastInBlock.equals (
681
+ i == model.items.length || model.items[i] is ! MessageListMessageItem );
504
682
}
683
+ check (model.items).length.equals (i);
505
684
}
506
685
507
686
extension MessageListRecipientHeaderItemChecks on Subject <MessageListRecipientHeaderItem > {
0 commit comments