Skip to content

Commit f3988b0

Browse files
committed
unreads: Add/use locatorMap, to efficiently locate messages
Fixes #332. Really the same optimization as described for zulip-mobile in zulip/zulip-mobile#4684 , and makes mark-as-read take 0ms (to the nearest ms) down from 3 or 4, in my testing, and so fixes #332.
1 parent 02a2898 commit f3988b0

File tree

2 files changed

+76
-50
lines changed

2 files changed

+76
-50
lines changed

lib/model/unreads.dart

Lines changed: 73 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
4141
required CorePerAccountStore core,
4242
required ChannelStore channelStore,
4343
}) {
44+
final locatorMap = <int, Narrow>{};
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,11 @@ 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+
final Map<int, Narrow> locatorMap;
112+
95113
// TODO excluded for now; would need to handle nuances around muting etc.
96114
// int count;
97115

@@ -233,11 +251,8 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
233251
/// The unread state for [messageId], or null if unknown.
234252
///
235253
/// May be unknown only if [oldUnreadsMissing].
236-
///
237-
/// This is inefficient; it iterates through [dms] and [channels].
238-
// TODO implement efficiently
239254
bool? isUnread(int messageId) {
240-
final isPresent = _slowIsPresentInDms(messageId) || _slowIsPresentInStreams(messageId);
255+
final isPresent = locatorMap.containsKey(messageId);
241256
if (oldUnreadsMissing && !isPresent) return null;
242257
return isPresent;
243258
}
@@ -248,6 +263,12 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
248263
return;
249264
}
250265

266+
final narrow = switch (message) {
267+
StreamMessage() => TopicNarrow.ofMessage(message),
268+
DmMessage() => DmNarrow.ofMessage(message, selfUserId: selfUserId),
269+
};
270+
locatorMap[event.message.id] = narrow;
271+
251272
switch (message) {
252273
case StreamMessage():
253274
_addLastInStreamTopic(message.id, message.streamId, message.topic);
@@ -346,9 +367,16 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
346367
// Unreads moved to an unsubscribed channel; just drop them.
347368
// See also:
348369
// https://chat.zulip.org/#narrow/channel/378-api-design/topic/mark-as-read.20events.20with.20message.20moves.3F/near/2101926
370+
for (final messageId in messageToMoveIds) {
371+
locatorMap.remove(messageId);
372+
}
349373
return true;
350374
}
351375

376+
final narrow = TopicNarrow(newStreamId, newTopic);
377+
for (final messageId in messageToMoveIds) {
378+
locatorMap[messageId] = narrow;
379+
}
352380
_addAllInStreamTopic(messageToMoveIds, newStreamId, newTopic);
353381

354382
return true;
@@ -363,7 +391,10 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
363391
final topic = event.topic!;
364392
_removeAllInStreamTopic(messageIdsSet, streamId, topic);
365393
case MessageType.direct:
366-
_slowRemoveAllInDms(messageIdsSet);
394+
_removeAllInDms(messageIdsSet);
395+
}
396+
for (final messageId in event.messageIds) {
397+
locatorMap.remove(messageId);
367398
}
368399

369400
// TODO skip notifyListeners if unchanged?
@@ -405,15 +436,19 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
405436
switch (event) {
406437
case UpdateMessageFlagsAddEvent():
407438
if (event.all) {
439+
locatorMap.clear();
408440
streams.clear();
409441
dms.clear();
410442
mentions.clear();
411443
oldUnreadsMissing = false;
412444
} else {
413445
final messageIdsSet = Set.of(event.messages);
414446
mentions.removeAll(messageIdsSet);
415-
_slowRemoveAllInStreams(messageIdsSet);
416-
_slowRemoveAllInDms(messageIdsSet);
447+
_removeAllInStreams(messageIdsSet);
448+
_removeAllInDms(messageIdsSet);
449+
for (final messageId in event.messages) {
450+
locatorMap.remove(messageId);
451+
}
417452
}
418453
case UpdateMessageFlagsRemoveEvent():
419454
final newlyUnreadInStreams = <int, TopicKeyedMap<QueueList<int>>>{};
@@ -431,12 +466,15 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
431466
}
432467
switch (detail.type) {
433468
case MessageType.stream:
434-
final topics = (newlyUnreadInStreams[detail.streamId!] ??= makeTopicKeyedMap());
435-
final messageIds = (topics[detail.topic!] ??= QueueList());
469+
final UpdateMessageFlagsMessageDetail(:streamId, :topic) = detail;
470+
locatorMap[messageId] = TopicNarrow(streamId!, topic!);
471+
final topics = (newlyUnreadInStreams[streamId] ??= makeTopicKeyedMap());
472+
final messageIds = (topics[topic] ??= QueueList());
436473
messageIds.add(messageId);
437474
case MessageType.direct:
438475
final narrow = DmNarrow.ofUpdateMessageFlagsMessageDetail(selfUserId: selfUserId,
439476
detail);
477+
locatorMap[messageId] = narrow;
440478
(newlyUnreadInDms[narrow] ??= QueueList())
441479
.add(messageId);
442480
}
@@ -489,15 +527,6 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
489527
notifyListeners();
490528
}
491529

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-
501530
void _addLastInStreamTopic(int messageId, int streamId, TopicName topic) {
502531
((streams[streamId] ??= makeTopicKeyedMap())[topic] ??= QueueList())
503532
.addLast(messageId);
@@ -517,26 +546,23 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
517546
);
518547
}
519548

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-
}
549+
/// Remove any of [idsToRemove] that are in [streams].
550+
void _removeAllInStreams(Set<int> idsToRemove) {
551+
for (final messageId in idsToRemove) {
552+
final narrow = locatorMap[messageId];
553+
if (narrow == null) continue;
554+
if (narrow is! TopicNarrow) continue;
555+
556+
final messageIds = streams[narrow.streamId]?[narrow.topic];
557+
if (messageIds == null) continue;
558+
559+
messageIds.remove(messageId);
560+
if (messageIds.isEmpty) {
561+
streams[narrow.streamId]!.remove(narrow.topic);
530562
}
531-
for (final topic in newlyEmptyTopics) {
532-
topics.remove(topic);
563+
if (streams[narrow.streamId]!.isEmpty) {
564+
streams.remove(narrow.streamId);
533565
}
534-
if (topics.isEmpty) {
535-
newlyEmptyStreams.add(streamId);
536-
}
537-
}
538-
for (final streamId in newlyEmptyStreams) {
539-
streams.remove(streamId);
540566
}
541567
}
542568

@@ -599,11 +625,6 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
599625
return poppedMessageIds;
600626
}
601627

602-
// TODO use efficient model lookups
603-
bool _slowIsPresentInDms(int messageId) {
604-
return dms.values.any((ids) => ids.contains(messageId));
605-
}
606-
607628
void _addLastInDm(int messageId, DmNarrow narrow) {
608629
(dms[narrow] ??= QueueList()).addLast(messageId);
609630
}
@@ -619,17 +640,19 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
619640
);
620641
}
621642

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));
643+
/// Remove any of [idsToRemove] that are in [dms].
644+
void _removeAllInDms(Set<int> idsToRemove) {
645+
for (final messageId in idsToRemove) {
646+
final narrow = locatorMap[messageId];
647+
if (narrow == null) continue;
648+
if (narrow is! DmNarrow) continue;
649+
650+
final messageIds = dms[narrow];
651+
if (messageIds == null) continue;
652+
messageIds.remove(messageId);
627653
if (messageIds.isEmpty) {
628-
newlyEmptyDms.add(dmNarrow);
654+
dms.remove(narrow);
629655
}
630656
}
631-
for (final dmNarrow in newlyEmptyDms) {
632-
dms.remove(dmNarrow);
633-
}
634657
}
635658
}

test/model/unreads_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,17 @@ void main() {
8383
final Set<int> expectedMentions = {};
8484
for (final message in messages) {
8585
if (message.flags.contains(MessageFlag.read)) {
86+
check(model.locatorMap).not((it) => it.containsKey(message.id));
8687
continue;
8788
}
8889
switch (message) {
8990
case StreamMessage():
91+
check(model.locatorMap)[message.id].equals(TopicNarrow.ofMessage(message));
9092
final perTopic = expectedStreams[message.streamId] ??= makeTopicKeyedMap();
9193
final messageIds = perTopic[message.topic] ??= QueueList();
9294
messageIds.add(message.id);
9395
case DmMessage():
96+
check(model.locatorMap)[message.id].equals(DmNarrow.ofMessage(message, selfUserId: store.selfUserId));
9497
final narrow = DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId);
9598
final messageIds = expectedDms[narrow] ??= QueueList();
9699
messageIds.add(message.id);

0 commit comments

Comments
 (0)