Skip to content

Commit 6f82fda

Browse files
committed
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.
1 parent 9517336 commit 6f82fda

File tree

4 files changed

+114
-66
lines changed

4 files changed

+114
-66
lines changed

lib/model/store.dart

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import 'recent_senders.dart';
3030
import 'channel.dart';
3131
import 'typing_status.dart';
3232
import 'unreads.dart';
33+
import 'user.dart';
3334

3435
export 'package:drift/drift.dart' show Value;
3536
export 'database.dart' show Account, AccountsCompanion, AccountAlreadyExistsException;
@@ -237,7 +238,7 @@ class AccountNotFoundException implements Exception {}
237238
/// This class does not attempt to poll an event queue
238239
/// to keep the data up to date. For that behavior, see
239240
/// [UpdateMachine].
240-
class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, MessageStore {
241+
class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, ChannelStore, MessageStore {
241242
/// Construct a store for the user's data, starting from the given snapshot.
242243
///
243244
/// The global store must already have been updated with
@@ -287,11 +288,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
287288
typingStartedWaitPeriod: Duration(
288289
milliseconds: initialSnapshot.serverTypingStartedWaitPeriodMilliseconds),
289290
),
290-
users: Map.fromEntries(
291-
initialSnapshot.realmUsers
292-
.followedBy(initialSnapshot.realmNonActiveUsers)
293-
.followedBy(initialSnapshot.crossRealmBots)
294-
.map((user) => MapEntry(user.userId, user))),
291+
users: UserStoreImpl(initialSnapshot: initialSnapshot),
295292
typingStatus: TypingStatus(
296293
selfUserId: account.userId,
297294
typingStartedExpiryPeriod: Duration(milliseconds: initialSnapshot.serverTypingStartedExpiryPeriodMilliseconds),
@@ -325,7 +322,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
325322
required this.selfUserId,
326323
required this.userSettings,
327324
required this.typingNotifier,
328-
required this.users,
325+
required UserStoreImpl users,
329326
required this.typingStatus,
330327
required ChannelStoreImpl channels,
331328
required MessageStoreImpl messages,
@@ -338,6 +335,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
338335
assert(emoji.realmUrl == realmUrl),
339336
_globalStore = globalStore,
340337
_emoji = emoji,
338+
_users = users,
341339
_channels = channels,
342340
_messages = messages;
343341

@@ -436,7 +434,10 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
436434
////////////////////////////////
437435
// Users and data about them.
438436

439-
final Map<int, User> users;
437+
@override
438+
Map<int, User> get users => _users.users;
439+
440+
final UserStoreImpl _users;
440441

441442
final TypingStatus typingStatus;
442443

@@ -605,44 +606,18 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
605606

606607
case RealmUserAddEvent():
607608
assert(debugLog("server event: realm_user/add"));
608-
users[event.person.userId] = event.person;
609+
_users.handleRealmUserEvent(event);
609610
notifyListeners();
610611

611612
case RealmUserRemoveEvent():
612613
assert(debugLog("server event: realm_user/remove"));
613-
users.remove(event.userId);
614+
_users.handleRealmUserEvent(event);
614615
autocompleteViewManager.handleRealmUserRemoveEvent(event);
615616
notifyListeners();
616617

617618
case RealmUserUpdateEvent():
618619
assert(debugLog("server event: realm_user/update"));
619-
final user = users[event.userId];
620-
if (user == null) {
621-
return; // TODO log
622-
}
623-
if (event.fullName != null) user.fullName = event.fullName!;
624-
if (event.avatarUrl != null) user.avatarUrl = event.avatarUrl!;
625-
if (event.avatarVersion != null) user.avatarVersion = event.avatarVersion!;
626-
if (event.timezone != null) user.timezone = event.timezone!;
627-
if (event.botOwnerId != null) user.botOwnerId = event.botOwnerId!;
628-
if (event.role != null) user.role = event.role!;
629-
if (event.isBillingAdmin != null) user.isBillingAdmin = event.isBillingAdmin!;
630-
if (event.deliveryEmail != null) user.deliveryEmail = event.deliveryEmail!.value;
631-
if (event.newEmail != null) user.email = event.newEmail!;
632-
if (event.isActive != null) user.isActive = event.isActive!;
633-
if (event.customProfileField != null) {
634-
final profileData = (user.profileData ??= {});
635-
final update = event.customProfileField!;
636-
if (update.value != null) {
637-
profileData[update.id] = ProfileFieldUserData(value: update.value!, renderedValue: update.renderedValue);
638-
} else {
639-
profileData.remove(update.id);
640-
}
641-
if (profileData.isEmpty) {
642-
// null is equivalent to `{}` for efficiency; see [User._readProfileData].
643-
user.profileData = null;
644-
}
645-
}
620+
_users.handleRealmUserEvent(event);
646621
autocompleteViewManager.handleRealmUserUpdateEvent(event);
647622
notifyListeners();
648623

lib/model/user.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import '../api/model/events.dart';
2+
import '../api/model/initial_snapshot.dart';
3+
import '../api/model/model.dart';
4+
5+
/// The portion of [PerAccountStore] describing the users in the realm.
6+
mixin UserStore {
7+
Map<int, User> get users;
8+
}
9+
10+
/// The implementation of [UserStore] that does the work.
11+
///
12+
/// Generally the only code that should need this class is [PerAccountStore]
13+
/// itself. Other code accesses this functionality through [PerAccountStore],
14+
/// or through the mixin [UserStore] which describes its interface.
15+
class UserStoreImpl with UserStore {
16+
UserStoreImpl({required InitialSnapshot initialSnapshot})
17+
: users = Map.fromEntries(
18+
initialSnapshot.realmUsers
19+
.followedBy(initialSnapshot.realmNonActiveUsers)
20+
.followedBy(initialSnapshot.crossRealmBots)
21+
.map((user) => MapEntry(user.userId, user)));
22+
23+
@override
24+
final Map<int, User> users;
25+
26+
void handleRealmUserEvent(RealmUserEvent event) {
27+
switch (event) {
28+
case RealmUserAddEvent():
29+
users[event.person.userId] = event.person;
30+
31+
case RealmUserRemoveEvent():
32+
users.remove(event.userId);
33+
34+
case RealmUserUpdateEvent():
35+
final user = users[event.userId];
36+
if (user == null) {
37+
return; // TODO log
38+
}
39+
if (event.fullName != null) user.fullName = event.fullName!;
40+
if (event.avatarUrl != null) user.avatarUrl = event.avatarUrl!;
41+
if (event.avatarVersion != null) user.avatarVersion = event.avatarVersion!;
42+
if (event.timezone != null) user.timezone = event.timezone!;
43+
if (event.botOwnerId != null) user.botOwnerId = event.botOwnerId!;
44+
if (event.role != null) user.role = event.role!;
45+
if (event.isBillingAdmin != null) user.isBillingAdmin = event.isBillingAdmin!;
46+
if (event.deliveryEmail != null) user.deliveryEmail = event.deliveryEmail!.value;
47+
if (event.newEmail != null) user.email = event.newEmail!;
48+
if (event.isActive != null) user.isActive = event.isActive!;
49+
if (event.customProfileField != null) {
50+
final profileData = (user.profileData ??= {});
51+
final update = event.customProfileField!;
52+
if (update.value != null) {
53+
profileData[update.id] = ProfileFieldUserData(value: update.value!, renderedValue: update.renderedValue);
54+
} else {
55+
profileData.remove(update.id);
56+
}
57+
if (profileData.isEmpty) {
58+
// null is equivalent to `{}` for efficiency; see [User._readProfileData].
59+
user.profileData = null;
60+
}
61+
}
62+
}
63+
}
64+
}

test/model/store_test.dart

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -347,36 +347,8 @@ void main() {
347347

348348
group('PerAccountStore.handleEvent', () {
349349
// Mostly this method just dispatches to ChannelStore and MessageStore etc.,
350-
// and so most of the tests live in the test files for those
350+
// and so its tests generally live in the test files for those
351351
// (but they call the handleEvent method because it's the entry point).
352-
353-
group('RealmUserUpdateEvent', () {
354-
// TODO write more tests for handling RealmUserUpdateEvent
355-
356-
test('deliveryEmail', () {
357-
final user = eg.user(deliveryEmail: '[email protected]');
358-
final store = eg.store(initialSnapshot: eg.initialSnapshot(
359-
realmUsers: [eg.selfUser, user]));
360-
361-
User getUser() => store.users[user.userId]!;
362-
363-
store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
364-
deliveryEmail: null));
365-
check(getUser()).deliveryEmail.equals('[email protected]');
366-
367-
store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
368-
deliveryEmail: const JsonNullable(null)));
369-
check(getUser()).deliveryEmail.isNull();
370-
371-
store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
372-
deliveryEmail: const JsonNullable('[email protected]')));
373-
check(getUser()).deliveryEmail.equals('[email protected]');
374-
375-
store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
376-
deliveryEmail: const JsonNullable('[email protected]')));
377-
check(getUser()).deliveryEmail.equals('[email protected]');
378-
});
379-
});
380352
});
381353

382354
group('PerAccountStore.sendMessage', () {

test/model/user_test.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:zulip/api/model/events.dart';
4+
import 'package:zulip/api/model/model.dart';
5+
6+
import '../api/model/model_checks.dart';
7+
import '../example_data.dart' as eg;
8+
9+
void main() {
10+
group('RealmUserUpdateEvent', () {
11+
// TODO write more tests for handling RealmUserUpdateEvent
12+
13+
test('deliveryEmail', () {
14+
final user = eg.user(deliveryEmail: '[email protected]');
15+
final store = eg.store(initialSnapshot: eg.initialSnapshot(
16+
realmUsers: [eg.selfUser, user]));
17+
18+
User getUser() => store.users[user.userId]!;
19+
20+
store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
21+
deliveryEmail: null));
22+
check(getUser()).deliveryEmail.equals('[email protected]');
23+
24+
store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
25+
deliveryEmail: const JsonNullable(null)));
26+
check(getUser()).deliveryEmail.isNull();
27+
28+
store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
29+
deliveryEmail: const JsonNullable('[email protected]')));
30+
check(getUser()).deliveryEmail.equals('[email protected]');
31+
32+
store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
33+
deliveryEmail: const JsonNullable('[email protected]')));
34+
check(getUser()).deliveryEmail.equals('[email protected]');
35+
});
36+
});
37+
}

0 commit comments

Comments
 (0)