-
Notifications
You must be signed in to change notification settings - Fork 904
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
[brave-extension] add settings option to hide Shields activity count #3150
Conversation
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.
👍 but just a couple of flow changes would be nice if possible
chrome.browserAction.setBadgeText({ | ||
tabId, | ||
text: String(text) | ||
getViewPreferences() |
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.
Calling in to another API from this API doesn't seem like great code organization to me for the following reasons:
- It's retrieving the data again, adding a (small) delay.
- The app decision logic is now in multiple places.
- It's complicating the otherwise simple module, perhaps also complicating testability. This means the API no longer just does what it says
setBadgeText
.
Instead, since we store this data in the redux state or some other main decision making area, can we simply read current value of the state before we call setBadgeText
and decide what to pass in?
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.
that one is tricky because change happens with shields closed so there's no state
since the component isn't mounted. I moved the data to the persistent state (shields localStorage). lmk if you have a better option
} | ||
export async function getViewPreferences (): Promise<GetViewPreferencesData> { | ||
const showAdvancedViewPref = await SettingsPrivate.getPreference(settingsKeys.showAdvancedView.key) | ||
const statsBadgeVisiblePref = await SettingsPrivate.getPreference(settingsKeys.statsBadgeVisible.key) |
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.
We can use promise.all
here instead of waiting for each setting. As an extra, since we store the neccessary config in one object, this code also lends itself to avoiding repetition with something like:
let settings = {}
await Promise.all(Object.keys(settingsKeys).map(async (name) => {
// Get setting by internal key
const pref = await SettingsPrivate.getPreference(settingsKeys[name].key)
// Validate setting type
if (pref.type !== settingsKeys[name].type) {
throw new Error(`Unexpected settings type received for "${settingsKeys[name].key}". Expected: ${settingsKeys[name].type}, Received: ${pref.type}`)
}
// Valid
settings[name] = pref.value
})
return settings
wdyt?
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.
sounds good, updated with the above
} | ||
|
||
const settingsKeys = { | ||
showAdvancedView: { key: 'brave.shields.advanced_view_enabled', type: chrome.settingsPrivate.PrefType.BOOLEAN } | ||
showAdvancedView: { key: 'brave.shields.advanced_view_enabled', type: chrome.settingsPrivate.PrefType.BOOLEAN }, | ||
statsBadgeVisible: { key: 'brave.shields.stats_badge_visible', type: chrome.settingsPrivate.PrefType.BOOLEAN }, |
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.
lint: comma (even though I personally like this style :-( )
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.
thought our linter would spot that. updated
<div class="label shields-primary-title">$i18n{lookFeelTitle}</div> | ||
</div> | ||
<settings-toggle-button | ||
class="first-after-text" |
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.
There is the .continuation
chromium settings css class, so you shouldn't need to make the .first-after-text
class.
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.
updated
getViewPreferences() | ||
.then((settings: chrome.braveShields.BraveShieldsViewPreferences) => { | ||
if (state.persistentData.statsBadgeVisible === settings.statsBadgeVisible) { | ||
state = shieldsPanelState.updatePersistentData(state, { statsBadgeVisible: settings.statsBadgeVisible }) |
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.
Setting state in a callback won't affect the state since the reducer will have already returned the state before this .then callback is run, e.g.
console.log('1');
Promise.resolve().then(() => console.log('2'));
console.log('3');
> 1
> 3
> 2
To solve all your issues with state, perhaps you should have a redux action called VIEW_PREFERENCES_CHANGED, and use chrome.settingsPrivate.onPrefsChanged.addListener
to call this when the relevant items change.
Perhaps in shieldsAPI.ts
you would have something line:
export function addViewPreferencesChangedListener(callback) {
chrome.settingsPrivate.onPrefsChanged.addListener(changedPrefs => {
// Reverse settings key / name lookup (can be done outside of this function)
const settingsNameByKey = {}
for (const name of Object.keys(settingsKeys)) {
settingsNameByKey[settingsKeys[name].key] = { ...settingsKeys[name], name }
}
// Get changed settings that are relevant
const changedSettings: SetViewPreferencesData = {}
for (const pref of changedPrefs) {
const setting = settingsNameByKey[pref.key]
if (setting) {
// Validate setting type
if (pref.type !== setting.type) {
throw new Error(`Unexpected settings type received for "${pref.key}". Expected: ${setting.type}, Received: ${pref.type}`)
}
// Valid
changedSettings[setting.name] = pref.value
}
}
if (Object.keys(changedSettings).length) {
callback(changedSettings)
}
})
}
and then in your redux store setup you could use:
shieldsAPI.addViewPreferencesChangedListener(actions.viewPreferencesChanged)
shieldsAPI.getViewPreferences.then(actions.viewPreferencesChanged)
and in the reducer store the value of these preferences in the state wherever you like.
wdyt?
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.
your proposal looks good but I think the call order doesn't matter since localStorage would update anyway, which is what is read by the reducer? also, shouldn't we avoid calling actions inside the store?
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.
I mean, I can't think of a case where the store would receive the wrong value as-is
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.
As-is, you are calling an async API inside the reducer, and then bypassing the action -> reducer flow to attempt to directly set the state inside that async callback. So that's the anti-pattern, but also the state =
isn't actually doing anything here, so all this code can be removed, right? Or IMO better, is to move shieldsAPI.addViewPreferencesChangedListener(actions.viewPreferencesChanged)
to your init function, whether that's the app init or store init (Setting up async events -> redux actions in the store init is a fine pattern IMO. The anti-pattern is calling actions inside the reducer function, which that wouldn't neccessarily be doing).
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.
e.g in background.ts you could setup the calls for the initial data which fires an action to set the initial data, as well as subscribe to events to update that data by firing an action again on change, unless I'm mistaking the architecture? I just don't think we want to exemplify a pattern where we go to an API for the data every time. Also when we receive an action that the preferences have changed, we can check the state to see if the statsBadgeVisible
changed and if it did, then go and update the badge text for every tab. That way other windows will be updated instantly when the setting changes.
// do not show any badge if there are no blocked items | ||
setBadgeText(tabId, total > 99 ? '99+' : total > 0 ? total.toString() : '') | ||
setBadgeText(tabId, text, state.persistentData.statsBadgeVisible) |
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.
Why pass in this boolean to a generic API and not just only pass text if statsBadgeVisible is true?
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.
we need to pass empty strings to override the previous call. otherwise we will need to update the page to have the change visible
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.
Suggestion is to pass empty string and keep the decision in the caller layer and keep setBadgeText
a simple API. setBadgeText(tabId, statsBadgeVisible ? '' : text)
components/definitions/chromel.d.ts
Outdated
} | ||
type BraveShieldsSetViewPreferencesData = { | ||
showAdvancedView?: boolean | ||
statsBadgeVisible?: boolean | ||
} |
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.
I think both these types can be removed since we use chrome.settingsPrivate
API now. Must have left this in by accident.
@@ -163,24 +163,34 @@ export const setAllowScriptOriginsOnce = (origins: Array<string>, tabId: number) | |||
}) | |||
|
|||
export type GetViewPreferencesData = { | |||
showAdvancedView: boolean | |||
showAdvancedView?: boolean | |||
statsBadgeVisible?: boolean |
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.
Why did you make these optional? As far as I can see, getViewPreferences
always returns all properties, so callers won't have to do extra type checks.
cbf3611
to
557e806
Compare
@petemill updated, could you re-review? |
@@ -7,6 +7,11 @@ import * as Shields from '../../types/state/shieldsPannelState' | |||
|
|||
const keyName = 'shields-persistent-data' | |||
|
|||
export const initialSettingsData = { |
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.
nit: needs type
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.
Thanks for addressing feedback! Most of this review is nits which are optional for you. There is only 1 issue which I think absolutely needs change - and that is parsing the settings keys from the onPrefsChanged
API.
@@ -65,6 +67,11 @@ export interface UpdatePersistentData { | |||
(state: State, persistentData: Partial<PersistentData>): State | |||
} | |||
|
|||
export interface MergeSettingsData { | |||
(state: State, settingsData: SettingsData): SettingsData | |||
// (state: State, settingsData: Partial<SettingsData>): State |
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.
commented out code
[key: string]: OneOfSettings | ||
} | ||
|
||
export type OneOfSettings = |
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.
nit: might be clearer to be named SettingsKey
or similar, since this isn't a setting but some kind of string key
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.
in fact you could just do keyof BraveShieldsViewPreferences
or keyof SettingsKeys
from https://github.com/brave/brave-core/pull/3150/files/557e80636c52fb4e7f09ac578ad555a16a26caaf#diff-5875b9a5788aaf15e33a6ded24690654R171
|
||
interface SetStoreSettingsChangeReturn { | ||
type: typeof types.SET_STORE_SETTINGS_CHANGE, | ||
settingsData: SettingsData | Partial<SettingsData> |
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.
nit: isn't SettingsData | Partial<SettingsData>
just the same result as Partial<SettingsData>
?
// do not show any badge if there are no blocked items | ||
setBadgeText(tabId, total > 99 ? '99+' : total > 0 ? total.toString() : '') | ||
? total > 99 ? '99+' : total > 0 ? total.toString() : '' |
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.
nit: indent? (up to you)
@@ -29,6 +29,11 @@ export const updatePersistentData: shieldState.UpdatePersistentData = (state, pe | |||
return { ...state, persistentData: { ...state.persistentData, ...persistentData } } | |||
} | |||
|
|||
export const mergeSettingsData: shieldState.MergeSettingsData = (state, settingsData) => { | |||
// return { ...state, settingsData: { ...state.settingsData, ...settingsData } } |
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.
commented out code
@@ -279,6 +282,11 @@ export default function shieldsPanelReducer ( | |||
}) | |||
break | |||
} | |||
case settingsTypes.SET_STORE_SETTINGS_CHANGE: { | |||
const settingsData: SettingsData | Partial<SettingsData> = action.settingsData |
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.
nit: possibly only need Partial<SettingsData>
here
return { | ||
showAdvancedView: showAdvancedViewPref.value | ||
} | ||
export async function getViewPreferences (): Promise<GetViewPreferencesData | {}> { |
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.
How could this function return {}
and not GetViewPreferencesData
? From the code it seems like it should return every property or throw an error. We want the signature to be correct so that callers can trust the type and typescript won't error if we don't check it.
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.
it doesn't but typescript will complain not having it since let newSettings = {}
is empty
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.
I think it's ok to do let newSettings = {} as GetViewPreferencesData
and keep the function return signature correct. WDYT?
@@ -96,6 +96,10 @@ void BraveAddCommonStrings(content::WebUIDataSource* html_source, | |||
IDS_SETTINGS_BRAVE_SHIELDS_HTTPS_EVERYWHERE_CONTROL_LABEL}, | |||
{"noScriptControlLabel", | |||
IDS_SETTINGS_BRAVE_SHIELDS_NO_SCRIPT_CONTROL_LABEL}, | |||
{"lookFeelTitle", |
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.
nit: prefix these strings with braveShields since these are mixed with chromium strings
import settingsActions from '../actions/settingsActions' | ||
|
||
chrome.settingsPrivate.onPrefsChanged.addListener(function (settings) { | ||
settingsActions.settingsDidChange(settings[0]) |
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.
This will fire for every settings change in chromium, not just the 2 we care about - many completely irrelevant things. And it can contain multiple settings, not just 1. I recommend that we:
- Iterate through the array and check for keys we care about (in
shieldsAPI.settingsKeys
) - Send all the relevant keys and new values to
settingsDidChange
@@ -12,4 +14,7 @@ chrome.webNavigation.onBeforeNavigate.addListener(function ({ tabId, url, frameI | |||
chrome.webNavigation.onCommitted.addListener(function ({ tabId, url, frameId }: chrome.webNavigation.WebNavigationTransitionCallbackDetails) { | |||
const isMainFrame: boolean = frameId === 0 | |||
actions.onCommitted(tabId, url, isMainFrame) | |||
// check whether or not the settings store should update based on settings changes. | |||
// this action is needed in the onCommitted phase for edge cases such as when after Brave is re-launched | |||
settingsActions.settingsDataShouldUpdate() |
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.
Seems overkill to fetch this data from the settings API every time a page is navigated, especially if we're subscribing to changes. Perhaps just check once at background script init, just before we subscribe to changes by including the events module?
@cezaraugusto Is there a code loop going on here?
Seems like |
@petemill thanks again, all comments addressed |
Removing milestone as this is not reviewed/approved/merged 👍 |
Change description of --skip_signing
@cezaraugusto can you give this a rebase? 😄 |
30c8e8b
to
cc16fae
Compare
Did a new rebase "just in case" 👀 |
Hello team did a new rebase, can you take a new look? |
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.
Code changes look good! Tests look good too
@petemill can you please give this a re-review 😄 |
Note for @bridiver who was flagged for patch review: See https://github.com/brave/brave-core/pull/3150/files#diff-9f40da04b5dd117f1be1af269b742ee7 |
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.
chromium_src override looks fine
@petemill can you please re-review 😄 |
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.
💯 left some nits
* Get a list of settings values from brave://settings and update if comparison | ||
* against settings values from store deosn't match. | ||
*/ | ||
export const settingsDataShouldUpdate: actions.SettingsDataShouldUpdate = () => { |
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.
nit: this name is confusing. Perhaps fetchAndDispatchSettings
or similar?
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.
Done
(settings: Settings): SetStoreSettingsChangeReturn | ||
} | ||
|
||
export interface SettingsDataShouldUpdate { |
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.
nit: Not sure this belongs here since it's not actually an action, can just be local to its function?
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.
Done
export type Settings = { | ||
key: string | ||
type: any | ||
value: boolean |
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.
nit: this won't always be a boolean but for now it's ok. Maybe leave a (TODO: can support multiple types, see PrefType in chromel.d.ts
).
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.
Done
components/definitions/chromel.d.ts
Outdated
@@ -1,6 +1,6 @@ | |||
/* This Source Code Form is subject to the terms of the Mozilla Public | |||
* License, v. 2.0. If a copy of the MPL was not distributed with this file, | |||
* You can obtain one at http://mozilla.org/MPL/2.0/. */ | |||
* License, v. 2.0. If a copy of the MPL was not distributed with this file, |
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.
did you mean to change this?
Thanks all for the reviews. Considering all feedback is addressed, I'm merging this one. |
fix brave/brave-browser#3121
Test Plan:
on
off
againnpm start
script)npm start
)Reviewer Checklist:
After-merge Checklist:
changes has landed on.