Skip to content

Commit d5caa78

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 fbee491 commit d5caa78

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
@@ -31,6 +31,7 @@ import 'recent_senders.dart';
3131
import 'channel.dart';
3232
import 'typing_status.dart';
3333
import 'unreads.dart';
34+
import 'user.dart';
3435

3536
export 'package:drift/drift.dart' show Value;
3637
export 'database.dart' show Account, AccountsCompanion, AccountAlreadyExistsException;
@@ -266,7 +267,7 @@ class AccountNotFoundException implements Exception {}
266267
/// This class does not attempt to poll an event queue
267268
/// to keep the data up to date. For that behavior, see
268269
/// [UpdateMachine].
269-
class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, MessageStore {
270+
class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, ChannelStore, MessageStore {
270271
/// Construct a store for the user's data, starting from the given snapshot.
271272
///
272273
/// The global store must already have been updated with
@@ -316,11 +317,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
316317
typingStartedWaitPeriod: Duration(
317318
milliseconds: initialSnapshot.serverTypingStartedWaitPeriodMilliseconds),
318319
),
319-
users: Map.fromEntries(
320-
initialSnapshot.realmUsers
321-
.followedBy(initialSnapshot.realmNonActiveUsers)
322-
.followedBy(initialSnapshot.crossRealmBots)
323-
.map((user) => MapEntry(user.userId, user))),
320+
users: UserStoreImpl(initialSnapshot: initialSnapshot),
324321
typingStatus: TypingStatus(
325322
selfUserId: account.userId,
326323
typingStartedExpiryPeriod: Duration(milliseconds: initialSnapshot.serverTypingStartedExpiryPeriodMilliseconds),
@@ -354,7 +351,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
354351
required this.selfUserId,
355352
required this.userSettings,
356353
required this.typingNotifier,
357-
required this.users,
354+
required UserStoreImpl users,
358355
required this.typingStatus,
359356
required ChannelStoreImpl channels,
360357
required MessageStoreImpl messages,
@@ -367,6 +364,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
367364
assert(emoji.realmUrl == realmUrl),
368365
_globalStore = globalStore,
369366
_emoji = emoji,
367+
_users = users,
370368
_channels = channels,
371369
_messages = messages;
372370

@@ -465,7 +463,10 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
465463
////////////////////////////////
466464
// Users and data about them.
467465

468-
final Map<int, User> users;
466+
@override
467+
Map<int, User> get users => _users.users;
468+
469+
final UserStoreImpl _users;
469470

470471
final TypingStatus typingStatus;
471472

@@ -634,44 +635,18 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, ChannelStore, Mess
634635

635636
case RealmUserAddEvent():
636637
assert(debugLog("server event: realm_user/add"));
637-
users[event.person.userId] = event.person;
638+
_users.handleRealmUserEvent(event);
638639
notifyListeners();
639640

640641
case RealmUserRemoveEvent():
641642
assert(debugLog("server event: realm_user/remove"));
642-
users.remove(event.userId);
643+
_users.handleRealmUserEvent(event);
643644
autocompleteViewManager.handleRealmUserRemoveEvent(event);
644645
notifyListeners();
645646

646647
case RealmUserUpdateEvent():
647648
assert(debugLog("server event: realm_user/update"));
648-
final user = users[event.userId];
649-
if (user == null) {
650-
return; // TODO log
651-
}
652-
if (event.fullName != null) user.fullName = event.fullName!;
653-
if (event.avatarUrl != null) user.avatarUrl = event.avatarUrl!;
654-
if (event.avatarVersion != null) user.avatarVersion = event.avatarVersion!;
655-
if (event.timezone != null) user.timezone = event.timezone!;
656-
if (event.botOwnerId != null) user.botOwnerId = event.botOwnerId!;
657-
if (event.role != null) user.role = event.role!;
658-
if (event.isBillingAdmin != null) user.isBillingAdmin = event.isBillingAdmin!;
659-
if (event.deliveryEmail != null) user.deliveryEmail = event.deliveryEmail!.value;
660-
if (event.newEmail != null) user.email = event.newEmail!;
661-
if (event.isActive != null) user.isActive = event.isActive!;
662-
if (event.customProfileField != null) {
663-
final profileData = (user.profileData ??= {});
664-
final update = event.customProfileField!;
665-
if (update.value != null) {
666-
profileData[update.id] = ProfileFieldUserData(value: update.value!, renderedValue: update.renderedValue);
667-
} else {
668-
profileData.remove(update.id);
669-
}
670-
if (profileData.isEmpty) {
671-
// null is equivalent to `{}` for efficiency; see [User._readProfileData].
672-
user.profileData = null;
673-
}
674-
}
649+
_users.handleRealmUserEvent(event);
675650
autocompleteViewManager.handleRealmUserUpdateEvent(event);
676651
notifyListeners();
677652

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
@@ -369,36 +369,8 @@ void main() {
369369

370370
group('PerAccountStore.handleEvent', () {
371371
// Mostly this method just dispatches to ChannelStore and MessageStore etc.,
372-
// and so most of the tests live in the test files for those
372+
// and so its tests generally live in the test files for those
373373
// (but they call the handleEvent method because it's the entry point).
374-
375-
group('RealmUserUpdateEvent', () {
376-
// TODO write more tests for handling RealmUserUpdateEvent
377-
378-
test('deliveryEmail', () async {
379-
final user = eg.user(deliveryEmail: '[email protected]');
380-
final store = eg.store(initialSnapshot: eg.initialSnapshot(
381-
realmUsers: [eg.selfUser, user]));
382-
383-
User getUser() => store.users[user.userId]!;
384-
385-
await store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
386-
deliveryEmail: null));
387-
check(getUser()).deliveryEmail.equals('[email protected]');
388-
389-
await store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
390-
deliveryEmail: const JsonNullable(null)));
391-
check(getUser()).deliveryEmail.isNull();
392-
393-
await store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
394-
deliveryEmail: const JsonNullable('[email protected]')));
395-
check(getUser()).deliveryEmail.equals('[email protected]');
396-
397-
await store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
398-
deliveryEmail: const JsonNullable('[email protected]')));
399-
check(getUser()).deliveryEmail.equals('[email protected]');
400-
});
401-
});
402374
});
403375

404376
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', () async {
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+
await store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
21+
deliveryEmail: null));
22+
check(getUser()).deliveryEmail.equals('[email protected]');
23+
24+
await store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
25+
deliveryEmail: const JsonNullable(null)));
26+
check(getUser()).deliveryEmail.isNull();
27+
28+
await store.handleEvent(RealmUserUpdateEvent(id: 1, userId: user.userId,
29+
deliveryEmail: const JsonNullable('[email protected]')));
30+
check(getUser()).deliveryEmail.equals('[email protected]');
31+
32+
await 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)