- 
                Notifications
    You must be signed in to change notification settings 
- Fork 348
Add UserStore; pull out getUser, userDisplayName, others #1327
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
          
     Merged
      
      
    
      
        
          +321
        
        
          −156
        
        
          
        
      
    
  
  
     Merged
                    Changes from all commits
      Commits
    
    
            Show all changes
          
          
            19 commits
          
        
        Select commit
          Hold shift + click to select a range
      
      fbee491
              
                test [nfc]: Add the few missing awaits on handleEvent calls
              
              
                gnprice d5caa78
              
                user: Split a UserStore out from PerAccountStore
              
              
                gnprice af10aea
              
                user [nfc]: Refer to "user store" rather than "store.users" in text
              
              
                gnprice 7168c48
              
                user [nfc]: Document users map, especially its incompleteness
              
              
                gnprice a7ecf5b
              
                user [nfc]: Factor out a userDisplayName
              
              
                gnprice cffe2e4
              
                inbox [nfc]: Inline and unhoist a self-user lookup
              
              
                gnprice c077a45
              
                recent dms [nfc]: Unhoist a self-user lookup
              
              
                gnprice 581e377
              
                compose [nfc]: Unhoist a self-user lookup
              
              
                gnprice ea46825
              
                store [nfc]: Add a zulipFeatureLevel getter
              
              
                gnprice 1fd0c3f
              
                user [nfc]: Move selfUserId to UserStore
              
              
                gnprice 626d473
              
                user [nfc]: Add a selfUser getter
              
              
                gnprice de98200
              
                compose [nfc]: Take UserStore in userMention, rather than bare Map
              
              
                gnprice 8b172ba
              
                user [nfc]: Introduce senderDisplayName
              
              
                gnprice 4adf1fc
              
                user [nfc]: Note unknown-user crashes where senderDisplayName can help
              
              
                gnprice b61f135
              
                user [nfc]: Note places lacking live-update where senderDisplayName h…
              
              
                gnprice 6bbe209
              
                autocomplete [nfc]: Make explicit why two user lookups have null-asse…
              
              
                gnprice 268a462
              
                user [nfc]: Factor out a getUser method
              
              
                gnprice dadf9de
              
                user [nfc]: Factor out an allUsers iterable
              
              
                gnprice 30c64a0
              
                user [nfc]: Make the actual users Map private
              
              
                gnprice File filter
Filter by extension
Conversations
          Failed to load comments.   
        
        
          
      Loading
        
  Jump to
        
          Jump to file
        
      
      
          Failed to load files.   
        
        
          
      Loading
        
  Diff view
Diff view
There are no files selected for viewing
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -31,6 +31,7 @@ import 'recent_senders.dart'; | |
| import 'channel.dart'; | ||
| import 'typing_status.dart'; | ||
| import 'unreads.dart'; | ||
| import 'user.dart'; | ||
|  | ||
| export 'package:drift/drift.dart' show Value; | ||
| export 'database.dart' show Account, AccountsCompanion, AccountAlreadyExistsException; | ||
|  | @@ -266,7 +267,7 @@ class AccountNotFoundException implements Exception {} | |
| /// This class does not attempt to poll an event queue | ||
| /// to keep the data up to date. For that behavior, see | ||
| /// [UpdateMachine]. | ||
| class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, MessageStore { | ||
| class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, ChannelStore, MessageStore { | ||
| /// Construct a store for the user's data, starting from the given snapshot. | ||
| /// | ||
| /// The global store must already have been updated with | ||
|  | @@ -307,7 +308,6 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess | |
| emoji: EmojiStoreImpl( | ||
| realmUrl: realmUrl, allRealmEmoji: initialSnapshot.realmEmoji), | ||
| accountId: accountId, | ||
| selfUserId: account.userId, | ||
| userSettings: initialSnapshot.userSettings, | ||
| typingNotifier: TypingNotifier( | ||
| connection: connection, | ||
|  | @@ -316,11 +316,9 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess | |
| typingStartedWaitPeriod: Duration( | ||
| milliseconds: initialSnapshot.serverTypingStartedWaitPeriodMilliseconds), | ||
| ), | ||
| users: Map.fromEntries( | ||
| initialSnapshot.realmUsers | ||
| .followedBy(initialSnapshot.realmNonActiveUsers) | ||
| .followedBy(initialSnapshot.crossRealmBots) | ||
| .map((user) => MapEntry(user.userId, user))), | ||
| users: UserStoreImpl( | ||
| selfUserId: account.userId, | ||
| initialSnapshot: initialSnapshot), | ||
| typingStatus: TypingStatus( | ||
| selfUserId: account.userId, | ||
| typingStartedExpiryPeriod: Duration(milliseconds: initialSnapshot.serverTypingStartedExpiryPeriodMilliseconds), | ||
|  | @@ -351,22 +349,21 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess | |
| required this.emailAddressVisibility, | ||
| required EmojiStoreImpl emoji, | ||
| required this.accountId, | ||
| required this.selfUserId, | ||
| required this.userSettings, | ||
| required this.typingNotifier, | ||
| required this.users, | ||
| required UserStoreImpl users, | ||
| required this.typingStatus, | ||
| required ChannelStoreImpl channels, | ||
| required MessageStoreImpl messages, | ||
| required this.unreads, | ||
| required this.recentDmConversationsView, | ||
| required this.recentSenders, | ||
| }) : assert(selfUserId == globalStore.getAccount(accountId)!.userId), | ||
| assert(realmUrl == globalStore.getAccount(accountId)!.realmUrl), | ||
| }) : assert(realmUrl == globalStore.getAccount(accountId)!.realmUrl), | ||
| assert(realmUrl == connection.realmUrl), | ||
| assert(emoji.realmUrl == realmUrl), | ||
| _globalStore = globalStore, | ||
| _emoji = emoji, | ||
| _users = users, | ||
| _channels = channels, | ||
| _messages = messages; | ||
|  | ||
|  | @@ -407,6 +404,10 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess | |
| /// 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!; | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Really convenient! | ||
|  | ||
| String get zulipVersion => account.zulipVersion; | ||
| final RealmWildcardMentionPolicy realmWildcardMentionPolicy; // TODO(#668): update this realm setting | ||
| final bool realmMandatoryTopics; // TODO(#668): update this realm setting | ||
|  | @@ -455,17 +456,23 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess | |
| /// Will throw if called after [dispose] has been called. | ||
| Account get account => _globalStore.getAccount(accountId)!; | ||
|  | ||
| /// Always equal to `account.userId`. | ||
| final int selfUserId; | ||
|  | ||
| final UserSettings? userSettings; // TODO(server-5) | ||
|  | ||
| final TypingNotifier typingNotifier; | ||
|  | ||
| //////////////////////////////// | ||
| // Users and data about them. | ||
|  | ||
| final Map<int, User> users; | ||
| @override | ||
| int get selfUserId => _users.selfUserId; | ||
|  | ||
| @override | ||
| User? getUser(int userId) => _users.getUser(userId); | ||
|  | ||
| @override | ||
| Iterable<User> get allUsers => _users.allUsers; | ||
|  | ||
| final UserStoreImpl _users; | ||
|  | ||
| final TypingStatus typingStatus; | ||
|  | ||
|  | @@ -634,44 +641,18 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess | |
|  | ||
| case RealmUserAddEvent(): | ||
| assert(debugLog("server event: realm_user/add")); | ||
| users[event.person.userId] = event.person; | ||
| _users.handleRealmUserEvent(event); | ||
| notifyListeners(); | ||
|  | ||
| case RealmUserRemoveEvent(): | ||
| assert(debugLog("server event: realm_user/remove")); | ||
| users.remove(event.userId); | ||
| _users.handleRealmUserEvent(event); | ||
| autocompleteViewManager.handleRealmUserRemoveEvent(event); | ||
| notifyListeners(); | ||
|  | ||
| case RealmUserUpdateEvent(): | ||
| assert(debugLog("server event: realm_user/update")); | ||
| final user = users[event.userId]; | ||
| if (user == null) { | ||
| return; // TODO log | ||
| } | ||
| if (event.fullName != null) user.fullName = event.fullName!; | ||
| if (event.avatarUrl != null) user.avatarUrl = event.avatarUrl!; | ||
| if (event.avatarVersion != null) user.avatarVersion = event.avatarVersion!; | ||
| if (event.timezone != null) user.timezone = event.timezone!; | ||
| if (event.botOwnerId != null) user.botOwnerId = event.botOwnerId!; | ||
| if (event.role != null) user.role = event.role!; | ||
| if (event.isBillingAdmin != null) user.isBillingAdmin = event.isBillingAdmin!; | ||
| if (event.deliveryEmail != null) user.deliveryEmail = event.deliveryEmail!.value; | ||
| if (event.newEmail != null) user.email = event.newEmail!; | ||
| if (event.isActive != null) user.isActive = event.isActive!; | ||
| if (event.customProfileField != null) { | ||
| final profileData = (user.profileData ??= {}); | ||
| final update = event.customProfileField!; | ||
| if (update.value != null) { | ||
| profileData[update.id] = ProfileFieldUserData(value: update.value!, renderedValue: update.renderedValue); | ||
| } else { | ||
| profileData.remove(update.id); | ||
| } | ||
| if (profileData.isEmpty) { | ||
| // null is equivalent to `{}` for efficiency; see [User._readProfileData]. | ||
| user.profileData = null; | ||
| } | ||
| } | ||
| _users.handleRealmUserEvent(event); | ||
| autocompleteViewManager.handleRealmUserUpdateEvent(event); | ||
| notifyListeners(); | ||
|  | ||
|  | ||
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| import '../api/model/events.dart'; | ||
| import '../api/model/initial_snapshot.dart'; | ||
| import '../api/model/model.dart'; | ||
| import 'localizations.dart'; | ||
|  | ||
| /// The portion of [PerAccountStore] describing the users in the realm. | ||
| mixin UserStore { | ||
| /// The user ID of the "self-user", | ||
| /// i.e. the account the person using this app is logged into. | ||
| /// | ||
| /// This always equals the [Account.userId] on [PerAccountStore.account]. | ||
| /// | ||
| /// For the corresponding [User] object, see [selfUser]. | ||
| int get selfUserId; | ||
|  | ||
| /// The user with the given ID, if that user is known. | ||
| /// | ||
| /// There may be other users that are perfectly real but are | ||
| /// not known to the app, for multiple reasons: | ||
| /// | ||
| /// * The self-user may not have permission to see all the users in the | ||
| /// realm, for example because the self-user is a guest. | ||
| /// | ||
| /// * Because of the fetch/event race, any data that the client fetched | ||
| /// outside of the event system may reflect an earlier or later time | ||
| /// than this data, which is maintained by the event system. | ||
| /// This includes messages fetched for a message list, and notifications. | ||
| /// Those may therefore refer to users for which we have yet to see the | ||
| /// [RealmUserAddEvent], or have already handled a [RealmUserRemoveEvent]. | ||
| /// | ||
| /// Code that looks up a user here should therefore always handle | ||
| /// the possibility that the user is not found (except | ||
| /// where there is a specific reason to know the user should be found). | ||
| /// Consider using [userDisplayName]. | ||
| User? getUser(int userId); | ||
|  | ||
| /// All known users in the realm. | ||
| /// | ||
| /// This may have a large number of elements, like tens of thousands. | ||
| /// Consider [getUser] or other alternatives to iterating through this. | ||
| /// | ||
| /// There may be perfectly real users which are not known | ||
| /// and so are not found here. For details, see [getUser]. | ||
| Iterable<User> get allUsers; | ||
|  | ||
| /// The [User] object for the "self-user", | ||
| /// i.e. the account the person using this app is logged into. | ||
| /// | ||
| /// When only the user ID is needed, see [selfUserId]. | ||
| User get selfUser => getUser(selfUserId)!; | ||
|  | ||
| /// The name to show the given user as in the UI, even for unknown users. | ||
| /// | ||
| /// This is the user's [User.fullName] if the user is known, | ||
| /// and otherwise a translation of "(unknown user)". | ||
| /// | ||
| /// When a [Message] is available which the user sent, | ||
| /// use [senderDisplayName] instead for a better-informed fallback. | ||
| String userDisplayName(int userId) { | ||
| return getUser(userId)?.fullName | ||
| ?? GlobalLocalizations.zulipLocalizations.unknownUserName; | ||
| } | ||
|  | ||
| /// The name to show for the given message's sender in the UI. | ||
| /// | ||
| /// If the user is known (see [getUser]), this is their current [User.fullName]. | ||
| /// If unknown, this uses the fallback value conveniently provided on the | ||
| /// [Message] object itself, namely [Message.senderFullName]. | ||
| /// | ||
| /// For a user who isn't the sender of some known message, | ||
| /// see [userDisplayName]. | ||
| String senderDisplayName(Message message) { | ||
| return getUser(message.senderId)?.fullName | ||
| ?? message.senderFullName; | ||
| } | ||
| } | ||
|  | ||
| /// The implementation of [UserStore] that does the work. | ||
| /// | ||
| /// Generally the only code that should need this class is [PerAccountStore] | ||
| /// itself. Other code accesses this functionality through [PerAccountStore], | ||
| /// or through the mixin [UserStore] which describes its interface. | ||
| class UserStoreImpl with UserStore { | ||
| UserStoreImpl({ | ||
| required this.selfUserId, | ||
| required InitialSnapshot initialSnapshot, | ||
| }) : _users = Map.fromEntries( | ||
| initialSnapshot.realmUsers | ||
| .followedBy(initialSnapshot.realmNonActiveUsers) | ||
| .followedBy(initialSnapshot.crossRealmBots) | ||
| .map((user) => MapEntry(user.userId, user))); | ||
|  | ||
| @override | ||
| final int selfUserId; | ||
|  | ||
| final Map<int, User> _users; | ||
|  | ||
| @override | ||
| User? getUser(int userId) => _users[userId]; | ||
|  | ||
| @override | ||
| Iterable<User> get allUsers => _users.values; | ||
|  | ||
| void handleRealmUserEvent(RealmUserEvent event) { | ||
| switch (event) { | ||
| case RealmUserAddEvent(): | ||
| _users[event.person.userId] = event.person; | ||
|  | ||
| case RealmUserRemoveEvent(): | ||
| _users.remove(event.userId); | ||
|  | ||
| case RealmUserUpdateEvent(): | ||
| final user = _users[event.userId]; | ||
| if (user == null) { | ||
| return; // TODO log | ||
| } | ||
| if (event.fullName != null) user.fullName = event.fullName!; | ||
| if (event.avatarUrl != null) user.avatarUrl = event.avatarUrl!; | ||
| if (event.avatarVersion != null) user.avatarVersion = event.avatarVersion!; | ||
| if (event.timezone != null) user.timezone = event.timezone!; | ||
| if (event.botOwnerId != null) user.botOwnerId = event.botOwnerId!; | ||
| if (event.role != null) user.role = event.role!; | ||
| if (event.isBillingAdmin != null) user.isBillingAdmin = event.isBillingAdmin!; | ||
| if (event.deliveryEmail != null) user.deliveryEmail = event.deliveryEmail!.value; | ||
| if (event.newEmail != null) user.email = event.newEmail!; | ||
| if (event.isActive != null) user.isActive = event.isActive!; | ||
| if (event.customProfileField != null) { | ||
| final profileData = (user.profileData ??= {}); | ||
| final update = event.customProfileField!; | ||
| if (update.value != null) { | ||
| profileData[update.id] = ProfileFieldUserData(value: update.value!, renderedValue: update.renderedValue); | ||
| } else { | ||
| profileData.remove(update.id); | ||
| } | ||
| if (profileData.isEmpty) { | ||
| // null is equivalent to `{}` for efficiency; see [User._readProfileData]. | ||
| user.profileData = null; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | 
      
      Oops, something went wrong.
        
    
  
  Add this suggestion to a batch that can be applied as a single commit.
  This suggestion is invalid because no changes were made to the code.
  Suggestions cannot be applied while the pull request is closed.
  Suggestions cannot be applied while viewing a subset of changes.
  Only one suggestion per line can be applied in a batch.
  Add this suggestion to a batch that can be applied as a single commit.
  Applying suggestions on deleted lines is not supported.
  You must change the existing code in this line in order to create a valid suggestion.
  Outdated suggestions cannot be applied.
  This suggestion has been applied or marked resolved.
  Suggestions cannot be applied from pending reviews.
  Suggestions cannot be applied on multi-line comments.
  Suggestions cannot be applied while the pull request is queued to merge.
  Suggestion cannot be applied right now. Please check back later.
  
    
  
    
There was a problem hiding this comment.
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, andRecentDmConversationsViewcould all take differentselfUserIds without detecting the discrepancy.In contrast, the three surviving assertions check that
realmUrlis consistent across the data stores. While we can do that forselfUserIdtoo, I'm not sure if that's useful. A bug can slip through if we add a newrealmUrlwithout adding a new assertion below, and the same issue applies toselfUserIdassertions if we are to maintain them.Perhaps a better way would be sharing the reference to
realmUrlandselfUserId, instead of passing them multiple times, but the wayPerAccountStore.fromInitialSnapshotis set up already makes it easy to check for mistakes, making this less of an issue.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
connectionand/oraccount(which subsumes them), and then these other sub-stores can sayon BasePerAccountStoreto get access to those getters.(Naturally that's for a future PR, though.)