@@ -5,11 +5,13 @@ import 'package:zulip/api/model/events.dart';
55import 'package:zulip/api/model/initial_snapshot.dart' ;
66import 'package:zulip/api/model/model.dart' ;
77import 'package:zulip/model/algorithms.dart' ;
8+ import 'package:zulip/model/channel.dart' ;
89import 'package:zulip/model/narrow.dart' ;
910import 'package:zulip/model/store.dart' ;
1011import 'package:zulip/model/unreads.dart' ;
1112
1213import '../example_data.dart' as eg;
14+ import '../stdlib_checks.dart' ;
1315import 'test_store.dart' ;
1416import 'unreads_checks.dart' ;
1517
@@ -76,7 +78,7 @@ void main() {
7678 assert (Set .of (messages.map ((m) => m.id)).length == messages.length,
7779 'checkMatchesMessages: duplicate messages in test input' );
7880
79- final Map <int , Map < TopicName , QueueList <int >>> expectedStreams = {};
81+ final Map <int , TopicKeyedMap < QueueList <int >>> expectedStreams = {};
8082 final Map <DmNarrow , QueueList <int >> expectedDms = {};
8183 final Set <int > expectedMentions = {};
8284 for (final message in messages) {
@@ -85,7 +87,7 @@ void main() {
8587 }
8688 switch (message) {
8789 case StreamMessage ():
88- final perTopic = expectedStreams[message.streamId] ?? = {} ;
90+ final perTopic = expectedStreams[message.streamId] ?? = makeTopicKeyedMap () ;
8991 final messageIds = perTopic[message.topic] ?? = QueueList ();
9092 messageIds.add (message.id);
9193 case DmMessage ():
@@ -136,6 +138,9 @@ void main() {
136138 eg.unreadChannelMsgs (streamId: stream1.streamId, topic: 'b' , unreadMessageIds: [3 , 4 ]),
137139 eg.unreadChannelMsgs (streamId: stream2.streamId, topic: 'b' , unreadMessageIds: [5 , 6 ]),
138140 eg.unreadChannelMsgs (streamId: stream2.streamId, topic: 'c' , unreadMessageIds: [7 , 8 ]),
141+
142+ // TODO(server-10) drop this (see implementation)
143+ eg.unreadChannelMsgs (streamId: stream2.streamId, topic: 'C' , unreadMessageIds: [9 , 10 ]),
139144 ],
140145 dms: [
141146 UnreadDmSnapshot (otherUserId: 1 , unreadMessageIds: [11 , 12 ]),
@@ -157,6 +162,8 @@ void main() {
157162 eg.streamMessage (id: 6 , stream: stream2, topic: 'b' , flags: [MessageFlag .mentioned]),
158163 eg.streamMessage (id: 7 , stream: stream2, topic: 'c' , flags: []),
159164 eg.streamMessage (id: 8 , stream: stream2, topic: 'c' , flags: []),
165+ eg.streamMessage (id: 9 , stream: stream2, topic: 'C' , flags: []),
166+ eg.streamMessage (id: 10 , stream: stream2, topic: 'C' , flags: []),
160167 eg.dmMessage (id: 11 , from: user1, to: [eg.selfUser], flags: []),
161168 eg.dmMessage (id: 12 , from: user1, to: [eg.selfUser], flags: []),
162169 eg.dmMessage (id: 13 , from: user2, to: [eg.selfUser], flags: []),
@@ -201,10 +208,10 @@ void main() {
201208 await store.addUserTopic (stream, 'c' , UserTopicVisibilityPolicy .muted);
202209 fillWithMessages ([
203210 eg.streamMessage (stream: stream, topic: 'a' , flags: []),
204- eg.streamMessage (stream: stream, topic: 'a' , flags: []),
205- eg.streamMessage (stream: stream, topic: 'b' , flags: []),
211+ eg.streamMessage (stream: stream, topic: 'A' , flags: []),
206212 eg.streamMessage (stream: stream, topic: 'b' , flags: []),
207213 eg.streamMessage (stream: stream, topic: 'b' , flags: []),
214+ eg.streamMessage (stream: stream, topic: 'B' , flags: []),
208215 eg.streamMessage (stream: stream, topic: 'c' , flags: []),
209216 ]);
210217 check (model.countInChannel (stream.streamId)).equals (5 );
@@ -220,9 +227,13 @@ void main() {
220227 test ('countInTopicNarrow' , () {
221228 final stream = eg.stream ();
222229 prepare ();
223- fillWithMessages (List .generate (7 , (i) => eg.streamMessage (
224- stream: stream, topic: 'a' , flags: [])));
225- check (model.countInTopicNarrow (stream.streamId, eg.t ('a' ))).equals (7 );
230+ final messages = [
231+ ...List .generate (7 , (i) => eg.streamMessage (stream: stream, topic: 'a' , flags: [])),
232+ ...List .generate (2 , (i) => eg.streamMessage (stream: stream, topic: 'A' , flags: [])),
233+ ];
234+ fillWithMessages (messages);
235+ check (model.countInTopicNarrow (stream.streamId, eg.t ('a' ))).equals (9 );
236+ check (model.countInTopicNarrow (stream.streamId, eg.t ('A' ))).equals (9 );
226237 });
227238
228239 test ('countInDmNarrow' , () {
@@ -370,6 +381,24 @@ void main() {
370381 });
371382 }
372383 });
384+
385+ test ('topics case-insensitive but case-preserving' , () {
386+ final stream = eg.stream ();
387+ final message1 = eg.streamMessage (stream: stream, topic: 'aaa' );
388+ final message2 = eg.streamMessage (stream: stream, topic: 'AaA' );
389+ final message3 = eg.streamMessage (stream: stream, topic: 'aAa' );
390+ prepare ();
391+ fillWithMessages ([message1]);
392+ model.handleMessageEvent (eg.messageEvent (message2));
393+ model.handleMessageEvent (eg.messageEvent (message3));
394+ checkNotified (count: 2 );
395+ checkMatchesMessages ([message1, message2, message3]);
396+ // Redundant with checkMatchesMessages, but for explicitness here:
397+ check (model).streams.values.single
398+ .entries.single
399+ ..key.equals (eg.t ('aaa' ))
400+ ..value.length.equals (3 );
401+ });
373402 });
374403
375404 group ('DM messages' , () {
@@ -629,6 +658,39 @@ void main() {
629658 checkMatchesMessages (copyMessagesWith (unreadMessages, newTopic: newTopic));
630659 });
631660
661+ test ('topics case-insensitive but case-preserving' , () async {
662+ final message1 = eg.streamMessage (stream: origChannel, topic: 'aaa' , flags: []);
663+ final message2 = eg.streamMessage (stream: origChannel, topic: 'aaa' , flags: []);
664+ final messages = [message1, message2];
665+ await prepareStore ();
666+ fillWithMessages (messages);
667+
668+ model.handleUpdateMessageEvent (eg.updateMessageEventMoveFrom (
669+ // 'AAA' finds the key 'aaa'
670+ origMessages: copyMessagesWith ([message1], newTopic: 'AAA' ),
671+ newTopicStr: 'bbb' ));
672+ checkNotifiedOnce ();
673+ checkMatchesMessages ([
674+ ...copyMessagesWith ([message1], newTopic: 'bbb' ),
675+ message2,
676+ ]);
677+
678+ model.handleUpdateMessageEvent (eg.updateMessageEventMoveFrom (
679+ origMessages: [message2],
680+ // 'BBB' finds the key 'bbb'
681+ newTopicStr: 'BBB' ));
682+ checkNotifiedOnce ();
683+ checkMatchesMessages ([
684+ ...copyMessagesWith ([message1], newTopic: 'bbb' ),
685+ ...copyMessagesWith ([message2], newTopic: 'BBB' ),
686+ ]);
687+ // Redundant with checkMatchesMessages, but for explicitness here:
688+ check (model).streams.values.single
689+ .entries.single
690+ ..key.equals (eg.t ('bbb' ))
691+ ..value.length.equals (2 );
692+ });
693+
632694 test ('tolerates unreads unknown to the model' , () async {
633695 await prepareStore ();
634696 fillWithMessages (unreadMessages);
@@ -690,14 +752,20 @@ void main() {
690752 fillWithMessages (messages);
691753
692754 final expectedRemainingMessages = Set .of (messages);
755+ assert (messages.any ((m) => m.id == 14 ));
693756 for (final message in messages) {
694757 final event = switch (message) {
695758 StreamMessage () => DeleteMessageEvent (
696759 id: 0 ,
697760 messageType: MessageType .stream,
698761 messageIds: [message.id],
699762 streamId: message.streamId,
700- topic: message.topic,
763+ topic: () {
764+ if (message.id != 14 ) return message.topic;
765+ final uppercase = message.topic.apiName.toUpperCase ();
766+ assert (message.topic.apiName != uppercase);
767+ return eg.t (uppercase); // exercise case-insensitivity of topics
768+ }(),
701769 ),
702770 DmMessage () => DeleteMessageEvent (
703771 id: 0 ,
0 commit comments