diff --git a/lib/model/store.dart b/lib/model/store.dart index 070c20fb27..e74a22c623 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -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._( diff --git a/lib/model/user.dart b/lib/model/user.dart index da6b828014..4983cf48ca 100644 --- a/lib/model/user.dart +++ b/lib/model/user.dart @@ -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 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 userMapFromInitialSnapshot(InitialSnapshot initialSnapshot) { + return Map.fromEntries( + initialSnapshot.realmUsers + .followedBy(initialSnapshot.realmNonActiveUsers) + .followedBy(initialSnapshot.crossRealmBots) + .map((user) => MapEntry(user.userId, user))); + } final Map _users; diff --git a/test/example_data.dart b/test/example_data.dart index 9ad6e8ca13..3e4da34b96 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -313,6 +313,7 @@ Account account({ ackedPushToken: ackedPushToken, ); } +const _account = account; /// A [User] which throws on attempting to mutate any of its fields. /// @@ -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 ?? [], ); @@ -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, diff --git a/test/model/autocomplete_test.dart b/test/model/autocomplete_test.dart index 92e5a81d9a..20f8adb6be 100644 --- a/test/model/autocomplete_test.dart +++ b/test/model/autocomplete_test.dart @@ -440,14 +440,19 @@ void main() { late PerAccountStore store; Future prepare({ + User? selfUser, List users = const [], List userGroups = const [], List dmConversations = const [], List 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); } @@ -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'), @@ -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. diff --git a/test/model/store_test.dart b/test/model/store_test.dart index 8697647cf6..d3c3e9c4e4 100644 --- a/test/model/store_test.dart +++ b/test/model/store_test.dart @@ -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); @@ -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); @@ -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); diff --git a/test/model/user_test.dart b/test/model/user_test.dart index 3638e3c214..6d5e5c2519 100644 --- a/test/model/user_test.dart +++ b/test/model/user_test.dart @@ -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(); diff --git a/test/notifications/open_test.dart b/test/notifications/open_test.dart index cdfd8ef361..2db54079c3 100644 --- a/test/notifications/open_test.dart +++ b/test/notifications/open_test.dart @@ -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()); @@ -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); diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart index 33c02dbcbd..83574786a7 100644 --- a/test/widgets/action_sheet_test.dart +++ b/test/widgets/action_sheet_test.dart @@ -77,6 +77,7 @@ Future setupToMessageActionSheet(WidgetTester tester, { await testBinding.globalStore.add( selfAccount, eg.initialSnapshot( + realmUsers: [selfUser], realmAllowMessageEditing: realmAllowMessageEditing, realmMessageContentEditLimitSeconds: realmMessageContentEditLimitSeconds, realmEnableReadReceipts: realmEnableReadReceipts, diff --git a/test/widgets/app_test.dart b/test/widgets/app_test.dart index eca2f95cab..022cd52ed3 100644 --- a/test/widgets/app_test.dart +++ b/test/widgets/app_test.dart @@ -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 = >[]; final poppedRoutes = >[]; diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 5a8d3cca33..5422a8973b 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -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(); @@ -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); diff --git a/test/widgets/new_dm_sheet_test.dart b/test/widgets/new_dm_sheet_test.dart index a95d42dc1a..a11e862b48 100644 --- a/test/widgets/new_dm_sheet_test.dart +++ b/test/widgets/new_dm_sheet_test.dart @@ -25,6 +25,7 @@ import 'test_app.dart'; late PerAccountStore store; Future setupSheet(WidgetTester tester, { + User? selfUser, required List users, List? mutedUserIds, }) async { @@ -34,16 +35,18 @@ Future 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(); @@ -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(); @@ -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); @@ -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(); @@ -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(); @@ -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); @@ -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(); @@ -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(); diff --git a/test/widgets/recent_dm_conversations_test.dart b/test/widgets/recent_dm_conversations_test.dart index a947420dad..32c1d3f28d 100644 --- a/test/widgets/recent_dm_conversations_test.dart +++ b/test/widgets/recent_dm_conversations_test.dart @@ -38,13 +38,10 @@ Future 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); } diff --git a/test/widgets/store_test.dart b/test/widgets/store_test.dart index e2c9be2d18..fe3ade165d 100644 --- a/test/widgets/store_test.dart +++ b/test/widgets/store_test.dart @@ -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])); @@ -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. //