Skip to content

Conversation

@chrisbobbe
Copy link
Collaborator

Fixes: #980

@chrisbobbe chrisbobbe added the maintainer review PR ready for review by Zulip maintainers label Jun 18, 2025
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking care of this! Generally this all looks good; mostly small comments below.

(I just saw you'd marked it for maintainer review first, ah well — already wrote this review, so posting it.)

Comment on lines 165 to -142
eg.streamMessage(id: 7, stream: stream2, topic: 'c', flags: []),
eg.streamMessage(id: 8, stream: stream2, topic: 'c', flags: []),
eg.dmMessage(id: 9, from: user1, to: [eg.selfUser], flags: []),
eg.dmMessage(id: 10, from: user1, to: [eg.selfUser], flags: []),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

unreads test: Make room for two more message IDs (7, 8) in a test's setup

9, 10, right?

Comment on lines 382 to 392
LinkedHashMap<TopicName, V> makeTopicKeyedMap<V>() => LinkedHashMap<TopicName, V>(
equals: (a, b) => a.isSameAs(b),
hashCode: (k) => k.canonicalize().hashCode,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the one way this is less efficient than a FoldDict approach is that when looking up a key and finding a match (or a match of the hash code), the FoldDict only needs to call toLowerCase on the new key because it already has the lower-case form of the key that's in the map; this version needs to call toLowerCase twice.

It's probably fine. If later we find this is hot in profiling, we can optimize.

(And if we do, we might go farther anyway: turn TopicName into a class instead of an extension type, memoizing its canonicalized form. That would make FoldDict unnecessary too.)

LinkedHashMap<TopicName, V> makeTopicKeyedMap<V>() => LinkedHashMap<TopicName, V>(
equals: (a, b) => a.isSameAs(b),
hashCode: (k) => k.canonicalize().hashCode,
isValidKey: (k) => k is TopicName,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this argument needed? It looks from the docs like this is the default behavior.

Comment on lines 19 to 22
final Map<int, Map<TopicName, Map<int, MessageIdTracker>>> topicSenders = {};
final Map<int, LinkedHashMap<TopicName, Map<int, MessageIdTracker>>> topicSenders = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this doesn't really express the meaning we care about. It is a LinkedHashMap, true — but so is what you'd get from just {}. If these were some other (reasonable) Map implementation instead, that'd be fine too as long as they used the particular equals and hashCode etc. that makeTopicKeyedMap passes.

I think just Map here would be fine (and better than saying LinkedHashMap). It could also be a type alias, like TopicKeyedMap<Map<int, …>> — that'd express the desired meaning, even though it wouldn't be enforced.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apropos of this: the upstream tree just converted places that were saying LinkedHashMap to say plain Map:
flutter/flutter#170713
which apparently will be needed as part of a future lint change there.

Comment on lines 5 to 6
extension MessageIdTrackerChecks on Subject<MessageIdTracker> {
Subject<QueueList<int>> get ids => has((x) => x.ids, 'ids');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's put this inside recent_senders_test.dart — for checks-extensions that are unlikely to be used outside a given test file, I've preferred doing it that way

I'm not totally satisfied with that pattern, because sometimes things are used in two or three files, and sometimes they start out used in just one and later are used in another, so that we end up having one test file import another which feels a little unclean.

But the trade-off is that when this file is present, it makes it harder to type a command like flutter test test/model/recent_senders_test.dart — the tab-completion has to stop at the …/recent_senders_ to ask if the next character should be a t or a c 🙂

Comment on lines 138 to 142
check(model.topicSenders).values.single.entries.single
..key.equals(eg.t('thing'))
..value.entries.single.which((it) => it
..key.equals(userX.userId)
..value.isA<MessageIdTracker>().ids.length.equals(2));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This felt a bit verbose to me, so I tried simplifying it by using deepEquals.

Not sure whether this is an improvement — the need for a Condition in there does dampen things:

      check(model.topicSenders).values.single.deepEquals(
        {eg.t('thing'): {userX.userId: (Subject<Object?> it) =>
           it.isA<MessageIdTracker>().ids.length.equals(2)}});

Maybe better if a bit more expanded in formatting:

      check(model.topicSenders).values.single.deepEquals(
        {eg.t('thing'):
          {userX.userId: (Subject<Object?> it) =>
             it.isA<MessageIdTracker>().ids.length.equals(2)}});

required ChannelStore channelStore,
}) {
final streams = <int, Map<TopicName, QueueList<int>>>{};
final streams = <int, LinkedHashMap<TopicName, QueueList<int>>>{};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Searching this file for "Map<TopicName", it looks like there's one more spot that probably needs makeTopicKeyedMap: in handleUpdateMessageFlagsEvent when marking unread.

Comment on lines 53 to 62
if (topics[topic] != null) {
// Older servers differentiate topics case-sensitively, but shouldn't:
// https://github.com/zulip/zulip/pull/31869
// Our topic-keyed map is case-insensitive. When we've seen this
// topic before, modulo case, aggregate instead of clobbering.
// TODO(server-10) simplify away
topics[topic] =
setUnion(topics[topic]!, unreadChannelSnapshot.unreadMessageIds);
} else {
topics[topic] = QueueList.from(unreadChannelSnapshot.unreadMessageIds);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will look up topic in the map two or three times. Since this is in a potentially hot path (processing the unreads data from the initial snapshot, so often 50k message IDs across perhaps 1000s of conversations), let's avoid that: topics.update(topic, …).

Comment on lines -286 to +290
}

Future<void> addUserTopic(ZulipStream stream, String topic, UserTopicVisibilityPolicy visibilityPolicy) async {
Future<void> setUserTopic(ZulipStream stream, String topic, UserTopicVisibilityPolicy visibilityPolicy) async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test [nfc]: s/addUserTopic/setUserTopic/ in PerAccountStoreTestExtension

The event's doc says:

https://zulip.com/api/get-events#user_topic
> Event sent to a user's clients when the user mutes/unmutes a
> topic, or otherwise modifies their personal per-topic
> configuration.

The new name is still accurate for "adding" a configuration (i.e.
setting it to something not-"none"), but now also covers updating a
configuration (moving between not-"none" configurations) and
removing one (resetting to "none).

Ah, good thought.

Comment on lines +166 to +168
// Case-insensitive
check(store.topicVisibilityPolicy(stream1.streamId, eg.t('ToPiC')))
.equals(policy);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my ideal version of these tests would have most of the new checks in this file be separate test cases, rather than added to existing tests. This also doesn't need a full matrix with the other variables: for example, this check can be done with any one of the non-none policies, rather than all three.

These are fine, though. No need to rewrite them — we have higher-priority things to do.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, makes sense.

chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request Jul 7, 2025
Worth noting some helpful feedback from Greg in review, about the
additions to test/model/channel_test.dart:

zulip#1608 (comment)
> I think my ideal version of these tests would have most of the new
> checks in this file be separate test cases, rather than added to
> existing tests. This also doesn't need a full matrix with the
> other variables: for example, this check can be done with any one
> of the non-`none` policies, rather than all three.
>
> These are fine, though. No need to rewrite them — we have
> higher-priority things to do.
@chrisbobbe chrisbobbe force-pushed the pr-topic-case-insensitivity branch from eb15d8b to 8c29baf Compare July 7, 2025 18:06
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed.

@gnprice gnprice added the integration review Added by maintainers when PR may be ready for integration label Jul 8, 2025
… tests

And fix a test that was breaking an invariant in its setup.
…ages

And change some callers slightly, to adapt.

Since fillWithMessages is implemented as handling a sequence of
new-message events, require the messages to be sorted without
duplicates, for realism.

This doesn't change what the model's state looks like for any of the
tests, because messages in the input with the "read" flag are
ignored by model.handleMessageEvent, and we're not changing the
order of the unread messages. Also, the previous commit started
checking the invariant that the model's message-ID lists are sorted.
The event's doc says:

https://zulip.com/api/get-events#user_topic
> Event sent to a user's clients when the user mutes/unmutes a
> topic, or otherwise modifies their personal per-topic
> configuration.

The new name is still accurate for "adding" a configuration (i.e.
setting it to something not-"none"), but now also covers updating a
configuration (moving between not-"none" configurations) and
removing one (resetting to "none).
Worth noting some helpful feedback from Greg in review, about the
additions to test/model/channel_test.dart:

zulip#1608 (comment)
> I think my ideal version of these tests would have most of the new
> checks in this file be separate test cases, rather than added to
> existing tests. This also doesn't need a full matrix with the
> other variables: for example, this check can be done with any one
> of the non-`none` policies, rather than all three.
>
> These are fine, though. No need to rewrite them — we have
> higher-priority things to do.
@gnprice gnprice force-pushed the pr-topic-case-insensitivity branch from 8c29baf to a81e43a Compare July 8, 2025 19:03
@gnprice
Copy link
Member

gnprice commented Jul 8, 2025

Thanks! Looks good; merging.

@gnprice gnprice merged commit a81e43a into zulip:main Jul 8, 2025
gnprice added a commit to gnprice/zulip-flutter that referenced this pull request Jul 22, 2025
…nsions

I've long had a recurring small annoyance that the "foo_checks.dart"
file interferes with tab-completion for "foo_test.dart".  For example
as mentioned in this comment:
  zulip#1608 (comment)

So here's an approach to solve that, beginning with the "checks" files
in test/widgets/.

For updating the imports, I used a bit of Perl:

  $ perl -i -0pe '
        s,import .\K(?!dialog)\w*_checks.dart,checks.dart,g
      ' test/widgets/*_test.dart
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration review Added by maintainers when PR may be ready for integration maintainer review PR ready for review by Zulip maintainers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Case-insensitive topics in unreads data

3 participants