diff --git a/lib/model/actions.dart b/lib/model/actions.dart index 3869faae03..826546e2af 100644 --- a/lib/model/actions.dart +++ b/lib/model/actions.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import '../notifications/display.dart'; import '../notifications/receive.dart'; +import 'settings.dart'; import 'store.dart'; // TODO: Make this a part of GlobalStore @@ -21,6 +22,18 @@ Future logOutAccount(GlobalStore globalStore, int accountId) async { await globalStore.removeAccount(accountId); } +Future removeLastVisitedAccountIfNecessary(GlobalStore store, int loggedOutAccountId) async { + // If account is not logged out yet, do nothing. + if (store.getAccount(loggedOutAccountId) != null) return; + + // If the logged-out account is different than the last visited one, do nothing. + if (loggedOutAccountId != store.settings.getInt(IntGlobalSetting.lastVisitedAccountId)) { + return; + } + + await store.settings.setInt(IntGlobalSetting.lastVisitedAccountId, null); +} + Future unregisterToken(GlobalStore globalStore, int accountId) async { final account = globalStore.getAccount(accountId); if (account == null) return; // TODO(log) diff --git a/lib/model/database.dart b/lib/model/database.dart index ca84fc949c..7407ae75af 100644 --- a/lib/model/database.dart +++ b/lib/model/database.dart @@ -73,6 +73,34 @@ class BoolGlobalSettings extends Table { Set>? get primaryKey => {name}; } +/// The table of the user's int-valued, account-independent settings. +/// +/// These apply across all the user's accounts on this client +/// (i.e. on this install of the app on this device). +/// +/// Each row is a [IntGlobalSettingRow], +/// referring to a possible setting from [IntGlobalSetting]. +/// For settings in [IntGlobalSetting] without a row in this table, +/// the setting's value is `null`. +@DataClassName('IntGlobalSettingRow') +class IntGlobalSettings extends Table { + /// The setting's name, a possible name from [IntGlobalSetting]. + /// + /// The table may have rows where [name] is not the name of any + /// enum value in [IntGlobalSetting]. + /// This happens if the app has previously run at a future or modified + /// version which had additional values in that enum, + /// and the user set one of those additional settings. + /// The app ignores any such unknown rows. + TextColumn get name => text()(); + + /// The user's chosen value for the setting. + IntColumn get value => integer()(); + + @override + Set>? get primaryKey => {name}; +} + /// The table of [Account] records in the app's database. class Accounts extends Table { /// The ID of this account in the app's local database. @@ -116,7 +144,7 @@ class UriConverter extends TypeConverter { @override Uri fromSql(String fromDb) => Uri.parse(fromDb); } -@DriftDatabase(tables: [GlobalSettings, BoolGlobalSettings, Accounts]) +@DriftDatabase(tables: [GlobalSettings, BoolGlobalSettings, IntGlobalSettings, Accounts]) class AppDatabase extends _$AppDatabase { AppDatabase(super.e); @@ -129,7 +157,7 @@ class AppDatabase extends _$AppDatabase { // information on using the build_runner. // * Write a migration in `_migrationSteps` below. // * Write tests. - static const int latestSchemaVersion = 9; // See note. + static const int latestSchemaVersion = 10; // See note. @override int get schemaVersion => latestSchemaVersion; @@ -200,7 +228,10 @@ class AppDatabase extends _$AppDatabase { // assume there wasn't also the legacy app before that. await m.database.update(schema.globalSettings).write( RawValuesInsertable({'legacy_upgrade_state': Constant('noLegacy')})); - } + }, + from9To10: (m, schema) async { + await m.createTable(schema.intGlobalSettings); + }, ); Future _createLatestSchema(Migrator m) async { @@ -256,6 +287,14 @@ class AppDatabase extends _$AppDatabase { return result; } + Future> getIntGlobalSettings() async { + return { + for (final row in await select(intGlobalSettings).get()) + if (IntGlobalSetting.byName(row.name) case final setting?) + setting: row.value + }; + } + Future createAccount(AccountsCompanion values) async { try { return await into(accounts).insert(values); diff --git a/lib/model/database.g.dart b/lib/model/database.g.dart index 6fdbec74f8..bfca5d5857 100644 --- a/lib/model/database.g.dart +++ b/lib/model/database.g.dart @@ -708,6 +708,225 @@ class BoolGlobalSettingsCompanion } } +class $IntGlobalSettingsTable extends IntGlobalSettings + with TableInfo<$IntGlobalSettingsTable, IntGlobalSettingRow> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $IntGlobalSettingsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _valueMeta = const VerificationMeta('value'); + @override + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [name, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'int_global_settings'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('value')) { + context.handle( + _valueMeta, + value.isAcceptableOrUnknown(data['value']!, _valueMeta), + ); + } else if (isInserting) { + context.missing(_valueMeta); + } + return context; + } + + @override + Set get $primaryKey => {name}; + @override + IntGlobalSettingRow map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return IntGlobalSettingRow( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + $IntGlobalSettingsTable createAlias(String alias) { + return $IntGlobalSettingsTable(attachedDatabase, alias); + } +} + +class IntGlobalSettingRow extends DataClass + implements Insertable { + /// The setting's name, a possible name from [IntGlobalSetting]. + /// + /// The table may have rows where [name] is not the name of any + /// enum value in [IntGlobalSetting]. + /// This happens if the app has previously run at a future or modified + /// version which had additional values in that enum, + /// and the user set one of those additional settings. + /// The app ignores any such unknown rows. + final String name; + + /// The user's chosen value for the setting. + final int value; + const IntGlobalSettingRow({required this.name, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['value'] = Variable(value); + return map; + } + + IntGlobalSettingsCompanion toCompanion(bool nullToAbsent) { + return IntGlobalSettingsCompanion(name: Value(name), value: Value(value)); + } + + factory IntGlobalSettingRow.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return IntGlobalSettingRow( + name: serializer.fromJson(json['name']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'value': serializer.toJson(value), + }; + } + + IntGlobalSettingRow copyWith({String? name, int? value}) => + IntGlobalSettingRow(name: name ?? this.name, value: value ?? this.value); + IntGlobalSettingRow copyWithCompanion(IntGlobalSettingsCompanion data) { + return IntGlobalSettingRow( + name: data.name.present ? data.name.value : this.name, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('IntGlobalSettingRow(') + ..write('name: $name, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is IntGlobalSettingRow && + other.name == this.name && + other.value == this.value); +} + +class IntGlobalSettingsCompanion extends UpdateCompanion { + final Value name; + final Value value; + final Value rowid; + const IntGlobalSettingsCompanion({ + this.name = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + IntGlobalSettingsCompanion.insert({ + required String name, + required int value, + this.rowid = const Value.absent(), + }) : name = Value(name), + value = Value(value); + static Insertable custom({ + Expression? name, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + IntGlobalSettingsCompanion copyWith({ + Value? name, + Value? value, + Value? rowid, + }) { + return IntGlobalSettingsCompanion( + name: name ?? this.name, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('IntGlobalSettingsCompanion(') + ..write('name: $name, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { @override final GeneratedDatabase attachedDatabase; @@ -1303,6 +1522,8 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $GlobalSettingsTable globalSettings = $GlobalSettingsTable(this); late final $BoolGlobalSettingsTable boolGlobalSettings = $BoolGlobalSettingsTable(this); + late final $IntGlobalSettingsTable intGlobalSettings = + $IntGlobalSettingsTable(this); late final $AccountsTable accounts = $AccountsTable(this); @override Iterable> get allTables => @@ -1311,6 +1532,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { List get allSchemaEntities => [ globalSettings, boolGlobalSettings, + intGlobalSettings, accounts, ]; } @@ -1717,6 +1939,162 @@ typedef $$BoolGlobalSettingsTableProcessedTableManager = BoolGlobalSettingRow, PrefetchHooks Function() >; +typedef $$IntGlobalSettingsTableCreateCompanionBuilder = + IntGlobalSettingsCompanion Function({ + required String name, + required int value, + Value rowid, + }); +typedef $$IntGlobalSettingsTableUpdateCompanionBuilder = + IntGlobalSettingsCompanion Function({ + Value name, + Value value, + Value rowid, + }); + +class $$IntGlobalSettingsTableFilterComposer + extends Composer<_$AppDatabase, $IntGlobalSettingsTable> { + $$IntGlobalSettingsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get value => $composableBuilder( + column: $table.value, + builder: (column) => ColumnFilters(column), + ); +} + +class $$IntGlobalSettingsTableOrderingComposer + extends Composer<_$AppDatabase, $IntGlobalSettingsTable> { + $$IntGlobalSettingsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get value => $composableBuilder( + column: $table.value, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$IntGlobalSettingsTableAnnotationComposer + extends Composer<_$AppDatabase, $IntGlobalSettingsTable> { + $$IntGlobalSettingsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get value => + $composableBuilder(column: $table.value, builder: (column) => column); +} + +class $$IntGlobalSettingsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $IntGlobalSettingsTable, + IntGlobalSettingRow, + $$IntGlobalSettingsTableFilterComposer, + $$IntGlobalSettingsTableOrderingComposer, + $$IntGlobalSettingsTableAnnotationComposer, + $$IntGlobalSettingsTableCreateCompanionBuilder, + $$IntGlobalSettingsTableUpdateCompanionBuilder, + ( + IntGlobalSettingRow, + BaseReferences< + _$AppDatabase, + $IntGlobalSettingsTable, + IntGlobalSettingRow + >, + ), + IntGlobalSettingRow, + PrefetchHooks Function() + > { + $$IntGlobalSettingsTableTableManager( + _$AppDatabase db, + $IntGlobalSettingsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$IntGlobalSettingsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$IntGlobalSettingsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$IntGlobalSettingsTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value name = const Value.absent(), + Value value = const Value.absent(), + Value rowid = const Value.absent(), + }) => IntGlobalSettingsCompanion( + name: name, + value: value, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String name, + required int value, + Value rowid = const Value.absent(), + }) => IntGlobalSettingsCompanion.insert( + name: name, + value: value, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$IntGlobalSettingsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $IntGlobalSettingsTable, + IntGlobalSettingRow, + $$IntGlobalSettingsTableFilterComposer, + $$IntGlobalSettingsTableOrderingComposer, + $$IntGlobalSettingsTableAnnotationComposer, + $$IntGlobalSettingsTableCreateCompanionBuilder, + $$IntGlobalSettingsTableUpdateCompanionBuilder, + ( + IntGlobalSettingRow, + BaseReferences< + _$AppDatabase, + $IntGlobalSettingsTable, + IntGlobalSettingRow + >, + ), + IntGlobalSettingRow, + PrefetchHooks Function() + >; typedef $$AccountsTableCreateCompanionBuilder = AccountsCompanion Function({ Value id, @@ -1998,6 +2376,8 @@ class $AppDatabaseManager { $$GlobalSettingsTableTableManager(_db, _db.globalSettings); $$BoolGlobalSettingsTableTableManager get boolGlobalSettings => $$BoolGlobalSettingsTableTableManager(_db, _db.boolGlobalSettings); + $$IntGlobalSettingsTableTableManager get intGlobalSettings => + $$IntGlobalSettingsTableTableManager(_db, _db.intGlobalSettings); $$AccountsTableTableManager get accounts => $$AccountsTableTableManager(_db, _db.accounts); } diff --git a/lib/model/schema_versions.g.dart b/lib/model/schema_versions.g.dart index 782b9409e2..65faa2791b 100644 --- a/lib/model/schema_versions.g.dart +++ b/lib/model/schema_versions.g.dart @@ -595,6 +595,90 @@ i1.GeneratedColumn _column_15(String aliasedName) => true, type: i1.DriftSqlType.string, ); + +final class Schema10 extends i0.VersionedSchema { + Schema10({required super.database}) : super(version: 10); + @override + late final List entities = [ + globalSettings, + boolGlobalSettings, + intGlobalSettings, + accounts, + ]; + late final Shape6 globalSettings = Shape6( + source: i0.VersionedTable( + entityName: 'global_settings', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_9, _column_10, _column_13, _column_14, _column_15], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 boolGlobalSettings = Shape3( + source: i0.VersionedTable( + entityName: 'bool_global_settings', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(name)'], + columns: [_column_11, _column_12], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 intGlobalSettings = Shape7( + source: i0.VersionedTable( + entityName: 'int_global_settings', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(name)'], + columns: [_column_11, _column_16], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape0 accounts = Shape0( + source: i0.VersionedTable( + entityName: 'accounts', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'UNIQUE(realm_url, user_id)', + 'UNIQUE(realm_url, email)', + ], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_7, + _column_8, + ], + attachedDatabase: database, + ), + alias: null, + ); +} + +class Shape7 extends i0.VersionedTable { + Shape7({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get value => + columnsByName['value']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_16(String aliasedName) => + i1.GeneratedColumn( + 'value', + aliasedName, + false, + type: i1.DriftSqlType.int, + ); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -604,6 +688,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, required Future Function(i1.Migrator m, Schema9 schema) from8To9, + required Future Function(i1.Migrator m, Schema10 schema) from9To10, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -647,6 +732,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from8To9(migrator, schema); return 9; + case 9: + final schema = Schema10(database: database); + final migrator = i1.Migrator(database, schema); + await from9To10(migrator, schema); + return 10; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -662,6 +752,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, required Future Function(i1.Migrator m, Schema9 schema) from8To9, + required Future Function(i1.Migrator m, Schema10 schema) from9To10, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -672,5 +763,6 @@ i1.OnUpgrade stepByStep({ from6To7: from6To7, from7To8: from7To8, from8To9: from8To9, + from9To10: from9To10, ), ); diff --git a/lib/model/settings.dart b/lib/model/settings.dart index 85a1c40f50..40c0589359 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -203,6 +203,41 @@ enum BoolGlobalSetting { }; } +/// An int-valued, account-independent setting the user might set. +/// +/// These are recorded in the table [IntGlobalSettings]. +/// To read the value of one of these settings, use [GlobalSettingsStore.getInt]; +/// to set the value, use [GlobalSettingsStore.setInt]. +/// +/// To introduce a new setting, add a value to this enum. +/// Avoid re-using any old names found in the "former settings" list. +/// +/// To remove a setting, comment it out and move to the "former settings" list. +/// Tracking the names of settings that formerly existed is important because +/// they may still appear in users' databases, which means that if we were to +/// accidentally reuse one for an unrelated new setting then users would +/// unwittingly get those values applied to the new setting, +/// which could cause very confusing buggy behavior. +/// +/// (If the list of former settings gets long, we could do a migration to clear +/// them from existing installs, and then drop the list. We don't do that +/// eagerly each time, to avoid creating a new schema version each time we +/// finish an experimental feature.) +enum IntGlobalSetting { + lastVisitedAccountId, + + // Former settings which might exist in the database, + // whose names should therefore not be reused: + ; + + static IntGlobalSetting? byName(String name) => _byName[name]; + + static final Map _byName = { + for (final v in values) + v.name: v, + }; +} + /// Store for the user's account-independent settings. /// /// From UI code, use [GlobalStoreWidget.settingsOf] to get hold of @@ -212,7 +247,8 @@ class GlobalSettingsStore extends ChangeNotifier { required GlobalStoreBackend backend, required GlobalSettingsData data, required Map boolData, - }) : _backend = backend, _data = data, _boolData = boolData; + required Map intData, + }) : _backend = backend, _data = data, _boolData = boolData, _intData = intData; static final List experimentalFeatureFlags = BoolGlobalSetting.values.where((setting) => @@ -386,4 +422,30 @@ class GlobalSettingsStore extends ChangeNotifier { } notifyListeners(); } + + /// The user's choice of the given int-valued setting, or null if not set. + /// + /// See also [setInt]. + int? getInt(IntGlobalSetting setting) { + return _intData[setting]; + } + + /// A cache of the [IntGlobalSettings] table in the underlying data store. + final Map _intData; + + /// Set or unset the given int-valued setting, + /// persistently for future runs of the app. + /// + /// A value of null means the setting will be cleared out. + /// + /// See also [getInt]. + Future setInt(IntGlobalSetting setting, int? value) async { + await _backend.doSetIntGlobalSetting(setting, value); + if (value == null) { + _intData.remove(setting); + } else { + _intData[setting] = value; + } + notifyListeners(); + } } diff --git a/lib/model/store.dart b/lib/model/store.dart index 4f3e72442f..3d4afe0c17 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -57,6 +57,11 @@ abstract class GlobalStoreBackend { /// This should only be called from [GlobalSettingsStore]. Future doSetBoolGlobalSetting(BoolGlobalSetting setting, bool? value); + /// Set or unset the given int-valued setting in the underlying data store. + /// + /// This should only be called from [GlobalSettingsStore]. + Future doSetIntGlobalSetting(IntGlobalSetting setting, int? value); + // TODO move here the similar methods for accounts; // perhaps the rest of the GlobalStore abstract methods, too. } @@ -82,10 +87,11 @@ abstract class GlobalStore extends ChangeNotifier { required GlobalStoreBackend backend, required GlobalSettingsData globalSettings, required Map boolGlobalSettings, + required Map intGlobalSettings, required Iterable accounts, }) : settings = GlobalSettingsStore(backend: backend, - data: globalSettings, boolData: boolGlobalSettings), + data: globalSettings, boolData: boolGlobalSettings, intData: intGlobalSettings), _accounts = Map.fromEntries(accounts.map((a) => MapEntry(a.id, a))); /// The store for the user's account-independent settings. @@ -864,6 +870,18 @@ class LiveGlobalStoreBackend implements GlobalStoreBackend { BoolGlobalSettingRow(name: setting.name, value: value)); } } + + @override + Future doSetIntGlobalSetting(IntGlobalSetting setting, int? value) async { + if (value == null) { + await (_db.delete(_db.intGlobalSettings) + ..where((r) => r.name.equals(setting.name)) + ).go(); + } else { + await _db.into(_db.intGlobalSettings).insertOnConflictUpdate( + IntGlobalSettingRow(name: setting.name, value: value)); + } + } } /// A [GlobalStore] that uses a live server and live, persistent local database. @@ -878,6 +896,7 @@ class LiveGlobalStore extends GlobalStore { required LiveGlobalStoreBackend backend, required super.globalSettings, required super.boolGlobalSettings, + required super.intGlobalSettings, required super.accounts, }) : _backend = backend, super(backend: backend); @@ -908,20 +927,23 @@ class LiveGlobalStore extends GlobalStore { final t2 = stopwatch.elapsed; final boolGlobalSettings = await db.getBoolGlobalSettings(); final t3 = stopwatch.elapsed; - final accounts = await db.select(db.accounts).get(); + final intGlobalSettings = await db.getIntGlobalSettings(); final t4 = stopwatch.elapsed; + final accounts = await db.select(db.accounts).get(); + final t5 = stopwatch.elapsed; if (kProfileMode) { String format(Duration d) => "${(d.inMicroseconds / 1000.0).toStringAsFixed(1)}ms"; - profilePrint("db load time ${format(t4)} total: ${format(t1)} init, " + profilePrint("db load time ${format(t5)} total: ${format(t1)} init, " "${format(t2 - t1)} settings, ${format(t3 - t2)} bool-settings, " - "${format(t4 - t3)} accounts"); + "${format(t4 - t3)} int-settings, ${format(t5 - t4)} accounts"); } return LiveGlobalStore._( backend: LiveGlobalStoreBackend._(db: db), globalSettings: globalSettings, boolGlobalSettings: boolGlobalSettings, + intGlobalSettings: intGlobalSettings, accounts: accounts); } diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index d3ed5c463d..99102f54af 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -8,6 +8,7 @@ import '../generated/l10n/zulip_localizations.dart'; import '../log.dart'; import '../model/actions.dart'; import '../model/localizations.dart'; +import '../model/settings.dart'; import '../model/store.dart'; import '../notifications/open.dart'; import 'about_zulip.dart'; @@ -210,14 +211,13 @@ class _ZulipAppState extends State with WidgetsBindingObserver { ]; } - final globalStore = GlobalStoreWidget.of(context); - // TODO(#524) choose initial account as last one used - final initialAccountId = globalStore.accounts.firstOrNull?.id; + final lastAccountId = GlobalStoreWidget.settingsOf(context) + .getInt(IntGlobalSetting.lastVisitedAccountId); return [ - if (initialAccountId == null) + if (lastAccountId == null) MaterialWidgetRoute(page: const ChooseAccountPage()) else - HomePage.buildRoute(accountId: initialAccountId), + HomePage.buildRoute(accountId: lastAccountId), ]; } @@ -254,6 +254,7 @@ class _ZulipAppState extends State with WidgetsBindingObserver { if (widget.navigatorObservers != null) ...widget.navigatorObservers!, _PreventEmptyStack(), + _UpdateLastVisitedAccount(GlobalStoreWidget.settingsOf(context)), ], builder: (BuildContext context, Widget? child) { if (!ZulipApp.ready.value) { @@ -305,6 +306,39 @@ class _PreventEmptyStack extends NavigatorObserver { } } +class _UpdateLastVisitedAccount extends NavigatorObserver { + _UpdateLastVisitedAccount(this.globalSettings); + + final GlobalSettingsStore globalSettings; + + void _changeLastVisitedAccountIfNecessary(Route? route) { + final old = globalSettings.getInt(IntGlobalSetting.lastVisitedAccountId); + if (route case AccountPageRouteMixin(accountId: var new_) when new_ != old) { + globalSettings.setInt(IntGlobalSetting.lastVisitedAccountId, new_); + } + } + + @override + void didPush(Route route, Route? previousRoute) { + _changeLastVisitedAccountIfNecessary(route); + } + + @override + void didPop(Route route, Route? previousRoute) { + _changeLastVisitedAccountIfNecessary(previousRoute); + } + + @override + void didRemove(Route route, Route? previousRoute) { + _changeLastVisitedAccountIfNecessary(previousRoute); + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + _changeLastVisitedAccountIfNecessary(newRoute); + } +} + class ChooseAccountPage extends StatelessWidget { const ChooseAccountPage({super.key}); @@ -337,7 +371,13 @@ class ChooseAccountPage extends StatelessWidget { if (await dialog.result == true) { if (!context.mounted) return; // TODO error handling if db write fails? - unawaited(logOutAccount(GlobalStoreWidget.of(context), accountId)); + unawaited(Future(() async { + if (!context.mounted) return; + await logOutAccount(GlobalStoreWidget.of(context), accountId); + if (!context.mounted) return; + await removeLastVisitedAccountIfNecessary( + GlobalStoreWidget.of(context), accountId); + })); } }, child: Text(zulipLocalizations.chooseAccountPageLogOutButton)), diff --git a/test/example_data.dart b/test/example_data.dart index 267d75d8ed..efe9e29433 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -1182,11 +1182,13 @@ ChannelUpdateEvent channelUpdateEvent( TestGlobalStore globalStore({ GlobalSettingsData? globalSettings, Map? boolGlobalSettings, + Map? intGlobalSettings, List accounts = const [], }) { return TestGlobalStore( globalSettings: globalSettings, boolGlobalSettings: boolGlobalSettings, + intGlobalSettings: intGlobalSettings, accounts: accounts, ); } diff --git a/test/model/database_test.dart b/test/model/database_test.dart index ecf9e03bb1..64fd221615 100644 --- a/test/model/database_test.dart +++ b/test/model/database_test.dart @@ -47,71 +47,141 @@ void main() { check(await db.getGlobalSettings()).themeSetting.equals(ThemeSetting.dark); }); - test('BoolGlobalSettings get ignores unknown names', () async { - await db.into(db.boolGlobalSettings) - .insert(BoolGlobalSettingRow(name: 'nonsense', value: true)); - check(await db.getBoolGlobalSettings()).isEmpty(); - - final setting = BoolGlobalSetting.placeholderIgnore; - await db.into(db.boolGlobalSettings) - .insert(BoolGlobalSettingRow(name: setting.name, value: true)); - check(await db.getBoolGlobalSettings()) - .deepEquals({setting: true}); - }); + group('BoolGlobalSettings', () { + test('get ignores unknown names', () async { + await db.into(db.boolGlobalSettings) + .insert(BoolGlobalSettingRow(name: 'nonsense', value: true)); + check(await db.getBoolGlobalSettings()).isEmpty(); + + final setting = BoolGlobalSetting.placeholderIgnore; + await db.into(db.boolGlobalSettings) + .insert(BoolGlobalSettingRow(name: setting.name, value: true)); + check(await db.getBoolGlobalSettings()) + .deepEquals({setting: true}); + }); - test('BoolGlobalSettings insert, then get', () async { - check(await db.getBoolGlobalSettings()).isEmpty(); + test('insert, then get', () async { + check(await db.getBoolGlobalSettings()).isEmpty(); - // As in doSetBoolGlobalSetting for `value` non-null. - final setting = BoolGlobalSetting.placeholderIgnore; - await db.into(db.boolGlobalSettings).insertOnConflictUpdate( - BoolGlobalSettingRow(name: setting.name, value: true)); - check(await db.getBoolGlobalSettings()) - .deepEquals({setting: true}); - check(await db.select(db.boolGlobalSettings).get()).length.equals(1); - }); + // As in doSetBoolGlobalSetting for `value` non-null. + final setting = BoolGlobalSetting.placeholderIgnore; + await db.into(db.boolGlobalSettings).insertOnConflictUpdate( + BoolGlobalSettingRow(name: setting.name, value: true)); + check(await db.getBoolGlobalSettings()) + .deepEquals({setting: true}); + check(await db.select(db.boolGlobalSettings).get()).length.equals(1); + }); - test('BoolGlobalSettings delete, then get', () async { - final setting = BoolGlobalSetting.placeholderIgnore; - await db.into(db.boolGlobalSettings).insertOnConflictUpdate( - BoolGlobalSettingRow(name: setting.name, value: true)); - check(await db.getBoolGlobalSettings()) - .deepEquals({setting: true}); - - // As in doSetBoolGlobalSetting for `value` null. - final query = db.delete(db.boolGlobalSettings) - ..where((r) => r.name.equals(setting.name)); - await query.go(); - check(await db.getBoolGlobalSettings()).isEmpty(); - check(await db.select(db.boolGlobalSettings).get()).isEmpty(); - }); + test('delete, then get', () async { + final setting = BoolGlobalSetting.placeholderIgnore; + await db.into(db.boolGlobalSettings).insertOnConflictUpdate( + BoolGlobalSettingRow(name: setting.name, value: true)); + check(await db.getBoolGlobalSettings()) + .deepEquals({setting: true}); + + // As in doSetBoolGlobalSetting for `value` null. + final query = db.delete(db.boolGlobalSettings) + ..where((r) => r.name.equals(setting.name)); + await query.go(); + check(await db.getBoolGlobalSettings()).isEmpty(); + check(await db.select(db.boolGlobalSettings).get()).isEmpty(); + }); + + test('insert replaces', () async { + final setting = BoolGlobalSetting.placeholderIgnore; + await db.into(db.boolGlobalSettings).insertOnConflictUpdate( + BoolGlobalSettingRow(name: setting.name, value: true)); + check(await db.getBoolGlobalSettings()) + .deepEquals({setting: true}); + + // As in doSetBoolGlobalSetting for `value` non-null. + await db.into(db.boolGlobalSettings).insertOnConflictUpdate( + BoolGlobalSettingRow(name: setting.name, value: false)); + check(await db.getBoolGlobalSettings()) + .deepEquals({setting: false}); + check(await db.select(db.boolGlobalSettings).get()).length.equals(1); + }); - test('BoolGlobalSettings insert replaces', () async { - final setting = BoolGlobalSetting.placeholderIgnore; - await db.into(db.boolGlobalSettings).insertOnConflictUpdate( - BoolGlobalSettingRow(name: setting.name, value: true)); - check(await db.getBoolGlobalSettings()) - .deepEquals({setting: true}); - - // As in doSetBoolGlobalSetting for `value` non-null. - await db.into(db.boolGlobalSettings).insertOnConflictUpdate( - BoolGlobalSettingRow(name: setting.name, value: false)); - check(await db.getBoolGlobalSettings()) - .deepEquals({setting: false}); - check(await db.select(db.boolGlobalSettings).get()).length.equals(1); + test('delete is idempotent', () async { + check(await db.getBoolGlobalSettings()).isEmpty(); + + // As in doSetBoolGlobalSetting for `value` null. + final setting = BoolGlobalSetting.placeholderIgnore; + final query = db.delete(db.boolGlobalSettings) + ..where((r) => r.name.equals(setting.name)); + await query.go(); + // (No error occurred, even though there was nothing to delete.) + check(await db.getBoolGlobalSettings()).isEmpty(); + check(await db.select(db.boolGlobalSettings).get()).isEmpty(); + }); }); - test('BoolGlobalSettings delete is idempotent', () async { - check(await db.getBoolGlobalSettings()).isEmpty(); - - // As in doSetBoolGlobalSetting for `value` null. - final setting = BoolGlobalSetting.placeholderIgnore; - final query = db.delete(db.boolGlobalSettings) - ..where((r) => r.name.equals(setting.name)); - await query.go(); - // (No error occurred, even though there was nothing to delete.) - check(await db.getBoolGlobalSettings()).isEmpty(); - check(await db.select(db.boolGlobalSettings).get()).isEmpty(); + group('IntGlobalSettings', () { + test('get ignores unknown names', () async { + await db.into(db.intGlobalSettings) + .insert(IntGlobalSettingRow(name: 'nonsense', value: 1)); + check(await db.getIntGlobalSettings()).isEmpty(); + + final setting = IntGlobalSetting.lastVisitedAccountId; + await db.into(db.intGlobalSettings) + .insert(IntGlobalSettingRow(name: setting.name, value: 1)); + check(await db.getIntGlobalSettings()) + .deepEquals({setting: 1}); + }); + + test('insert, then get', () async { + check(await db.getIntGlobalSettings()).isEmpty(); + + // As in doSetIntGlobalSetting for `value` non-null. + final setting = IntGlobalSetting.lastVisitedAccountId; + await db.into(db.intGlobalSettings).insertOnConflictUpdate( + IntGlobalSettingRow(name: setting.name, value: 1)); + check(await db.getIntGlobalSettings()) + .deepEquals({setting: 1}); + check(await db.select(db.intGlobalSettings).get()).length.equals(1); + }); + + test('delete, then get', () async { + final setting = IntGlobalSetting.lastVisitedAccountId; + await db.into(db.intGlobalSettings).insertOnConflictUpdate( + IntGlobalSettingRow(name: setting.name, value: 1)); + check(await db.getIntGlobalSettings()).deepEquals({setting: 1}); + + // As in doSetIntGlobalSetting for `value` null. + final query = db.delete(db.intGlobalSettings) + ..where((r) => r.name.equals(setting.name)); + await query.go(); + check(await db.getIntGlobalSettings()).isEmpty(); + check(await db.select(db.intGlobalSettings).get()).isEmpty(); + }); + + test('insert replaces', () async { + final setting = IntGlobalSetting.lastVisitedAccountId; + await db.into(db.intGlobalSettings).insertOnConflictUpdate( + IntGlobalSettingRow(name: setting.name, value: 1)); + check(await db.getIntGlobalSettings()) + .deepEquals({setting: 1}); + + // As in doSetIntGlobalSetting for `value` non-null. + await db.into(db.intGlobalSettings).insertOnConflictUpdate( + IntGlobalSettingRow(name: setting.name, value: 2)); + check(await db.getIntGlobalSettings()) + .deepEquals({setting: 2}); + check(await db.select(db.intGlobalSettings).get()).length.equals(1); + }); + + test('delete is idempotent', () async { + check(await db.getIntGlobalSettings()).isEmpty(); + + // As in doSetIntGlobalSetting for `value` null. + final setting = IntGlobalSetting.lastVisitedAccountId; + final query = db.delete(db.intGlobalSettings) + ..where((r) => r.name.equals(setting.name)); + await query.go(); + // (No error occurred, even though there was nothing to delete.) + check(await db.getIntGlobalSettings()).isEmpty(); + check(await db.select(db.intGlobalSettings).get()).isEmpty(); + }); }); test('create account', () async { diff --git a/test/model/schemas/drift_schema_v10.json b/test/model/schemas/drift_schema_v10.json new file mode 100644 index 0000000000..9a9a884263 --- /dev/null +++ b/test/model/schemas/drift_schema_v10.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"global_settings","was_declared_in_moor":false,"columns":[{"name":"theme_setting","getter_name":"themeSetting","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ThemeSetting.values)","dart_type_name":"ThemeSetting"}},{"name":"browser_preference","getter_name":"browserPreference","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BrowserPreference.values)","dart_type_name":"BrowserPreference"}},{"name":"visit_first_unread","getter_name":"visitFirstUnread","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(VisitFirstUnreadSetting.values)","dart_type_name":"VisitFirstUnreadSetting"}},{"name":"mark_read_on_scroll","getter_name":"markReadOnScroll","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(MarkReadOnScrollSetting.values)","dart_type_name":"MarkReadOnScrollSetting"}},{"name":"legacy_upgrade_state","getter_name":"legacyUpgradeState","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(LegacyUpgradeState.values)","dart_type_name":"LegacyUpgradeState"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"bool_global_settings","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"value","getter_name":"value","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"value\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"value\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["name"]}},{"id":2,"references":[],"type":"table","data":{"name":"int_global_settings","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"value","getter_name":"value","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["name"]}},{"id":3,"references":[],"type":"table","data":{"name":"accounts","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"realm_url","getter_name":"realmUrl","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const UriConverter()","dart_type_name":"Uri"}},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"api_key","getter_name":"apiKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_version","getter_name":"zulipVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_merge_base","getter_name":"zulipMergeBase","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_feature_level","getter_name":"zulipFeatureLevel","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"acked_push_token","getter_name":"ackedPushToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"unique_keys":[["realm_url","user_id"],["realm_url","email"]]}}]} \ No newline at end of file diff --git a/test/model/schemas/schema.dart b/test/model/schemas/schema.dart index 413b4408c4..76573e4997 100644 --- a/test/model/schemas/schema.dart +++ b/test/model/schemas/schema.dart @@ -12,6 +12,7 @@ import 'schema_v6.dart' as v6; import 'schema_v7.dart' as v7; import 'schema_v8.dart' as v8; import 'schema_v9.dart' as v9; +import 'schema_v10.dart' as v10; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -35,10 +36,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v8.DatabaseAtV8(db); case 9: return v9.DatabaseAtV9(db); + case 10: + return v10.DatabaseAtV10(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9]; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; } diff --git a/test/model/schemas/schema_v10.dart b/test/model/schemas/schema_v10.dart new file mode 100644 index 0000000000..4de2d2b356 --- /dev/null +++ b/test/model/schemas/schema_v10.dart @@ -0,0 +1,1199 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class GlobalSettings extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GlobalSettings(this.attachedDatabase, [this._alias]); + late final GeneratedColumn themeSetting = GeneratedColumn( + 'theme_setting', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn browserPreference = + GeneratedColumn( + 'browser_preference', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visitFirstUnread = GeneratedColumn( + 'visit_first_unread', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn markReadOnScroll = GeneratedColumn( + 'mark_read_on_scroll', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn legacyUpgradeState = + GeneratedColumn( + 'legacy_upgrade_state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + themeSetting, + browserPreference, + visitFirstUnread, + markReadOnScroll, + legacyUpgradeState, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'global_settings'; + @override + Set get $primaryKey => const {}; + @override + GlobalSettingsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GlobalSettingsData( + themeSetting: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}theme_setting'], + ), + browserPreference: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}browser_preference'], + ), + visitFirstUnread: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}visit_first_unread'], + ), + markReadOnScroll: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}mark_read_on_scroll'], + ), + legacyUpgradeState: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}legacy_upgrade_state'], + ), + ); + } + + @override + GlobalSettings createAlias(String alias) { + return GlobalSettings(attachedDatabase, alias); + } +} + +class GlobalSettingsData extends DataClass + implements Insertable { + final String? themeSetting; + final String? browserPreference; + final String? visitFirstUnread; + final String? markReadOnScroll; + final String? legacyUpgradeState; + const GlobalSettingsData({ + this.themeSetting, + this.browserPreference, + this.visitFirstUnread, + this.markReadOnScroll, + this.legacyUpgradeState, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || themeSetting != null) { + map['theme_setting'] = Variable(themeSetting); + } + if (!nullToAbsent || browserPreference != null) { + map['browser_preference'] = Variable(browserPreference); + } + if (!nullToAbsent || visitFirstUnread != null) { + map['visit_first_unread'] = Variable(visitFirstUnread); + } + if (!nullToAbsent || markReadOnScroll != null) { + map['mark_read_on_scroll'] = Variable(markReadOnScroll); + } + if (!nullToAbsent || legacyUpgradeState != null) { + map['legacy_upgrade_state'] = Variable(legacyUpgradeState); + } + return map; + } + + GlobalSettingsCompanion toCompanion(bool nullToAbsent) { + return GlobalSettingsCompanion( + themeSetting: themeSetting == null && nullToAbsent + ? const Value.absent() + : Value(themeSetting), + browserPreference: browserPreference == null && nullToAbsent + ? const Value.absent() + : Value(browserPreference), + visitFirstUnread: visitFirstUnread == null && nullToAbsent + ? const Value.absent() + : Value(visitFirstUnread), + markReadOnScroll: markReadOnScroll == null && nullToAbsent + ? const Value.absent() + : Value(markReadOnScroll), + legacyUpgradeState: legacyUpgradeState == null && nullToAbsent + ? const Value.absent() + : Value(legacyUpgradeState), + ); + } + + factory GlobalSettingsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GlobalSettingsData( + themeSetting: serializer.fromJson(json['themeSetting']), + browserPreference: serializer.fromJson( + json['browserPreference'], + ), + visitFirstUnread: serializer.fromJson(json['visitFirstUnread']), + markReadOnScroll: serializer.fromJson(json['markReadOnScroll']), + legacyUpgradeState: serializer.fromJson( + json['legacyUpgradeState'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'themeSetting': serializer.toJson(themeSetting), + 'browserPreference': serializer.toJson(browserPreference), + 'visitFirstUnread': serializer.toJson(visitFirstUnread), + 'markReadOnScroll': serializer.toJson(markReadOnScroll), + 'legacyUpgradeState': serializer.toJson(legacyUpgradeState), + }; + } + + GlobalSettingsData copyWith({ + Value themeSetting = const Value.absent(), + Value browserPreference = const Value.absent(), + Value visitFirstUnread = const Value.absent(), + Value markReadOnScroll = const Value.absent(), + Value legacyUpgradeState = const Value.absent(), + }) => GlobalSettingsData( + themeSetting: themeSetting.present ? themeSetting.value : this.themeSetting, + browserPreference: browserPreference.present + ? browserPreference.value + : this.browserPreference, + visitFirstUnread: visitFirstUnread.present + ? visitFirstUnread.value + : this.visitFirstUnread, + markReadOnScroll: markReadOnScroll.present + ? markReadOnScroll.value + : this.markReadOnScroll, + legacyUpgradeState: legacyUpgradeState.present + ? legacyUpgradeState.value + : this.legacyUpgradeState, + ); + GlobalSettingsData copyWithCompanion(GlobalSettingsCompanion data) { + return GlobalSettingsData( + themeSetting: data.themeSetting.present + ? data.themeSetting.value + : this.themeSetting, + browserPreference: data.browserPreference.present + ? data.browserPreference.value + : this.browserPreference, + visitFirstUnread: data.visitFirstUnread.present + ? data.visitFirstUnread.value + : this.visitFirstUnread, + markReadOnScroll: data.markReadOnScroll.present + ? data.markReadOnScroll.value + : this.markReadOnScroll, + legacyUpgradeState: data.legacyUpgradeState.present + ? data.legacyUpgradeState.value + : this.legacyUpgradeState, + ); + } + + @override + String toString() { + return (StringBuffer('GlobalSettingsData(') + ..write('themeSetting: $themeSetting, ') + ..write('browserPreference: $browserPreference, ') + ..write('visitFirstUnread: $visitFirstUnread, ') + ..write('markReadOnScroll: $markReadOnScroll, ') + ..write('legacyUpgradeState: $legacyUpgradeState') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + themeSetting, + browserPreference, + visitFirstUnread, + markReadOnScroll, + legacyUpgradeState, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GlobalSettingsData && + other.themeSetting == this.themeSetting && + other.browserPreference == this.browserPreference && + other.visitFirstUnread == this.visitFirstUnread && + other.markReadOnScroll == this.markReadOnScroll && + other.legacyUpgradeState == this.legacyUpgradeState); +} + +class GlobalSettingsCompanion extends UpdateCompanion { + final Value themeSetting; + final Value browserPreference; + final Value visitFirstUnread; + final Value markReadOnScroll; + final Value legacyUpgradeState; + final Value rowid; + const GlobalSettingsCompanion({ + this.themeSetting = const Value.absent(), + this.browserPreference = const Value.absent(), + this.visitFirstUnread = const Value.absent(), + this.markReadOnScroll = const Value.absent(), + this.legacyUpgradeState = const Value.absent(), + this.rowid = const Value.absent(), + }); + GlobalSettingsCompanion.insert({ + this.themeSetting = const Value.absent(), + this.browserPreference = const Value.absent(), + this.visitFirstUnread = const Value.absent(), + this.markReadOnScroll = const Value.absent(), + this.legacyUpgradeState = const Value.absent(), + this.rowid = const Value.absent(), + }); + static Insertable custom({ + Expression? themeSetting, + Expression? browserPreference, + Expression? visitFirstUnread, + Expression? markReadOnScroll, + Expression? legacyUpgradeState, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (themeSetting != null) 'theme_setting': themeSetting, + if (browserPreference != null) 'browser_preference': browserPreference, + if (visitFirstUnread != null) 'visit_first_unread': visitFirstUnread, + if (markReadOnScroll != null) 'mark_read_on_scroll': markReadOnScroll, + if (legacyUpgradeState != null) + 'legacy_upgrade_state': legacyUpgradeState, + if (rowid != null) 'rowid': rowid, + }); + } + + GlobalSettingsCompanion copyWith({ + Value? themeSetting, + Value? browserPreference, + Value? visitFirstUnread, + Value? markReadOnScroll, + Value? legacyUpgradeState, + Value? rowid, + }) { + return GlobalSettingsCompanion( + themeSetting: themeSetting ?? this.themeSetting, + browserPreference: browserPreference ?? this.browserPreference, + visitFirstUnread: visitFirstUnread ?? this.visitFirstUnread, + markReadOnScroll: markReadOnScroll ?? this.markReadOnScroll, + legacyUpgradeState: legacyUpgradeState ?? this.legacyUpgradeState, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (themeSetting.present) { + map['theme_setting'] = Variable(themeSetting.value); + } + if (browserPreference.present) { + map['browser_preference'] = Variable(browserPreference.value); + } + if (visitFirstUnread.present) { + map['visit_first_unread'] = Variable(visitFirstUnread.value); + } + if (markReadOnScroll.present) { + map['mark_read_on_scroll'] = Variable(markReadOnScroll.value); + } + if (legacyUpgradeState.present) { + map['legacy_upgrade_state'] = Variable(legacyUpgradeState.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GlobalSettingsCompanion(') + ..write('themeSetting: $themeSetting, ') + ..write('browserPreference: $browserPreference, ') + ..write('visitFirstUnread: $visitFirstUnread, ') + ..write('markReadOnScroll: $markReadOnScroll, ') + ..write('legacyUpgradeState: $legacyUpgradeState, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class BoolGlobalSettings extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + BoolGlobalSettings(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("value" IN (0, 1))', + ), + ); + @override + List get $columns => [name, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'bool_global_settings'; + @override + Set get $primaryKey => {name}; + @override + BoolGlobalSettingsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BoolGlobalSettingsData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + BoolGlobalSettings createAlias(String alias) { + return BoolGlobalSettings(attachedDatabase, alias); + } +} + +class BoolGlobalSettingsData extends DataClass + implements Insertable { + final String name; + final bool value; + const BoolGlobalSettingsData({required this.name, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['value'] = Variable(value); + return map; + } + + BoolGlobalSettingsCompanion toCompanion(bool nullToAbsent) { + return BoolGlobalSettingsCompanion(name: Value(name), value: Value(value)); + } + + factory BoolGlobalSettingsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BoolGlobalSettingsData( + name: serializer.fromJson(json['name']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'value': serializer.toJson(value), + }; + } + + BoolGlobalSettingsData copyWith({String? name, bool? value}) => + BoolGlobalSettingsData( + name: name ?? this.name, + value: value ?? this.value, + ); + BoolGlobalSettingsData copyWithCompanion(BoolGlobalSettingsCompanion data) { + return BoolGlobalSettingsData( + name: data.name.present ? data.name.value : this.name, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('BoolGlobalSettingsData(') + ..write('name: $name, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BoolGlobalSettingsData && + other.name == this.name && + other.value == this.value); +} + +class BoolGlobalSettingsCompanion + extends UpdateCompanion { + final Value name; + final Value value; + final Value rowid; + const BoolGlobalSettingsCompanion({ + this.name = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + BoolGlobalSettingsCompanion.insert({ + required String name, + required bool value, + this.rowid = const Value.absent(), + }) : name = Value(name), + value = Value(value); + static Insertable custom({ + Expression? name, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + BoolGlobalSettingsCompanion copyWith({ + Value? name, + Value? value, + Value? rowid, + }) { + return BoolGlobalSettingsCompanion( + name: name ?? this.name, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BoolGlobalSettingsCompanion(') + ..write('name: $name, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class IntGlobalSettings extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + IntGlobalSettings(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [name, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'int_global_settings'; + @override + Set get $primaryKey => {name}; + @override + IntGlobalSettingsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return IntGlobalSettingsData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + IntGlobalSettings createAlias(String alias) { + return IntGlobalSettings(attachedDatabase, alias); + } +} + +class IntGlobalSettingsData extends DataClass + implements Insertable { + final String name; + final int value; + const IntGlobalSettingsData({required this.name, required this.value}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['value'] = Variable(value); + return map; + } + + IntGlobalSettingsCompanion toCompanion(bool nullToAbsent) { + return IntGlobalSettingsCompanion(name: Value(name), value: Value(value)); + } + + factory IntGlobalSettingsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return IntGlobalSettingsData( + name: serializer.fromJson(json['name']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'value': serializer.toJson(value), + }; + } + + IntGlobalSettingsData copyWith({String? name, int? value}) => + IntGlobalSettingsData( + name: name ?? this.name, + value: value ?? this.value, + ); + IntGlobalSettingsData copyWithCompanion(IntGlobalSettingsCompanion data) { + return IntGlobalSettingsData( + name: data.name.present ? data.name.value : this.name, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('IntGlobalSettingsData(') + ..write('name: $name, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, value); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is IntGlobalSettingsData && + other.name == this.name && + other.value == this.value); +} + +class IntGlobalSettingsCompanion + extends UpdateCompanion { + final Value name; + final Value value; + final Value rowid; + const IntGlobalSettingsCompanion({ + this.name = const Value.absent(), + this.value = const Value.absent(), + this.rowid = const Value.absent(), + }); + IntGlobalSettingsCompanion.insert({ + required String name, + required int value, + this.rowid = const Value.absent(), + }) : name = Value(name), + value = Value(value); + static Insertable custom({ + Expression? name, + Expression? value, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (value != null) 'value': value, + if (rowid != null) 'rowid': rowid, + }); + } + + IntGlobalSettingsCompanion copyWith({ + Value? name, + Value? value, + Value? rowid, + }) { + return IntGlobalSettingsCompanion( + name: name ?? this.name, + value: value ?? this.value, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('IntGlobalSettingsCompanion(') + ..write('name: $name, ') + ..write('value: $value, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Accounts extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Accounts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + late final GeneratedColumn realmUrl = GeneratedColumn( + 'realm_url', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn apiKey = GeneratedColumn( + 'api_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn zulipVersion = GeneratedColumn( + 'zulip_version', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn zulipMergeBase = GeneratedColumn( + 'zulip_merge_base', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn zulipFeatureLevel = GeneratedColumn( + 'zulip_feature_level', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn ackedPushToken = GeneratedColumn( + 'acked_push_token', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + realmUrl, + userId, + email, + apiKey, + zulipVersion, + zulipMergeBase, + zulipFeatureLevel, + ackedPushToken, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'accounts'; + @override + Set get $primaryKey => {id}; + @override + List> get uniqueKeys => [ + {realmUrl, userId}, + {realmUrl, email}, + ]; + @override + AccountsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AccountsData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + realmUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}realm_url'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_id'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + apiKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}api_key'], + )!, + zulipVersion: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}zulip_version'], + )!, + zulipMergeBase: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}zulip_merge_base'], + ), + zulipFeatureLevel: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}zulip_feature_level'], + )!, + ackedPushToken: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}acked_push_token'], + ), + ); + } + + @override + Accounts createAlias(String alias) { + return Accounts(attachedDatabase, alias); + } +} + +class AccountsData extends DataClass implements Insertable { + final int id; + final String realmUrl; + final int userId; + final String email; + final String apiKey; + final String zulipVersion; + final String? zulipMergeBase; + final int zulipFeatureLevel; + final String? ackedPushToken; + const AccountsData({ + required this.id, + required this.realmUrl, + required this.userId, + required this.email, + required this.apiKey, + required this.zulipVersion, + this.zulipMergeBase, + required this.zulipFeatureLevel, + this.ackedPushToken, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['realm_url'] = Variable(realmUrl); + map['user_id'] = Variable(userId); + map['email'] = Variable(email); + map['api_key'] = Variable(apiKey); + map['zulip_version'] = Variable(zulipVersion); + if (!nullToAbsent || zulipMergeBase != null) { + map['zulip_merge_base'] = Variable(zulipMergeBase); + } + map['zulip_feature_level'] = Variable(zulipFeatureLevel); + if (!nullToAbsent || ackedPushToken != null) { + map['acked_push_token'] = Variable(ackedPushToken); + } + return map; + } + + AccountsCompanion toCompanion(bool nullToAbsent) { + return AccountsCompanion( + id: Value(id), + realmUrl: Value(realmUrl), + userId: Value(userId), + email: Value(email), + apiKey: Value(apiKey), + zulipVersion: Value(zulipVersion), + zulipMergeBase: zulipMergeBase == null && nullToAbsent + ? const Value.absent() + : Value(zulipMergeBase), + zulipFeatureLevel: Value(zulipFeatureLevel), + ackedPushToken: ackedPushToken == null && nullToAbsent + ? const Value.absent() + : Value(ackedPushToken), + ); + } + + factory AccountsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AccountsData( + id: serializer.fromJson(json['id']), + realmUrl: serializer.fromJson(json['realmUrl']), + userId: serializer.fromJson(json['userId']), + email: serializer.fromJson(json['email']), + apiKey: serializer.fromJson(json['apiKey']), + zulipVersion: serializer.fromJson(json['zulipVersion']), + zulipMergeBase: serializer.fromJson(json['zulipMergeBase']), + zulipFeatureLevel: serializer.fromJson(json['zulipFeatureLevel']), + ackedPushToken: serializer.fromJson(json['ackedPushToken']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'realmUrl': serializer.toJson(realmUrl), + 'userId': serializer.toJson(userId), + 'email': serializer.toJson(email), + 'apiKey': serializer.toJson(apiKey), + 'zulipVersion': serializer.toJson(zulipVersion), + 'zulipMergeBase': serializer.toJson(zulipMergeBase), + 'zulipFeatureLevel': serializer.toJson(zulipFeatureLevel), + 'ackedPushToken': serializer.toJson(ackedPushToken), + }; + } + + AccountsData copyWith({ + int? id, + String? realmUrl, + int? userId, + String? email, + String? apiKey, + String? zulipVersion, + Value zulipMergeBase = const Value.absent(), + int? zulipFeatureLevel, + Value ackedPushToken = const Value.absent(), + }) => AccountsData( + id: id ?? this.id, + realmUrl: realmUrl ?? this.realmUrl, + userId: userId ?? this.userId, + email: email ?? this.email, + apiKey: apiKey ?? this.apiKey, + zulipVersion: zulipVersion ?? this.zulipVersion, + zulipMergeBase: zulipMergeBase.present + ? zulipMergeBase.value + : this.zulipMergeBase, + zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel, + ackedPushToken: ackedPushToken.present + ? ackedPushToken.value + : this.ackedPushToken, + ); + AccountsData copyWithCompanion(AccountsCompanion data) { + return AccountsData( + id: data.id.present ? data.id.value : this.id, + realmUrl: data.realmUrl.present ? data.realmUrl.value : this.realmUrl, + userId: data.userId.present ? data.userId.value : this.userId, + email: data.email.present ? data.email.value : this.email, + apiKey: data.apiKey.present ? data.apiKey.value : this.apiKey, + zulipVersion: data.zulipVersion.present + ? data.zulipVersion.value + : this.zulipVersion, + zulipMergeBase: data.zulipMergeBase.present + ? data.zulipMergeBase.value + : this.zulipMergeBase, + zulipFeatureLevel: data.zulipFeatureLevel.present + ? data.zulipFeatureLevel.value + : this.zulipFeatureLevel, + ackedPushToken: data.ackedPushToken.present + ? data.ackedPushToken.value + : this.ackedPushToken, + ); + } + + @override + String toString() { + return (StringBuffer('AccountsData(') + ..write('id: $id, ') + ..write('realmUrl: $realmUrl, ') + ..write('userId: $userId, ') + ..write('email: $email, ') + ..write('apiKey: $apiKey, ') + ..write('zulipVersion: $zulipVersion, ') + ..write('zulipMergeBase: $zulipMergeBase, ') + ..write('zulipFeatureLevel: $zulipFeatureLevel, ') + ..write('ackedPushToken: $ackedPushToken') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + realmUrl, + userId, + email, + apiKey, + zulipVersion, + zulipMergeBase, + zulipFeatureLevel, + ackedPushToken, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AccountsData && + other.id == this.id && + other.realmUrl == this.realmUrl && + other.userId == this.userId && + other.email == this.email && + other.apiKey == this.apiKey && + other.zulipVersion == this.zulipVersion && + other.zulipMergeBase == this.zulipMergeBase && + other.zulipFeatureLevel == this.zulipFeatureLevel && + other.ackedPushToken == this.ackedPushToken); +} + +class AccountsCompanion extends UpdateCompanion { + final Value id; + final Value realmUrl; + final Value userId; + final Value email; + final Value apiKey; + final Value zulipVersion; + final Value zulipMergeBase; + final Value zulipFeatureLevel; + final Value ackedPushToken; + const AccountsCompanion({ + this.id = const Value.absent(), + this.realmUrl = const Value.absent(), + this.userId = const Value.absent(), + this.email = const Value.absent(), + this.apiKey = const Value.absent(), + this.zulipVersion = const Value.absent(), + this.zulipMergeBase = const Value.absent(), + this.zulipFeatureLevel = const Value.absent(), + this.ackedPushToken = const Value.absent(), + }); + AccountsCompanion.insert({ + this.id = const Value.absent(), + required String realmUrl, + required int userId, + required String email, + required String apiKey, + required String zulipVersion, + this.zulipMergeBase = const Value.absent(), + required int zulipFeatureLevel, + this.ackedPushToken = const Value.absent(), + }) : realmUrl = Value(realmUrl), + userId = Value(userId), + email = Value(email), + apiKey = Value(apiKey), + zulipVersion = Value(zulipVersion), + zulipFeatureLevel = Value(zulipFeatureLevel); + static Insertable custom({ + Expression? id, + Expression? realmUrl, + Expression? userId, + Expression? email, + Expression? apiKey, + Expression? zulipVersion, + Expression? zulipMergeBase, + Expression? zulipFeatureLevel, + Expression? ackedPushToken, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (realmUrl != null) 'realm_url': realmUrl, + if (userId != null) 'user_id': userId, + if (email != null) 'email': email, + if (apiKey != null) 'api_key': apiKey, + if (zulipVersion != null) 'zulip_version': zulipVersion, + if (zulipMergeBase != null) 'zulip_merge_base': zulipMergeBase, + if (zulipFeatureLevel != null) 'zulip_feature_level': zulipFeatureLevel, + if (ackedPushToken != null) 'acked_push_token': ackedPushToken, + }); + } + + AccountsCompanion copyWith({ + Value? id, + Value? realmUrl, + Value? userId, + Value? email, + Value? apiKey, + Value? zulipVersion, + Value? zulipMergeBase, + Value? zulipFeatureLevel, + Value? ackedPushToken, + }) { + return AccountsCompanion( + id: id ?? this.id, + realmUrl: realmUrl ?? this.realmUrl, + userId: userId ?? this.userId, + email: email ?? this.email, + apiKey: apiKey ?? this.apiKey, + zulipVersion: zulipVersion ?? this.zulipVersion, + zulipMergeBase: zulipMergeBase ?? this.zulipMergeBase, + zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel, + ackedPushToken: ackedPushToken ?? this.ackedPushToken, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (realmUrl.present) { + map['realm_url'] = Variable(realmUrl.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (apiKey.present) { + map['api_key'] = Variable(apiKey.value); + } + if (zulipVersion.present) { + map['zulip_version'] = Variable(zulipVersion.value); + } + if (zulipMergeBase.present) { + map['zulip_merge_base'] = Variable(zulipMergeBase.value); + } + if (zulipFeatureLevel.present) { + map['zulip_feature_level'] = Variable(zulipFeatureLevel.value); + } + if (ackedPushToken.present) { + map['acked_push_token'] = Variable(ackedPushToken.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AccountsCompanion(') + ..write('id: $id, ') + ..write('realmUrl: $realmUrl, ') + ..write('userId: $userId, ') + ..write('email: $email, ') + ..write('apiKey: $apiKey, ') + ..write('zulipVersion: $zulipVersion, ') + ..write('zulipMergeBase: $zulipMergeBase, ') + ..write('zulipFeatureLevel: $zulipFeatureLevel, ') + ..write('ackedPushToken: $ackedPushToken') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV10 extends GeneratedDatabase { + DatabaseAtV10(QueryExecutor e) : super(e); + late final GlobalSettings globalSettings = GlobalSettings(this); + late final BoolGlobalSettings boolGlobalSettings = BoolGlobalSettings(this); + late final IntGlobalSettings intGlobalSettings = IntGlobalSettings(this); + late final Accounts accounts = Accounts(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + globalSettings, + boolGlobalSettings, + intGlobalSettings, + accounts, + ]; + @override + int get schemaVersion => 10; +} diff --git a/test/model/settings_test.dart b/test/model/settings_test.dart index b4842ecd04..f8774a8a79 100644 --- a/test/model/settings_test.dart +++ b/test/model/settings_test.dart @@ -126,4 +126,40 @@ void main() { assert(!BoolGlobalSetting.placeholderIgnore.default_); }); }); + + group('getInt/setInt', () { + test('get from initial load', () { + final globalSettings = eg.globalStore(intGlobalSettings: { + IntGlobalSetting.lastVisitedAccountId: 1, + }).settings; + check(globalSettings).getInt(IntGlobalSetting.lastVisitedAccountId) + .equals(1); + }); + + test('set, get', () async { + final globalSettings = eg.globalStore(intGlobalSettings: {}).settings; + check(globalSettings).getInt(IntGlobalSetting.lastVisitedAccountId) + .isNull(); + + await globalSettings.setInt(IntGlobalSetting.lastVisitedAccountId, 1); + check(globalSettings).getInt(IntGlobalSetting.lastVisitedAccountId) + .equals(1); + + await globalSettings.setInt(IntGlobalSetting.lastVisitedAccountId, 100); + check(globalSettings).getInt(IntGlobalSetting.lastVisitedAccountId) + .equals(100); + }); + + test('set to null -> get returns null', () async { + final globalSettings = eg.globalStore(intGlobalSettings: { + IntGlobalSetting.lastVisitedAccountId: 1, + }).settings; + check(globalSettings).getInt(IntGlobalSetting.lastVisitedAccountId) + .equals(1); + + await globalSettings.setInt(IntGlobalSetting.lastVisitedAccountId, null); + check(globalSettings).getInt(IntGlobalSetting.lastVisitedAccountId) + .isNull(); + }); + }); } diff --git a/test/model/store_checks.dart b/test/model/store_checks.dart index b56732ef4f..487eb98f4b 100644 --- a/test/model/store_checks.dart +++ b/test/model/store_checks.dart @@ -36,6 +36,7 @@ extension GlobalSettingsStoreChecks on Subject { Subject get visitFirstUnread => has((x) => x.visitFirstUnread, 'visitFirstUnread'); Subject get markReadOnScroll => has((x) => x.markReadOnScroll, 'markReadOnScroll'); Subject getBool(BoolGlobalSetting setting) => has((x) => x.getBool(setting), 'getBool(${setting.name}'); + Subject getInt(IntGlobalSetting setting) => has((x) => x.getInt(setting), 'getInt(${setting.name}'); } extension GlobalStoreChecks on Subject { diff --git a/test/model/test_store.dart b/test/model/test_store.dart index b25f52ba46..54d36b4b7d 100644 --- a/test/model/test_store.dart +++ b/test/model/test_store.dart @@ -73,6 +73,11 @@ class _TestGlobalStoreBackend implements GlobalStoreBackend { Future doSetBoolGlobalSetting(BoolGlobalSetting setting, bool? value) async { // Nothing to do. } + + @override + Future doSetIntGlobalSetting(IntGlobalSetting setting, int? value) async { + // Nothing to do. + } } mixin _DatabaseMixin on GlobalStore { @@ -146,10 +151,12 @@ class TestGlobalStore extends GlobalStore with _ApiConnectionsMixin, _DatabaseMi TestGlobalStore({ GlobalSettingsData? globalSettings, Map? boolGlobalSettings, + Map? intGlobalSettings, required super.accounts, }) : super(backend: _TestGlobalStoreBackend(), globalSettings: globalSettings ?? GlobalSettingsData(), boolGlobalSettings: boolGlobalSettings ?? {}, + intGlobalSettings: intGlobalSettings ?? {} ); final Map _initialSnapshots = {}; @@ -214,10 +221,12 @@ class UpdateMachineTestGlobalStore extends GlobalStore with _ApiConnectionsMixin UpdateMachineTestGlobalStore({ GlobalSettingsData? globalSettings, Map? boolGlobalSettings, + Map? intGlobalSettings, required super.accounts, }) : super(backend: _TestGlobalStoreBackend(), globalSettings: globalSettings ?? GlobalSettingsData(), boolGlobalSettings: boolGlobalSettings ?? {}, + intGlobalSettings: intGlobalSettings ?? {}, ); // [doLoadPerAccount] depends on the cache to prepare the API responses. diff --git a/test/notifications/open_test.dart b/test/notifications/open_test.dart index cdfd8ef361..15dd697086 100644 --- a/test/notifications/open_test.dart +++ b/test/notifications/open_test.dart @@ -10,6 +10,7 @@ import 'package:zulip/host/notifications.dart'; import 'package:zulip/model/database.dart'; import 'package:zulip/model/localizations.dart'; import 'package:zulip/model/narrow.dart'; +import 'package:zulip/model/settings.dart'; import 'package:zulip/notifications/open.dart'; import 'package:zulip/notifications/receive.dart'; import 'package:zulip/widgets/app.dart'; @@ -20,6 +21,7 @@ import 'package:zulip/widgets/page.dart'; import '../example_data.dart' as eg; import '../model/binding.dart'; import '../model/narrow_checks.dart'; +import '../model/store_checks.dart'; import '../stdlib_checks.dart'; import '../test_navigation.dart'; import '../widgets/checks.dart'; @@ -190,6 +192,8 @@ void main() { testWidgets('stream message', (tester) async { addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); await prepare(tester); await checkOpenNotification(tester, eg.selfAccount, eg.streamMessage()); }, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); @@ -197,16 +201,32 @@ void main() { testWidgets('direct message', (tester) async { addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); await prepare(tester); await checkOpenNotification(tester, eg.selfAccount, eg.dmMessage(from: eg.otherUser, to: [eg.selfUser])); }, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); + testWidgets('changes last visited account', (tester) async { + addTearDown(testBinding.reset); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); + await prepare(tester); + await checkOpenNotification(tester, eg.otherAccount, eg.streamMessage()); + check(testBinding.globalStore.settings) + .getInt(IntGlobalSetting.lastVisitedAccountId).equals(eg.otherAccount.id); + }, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); + testWidgets('account queried by realmUrl origin component', (tester) async { addTearDown(testBinding.reset); await testBinding.globalStore.add( eg.selfAccount.copyWith(realmUrl: Uri.parse('http://chat.example')), eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); await prepare(tester); await checkOpenNotification(tester, @@ -230,6 +250,8 @@ void main() { testWidgets('mismatching account', (tester) async { addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); await prepare(tester); await openNotification(tester, eg.otherAccount, eg.streamMessage()); await tester.pump(); @@ -254,6 +276,8 @@ void main() { for (final account in accounts) { await testBinding.globalStore.add(account, eg.initialSnapshot()); } + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); await prepare(tester); await checkOpenNotification(tester, accounts[0], eg.streamMessage()); @@ -265,6 +289,8 @@ void main() { testWidgets('wait for app to become ready', (tester) async { addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); await prepare(tester, early: true); final message = eg.streamMessage(); await openNotification(tester, eg.selfAccount, message); diff --git a/test/widgets/app_test.dart b/test/widgets/app_test.dart index eca2f95cab..99b7f5b528 100644 --- a/test/widgets/app_test.dart +++ b/test/widgets/app_test.dart @@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:zulip/log.dart'; import 'package:zulip/model/actions.dart'; import 'package:zulip/model/database.dart'; +import 'package:zulip/model/settings.dart'; import 'package:zulip/widgets/app.dart'; import 'package:zulip/widgets/home.dart'; import 'package:zulip/widgets/page.dart'; @@ -43,18 +44,31 @@ void main() { ]); }); - testWidgets('when have accounts, go to home page for first account', (tester) async { - // We'll need per-account data for the account that a page will be opened - // for, but not for the other account. - await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); - await testBinding.globalStore.insertAccount(eg.otherAccount.toCompanion(false)); - await prepare(tester); + group('when have accounts', () { + testWidgets('with no last account visited, go to choose account', (tester) async { + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + check(testBinding.globalStore.settings) + .getInt(IntGlobalSetting.lastVisitedAccountId).isNull(); + await prepare(tester); - check(pushedRoutes).deepEquals(>[ - (it) => it.isA() - ..accountId.equals(eg.selfAccount.id) - ..page.isA(), - ]); + check(pushedRoutes).deepEquals(>[ + (it) => it.isA().page.isA(), + ]); + }); + + testWidgets('with last account visited, go to home page for last account', (tester) async { + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.otherAccount.id); + await prepare(tester); + + check(pushedRoutes).deepEquals(>[ + (it) => it.isA() + ..accountId.equals(eg.otherAccount.id) + ..page.isA(), + ]); + }); }); }); @@ -82,6 +96,8 @@ void main() { testWidgets('push route when removing last route on stack', (tester) async { await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); await prepare(tester); // The navigator stack should contain only a home page route. @@ -99,6 +115,8 @@ void main() { testWidgets('push route when popping last route on stack', (tester) async { // Set up the loading of per-account data to fail. await testBinding.globalStore.insertAccount(eg.selfAccount.toCompanion(false)); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); testBinding.globalStore.loadPerAccountDuration = Duration.zero; testBinding.globalStore.loadPerAccountException = eg.apiExceptionUnauthorized(); await prepare(tester); @@ -133,6 +151,8 @@ void main() { const loadPerAccountDuration = Duration(seconds: 30); assert(loadPerAccountDuration > kTryAnotherAccountWaitPeriod); await testBinding.globalStore.insertAccount(eg.selfAccount.toCompanion(false)); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); testBinding.globalStore.loadPerAccountDuration = loadPerAccountDuration; testBinding.globalStore.loadPerAccountException = eg.apiExceptionUnauthorized(); await prepare(tester); @@ -282,6 +302,8 @@ void main() { addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); final pushedRoutes = >[]; final poppedRoutes = >[]; @@ -318,14 +340,63 @@ void main() { ..page.isA(); }); - group('log out', () { - Future<(Widget, Widget)> prepare(WidgetTester tester, {required Account account}) async { - await setupChooseAccountPage(tester, accounts: [account]); + group('choosing an account changes the last visited account', () { + testWidgets('first null, then changes to the chosen account', (tester) async { + addTearDown(testBinding.reset); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); - final findThreeDotsButton = find.descendant( - of: find.widgetWithText(Card, eg.selfAccount.realmUrl.toString()), + await tester.pumpWidget(ZulipApp()); + await tester.pump(); + + check(testBinding.globalStore.settings) + .getInt(IntGlobalSetting.lastVisitedAccountId).isNull(); + await tester.tap(find.text(eg.selfAccount.email)); + await tester.pump(); + check(testBinding.globalStore.settings) + .getInt(IntGlobalSetting.lastVisitedAccountId).equals(eg.selfAccount.id); + }); + + testWidgets('first non-null, then changes to the chosen account', (tester) async { + addTearDown(testBinding.reset); + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); + + await tester.pumpWidget(ZulipApp()); + await tester.pump(); + + final navigator = await ZulipApp.navigator; + unawaited(navigator.push( + MaterialWidgetRoute(page: const ChooseAccountPage()))); + await tester.pump(); + await tester.pump(); + + check(testBinding.globalStore.settings) + .getInt(IntGlobalSetting.lastVisitedAccountId).equals(eg.selfAccount.id); + await tester.tap(find.text(eg.otherAccount.email)); + await tester.pump(); + check(testBinding.globalStore.settings) + .getInt(IntGlobalSetting.lastVisitedAccountId).equals(eg.otherAccount.id); + }); + }); + + group('log out', () { + Future<(Widget, Widget)> prepare(WidgetTester tester, { + required List accounts, + required Account logoutAccount, + }) async { + assert(accounts.contains(logoutAccount)); + + await setupChooseAccountPage(tester, accounts: accounts); + + final findAccountCard = find.ancestor(of: find.text(logoutAccount.realmUrl.toString()), + matching: find.ancestor(of: find.text(logoutAccount.email), + matching: find.byType(Card))); + final findThreeDotsButton = find.descendant(of: findAccountCard, matching: find.byIcon(Icons.adaptive.more)); + await tester.scrollUntilVisible(findThreeDotsButton, 50); await tester.tap(findThreeDotsButton); await tester.pump(); await tester.tap(find.descendant( @@ -338,18 +409,49 @@ void main() { } testWidgets('user confirms logging out', (tester) async { - final (actionButton, _) = await prepare(tester, account: eg.selfAccount); + final (actionButton, _) = await prepare(tester, + accounts: [eg.selfAccount], logoutAccount: eg.selfAccount); await tester.tap(find.byWidget(actionButton)); await tester.pump(TestGlobalStore.removeAccountDuration); check(testBinding.globalStore).accounts.isEmpty(); }); testWidgets('user cancels logging out', (tester) async { - final (_, cancelButton) = await prepare(tester, account: eg.selfAccount); + final (_, cancelButton) = await prepare(tester, + accounts: [eg.selfAccount], logoutAccount: eg.selfAccount); await tester.tap(find.byWidget(cancelButton)); await tester.pumpAndSettle(); check(testBinding.globalStore).accounts.deepEquals([eg.selfAccount]); }); + + group('last visited account', () { + testWidgets('is the logged out one -> last account set to null', (tester) async { + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); + + final (actionButton, _) = await prepare(tester, + accounts: [eg.selfAccount, eg.otherAccount], logoutAccount: eg.selfAccount); + await tester.tap(find.byWidget(actionButton)); + await tester.pump(TestGlobalStore.removeAccountDuration); + check(testBinding.globalStore) + ..accounts.deepEquals([eg.otherAccount]) + ..settings.getInt(IntGlobalSetting.lastVisitedAccountId).isNull(); + }); + + testWidgets('is not the logged out one -> last account not changed', (tester) async { + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.otherAccount.id); + + final (actionButton, _) = await prepare(tester, + accounts: [eg.selfAccount, eg.otherAccount], logoutAccount: eg.selfAccount); + await tester.tap(find.byWidget(actionButton)); + await tester.pump(TestGlobalStore.removeAccountDuration); + check(testBinding.globalStore) + ..accounts.deepEquals([eg.otherAccount]) + ..settings.getInt(IntGlobalSetting.lastVisitedAccountId) + .equals(eg.otherAccount.id); + }); + }); }); }); diff --git a/test/widgets/home_test.dart b/test/widgets/home_test.dart index 5a8d3cca33..8aadffb454 100644 --- a/test/widgets/home_test.dart +++ b/test/widgets/home_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_checks/flutter_checks.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:zulip/model/actions.dart'; import 'package:zulip/model/narrow.dart'; +import 'package:zulip/model/settings.dart'; import 'package:zulip/model/store.dart'; import 'package:zulip/widgets/about_zulip.dart'; import 'package:zulip/widgets/app.dart'; @@ -277,6 +278,8 @@ void main () { pushedRoutes = []; lastPoppedRoute = null; await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver])); final store = await testBinding.globalStore.perAccount(eg.selfAccount.id); @@ -334,6 +337,8 @@ void main () { lastPoppedRoute = null; await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); await testBinding.globalStore.add(eg.otherAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver])); await tester.pump(Duration.zero); // wait for the loading page checkOnLoadingPage(); @@ -523,6 +528,8 @@ void main () { // Regression test for: https://github.com/zulip/zulip-flutter/issues/1219 addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); await tester.pumpWidget(const ZulipApp()); await tester.pump(); // wait for the loading page checkOnLoadingPage(); @@ -539,6 +546,8 @@ void main () { // Regression test for: https://github.com/zulip/zulip-flutter/issues/1219 addTearDown(testBinding.reset); await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); await tester.pumpWidget(const ZulipApp()); await tester.pump(); // wait for the loading page await tester.pump(); // wait for store diff --git a/test/widgets/login_test.dart b/test/widgets/login_test.dart index 9bc4159f72..bf550cdceb 100644 --- a/test/widgets/login_test.dart +++ b/test/widgets/login_test.dart @@ -12,6 +12,7 @@ import 'package:zulip/api/route/realm.dart'; import 'package:zulip/model/binding.dart'; import 'package:zulip/model/database.dart'; import 'package:zulip/model/localizations.dart'; +import 'package:zulip/model/settings.dart'; import 'package:zulip/widgets/app.dart'; import 'package:zulip/widgets/home.dart'; import 'package:zulip/widgets/login.dart'; @@ -262,6 +263,8 @@ void main() { testWidgets('logging into a second account', (tester) async { await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot()); + await testBinding.globalStore.settings + .setInt(IntGlobalSetting.lastVisitedAccountId, eg.selfAccount.id); final serverSettings = eg.serverSettings(); await prepare(tester, serverSettings); check(poppedRoutes).isEmpty();