Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion lib/model/store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,24 @@ class PerAccountStore extends PerAccountStoreBase with
accountId: accountId,
selfUserId: account.userId,
);

final userMap = UserStoreImpl.userMapFromInitialSnapshot(initialSnapshot);
final selfUser = userMap[core.selfUserId];
if (selfUser == null) {
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
reportErrorToUserModally(
zulipLocalizations.errorCouldNotConnectTitle,
message: zulipLocalizations.errorMalformedResponseWithCause(200,
// skip-i18n: This would be an unlikely bug (in the server?). We're
// showing the user these details at all only because it would be a
// very nasty bug (so, important to resolve ASAP) if it ever did happen.
'self-user missing from user list'));
throw Exception("bad initial snapshot: self-user missing from user list");
}

final realm = RealmStoreImpl(core: core, initialSnapshot: initialSnapshot);
final users = UserStoreImpl(realm: realm, initialSnapshot: initialSnapshot);
final users = UserStoreImpl(realm: realm, initialSnapshot: initialSnapshot,
userMap: userMap);
final channels = ChannelStoreImpl(users: users,
initialSnapshot: initialSnapshot);
return PerAccountStore._(
Expand Down
24 changes: 18 additions & 6 deletions lib/model/user.dart
Original file line number Diff line number Diff line change
Expand Up @@ -196,17 +196,29 @@ abstract class HasUserStore extends HasRealmStore with UserStore, ProxyUserStore
/// itself. Other code accesses this functionality through [PerAccountStore],
/// or through the mixin [UserStore] which describes its interface.
class UserStoreImpl extends HasRealmStore with UserStore {
/// Construct an implementation of [UserStore] that does the work itself.
///
/// The `userMap` parameter should be the result of
/// [UserStoreImpl.userMapFromInitialSnapshot] applied to `initialSnapshot`.
UserStoreImpl({
required super.realm,
required InitialSnapshot initialSnapshot,
}) : _users = Map.fromEntries(
initialSnapshot.realmUsers
.followedBy(initialSnapshot.realmNonActiveUsers)
.followedBy(initialSnapshot.crossRealmBots)
.map((user) => MapEntry(user.userId, user))),
required Map<int, User> userMap,
}) : _users = userMap,
_mutedUsers = Set.from(initialSnapshot.mutedUsers.map((item) => item.id)),
_userStatuses = initialSnapshot.userStatuses.map((userId, change) =>
MapEntry(userId, change.apply(UserStatus.zero)));
MapEntry(userId, change.apply(UserStatus.zero))) {
// Verify that [selfUser] will work.
assert(_users.containsKey(selfUserId));
}

static Map<int, User> userMapFromInitialSnapshot(InitialSnapshot initialSnapshot) {
return Map.fromEntries(
initialSnapshot.realmUsers
.followedBy(initialSnapshot.realmNonActiveUsers)
.followedBy(initialSnapshot.crossRealmBots)
.map((user) => MapEntry(user.userId, user)));
}

final Map<int, User> _users;

Expand Down
8 changes: 6 additions & 2 deletions test/example_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ Account account({
ackedPushToken: ackedPushToken,
);
}
const _account = account;

/// A [User] which throws on attempting to mutate any of its fields.
///
Expand Down Expand Up @@ -1276,7 +1277,7 @@ InitialSnapshot initialSnapshot({
serverEmojiDataUrl: serverEmojiDataUrl
?? realmUrl.replace(path: '/static/emoji.json'),
realmEmptyTopicDisplayName: realmEmptyTopicDisplayName ?? defaultRealmEmptyTopicDisplayName,
realmUsers: realmUsers ?? [],
realmUsers: realmUsers ?? [selfUser],
realmNonActiveUsers: realmNonActiveUsers ?? [],
crossRealmBots: crossRealmBots ?? [],
);
Expand All @@ -1285,10 +1286,13 @@ const _initialSnapshot = initialSnapshot;

PerAccountStore store({
GlobalStore? globalStore,
User? selfUser,
Account? account,
InitialSnapshot? initialSnapshot,
}) {
final effectiveAccount = account ?? selfAccount;
assert(!(account != null && selfUser != null));
final effectiveAccount = account
?? (selfUser != null ? _account(user: selfUser) : selfAccount);
return PerAccountStore.fromInitialSnapshot(
globalStore: globalStore ?? _globalStore(accounts: [effectiveAccount]),
accountId: effectiveAccount.id,
Expand Down
25 changes: 16 additions & 9 deletions test/model/autocomplete_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -440,14 +440,19 @@ void main() {
late PerAccountStore store;

Future<void> prepare({
User? selfUser,
List<User> users = const [],
List<UserGroup> userGroups = const [],
List<RecentDmConversation> dmConversations = const [],
List<Message> messages = const [],
}) async {
store = eg.store(initialSnapshot: eg.initialSnapshot(
selfUser ??= eg.selfUser;
if (!users.contains(selfUser)) {
users = [...users, selfUser];
}
store = eg.store(selfUser: selfUser, initialSnapshot: eg.initialSnapshot(
realmUsers: users,
recentPrivateConversations: dmConversations));
await store.addUsers(users);
await store.addUserGroups(userGroups);
await store.addMessages(messages);
}
Expand Down Expand Up @@ -817,6 +822,7 @@ void main() {
eg.user(userId: 6, fullName: 'User Six', isBot: true),
eg.user(userId: 7, fullName: 'User Seven'),
];
final selfUser = users.last;

final userGroups = [
eg.userGroup(id: 1, name: 'User Group One'),
Expand All @@ -825,13 +831,14 @@ void main() {
eg.userGroup(id: 4, name: 'User Group Four'),
];

await prepare(users: users, userGroups: userGroups, messages: [
eg.streamMessage(sender: users[1-1], stream: stream, topic: topic),
eg.streamMessage(sender: users[5-1], stream: stream, topic: 'other $topic'),
eg.dmMessage(from: users[1-1], to: [users[2-1], eg.selfUser]),
eg.dmMessage(from: users[1-1], to: [eg.selfUser]),
eg.dmMessage(from: users[4-1], to: [eg.selfUser]),
]);
await prepare(users: users, selfUser: selfUser, userGroups: userGroups,
messages: [
eg.streamMessage(sender: users[1-1], stream: stream, topic: topic),
eg.streamMessage(sender: users[5-1], stream: stream, topic: 'other $topic'),
eg.dmMessage(from: users[1-1], to: [users[2-1], selfUser]),
eg.dmMessage(from: users[1-1], to: [selfUser]),
eg.dmMessage(from: users[4-1], to: [selfUser]),
]);

// Check the ranking of the full list of mentions,
// i.e. the results for an empty query.
Expand Down
8 changes: 4 additions & 4 deletions test/model/store_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ void main() {
final store1 = PerAccountStore.fromInitialSnapshot(
globalStore: globalStore,
accountId: 1,
initialSnapshot: eg.initialSnapshot(),
initialSnapshot: eg.initialSnapshot(realmUsers: [eg.selfUser]),
);
completers(1).single.complete(store1);
check(await future1).identicalTo(store1);
Expand All @@ -58,7 +58,7 @@ void main() {
final store2 = PerAccountStore.fromInitialSnapshot(
globalStore: globalStore,
accountId: 2,
initialSnapshot: eg.initialSnapshot(),
initialSnapshot: eg.initialSnapshot(realmUsers: [eg.otherUser]),
);
completers(2).single.complete(store2);
check(await future2).identicalTo(store2);
Expand All @@ -85,12 +85,12 @@ void main() {
final store1 = PerAccountStore.fromInitialSnapshot(
globalStore: globalStore,
accountId: 1,
initialSnapshot: eg.initialSnapshot(),
initialSnapshot: eg.initialSnapshot(realmUsers: [eg.selfUser]),
);
final store2 = PerAccountStore.fromInitialSnapshot(
globalStore: globalStore,
accountId: 2,
initialSnapshot: eg.initialSnapshot(),
initialSnapshot: eg.initialSnapshot(realmUsers: [eg.otherUser]),
);
completers(1).single.complete(store1);
completers(2).single.complete(store2);
Expand Down
2 changes: 1 addition & 1 deletion test/model/user_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ void main() {
final user3 = eg.user(userId: 3);

store = eg.store(initialSnapshot: eg.initialSnapshot(
realmUsers: [user1, user2, user3],
realmUsers: [user1, user2, user3, eg.selfUser],
mutedUsers: [MutedUserItem(id: 2), MutedUserItem(id: 1)]));
check(store.isUserMuted(1)).isTrue();
check(store.isUserMuted(2)).isTrue();
Expand Down
14 changes: 10 additions & 4 deletions test/notifications/open_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,14 @@ void main() {
eg.account(id: 1003, realmUrl: realmUrlB, user: user1),
eg.account(id: 1004, realmUrl: realmUrlB, user: user2),
];
for (final account in accounts) {
await testBinding.globalStore.add(account, eg.initialSnapshot());
}
await testBinding.globalStore.add(
accounts[0], eg.initialSnapshot(realmUsers: [user1]));
await testBinding.globalStore.add(
accounts[1], eg.initialSnapshot(realmUsers: [user2]));
await testBinding.globalStore.add(
accounts[2], eg.initialSnapshot(realmUsers: [user1]));
await testBinding.globalStore.add(
accounts[3], eg.initialSnapshot(realmUsers: [user2]));
await prepare(tester);

await checkOpenNotification(tester, accounts[0], eg.streamMessage());
Expand Down Expand Up @@ -306,7 +311,8 @@ void main() {
final accountB = eg.otherAccount;
final message = eg.streamMessage();
await testBinding.globalStore.add(accountA, eg.initialSnapshot());
await testBinding.globalStore.add(accountB, eg.initialSnapshot());
await testBinding.globalStore.add(accountB, eg.initialSnapshot(
realmUsers: [eg.otherUser]));
setupNotificationDataForLaunch(tester, accountB, message);

await prepare(tester, early: true);
Expand Down
1 change: 1 addition & 0 deletions test/widgets/action_sheet_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Future<void> setupToMessageActionSheet(WidgetTester tester, {
await testBinding.globalStore.add(
selfAccount,
eg.initialSnapshot(
realmUsers: [selfUser],
realmAllowMessageEditing: realmAllowMessageEditing,
realmMessageContentEditLimitSeconds: realmMessageContentEditLimitSeconds,
realmEnableReadReceipts: realmEnableReadReceipts,
Expand Down
3 changes: 2 additions & 1 deletion test/widgets/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ void main() {
testWidgets('choosing an account clears the navigator stack', (tester) async {
addTearDown(testBinding.reset);
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot());
await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot(
realmUsers: [eg.otherUser]));

final pushedRoutes = <Route<void>>[];
final poppedRoutes = <Route<void>>[];
Expand Down
6 changes: 4 additions & 2 deletions test/widgets/home_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ void main () {
pushedRoutes = [];
lastPoppedRoute = null;
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot());
await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot(
realmUsers: [eg.otherUser]));
await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver]));
await tester.pump(Duration.zero); // wait for the loading page
checkOnLoadingPage();
Expand Down Expand Up @@ -445,7 +446,8 @@ void main () {
testWidgets('while loading, go to nested levels of ChooseAccountPage', (tester) async {
testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration;
final thirdAccount = eg.account(user: eg.thirdUser);
await testBinding.globalStore.add(thirdAccount, eg.initialSnapshot());
await testBinding.globalStore.add(thirdAccount, eg.initialSnapshot(
realmUsers: [eg.thirdUser]));
await prepare(tester);

await tester.pump(kTryAnotherAccountWaitPeriod);
Expand Down
26 changes: 15 additions & 11 deletions test/widgets/new_dm_sheet_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import 'test_app.dart';
late PerAccountStore store;

Future<void> setupSheet(WidgetTester tester, {
User? selfUser,
required List<User> users,
List<int>? mutedUserIds,
}) async {
Expand All @@ -34,16 +35,18 @@ Future<void> setupSheet(WidgetTester tester, {
final testNavObserver = TestNavigatorObserver()
..onPushed = (route, _) => lastPushedRoute = route;

await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
await store.addUsers(users);
selfUser ??= eg.selfUser;
final account = eg.account(user: selfUser);
await testBinding.globalStore.add(account, eg.initialSnapshot(
realmUsers: [selfUser, ...users]));
store = await testBinding.globalStore.perAccount(account.id);
if (mutedUserIds != null) {
await store.setMutedUsers(mutedUserIds);
}

await tester.pumpWidget(TestZulipApp(
navigatorObservers: [testNavObserver],
accountId: eg.selfAccount.id,
accountId: account.id,
child: const HomePage()));
await tester.pumpAndSettle();

Expand Down Expand Up @@ -123,7 +126,7 @@ void main() {
];

testWidgets('shows full list initially', (tester) async {
await setupSheet(tester, users: testUsers);
await setupSheet(tester, selfUser: testUsers[0], users: testUsers);
check(findText(includePlaceholders: false, 'Alice Anderson')).findsOne();
check(findText(includePlaceholders: false, 'Bob Brown')).findsOne();
check(findText(includePlaceholders: false, 'Charlie Carter')).findsOne();
Expand All @@ -143,7 +146,8 @@ void main() {
testWidgets('deactivated users excluded', (tester) async {
// Omit a deactivated user both before there's a query…
final deactivatedUser = eg.user(fullName: 'Impostor Charlie', isActive: false);
await setupSheet(tester, users: [...testUsers, deactivatedUser]);
await setupSheet(tester, selfUser: testUsers[0],
users: [...testUsers, deactivatedUser]);
check(findText(includePlaceholders: false, 'Impostor Charlie')).findsNothing();
check(findText(includePlaceholders: false, 'Charlie Carter')).findsOne();
check(find.byIcon(ZulipIcons.check_circle_unchecked)).findsExactly(3);
Expand All @@ -159,7 +163,7 @@ void main() {
testWidgets('muted users excluded', (tester) async {
// Omit muted users both before there's a query…
final mutedUser = eg.user(fullName: 'Someone Muted');
await setupSheet(tester,
await setupSheet(tester, selfUser: testUsers[0],
users: [...testUsers, mutedUser], mutedUserIds: [mutedUser.userId]);
check(findText(includePlaceholders: false, 'Someone Muted')).findsNothing();
check(findText(includePlaceholders: false, 'Muted user')).findsNothing();
Expand Down Expand Up @@ -254,7 +258,7 @@ void main() {
}

testWidgets('tapping user chip deselects the user', (tester) async {
await setupSheet(tester, users: [eg.selfUser, eg.otherUser, eg.thirdUser]);
await setupSheet(tester, users: [eg.otherUser, eg.thirdUser]);

await tester.tap(findUserTile(eg.otherUser));
await tester.pump();
Expand All @@ -266,7 +270,7 @@ void main() {

testWidgets('selecting and deselecting a user', (tester) async {
final user = eg.user(fullName: 'Test User');
await setupSheet(tester, users: [eg.selfUser, user]);
await setupSheet(tester, users: [user]);

checkUserSelected(tester, user, false);
checkUserSelected(tester, eg.selfUser, false);
Expand All @@ -285,7 +289,7 @@ void main() {

testWidgets('other user selection deselects self user', (tester) async {
final otherUser = eg.user(fullName: 'Other User');
await setupSheet(tester, users: [eg.selfUser, otherUser]);
await setupSheet(tester, users: [otherUser]);

await tester.tap(findUserTile(eg.selfUser));
await tester.pump();
Expand All @@ -300,7 +304,7 @@ void main() {

testWidgets('other user selection hides self user', (tester) async {
final otherUser = eg.user(fullName: 'Other User');
await setupSheet(tester, users: [eg.selfUser, otherUser]);
await setupSheet(tester, users: [otherUser]);

check(findText(includePlaceholders: false, eg.selfUser.fullName)).findsOne();

Expand Down
7 changes: 2 additions & 5 deletions test/widgets/recent_dm_conversations_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,10 @@ Future<void> setupPage(WidgetTester tester, {

selfUser ??= eg.selfUser;
final selfAccount = eg.account(user: selfUser);
await testBinding.globalStore.add(selfAccount, eg.initialSnapshot());
await testBinding.globalStore.add(selfAccount, eg.initialSnapshot(
realmUsers: [selfUser, ...users]));
store = await testBinding.globalStore.perAccount(selfAccount.id);

await store.addUser(selfUser);
for (final user in users) {
await store.addUser(user);
}
if (mutedUserIds != null) {
await store.setMutedUsers(mutedUserIds);
}
Expand Down
15 changes: 10 additions & 5 deletions test/widgets/store_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,14 @@ void main() {

addTearDown(testBinding.reset);

final account1 = eg.account(id: 1, user: eg.user());
final account2 = eg.account(id: 2, user: eg.user());
await testBinding.globalStore.add(account1, eg.initialSnapshot());
await testBinding.globalStore.add(account2, eg.initialSnapshot());
final user1 = eg.user();
final user2 = eg.user();
final account1 = eg.account(id: 1, user: user1);
final account2 = eg.account(id: 2, user: user2);
await testBinding.globalStore.add(account1, eg.initialSnapshot(
realmUsers: [user1]));
await testBinding.globalStore.add(account2, eg.initialSnapshot(
realmUsers: [user2]));

final testNavObserver = TestNavigatorObserver();
await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver]));
Expand Down Expand Up @@ -374,7 +378,8 @@ void main() {
// production code, where we could reasonably add an assert against it.
// If forced, we could let this test code proceed despite such an assert…)
// hack; the snapshot probably corresponds to selfAccount, not otherAccount.
await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot());
await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot(
realmUsers: [eg.otherUser]));
await pumpWithParams(light: false, accountId: eg.otherAccount.id);
// Nudge PerAccountStoreWidget to send its updated store to MyWidgetWithMixin.
//
Expand Down