Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UserStore; pull out getUser, userDisplayName, others #1327

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

gnprice
Copy link
Member

@gnprice gnprice commented Feb 5, 2025

I've been saying for a while that I plan to make a UserStore type akin to ChannelStore and MessageStore. Today merging #1148 and in particular 76425ed gave me a fresh reminder that such a thing would be useful. And I decided some nice refactoring would be a relaxing task, so I sat down and did it. 🙂

The immediate motivating improvement is the new userDisplayName method, which simplifies a lot of spots that were otherwise falling back explicitly to the "(unknown user)" translation.

There's also a bunch of new dartdoc, particularly on the subject of unknown users (cf #716), which there effectively wasn't room for when it would have had to live in the middle of the PerAccountStore definition. And at the end of the branch, the actual Map of users becomes private — the bulk of the former references to it now use getUser, as they're looking up a particular user by ID, while two others stand out (as they should) because they iterate through the new allUsers, an expensive operation.

Along the way, and basically as a side effect of looking at all the code touched in this PR, the branch also includes identifying some spots that need to change for #716 (handling unknown users), plus commenting (in the "autocomplete [nfc]: Make explicit …" commit below) on a couple of spots that look like they might need to change for that issue but in fact don't.

These changes are all NFC with the small exception noted in the first commit.

Selected commit messages

6f82fda user: Split a UserStore out from PerAccountStore

Like ChannelStore and others, this helps reduce the amount of
complexity that's concentrated centrally in PerAccountStore.

This change is all NFC except that if we get a RealmUserUpdateEvent
for an unknown user, we'll now call notifyListeners, and so might
cause some widgets to rebuild, when previously we wouldn't.
That's pretty low-stakes, so I'm not bothering to wire through
the data flow to avoid it.

c253f93 user [nfc]: Document users map, especially its incompleteness

c448083 user [nfc]: Factor out a userDisplayName

06be949 store [nfc]: Add a zulipFeatureLevel getter

This is more convenient than going through store.connection,
and should reduce the temptation to go through store.account.

2063975 user [nfc]: Add a selfUser getter

196966d user [nfc]: Introduce senderDisplayName

7103868 user [nfc]: Note unknown-user crashes where senderDisplayName can help

ff9c6a9 user [nfc]: Note places lacking live-update where senderDisplayName helps

f62379a autocomplete [nfc]: Make explicit why two user lookups have null-assertions

40c06f2 user [nfc]: Factor out a getUser method

3655582 user [nfc]: Factor out an allUsers iterable

4b42269 user [nfc]: Make the actual users Map private

This gives somewhat better encapsulation -- other code isn't expected
to add or remove items from this map.

@gnprice gnprice added the maintainer review PR ready for review by Zulip maintainers label Feb 5, 2025
@gnprice
Copy link
Member Author

gnprice commented Feb 5, 2025

A series of follow-up changes that are enabled by this, but that I didn't get to before deciding I was done with this for the evening:

  • Move hasPassedWaitingPeriod to the UserStore mixin.
  • As a prerequisite for that, make a RealmSettingStore mixin along the same lines. (That way we can say mixin UserStore on RealmSettingStore so that a method defined on UserStore can access realmWaitingPeriodThreshold.)
  • Then move hasPostingPermission to ChannelStore, after writing mixin ChannelStore on UserStore so that it can access hasPassedWaitingPeriod.

That would be a nice further improvement because those two methods are two of the very few remaining bits of specific model logic remaining on PerAccountStore. Moving them out from there would therefore get PerAccountStore very close to its pure desired form as something that just stitches together and orchestrates all the different parts of the Zulip data model, delegating all the details to other more-specialized classes defined in other files.

Copy link
Member

@PIG208 PIG208 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 the PR! Left some comments.

@@ -376,6 +376,10 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
/// This returns null if [reference] fails to parse as a URL.
Uri? tryResolveUrl(String reference) => _tryResolveUrl(realmUrl, reference);

/// Always equal to `connection.zulipFeatureLevel`
/// and `account.zulipFeatureLevel`.
int get zulipFeatureLevel => connection.zulipFeatureLevel!;
Copy link
Member

Choose a reason for hiding this comment

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

Really convenient!

@@ -329,8 +329,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
required this.unreads,
required this.recentDmConversationsView,
required this.recentSenders,
}) : assert(selfUserId == globalStore.getAccount(accountId)!.userId),
Copy link
Member

Choose a reason for hiding this comment

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

I can see that this assertion was already ineffective — data stores like TypingStatus, Unreads, and RecentDmConversationsView could all take different selfUserIds without detecting the discrepancy.

In contrast, the three surviving assertions check that realmUrl is consistent across the data stores. While we can do that for selfUserId too, I'm not sure if that's useful. A bug can slip through if we add a new realmUrl without adding a new assertion below, and the same issue applies to selfUserId assertions if we are to maintain them.

Perhaps a better way would be sharing the reference to realmUrl and selfUserId, instead of passing them multiple times, but the way PerAccountStore.fromInitialSnapshot is set up already makes it easy to check for mistakes, making this less of an issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, this check is basically already internal to the implementation of the PerAccountStore constructors, so I think it's not essential to keep.

Copy link
Member Author

Choose a reason for hiding this comment

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

Perhaps a better way would be sharing the reference to realmUrl and selfUserId, instead of passing them multiple times,

This is also something I think we'll want to do — what I'm imagining is making a type named perhaps BasePerAccountStore, with those and perhaps connection and/or account (which subsumes them), and then these other sub-stores can say on BasePerAccountStore to get access to those getters.

(Naturally that's for a future PR, though.)

@@ -21,6 +21,36 @@ void main() {
});
});

group('senderDisplayName', () {
test('on a known user', () {
Copy link
Member

Choose a reason for hiding this comment

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

Need async here. Expressions like store.addUser(…) need to be awaited.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, yeah.

That doesn't have any actual effect on behavior, because handleEvent doesn't have any awaits in it. But it will in the future, for the reasons described in baea6d7 which made that method async, so in principle these should get awaited.

Looks like there are a small number of other places in the tests that lack such awaits. I'll fix those too while at it.

@@ -438,7 +438,10 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
int get selfUserId => _users.selfUserId;

@override
Map<int, User> get users => _users.users;
Copy link
Member

Choose a reason for hiding this comment

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

There are some outstanding references to store.users the Map.

$ git grep 'store\.users'
test/model/autocomplete_test.dart:  test('MentionAutocompleteView mutating store.users while in progress does not '
test/widgets/recent_dm_conversations_test.dart:        testWidgets('no error when user somehow missing from store.users', (tester) async {
test/widgets/recent_dm_conversations_test.dart:        testWidgets('no error when one user somehow missing from store.users', (tester) async {

This is NFC because there isn't currently actually anything to wait
for; the implementation of handleEvent is entirely synchronous.
But in principle it's async, because in the future it will be, for
the reasons described in baea6d7.

Thanks to Zixuan for spotting this pattern in some newly-added tests
(coming up):
  zulip#1327 (comment)
Like ChannelStore and others, this helps reduce the amount of
complexity that's concentrated centrally in PerAccountStore.

This change is all NFC except that if we get a RealmUserUpdateEvent
for an unknown user, we'll now call notifyListeners, and so might
cause some widgets to rebuild, when previously we wouldn't.
That's pretty low-stakes, so I'm not bothering to wire through
the data flow to avoid it.
At the end of this series, there won't be a `users` member on the
store, so this code-like reference will no longer make sense.
The underlying meaning in each of these contexts will still be apt,
though: the data structure where the known users are tracked.  So
update them to make that reference more generically.
This way we don't actually do the lookup unless we need it.
Also add some blank lines to make the structure of this switch
a bit easier to see.
This is more convenient than going through store.connection,
and should reduce the temptation to go through store.account.
Now that we have a UserStore type, we can give this a bit more
abstraction.
This gives somewhat better encapsulation -- other code isn't expected
to add or remove items from this map.
@gnprice
Copy link
Member Author

gnprice commented Feb 15, 2025

Thanks for the review! Pushed an update (including rebasing atop #1331 so that CI should work); PTAL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
maintainer review PR ready for review by Zulip maintainers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants