diff --git a/lib/model/store.dart b/lib/model/store.dart index 6800fc6af1..ef73317e20 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -51,12 +51,19 @@ export 'database.dart' show Account, AccountsCompanion, AccountAlreadyExistsExce /// * [LiveGlobalStore], the implementation of this class that /// we use outside of tests. abstract class GlobalStore extends ChangeNotifier { - GlobalStore({required Iterable accounts}) - : _accounts = Map.fromEntries(accounts.map((a) => MapEntry(a.id, a))); + GlobalStore({ + required Iterable accounts, + required GlobalSettingsData globalSettings, + }) + : _accounts = Map.fromEntries(accounts.map((a) => MapEntry(a.id, a))), + _globalSettings = globalSettings; /// A cache of the [Accounts] table in the underlying data store. final Map _accounts; + /// A cache of the [GlobalSettingsData] singleton in the underlying data store. + GlobalSettingsData _globalSettings; + // TODO settings (those that are per-device rather than per-account) // TODO push token, and other data corresponding to GlobalSessionState @@ -223,6 +230,23 @@ abstract class GlobalStore extends ChangeNotifier { /// Remove an account from the underlying data store. Future doRemoveAccount(int accountId); + GlobalSettingsData get globalSettings => _globalSettings; + + /// Update the global settings in the store, return the new version. + /// + /// The global settings must already exist in the store. + Future updateGlobalSettings(GlobalSettingsCompanion data) async { + await doUpdateGlobalSettings(data); + _globalSettings = _globalSettings.copyWithCompanion(data); + notifyListeners(); + return _globalSettings; + } + + /// Update the global settings in the underlying data store. + /// + /// This should only be called from [updateGlobalSettings]. + Future doUpdateGlobalSettings(GlobalSettingsCompanion data); + @override String toString() => '${objectRuntimeType(this, 'GlobalStore')}#${shortHash(this)}'; } @@ -757,6 +781,7 @@ class LiveGlobalStore extends GlobalStore { LiveGlobalStore._({ required AppDatabase db, required super.accounts, + required super.globalSettings, }) : _db = db; @override @@ -773,7 +798,10 @@ class LiveGlobalStore extends GlobalStore { static Future load() async { final db = AppDatabase(NativeDatabase.createInBackground(await _dbFile())); final accounts = await db.select(db.accounts).get(); - return LiveGlobalStore._(db: db, accounts: accounts); + final globalSettings = await db.ensureGlobalSettings(); + return LiveGlobalStore._(db: db, + accounts: accounts, + globalSettings: globalSettings); } /// The file path to use for the app database. @@ -835,6 +863,12 @@ class LiveGlobalStore extends GlobalStore { assert(rowsAffected == 1); } + @override + Future doUpdateGlobalSettings(GlobalSettingsCompanion data) async { + final rowsAffected = await _db.update(_db.globalSettings).write(data); + assert(rowsAffected == 1); + } + @override String toString() => '${objectRuntimeType(this, 'LiveGlobalStore')}#${shortHash(this)}'; } diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index f3b369e876..d4fdc8cec7 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -14,6 +14,7 @@ import '../generated/l10n/zulip_localizations.dart'; import '../model/avatar_url.dart'; import '../model/binding.dart'; import '../model/content.dart'; +import '../model/database.dart'; import '../model/internal_link.dart'; import 'code_block.dart'; import 'dialog.dart'; @@ -1342,17 +1343,20 @@ void _launchUrl(BuildContext context, String urlString) async { return; } + final globalSettings = GlobalStoreWidget.of(context).globalSettings; bool launched = false; String? errorMessage; try { launched = await ZulipBinding.instance.launchUrl(url, - mode: switch (defaultTargetPlatform) { + mode: switch ((globalSettings.browserPreference, defaultTargetPlatform)) { + (BrowserPreference.embedded, _) => UrlLaunchMode.inAppBrowserView, + (BrowserPreference.external, _) => UrlLaunchMode.externalApplication, // On iOS we prefer LaunchMode.externalApplication because (for // HTTP URLs) LaunchMode.platformDefault uses SFSafariViewController, // which gives an awkward UX as described here: // https://chat.zulip.org/#narrow/stream/48-mobile/topic/in-app.20browser/near/1169118 - TargetPlatform.iOS => UrlLaunchMode.externalApplication, - _ => UrlLaunchMode.platformDefault, + (BrowserPreference.unset, TargetPlatform.iOS) => UrlLaunchMode.externalApplication, + (BrowserPreference.unset, _) => UrlLaunchMode.platformDefault, }); } on PlatformException catch (e) { errorMessage = e.message; diff --git a/lib/widgets/theme.dart b/lib/widgets/theme.dart index ec6cd81ce0..fd7ca6d277 100644 --- a/lib/widgets/theme.dart +++ b/lib/widgets/theme.dart @@ -1,16 +1,27 @@ import 'package:flutter/material.dart'; import '../api/model/model.dart'; +import '../model/database.dart'; import 'content.dart'; import 'emoji_reaction.dart'; import 'message_list.dart'; import 'channel_colors.dart'; +import 'store.dart'; import 'text.dart'; ThemeData zulipThemeData(BuildContext context) { final DesignVariables designVariables; final List themeExtensions; - Brightness brightness = MediaQuery.platformBrightnessOf(context); + final globalSettings = GlobalStoreWidget.of(context).globalSettings; + Brightness brightness; + switch (globalSettings.themeSetting) { + case ThemeSetting.unset: + brightness = MediaQuery.platformBrightnessOf(context); + case ThemeSetting.light: + brightness = Brightness.light; + case ThemeSetting.dark: + brightness = Brightness.dark; + } // This applies Material 3's color system to produce a palette of // appropriately matching and contrasting colors for use in a UI. diff --git a/test/example_data.dart b/test/example_data.dart index d071307ec4..42c3662940 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -8,6 +8,7 @@ import 'package:zulip/api/model/submessage.dart'; import 'package:zulip/api/route/messages.dart'; import 'package:zulip/api/route/realm.dart'; import 'package:zulip/api/route/channels.dart'; +import 'package:zulip/model/database.dart'; import 'package:zulip/model/narrow.dart'; import 'package:zulip/model/store.dart'; @@ -805,8 +806,16 @@ ChannelUpdateEvent channelUpdateEvent( // The entire per-account or global state. // -TestGlobalStore globalStore({List accounts = const []}) { - return TestGlobalStore(accounts: accounts); +const globalSettings = GlobalSettingsData( + themeSetting: ThemeSetting.unset, + browserPreference: BrowserPreference.unset, +); + +TestGlobalStore globalStore({ + List accounts = const [], + GlobalSettingsData globalSettings = globalSettings, +}) { + return TestGlobalStore(accounts: accounts, globalSettings: globalSettings); } InitialSnapshot initialSnapshot({ diff --git a/test/model/binding.dart b/test/model/binding.dart index 30eb40c17b..ab446b5bcb 100644 --- a/test/model/binding.dart +++ b/test/model/binding.dart @@ -11,6 +11,7 @@ import 'package:zulip/model/binding.dart'; import 'package:zulip/model/store.dart'; import 'package:zulip/widgets/app.dart'; +import '../example_data.dart' as eg; import 'test_store.dart'; /// The binding instance used in tests. @@ -85,7 +86,7 @@ class TestZulipBinding extends ZulipBinding { /// /// Tests that access this getter, or that mount a [GlobalStoreWidget], /// should clean up by calling [reset]. - TestGlobalStore get globalStore => _globalStore ??= TestGlobalStore(accounts: []); + TestGlobalStore get globalStore => _globalStore ??= eg.globalStore(); TestGlobalStore? _globalStore; bool _debugAlreadyLoadedStore = false; diff --git a/test/model/store_test.dart b/test/model/store_test.dart index 10e8698360..821d3e3fe5 100644 --- a/test/model/store_test.dart +++ b/test/model/store_test.dart @@ -406,7 +406,7 @@ void main() { late FakeApiConnection connection; Future prepareStore({Account? account}) async { - globalStore = TestGlobalStore(accounts: []); + globalStore = eg.globalStore(); account ??= eg.selfAccount; await globalStore.insertAccount(account.toCompanion(false)); connection = (globalStore.apiConnectionFromAccount(account) @@ -581,7 +581,7 @@ void main() { } Future preparePoll({int? lastEventId}) async { - globalStore = TestGlobalStore(accounts: []); + globalStore = eg.globalStore(); await globalStore.add(eg.selfAccount, eg.initialSnapshot( lastEventId: lastEventId)); await globalStore.perAccount(eg.selfAccount.id); @@ -1086,7 +1086,10 @@ void main() { } class LoadingTestGlobalStore extends TestGlobalStore { - LoadingTestGlobalStore({required super.accounts}); + LoadingTestGlobalStore({ + required super.accounts, + super.globalSettings = eg.globalSettings, + }); Map>> completers = {}; diff --git a/test/model/test_store.dart b/test/model/test_store.dart index 2322e78d7a..898beccdde 100644 --- a/test/model/test_store.dart +++ b/test/model/test_store.dart @@ -1,6 +1,7 @@ import 'package:zulip/api/model/events.dart'; import 'package:zulip/api/model/initial_snapshot.dart'; import 'package:zulip/api/model/model.dart'; +import 'package:zulip/model/database.dart'; import 'package:zulip/model/store.dart'; import 'package:zulip/widgets/store.dart'; @@ -22,7 +23,7 @@ import '../example_data.dart' as eg; /// /// See also [TestZulipBinding.globalStore], which provides one of these. class TestGlobalStore extends GlobalStore { - TestGlobalStore({required super.accounts}); + TestGlobalStore({required super.accounts, required super.globalSettings}); final Map< ({Uri realmUrl, int? zulipFeatureLevel, String? email, String? apiKey}), @@ -157,6 +158,13 @@ class TestGlobalStore extends GlobalStore { store: store, initialSnapshot: initialSnapshot); return Future.value(store); } + + GlobalSettingsData? _globalSettings; + + @override + Future doUpdateGlobalSettings(GlobalSettingsCompanion data) async { + _globalSettings = _globalSettings!.copyWithCompanion(data); + } } extension PerAccountStoreTestExtension on PerAccountStore {