Skip to content

Commit 5b15b2a

Browse files
committed
group: Add UserGroupStore
Fixes #662. The "proxy" store mixin foreshadows a pattern I plan to use for the other substores too, to get the proxy boilerplate out of the central store.dart and into the individual substore files. That'll happen in an independent PR, though.
1 parent fc81428 commit 5b15b2a

File tree

3 files changed

+214
-2
lines changed

3 files changed

+214
-2
lines changed

lib/model/store.dart

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import 'settings.dart';
3535
import 'typing_status.dart';
3636
import 'unreads.dart';
3737
import 'user.dart';
38+
import 'user_group.dart';
3839

3940
export 'package:drift/drift.dart' show Value;
4041
export 'database.dart' show Account, AccountsCompanion, AccountAlreadyExistsException;
@@ -435,6 +436,7 @@ Uri? tryResolveUrl(Uri baseUrl, String reference) {
435436
/// [UpdateMachine].
436437
class PerAccountStore extends PerAccountStoreBase with
437438
ChangeNotifier,
439+
UserGroupStore, ProxyUserGroupStore,
438440
EmojiStore,
439441
SavedSnippetStore,
440442
UserStore,
@@ -481,6 +483,8 @@ class PerAccountStore extends PerAccountStoreBase with
481483
final channels = ChannelStoreImpl(initialSnapshot: initialSnapshot);
482484
return PerAccountStore._(
483485
core: core,
486+
groups: UserGroupStoreImpl(core: core,
487+
groups: initialSnapshot.realmUserGroups),
484488
serverPresencePingIntervalSeconds: initialSnapshot.serverPresencePingIntervalSeconds,
485489
serverPresenceOfflineThresholdSeconds: initialSnapshot.serverPresenceOfflineThresholdSeconds,
486490
realmWildcardMentionPolicy: initialSnapshot.realmWildcardMentionPolicy,
@@ -530,6 +534,7 @@ class PerAccountStore extends PerAccountStoreBase with
530534

531535
PerAccountStore._({
532536
required super.core,
537+
required UserGroupStoreImpl groups,
533538
required this.serverPresencePingIntervalSeconds,
534539
required this.serverPresenceOfflineThresholdSeconds,
535540
required this.realmWildcardMentionPolicy,
@@ -555,7 +560,8 @@ class PerAccountStore extends PerAccountStoreBase with
555560
required this.unreads,
556561
required this.recentDmConversationsView,
557562
required this.recentSenders,
558-
}) : _realmEmptyTopicDisplayName = realmEmptyTopicDisplayName,
563+
}) : _groups = groups,
564+
_realmEmptyTopicDisplayName = realmEmptyTopicDisplayName,
559565
_emoji = emoji,
560566
_savedSnippets = savedSnippets,
561567
_users = users,
@@ -588,6 +594,13 @@ class PerAccountStore extends PerAccountStoreBase with
588594
////////////////////////////////
589595
// Data attached to the realm or the server.
590596

597+
// (User groups come before even realm settings,
598+
// because they'll be used for interpreting many realm settings.)
599+
@protected
600+
@override
601+
UserGroupStore get userGroupStore => _groups;
602+
final UserGroupStoreImpl _groups;
603+
591604
final int serverPresencePingIntervalSeconds;
592605
final int serverPresenceOfflineThresholdSeconds;
593606

@@ -909,7 +922,8 @@ class PerAccountStore extends PerAccountStoreBase with
909922

910923
case UserGroupEvent():
911924
assert(debugLog("server event: user_group/${event.op}"));
912-
// TODO(#662) handle
925+
_groups.handleUserGroupEvent(event);
926+
notifyListeners();
913927

914928
case RealmUserAddEvent():
915929
assert(debugLog("server event: realm_user/add"));

lib/model/user_group.dart

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import 'package:flutter/foundation.dart';
2+
3+
import '../api/model/events.dart';
4+
import '../api/model/model.dart';
5+
import 'store.dart';
6+
7+
/// The portion of [PerAccountStore] describing user groups.
8+
mixin UserGroupStore on PerAccountStoreBase {
9+
/// The user group with the given ID, if any.
10+
UserGroup? getGroup(int userGroupId);
11+
12+
/// All non-deactivated user groups in the realm.
13+
///
14+
/// For when deactivated groups are desired too, see [allGroups].
15+
Iterable<UserGroup> get activeGroups;
16+
17+
/// All user groups in the realm, even those deactivated.
18+
///
19+
/// Consider using [activeGroups] instead.
20+
Iterable<UserGroup> get allGroups;
21+
}
22+
23+
mixin ProxyUserGroupStore on UserGroupStore {
24+
@protected
25+
UserGroupStore get userGroupStore;
26+
27+
@override
28+
UserGroup? getGroup(int userGroupId) => userGroupStore.getGroup(userGroupId);
29+
@override
30+
Iterable<UserGroup> get activeGroups => userGroupStore.activeGroups;
31+
@override
32+
Iterable<UserGroup> get allGroups => userGroupStore.allGroups;
33+
}
34+
35+
/// The implementation of [UserGroupStore] that does the work.
36+
class UserGroupStoreImpl extends PerAccountStoreBase with UserGroupStore {
37+
UserGroupStoreImpl({required super.core, required List<UserGroup> groups})
38+
: _groups = {
39+
for (final group in groups)
40+
group.id: group,
41+
};
42+
43+
@override
44+
UserGroup? getGroup(int userGroupId) {
45+
return _groups[userGroupId];
46+
}
47+
48+
@override
49+
Iterable<UserGroup> get activeGroups {
50+
return _groups.values.where((group) => !group.deactivated);
51+
}
52+
53+
@override
54+
Iterable<UserGroup> get allGroups {
55+
return _groups.values;
56+
}
57+
58+
final Map<int, UserGroup> _groups;
59+
60+
void handleUserGroupEvent(UserGroupEvent event) {
61+
switch (event) {
62+
case UserGroupAddEvent():
63+
_groups[event.group.id] = event.group;
64+
65+
case UserGroupRemoveEvent():
66+
_groups.remove(event.groupId);
67+
68+
case UserGroupUpdateEvent():
69+
final group = _groups[event.groupId];
70+
if (group == null) {
71+
return; // TODO log
72+
}
73+
final data = event.data;
74+
if (data.name != null) group.name = data.name!;
75+
if (data.description != null) group.description = data.description!;
76+
if (data.deactivated != null) group.deactivated = data.deactivated!;
77+
}
78+
}
79+
}

test/model/user_group_test.dart

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:test_api/scaffolding.dart';
3+
import 'package:zulip/api/model/events.dart';
4+
import 'package:zulip/api/model/model.dart';
5+
import 'package:zulip/model/user_group.dart';
6+
7+
import '../api/model/model_checks.dart';
8+
import '../example_data.dart' as eg;
9+
import '../stdlib_checks.dart';
10+
11+
void main() {
12+
List<UserGroup> sorted(Iterable<UserGroup> groups) {
13+
return groups.toList()..sort((a, b) => a.id.compareTo(b.id));
14+
}
15+
16+
void checkGroupsEqual(UserGroupStore store, Iterable<Object?> expected) {
17+
check(sorted(store.allGroups)).jsonEquals(expected);
18+
}
19+
20+
test('initialize', () {
21+
final groups = [eg.userGroup(), eg.userGroup()];
22+
final store = eg.store(initialSnapshot: eg.initialSnapshot(
23+
realmUserGroups: groups));
24+
checkGroupsEqual(store, groups);
25+
});
26+
27+
test('getGroup', () {
28+
final group1 = eg.userGroup();
29+
final group2 = eg.userGroup();
30+
final store = eg.store(initialSnapshot: eg.initialSnapshot(
31+
realmUserGroups: [group1, group2]));
32+
check(store.getGroup(group1.id)).jsonEquals(group1);
33+
check(store.getGroup(group2.id)).jsonEquals(group2);
34+
check(store.getGroup(eg.userGroup().id)).isNull();
35+
});
36+
37+
test('activeGroups, allGroups', () async {
38+
final group1 = eg.userGroup(deactivated: false);
39+
final group2 = eg.userGroup(deactivated: true);
40+
final group3 = eg.userGroup(deactivated: false);
41+
final store = eg.store(initialSnapshot: eg.initialSnapshot(
42+
realmUserGroups: [group1, group2, group3]));
43+
check(sorted(store.allGroups)).jsonEquals([group1, group2, group3]);
44+
check(sorted(store.activeGroups)).jsonEquals([group1, group3]);
45+
46+
await store.handleEvent(UserGroupUpdateEvent(id: 1, groupId: group1.id,
47+
data: UserGroupUpdateData(name: null, description: null, deactivated: true)));
48+
check(sorted(store.activeGroups)).jsonEquals([group3]);
49+
});
50+
51+
test('UserGroupAddEvent, UserGroupRemoveEvent', () async {
52+
final group1 = eg.userGroup();
53+
final store = eg.store(initialSnapshot: eg.initialSnapshot(
54+
realmUserGroups: [group1]));
55+
checkGroupsEqual(store, [group1]);
56+
57+
final group2 = eg.userGroup();
58+
await store.handleEvent(UserGroupAddEvent(id: 1, group: group2));
59+
checkGroupsEqual(store, [group1, group2]);
60+
61+
await store.handleEvent(UserGroupRemoveEvent(id: 2, groupId: group1.id));
62+
checkGroupsEqual(store, [group2]);
63+
});
64+
65+
test('UserGroupUpdateEvent', () async {
66+
final store = eg.store();
67+
final group = eg.userGroup(
68+
name: 'a group', description: 'is a group', deactivated: false);
69+
await store.handleEvent(UserGroupAddEvent(id: 1, group: group));
70+
checkGroupsEqual(store, [group]);
71+
72+
// Handles all the properties being updated at once.
73+
await store.handleEvent(UserGroupUpdateEvent(id: 2, groupId: group.id,
74+
data: UserGroupUpdateData(name: 'revised group',
75+
description: 'different description', deactivated: true)));
76+
checkGroupsEqual(store, [{
77+
...group.toJson(),
78+
'name': 'revised group',
79+
'description': 'different description',
80+
'deactivated': true,
81+
}]);
82+
83+
// Handles some properties being null, still updating the one that's present.
84+
await store.handleEvent(UserGroupUpdateEvent(id: 2, groupId: group.id,
85+
data: UserGroupUpdateData(name: null,
86+
description: null, deactivated: false)));
87+
checkGroupsEqual(store, [{
88+
...group.toJson(),
89+
'name': 'revised group',
90+
'description': 'different description',
91+
'deactivated': false,
92+
}]);
93+
});
94+
95+
test('various fields make it through', () async {
96+
final store = eg.store(initialSnapshot: eg.initialSnapshot(
97+
realmUserGroups: [
98+
eg.userGroup(id: 3, name: 'some group', description: 'this is a group',
99+
isSystemGroup: true, deactivated: false),
100+
]));
101+
await store.handleEvent(UserGroupAddEvent(id: 1, group: eg.userGroup(
102+
id: 5, name: 'a different group', description: 'also a group',
103+
isSystemGroup: false, deactivated: true)));
104+
check(sorted(store.allGroups)).deepEquals(<Condition<Object?>>[
105+
(it) => it.isA<UserGroup>()
106+
..id.equals(3)
107+
..name.equals('some group')
108+
..description.equals('this is a group')
109+
..isSystemGroup.isTrue()
110+
..deactivated.isFalse(),
111+
(it) => it.isA<UserGroup>()
112+
..id.equals(5)
113+
..name.equals('a different group')
114+
..description.equals('also a group')
115+
..isSystemGroup.isFalse()
116+
..deactivated.isTrue(),
117+
]);
118+
});
119+
}

0 commit comments

Comments
 (0)