Skip to content
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

Merged
merged 1 commit into from
Mar 5, 2020

Conversation

cezaraugusto
Copy link
Contributor

@cezaraugusto cezaraugusto commented Aug 12, 2019

fix brave/brave-browser#3121

Test Plan:

  1. Visit a website with ads/trackers
  2. Assert resources blocked count is visible
  3. In a new tab, go to brave://settings/shields/
  4. Set the option to show activity count to false
  5. Go back to the website
  6. Assert resources blocked count is not visible. Refresh page
  7. Assert count is still not visible
  8. Go back to brave://settings/shields/ and toggle the option on
  9. Repeat the same operation. Badge count should be visible again
  10. Go back to brave://settings/shields/ and toggle the option off again
  11. Activate the website tab
  12. Quit Brave (cancel npm start script)
  13. Open Brave (npm start)
  14. Ensure counter does not show

Reviewer Checklist:

  • New files have MPL-2.0 license header.
  • Request a security/privacy review as needed.
  • Adequate test coverage exists to prevent regressions
  • Verify test plan is specified in PR before merging to source

After-merge Checklist:

  • The associated issue milestone is set to the smallest version that the
    changes has landed on.
  • All relevant documentation has been updated.

Copy link
Member

@petemill petemill left a 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()
Copy link
Member

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:

  1. It's retrieving the data again, adding a (small) delay.
  2. The app decision logic is now in multiple places.
  3. 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?

Copy link
Contributor Author

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)
Copy link
Member

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?

Copy link
Contributor Author

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 },
Copy link
Member

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 :-( )

Copy link
Contributor Author

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"
Copy link
Member

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.

Copy link
Contributor Author

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 })
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Contributor Author

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

Copy link
Member

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).

Copy link
Member

@petemill petemill Aug 14, 2019

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)
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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)

}
type BraveShieldsSetViewPreferencesData = {
showAdvancedView?: boolean
statsBadgeVisible?: boolean
}
Copy link
Member

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
Copy link
Member

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.

@cezaraugusto
Copy link
Contributor Author

@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 = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: needs type

Copy link
Member

@petemill petemill left a 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
Copy link
Member

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 =
Copy link
Member

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

Copy link
Member

@petemill petemill Aug 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


interface SetStoreSettingsChangeReturn {
type: typeof types.SET_STORE_SETTINGS_CHANGE,
settingsData: SettingsData | Partial<SettingsData>
Copy link
Member

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() : ''
Copy link
Member

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 } }
Copy link
Member

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
Copy link
Member

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 | {}> {
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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",
Copy link
Member

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])
Copy link
Member

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:

  1. Iterate through the array and check for keys we care about (in shieldsAPI.settingsKeys)
  2. 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()
Copy link
Member

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?

@petemill
Copy link
Member

@cezaraugusto Is there a code loop going on here?

chrome.settingsPrivate.onPrefsChanged -> settingsActions.settingsDidChange -> shieldsAPI.setViewPreferences -> chrome.settingsPrivate.setPreference

Seems like onPrefsChanged should just update the state.

@cezaraugusto
Copy link
Contributor Author

@petemill thanks again, all comments addressed

@bsclifton
Copy link
Member

Removing milestone as this is not reviewed/approved/merged 👍

fmarier pushed a commit that referenced this pull request Oct 29, 2019
@bsclifton bsclifton self-requested a review December 2, 2019 06:24
@bsclifton
Copy link
Member

@cezaraugusto can you give this a rebase? 😄

@cezaraugusto cezaraugusto force-pushed the ca-3121 branch 2 times, most recently from 30c8e8b to cc16fae Compare December 3, 2019 18:01
@cezaraugusto
Copy link
Contributor Author

Did a new rebase "just in case" 👀

@cezaraugusto
Copy link
Contributor Author

Hello team did a new rebase, can you take a new look?

@bsclifton
Copy link
Member

Screen Shot 2020-03-02 at 2 11 37 PM

New setting LGTM 👍 Confirmed test plan works great too- will look at the code now

Copy link
Member

@bsclifton bsclifton left a 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

@bsclifton
Copy link
Member

@petemill can you please give this a re-review 😄

@bsclifton
Copy link
Member

Note for @bridiver who was flagged for patch review:
Only patch related change made was adding extra text values to chromium_src/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc

See https://github.com/brave/brave-core/pull/3150/files#diff-9f40da04b5dd117f1be1af269b742ee7

Copy link
Collaborator

@bridiver bridiver left a 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

@bsclifton bsclifton added this to the 1.7.x - Nightly milestone Mar 4, 2020
@bsclifton
Copy link
Member

@petemill can you please re-review 😄

Copy link
Member

@petemill petemill left a 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 = () => {
Copy link
Member

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?

Copy link
Contributor Author

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 {
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -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,
Copy link
Member

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?

@cezaraugusto
Copy link
Contributor Author

Thanks all for the reviews. Considering all feedback is addressed, I'm merging this one.

@cezaraugusto cezaraugusto merged commit 0985439 into master Mar 5, 2020
@cezaraugusto cezaraugusto deleted the ca-3121 branch March 5, 2020 03:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Option to hide Brave Shields activity count
4 participants