Support some client settings; add settings page#1167
Conversation
370a328 to
e91e9d5
Compare
|
Thanks for taking this on! Looking forward to it. One first high-level comment: in the branch we merge, let's have at least one of these two settings get added as its own separate commit, complete with a migration. That way we have a nice demonstration of what it'll look like to add each new setting in the future. |
gnprice
left a comment
There was a problem hiding this comment.
I've skimmed now through more of this code. A few other high-level comments below.
| from2To3: (m, schema) async { | ||
| await m.createTable(schema.globalSettings); | ||
| }, | ||
| from3To4: (m, schema) async { | ||
| await m.addColumn( | ||
| schema.globalSettings, schema.globalSettings.browserPreference); |
There was a problem hiding this comment.
db: Start generating step by step migration helper
https://drift.simonbinder.eu/migrations/step_by_step/#manual-generation
Hmm, interesting. Yeah, this seems useful — I think this migration to 4 is already an example where this structure lets the new migration get added as just a separate thing, and with the more manual structure we'd have to have logic for it to interact with the migration to 3.
| import 'package:drift/drift.dart' as i1; | ||
| import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import | ||
|
|
||
| // GENERATED BY drift_dev, DO NOT MODIFY. |
There was a problem hiding this comment.
Let's give this a .g.dart name, so it's clear in a more uniform way that it's generated.
| # Generated code from drift can contain unused local variables. | ||
| - lib/model/database.g.dart |
There was a problem hiding this comment.
This should be reported as a bug in Drift — it's fine for the generated code to do this but then it should contain an ignore comment for it.
There was a problem hiding this comment.
That issue has been marked as fixed! 🎉 So can we drop this commit?
There was a problem hiding this comment.
I hope that there will be a release with this patch soon (last one was 2 weeks ago). If not we can just ask politely.
There was a problem hiding this comment.
Seems like a good time to ask Simon nicely about when he anticipates making the next release. It looks like he's not doing the thing where every bugfix prompts an immediate release.
If a release doesn't come out shortly, let's just have this branch upgrade our drift_dev to a version from Git, namely the commit that fixed this small bug.
| /// The visual theme of the app. | ||
| /// | ||
| /// See [zulipThemeData] for how themes are determined. | ||
| /// | ||
| /// Renaming existing enum values will invalidate the database. | ||
| /// Write a migration if such a change is necessary. | ||
| enum ThemeSetting { |
There was a problem hiding this comment.
Let's put these enum definitions in a new file lib/model/settings.dart.
Then that can also be the home of other logic we add in the future for individual settings.
chrisbobbe
left a comment
There was a problem hiding this comment.
This is exciting!
One question: what UI text does zulip-mobile use for the browser setting? Would it make sense to copy that?
| # Generated code from drift can contain unused local variables. | ||
| - lib/model/database.g.dart |
There was a problem hiding this comment.
That issue has been marked as fixed! 🎉 So can we drop this commit?
| await tester.tap(find.byIcon(Icons.play_arrow_rounded)); | ||
| check(testBinding.takeLaunchUrlCalls()) | ||
| .single.equals((url: Uri.parse(expectedLaunchUrl), mode: LaunchMode.platformDefault)); | ||
| .single.equals((url: Uri.parse(expectedLaunchUrl), mode: LaunchMode.inAppBrowserView)); |
There was a problem hiding this comment.
db: Store browser preference in database
I think these test changes are meant for a later commit:
content: Follow browser preference setting when opening links
(caught with git rebase --exec 'tools/check --diff @~')
|
|
||
| /// Get the app's singleton [GlobalStore], | ||
| /// calling [loadGlobalStore] if not already loaded. | ||
| /// loading it asynchronously if not already loaded. |
There was a problem hiding this comment.
store [nfc]: Update outdated references to loadGlobalStore
commit-message nit: truncate commit ID to 9 characters
| // The context has to be taken from the [Builder] because | ||
| // [zulipThemeData] requires access to [GlobalStoreWidget] in the tree. | ||
| // TODO: any way to remove the [Builder], like how we pulled out | ||
| // [_handleGenerateInitialRoutes]? | ||
| theme: zulipThemeData(context), |
There was a problem hiding this comment.
I guess we could have GlobalStoreWidget take a buildChild param instead of child (or support passing one or the other 🤷♂️), and it would pass its own context to buildChild?
There was a problem hiding this comment.
Chose to name it builder to match Builder.builder and refactored places that use GlobalStoreWidget (one in lib and a handful in tests).
| }); | ||
| }); | ||
|
|
||
| testWidgets('follow globalSettings.themeSetting if not unset', (tester) async { |
There was a problem hiding this comment.
follow globalSettings.themeSetting if not unset
I know what you mean by "if not unset", but it feels slightly unsatisfying: we do still want to follow globalSettings.themeSetting if it's unset 🙂, but the details are just a bit more complicated for that value.
I also see that this test exercises the "unset" value, but incompletely: with "unset", let's also test that the theme responds when the device setting is changed:
tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
await tester.pump();
check(zulipThemeData(element)).brightness.equals(Brightness.dark);| of: find.text('Use external browser'), | ||
| matching: find.byType(SwitchListTile)); | ||
|
|
||
| testWidgets('smoke', (tester) async { |
There was a problem hiding this comment.
Let's also test that the switch is in the right place, and responds correctly, when the setting hasn't been set yet (the part meant by "effective" in effectiveBrowserPreference)
|
Oh, and we have an issue for the theme setting 🙂 so the PR description and relevant commit can be marked as fixing that: |
|
There is also one for browser: I didn't fix them because they do not implement a final design. Perhaps we can have a new design issue for the settings page. |
Sure, that sounds good: a new issue for redesigning the settings page as a whole. Then the individual issues, #1216 and #1228, don't need to stay open to track that redesign, and they can be marked as fixed in this PR; what do you think? |
|
Opened #1375. |
|
Forgot to post a message here. This PR is ready for review.
Good point! The string is "Open links with in-app browser"; switched to that instead. |
|
Thanks, LGTM! Marking for Greg's review. |
20349a1 to
15df1d9
Compare
|
Still need to make some updates the browser settings changes. Will push again once I'm done. |
|
Updated the PR. This should be ready for review again! |
gnprice
left a comment
There was a problem hiding this comment.
Thanks for the revision! Here's comments on the same prefix as before, 6 commits:
6230f5d do not merge; disable checks
f28eeda db: Store GlobalSettings in database
e6f9809 store [nfc]: Update outdated references to loadGlobalStore
3ef3507 test [nfc]: Use eg.globalStore when possible
9897cff store: Store global settings on global store
c54b92a theme: Track theme through global settings
All small, so I'll also take a look next at the subsequent commits.
| /// These apply across all the user's accounts on this client (i.e. on this | ||
| /// install of the app on this device). | ||
| @DataClassName('GlobalSettingsData') | ||
| class GlobalSettings extends Table { |
There was a problem hiding this comment.
nit: order the class definitions matching #1167 (comment)
| child: Builder( | ||
| builder: (context) { | ||
| return MaterialApp( |
There was a problem hiding this comment.
nit: keep a bit more compact (and less indented):
| child: Builder( | |
| builder: (context) { | |
| return MaterialApp( | |
| child: Builder(builder: (context) { | |
| return MaterialApp( |
| import 'binding.dart'; | ||
| import 'database.dart'; | ||
|
|
||
| /// The user's choice of visual theme for the app |
There was a problem hiding this comment.
nit: summary line of dartdoc gets a period:
| /// The user's choice of visual theme for the app | |
| /// The user's choice of visual theme for the app. |
| Subject<AutocompleteViewManager> get autocompleteViewManager => has((x) => x.autocompleteViewManager, 'autocompleteViewManager'); | ||
| } | ||
|
|
||
| extension GlobalSettingsDataChecks on Subject<GlobalSettingsData> { |
There was a problem hiding this comment.
nit: keep in logical order, matching the corresponding code
| GlobalSettingsData get globalSettings => _globalSettings; | ||
| GlobalSettingsData _globalSettings; | ||
|
|
||
| /// Update the global settings in the store, return the new version. |
There was a problem hiding this comment.
nit: the syntax in the existing updateAccount doc is correct:
| /// Update the global settings in the store, return the new version. | |
| /// Update the global settings in the store, returning the new version. |
Other examples would include "do A, resulting in B" or "do A, arriving at B".
| Subject<Iterable<int>> get accountIds => has((x) => x.accountIds, 'accountIds'); | ||
| Subject<Iterable<({ int accountId, Account account })>> get accountEntries => has((x) => x.accountEntries, 'accountEntries'); | ||
| Subject<Account?> getAccount(int id) => has((x) => x.getAccount(id), 'getAccount($id)'); | ||
| Subject<GlobalSettingsData> get globalSettings => has((x) => x.globalSettings, 'globalSettings'); |
There was a problem hiding this comment.
nit: match order of the underlying properties
| TestGlobalStore({required super.globalSettings, required super.accounts}) | ||
| : _globalSettings = globalSettings; | ||
|
|
||
| GlobalSettingsData? _globalSettings; |
There was a problem hiding this comment.
| GlobalSettingsData? _globalSettings; | |
| GlobalSettingsData _globalSettings; |
right?
There was a problem hiding this comment.
Hmm or: what if we just leave this field out? Seems like the base class takes care of it.
And in the case of accounts we don't find we need a corresponding field here.
There was a problem hiding this comment.
Yeah. It should be very similar to doUpdateAccount:
@override
Future<void> doUpdateAccount(int accountId, AccountsCompanion data) async {
// Nothing to do.
}| switch (themeSetting) { | ||
| case null: | ||
| return zulipLocalizations.themeSettingSystem; |
| Subject<T> get value => has((c) => c.value, 'value'); | ||
| } | ||
|
|
||
| extension ThemeDataChecks on Subject<ThemeData> { |
There was a problem hiding this comment.
nit: for logical ordering, put these Material-specific items down among the other Material-specific items near the end of the file
| /// Update the global settings in the store, return the new version. | ||
| /// | ||
| /// The global settings must already exist in the store. | ||
| Future<GlobalSettingsData> updateGlobalSettings(GlobalSettingsCompanion data) async { |
There was a problem hiding this comment.
This should get a couple of tests. See the GlobalStore.updateAccount tests for examples.
|
|
||
| final after = v4.DatabaseAtV4(schema.newConnection()); | ||
| final globalSettings = await after.select(after.globalSettings).getSingle(); | ||
| check(globalSettings.themeSetting).equals(ThemeSetting.light.name); |
There was a problem hiding this comment.
nit:
| check(globalSettings.themeSetting).equals(ThemeSetting.light.name); | |
| check(globalSettings).themeSetting.equals(ThemeSetting.light.name); |
(then cascade as usual)
There was a problem hiding this comment.
… Oh I see, the type here is v4.GlobalSettingsData, not something more widely shared. OK, no need to have a checks-extension for it.
| import 'schemas/schema_v2.dart' as v2; | ||
| import 'store_checks.dart'; | ||
| import 'schemas/schema_v3.dart' as v3; |
| const _BrowserPreferenceSetting(), | ||
| const _ThemeSetting(), |
There was a problem hiding this comment.
Seems like theme should come first — it's going to be a much more popular setting to consider.
|
Updated the PR. This should be ready now. |
gnprice
left a comment
There was a problem hiding this comment.
Thanks! I've now read the whole thing; comments below.
| if (data.containsKey('id')) { | ||
| context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); | ||
| } | ||
| context.handle(_realmUrlMeta, const VerificationResult.success()); |
There was a problem hiding this comment.
nit: generated code should update in tandem with the deps upgrade that causes it
| check(notifyCount).equals(1); | ||
| }); | ||
|
|
||
| // TOOD integration tests with sqlite |
There was a problem hiding this comment.
nit:
| // TOOD integration tests with sqlite | |
| // TODO integration tests with sqlite |
| check(globalStore).globalSettings.browserPreference.equals( | ||
| BrowserPreference.external); |
There was a problem hiding this comment.
This doesn't need to test each setting as we add it; at that point we'd just be testing the generated code from Drift.
| UrlLaunchMode get urlLaunchMode { | ||
| if (browserPreference != null) { | ||
| return switch (browserPreference!) { | ||
| BrowserPreference.embedded => UrlLaunchMode.platformDefault, |
There was a problem hiding this comment.
This line doesn't look right, does it. 🙂 If the user has explicitly chosen in-app, let's make it in-app even if that isn't the default on a given platform.
(I think you may have asked me about this the other day, but if I said to keep this I was probably thinking of the spot a few lines below where we're defining our own defaults.)
Or is there a reason why that translation doesn't work?
There was a problem hiding this comment.
For Android and iOS, platformDefault for HTTP URLs is already in-app browser view; I remember that we decided to keep platformDefault because that setting, "open links with in-app browser", applies to those links to the end-user as expected, but does not make sense for other types of links like mailto.
Testing this, it looks like attempting to open mailto links in-app results in an error:
E/flutter (14030): [ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: Invalid argument (url): To use an in-app web view, you must provide an http(s) URL.: Instance of '_SimpleUri'
E/flutter (14030): #0 launchUrl (package:url_launcher/src/url_launcher_uri.dart:49:5)
E/flutter (14030): #1 LiveZulipBinding.launchUrl (package:zulip/model/binding.dart:355:25)
E/flutter (14030): #2 _launchUrl (package:zulip/widgets/content.dart:1446:44)
The comment on the rationale of this is in the build method of the widget that controls this setting, but you bringing it up makes me feel that it probably fits better in the dartdoc of this extension method.
There was a problem hiding this comment.
Or perhaps better, in the dartdoc for the embedded (or inApp) enum.
| check(testBinding.takeLaunchUrlCalls()) | ||
| .single.equals(( | ||
| url: Uri.parse('https://example/'), mode: LaunchMode.platformDefault)); |
There was a problem hiding this comment.
nit: .single compacts nicely onto previous line:
| check(testBinding.takeLaunchUrlCalls()) | |
| .single.equals(( | |
| url: Uri.parse('https://example/'), mode: LaunchMode.platformDefault)); | |
| check(testBinding.takeLaunchUrlCalls()).single.equals(( | |
| url: Uri.parse('https://example/'), mode: LaunchMode.platformDefault)); |
| /// Use the in-app browser. | ||
| embedded, |
There was a problem hiding this comment.
How about:
| /// Use the in-app browser. | |
| embedded, | |
| /// Use the in-app browser. | |
| inApp, |
Seems like that would best align the name with how we're calling it in other code and in the UI.
| import 'page.dart'; | ||
| import 'store.dart'; | ||
|
|
||
| class SettingsPage extends StatelessWidget { |
There was a problem hiding this comment.
This does not aim to fully implement the wip design for the settings
page. We offer it mainly to test settings, so the implementation is
kept as simple as possible.
WIP design:
https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=446-21372&t=BZKpTQPSiBDNxwvB-0
Let's replace "WIP design" with more of a complete sentence — it keeps catching my eye as if the "WIP" is a marker about the state of this commit itself.
There was a problem hiding this comment.
Cool, this is helpful:
This does not aim to fully implement the wip design for the settings
page. We offer it mainly to test settings, so the implementation is
kept as simple as possible.
Old design for reference:
https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=446-21372&t=BZKpTQPSiBDNxwvB-0
Let's make a couple of adjustments: s/wip design/draft design/, s/Old design/Draft design (old)/. That way (a) it more fully avoids that "wip" keyword 🙂, (b) it's a bit clearer that the two references are to the same design.
There was a problem hiding this comment.
Oh, and a separate nit on the commit message:
settings: Add settings page with vanilla Flutter widgets
I'd read "vanilla Flutter widgets" first of all to mean package:flutter/widgets.dart, aka "the widgets library", in contrast to the Material or Cupertino libraries. Here what we actually mean is we're using Flutter's Material widgets, specifically the adaptive flavor of them.
So let's say:
settings: Add easy settings page using (adaptive) Material widgets
This will use generic Material-style controls on Android, and
generic iOS-style controls on iOS.
(and then the other information).
|
This should be ready for review. Made some follow-up comments to #1167 (comment). |
gnprice
left a comment
There was a problem hiding this comment.
Thanks for the revision!
The first 7/10 commits, up through the settings UI, are virtually ready to merge:
7629e44 fix url; deps: Update drift to 2.26.0
98a1a5a db: Store GlobalSettings in database
2ab1c5c store [nfc]: Update outdated references to loadGlobalStore
c84aed0 test [nfc]: Use eg.globalStore when possible
80da3ae store: Store global settings on global store
b214218 theme: Track theme through global settings
ed0afcb settings: Add settings page with vanilla Flutter widgets
but for the browser preference, there's still a substantive comment below. So let's split that part out to a followup PR.
For those first 7 commits, other than nits below, the first commit message just needs to be updated for merge.
| /// Use the in-app browser for HTTP links. | ||
| /// | ||
| /// This translates to [UrlLaunchMode.platformDefault], so that non-in-app | ||
| /// browsers/apps will still be used for other types of links (e.g. mailto) | ||
| /// on both iOS and Android. | ||
| inApp, |
There was a problem hiding this comment.
Cool, putting it here on the enum's doc is helpful for explanation (continuing from #1167 (comment)). Still this translation doesn't seem right.
The launchUrl doc says:
The default behavior of LaunchMode.platformDefault is up to each platform, and its behavior for a given platform may change over time as new modes are supported, so clients that want a specific mode should request it rather than rely on any currently observed default behavior.
so it's explicitly warning not to make the assumption this is making.
The logic in launchUrl that can reject inAppWebView is pretty specific:
if ((mode == LaunchMode.inAppWebView ||
mode == LaunchMode.inAppBrowserView) &&
!(url.scheme == 'https' || url.scheme == 'http')) {
throw ArgumentError.value(url, 'url',
'To use an in-app web view, you must provide an http(s) URL.');
}so I think we can just have our own bit of logic that checks if the scheme is http or https, and if not then it replaces inAppBrowserView with platformDefault.
| import 'page.dart'; | ||
| import 'store.dart'; | ||
|
|
||
| class SettingsPage extends StatelessWidget { |
There was a problem hiding this comment.
Cool, this is helpful:
This does not aim to fully implement the wip design for the settings
page. We offer it mainly to test settings, so the implementation is
kept as simple as possible.
Old design for reference:
https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=446-21372&t=BZKpTQPSiBDNxwvB-0
Let's make a couple of adjustments: s/wip design/draft design/, s/Old design/Draft design (old)/. That way (a) it more fully avoids that "wip" keyword 🙂, (b) it's a bit clearer that the two references are to the same design.
| import 'page.dart'; | ||
| import 'store.dart'; | ||
|
|
||
| class SettingsPage extends StatelessWidget { |
There was a problem hiding this comment.
Oh, and a separate nit on the commit message:
settings: Add settings page with vanilla Flutter widgets
I'd read "vanilla Flutter widgets" first of all to mean package:flutter/widgets.dart, aka "the widgets library", in contrast to the Material or Cupertino libraries. Here what we actually mean is we're using Flutter's Material widgets, specifically the adaptive flavor of them.
So let's say:
settings: Add easy settings page using (adaptive) Material widgets
This will use generic Material-style controls on Android, and
generic iOS-style controls on iOS.
(and then the other information).
This contains a fix for simolus3/drift#3384. Signed-off-by: Zixuan James Li <zixuan@zulip.com>
Signed-off-by: Zixuan James Li <zixuan@zulip.com>
These were supposed to be updated in commit fc80be1 where we replaced loadGlobalStore with getGlobalStore. `LiveGlobalBindings.load` is the only thing left analogous to the previously referred to `loadGlobalStore` method. To avoid direct referencce to this specific implementation on `GlobalStore`, mention that loading it for the first time is expected to be asynchronous instead. Signed-off-by: Zixuan James Li <zixuan@zulip.com>
Later we will add more required parameters to TestGlobalStore. Signed-off-by: Zixuan James Li <zixuan@zulip.com>
To add integration tests for global store with sqlite, we might want to switch to LiveGlobalStore, or at least make part of it testable with an in-memory database (like we do for database_test.dart). Leaving that to the future. Signed-off-by: Zixuan James Li <zixuan@zulip.com>
Signed-off-by: Zixuan James Li <zixuan@zulip.com>
This will use generic Material-style controls on Android, and generic iOS-style controls on iOS. This does not aim to fully implement the draft design for the settings page. We offer it mainly to test settings, so the implementation is kept as simple as possible. Draft design for reference: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=446-21372&t=BZKpTQPSiBDNxwvB-0 Fixes: zulip#1216 Signed-off-by: Zixuan James Li <zixuan@zulip.com>
|
Thanks! Updated the PR and the first comment in this thread. |
|
Thanks! Looks good; merging. |
Work towards #97.
Fixes #1216.
screenshots