-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: consolidate settings-related state #1027
base: develop
Are you sure you want to change the base?
Conversation
src/frontend/lib/utils.test.ts
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved to a more specific file in this directory
src/frontend/lib/utils.ts
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved to coordinateFormat.ts
export const SettingsStateSchema = v.object({ | ||
coordinateFormat: v.union([CoordinateFormatSchema, v.null()]), | ||
locale: v.union([ | ||
v.object({ | ||
languageTag: v.string(), | ||
}), | ||
v.null(), | ||
]), | ||
manualCoordinateEntryFormat: v.union([CoordinateFormatSchema, v.null()]), | ||
metricsDiagnosticsPermissionsEnabled: v.union([v.boolean(), v.null()]), | ||
obscureCode: v.union([v.string(), v.null()]), | ||
passcode: v.union([v.string(), v.null()]), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
welcoming input on whether this feels okay in terms of structure. Was thinking about doing some nesting to group related fields together, but that probably satisfies my OCD more than an actual need 😅
export type SettingsState = v.InferOutput<typeof SettingsStateSchema>; | ||
|
||
// NOTE: Do not change! | ||
export const STORAGE_KEY = 'Settings' as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wondering if it's really worth using the same key as before as opposed to starting fresh and using a completely new key. The only pre-existing values associated with this storage key are the coordinate format and manual entry coordinate format.
8ba21fe
to
0019413
Compare
@gmaclennan Andrew and I would like you opinion on this. We discussed this on slack, and we disagree with the best approach. So I am going to write down my thoughts, and he is going to further summarize his thoughts and we were hoping to have you help us come to a consensus. I believe that having a separate default value and persisted state adds unnecessary technical complexity to solve a problem that doesn’t need solving. Current Approach in This PR
Pros
Cons
Proposed Alternative
Pros
Cons
While dynamic defaults were necessary for language settings, I don’t think it’s worth maintaining this logic for other settings. I dont think default value will change for setting, and even if they do, these values are not detrimental to the user experience. Following our principles of test ability and maintainability, we should avoid unnecessary complexity. While I see why this approach was introduced for language settings, I don't think it’s needed for other settings. |
On a side note, I hadn’t seen the language-related changes earlier, but I think the technical overhead of maintaining the default language might be unnecessary. I understand that the goal is for the app language to follow the device language if the user hasn’t explicitly set a preference, and while I agree that’s the ideal behavior, I’m not sure it was worth the added complexity of introducing a default state. That said, since the work is already done, it may not matter at this point—sorry that we didnt do this architecture exploration before hand. |
To address some implementation concerns that have been raised: Why not specify the default values for certain fields when setting up the store state?Given that the store state can be persisted, the main arguments I will make are:
In the context of the store setup, this is what is meant by using default values: function createInitialState() {
return {
// Use the default value instead of `null`
coordinateFormat: 'utm',
// Other fields...
}
} #978 is a good example of this of why we shouldn't do this. Using a simplified example, this is a sequence that can occur if you made the above change:
In this example, we're talking about a setting that is relatively inconsequential in the grand scheme of things. However, there's nothing that prevents this happening for data that has much larger consequences or causes more problematic issues for users. I would like to effectively communicate that the There is an implicit knowledge that one needs to use the more granular hooks instead of
|
To clarify, certain fields within the settings state will have defaults defined by app/design requirements. When we read the settings state and check each field, if the field does not have a meaningful persisted value (we use EDIT: possible that by "default state", you mean "initial state"? if that's the case, this PR is introducing an initial state that is more resilient/reflective to how user settings works from a design perspective.
Not sure what the "constantly" refers to? We read persisted state when the app starts. If you're referring to resolution for certain fields, yes that happens on every update of the store state but that's because settings can/should be un-settable by users throughout the app's lifetime, in order to allow defaults to be applied if necessary. re: "reconciling two sources of truth", i could be misunderstanding what this refers to, but from my perspective, the store state remains the sole source of truth for settings that are actually defined by the user. We still have to account for the case where they don't specify some settings in order for the app to be usable, which should be done at runtime without implicitly persisting that choice for the user. Alternatively, if this is referring to the existence of the more granular hooks, nothing stops us from doing something like this in multiple places: function Foo() {
const coordinateFormat = useSettingsState(state => state.coordinateFormat || 'utm')
}
function Bar() {
const coordinateFormat = useSettingsState(state => state.coordinateFormat || 'utm')
} While this works fine, it's prone to inconsistency (what if you did
I think this comment is misunderstanding the language issue a bit. The changes to language settings in this PR is NOT introducing a default state - we already had that but the way it is done is flawed and causes user-facing issues. Instead, what this PR is introducing is (more) properly representing when the user has not specified a language to use, and it's updating the calculation of the default value to use more thoroughly. |
Fixes #978
Closes #1018
Towards #1019
Couldn't figure out a great way of doing this incrementally so apologies in advance for the large scope of this PR. Would generally recommend following the commits to get a somewhat sequential idea of how changes were applied.
Implementation notes:
Consolidates the following states into a single state:
As of this writing, the implementation is re-using the persistence key associated with the coordinate format and manual entry coordinate format, meaning that those are the only values that are explicitly migrated/preserved as part of this PR. During my discussion with @ErikSin about migration of the persistence, we decided that at least as part of the initial implementation, migrating previously stored values wasn't a hard requirement given the nature of the app's current usage. There may still be a path where we try to migrate the previous values anyways, but not a focus for now.
For all language-related changes, please refer to the details written in fix: improve language selection resolution #1011 to understand the changes. I basically ported the changes from there into this PR because an incremental approach was too tricky. There are some small implementation differences (mostly related to naming) but aside from that, what's written in that PR is still very relevant here.
Not in love with the naming, but made an effort to distinguish between setting state that acts as source of truth and the "resolved" more granular states that should be used within the app. Basically, any settings-related state that may have a default/fallback value when used in the app falls under this classification. Generally, the resolution of these settings happens in the following order:
null
, it generally means that the user hasn't done anything to explicitly choose a value for that field.Using the displayed coordinate format as an example of the above:
'utm'
,'dd'
,'dms'
), use that value.null
, so we default to'utm'
.The relevant settings with some form of this approach are:
It is HIGHLY recommended to use the hooks from
resolvedSettings/
when reading settings-related values for the above. Reading from theuseSettingsState()
hook requires more work and is prone to inconsistency since it doesn't handle resolution of values when they are not explicitly set. However, reading the passcode and obscure code from theuseSettingsState()
directly is fine, as those will never have a fallback value when not set by the user.General features that are affected by the changes:
The language settings is the only feature that should see different behavior as a result of the changes (since it fixes an existing bug). The other features should not exhibit any notable change in behavior.