@@ -41,6 +41,7 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
4141 required ChannelStore channelStore,
4242 required UnreadMessagesSnapshot initial,
4343 }) {
44+ final locatorMap = < int , SendableNarrow > {};
4445 final streams = < int , TopicKeyedMap <QueueList <int >>> {};
4546 final dms = < DmNarrow , QueueList <int >> {};
4647 final mentions = Set .of (initial.mentions);
@@ -57,23 +58,34 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
5758 // TODO(server-10) simplify away
5859 (value) => setUnion (value, unreadChannelSnapshot.unreadMessageIds),
5960 ifAbsent: () => QueueList .from (unreadChannelSnapshot.unreadMessageIds));
61+ final narrow = TopicNarrow (streamId, topic);
62+ for (final messageId in unreadChannelSnapshot.unreadMessageIds) {
63+ locatorMap[messageId] = narrow;
64+ }
6065 }
6166
6267 for (final unreadDmSnapshot in initial.dms) {
6368 final otherUserId = unreadDmSnapshot.otherUserId;
6469 final narrow = DmNarrow .withUser (otherUserId, selfUserId: core.selfUserId);
6570 dms[narrow] = QueueList .from (unreadDmSnapshot.unreadMessageIds);
71+ for (final messageId in dms[narrow]! ) {
72+ locatorMap[messageId] = narrow;
73+ }
6674 }
6775
6876 for (final unreadHuddleSnapshot in initial.huddles) {
6977 final narrow = DmNarrow .ofUnreadHuddleSnapshot (unreadHuddleSnapshot,
7078 selfUserId: core.selfUserId);
7179 dms[narrow] = QueueList .from (unreadHuddleSnapshot.unreadMessageIds);
80+ for (final messageId in dms[narrow]! ) {
81+ locatorMap[messageId] = narrow;
82+ }
7283 }
7384
7485 return Unreads ._(
7586 core: core,
7687 channelStore: channelStore,
88+ locatorMap: locatorMap,
7789 streams: streams,
7890 dms: dms,
7991 mentions: mentions,
@@ -84,6 +96,7 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
8496 Unreads ._({
8597 required super .core,
8698 required this .channelStore,
99+ required this .locatorMap,
87100 required this .streams,
88101 required this .dms,
89102 required this .mentions,
@@ -92,6 +105,12 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
92105
93106 final ChannelStore channelStore;
94107
108+ /// All unread messages, as: message ID → narrow ([TopicNarrow] or [DmNarrow] ).
109+ ///
110+ /// Enables efficient [isUnread] and efficient lookups in [streams] and [dms] .
111+ @visibleForTesting
112+ final Map <int , SendableNarrow > locatorMap;
113+
95114 // TODO excluded for now; would need to handle nuances around muting etc.
96115 // int count;
97116
@@ -233,11 +252,8 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
233252 /// The unread state for [messageId] , or null if unknown.
234253 ///
235254 /// May be unknown only if [oldUnreadsMissing] .
236- ///
237- /// This is inefficient; it iterates through [dms] and [channels] .
238- // TODO implement efficiently
239255 bool ? isUnread (int messageId) {
240- final isPresent = _slowIsPresentInDms (messageId) || _slowIsPresentInStreams (messageId);
256+ final isPresent = locatorMap. containsKey (messageId);
241257 if (oldUnreadsMissing && ! isPresent) return null ;
242258 return isPresent;
243259 }
@@ -250,9 +266,12 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
250266
251267 switch (message) {
252268 case StreamMessage ():
269+ final narrow = TopicNarrow .ofMessage (message);
270+ locatorMap[event.message.id] = narrow;
253271 _addLastInStreamTopic (message.id, message.streamId, message.topic);
254272 case DmMessage ():
255273 final narrow = DmNarrow .ofMessage (message, selfUserId: selfUserId);
274+ locatorMap[event.message.id] = narrow;
256275 _addLastInDm (message.id, narrow);
257276 }
258277 if (
@@ -346,24 +365,35 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
346365 // Unreads moved to an unsubscribed channel; just drop them.
347366 // See also:
348367 // https://chat.zulip.org/#narrow/channel/378-api-design/topic/mark-as-read.20events.20with.20message.20moves.3F/near/2101926
368+ for (final messageId in messageToMoveIds) {
369+ locatorMap.remove (messageId);
370+ }
349371 return true ;
350372 }
351373
374+ final narrow = TopicNarrow (newStreamId, newTopic);
375+ for (final messageId in messageToMoveIds) {
376+ locatorMap[messageId] = narrow;
377+ }
352378 _addAllInStreamTopic (messageToMoveIds, newStreamId, newTopic);
353379
354380 return true ;
355381 }
356382
357383 void handleDeleteMessageEvent (DeleteMessageEvent event) {
358384 mentions.removeAll (event.messageIds);
359- final messageIdsSet = Set .of (event.messageIds);
360385 switch (event.messageType) {
361386 case MessageType .stream:
387+ // All the messages are in [event.streamId] and [event.topic],
388+ // so we can be more efficient than _removeAllInStreamsAndDms.
362389 final streamId = event.streamId! ;
363390 final topic = event.topic! ;
364- _removeAllInStreamTopic (messageIdsSet , streamId, topic);
391+ _removeAllInStreamTopic (Set . of (event.messageIds) , streamId, topic);
365392 case MessageType .direct:
366- _slowRemoveAllInDms (messageIdsSet);
393+ _removeAllInStreamsAndDms (event.messageIds, expectOnlyDms: true );
394+ }
395+ for (final messageId in event.messageIds) {
396+ locatorMap.remove (messageId);
367397 }
368398
369399 // TODO skip notifyListeners if unchanged?
@@ -405,15 +435,18 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
405435 switch (event) {
406436 case UpdateMessageFlagsAddEvent ():
407437 if (event.all) {
438+ locatorMap.clear ();
408439 streams.clear ();
409440 dms.clear ();
410441 mentions.clear ();
411442 oldUnreadsMissing = false ;
412443 } else {
413- final messageIdsSet = Set .of (event.messages);
414- mentions.removeAll (messageIdsSet);
415- _slowRemoveAllInStreams (messageIdsSet);
416- _slowRemoveAllInDms (messageIdsSet);
444+ final messageIds = event.messages;
445+ mentions.removeAll (messageIds);
446+ _removeAllInStreamsAndDms (messageIds);
447+ for (final messageId in messageIds) {
448+ locatorMap.remove (messageId);
449+ }
417450 }
418451 case UpdateMessageFlagsRemoveEvent ():
419452 final newlyUnreadInStreams = < int , TopicKeyedMap <QueueList <int >>> {};
@@ -431,12 +464,15 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
431464 }
432465 switch (detail.type) {
433466 case MessageType .stream:
434- final topics = (newlyUnreadInStreams[detail.streamId! ] ?? = makeTopicKeyedMap ());
435- final messageIds = (topics[detail.topic! ] ?? = QueueList ());
467+ final UpdateMessageFlagsMessageDetail (: streamId, : topic) = detail;
468+ locatorMap[messageId] = TopicNarrow (streamId! , topic! );
469+ final topics = (newlyUnreadInStreams[streamId] ?? = makeTopicKeyedMap ());
470+ final messageIds = (topics[topic] ?? = QueueList ());
436471 messageIds.add (messageId);
437472 case MessageType .direct:
438473 final narrow = DmNarrow .ofUpdateMessageFlagsMessageDetail (selfUserId: selfUserId,
439474 detail);
475+ locatorMap[messageId] = narrow;
440476 (newlyUnreadInDms[narrow] ?? = QueueList ())
441477 .add (messageId);
442478 }
@@ -489,15 +525,6 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
489525 notifyListeners ();
490526 }
491527
492- // TODO use efficient lookups
493- bool _slowIsPresentInStreams (int messageId) {
494- return streams.values.any (
495- (topics) => topics.values.any (
496- (messageIds) => messageIds.contains (messageId),
497- ),
498- );
499- }
500-
501528 void _addLastInStreamTopic (int messageId, int streamId, TopicName topic) {
502529 ((streams[streamId] ?? = makeTopicKeyedMap ())[topic] ?? = QueueList ())
503530 .addLast (messageId);
@@ -517,26 +544,32 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
517544 );
518545 }
519546
520- // TODO use efficient model lookups
521- void _slowRemoveAllInStreams (Set <int > idsToRemove) {
522- final newlyEmptyStreams = < int > [];
523- for (final MapEntry (key: streamId, value: topics) in streams.entries) {
524- final newlyEmptyTopics = < TopicName > [];
525- for (final MapEntry (key: topic, value: messageIds) in topics.entries) {
526- messageIds.removeWhere ((id) => idsToRemove.contains (id));
527- if (messageIds.isEmpty) {
528- newlyEmptyTopics.add (topic);
529- }
530- }
531- for (final topic in newlyEmptyTopics) {
532- topics.remove (topic);
533- }
534- if (topics.isEmpty) {
535- newlyEmptyStreams.add (streamId);
536- }
547+ /// Remove [idsToRemove] from [streams] and [dms] .
548+ void _removeAllInStreamsAndDms (Iterable <int > idsToRemove, {bool expectOnlyDms = false }) {
549+ final idsPresentByNarrow = < SendableNarrow , Set <int >> {};
550+ for (final id in idsToRemove) {
551+ final narrow = locatorMap[id];
552+ if (narrow == null ) continue ;
553+ (idsPresentByNarrow[narrow] ?? = {}).add (id);
537554 }
538- for (final streamId in newlyEmptyStreams) {
539- streams.remove (streamId);
555+
556+ for (final MapEntry (key: narrow, value: ids) in idsPresentByNarrow.entries) {
557+ switch (narrow) {
558+ case TopicNarrow ():
559+ if (expectOnlyDms) {
560+ // TODO(log)?
561+ }
562+ _removeAllInStreamTopic (ids, narrow.streamId, narrow.topic);
563+ case DmNarrow ():
564+ final messageIds = dms[narrow];
565+ if (messageIds == null ) return ;
566+
567+ // ([QueueList] doesn't have a `removeAll`)
568+ messageIds.removeWhere ((id) => ids.contains (id));
569+ if (messageIds.isEmpty) {
570+ dms.remove (narrow);
571+ }
572+ }
540573 }
541574 }
542575
@@ -599,11 +632,6 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
599632 return poppedMessageIds;
600633 }
601634
602- // TODO use efficient model lookups
603- bool _slowIsPresentInDms (int messageId) {
604- return dms.values.any ((ids) => ids.contains (messageId));
605- }
606-
607635 void _addLastInDm (int messageId, DmNarrow narrow) {
608636 (dms[narrow] ?? = QueueList ()).addLast (messageId);
609637 }
@@ -618,18 +646,4 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
618646 (existing) => setUnion (existing, messageIds),
619647 );
620648 }
621-
622- // TODO use efficient model lookups
623- void _slowRemoveAllInDms (Set <int > idsToRemove) {
624- final newlyEmptyDms = < DmNarrow > [];
625- for (final MapEntry (key: dmNarrow, value: messageIds) in dms.entries) {
626- messageIds.removeWhere ((id) => idsToRemove.contains (id));
627- if (messageIds.isEmpty) {
628- newlyEmptyDms.add (dmNarrow);
629- }
630- }
631- for (final dmNarrow in newlyEmptyDms) {
632- dms.remove (dmNarrow);
633- }
634- }
635649}
0 commit comments