From 75b00917cde978161b75cc58c5fa205173404577 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Wed, 29 Apr 2020 14:56:56 +0200 Subject: [PATCH 01/89] add linting for no-param-reassign and fix resulting linting errors --- .eslintrc.js | 9 +- app/content-scripts/blocked_redirect.js | 3 +- app/content-scripts/click_to_play.js | 5 +- app/content-scripts/page_performance.js | 3 +- app/panel-android/actions/trackerActions.js | 78 +++++++++--------- app/panel/actions/SummaryActions.js | 3 +- app/panel/components/Blocking.jsx | 60 +++++++------- .../components/BuildingBlocks/DonutGraph.jsx | 14 ++-- .../components/BuildingBlocks/StatsGraph.jsx | 3 +- app/panel/reducers/blocking.js | 16 ++-- app/panel/reducers/panel.js | 82 ++++++++++++------- app/panel/reducers/settings.js | 58 ++++++------- app/panel/utils/blocking.js | 66 +++++++-------- app/panel/utils/utils.js | 6 +- src/background.js | 7 +- src/classes/Account.js | 3 +- src/classes/CMP.js | 3 +- src/classes/ConfData.js | 3 +- src/classes/EventHandlers.js | 10 ++- src/classes/ExtMessenger.js | 3 +- src/classes/FoundBugs.js | 9 +- src/classes/PanelData.js | 19 +++-- src/classes/PromoModals.js | 2 +- src/classes/SurrogateDb.js | 3 +- src/utils/cliqzSettingImport.js | 10 ++- 25 files changed, 264 insertions(+), 214 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d1af6c8b8..a210dbf6c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -56,7 +56,14 @@ module.exports = { 'newline-per-chained-call': [0, { 'ignoreChainWithDepth': 2 }], 'no-mixed-operators': [0], 'no-nested-ternary': [0], - 'no-param-reassign': [0], // TODO: enable this check + 'no-param-reassign': ['error', { + props: true, + ignorePropertyModificationsFor: [ + 'acc', // for reduce accumulators + 'trackerEl', // for trackers.forEach() + 'categoryEl' // for categories.forEach() + ] + }], 'no-plusplus': [0], 'no-prototype-builtins': [0], // TODO: enable this check 'no-restricted-syntax': [0], // TODO: enable this check diff --git a/app/content-scripts/blocked_redirect.js b/app/content-scripts/blocked_redirect.js index 0a4185f1a..e94acc1de 100644 --- a/app/content-scripts/blocked_redirect.js +++ b/app/content-scripts/blocked_redirect.js @@ -40,7 +40,8 @@ const { sendMessage, sendMessageInPromise } = msg; * but another one, down the chain of redirects - is. It is loaded * by app/blocked_redirect.html when we navigate browser to it. */ - (function BlockedRedirectContentScript(window, document) { + (function BlockedRedirectContentScript(window, doc) { + const document = doc; /** * Calculate window height. * @memberof BlockedRedirectContentScript diff --git a/app/content-scripts/click_to_play.js b/app/content-scripts/click_to_play.js index 3164fc14b..764205c42 100644 --- a/app/content-scripts/click_to_play.js +++ b/app/content-scripts/click_to_play.js @@ -59,11 +59,12 @@ const Click2PlayContentScript = (function(win, doc) { * @memberof Click2PlayContentScript * @package * - * @param {Object} c2pFrame iframe DOM element + * @param {Object} c2pFrameEl iframe DOM element * @param {Object} c2pAppDef replacement data * @param {string} html a fragment of html to be used in replacement. */ - const buildC2P = function(c2pFrame, c2pAppDef, html) { + const buildC2P = function(c2pFrameEl, c2pAppDef, html) { + const c2pFrame = c2pFrameEl; c2pFrame.addEventListener('load', () => { const idoc = c2pFrame.contentDocument; diff --git a/app/content-scripts/page_performance.js b/app/content-scripts/page_performance.js index 2360367dc..467333c4d 100644 --- a/app/content-scripts/page_performance.js +++ b/app/content-scripts/page_performance.js @@ -27,7 +27,8 @@ const { sendMessage } = msg; * Use to call init to initialize functionality * @var {Object} initialized to an object with init method as its property */ -const PageInfo = (function(window, document) { +const PageInfo = (function(window, doc) { + const document = doc; let state = document.readyState; /** * Calculate page domain and latency. Send pageInfo to background.js. diff --git a/app/panel-android/actions/trackerActions.js b/app/panel-android/actions/trackerActions.js index 820224ed7..9594a6d6a 100644 --- a/app/panel-android/actions/trackerActions.js +++ b/app/panel-android/actions/trackerActions.js @@ -218,30 +218,30 @@ export function blockUnBlockAllTrackers({ actionData, state }) { const app_ids = []; if (isSiteTrackers) { - updated_blocking_categories.forEach((category) => { - if (categoryId && category.id !== categoryId) { + updated_blocking_categories.forEach((categoryEl) => { + if (categoryId && categoryEl.id !== categoryId) { return; } - const updated_settings_category = updated_settings_categories.find(item => item.id === category.id); - category.num_blocked = 0; + const updated_settings_category = updated_settings_categories.find(item => item.id === categoryEl.id); + categoryEl.num_blocked = 0; // TODO: change the logic here - category.trackers.forEach((tracker) => { - if (tracker.shouldShow) { - tracker.blocked = block; - const key = tracker.id; + categoryEl.trackers.forEach((trackerEl) => { + if (trackerEl.shouldShow) { + trackerEl.blocked = block; + const key = trackerEl.id; if (block) { if (!app_ids.includes(key)) { app_ids.push(key); } - tracker.ss_allowed = false; - tracker.ss_blocked = false; + trackerEl.ss_allowed = false; + trackerEl.ss_blocked = false; } - if (block || tracker.ss_blocked) { - category.num_blocked += 1; + if (block || trackerEl.ss_blocked) { + categoryEl.num_blocked += 1; updated_app_ids[key] = 1; } else { delete updated_app_ids[key]; @@ -258,19 +258,19 @@ export function blockUnBlockAllTrackers({ actionData, state }) { }); }); } else { - updated_settings_categories.forEach((category) => { - if (categoryId && category.id !== categoryId) { + updated_settings_categories.forEach((categoryEl) => { + if (categoryId && categoryEl.id !== categoryId) { return; } - category.num_blocked = 0; - category.trackers.forEach((tracker) => { - if (tracker.shouldShow) { - tracker.blocked = block; - const key = tracker.id; + categoryEl.num_blocked = 0; + categoryEl.trackers.forEach((trackerEl) => { + if (trackerEl.shouldShow) { + trackerEl.blocked = block; + const key = trackerEl.id; if (block) { - category.num_blocked += 1; + categoryEl.num_blocked += 1; updated_app_ids[key] = 1; } else { delete updated_app_ids[key]; @@ -279,13 +279,13 @@ export function blockUnBlockAllTrackers({ actionData, state }) { }); }); - updated_blocking_categories.forEach((category) => { - category.trackers.forEach((tracker) => { - if (tracker.shouldShow && !tracker.ss_allowed && !tracker.ss_blocked) { - tracker.blocked = block; + updated_blocking_categories.forEach((categoryEl) => { + categoryEl.trackers.forEach((trackerEl) => { + if (trackerEl.shouldShow && !trackerEl.ss_allowed && !trackerEl.ss_blocked) { + trackerEl.blocked = block; } }); - category.num_blocked = category.trackers.filter(tracker => tracker.blocked || tracker.ss_blocked).length; + categoryEl.num_blocked = categoryEl.trackers.filter(trackerEl => trackerEl.blocked || trackerEl.ss_blocked).length; }); } @@ -326,24 +326,24 @@ export function resetSettings({ state }) { const blockingCategories = JSON.parse(JSON.stringify(blocking.categories)) || []; const settingsCategories = JSON.parse(JSON.stringify(settings.categories)) || []; - blockingCategories.forEach((category) => { - category.num_blocked = 0; - category.trackers.forEach((tracker) => { - if (tracker.shouldShow) { - tracker.blocked = false; - tracker.ss_blocked = false; - tracker.ss_allowed = false; + blockingCategories.forEach((categoryEl) => { + categoryEl.num_blocked = 0; + categoryEl.trackers.forEach((trackerEl) => { + if (trackerEl.shouldShow) { + trackerEl.blocked = false; + trackerEl.ss_blocked = false; + trackerEl.ss_allowed = false; } }); }); - settingsCategories.forEach((category) => { - category.num_blocked = 0; - category.trackers.forEach((tracker) => { - if (tracker.shouldShow) { - tracker.blocked = false; - tracker.ss_blocked = false; - tracker.ss_allowed = false; + settingsCategories.forEach((categoryEl) => { + categoryEl.num_blocked = 0; + categoryEl.trackers.forEach((trackerEl) => { + if (trackerEl.shouldShow) { + trackerEl.blocked = false; + trackerEl.ss_blocked = false; + trackerEl.ss_allowed = false; } }); }); diff --git a/app/panel/actions/SummaryActions.js b/app/panel/actions/SummaryActions.js index 3dd212710..c987333c9 100644 --- a/app/panel/actions/SummaryActions.js +++ b/app/panel/actions/SummaryActions.js @@ -72,10 +72,9 @@ export function updateGhosteryPaused(data) { }); if (data.time) { setTimeout(() => { - data.ghosteryPaused = !data.ghosteryPaused; dispatch({ type: UPDATE_GHOSTERY_PAUSED, - data + data: { ...data, ghosteryPaused: !data.ghosteryPaused } }); }, data.time); } diff --git a/app/panel/components/Blocking.jsx b/app/panel/components/Blocking.jsx index cd430a761..37b6e79aa 100644 --- a/app/panel/components/Blocking.jsx +++ b/app/panel/components/Blocking.jsx @@ -95,21 +95,21 @@ class Blocking extends React.Component { const updated_categories = JSON.parse(JSON.stringify(this.props.categories)); // deep clone const updatedUnknownCategory = JSON.parse(JSON.stringify(this.props.unknownCategory)); // deep clone - updated_categories.forEach((category) => { + updated_categories.forEach((categoryEl) => { let count = 0; let show = true; // filter by donut wheel categories - if (filterName !== 'all' && filterName !== category.id) { + if (filterName !== 'all' && filterName !== categoryEl.id) { show = false; } - category.trackers.forEach((tracker) => { - tracker.shouldShow = show; + categoryEl.trackers.forEach((trackerEl) => { + trackerEl.shouldShow = show; count++; }); - category.num_shown = (show) ? count : 0; + categoryEl.num_shown = (show) ? count : 0; }); updatedUnknownCategory.hide = !(filterName === 'all' || filterName === 'unknown'); @@ -124,18 +124,18 @@ class Blocking extends React.Component { setBlockedShow() { const updated_categories = JSON.parse(JSON.stringify(this.props.categories)); // deep clone - updated_categories.forEach((category) => { + updated_categories.forEach((categoryEl) => { let count = 0; - category.trackers.forEach((tracker) => { - const isSbBlocked = this.props.smartBlockActive && tracker.warningSmartBlock; - if ((tracker.blocked && !tracker.ss_allowed) || isSbBlocked || tracker.ss_blocked) { - tracker.shouldShow = true; + categoryEl.trackers.forEach((trackerEl) => { + const isSbBlocked = this.props.smartBlockActive && trackerEl.warningSmartBlock; + if ((trackerEl.blocked && !trackerEl.ss_allowed) || isSbBlocked || trackerEl.ss_blocked) { + trackerEl.shouldShow = true; count++; } else { - tracker.shouldShow = false; + trackerEl.shouldShow = false; } }); - category.num_shown = count; + categoryEl.num_shown = count; }); this.props.actions.updateCategories(updated_categories); @@ -148,18 +148,18 @@ class Blocking extends React.Component { setWarningShow() { const updated_categories = JSON.parse(JSON.stringify(this.props.categories)); // deep clone - updated_categories.forEach((category) => { + updated_categories.forEach((categoryEl) => { let count = 0; - category.trackers.forEach((tracker) => { - if (tracker.warningCompatibility || tracker.warningInsecure || tracker.warningSlow) { - tracker.shouldShow = true; + categoryEl.trackers.forEach((trackerEl) => { + if (trackerEl.warningCompatibility || trackerEl.warningInsecure || trackerEl.warningSlow) { + trackerEl.shouldShow = true; count++; } else { - tracker.shouldShow = false; + trackerEl.shouldShow = false; } }); - category.num_shown = count; + categoryEl.num_shown = count; }); this.props.actions.updateCategories(updated_categories); @@ -172,18 +172,18 @@ class Blocking extends React.Component { setWarningCompatibilityShow() { const updated_categories = JSON.parse(JSON.stringify(this.props.categories)); // deep clone - updated_categories.forEach((category) => { + updated_categories.forEach((categoryEl) => { let count = 0; - category.trackers.forEach((tracker) => { - if (tracker.warningCompatibility) { - tracker.shouldShow = true; + categoryEl.trackers.forEach((trackerEl) => { + if (trackerEl.warningCompatibility) { + trackerEl.shouldShow = true; count++; } else { - tracker.shouldShow = false; + trackerEl.shouldShow = false; } }); - category.num_shown = count; + categoryEl.num_shown = count; }); this.props.actions.updateCategories(updated_categories); @@ -196,18 +196,18 @@ class Blocking extends React.Component { setWarningSlowInsecureShow() { const updated_categories = JSON.parse(JSON.stringify(this.props.categories)); // deep clone - updated_categories.forEach((category) => { + updated_categories.forEach((categoryEl) => { let count = 0; - category.trackers.forEach((tracker) => { - if (tracker.warningInsecure || tracker.warningSlow) { - tracker.shouldShow = true; + categoryEl.trackers.forEach((trackerEl) => { + if (trackerEl.warningInsecure || trackerEl.warningSlow) { + trackerEl.shouldShow = true; count++; } else { - tracker.shouldShow = false; + trackerEl.shouldShow = false; } }); - category.num_shown = count; + categoryEl.num_shown = count; }); this.props.actions.updateCategories(updated_categories); diff --git a/app/panel/components/BuildingBlocks/DonutGraph.jsx b/app/panel/components/BuildingBlocks/DonutGraph.jsx index 67a9bd90a..687253c59 100644 --- a/app/panel/components/BuildingBlocks/DonutGraph.jsx +++ b/app/panel/components/BuildingBlocks/DonutGraph.jsx @@ -264,9 +264,11 @@ class DonutGraph extends React.Component { this._endAngles.set(catId, d.endAngle); return function(t) { - d.startAngle = lerpStartAngle(t); - d.endAngle = lerpEndAngle(t); - return trackerArc(d); + return trackerArc({ + ...d, + startAngle: lerpStartAngle(t), + endAngle: lerpEndAngle(t), + }); }; }); @@ -320,8 +322,10 @@ class DonutGraph extends React.Component { const i = interpolate(d.startAngle, d.endAngle); return function(t) { - d.endAngle = i(t); - return trackerArc(d); + return trackerArc({ + ...d, + endAngle: i(t) + }); }; }) .ease(easeLinear); diff --git a/app/panel/components/BuildingBlocks/StatsGraph.jsx b/app/panel/components/BuildingBlocks/StatsGraph.jsx index 80e89ec4d..e919018ea 100644 --- a/app/panel/components/BuildingBlocks/StatsGraph.jsx +++ b/app/panel/components/BuildingBlocks/StatsGraph.jsx @@ -79,7 +79,8 @@ class StatsGraph extends React.Component { } const data = JSON.parse(JSON.stringify(this.props.data)); - data.forEach((entry) => { + data.forEach((e) => { + const entry = e; entry.date = parseMonth(entry.date); }); diff --git a/app/panel/reducers/blocking.js b/app/panel/reducers/blocking.js index 2aebc09bb..a543bf339 100644 --- a/app/panel/reducers/blocking.js +++ b/app/panel/reducers/blocking.js @@ -168,11 +168,11 @@ const _updateTrackerTrustRestrict = (state, action) => { // update tracker category for site-specific blocking const updated_category = updated_categories[updated_categories.findIndex(item => item.id === msg.cat_id)]; - updated_category.trackers.forEach((tracker) => { - if (tracker.shouldShow) { - if (tracker.id === app_id) { - tracker.ss_allowed = msg.trust; - tracker.ss_blocked = msg.restrict; + updated_category.trackers.forEach((trackerEl) => { + if (trackerEl.shouldShow) { + if (trackerEl.id === app_id) { + trackerEl.ss_allowed = msg.trust; + trackerEl.ss_blocked = msg.restrict; } } }); @@ -242,9 +242,9 @@ const _updateCliqzModuleWhitelist = (state, action) => { addToWhitelist(); } - updatedUnknownCategory.unknownTrackers.forEach((tracker) => { - if (tracker.name === unknownTracker.name) { - tracker.whitelisted = !tracker.whitelisted; + updatedUnknownCategory.unknownTrackers.forEach((trackerEl) => { + if (trackerEl.name === unknownTracker.name) { + trackerEl.whitelisted = !trackerEl.whitelisted; } }); diff --git a/app/panel/reducers/panel.js b/app/panel/reducers/panel.js index 756db8c47..cc8448e79 100644 --- a/app/panel/reducers/panel.js +++ b/app/panel/reducers/panel.js @@ -93,10 +93,14 @@ export default (state = initialState, action) => { return Object.assign({}, state, updated); } case LOGIN_SUCCESS: { - action.payload.text = `${t('panel_signin_success')} ${action.payload.email}`; - action.payload.classes = 'success'; - action.payload.overrideNotificationShown = true; - const updated = _showNotification(state, action); + const notificationAction = { + payload: { + text: `${t('panel_signin_success')} ${action.payload.email}`, + classes: 'success', + overrideNotificationShown: true, + } + }; + const updated = _showNotification(state, notificationAction); return Object.assign({}, state, updated, { loggedIn: true, }); @@ -114,18 +118,26 @@ export default (state = initialState, action) => { errorText = t('server_error_message'); } }); - action.payload.text = errorText; - action.payload.classes = 'alert'; - action.payload.overrideNotificationShown = true; - const updated = _showNotification(state, action); + const notificationAction = { + payload: { + text: errorText, + classes: 'alert', + overrideNotificationShown: true, + } + }; + const updated = _showNotification(state, notificationAction); return Object.assign({}, state, updated); } case REGISTER_SUCCESS: { const { email } = action.payload; - action.payload.text = t('panel_email_verification_sent', email); - action.payload.classes = 'success'; - action.payload.overrideNotificationShown = true; - const updated = _showNotification(state, action); + const notificationAction = { + payload: { + text: t('panel_email_verification_sent', email), + classes: 'success', + overrideNotificationShown: true, + } + }; + const updated = _showNotification(state, notificationAction); return Object.assign({}, state, updated, { email }); @@ -145,10 +157,14 @@ export default (state = initialState, action) => { errorText = t('server_error_message'); } }); - action.payload.text = errorText; - action.payload.classes = 'alert'; - action.payload.overrideNotificationShown = true; - const updated = _showNotification(state, action); + const notificationAction = { + payload: { + text: errorText, + classes: 'alert', + overrideNotificationShown: true, + } + }; + const updated = _showNotification(state, notificationAction); return Object.assign({}, state, updated); } case LOGOUT_SUCCESS: { @@ -157,20 +173,24 @@ export default (state = initialState, action) => { } // @TODO? // case LOGOUT_SUCCESS: { - // action.payload = { - // text: 'Logged out successfully.', - // classes: 'success', + // const notificationAction = { + // payload: { + // text: 'Logged out successfully.', + // classes: 'success', + // } // }; - // const updated = _showNotification(state, action); + // const updated = _showNotification(state, notificationAction); // return Object.assign({}, state, updated); // } case RESET_PASSWORD_SUCCESS: { - action.payload = { - text: t('banner_check_your_email_title'), - classes: 'success', - overrideNotificationShown: true, + const notificationAction = { + payload: { + text: t('banner_check_your_email_title'), + classes: 'success', + overrideNotificationShown: true, + } }; - const updated = _showNotification(state, action); + const updated = _showNotification(state, notificationAction); return Object.assign({}, state, updated); } case RESET_PASSWORD_FAIL: { @@ -186,10 +206,14 @@ export default (state = initialState, action) => { errorText = t('server_error_message'); } }); - action.payload.text = errorText; - action.payload.classes = 'alert'; - action.payload.overrideNotificationShown = true; - const updated = _showNotification(state, action); + const notificationAction = { + payload: { + text: errorText, + classes: 'alert', + overrideNotificationShown: true, + } + }; + const updated = _showNotification(state, notificationAction); return Object.assign({}, state, updated); } case TOGGLE_CLIQZ_FEATURE: { diff --git a/app/panel/reducers/settings.js b/app/panel/reducers/settings.js index 10e9f29c8..9618e474e 100644 --- a/app/panel/reducers/settings.js +++ b/app/panel/reducers/settings.js @@ -165,14 +165,14 @@ const _exportSettings = (state, action) => { */ const _importSettingsDialog = (state, action) => { const result = action.data; - const updated_actionSuccess = state.actionSuccess; + let updated_actionSuccess = state.actionSuccess; let updated_importResultText = state.importResultText; if (result === true) { // showBrowseWindow was successful window.close(); } else { - state.updated_actionSuccess = false; + updated_actionSuccess = false; updated_importResultText = t('settings_import_dialog_error'); } @@ -283,19 +283,19 @@ const _updateTrackerDatabase = (state, action) => { const _updateSearchValue = (state, action) => { const query = action.data || ''; const updated_categories = JSON.parse(JSON.stringify(state.categories)) || []; // deep clone - updated_categories.forEach((category) => { - category.num_total = 0; - category.num_blocked = 0; - category.trackers.forEach((tracker) => { + updated_categories.forEach((categoryEl) => { + categoryEl.num_total = 0; + categoryEl.num_blocked = 0; + categoryEl.trackers.forEach((trackerEl) => { if (query) { - tracker.shouldShow = !!(tracker.name.toLowerCase().indexOf(query) !== -1); + trackerEl.shouldShow = !!(trackerEl.name.toLowerCase().indexOf(query) !== -1); } else { - tracker.shouldShow = true; + trackerEl.shouldShow = true; } - if (tracker.shouldShow) { - category.num_total++; - if (tracker.blocked) { - category.num_blocked++; + if (trackerEl.shouldShow) { + categoryEl.num_total++; + if (trackerEl.blocked) { + categoryEl.num_blocked++; } } }); @@ -319,34 +319,34 @@ const _updateSearchValue = (state, action) => { const _filter = (state, action) => { const updated_categories = JSON.parse(JSON.stringify(state.categories)) || []; // deep clone const new_app_ids = state.new_app_ids || []; - updated_categories.forEach((category) => { - category.num_total = 0; - category.num_blocked = 0; - category.trackers.forEach((tracker) => { + updated_categories.forEach((categoryEl) => { + categoryEl.num_total = 0; + categoryEl.num_blocked = 0; + categoryEl.trackers.forEach((trackerEl) => { switch (action.data) { case 'all': - tracker.shouldShow = true; - category.num_total++; - if (tracker.blocked) { - category.num_blocked++; + trackerEl.shouldShow = true; + categoryEl.num_total++; + if (trackerEl.blocked) { + categoryEl.num_blocked++; } break; case 'blocked': - tracker.shouldShow = tracker.blocked; - if (tracker.shouldShow) { - category.num_total++; + trackerEl.shouldShow = trackerEl.blocked; + if (trackerEl.shouldShow) { + categoryEl.num_total++; } break; case 'unblocked': - tracker.shouldShow = !tracker.blocked; - if (tracker.shouldShow) { - category.num_total++; + trackerEl.shouldShow = !trackerEl.blocked; + if (trackerEl.shouldShow) { + categoryEl.num_total++; } break; case 'new': - tracker.shouldShow = !!(new_app_ids.indexOf(+tracker.id) !== -1); - if (tracker.shouldShow) { - category.num_total++; + trackerEl.shouldShow = !!(new_app_ids.indexOf(+trackerEl.id) !== -1); + if (trackerEl.shouldShow) { + categoryEl.num_total++; } break; default: diff --git a/app/panel/utils/blocking.js b/app/panel/utils/blocking.js index c797e87df..b3f2c5e0d 100644 --- a/app/panel/utils/blocking.js +++ b/app/panel/utils/blocking.js @@ -30,19 +30,19 @@ export function updateSummaryBlockingCount(categories = [], smartBlock, updateTr let numTotalSbBlocked = 0; let numTotalSbUnblocked = 0; - categories.forEach((category) => { - category.trackers.forEach((tracker) => { + categories.forEach((categoryEl) => { + categoryEl.trackers.forEach((trackerEl) => { numTotal++; - const sbBlocked = smartBlock.blocked.hasOwnProperty(tracker.id); - const sbUnblocked = smartBlock.unblocked.hasOwnProperty(tracker.id); + const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); + const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); - if (tracker.ss_blocked || sbBlocked || (tracker.blocked && !tracker.ss_allowed && !sbUnblocked)) { + if (trackerEl.ss_blocked || sbBlocked || (trackerEl.blocked && !trackerEl.ss_allowed && !sbUnblocked)) { numTotalBlocked++; } - if (tracker.ss_blocked) { + if (trackerEl.ss_blocked) { numTotalSsBlocked++; } - if (tracker.ss_allowed) { + if (trackerEl.ss_allowed) { numTotalSsUnblocked++; } if (sbBlocked) { @@ -78,17 +78,17 @@ export function updateBlockAllTrackers(state, action) { const { smartBlockActive } = action.data; const smartBlock = smartBlockActive && action.data.smartBlock || { blocked: {}, unblocked: {} }; - updated_categories.forEach((category) => { - category.num_blocked = 0; - category.trackers.forEach((tracker) => { - const sbBlocked = smartBlock.blocked.hasOwnProperty(tracker.id); - const sbUnblocked = smartBlock.unblocked.hasOwnProperty(tracker.id); + updated_categories.forEach((categoryEl) => { + categoryEl.num_blocked = 0; + categoryEl.trackers.forEach((trackerEl) => { + const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); + const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); - if (tracker.shouldShow) { - tracker.blocked = blocked; - const key = tracker.id; + if (trackerEl.shouldShow) { + trackerEl.blocked = blocked; + const key = trackerEl.id; if (sbBlocked || (blocked && !sbUnblocked)) { - category.num_blocked++; + categoryEl.num_blocked++; } if (blocked) { updated_app_ids[key] = 1; @@ -123,13 +123,13 @@ export function updateCategoryBlocked(state, action) { const catIndex = updated_categories.findIndex(item => item.id === action.data.category); const updated_category = updated_categories[catIndex]; updated_category.num_blocked = 0; - updated_category.trackers.forEach((tracker) => { - const sbBlocked = smartBlock.blocked.hasOwnProperty(tracker.id); - const sbUnblocked = smartBlock.unblocked.hasOwnProperty(tracker.id); + updated_category.trackers.forEach((trackerEl) => { + const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); + const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); - if (tracker.shouldShow) { - tracker.blocked = blocked; - const key = tracker.id; + if (trackerEl.shouldShow) { + trackerEl.blocked = blocked; + const key = trackerEl.id; if (sbBlocked || (blocked && !sbUnblocked)) { updated_category.num_blocked++; } @@ -160,8 +160,8 @@ export function updateCategoryBlocked(state, action) { export function toggleExpandAll(state, action) { sendMessage('setPanelData', { expand_all_trackers: action.data }); const updated_categories = JSON.parse(JSON.stringify(state.categories)); // deep clone - updated_categories.forEach((category) => { - category.expanded = action.data; + updated_categories.forEach((categoryEl) => { + categoryEl.expanded = action.data; }); return { categories: updated_categories, @@ -193,21 +193,21 @@ export function updateTrackerBlocked(state, action) { const updated_category = updated_categories[catIndex]; updated_category.num_blocked = 0; - updated_category.trackers.forEach((tracker) => { - const sbBlocked = smartBlock.blocked.hasOwnProperty(tracker.id); - const sbUnblocked = smartBlock.unblocked.hasOwnProperty(tracker.id); - - if (tracker.shouldShow) { - if (tracker.id === action.data.app_id) { - tracker.blocked = blocked; - const key = tracker.id; + updated_category.trackers.forEach((trackerEl) => { + const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); + const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); + + if (trackerEl.shouldShow) { + if (trackerEl.id === action.data.app_id) { + trackerEl.blocked = blocked; + const key = trackerEl.id; if (blocked) { updated_app_ids[key] = 1; } else { delete updated_app_ids[key]; } } - if (sbBlocked || (tracker.blocked && !sbUnblocked)) { + if (sbBlocked || (trackerEl.blocked && !sbUnblocked)) { updated_category.num_blocked++; } } diff --git a/app/panel/utils/utils.js b/app/panel/utils/utils.js index 2e4a07148..7b91b551b 100644 --- a/app/panel/utils/utils.js +++ b/app/panel/utils/utils.js @@ -35,9 +35,9 @@ export function updateObject(obj, key, value) { * @return {Object} new object */ export function removeFromObject(obj, key) { - return Object.keys(obj).filter(k => k !== key.toString()).reduce((result, k) => { - result[k] = obj[k]; - return result; + return Object.keys(obj).filter(k => k !== key.toString()).reduce((acc, k) => { + acc[k] = obj[k]; + return acc; }, {}); } diff --git a/src/background.js b/src/background.js index aaad084d4..6b55f460d 100644 --- a/src/background.js +++ b/src/background.js @@ -879,10 +879,11 @@ function onMessageHandler(request, sender, callback) { if (name === 'account.getUser') { account.getUser(message) .then((user) => { - if (user) { - user.subscriptionsPlus = account.hasScopesUnverified(['subscriptions:plus']); + const foundUser = user; + if (foundUser) { + foundUser.subscriptionsPlus = account.hasScopesUnverified(['subscriptions:plus']); } - callback({ user }); + callback({ foundUser }); }) .catch((err) => { callback({ errors: _getJSONAPIErrorsObject(err) }); diff --git a/src/classes/Account.js b/src/classes/Account.js index 4dfd48a30..7b1ab9e8f 100644 --- a/src/classes/Account.js +++ b/src/classes/Account.js @@ -520,7 +520,8 @@ class Account { * * @return {Promise} user settings json or error */ - _setConfUserSettings = (settings) => { + _setConfUserSettings = (s) => { + const settings = s; log('SET USER SETTINGS', settings); if (IS_CLIQZ) { settings.enable_human_web = false; diff --git a/src/classes/CMP.js b/src/classes/CMP.js index e22ad49d4..0c674c0d9 100644 --- a/src/classes/CMP.js +++ b/src/classes/CMP.js @@ -52,7 +52,8 @@ class CMP { return getJson(URL).then((data) => { if (data && (!conf.cmp_version || data.Version > conf.cmp_version)) { // set default dismiss - data.Campaigns.forEach((campaign) => { + data.Campaigns.forEach((c) => { + const campaign = c; if (campaign.Dismiss === 0) { campaign.Dismiss = 10; } diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index cd62556e2..453857aa8 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -47,7 +47,8 @@ class ConfData { * This method is called once on startup. */ init() { - return prefsGet().then((data) => { + return prefsGet().then((d) => { + const data = d; const nowTime = Number(new Date().getTime()); const _initProperty = (name, value) => { if (data[name] === null || typeof (data[name]) === 'undefined') { diff --git a/src/classes/EventHandlers.js b/src/classes/EventHandlers.js index 55ba5f4e1..b58f9f055 100644 --- a/src/classes/EventHandlers.js +++ b/src/classes/EventHandlers.js @@ -328,10 +328,11 @@ class EventHandlers { * + Speed this up by making it asynchronous when blocking is disabled? * + Also speed it up for blocking-whitelisted pages (by delaying isBug scanning)? * - * @param {Object} details event data + * @param {Object} d event data * @return {Object} optionaly return {cancel: true} to force dropping the request */ - onBeforeRequest(details) { + onBeforeRequest(d) { + const details = d; const tab_id = details.tabId; const request_id = details.requestId; @@ -450,10 +451,11 @@ class EventHandlers { * Handler for webRequest.onBeforeSendHeaders event. * Called each time that an HTTP(S) request is about to send headers * - * @param {Object} details event data + * @param {Object} d event data * @return {Object} optionally return headers to send */ - onBeforeSendHeaders(details) { + onBeforeSendHeaders(d) { + const details = d; for (let i = 0; i < details.requestHeaders.length; ++i) { // Fetch requests in Firefox web-extension has a flaw. They attach // origin: moz-extension//ID , which is specific to a user. diff --git a/src/classes/ExtMessenger.js b/src/classes/ExtMessenger.js index badcc0ecc..3dbf267a2 100644 --- a/src/classes/ExtMessenger.js +++ b/src/classes/ExtMessenger.js @@ -67,7 +67,8 @@ export default class KordInjector { } _createModuleWrapper(moduleName) { - return new Spanan((message) => { + return new Spanan((m) => { + const message = m; message.moduleName = moduleName; this.messenger.sendMessage(this.extensionId, message); }); diff --git a/src/classes/FoundBugs.js b/src/classes/FoundBugs.js index 2d1dd854a..d1b2aa80b 100644 --- a/src/classes/FoundBugs.js +++ b/src/classes/FoundBugs.js @@ -247,9 +247,9 @@ class FoundBugs { if (sorted) { cats_arr.sort((a, b) => { - a = a.name.toLowerCase(); - b = b.name.toLowerCase(); - return (a > b ? 1 : (a < b ? -1 : 0)); + const a1 = a.name.toLowerCase(); + const b1 = b.name.toLowerCase(); + return (a1 > b1 ? 1 : (a1 < b1 ? -1 : 0)); }); } @@ -420,7 +420,8 @@ class FoundBugs { */ _checkForCompatibilityIssues(tab_id, tab_url) { const { apps, appsMetadata, issueCounts } = this._foundApps[tab_id]; - apps.forEach((app) => { + apps.forEach((a) => { + const app = a; const { id } = app; if (appsMetadata[id].needsCompatibilityCheck) { app.hasCompatibilityIssue = app.blocked ? compDb.hasIssue(id, tab_url) : false; diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 281433511..43aaaab1e 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -575,9 +575,10 @@ class PanelData { /** * Update Conf properties with new data from the UI. * Called via setPanelData message. - * @param {Object} data + * @param {Object} d */ - set(data) { + set(d) { + const data = d; let syncSetDataChanged = false; if (IS_CLIQZ) { @@ -787,13 +788,13 @@ class PanelData { _buildGlobalCategories() { const categories = bugDb.db.categories || []; const selectedApps = conf.selected_app_ids || {}; - categories.forEach((category) => { - const { trackers } = category; - category.num_blocked = 0; - trackers.forEach((tracker) => { - tracker.blocked = selectedApps.hasOwnProperty(tracker.id); - if (tracker.blocked) { - category.num_blocked++; + categories.forEach((categoryEl) => { + const { trackers } = categoryEl; + categoryEl.num_blocked = 0; + trackers.forEach((trackerEl) => { + trackerEl.blocked = selectedApps.hasOwnProperty(trackerEl.id); + if (trackerEl.blocked) { + categoryEl.num_blocked++; } }); }); diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index 2f49320a9..292ea0d9c 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -102,7 +102,7 @@ class PromoModals { static _hasEngagedFrequently() { const { engaged_daily_count } = conf.metrics || []; - const very_engaged_days = engaged_daily_count.reduce((acc, count) => (count >= DAILY_INSIGHTS_TARGET ? ++acc : acc), 0); + const very_engaged_days = engaged_daily_count.reduce((acc, count) => (count >= DAILY_INSIGHTS_TARGET ? acc + 1 : acc), 0); return very_engaged_days >= WEEKLY_INSIGHTS_TARGET; } diff --git a/src/classes/SurrogateDb.js b/src/classes/SurrogateDb.js index 5fc99ba05..564705c27 100644 --- a/src/classes/SurrogateDb.js +++ b/src/classes/SurrogateDb.js @@ -57,7 +57,8 @@ class SurrogateDb extends Updatable { processList(fromMemory, data) { log('processing surrogates...'); - data.mappings.forEach((s) => { + data.mappings.forEach((souragate) => { + const s = souragate; s.code = data.surrogates[s.sid]; // convert single values to arrays first diff --git a/src/utils/cliqzSettingImport.js b/src/utils/cliqzSettingImport.js index 581409aa3..ba3427e41 100644 --- a/src/utils/cliqzSettingImport.js +++ b/src/utils/cliqzSettingImport.js @@ -34,10 +34,11 @@ function _promiseTimeout(timeout) { * @private * * @param {Object} cliqz - * @param {Object} conf + * @param {Object} c conf * @return {Promise} */ -function _runCliqzSettingsImport(cliqz, conf) { +function _runCliqzSettingsImport(cliqz, c) { + const conf = c; log('CliqzSettingsImport: Run Cliqz settings importer'); const inject = new KordInjector(); inject.init(); @@ -90,9 +91,10 @@ function _runCliqzSettingsImport(cliqz, conf) { * @memberOf BackgroundUtils * * @param {Object} cliqz - * @param {Object} conf + * @param {Object} c conf */ -export function importCliqzSettings(cliqz, conf) { +export function importCliqzSettings(cliqz, c) { + const conf = c; log('checking cliqz import', conf.cliqz_import_state); if (!conf.cliqz_import_state) { _runCliqzSettingsImport(cliqz, conf).then(() => { From 8d37082d9591de536dcde05b5efafb2aa19a09cf Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Wed, 29 Apr 2020 15:15:48 +0200 Subject: [PATCH 02/89] add linting for prefer-object-spread and fix resulting linting errors --- .eslintrc.js | 2 +- app/Account/AccountReducer.js | 44 ++++++------- app/hub/Views/AppView/AppViewReducer.js | 9 +-- app/hub/Views/AppView/index.js | 4 +- app/hub/Views/CreateAccountView/index.js | 8 +-- app/hub/Views/HomeView/HomeViewReducer.js | 25 ++++---- app/hub/Views/HomeView/index.js | 4 +- app/hub/Views/LogInView/index.js | 8 +-- app/hub/Views/PlusView/index.js | 8 +-- app/hub/Views/ProductsView/index.js | 2 +- app/hub/Views/SetupView/SetupViewReducer.js | 47 ++++++-------- app/hub/Views/SetupView/index.js | 9 +-- .../SetupViews/SetupAntiSuiteView/index.js | 9 +-- .../SetupViews/SetupBlockingDropdown/index.js | 4 +- .../SetupViews/SetupBlockingView/index.js | 9 +-- .../Views/SetupViews/SetupDoneView/index.js | 9 +-- .../SetupViews/SetupHumanWebView/index.js | 7 ++- .../Views/SetupViews/SetupNavigation/index.js | 2 +- app/hub/Views/SideNavigationView/index.js | 8 +-- .../Views/TutorialView/TutorialViewReducer.js | 19 +++--- app/hub/Views/TutorialView/index.js | 4 +- .../TutorialAntiSuiteView/index.js | 4 +- .../TutorialBlockingView/index.js | 4 +- .../TutorialViews/TutorialLayoutView/index.js | 4 +- .../TutorialViews/TutorialNavigation/index.js | 2 +- .../TutorialTrackerListView/index.js | 4 +- .../TutorialViews/TutorialTrustView/index.js | 4 +- .../TutorialViews/TutorialVideoView/index.js | 4 +- app/panel-android/components/Panel.jsx | 2 +- app/panel/components/DetailMenu.jsx | 2 +- .../components/Settings/TrustAndRestrict.jsx | 2 +- app/panel/components/Stats.jsx | 10 +-- .../containers/AccountSuccessContainer.js | 5 +- app/panel/containers/BlockingContainer.js | 5 +- .../containers/CreateAccountContainer.js | 5 +- app/panel/containers/DetailContainer.js | 6 +- app/panel/containers/HeaderContainer.js | 5 +- app/panel/containers/LoginContainer.js | 5 +- app/panel/containers/PanelContainer.js | 11 +++- app/panel/containers/RewardsContainer.js | 4 +- app/panel/containers/SettingsContainer.js | 5 +- app/panel/containers/StatsContainer.js | 2 +- app/panel/containers/SubscriptionContainer.js | 9 +-- app/panel/containers/SummaryContainer.js | 6 +- app/panel/reducers/blocking.js | 24 +++---- app/panel/reducers/panel.js | 40 ++++++------ app/panel/reducers/rewards.js | 4 +- app/panel/reducers/settings.js | 33 +++++----- app/panel/reducers/summary.js | 13 ++-- app/panel/utils/utils.js | 2 +- .../ForgotPassword/ForgotPasswordContainer.js | 2 +- .../PromoModal/PromoModalContainer.js | 4 +- src/classes/Account.js | 2 +- src/classes/Metrics.js | 12 ++-- src/classes/PanelData.js | 62 +++++++++---------- 55 files changed, 278 insertions(+), 271 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a210dbf6c..a86b207c8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -72,7 +72,7 @@ module.exports = { 'no-unused-vars': [1], 'no-useless-escape': [1], 'operator-linebreak': [0], - 'prefer-object-spread': [0], // TODO: enable this check + 'prefer-object-spread': ['error'], 'space-before-function-paren': [2, 'never'], 'template-curly-spacing': [0], diff --git a/app/Account/AccountReducer.js b/app/Account/AccountReducer.js index 48076f6b0..8365f113a 100644 --- a/app/Account/AccountReducer.js +++ b/app/Account/AccountReducer.js @@ -39,61 +39,62 @@ export default (state = initialState, action) => { case UPDATE_PANEL_DATA: { const { account } = action.data; if (account === null) { - return Object.assign({}, initialState); + return { ...initialState }; } const { userID, user, userSettings, subscriptionData } = account; - return Object.assign({}, state, { + return { + ...state, loggedIn: true, userID, user, userSettings, - subscriptionData, - }); + subscriptionData + }; } case REGISTER_SUCCESS: case LOGIN_SUCCESS: { - return Object.assign({}, state, { - loggedIn: true, - }); + return { ...state, loggedIn: true }; } case LOGOUT_SUCCESS: { - return Object.assign({}, initialState); + return { ...initialState }; } case GET_USER_SUCCESS: { const { user } = action.payload; - return Object.assign({}, state, { + return { + ...state, loggedIn: true, user - }); + }; } case GET_USER_SETTINGS_SUCCESS: { const { settings } = action.payload; - return Object.assign({}, state, { + return { + ...state, loggedIn: true, userSettings: settings - }); + }; } case GET_USER_SUBSCRIPTION_DATA_FAIL: { const { subscriptionData } = initialState; - return Object.assign({}, state, { - subscriptionData, - }); + return { ...state, subscriptionData }; } case GET_USER_SUBSCRIPTION_DATA_SUCCESS: { const { subscriptionData } = action.payload; - return Object.assign({}, state, { + return { + ...state, loggedIn: true, subscriptionData - }); + }; } case RESET_PASSWORD_SUCCESS: { const toastMessage = t('banner_check_your_email_title'); - return Object.assign({}, state, { + return { + ...state, toastMessage, resetPasswordError: false - }); + }; } case RESET_PASSWORD_FAIL: { const { errors } = action.payload; @@ -108,10 +109,11 @@ export default (state = initialState, action) => { errorText = t('server_error_message'); } }); - return Object.assign({}, state, { + return { + ...state, toastMessage: errorText, resetPasswordError: true - }); + }; } default: return state; diff --git a/app/hub/Views/AppView/AppViewReducer.js b/app/hub/Views/AppView/AppViewReducer.js index 970f51132..94ee686f2 100644 --- a/app/hub/Views/AppView/AppViewReducer.js +++ b/app/hub/Views/AppView/AppViewReducer.js @@ -19,12 +19,13 @@ function AppViewReducer(state = initialState, action) { switch (action.type) { case SET_TOAST: { const { toastMessage, toastClass } = action.data; - return Object.assign({}, state, { - app: Object.assign({}, { + return { + ...state, + app: { toastMessage, toastClass - }), - }); + } + }; } default: return state; } diff --git a/app/hub/Views/AppView/index.js b/app/hub/Views/AppView/index.js index 8fc86ded9..9d9d20ffc 100644 --- a/app/hub/Views/AppView/index.js +++ b/app/hub/Views/AppView/index.js @@ -23,7 +23,7 @@ import AppViewReducer from './AppViewReducer'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.app); +const mapStateToProps = state => ({ ...state.app }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -32,7 +32,7 @@ const mapStateToProps = state => Object.assign({}, state.app); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { setToast }), dispatch), + actions: bindActionCreators({ setToast }, dispatch), }); export const reducer = AppViewReducer; diff --git a/app/hub/Views/CreateAccountView/index.js b/app/hub/Views/CreateAccountView/index.js index 2fe88b9f9..da18f1190 100644 --- a/app/hub/Views/CreateAccountView/index.js +++ b/app/hub/Views/CreateAccountView/index.js @@ -24,7 +24,7 @@ import { setToast } from '../AppView/AppViewActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.account); +const mapStateToProps = state => ({ ...state.account }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -33,11 +33,11 @@ const mapStateToProps = state => Object.assign({}, state.account); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { + actions: bindActionCreators({ setToast, register, - getUser, - }), dispatch), + getUser + }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(CreateAccountViewContainer); diff --git a/app/hub/Views/HomeView/HomeViewReducer.js b/app/hub/Views/HomeView/HomeViewReducer.js index 1582d7e62..2461abac6 100644 --- a/app/hub/Views/HomeView/HomeViewReducer.js +++ b/app/hub/Views/HomeView/HomeViewReducer.js @@ -23,26 +23,25 @@ function HomeViewReducer(state = initialState, action) { tutorial_complete, enable_metrics, } = action.data; - return Object.assign({}, state, { - home: Object.assign({}, state.home, { + return { + ...state, + home: { + ...state.home, setup_complete, tutorial_complete, - enable_metrics, - }), - }); + enable_metrics + } + }; } case MARK_PREMIUM_PROMO_MODAL_SHOWN: { - return Object.assign({}, state, { - home: Object.assign({}, state.home, { - premium_promo_modal_shown: true, - }) - }); + return { + ...state, + home: { ...state.home, premium_promo_modal_shown: true } + }; } case SET_METRICS: { const { enable_metrics } = action.data; - return Object.assign({}, state, { - home: Object.assign({}, state.home, { enable_metrics }), - }); + return { ...state, home: { ...state.home, enable_metrics } }; } default: return state; diff --git a/app/hub/Views/HomeView/index.js b/app/hub/Views/HomeView/index.js index 4e4a35da3..9a947b809 100644 --- a/app/hub/Views/HomeView/index.js +++ b/app/hub/Views/HomeView/index.js @@ -25,7 +25,7 @@ import { getUser } from '../../../Account/AccountActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.home, state.account); +const mapStateToProps = state => ({ ...state.home, ...state.account }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -34,7 +34,7 @@ const mapStateToProps = state => Object.assign({}, state.home, state.account); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, HomeViewActions, { getUser }), dispatch), + actions: bindActionCreators({ ...HomeViewActions, getUser }, dispatch), }); export const reducer = HomeViewReducer; diff --git a/app/hub/Views/LogInView/index.js b/app/hub/Views/LogInView/index.js index e53caf771..ebcd60244 100644 --- a/app/hub/Views/LogInView/index.js +++ b/app/hub/Views/LogInView/index.js @@ -25,7 +25,7 @@ import { setToast } from '../AppView/AppViewActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.account); +const mapStateToProps = state => ({ ...state.account }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -34,13 +34,13 @@ const mapStateToProps = state => Object.assign({}, state.account); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { + actions: bindActionCreators({ setToast, login, getUser, getUserSettings, - getTheme, - }), dispatch), + getTheme + }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(LogInViewContainer); diff --git a/app/hub/Views/PlusView/index.js b/app/hub/Views/PlusView/index.js index ee753c74b..cd19404b6 100644 --- a/app/hub/Views/PlusView/index.js +++ b/app/hub/Views/PlusView/index.js @@ -24,7 +24,7 @@ import { getUser } from '../../../Account/AccountActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.account); +const mapStateToProps = state => ({ ...state.account }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -33,10 +33,10 @@ const mapStateToProps = state => Object.assign({}, state.account); * @memberof TutorialContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { + actions: bindActionCreators({ sendPing, - getUser, - }), dispatch), + getUser + }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(PlusViewContainer); diff --git a/app/hub/Views/ProductsView/index.js b/app/hub/Views/ProductsView/index.js index 977e4bf28..955600e4c 100644 --- a/app/hub/Views/ProductsView/index.js +++ b/app/hub/Views/ProductsView/index.js @@ -24,7 +24,7 @@ import { sendPing } from '../AppView/AppViewActions'; * @memberof TutorialContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { sendPing }), dispatch), + actions: bindActionCreators({ sendPing }, dispatch), }); export default connect(null, mapDispatchToProps)(ProductsViewContainer); diff --git a/app/hub/Views/SetupView/SetupViewReducer.js b/app/hub/Views/SetupView/SetupViewReducer.js index e17dcf514..46c5c51a1 100644 --- a/app/hub/Views/SetupView/SetupViewReducer.js +++ b/app/hub/Views/SetupView/SetupViewReducer.js @@ -32,9 +32,7 @@ function SetupViewReducer(state = initialState, action) { case GET_SETUP_SHOW_WARNING_OVERRIDE: // Same as SET_SETUP_SHOW_WARNING_OVERRIDE case SET_SETUP_SHOW_WARNING_OVERRIDE: { const { setup_show_warning_override } = action.data; - return Object.assign({}, state, { - setup: Object.assign({}, state.setup, { setup_show_warning_override }), - }); + return { ...state, setup: { ...state.setup, setup_show_warning_override } }; } case INIT_SETUP_PROPS: { const { @@ -56,7 +54,8 @@ function SetupViewReducer(state = initialState, action) { textNext, textDone, } = navigation; - return Object.assign({}, state, { + return { + ...state, setup: { navigation: { activeIndex, @@ -74,8 +73,8 @@ function SetupViewReducer(state = initialState, action) { enable_smart_block, enable_ghostery_rewards, enable_human_web, - }, - }); + } + }; } case SET_SETUP_NAVIGATION: { const { @@ -87,8 +86,10 @@ function SetupViewReducer(state = initialState, action) { textNext, textDone, } = action.data; - return Object.assign({}, state, { - setup: Object.assign({}, state.setup, { + return { + ...state, + setup: { + ...state.setup, navigation: { activeIndex, hrefPrev, @@ -97,51 +98,39 @@ function SetupViewReducer(state = initialState, action) { textPrev, textNext, textDone, - }, - }), - }); + } + } + }; } // Setup Blocking View case SET_BLOCKING_POLICY: { const { blockingPolicy } = action.data; - return Object.assign({}, state, { - setup: Object.assign({}, state.setup, { blockingPolicy }), - }); + return { ...state, setup: { ...state.setup, blockingPolicy } }; } // Setup Anti-Suite View case SET_ANTI_TRACKING: { const { enable_anti_tracking } = action.data; - return Object.assign({}, state, { - setup: Object.assign({}, state.setup, { enable_anti_tracking }), - }); + return { ...state, setup: { ...state.setup, enable_anti_tracking } }; } case SET_AD_BLOCK: { const { enable_ad_block } = action.data; - return Object.assign({}, state, { - setup: Object.assign({}, state.setup, { enable_ad_block }), - }); + return { ...state, setup: { ...state.setup, enable_ad_block } }; } case SET_SMART_BLOCK: { const { enable_smart_block } = action.data; - return Object.assign({}, state, { - setup: Object.assign({}, state.setup, { enable_smart_block }), - }); + return { ...state, setup: { ...state.setup, enable_smart_block } }; } case SET_GHOSTERY_REWARDS: { const { enable_ghostery_rewards } = action.data; - return Object.assign({}, state, { - setup: Object.assign({}, state.setup, { enable_ghostery_rewards }), - }); + return { ...state, setup: { ...state.setup, enable_ghostery_rewards } }; } // Setup Human Web View case SET_HUMAN_WEB: { const { enable_human_web } = action.data; - return Object.assign({}, state, { - setup: Object.assign({}, state.setup, { enable_human_web }), - }); + return { ...state, setup: { ...state.setup, enable_human_web } }; } default: return state; diff --git a/app/hub/Views/SetupView/index.js b/app/hub/Views/SetupView/index.js index ab86ba273..595c53903 100644 --- a/app/hub/Views/SetupView/index.js +++ b/app/hub/Views/SetupView/index.js @@ -34,7 +34,7 @@ import { setSetupComplete } from '../SetupViews/SetupDoneView/SetupDoneViewActio * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.setup, state.account); +const mapStateToProps = state => ({ ...state.setup, ...state.account }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -43,15 +43,16 @@ const mapStateToProps = state => Object.assign({}, state.setup, state.account); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, SetupViewActions, { + actions: bindActionCreators({ + ...SetupViewActions, setBlockingPolicy, setAntiTracking, setAdBlock, setSmartBlocking, setGhosteryRewards, setHumanWeb, - setSetupComplete, - }), dispatch), + setSetupComplete + }, dispatch), }); export const reducer = SetupViewReducer; diff --git a/app/hub/Views/SetupViews/SetupAntiSuiteView/index.js b/app/hub/Views/SetupViews/SetupAntiSuiteView/index.js index d07fd0ecc..9d5dfcf5d 100644 --- a/app/hub/Views/SetupViews/SetupAntiSuiteView/index.js +++ b/app/hub/Views/SetupViews/SetupAntiSuiteView/index.js @@ -24,7 +24,7 @@ import { setSetupStep, setSetupNavigation } from '../../SetupView/SetupViewActio * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.setup); +const mapStateToProps = state => ({ ...state.setup }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -33,10 +33,11 @@ const mapStateToProps = state => Object.assign({}, state.setup); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, SetupAntiSuiteViewActions, { + actions: bindActionCreators({ + ...SetupAntiSuiteViewActions, setSetupStep, - setSetupNavigation, - }), dispatch), + setSetupNavigation + }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(SetupAntiSuiteViewContainer); diff --git a/app/hub/Views/SetupViews/SetupBlockingDropdown/index.js b/app/hub/Views/SetupViews/SetupBlockingDropdown/index.js index 3eedd4039..2c9066551 100644 --- a/app/hub/Views/SetupViews/SetupBlockingDropdown/index.js +++ b/app/hub/Views/SetupViews/SetupBlockingDropdown/index.js @@ -24,7 +24,7 @@ import * as SettingsActions from '../../../../panel/actions/SettingsActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.settings); +const mapStateToProps = state => ({ ...state.settings }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -33,7 +33,7 @@ const mapStateToProps = state => Object.assign({}, state.settings); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, SettingsActions), dispatch), + actions: bindActionCreators({ ...SettingsActions }, dispatch), }); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SetupBlockingDropdownContainer)); diff --git a/app/hub/Views/SetupViews/SetupBlockingView/index.js b/app/hub/Views/SetupViews/SetupBlockingView/index.js index 2c4e76a68..2d0825f6a 100644 --- a/app/hub/Views/SetupViews/SetupBlockingView/index.js +++ b/app/hub/Views/SetupViews/SetupBlockingView/index.js @@ -25,7 +25,7 @@ import { setSetupStep, setSetupNavigation } from '../../SetupView/SetupViewActio * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.setup); +const mapStateToProps = state => ({ ...state.setup }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -34,10 +34,11 @@ const mapStateToProps = state => Object.assign({}, state.setup); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, SetupBlockingViewActions, { + actions: bindActionCreators({ + ...SetupBlockingViewActions, setSetupStep, - setSetupNavigation, - }), dispatch), + setSetupNavigation + }, dispatch), }); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SetupBlockingViewContainer)); diff --git a/app/hub/Views/SetupViews/SetupDoneView/index.js b/app/hub/Views/SetupViews/SetupDoneView/index.js index bcce6c53d..1ed399b91 100644 --- a/app/hub/Views/SetupViews/SetupDoneView/index.js +++ b/app/hub/Views/SetupViews/SetupDoneView/index.js @@ -24,7 +24,7 @@ import { setSetupStep, setSetupNavigation } from '../../SetupView/SetupViewActio * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.setup); +const mapStateToProps = state => ({ ...state.setup }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -33,10 +33,11 @@ const mapStateToProps = state => Object.assign({}, state.setup); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, SetupDoneViewActions, { + actions: bindActionCreators({ + ...SetupDoneViewActions, setSetupStep, - setSetupNavigation, - }), dispatch), + setSetupNavigation + }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(SetupDoneViewContainer); diff --git a/app/hub/Views/SetupViews/SetupHumanWebView/index.js b/app/hub/Views/SetupViews/SetupHumanWebView/index.js index 9b207c564..e5c9110e8 100644 --- a/app/hub/Views/SetupViews/SetupHumanWebView/index.js +++ b/app/hub/Views/SetupViews/SetupHumanWebView/index.js @@ -24,7 +24,7 @@ import { setSetupStep, setSetupNavigation } from '../../SetupView/SetupViewActio * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.setup); +const mapStateToProps = state => ({ ...state.setup }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -33,10 +33,11 @@ const mapStateToProps = state => Object.assign({}, state.setup); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, SetupHumanWebViewActions, { + actions: bindActionCreators({ + ...SetupHumanWebViewActions, setSetupStep, setSetupNavigation - }), dispatch), + }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(SetupHumanWebViewContainer); diff --git a/app/hub/Views/SetupViews/SetupNavigation/index.js b/app/hub/Views/SetupViews/SetupNavigation/index.js index 9f40ac618..cc9ae5f59 100644 --- a/app/hub/Views/SetupViews/SetupNavigation/index.js +++ b/app/hub/Views/SetupViews/SetupNavigation/index.js @@ -20,6 +20,6 @@ import SetupNavigationContainer from './SetupNavigationContainer'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.setup); +const mapStateToProps = state => ({ ...state.setup }); export default connect(mapStateToProps)(SetupNavigationContainer); diff --git a/app/hub/Views/SideNavigationView/index.js b/app/hub/Views/SideNavigationView/index.js index adf9d60dc..da287bf7d 100644 --- a/app/hub/Views/SideNavigationView/index.js +++ b/app/hub/Views/SideNavigationView/index.js @@ -25,7 +25,7 @@ import { setToast } from '../AppView/AppViewActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.account); +const mapStateToProps = state => ({ ...state.account }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -34,11 +34,11 @@ const mapStateToProps = state => Object.assign({}, state.account); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { + actions: bindActionCreators({ setToast, getUser, - logout, - }), dispatch), + logout + }, dispatch), }); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SideNavigationViewContainer)); diff --git a/app/hub/Views/TutorialView/TutorialViewReducer.js b/app/hub/Views/TutorialView/TutorialViewReducer.js index 3a4fc9016..cbbf8bed1 100644 --- a/app/hub/Views/TutorialView/TutorialViewReducer.js +++ b/app/hub/Views/TutorialView/TutorialViewReducer.js @@ -27,7 +27,8 @@ function TutorialViewReducer(state = initialState, action) { textNext, textDone, } = action.data.navigation; - return Object.assign({}, state, { + return { + ...state, tutorial: { navigation: { activeIndex, @@ -38,8 +39,8 @@ function TutorialViewReducer(state = initialState, action) { textNext, textDone, } - }, - }); + } + }; } case SET_TUTORIAL_NAVIGATION: { const { @@ -51,8 +52,10 @@ function TutorialViewReducer(state = initialState, action) { textNext, textDone, } = action.data; - return Object.assign({}, state, { - tutorial: Object.assign({}, state.tutorial, { + return { + ...state, + tutorial: { + ...state.tutorial, navigation: { activeIndex, hrefPrev, @@ -61,9 +64,9 @@ function TutorialViewReducer(state = initialState, action) { textPrev, textNext, textDone, - }, - }), - }); + } + } + }; } default: return state; diff --git a/app/hub/Views/TutorialView/index.js b/app/hub/Views/TutorialView/index.js index f9c1a0157..1b2d90d3b 100644 --- a/app/hub/Views/TutorialView/index.js +++ b/app/hub/Views/TutorialView/index.js @@ -26,7 +26,7 @@ import { sendPing } from '../AppView/AppViewActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.tutorial); +const mapStateToProps = state => ({ ...state.tutorial }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -35,7 +35,7 @@ const mapStateToProps = state => Object.assign({}, state.tutorial); * @memberof SetupContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, TutorialViewActions, { sendPing }), dispatch), + actions: bindActionCreators({ ...TutorialViewActions, sendPing }, dispatch), }); export const reducer = TutorialViewReducer; diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/index.js b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/index.js index c0faf59dd..416e6a124 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/index.js +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/index.js @@ -24,7 +24,7 @@ import { setTutorialNavigation } from '../../TutorialView/TutorialViewActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.tutorial); +const mapStateToProps = state => ({ ...state.tutorial }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -33,7 +33,7 @@ const mapStateToProps = state => Object.assign({}, state.tutorial); * @memberof TutorialContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, TutorialAntiSuiteViewActions, { setTutorialNavigation }), dispatch), + actions: bindActionCreators({ ...TutorialAntiSuiteViewActions, setTutorialNavigation }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(TutorialAntiSuiteViewContainer); diff --git a/app/hub/Views/TutorialViews/TutorialBlockingView/index.js b/app/hub/Views/TutorialViews/TutorialBlockingView/index.js index a2f19348a..9e8be0d15 100644 --- a/app/hub/Views/TutorialViews/TutorialBlockingView/index.js +++ b/app/hub/Views/TutorialViews/TutorialBlockingView/index.js @@ -23,7 +23,7 @@ import { setTutorialNavigation } from '../../TutorialView/TutorialViewActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.tutorial); +const mapStateToProps = state => ({ ...state.tutorial }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -32,7 +32,7 @@ const mapStateToProps = state => Object.assign({}, state.tutorial); * @memberof TutorialContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { setTutorialNavigation }), dispatch), + actions: bindActionCreators({ setTutorialNavigation }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(TutorialBlockingViewContainer); diff --git a/app/hub/Views/TutorialViews/TutorialLayoutView/index.js b/app/hub/Views/TutorialViews/TutorialLayoutView/index.js index 9929161b9..14f8e4bdf 100644 --- a/app/hub/Views/TutorialViews/TutorialLayoutView/index.js +++ b/app/hub/Views/TutorialViews/TutorialLayoutView/index.js @@ -23,7 +23,7 @@ import { setTutorialNavigation } from '../../TutorialView/TutorialViewActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.tutorial); +const mapStateToProps = state => ({ ...state.tutorial }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -32,7 +32,7 @@ const mapStateToProps = state => Object.assign({}, state.tutorial); * @memberof TutorialContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { setTutorialNavigation }), dispatch), + actions: bindActionCreators({ setTutorialNavigation }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(TutorialLayoutViewContainer); diff --git a/app/hub/Views/TutorialViews/TutorialNavigation/index.js b/app/hub/Views/TutorialViews/TutorialNavigation/index.js index c4e9ae922..f83a26627 100644 --- a/app/hub/Views/TutorialViews/TutorialNavigation/index.js +++ b/app/hub/Views/TutorialViews/TutorialNavigation/index.js @@ -20,6 +20,6 @@ import TutorialNavigationContainer from './TutorialNavigationContainer'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.tutorial); +const mapStateToProps = state => ({ ...state.tutorial }); export default connect(mapStateToProps)(TutorialNavigationContainer); diff --git a/app/hub/Views/TutorialViews/TutorialTrackerListView/index.js b/app/hub/Views/TutorialViews/TutorialTrackerListView/index.js index bc09a915a..677b18f4f 100644 --- a/app/hub/Views/TutorialViews/TutorialTrackerListView/index.js +++ b/app/hub/Views/TutorialViews/TutorialTrackerListView/index.js @@ -23,7 +23,7 @@ import { setTutorialNavigation } from '../../TutorialView/TutorialViewActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.tutorial); +const mapStateToProps = state => ({ ...state.tutorial }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -32,7 +32,7 @@ const mapStateToProps = state => Object.assign({}, state.tutorial); * @memberof TutorialContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { setTutorialNavigation }), dispatch), + actions: bindActionCreators({ setTutorialNavigation }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(TutorialTrackerListViewContainer); diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/index.js b/app/hub/Views/TutorialViews/TutorialTrustView/index.js index 56ab74c4b..f839f8805 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/index.js +++ b/app/hub/Views/TutorialViews/TutorialTrustView/index.js @@ -23,7 +23,7 @@ import { setTutorialNavigation } from '../../TutorialView/TutorialViewActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.tutorial); +const mapStateToProps = state => ({ ...state.tutorial }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -32,7 +32,7 @@ const mapStateToProps = state => Object.assign({}, state.tutorial); * @memberof TutorialContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { setTutorialNavigation }), dispatch), + actions: bindActionCreators({ setTutorialNavigation }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(TutorialTrustViewContainer); diff --git a/app/hub/Views/TutorialViews/TutorialVideoView/index.js b/app/hub/Views/TutorialViews/TutorialVideoView/index.js index bd4d12e0b..3171af418 100644 --- a/app/hub/Views/TutorialViews/TutorialVideoView/index.js +++ b/app/hub/Views/TutorialViews/TutorialVideoView/index.js @@ -23,7 +23,7 @@ import { setTutorialNavigation } from '../../TutorialView/TutorialViewActions'; * @return {function} this function returns a plain object, which will be merged into the component's props * @memberof HubContainers */ -const mapStateToProps = state => Object.assign({}, state.tutorial); +const mapStateToProps = state => ({ ...state.tutorial }); /** * Bind the component's action creators using Redux's bindActionCreators. @@ -32,7 +32,7 @@ const mapStateToProps = state => Object.assign({}, state.tutorial); * @memberof TutorialContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { setTutorialNavigation }), dispatch), + actions: bindActionCreators({ setTutorialNavigation }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(TutorialVideoViewContainer); diff --git a/app/panel-android/components/Panel.jsx b/app/panel-android/components/Panel.jsx index 6686379d3..204f4ce21 100644 --- a/app/panel-android/components/Panel.jsx +++ b/app/panel-android/components/Panel.jsx @@ -131,7 +131,7 @@ export default class Panel extends React.Component { setGlobalState = (updated) => { const newState = {}; Object.keys(updated).forEach((key) => { - newState[key] = Object.assign({}, this.state[key], updated[key]); + newState[key] = { ...this.state[key], ...updated[key] }; }); this.setState(newState); diff --git a/app/panel/components/DetailMenu.jsx b/app/panel/components/DetailMenu.jsx index 9443029d5..123781a9e 100644 --- a/app/panel/components/DetailMenu.jsx +++ b/app/panel/components/DetailMenu.jsx @@ -41,7 +41,7 @@ class DetailMenu extends React.Component { * @param {Object} event click event */ setActiveTab(event) { - const menu = Object.assign({}, this.state.menu); + const menu = { ...this.state.menu }; const selectionId = event.currentTarget.id; Object.keys(menu).forEach((key) => { menu[key] = selectionId === key; }); diff --git a/app/panel/components/Settings/TrustAndRestrict.jsx b/app/panel/components/Settings/TrustAndRestrict.jsx index d6e6371b8..5eb677a9d 100644 --- a/app/panel/components/Settings/TrustAndRestrict.jsx +++ b/app/panel/components/Settings/TrustAndRestrict.jsx @@ -43,7 +43,7 @@ class TrustAndRestrict extends React.Component { */ setActivePane(event) { this.showWarning(''); - const newMenuState = Object.assign({}, this.state.menu); + const newMenuState = { ...this.state.menu }; Object.keys(newMenuState).forEach((key) => { if (key === event.currentTarget.id) { newMenuState[key] = true; diff --git a/app/panel/components/Stats.jsx b/app/panel/components/Stats.jsx index 8ec7ac7c7..18a06d8a6 100644 --- a/app/panel/components/Stats.jsx +++ b/app/panel/components/Stats.jsx @@ -156,7 +156,7 @@ class Stats extends React.Component { if (!this._isPlus(this.props)) { return; } - const state = Object.assign({}, this.state); + const state = { ...this.state }; const { selection } = state; if (event.currentTarget.id !== selection.view) { selection.view = event.currentTarget.id; @@ -180,7 +180,7 @@ class Stats extends React.Component { if (!this._isPlus(this.props)) { return; } - const state = Object.assign({}, this.state); + const state = { ...this.state }; const { selection } = state; if (event.currentTarget.id !== selection.type) { const lastType = selection.type; @@ -230,7 +230,7 @@ class Stats extends React.Component { if (!this._isPlus(this.props)) { return; } - const state = Object.assign({}, this.state); + const state = { ...this.state }; const data = state.selection.type === 'daily' ? state.dailyData : state.monthlyData; if (e.target.id === 'stats-forward') { state.selection.currentIndex += 6; @@ -343,7 +343,7 @@ class Stats extends React.Component { * Save it in component's state */ _init = () => { - const state = Object.assign({}, this.state); + const state = { ...this.state }; this._getAllStats().then((allData) => { if (Array.isArray(allData)) { if (allData.length === 0) { @@ -497,7 +497,7 @@ class Stats extends React.Component { * Determine data selection for Stats Graph according to parameters in state * Save it in component's state */ - _determineSelectionData = (state = Object.assign({}, this.state)) => { + _determineSelectionData = (state = ({ ...this.state })) => { const { dailyData, monthlyData, cumulativeMonthlyData, selection } = state; diff --git a/app/panel/containers/AccountSuccessContainer.js b/app/panel/containers/AccountSuccessContainer.js index 1c868b362..4e139965a 100644 --- a/app/panel/containers/AccountSuccessContainer.js +++ b/app/panel/containers/AccountSuccessContainer.js @@ -24,10 +24,11 @@ import AccountSuccess from '../components/AccountSuccess'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.accountSuccess, { +const mapStateToProps = state => ({ + ...state.accountSuccess, // get properties from panel redux store email: state.panel.email, - is_expert: state.panel.is_expert, + is_expert: state.panel.is_expert }); /** * Connects AccountSuccess component to the Redux store. diff --git a/app/panel/containers/BlockingContainer.js b/app/panel/containers/BlockingContainer.js index 681194a9c..ab4509022 100644 --- a/app/panel/containers/BlockingContainer.js +++ b/app/panel/containers/BlockingContainer.js @@ -26,7 +26,8 @@ import { showNotification, toggleCliqzFeature } from '../actions/PanelActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.blocking, { +const mapStateToProps = state => ({ + ...state.blocking, is_expanded: state.panel.is_expanded, language: state.panel.language, smartBlock: state.panel.smartBlock, @@ -34,7 +35,7 @@ const mapStateToProps = state => Object.assign({}, state.blocking, { pageHost: state.summary.pageHost, paused_blocking: state.summary.paused_blocking, sitePolicy: state.summary.sitePolicy, - smartBlockActive: state.panel.enable_smart_block, + smartBlockActive: state.panel.enable_smart_block }); /** * Bind Blocking view component action creators using Redux's bindActionCreators diff --git a/app/panel/containers/CreateAccountContainer.js b/app/panel/containers/CreateAccountContainer.js index 0811d6cd7..c88ae9bbb 100644 --- a/app/panel/containers/CreateAccountContainer.js +++ b/app/panel/containers/CreateAccountContainer.js @@ -26,10 +26,11 @@ import { register, getUser } from '../../Account/AccountActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.createAccount, { +const mapStateToProps = state => ({ + ...state.createAccount, // get properties from panel redux store is_expert: state.panel.is_expert, - language: state.panel.language, + language: state.panel.language }); /** * Bind CreateAccount view component action creators using Redux's bindActionCreators diff --git a/app/panel/containers/DetailContainer.js b/app/panel/containers/DetailContainer.js index b2e734998..6d103facf 100644 --- a/app/panel/containers/DetailContainer.js +++ b/app/panel/containers/DetailContainer.js @@ -24,9 +24,11 @@ import * as actions from '../actions/DetailActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.detail, state.account, { +const mapStateToProps = state => ({ + ...state.detail, + ...state.account, is_expanded: state.panel.is_expanded, - enable_offers: state.panel.enable_offers, + enable_offers: state.panel.enable_offers }); /** * Bind Detailed view action creators using Redux's bindActionCreators diff --git a/app/panel/containers/HeaderContainer.js b/app/panel/containers/HeaderContainer.js index 5d375a4ea..65c22c8af 100644 --- a/app/panel/containers/HeaderContainer.js +++ b/app/panel/containers/HeaderContainer.js @@ -26,12 +26,13 @@ import { logout } from '../../Account/AccountActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.account, { +const mapStateToProps = state => ({ + ...state.account, // get properties from panel redux store is_expanded: state.panel.is_expanded, is_expert: state.panel.is_expert, language: state.panel.language, - tab_id: state.panel.tab_id, + tab_id: state.panel.tab_id }); /** * Bind Header component action creators using Redux's bindActionCreators. Pass updated match, location, and history props to the wrapped component. diff --git a/app/panel/containers/LoginContainer.js b/app/panel/containers/LoginContainer.js index f1a5c2c6d..e5e8650b9 100644 --- a/app/panel/containers/LoginContainer.js +++ b/app/panel/containers/LoginContainer.js @@ -25,9 +25,8 @@ import { login, getUser, getUserSettings } from '../../Account/AccountActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, { - // get properties from panel redux store - is_expert: state.panel.is_expert, +const mapStateToProps = state => ({ // get properties from panel redux store + is_expert: state.panel.is_expert }); /** * Bind Login view component action creators using Redux's bindActionCreators diff --git a/app/panel/containers/PanelContainer.js b/app/panel/containers/PanelContainer.js index 2a4b9e007..9657b87b7 100644 --- a/app/panel/containers/PanelContainer.js +++ b/app/panel/containers/PanelContainer.js @@ -27,10 +27,13 @@ import { updateBlockingData } from '../actions/BlockingActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.panel, state.drawer, state.account, { +const mapStateToProps = state => ({ + ...state.panel, + ...state.drawer, + ...state.account, paused_blocking: state.summary.paused_blocking, sitePolicy: state.summary.sitePolicy, - trackerCounts: state.summary.trackerCounts, + trackerCounts: state.summary.trackerCounts }); /** * Bind Panel view component action creators using Redux's bindActionCreators @@ -40,7 +43,9 @@ const mapStateToProps = state => Object.assign({}, state.panel, state.drawer, st * @return {function} to be used as an argument in redux connect call */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, panelActions, { filterTrackers, updateSummaryData }, { updateBlockingData }), dispatch), + actions: bindActionCreators({ + ...panelActions, filterTrackers, updateSummaryData, updateBlockingData + }, dispatch), }); /** * Connects Panel component to the Redux store. Pass updated match, location, and history props to the wrapped component. diff --git a/app/panel/containers/RewardsContainer.js b/app/panel/containers/RewardsContainer.js index e289896f4..8568970de 100644 --- a/app/panel/containers/RewardsContainer.js +++ b/app/panel/containers/RewardsContainer.js @@ -27,9 +27,7 @@ import { showNotification } from '../actions/PanelActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.rewards, { - is_expanded: state.panel.is_expanded, -}); +const mapStateToProps = state => ({ ...state.rewards, is_expanded: state.panel.is_expanded }); /** * Bind Rewards view action creators using Redux's bindActionCreators diff --git a/app/panel/containers/SettingsContainer.js b/app/panel/containers/SettingsContainer.js index 131a3f994..1b01a747c 100644 --- a/app/panel/containers/SettingsContainer.js +++ b/app/panel/containers/SettingsContainer.js @@ -26,7 +26,8 @@ import { sendSignal } from '../actions/RewardsActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.settings, { +const mapStateToProps = state => ({ + ...state.settings, user: state.account.user, is_expanded: state.panel.is_expanded, language: state.panel.language, @@ -37,7 +38,7 @@ const mapStateToProps = state => Object.assign({}, state.settings, { site_blacklist: state.summary.site_blacklist, site_whitelist: state.summary.site_whitelist, trackers_banner_status: state.panel.trackers_banner_status, - trackerCounts: state.summary.trackerCounts, + trackerCounts: state.summary.trackerCounts }); /** * Bind Settings view component action creators using Redux's bindActionCreators diff --git a/app/panel/containers/StatsContainer.js b/app/panel/containers/StatsContainer.js index 5819bd7f7..6bb8fe379 100644 --- a/app/panel/containers/StatsContainer.js +++ b/app/panel/containers/StatsContainer.js @@ -22,7 +22,7 @@ import Stats from '../components/Stats'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.account); +const mapStateToProps = state => ({ ...state.account }); /** * Connects Subscription view component to the Redux store. Pass updated match, location, and history props to the wrapped component. diff --git a/app/panel/containers/SubscriptionContainer.js b/app/panel/containers/SubscriptionContainer.js index f0e2555a1..15e80c661 100644 --- a/app/panel/containers/SubscriptionContainer.js +++ b/app/panel/containers/SubscriptionContainer.js @@ -25,11 +25,12 @@ import { getUserSubscriptionData } from '../../Account/AccountActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.account, { +const mapStateToProps = state => ({ + ...state.account, theme: state.panel.theme, current_theme: state.panel.current_theme, subscriber: state.panel.subscriber, - language: state.panel.language, + language: state.panel.language }); /** * Bind Subscription view component action creators using Redux's bindActionCreators @@ -39,10 +40,10 @@ const mapStateToProps = state => Object.assign({}, state.account, { * @return {function} to be used as an argument in redux connect call */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(Object.assign({}, { + actions: bindActionCreators({ getTheme, getUserSubscriptionData - }), dispatch), + }, dispatch), }); /** * Connects Subscription view component to the Redux store. Pass updated match, location, and history props to the wrapped component. diff --git a/app/panel/containers/SummaryContainer.js b/app/panel/containers/SummaryContainer.js index fac24f3ba..993081cac 100644 --- a/app/panel/containers/SummaryContainer.js +++ b/app/panel/containers/SummaryContainer.js @@ -25,12 +25,14 @@ import * as panelActions from '../actions/PanelActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = state => Object.assign({}, state.summary, state.panel, { +const mapStateToProps = state => ({ + ...state.summary, + ...state.panel, is_expanded: state.panel.is_expanded, is_expert: state.panel.is_expert, tab_id: state.panel.tab_id, user: state.account.user, - current_theme: state.panel.current_theme, + current_theme: state.panel.current_theme }); /** * Bind Summary view component action creators using Redux's bindActionCreators diff --git a/app/panel/reducers/blocking.js b/app/panel/reducers/blocking.js index a543bf339..626a0c5b0 100644 --- a/app/panel/reducers/blocking.js +++ b/app/panel/reducers/blocking.js @@ -63,44 +63,44 @@ const initialState = { export default (state = initialState, action) => { switch (action.type) { case UPDATE_BLOCKING_DATA: { - return Object.assign({}, state, action.data); + return { ...state, ...action.data }; } case FILTER_TRACKERS: { if (state.filter.type === action.data.type && state.filter.name === action.data.name) { // prevent re-render if filter hasn't changed return state; } - return Object.assign({}, state, { filter: action.data }); + return { ...state, filter: action.data }; } case UPDATE_BLOCK_ALL_TRACKERS: { const updated = updateBlockAllTrackers(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case UPDATE_CATEGORIES: { - return Object.assign({}, state, { categories: action.data }); + return { ...state, categories: action.data }; } case UPDATE_UNKNOWN_CATEGORY_HIDE: { - return Object.assign({}, state, { unknownCategory: action.data }); + return { ...state, unknownCategory: action.data }; } case UPDATE_CATEGORY_BLOCKED: { const updated = updateCategoryBlocked(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case UPDATE_TRACKER_BLOCKED: { const updated = updateTrackerBlocked(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case TOGGLE_EXPAND_ALL: { const updated = toggleExpandAll(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case UPDATE_TRACKER_TRUST_RESTRICT: { const updated = _updateTrackerTrustRestrict(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case UPDATE_CLIQZ_MODULE_WHITELIST: { const unknownCategory = _updateCliqzModuleWhitelist(state, action); - return Object.assign({}, state, { unknownCategory }); + return { ...state, unknownCategory }; } case UPDATE_CLIQZ_MODULE_DATA: case UPDATE_SUMMARY_DATA: { @@ -112,10 +112,10 @@ export default (state = initialState, action) => { trackerCount: antiTracking.trackerCount + adBlock.trackerCount, unknownTrackerCount: antiTracking.unknownTrackerCount + adBlock.unknownTrackerCount, unknownTrackers: Array.from(new Set(antiTracking.unknownTrackers.concat(adBlock.unknownTrackers))), - whitelistedUrls: Object.assign({}, antiTracking.whitelistedUrls, adBlock.whitelistedUrls), + whitelistedUrls: { ...antiTracking.whitelistedUrls, ...adBlock.whitelistedUrls }, hide: state.unknownCategory.hide, }; - return Object.assign({}, state, { unknownCategory }); + return { ...state, unknownCategory }; } return state; } diff --git a/app/panel/reducers/panel.js b/app/panel/reducers/panel.js index cc8448e79..e814d834e 100644 --- a/app/panel/reducers/panel.js +++ b/app/panel/reducers/panel.js @@ -73,24 +73,24 @@ const initialState = { export default (state = initialState, action) => { switch (action.type) { case UPDATE_PANEL_DATA: { - return Object.assign({}, state, action.data, { initialized: true }); + return { ...state, ...action.data, initialized: true }; } case SET_THEME: { const { name, css } = action.data; setTheme(document, name, { themeData: { [name]: { name, css } } }); - return Object.assign({}, state, { current_theme: name }); + return { ...state, current_theme: name }; } case CLEAR_THEME: { setTheme(document, initialState.current_theme); - return Object.assign({}, state, { current_theme: initialState.current_theme }); + return { ...state, current_theme: initialState.current_theme }; } case SHOW_NOTIFICATION: { const updated = _showNotification(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case CLOSE_NOTIFICATION: { const updated = _closeNotification(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case LOGIN_SUCCESS: { const notificationAction = { @@ -101,9 +101,7 @@ export default (state = initialState, action) => { } }; const updated = _showNotification(state, notificationAction); - return Object.assign({}, state, updated, { - loggedIn: true, - }); + return { ...state, ...updated, loggedIn: true }; } case LOGIN_FAIL: { const { errors } = action.payload; @@ -126,7 +124,7 @@ export default (state = initialState, action) => { } }; const updated = _showNotification(state, notificationAction); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case REGISTER_SUCCESS: { const { email } = action.payload; @@ -138,9 +136,7 @@ export default (state = initialState, action) => { } }; const updated = _showNotification(state, notificationAction); - return Object.assign({}, state, updated, { - email - }); + return { ...state, ...updated, email }; } case REGISTER_FAIL: { const { errors } = action.payload; @@ -165,11 +161,11 @@ export default (state = initialState, action) => { } }; const updated = _showNotification(state, notificationAction); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case LOGOUT_SUCCESS: { setTheme(document); - return Object.assign({}, state, { current_theme: initialState.current_theme }); + return { ...state, current_theme: initialState.current_theme }; } // @TODO? // case LOGOUT_SUCCESS: { @@ -191,7 +187,7 @@ export default (state = initialState, action) => { } }; const updated = _showNotification(state, notificationAction); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case RESET_PASSWORD_FAIL: { const { errors } = action.payload; @@ -214,7 +210,7 @@ export default (state = initialState, action) => { } }; const updated = _showNotification(state, notificationAction); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case TOGGLE_CLIQZ_FEATURE: { let pingName = ''; @@ -236,13 +232,13 @@ export default (state = initialState, action) => { sendMessage('ping', pingName); } }); - return Object.assign({}, state, { [action.data.featureName]: !action.data.isEnabled }); + return { ...state, [action.data.featureName]: !action.data.isEnabled }; } case TOGGLE_EXPANDED: { sendMessage('setPanelData', { is_expanded: !state.is_expanded }); sendMessage('ping', state.is_expanded ? 'viewchange_from_expanded' : 'viewchange_from_detailed'); - return Object.assign({}, state, { is_expanded: !state.is_expanded }); + return { ...state, is_expanded: !state.is_expanded }; } case TOGGLE_EXPERT: { sendMessage('setPanelData', { is_expert: !state.is_expert }); @@ -257,22 +253,22 @@ export default (state = initialState, action) => { pingName = 'viewchange_from_simple'; } sendMessage('ping', pingName); - return Object.assign({}, state, { is_expert: !state.is_expert }); + return { ...state, is_expert: !state.is_expert }; } case UPDATE_NOTIFICATION_STATUS: { const updated = _updateNotificationStatus(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case TOGGLE_CHECKBOX: { if (action.data.event === 'enable_offers') { const enable_offers = action.data.checked; - return Object.assign({}, state, { enable_offers }); + return { ...state, enable_offers }; } return state; } case TOGGLE_OFFERS_ENABLED: { const enable_offers = action.data.enabled; - return Object.assign({}, state, { enable_offers }); + return { ...state, enable_offers }; } case TOGGLE_PROMO_MODAL: { return { diff --git a/app/panel/reducers/rewards.js b/app/panel/reducers/rewards.js index 52a6f2b6c..fb98cd7b4 100644 --- a/app/panel/reducers/rewards.js +++ b/app/panel/reducers/rewards.js @@ -33,11 +33,11 @@ const initialState = { export default (state = initialState, action) => { switch (action.type) { case UPDATE_REWARDS_DATA: { - return Object.assign({}, state, action.data); + return { ...state, ...action.data }; } case TOGGLE_OFFERS_ENABLED: { const enable_offers = action.data.enabled; - return Object.assign({}, state, { enable_offers }); + return { ...state, enable_offers }; } case SEND_SIGNAL: { diff --git a/app/panel/reducers/settings.js b/app/panel/reducers/settings.js index 9618e474e..0e5d26394 100644 --- a/app/panel/reducers/settings.js +++ b/app/panel/reducers/settings.js @@ -58,60 +58,61 @@ const initialState = { export default (state = initialState, action) => { switch (action.type) { case GET_SETTINGS_DATA: { - return Object.assign({}, state, action.data); + return { ...state, ...action.data }; } case EXPORT_SETTINGS: { const updated = _exportSettings(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case IMPORT_SETTINGS_DIALOG: { const updated = _importSettingsDialog(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case IMPORT_SETTINGS_NATIVE: { const updated = _importSettingsNative(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case IMPORT_SETTINGS_FAILED: { - return Object.assign({}, state, { + return { + ...state, importResultText: t('settings_import_file_error'), - actionSuccess: false, - }); + actionSuccess: false + }; } case SELECT_ITEM: { const updated = _updateSelectValue(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case TOGGLE_CHECKBOX: { const updated = _updateSettingsCheckbox(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case UPDATE_DATABASE: { const updated = _updateTrackerDatabase(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case UPDATE_SETTINGS_BLOCK_ALL_TRACKERS: { const updated = updateBlockAllTrackers(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case UPDATE_SETTINGS_CATEGORY_BLOCKED: { const updated = updateCategoryBlocked(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case SETTINGS_TOGGLE_EXPAND_ALL: { const updated = toggleExpandAll(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case UPDATE_SETTINGS_TRACKER_BLOCKED: { const updated = updateTrackerBlocked(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case SETTINGS_UPDATE_SEARCH_VALUE: { const updated = _updateSearchValue(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case SETTINGS_FILTER: { const updated = _filter(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } default: return state; diff --git a/app/panel/reducers/summary.js b/app/panel/reducers/summary.js index 41c575daf..17b7cd9c8 100644 --- a/app/panel/reducers/summary.js +++ b/app/panel/reducers/summary.js @@ -66,17 +66,18 @@ export default (state = initialState, action) => { switch (action.type) { case UPDATE_SUMMARY_DATA: case UPDATE_CLIQZ_MODULE_DATA: { - return Object.assign({}, state, action.data); + return { ...state, ...action.data }; } case UPDATE_GHOSTERY_PAUSED: { - return Object.assign({}, state, { paused_blocking: action.data.ghosteryPaused, paused_blocking_timeout: action.data.time }); + return { ...state, paused_blocking: action.data.ghosteryPaused, paused_blocking_timeout: action.data.time }; } case UPDATE_SITE_POLICY: { const updated = _updateSitePolicy(state, action); - return Object.assign({}, state, updated); + return { ...state, ...updated }; } case UPDATE_TRACKER_COUNTS: { - return Object.assign({}, state, { + return { + ...state, trackerCounts: { blocked: action.data.num_blocked, allowed: action.data.num_total - action.data.num_blocked, @@ -84,8 +85,8 @@ export default (state = initialState, action) => { ssAllowed: action.data.num_ss_allowed, sbBlocked: action.data.num_sb_blocked, sbAllowed: action.data.num_sb_allowed, - }, - }); + } + }; } default: return state; } diff --git a/app/panel/utils/utils.js b/app/panel/utils/utils.js index 7b91b551b..cf1c27cb7 100644 --- a/app/panel/utils/utils.js +++ b/app/panel/utils/utils.js @@ -24,7 +24,7 @@ import { log } from '../../../src/utils/common'; export function updateObject(obj, key, value) { const output = {}; output[key] = value; - return Object.assign({}, obj, output); + return { ...obj, ...output }; } /** diff --git a/app/shared-components/ForgotPassword/ForgotPasswordContainer.js b/app/shared-components/ForgotPassword/ForgotPasswordContainer.js index 0add4b2dc..6f1d3b41c 100644 --- a/app/shared-components/ForgotPassword/ForgotPasswordContainer.js +++ b/app/shared-components/ForgotPassword/ForgotPasswordContainer.js @@ -26,7 +26,7 @@ import { resetPassword } from '../../Account/AccountActions'; * @todo We are not using ownProps, so we better not specify it explicitly, * in this case it won't be passed by React (see https://github.com/reactjs/react-redux/blob/master/docs/api.md). */ -const mapStateToProps = () => Object.assign({}); +const mapStateToProps = () => ({}); /** * Bind ForgotPassword component action creators using Redux's bindActionCreators * @memberOf PanelContainers diff --git a/app/shared-components/PromoModal/PromoModalContainer.js b/app/shared-components/PromoModal/PromoModalContainer.js index 50585c692..44e106050 100644 --- a/app/shared-components/PromoModal/PromoModalContainer.js +++ b/app/shared-components/PromoModal/PromoModalContainer.js @@ -40,10 +40,10 @@ import { togglePromoModal, showNotification } from '../../panel/actions/PanelAct */ const mapDispatchToProps = dispatch => ({ actions: bindActionCreators( - Object.assign({ + { togglePromoModal, showNotification - }), dispatch + }, dispatch ) }); /** diff --git a/src/classes/Account.js b/src/classes/Account.js index 7b1ab9e8f..5140b786c 100644 --- a/src/classes/Account.js +++ b/src/classes/Account.js @@ -490,7 +490,7 @@ class Account { conf.account.themeData = {}; } const { name } = data; - conf.account.themeData[name] = Object.assign({ timestamp: Date.now() }, data); + conf.account.themeData[name] = { timestamp: Date.now(), ...data }; dispatcher.trigger('conf.save.account'); } diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 1ef073f08..20e75aeb3 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -132,13 +132,13 @@ class Metrics { getActiveTab((tab) => { const tabUrl = tab && tab.url ? tab.url : ''; - this._brokenPageWatcher = Object.assign({}, { + this._brokenPageWatcher = { on: true, triggerId, triggerTime: Date.now(), timeoutId: setTimeout(this._clearBrokenPageWatcherTimeout.bind(this), BROKEN_PAGE_METRICS_THRESHOLD), - url: tabUrl, - }); + url: tabUrl + }; }); } @@ -149,13 +149,13 @@ class Metrics { _unplugBrokenPageWatcher() { this._clearBrokenPageWatcherTimeout(); - this._brokenPageWatcher = Object.assign({}, { + this._brokenPageWatcher = { on: false, triggerId: '', triggerTime: '', timeoutId: null, - url: '', - }); + url: '' + }; } _clearBrokenPageWatcherTimeout() { diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 43aaaab1e..6442f8973 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -247,14 +247,15 @@ class PanelData { site_specific_blocks, site_specific_unblocks, toggle_individual_trackers, } = conf; - return Object.assign({}, { + return { expand_all_trackers, selected_app_ids, show_tracker_urls, site_specific_blocks, site_specific_unblocks, - toggle_individual_trackers - }, this._getDynamicBlockingData()); + toggle_individual_trackers, + ...this._getDynamicBlockingData() + }; } /** @@ -336,7 +337,7 @@ class PanelData { trackers_banner_status, } = conf; - return Object.assign({}, { + return { current_theme, enable_ad_block, enable_anti_tracking, @@ -350,7 +351,8 @@ class PanelData { reload_banner_status, tab_id, trackers_banner_status, - }, this._getDynamicPanelData(tab_id)); + ...this._getDynamicPanelData(tab_id) + }; } /** @@ -386,19 +388,17 @@ class PanelData { settings_last_exported, settings_last_imported } = conf; - return Object.assign( - {}, - { - bugs_last_updated, - categories: this._buildGlobalCategories(), - language, // required for the setup page that does not have access to panelView data - new_app_ids, - offer_human_web: true, - settings_last_exported, - settings_last_imported, - }, - this._getUserSettingsForSettingsView(conf), - ); + return { + + bugs_last_updated, + categories: this._buildGlobalCategories(), + language, // required for the setup page that does not have access to panelView data + new_app_ids, + offer_human_web: true, + settings_last_exported, + settings_last_imported, + ...this._getUserSettingsForSettingsView(conf), + }; } /** @@ -412,20 +412,18 @@ class PanelData { const { paused_blocking, paused_blocking_timeout } = globals.SESSION; const { site_blacklist, site_whitelist } = conf; - return Object.assign( - {}, - { - paused_blocking, - paused_blocking_timeout, - site_blacklist, - site_whitelist, - pageHost, - pageUrl: url || '', - siteNotScanned: !this._trackerList || false, - sitePolicy: policy.getSitePolicy(url) || false, - }, - this._getDynamicSummaryData() - ); + return { + + paused_blocking, + paused_blocking_timeout, + site_blacklist, + site_whitelist, + pageHost, + pageUrl: url || '', + siteNotScanned: !this._trackerList || false, + sitePolicy: policy.getSitePolicy(url) || false, + ...this._getDynamicSummaryData() + }; } /** From 3049bd1ca5ecd5fdb3e84678249bac9964196055 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Fri, 1 May 2020 18:04:15 +0200 Subject: [PATCH 03/89] add linting for no-restricted-syntax and fix 1/2 of resulting errors --- .eslintrc.js | 11 +++++------ app/content-scripts/click_to_play.js | 4 +++- app/panel-android/components/content/FixedMenu.jsx | 11 ++++++++--- src/background.js | 8 ++++++-- src/classes/BugDb.js | 4 +++- src/classes/FoundBugs.js | 8 ++++++-- src/utils/matcher.js | 8 ++++++-- 7 files changed, 37 insertions(+), 17 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a86b207c8..0033f9b64 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -40,7 +40,7 @@ module.exports = { rules: { 'arrow-parens': [2, 'as-needed', { 'requireForBlockBody': true }], 'camelcase': [0], - 'class-methods-use-this': [0], + 'class-methods-use-this': [0], // TODO: enable this check 'comma-dangle': [2, { 'arrays': 'only-multiline', 'objects': 'only-multiline', @@ -54,7 +54,7 @@ module.exports = { 'lines-between-class-members': [1], 'max-len': [0], 'newline-per-chained-call': [0, { 'ignoreChainWithDepth': 2 }], - 'no-mixed-operators': [0], + 'no-mixed-operators': [0], // TODO: enable this check 'no-nested-ternary': [0], 'no-param-reassign': ['error', { props: true, @@ -66,19 +66,18 @@ module.exports = { }], 'no-plusplus': [0], 'no-prototype-builtins': [0], // TODO: enable this check - 'no-restricted-syntax': [0], // TODO: enable this check + 'no-restricted-syntax': [1], 'no-tabs': [0], 'no-underscore-dangle': [0], 'no-unused-vars': [1], 'no-useless-escape': [1], 'operator-linebreak': [0], - 'prefer-object-spread': ['error'], + 'prefer-object-spread': [1], 'space-before-function-paren': [2, 'never'], - 'template-curly-spacing': [0], // Plugin: Import 'import/no-cycle': [0], - 'import/prefer-default-export': [0], + 'import/prefer-default-export': [0], // TODO: enable this check // Plugin: React 'react/destructuring-assignment': [0], diff --git a/app/content-scripts/click_to_play.js b/app/content-scripts/click_to_play.js index 764205c42..92179ae4b 100644 --- a/app/content-scripts/click_to_play.js +++ b/app/content-scripts/click_to_play.js @@ -181,7 +181,9 @@ const Click2PlayContentScript = (function(win, doc) { }); window.addEventListener('load', () => { - for (const app_id in C2P_DATA) { + const app_ids = Object.keys(C2P_DATA); + for (let i = 0; i < app_ids.length; i++) { + const app_id = app_ids[i]; if (C2P_DATA.hasOwnProperty(app_id)) { if (C2P_DATA[app_id].length >= 3) { applyC2P(C2P_DATA[app_id][0], C2P_DATA[app_id][1], C2P_DATA[app_id][2]); diff --git a/app/panel-android/components/content/FixedMenu.jsx b/app/panel-android/components/content/FixedMenu.jsx index 4951b58b3..829a9b50a 100644 --- a/app/panel-android/components/content/FixedMenu.jsx +++ b/app/panel-android/components/content/FixedMenu.jsx @@ -47,10 +47,14 @@ export default class FixedMenu extends React.Component { getCount = (type) => { let total = 0; switch (type) { - case 'enable_anti_tracking': - for (const category in this.antiTrackingData) { + case 'enable_anti_tracking': { + const categories = Object.keys(this.antiTrackingData); + for (let i = 0; i < categories.length; i++) { + const category = categories[i]; if (this.antiTrackingData.hasOwnProperty(category)) { - for (const app in this.antiTrackingData[category]) { + const apps = Object.keys(this.antiTrackingData[category]); + for (let j = 0; j < apps.length; j++) { + const app = apps[j]; if (this.antiTrackingData[category][app] === 'unsafe') { total++; } @@ -58,6 +62,7 @@ export default class FixedMenu extends React.Component { } } return total; + } case 'enable_ad_block': return this.adBlockData && this.adBlockData.totalCount || 0; case 'enable_smart_block': diff --git a/src/background.js b/src/background.js index 6b55f460d..d47527fb7 100644 --- a/src/background.js +++ b/src/background.js @@ -125,7 +125,9 @@ function setGhosteryDefaultBlocking() { const categoriesBlock = ['advertising', 'pornvertising', 'site_analytics']; log('Blocking all trackers in categories:', ...categoriesBlock); const selected_app_ids = {}; - for (const app_id in bugDb.db.apps) { + const app_ids = Object.keys(bugDb.db.apps); + for (let i = 0; i < app_ids.length; i++) { + const app_id = app_ids[i]; if (bugDb.db.apps.hasOwnProperty(app_id)) { const category = bugDb.db.apps[app_id].cat; if (categoriesBlock.indexOf(category) >= 0 && @@ -574,7 +576,9 @@ function handleGhosteryHub(name, message, callback) { case 'BLOCKING_POLICY_EVERYTHING': { panelData.set({ setup_block: 3 }); const selected_app_ids = {}; - for (const app_id in bugDb.db.apps) { + const app_ids = Object.keys(bugDb.db.apps); + for (let i = 0; i < app_ids.length; i++) { + const app_id = app_ids[i]; if (!selected_app_ids.hasOwnProperty(app_id)) { selected_app_ids[app_id] = 1; } diff --git a/src/classes/BugDb.js b/src/classes/BugDb.js index ea1885ae6..28284cdb6 100644 --- a/src/classes/BugDb.js +++ b/src/classes/BugDb.js @@ -126,7 +126,9 @@ class BugDb extends Updatable { } } - for (categoryName in categories) { + const categoryNames = Object.keys(categories); + for (let i = 0; i < categoryNames.length; i++) { + categoryName = categoryNames[i]; if (categories.hasOwnProperty(categoryName)) { const category = categories[categoryName]; if (category.trackers) { diff --git a/src/classes/FoundBugs.js b/src/classes/FoundBugs.js index d1b2aa80b..1404106c8 100644 --- a/src/classes/FoundBugs.js +++ b/src/classes/FoundBugs.js @@ -200,7 +200,9 @@ class FoundBugs { } // squish all the bugs into categories first - for (id in bugs) { + const ids = Object.keys(bugs); + for (let i = 0; i < ids.length; i++) { + id = ids[i]; if (bugs.hasOwnProperty(id)) { aid = db.bugs[id].aid; // eslint-disable-line prefer-destructuring cid = db.apps[aid].cat; @@ -239,7 +241,9 @@ class FoundBugs { } // convert categories hash to array - for (cid in cats_obj) { + const cids = Object.keys(cats_obj); + for (let i = 0; i < cids.length; i++) { + cid = cids[i]; if (cats_obj.hasOwnProperty(cid)) { cats_arr.push(cats_obj[cid]); } diff --git a/src/utils/matcher.js b/src/utils/matcher.js index 796802507..1e34b5e1d 100644 --- a/src/utils/matcher.js +++ b/src/utils/matcher.js @@ -197,7 +197,9 @@ function _matchesHost(root, src_host, src_path) { function _matchesRegex(src) { const regexes = bugDb.db.patterns.regex; - for (const bug_id in regexes) { + const bug_ids = Object.keys(regexes); + for (let i = 0; i < bug_ids.length; i++) { + const bug_id = bug_ids[i]; if (regexes[bug_id].test(src)) { return +bug_id; } @@ -220,7 +222,9 @@ function _matchesPath(src_path) { // NOTE: we re-add the "/" in order to match patterns that include "/" const srcPath = `/${src_path}`; - for (const path in paths) { + const pathArr = Object.keys(paths); + for (let i = 0; i < pathArr.length; i++) { + const path = pathArr[i]; if (srcPath.includes(path)) { return paths[path]; } From fe03ad7a1bea609af11e8dc47aecb8c262efc970 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Mon, 4 May 2020 14:36:53 +0200 Subject: [PATCH 04/89] add linting for no-prototype-builtins and fix resulting linting errors --- .eslintrc.js | 4 ++-- app/content-scripts/click_to_play.js | 2 +- .../components/content/FixedMenu.jsx | 2 +- app/panel/reducers/blocking.js | 2 +- app/panel/reducers/panel.js | 6 ++--- app/panel/utils/blocking.js | 16 +++++++------- src/background.js | 14 ++++++------ src/classes/ABTest.js | 2 +- src/classes/BugDb.js | 16 +++++++------- src/classes/Click2PlayDb.js | 8 +++---- src/classes/CompatibilityDb.js | 2 +- src/classes/ConfData.js | 4 ++-- src/classes/FoundBugs.js | 18 +++++++-------- src/classes/Latency.js | 4 ++-- src/classes/PanelData.js | 14 ++++++------ src/classes/Policy.js | 8 +++---- src/classes/PolicySmartBlock.js | 6 ++--- src/classes/SurrogateDb.js | 22 +++++++++---------- src/classes/TabInfo.js | 10 ++++----- src/classes/Updatable.js | 2 +- src/utils/click2play.js | 4 ++-- src/utils/common.js | 4 ++-- src/utils/matcher.js | 8 +++---- 23 files changed, 89 insertions(+), 89 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 0033f9b64..2c0ef4af4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -65,8 +65,8 @@ module.exports = { ] }], 'no-plusplus': [0], - 'no-prototype-builtins': [0], // TODO: enable this check - 'no-restricted-syntax': [1], + 'no-prototype-builtins': [1], + 'no-restricted-syntax': [0], // TODO: enable this check 'no-tabs': [0], 'no-underscore-dangle': [0], 'no-unused-vars': [1], diff --git a/app/content-scripts/click_to_play.js b/app/content-scripts/click_to_play.js index 92179ae4b..4874be628 100644 --- a/app/content-scripts/click_to_play.js +++ b/app/content-scripts/click_to_play.js @@ -184,7 +184,7 @@ const Click2PlayContentScript = (function(win, doc) { const app_ids = Object.keys(C2P_DATA); for (let i = 0; i < app_ids.length; i++) { const app_id = app_ids[i]; - if (C2P_DATA.hasOwnProperty(app_id)) { + if (Object.prototype.hasOwnProperty.call(C2P_DATA, app_id)) { if (C2P_DATA[app_id].length >= 3) { applyC2P(C2P_DATA[app_id][0], C2P_DATA[app_id][1], C2P_DATA[app_id][2]); } diff --git a/app/panel-android/components/content/FixedMenu.jsx b/app/panel-android/components/content/FixedMenu.jsx index 829a9b50a..97a3a0f9f 100644 --- a/app/panel-android/components/content/FixedMenu.jsx +++ b/app/panel-android/components/content/FixedMenu.jsx @@ -51,7 +51,7 @@ export default class FixedMenu extends React.Component { const categories = Object.keys(this.antiTrackingData); for (let i = 0; i < categories.length; i++) { const category = categories[i]; - if (this.antiTrackingData.hasOwnProperty(category)) { + if (Object.prototype.hasOwnProperty.call(this.antiTrackingData, category)) { const apps = Object.keys(this.antiTrackingData[category]); for (let j = 0; j < apps.length; j++) { const app = apps[j]; diff --git a/app/panel/reducers/blocking.js b/app/panel/reducers/blocking.js index 626a0c5b0..fba6086b5 100644 --- a/app/panel/reducers/blocking.js +++ b/app/panel/reducers/blocking.js @@ -206,7 +206,7 @@ const _updateCliqzModuleWhitelist = (state, action) => { const addToWhitelist = () => { unknownTracker.domains.forEach((domain) => { - if (whitelistedUrls.hasOwnProperty(domain)) { + if (Object.prototype.hasOwnProperty.call(whitelistedUrls, domain)) { whitelistedUrls[domain].name = unknownTracker.name; whitelistedUrls[domain].hosts.push(pageHost); } else { diff --git a/app/panel/reducers/panel.js b/app/panel/reducers/panel.js index e814d834e..b6f644fc2 100644 --- a/app/panel/reducers/panel.js +++ b/app/panel/reducers/panel.js @@ -303,14 +303,14 @@ const _showNotification = (state, action) => { updated_needsReload = { ...state.needsReload, changes: { ...state.needsReload.changes } }; // handle case where user clicks 'whitelist' then 'blacklist', or inverse - if (msg.updated === 'blacklist' && updated_needsReload.changes.hasOwnProperty('whitelist')) { + if (msg.updated === 'blacklist' && Object.prototype.hasOwnProperty.call(updated_needsReload.changes, 'whitelist')) { delete updated_needsReload.changes.whitelist; - } else if (msg.updated === 'whitelist' && updated_needsReload.changes.hasOwnProperty('blacklist')) { + } else if (msg.updated === 'whitelist' && Object.prototype.hasOwnProperty.call(updated_needsReload.changes, 'blacklist')) { delete updated_needsReload.changes.blacklist; } // update the 'changes' object. if the changed item already exists, remove it to signal a disable has occurred - if (updated_needsReload.changes.hasOwnProperty(msg.updated)) { + if (Object.prototype.hasOwnProperty.call(updated_needsReload.changes, msg.updated)) { delete updated_needsReload.changes[msg.updated]; } else if (msg.updated !== 'init') { // ignore the 'init' change, which comes from Panel.jsx to persist banners updated_needsReload.changes[msg.updated] = true; diff --git a/app/panel/utils/blocking.js b/app/panel/utils/blocking.js index b3f2c5e0d..46669f5ef 100644 --- a/app/panel/utils/blocking.js +++ b/app/panel/utils/blocking.js @@ -33,8 +33,8 @@ export function updateSummaryBlockingCount(categories = [], smartBlock, updateTr categories.forEach((categoryEl) => { categoryEl.trackers.forEach((trackerEl) => { numTotal++; - const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); - const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); + const sbBlocked = Object.prototype.hasOwnProperty.call(smartBlock.blocked, trackerEl.id); + const sbUnblocked = Object.prototype.hasOwnProperty.call(smartBlock.unblocked, trackerEl.id); if (trackerEl.ss_blocked || sbBlocked || (trackerEl.blocked && !trackerEl.ss_allowed && !sbUnblocked)) { numTotalBlocked++; @@ -81,8 +81,8 @@ export function updateBlockAllTrackers(state, action) { updated_categories.forEach((categoryEl) => { categoryEl.num_blocked = 0; categoryEl.trackers.forEach((trackerEl) => { - const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); - const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); + const sbBlocked = Object.prototype.hasOwnProperty.call(smartBlock.blocked, trackerEl.id); + const sbUnblocked = Object.prototype.hasOwnProperty.call(smartBlock.unblocked, trackerEl.id); if (trackerEl.shouldShow) { trackerEl.blocked = blocked; @@ -124,8 +124,8 @@ export function updateCategoryBlocked(state, action) { const updated_category = updated_categories[catIndex]; updated_category.num_blocked = 0; updated_category.trackers.forEach((trackerEl) => { - const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); - const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); + const sbBlocked = Object.prototype.hasOwnProperty.call(smartBlock.blocked, trackerEl.id); + const sbUnblocked = Object.prototype.hasOwnProperty.call(smartBlock.unblocked, trackerEl.id); if (trackerEl.shouldShow) { trackerEl.blocked = blocked; @@ -194,8 +194,8 @@ export function updateTrackerBlocked(state, action) { updated_category.num_blocked = 0; updated_category.trackers.forEach((trackerEl) => { - const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); - const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); + const sbBlocked = Object.prototype.hasOwnProperty.call(smartBlock.blocked, trackerEl.id); + const sbUnblocked = Object.prototype.hasOwnProperty.call(smartBlock.unblocked, trackerEl.id); if (trackerEl.shouldShow) { if (trackerEl.id === action.data.app_id) { diff --git a/src/background.js b/src/background.js index d47527fb7..67172433d 100644 --- a/src/background.js +++ b/src/background.js @@ -128,10 +128,10 @@ function setGhosteryDefaultBlocking() { const app_ids = Object.keys(bugDb.db.apps); for (let i = 0; i < app_ids.length; i++) { const app_id = app_ids[i]; - if (bugDb.db.apps.hasOwnProperty(app_id)) { + if (Object.prototype.hasOwnProperty.call(bugDb.db.apps, app_id)) { const category = bugDb.db.apps[app_id].cat; if (categoriesBlock.indexOf(category) >= 0 && - !selected_app_ids.hasOwnProperty(app_id)) { + !Object.prototype.hasOwnProperty.call(selected_app_ids, app_id)) { selected_app_ids[app_id] = 1; } } @@ -497,7 +497,7 @@ function handleRewards(name, message, callback) { metrics.ping(message); break; case 'setPanelData': - if (message.hasOwnProperty('enable_offers')) { + if (Object.prototype.hasOwnProperty.call(message, 'enable_offers')) { rewards.sendSignal(message.signal); panelData.set({ enable_offers: message.enable_offers }); } @@ -579,7 +579,7 @@ function handleGhosteryHub(name, message, callback) { const app_ids = Object.keys(bugDb.db.apps); for (let i = 0; i < app_ids.length; i++) { const app_id = app_ids[i]; - if (!selected_app_ids.hasOwnProperty(app_id)) { + if (!Object.prototype.hasOwnProperty.call(selected_app_ids, app_id)) { selected_app_ids[app_id] = 1; } } @@ -775,7 +775,7 @@ function onMessageHandler(request, sender, callback) { const { email, password } = message; account.login(email, password) .then((response) => { - if (!response.hasOwnProperty('errors')) { + if (!Object.property.hasOwnProperty.call(response, 'errors')) { metrics.ping('sign_in_success'); } callback(response); @@ -792,7 +792,7 @@ function onMessageHandler(request, sender, callback) { } = message; account.register(email, confirmEmail, password, firstName, lastName) .then((response) => { - if (!response.hasOwnProperty('errors')) { + if (!Object.property.hasOwnProperty.call(response, 'errors')) { metrics.ping('create_account_success'); } callback(response); @@ -1012,7 +1012,7 @@ function initializeDispatcher() { const { db } = bugDb; db.noneSelected = (num_selected === 0); // can't simply compare num_selected and size(db.apps) since apps get removed sometimes - db.allSelected = (!!num_selected && every(db.apps, (app, app_id) => appIds.hasOwnProperty(app_id))); + db.allSelected = (!!num_selected && every(db.apps, (app, app_id) => Object.property.hasOwnProperty.call(appIds, app_id))); }); dispatcher.on('conf.save.site_whitelist', () => { // TODO debounce with below diff --git a/src/classes/ABTest.js b/src/classes/ABTest.js index df7a235fb..51a47110c 100644 --- a/src/classes/ABTest.js +++ b/src/classes/ABTest.js @@ -34,7 +34,7 @@ class ABTest { * @param {string} name test name */ hasTest(name) { - return this.tests.hasOwnProperty(name); + return Object.prototype.hasOwnProperty.call(this.tests, name); } /** diff --git a/src/classes/BugDb.js b/src/classes/BugDb.js index 28284cdb6..aeee2f505 100644 --- a/src/classes/BugDb.js +++ b/src/classes/BugDb.js @@ -85,12 +85,12 @@ class BugDb extends Updatable { const categories = {}; for (appId in db.apps) { - if (db.apps.hasOwnProperty(appId)) { + if (Object.prototype.hasOwnProperty.call(db.apps, appId)) { category = db.apps[appId].cat; if (t(`category_${category}`) === `category_${category}`) { category = 'uncategorized'; } - blocked = selectedApps.hasOwnProperty(appId); + blocked = Object.prototype.hasOwnProperty.call(selectedApps, appId); // Because we have two trackers in the DB with the same name if ((categories[category] && categories[category].trackers[db.apps[appId].name])) { @@ -98,7 +98,7 @@ class BugDb extends Updatable { continue; } - if (categories.hasOwnProperty(category)) { + if (Object.prototype.hasOwnProperty.call(categories, category)) { categories[category].num_total++; if (blocked) { categories[category].num_blocked++; @@ -129,7 +129,7 @@ class BugDb extends Updatable { const categoryNames = Object.keys(categories); for (let i = 0; i < categoryNames.length; i++) { categoryName = categoryNames[i]; - if (categories.hasOwnProperty(categoryName)) { + if (Object.prototype.hasOwnProperty.call(categories, categoryName)) { const category = categories[categoryName]; if (category.trackers) { category.trackers.sort((a, b) => { @@ -182,7 +182,7 @@ class BugDb extends Updatable { log('initializing bugdb regexes...'); for (const id in regexes) { - if (regexes.hasOwnProperty(id)) { + if (Object.prototype.hasOwnProperty.call(regexes, id)) { db.patterns.regex[id] = new RegExp(regexes[id], 'i'); } } @@ -195,7 +195,7 @@ class BugDb extends Updatable { // since allSelected is slow to eval, make it lazy defineLazyProperty(db, 'allSelected', () => { const num_selected = size(conf.selected_app_ids); - return (!!num_selected && every(db.apps, (app, app_id) => conf.selected_app_ids.hasOwnProperty(app_id))); + return (!!num_selected && every(db.apps, (app, app_id) => Object.prototype.hasOwnProperty.call(conf.selected_app_ids, app_id))); }); log('processed bugdb...'); @@ -206,7 +206,7 @@ class BugDb extends Updatable { // if there is an older bugs object in storage, // update newAppIds and apply block-by-default if (old_bugs) { - if (old_bugs.hasOwnProperty('version') && bugs.version > old_bugs.version) { + if (Object.prototype.hasOwnProperty.call(old_bugs, 'version') && bugs.version > old_bugs.version) { new_app_ids = this.updateNewAppIds(bugs.apps, old_bugs.apps); if (new_app_ids.length) { @@ -215,7 +215,7 @@ class BugDb extends Updatable { } // pre-trie/legacy db - } else if (old_bugs.hasOwnProperty('bugsVersion') && bugs.version !== old_bugs.bugsVersion) { + } else if (Object.prototype.hasOwnProperty.call(old_bugs, 'bugsVersion') && bugs.version !== old_bugs.bugsVersion) { const old_apps = reduce(old_bugs.bugs, (memo, bug) => { memo[bug.aid] = true; return memo; diff --git a/src/classes/Click2PlayDb.js b/src/classes/Click2PlayDb.js index 40324e497..cdeefa4c7 100644 --- a/src/classes/Click2PlayDb.js +++ b/src/classes/Click2PlayDb.js @@ -66,7 +66,7 @@ class Click2PlayDb extends Updatable { // TODO memory leak when you close tabs before reset() can run? reset(tab_id) { - if (!this.allowOnceList.hasOwnProperty(tab_id)) { return; } + if (!Object.prototype.hasOwnProperty.call(this.allowOnceList, tab_id)) { return; } const entries = Object.entries(this.allowOnceList[tab_id]); let keep = false; @@ -84,8 +84,8 @@ class Click2PlayDb extends Updatable { allowedOnce(tab_id, aid) { return ( - this.allowOnceList.hasOwnProperty(tab_id) && - this.allowOnceList[tab_id].hasOwnProperty(aid) && + Object.prototype.hasOwnProperty.call(this.allowOnceList, tab_id) && + Object.prototype.hasOwnProperty.call(this.allowOnceList[tab_id], aid) && this.allowOnceList[tab_id][aid] > 0 ); } @@ -113,7 +113,7 @@ class Click2PlayDb extends Updatable { let allow; entries.forEach((entry) => { - if (!apps.hasOwnProperty(entry.aid)) { + if (!Object.prototype.hasOwnProperty.call(apps, entry.aid)) { apps[entry.aid] = []; } diff --git a/src/classes/CompatibilityDb.js b/src/classes/CompatibilityDb.js index 914e7d613..bb81c334a 100644 --- a/src/classes/CompatibilityDb.js +++ b/src/classes/CompatibilityDb.js @@ -66,7 +66,7 @@ class CompatibilityDb extends Updatable { * @return {Boolean} */ hasIssue(aid, tab_url) { - return this.db.list && this.db.list.hasOwnProperty(aid) && fuzzyUrlMatcher(tab_url, this.db.list[aid]); + return this.db.list && Object.prototype.hasOwnProperty.call(this.db.list, aid) && fuzzyUrlMatcher(tab_url, this.db.list[aid]); } /** diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 453857aa8..23dcaa013 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -186,12 +186,12 @@ class ConfData { let lang = window.navigator.language.replace('-', '_'); - if (SUPPORTED_LANGUAGES.hasOwnProperty(lang)) { + if (Object.prototype.hasOwnProperty.call(SUPPORTED_LANGUAGES, lang)) { return lang; } lang = lang.slice(0, 2); - if (SUPPORTED_LANGUAGES.hasOwnProperty(lang)) { + if (Object.prototype.hasOwnProperty.call(SUPPORTED_LANGUAGES, lang)) { return lang; } diff --git a/src/classes/FoundBugs.js b/src/classes/FoundBugs.js index 1404106c8..1d956743e 100644 --- a/src/classes/FoundBugs.js +++ b/src/classes/FoundBugs.js @@ -139,7 +139,7 @@ class FoundBugs { if (app_id) { const { appsById } = this._foundApps[tab_id]; - if (appsById.hasOwnProperty(app_id)) { + if (Object.prototype.hasOwnProperty.call(appsById, app_id)) { apps_arr.push(apps[appsById[app_id]]); } } else { @@ -203,11 +203,11 @@ class FoundBugs { const ids = Object.keys(bugs); for (let i = 0; i < ids.length; i++) { id = ids[i]; - if (bugs.hasOwnProperty(id)) { + if (Object.prototype.hasOwnProperty.call(bugs, id)) { aid = db.bugs[id].aid; // eslint-disable-line prefer-destructuring cid = db.apps[aid].cat; - if (cats_obj.hasOwnProperty(cid)) { + if (Object.prototype.hasOwnProperty.call(cats_obj, cid)) { if (!cats_obj[cid].appIds.includes(aid)) { cats_obj[cid].appIds.push(aid); cats_obj[cid].trackers.push({ @@ -244,7 +244,7 @@ class FoundBugs { const cids = Object.keys(cats_obj); for (let i = 0; i < cids.length; i++) { cid = cids[i]; - if (cats_obj.hasOwnProperty(cid)) { + if (Object.prototype.hasOwnProperty.call(cats_obj, cid)) { cats_arr.push(cats_obj[cid]); } } @@ -364,7 +364,7 @@ class FoundBugs { const { aid } = bugDb.db.bugs[bug_id]; const { apps, appsById, issueCounts } = this._foundApps[tab_id]; - if (appsById.hasOwnProperty(aid)) { + if (Object.prototype.hasOwnProperty.call(appsById, aid)) { const app = apps[appsById[aid]]; if (!app.hasLatencyIssue) { issueCounts.latency++; @@ -395,11 +395,11 @@ class FoundBugs { return false; } - if (!this._foundBugs.hasOwnProperty(tab_id)) { + if (!Object.prototype.hasOwnProperty.call(this._foundBugs, tab_id)) { this._foundBugs[tab_id] = {}; } - if (!this._foundApps.hasOwnProperty(tab_id)) { + if (!Object.prototype.hasOwnProperty.call(this._foundApps, tab_id)) { this._foundApps[tab_id] = { apps: [], appsMetadata: {}, @@ -445,7 +445,7 @@ class FoundBugs { * @param {string} type */ _updateFoundBugs(tab_id, bug_id, src, blocked, type) { - if (!this._foundBugs[tab_id].hasOwnProperty(bug_id)) { + if (!Object.prototype.hasOwnProperty.call(this._foundBugs[tab_id], bug_id)) { this._foundBugs[tab_id][bug_id] = { sources: [], hasLatencyIssue: false, @@ -487,7 +487,7 @@ class FoundBugs { apps, appsMetadata, appsById, issueCounts } = this._foundApps[tab_id]; - if (appsById.hasOwnProperty(aid)) { + if (Object.prototype.hasOwnProperty.call(appsById, aid)) { const app = apps[appsById[aid]]; if (!app.hasLatencyIssue && hasLatencyIssue) { issueCounts.latency++; } diff --git a/src/classes/Latency.js b/src/classes/Latency.js index f5b331111..f8acc509f 100644 --- a/src/classes/Latency.js +++ b/src/classes/Latency.js @@ -33,7 +33,7 @@ class Latency { const request_id = details.requestId; const tab_id = details.tabId; - if (!this.latencies.hasOwnProperty(request_id)) { + if (!Object.prototype.hasOwnProperty.call(this.latencies, request_id)) { return 0; } // If the latencies object for this request id is empty then this is @@ -46,7 +46,7 @@ class Latency { // TRACKER1 --> NON-TRACKER --> TRACKER2 // TRACKER2's onBeforeRequest sync callback could maybe fire before // NON-TRACKER's onBeforeRedirect async callback - if (!this.latencies[request_id].hasOwnProperty(details.url)) { + if (!Object.prototype.hasOwnProperty.call(this.latencies[request_id], details.url)) { return 0; } diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 6442f8973..4d3e07e38 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -589,7 +589,7 @@ class PanelData { // Set the conf from data // TODO can this now be replaced by Object.entries? for (const [key, value] of objectEntries(data)) { - if (conf.hasOwnProperty(key) && !isEqual(conf[key], value)) { + if (Object.prototype.hasOwnProperty.call(conf, key) && !isEqual(conf[key], value)) { conf[key] = value; syncSetDataChanged = SYNC_SET.has(key) ? true : syncSetDataChanged; // TODO refactor - this work should probably be the direct responsibility of Globals @@ -658,7 +658,7 @@ class PanelData { cat = 'uncategorized'; } - if (categories.hasOwnProperty(cat)) { + if (Object.prototype.hasOwnProperty.call(categories, cat)) { categories[cat].num_total++; if (this._addsUpToBlocked(trackerState)) { categories[cat].num_blocked++; } } else { @@ -745,7 +745,7 @@ class PanelData { warningCompatibility: hasCompatibilityIssue, warningInsecure: hasInsecureIssue, warningSlow: hasLatencyIssue, - warningSmartBlock: (smartBlock.blocked.hasOwnProperty(id) && 'blocked') || (smartBlock.unblocked.hasOwnProperty(id) && 'unblocked') || false, + warningSmartBlock: (Object.prototype.hasOwnProperty.call(smartBlock.blocked, id) && 'blocked') || (Object.prototype.hasOwnProperty.call(smartBlock.unblocked, id) && 'unblocked') || false, cliqzAdCount, cliqzCookieCount, cliqzFingerprintCount, @@ -770,11 +770,11 @@ class PanelData { const pageBlocks = (pageHost && conf.site_specific_blocks[pageHost]) || []; return { - blocked: selectedAppIds.hasOwnProperty(trackerId), + blocked: Object.prototype.hasOwnProperty.call(selectedAppIds, trackerId), ss_allowed: pageUnblocks.includes(+trackerId), ss_blocked: pageBlocks.includes(+trackerId), - sb_blocked: smartBlockActive && smartBlock.blocked.hasOwnProperty(`${trackerId}`), - sb_allowed: smartBlockActive && smartBlock.unblocked.hasOwnProperty(`${trackerId}`) + sb_blocked: smartBlockActive && Object.prototype.hasOwnProperty.call(smartBlock.blocked, `${trackerId}`), + sb_allowed: smartBlockActive && Object.prototype.hasOwnProperty.call(smartBlock.unblocked, `${trackerId}`) }; } @@ -790,7 +790,7 @@ class PanelData { const { trackers } = categoryEl; categoryEl.num_blocked = 0; trackers.forEach((trackerEl) => { - trackerEl.blocked = selectedApps.hasOwnProperty(trackerEl.id); + trackerEl.blocked = Object.prototype.hasOwnProperty.call(selectedApps, trackerEl.id); if (trackerEl.blocked) { categoryEl.num_blocked++; } diff --git a/src/classes/Policy.js b/src/classes/Policy.js index d8b30f814..294bfc1c5 100644 --- a/src/classes/Policy.js +++ b/src/classes/Policy.js @@ -156,9 +156,9 @@ class Policy { const allowedOnce = c2pDb.allowedOnce(tab_id, app_id); // The app_id has been globally blocked - if (conf.selected_app_ids.hasOwnProperty(app_id)) { + if (Object.prototype.hasOwnProperty.call(conf.selected_app_ids, app_id)) { // The app_id is on the site-specific allow list for this tab_host - if (conf.toggle_individual_trackers && conf.site_specific_unblocks.hasOwnProperty(tab_host) && conf.site_specific_unblocks[tab_host].includes(+app_id)) { + if (conf.toggle_individual_trackers && Object.prototype.hasOwnProperty.call(conf.site_specific_unblocks, tab_host) && conf.site_specific_unblocks[tab_host].includes(+app_id)) { // Site blacklist overrides all block settings except C2P allow once if (this.blacklisted(tab_url)) { return { block: !allowedOnce, reason: allowedOnce ? BLOCK_REASON_C2P_ALLOWED_ONCE : BLOCK_REASON_BLACKLISTED }; @@ -175,7 +175,7 @@ class Policy { // The app_id has not been globally blocked // Check to see if the app_id is on the site-specific block list for this tab_host - if (conf.toggle_individual_trackers && conf.site_specific_blocks.hasOwnProperty(tab_host) && conf.site_specific_blocks[tab_host].includes(+app_id)) { + if (conf.toggle_individual_trackers && Object.prototype.hasOwnProperty.call(conf.site_specific_blocks, tab_host) && conf.site_specific_blocks[tab_host].includes(+app_id)) { // Site white-listing overrides blocking settings if (this.checkSiteWhitelist(tab_url)) { return { block: false, reason: BLOCK_REASON_WHITELISTED }; @@ -183,7 +183,7 @@ class Policy { return { block: !allowedOnce, reason: allowedOnce ? BLOCK_REASON_C2P_ALLOWED_ONCE : BLOCK_REASON_SS_BLOCKED }; } // Check to see if the app_id is on the site-specific allow list for this tab_host - if (conf.toggle_individual_trackers && conf.site_specific_unblocks.hasOwnProperty(tab_host) && conf.site_specific_unblocks[tab_host].includes(+app_id)) { + if (conf.toggle_individual_trackers && Object.prototype.hasOwnProperty.call(conf.site_specific_unblocks, tab_host) && conf.site_specific_unblocks[tab_host].includes(+app_id)) { // Site blacklist overrides all block settings except C2P allow once if (this.blacklisted(tab_url)) { return { block: !allowedOnce, reason: allowedOnce ? BLOCK_REASON_C2P_ALLOWED_ONCE : BLOCK_REASON_BLACKLISTED }; diff --git a/src/classes/PolicySmartBlock.js b/src/classes/PolicySmartBlock.js index 8bed9ea1d..65a95020f 100644 --- a/src/classes/PolicySmartBlock.js +++ b/src/classes/PolicySmartBlock.js @@ -135,9 +135,9 @@ class PolicySmartBlock { conf.enable_smart_block && !globals.SESSION.paused_blocking && !this.policy.getSitePolicy(tabUrl) && - ((appId && (!conf.site_specific_unblocks.hasOwnProperty(tabHost) || !conf.site_specific_unblocks[tabHost].includes(+appId))) || appId === false) && - ((appId && (!conf.site_specific_blocks.hasOwnProperty(tabHost) || !conf.site_specific_blocks[tabHost].includes(+appId))) || appId === false) && - (c2pDb.db.apps && !c2pDb.db.apps.hasOwnProperty(appId)) + ((appId && (!Object.prototype.hasOwnProperty.call(conf.site_specific_unblocks, tabHost) || !conf.site_specific_unblocks[tabHost].includes(+appId))) || appId === false) && + ((appId && (!Object.prototype.hasOwnProperty.call(conf.site_specific_blocks, tabHost) || !conf.site_specific_blocks[tabHost].includes(+appId))) || appId === false) && + (c2pDb.db.apps && !Object.prototype.hasOwnProperty.call(c2pDb.db.apps, appId)) ); } diff --git a/src/classes/SurrogateDb.js b/src/classes/SurrogateDb.js index 564705c27..535a01b61 100644 --- a/src/classes/SurrogateDb.js +++ b/src/classes/SurrogateDb.js @@ -63,24 +63,24 @@ class SurrogateDb extends Updatable { // convert single values to arrays first ['pattern_id', 'app_id', 'sites', 'match'].forEach((prop) => { - if (s.hasOwnProperty(prop) && !isArray(s[prop])) { + if (Object.prototype.hasOwnProperty.call(s, prop) && !isArray(s[prop])) { s[prop] = [s[prop]]; } }); // initialize regexes - if (s.hasOwnProperty('match')) { + if (Object.prototype.hasOwnProperty.call(s, 'match')) { s.match = map(s.match, match => new RegExp(match, '')); } - if (s.hasOwnProperty('pattern_id') || s.hasOwnProperty('app_id')) { + if (Object.prototype.hasOwnProperty.call(s, 'pattern_id') || Object.prototype.hasOwnProperty.call(s, 'app_id')) { // tracker-level surrogate - if (s.hasOwnProperty('pattern_id')) { + if (Object.prototype.hasOwnProperty.call(s, 'pattern_id')) { this._buildDb(s, 'pattern_id', 'pattern_ids'); - } else if (s.hasOwnProperty('app_id')) { + } else if (Object.prototype.hasOwnProperty.call(s, 'app_id')) { this._buildDb(s, 'app_id', 'app_ids'); } - } else if (s.hasOwnProperty('sites')) { + } else if (Object.prototype.hasOwnProperty.call(s, 'sites')) { // we have a "sites" property, but not pattern_id/app_id: // it's a site surrogate this._buildDb(s, 'sites', 'site_surrogates'); @@ -104,23 +104,23 @@ class SurrogateDb extends Updatable { getForTracker(script_src, app_id, pattern_id, host_name) { let candidates = []; - if (this.db.app_ids.hasOwnProperty(app_id)) { + if (Object.prototype.hasOwnProperty.call(this.db.app_ids, app_id)) { candidates = candidates.concat(this.db.app_ids[app_id]); } - if (this.db.pattern_ids.hasOwnProperty(pattern_id)) { + if (Object.prototype.hasOwnProperty.call(this.db.pattern_ids, pattern_id)) { candidates = candidates.concat(this.db.pattern_ids[pattern_id]); } return filter(candidates, (surrogate) => { // note: does not support *.example.com (exact matches only) - if (surrogate.hasOwnProperty('sites')) { // array of site hosts + if (Object.prototype.hasOwnProperty.call(surrogate, 'sites')) { // array of site hosts if (!surrogate.sites.includes(host_name)) { return false; } } - if (surrogate.hasOwnProperty('match')) { + if (Object.prototype.hasOwnProperty.call(surrogate, 'match')) { if (!any(surrogate.match, match => script_src.match(match))) { return false; } @@ -142,7 +142,7 @@ class SurrogateDb extends Updatable { */ _buildDb(surrogate, property, db_name) { surrogate[property].forEach((val) => { - if (!this.db[db_name].hasOwnProperty(val)) { + if (!Object.prototype.hasOwnProperty.call(this.db[db_name], val)) { this.db[db_name][val] = []; } this.db[db_name][val].push(surrogate); diff --git a/src/classes/TabInfo.js b/src/classes/TabInfo.js index 9d3e60559..4375f663c 100644 --- a/src/classes/TabInfo.js +++ b/src/classes/TabInfo.js @@ -83,7 +83,7 @@ class TabInfo { // TODO consider improving handling of what if we mistype the property name. // always returning object where property might sometimes have returned false could result in subtle bugs. getTabInfo(tab_id, property) { - if (this._tabInfo.hasOwnProperty(tab_id)) { + if (Object.prototype.hasOwnProperty.call(this._tabInfo, tab_id)) { if (property) { return this._tabInfo[tab_id][property]; } @@ -99,7 +99,7 @@ class TabInfo { * @return {Object} persistent data for this tab */ getTabInfoPersist(tab_id, property) { - if (this._tabInfoPersist.hasOwnProperty(tab_id)) { + if (Object.prototype.hasOwnProperty.call(this._tabInfoPersist, tab_id)) { if (property) { return this._tabInfoPersist[tab_id][property]; } @@ -115,7 +115,7 @@ class TabInfo { * @param {*} value property value */ setTabInfo(tab_id, property, value) { - if (this._tabInfo.hasOwnProperty(tab_id)) { + if (Object.prototype.hasOwnProperty.call(this._tabInfo, tab_id)) { // check for 'url' property case if (property === 'url') { this._updateUrl(tab_id, value); @@ -133,7 +133,7 @@ class TabInfo { * @param {boolean} blocked kind of policy to set */ setTabSmartBlockAppInfo(tabId, appId, rule, blocked) { - if (!this._tabInfo.hasOwnProperty(tabId)) { return; } + if (!Object.prototype.hasOwnProperty.call(this._tabInfo, tabId)) { return; } const policy = blocked ? 'blocked' : 'unblocked'; if (typeof this._tabInfo[tabId].smartBlock[policy][appId] === 'undefined') { @@ -151,7 +151,7 @@ class TabInfo { * @param {number} tab_id tab id */ clear(tab_id) { - if (!this._tabInfo.hasOwnProperty(tab_id)) { return; } + if (!Object.prototype.hasOwnProperty.call(this._tabInfo, tab_id)) { return; } const { numOfReloads, firstLoadTimestamp } = this._tabInfo[tab_id]; // TODO potential memory leak? diff --git a/src/classes/Updatable.js b/src/classes/Updatable.js index b8a5ba576..cb191d47a 100644 --- a/src/classes/Updatable.js +++ b/src/classes/Updatable.js @@ -77,7 +77,7 @@ class Updatable { const version_property = (this.type === 'bugs' || this.type === 'surrogates' ? 'version' : (`${this.type}Version`)); // nothing in storage, or it's so old it doesn't have a version - if (!memory || !memory.hasOwnProperty(version_property)) { + if (!memory || !Object.prototype.hasOwnProperty.call(memory, version_property)) { // return what's on disk log(`fetching ${this.type} from disk`); diff --git a/src/utils/click2play.js b/src/utils/click2play.js index 5e433db32..6dc0f0327 100644 --- a/src/utils/click2play.js +++ b/src/utils/click2play.js @@ -154,7 +154,7 @@ export function allowAllwaysC2P(app_id, tab_host) { // Remove fron site-specific-blocked - if (conf.site_specific_blocks.hasOwnProperty(tab_host) && conf.site_specific_blocks[tab_host].includes(+app_id)) { + if (Object.prototype.hasOwnProperty.call(conf.site_specific_blocks, tab_host) && conf.site_specific_blocks[tab_host].includes(+app_id)) { const index = conf.site_specific_blocks[tab_host].indexOf(+app_id); const { site_specific_blocks } = conf; site_specific_blocks[tab_host].splice(index); @@ -163,7 +163,7 @@ export function allowAllwaysC2P(app_id, tab_host) { // Add tracker to site-specific-allowed const { site_specific_unblocks } = conf; - if (!site_specific_unblocks.hasOwnProperty(tab_host)) { + if (!Object.prototype.hasOwnProperty.call(site_specific_unblocks, tab_host)) { // create new array of unblocks for this host site_specific_unblocks[tab_host] = []; } diff --git a/src/utils/common.js b/src/utils/common.js index 06767279e..e870cfb0a 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -65,14 +65,14 @@ export function prefsGet(...args) { result = items; } else if (args.length === 1) { const key = args[0]; // extract value from array - if (items && items.hasOwnProperty(key)) { + if (items && Object.prototype.hasOwnProperty.call(items, key)) { result = items[key]; } } else { result = {}; // instantiate an empty object args.forEach((key) => { result[key] = null; - if (items && items.hasOwnProperty(key)) { + if (items && Object.prototype.hasOwnProperty.call(items, key)) { result[key] = items[key]; } }); diff --git a/src/utils/matcher.js b/src/utils/matcher.js index 1e34b5e1d..1f9936010 100644 --- a/src/utils/matcher.js +++ b/src/utils/matcher.js @@ -120,7 +120,7 @@ function _matchesHostPath(roots, src_path) { for (i = 0; i < roots.length; i++) { root = roots[i]; - if (root.hasOwnProperty('$')) { + if (Object.prototype.hasOwnProperty.call(root, '$')) { paths = root.$; for (j = 0; j < paths.length; j++) { if (src_path.startsWith(paths[j].path)) { @@ -154,14 +154,14 @@ function _matchesHost(root, src_host, src_path) { for (let i = 0; i < host_rev_arr.length; i++) { host_part = host_rev_arr[i]; // if node has domain, advance and try to update bug_id - if (node.hasOwnProperty(host_part)) { + if (Object.prototype.hasOwnProperty.call(node, host_part)) { // advance node node = node[host_part]; - bug_id = (node.hasOwnProperty('$') ? node.$ : bug_id); + bug_id = (Object.prototype.hasOwnProperty.call(node, '$') ? node.$ : bug_id); // we store all traversed nodes that contained paths in case the final // node does not have the matching path - if (src_path !== undefined && node.hasOwnProperty('$')) { + if (src_path !== undefined && Object.prototype.hasOwnProperty.call(node, '$')) { nodes_with_paths.push(node); } From f11bc18abacdc12a4250bcbf6c6cc777fa64370d Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Tue, 5 May 2020 21:30:03 +0200 Subject: [PATCH 05/89] add linting for class-methods-use-this and fix most resulting errors. Add /tools and /tests to linter. Update height in UpgradeBanner --- .eslintrc.js | 4 +- app/licenses/Licenses.jsx | 4 +- .../components/content/FixedMenu.jsx | 6 +- app/panel-android/components/content/Path.jsx | 10 +-- app/panel/components/Blocking/Tracker.jsx | 14 ++-- .../BuildingBlocks/CliqzFeature.jsx | 16 ++-- .../components/BuildingBlocks/DonutGraph.jsx | 12 +-- .../BuildingBlocks/GhosteryFeature.jsx | 12 +-- app/panel/components/Rewards.jsx | 25 +++--- .../components/Settings/TrustAndRestrict.jsx | 4 +- .../Settings/__tests__/TrustAndRestrict.jsx | 79 ++++++++----------- app/scss/partials/_upgrade_banner.scss | 4 +- package.json | 4 +- src/background.js | 15 ++-- src/classes/BrowserButton.js | 7 +- src/classes/Click2PlayDb.js | 4 +- src/classes/CompatibilityDb.js | 4 +- src/classes/ConfData.js | 4 +- src/classes/EventHandlers.js | 6 +- src/classes/ExtMessenger.js | 13 ++- src/classes/Metrics.js | 34 ++++---- src/classes/PanelData.js | 63 ++++++++------- src/classes/Policy.js | 30 +++---- src/classes/PolicySmartBlock.js | 36 ++++----- src/classes/PurpleBox.js | 3 +- src/classes/Rewards.js | 12 +-- src/classes/SurrogateDb.js | 2 +- src/classes/TabInfo.js | 6 +- src/utils/api.js | 6 +- src/utils/click2play.js | 6 +- test/src/Policy.test.js | 68 ++++++++-------- test/src/PolicySmartBlock.test.js | 14 ++-- tools/i18n-checker.js | 18 ++--- tools/leet/leet-en.js | 4 +- tools/transifex.js | 4 +- 35 files changed, 265 insertions(+), 288 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 2c0ef4af4..739230bb0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -40,7 +40,7 @@ module.exports = { rules: { 'arrow-parens': [2, 'as-needed', { 'requireForBlockBody': true }], 'camelcase': [0], - 'class-methods-use-this': [0], // TODO: enable this check + 'class-methods-use-this': [1], 'comma-dangle': [2, { 'arrays': 'only-multiline', 'objects': 'only-multiline', @@ -90,7 +90,7 @@ module.exports = { 'react/no-danger': [0], 'react/prop-types': [0], 'react/jsx-fragments': [1, 'element'], - 'react/sort-comp': [2, { + 'react/sort-comp': [0, { //TODO: enable this check order: [ "static-variables", "instance-variables", diff --git a/app/licenses/Licenses.jsx b/app/licenses/Licenses.jsx index cd9aaf339..fb083fc9e 100644 --- a/app/licenses/Licenses.jsx +++ b/app/licenses/Licenses.jsx @@ -31,7 +31,7 @@ class Licenses extends React.Component { * Wrapper function for dangerouslySetInnerHTML. Provides extra security * @return {Object} */ - createFooterMarkup() { + static createFooterMarkup() { return { __html: t('license_footer') }; } @@ -60,7 +60,7 @@ class Licenses extends React.Component {
{ list }
)} {oneOrMoreAds && (
{this._renderCliqzAdsIcon()} - {this._renderCliqzAdStat(cliqzAdCount)} + {Tracker._renderCliqzAdStat(cliqzAdCount)}
)} @@ -275,13 +275,13 @@ class Tracker extends React.Component { ); } - _renderCliqzCookieStat(count) { return this._renderCliqzStat(count, 'cookie'); } + static _renderCliqzCookieStat(count) { return Tracker._renderCliqzStat(count, 'cookie'); } - _renderCliqzFingerprintStat(count) { return this._renderCliqzStat(count, 'fingerprint'); } + static _renderCliqzFingerprintStat(count) { return Tracker._renderCliqzStat(count, 'fingerprint'); } - _renderCliqzAdStat(count) { return this._renderCliqzStat(count, 'ad'); } + static _renderCliqzAdStat(count) { return Tracker._renderCliqzStat(count, 'ad'); } - _renderCliqzStat(count, type) { + static _renderCliqzStat(count, type) { const exactlyOne = count === 1; const label = exactlyOne ? t(`${type}`) : diff --git a/app/panel/components/BuildingBlocks/CliqzFeature.jsx b/app/panel/components/BuildingBlocks/CliqzFeature.jsx index b699e387a..353cf635c 100644 --- a/app/panel/components/BuildingBlocks/CliqzFeature.jsx +++ b/app/panel/components/BuildingBlocks/CliqzFeature.jsx @@ -46,15 +46,15 @@ class CliqzFeature extends React.Component { clickButton({ feature: `enable_${featureType}`, status: active, - text: this._getAlertText(active, type), + text: CliqzFeature._getAlertText(active, type), }); } - _getStatus(active) { + static _getStatus(active) { return active ? t('on') : t('off'); } - _getTooltipBodyText(active, isTooltipBody, type) { + static _getTooltipBodyText(active, isTooltipBody, type) { if (!isTooltipBody) return false; if (active) { @@ -82,7 +82,7 @@ class CliqzFeature extends React.Component { } } - _getTooltipHeaderText(isTooltipHeader, type) { + static _getTooltipHeaderText(isTooltipHeader, type) { if (!isTooltipHeader) return false; switch (type) { @@ -97,7 +97,7 @@ class CliqzFeature extends React.Component { } } - _getAlertText(active, type) { + static _getAlertText(active, type) { return active ? t(`alert_${type}_off`) : t(`alert_${type}_on`); @@ -145,11 +145,11 @@ class CliqzFeature extends React.Component { return (
-
{this._getStatus(active)}
+
{CliqzFeature._getStatus(active)}
diff --git a/app/panel/components/BuildingBlocks/DonutGraph.jsx b/app/panel/components/BuildingBlocks/DonutGraph.jsx index 687253c59..17284a75a 100644 --- a/app/panel/components/BuildingBlocks/DonutGraph.jsx +++ b/app/panel/components/BuildingBlocks/DonutGraph.jsx @@ -142,14 +142,14 @@ class DonutGraph extends React.Component { /** * Helper function that calculates domain value for greyscale / redscale rendering */ - getTone(catCount, catIndex) { + static getTone(catCount, catIndex) { return catCount > 1 ? 100 / (catCount - 1) * catIndex * 0.01 : 0; } /** * Helper to retrieve a category's tooltip from the DOM */ - grabTooltip(d) { + static grabTooltip(d) { return document.getElementById(`${d.data.id}_tooltip`); } @@ -278,10 +278,10 @@ class DonutGraph extends React.Component { .append('path') .style('fill', (d, i) => { if (renderGreyscale) { - return this.colors.greyscale(this.getTone(categoryCount, i)); + return this.colors.greyscale(DonutGraph.getTone(categoryCount, i)); } if (renderRedscale) { - return this.colors.redscale(this.getTone(categoryCount, i)); + return this.colors.redscale(DonutGraph.getTone(categoryCount, i)); } return this.colors.regular(d.data.id); }) @@ -295,7 +295,7 @@ class DonutGraph extends React.Component { const centroid = trackerArc.centroid(d); const pX = centroid[0] + this.donutRadius; const pY = centroid[1] + this.donutRadius; - const tooltip = this.grabTooltip(d); + const tooltip = DonutGraph.grabTooltip(d); if (tooltip) { tooltip.style.left = `${pX - (tooltip.offsetWidth / 2)}px`; tooltip.style.top = `${pY - (tooltip.offsetHeight + 8)}px`; @@ -303,7 +303,7 @@ class DonutGraph extends React.Component { } }) .on('mouseout', (d) => { - const tooltip = this.grabTooltip(d); + const tooltip = DonutGraph.grabTooltip(d); if (tooltip) { tooltip.classList.remove('DonutGraph__tooltip--show'); } diff --git a/app/panel/components/BuildingBlocks/GhosteryFeature.jsx b/app/panel/components/BuildingBlocks/GhosteryFeature.jsx index afd498a35..8d127474e 100644 --- a/app/panel/components/BuildingBlocks/GhosteryFeature.jsx +++ b/app/panel/components/BuildingBlocks/GhosteryFeature.jsx @@ -40,7 +40,7 @@ class GhosteryFeature extends React.Component { this.props.handleClick(this.props.type); } - _getButtonText(sitePolicy, showText, type) { + static _getButtonText(sitePolicy, showText, type) { if (!showText) { return ''; } @@ -59,7 +59,7 @@ class GhosteryFeature extends React.Component { } } - _getTooltipText(sitePolicy, type) { + static _getTooltipText(sitePolicy, type) { switch (type) { case 'trust': return (sitePolicy === WHITELISTED ? @@ -74,7 +74,7 @@ class GhosteryFeature extends React.Component { } } - _isFeatureActive(type, sitePolicy) { + static _isFeatureActive(type, sitePolicy) { switch (type) { case 'trust': return sitePolicy === WHITELISTED; @@ -96,7 +96,7 @@ class GhosteryFeature extends React.Component { type } = this.props; - const active = this._isFeatureActive(type, sitePolicy); + const active = GhosteryFeature._isFeatureActive(type, sitePolicy); // TODO Foundation dependency: button const ghosteryFeatureClassNames = ClassNames( 'button', @@ -120,10 +120,10 @@ class GhosteryFeature extends React.Component {
- {this._getButtonText(sitePolicy, showText, type)} + {GhosteryFeature._getButtonText(sitePolicy, showText, type)} - +
); } diff --git a/app/panel/components/Rewards.jsx b/app/panel/components/Rewards.jsx index 3b70cc19d..dee9602d6 100644 --- a/app/panel/components/Rewards.jsx +++ b/app/panel/components/Rewards.jsx @@ -45,7 +45,6 @@ class Rewards extends React.Component { // helper render functions this.renderRewardListComponent = this.renderRewardListComponent.bind(this); - this.handleFaqClick = this.handleFaqClick.bind(this); this.handlePortMessage = this.handlePortMessage.bind(this); @@ -141,7 +140,7 @@ class Rewards extends React.Component { /** * Handles clicking the learn more button */ - handleFaqClick() { + static handleFaqClick() { sendMessage('openNewTab', { url: 'https://www.ghostery.com/faqs/what-new-ghostery-features-can-we-expect-in-the-future/', become_active: true, @@ -202,7 +201,7 @@ class Rewards extends React.Component { * Helper render function for Reward Icon SVG * @return {JSX} JSX for the Rewards Icon SVG */ - renderRewardSvg() { + static renderRewardSvg() { return ( @@ -212,15 +211,15 @@ class Rewards extends React.Component { ); } - renderCLIQZtext() { + static renderCLIQZtext() { return (
- { this.renderRewardSvg() } + { Rewards.renderRewardSvg() }
{ t('panel_detail_rewards_cliqz_text') }

{ t('panel_detail_learn_more') }
@@ -228,19 +227,19 @@ class Rewards extends React.Component { ); } - renderRewardsTurnoffText() { + static renderRewardsTurnoffText() { return (
- { this.renderRewardSvg() } + { Rewards.renderRewardSvg() }
{ t('panel_detail_rewards_off') }
); } - renderRewardsNoneFoundText() { + static renderRewardsNoneFoundText() { return (
- { this.renderRewardSvg() } + { Rewards.renderRewardSvg() }
{ t('panel_detail_rewards_none_found') }
); @@ -251,9 +250,9 @@ class Rewards extends React.Component { * @return {JSX} JSX for the Rewards Items List */ renderRewardListComponent() { - if (IS_CLIQZ) { return this.renderCLIQZtext(); } + if (IS_CLIQZ) { return Rewards.renderCLIQZtext(); } const { enable_offers, is_expanded } = this.props; - if (!enable_offers) { return this.renderRewardsTurnoffText(); } + if (!enable_offers) { return Rewards.renderRewardsTurnoffText(); } const { shouldHideRewards, @@ -261,7 +260,7 @@ class Rewards extends React.Component { iframeHeight, rewardsCount, } = this.state; - if (shouldHideRewards) { return this.renderRewardsNoneFoundText(); } + if (shouldHideRewards) { return Rewards.renderRewardsNoneFoundText(); } const src = chrome.runtime.getURL('cliqz/offers-templates/control-center.html?cross-origin'); const text = t(`panel_rewards_view__reward${rewardsCount === 1 ? '' : 's'}`); diff --git a/app/panel/components/Settings/TrustAndRestrict.jsx b/app/panel/components/Settings/TrustAndRestrict.jsx index 5eb677a9d..3f5805918 100644 --- a/app/panel/components/Settings/TrustAndRestrict.jsx +++ b/app/panel/components/Settings/TrustAndRestrict.jsx @@ -120,7 +120,7 @@ class TrustAndRestrict extends React.Component { // Check for Validity if (pageHost.length >= 2083 - || !this.isValidUrlorWildcard(pageHost)) { + || !TrustAndRestrict.isValidUrlorWildcard(pageHost)) { this.showWarning(t('white_black_list_error_invalid_url')); return; } @@ -144,7 +144,7 @@ class TrustAndRestrict extends React.Component { } } - isValidUrlorWildcard(pageHost) { + static isValidUrlorWildcard(pageHost) { // Only allow valid host name characters, ':' for port numbers and '*' for wildcards const isSafePageHost = /^[a-zA-Z0-9-.:*]*$/; if (!isSafePageHost.test(pageHost)) { return false; } diff --git a/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx b/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx index 1904a38a6..299ebe5af 100644 --- a/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx +++ b/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx @@ -15,7 +15,6 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import { shallow } from 'enzyme'; import { when } from 'jest-when'; import TrustAndRestrict from '../TrustAndRestrict'; @@ -32,124 +31,114 @@ describe('app/panel/components/Settings/TrustAndRestrict', () => { describe('app/panel/components/Settings/', () => { test('isValidUrlorWildcard should return true with url entered', () => { - const wrapper = shallow(); let input = 'ghostery.com'; - - let fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + let fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - let returnValue = wrapper.instance().isValidUrlorWildcard(input); + let returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(true); input = 'localhost:3000'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(true); }); test('isValidUrlorWildcard should return true with wildcard URL entered', () => { - const wrapper = shallow(); - let input = 'developer.*.org'; - let fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + let fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - let returnValue = wrapper.instance().isValidUrlorWildcard(input); + let returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(true); input = '*.com'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(true); input = '*'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(true); input = 'developer.*'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(true); input = '****'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(true); }); test('isValidUrlorWildcard should return false with wildcard URL entered', () => { - const wrapper = shallow(); - let input = ''; - let fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + let fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - let returnValue = wrapper.instance().isValidUrlorWildcard(input); + let returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(false); input = '+$@@#$*'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(false); input = 'αράδειγμα.δοκιμ.*'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(false); input = 'SELECT * FROM USERS'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(false); }); test('isValidUrlorWildcard should return false with regex entered', () => { - const wrapper = shallow(); - let input = ')'; - let fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + let fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - let returnValue = wrapper.instance().isValidUrlorWildcard(input); + let returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(false); input = '++'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(false); input = '/foo(?)/'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(false); }); test('isValidUrlorWildcard should return false with unsafe test entered', () => { - const wrapper = shallow(); - let input = '/^(\w+\s?)*$/'; - let fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + let fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - let returnValue = wrapper.instance().isValidUrlorWildcard(input); + let returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(false); input = '/^([0-9]+)*$/'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(false); input = '(x\w{1,10})+y'; - fn = jest.spyOn(wrapper.instance(), 'isValidUrlorWildcard'); + fn = jest.spyOn(TrustAndRestrict, 'isValidUrlorWildcard'); when(fn).calledWith(input); - returnValue = wrapper.instance().isValidUrlorWildcard(input); + returnValue = TrustAndRestrict.isValidUrlorWildcard(input); expect(returnValue).toBe(false); }); }); diff --git a/app/scss/partials/_upgrade_banner.scss b/app/scss/partials/_upgrade_banner.scss index 13f405cd6..d942e8dae 100644 --- a/app/scss/partials/_upgrade_banner.scss +++ b/app/scss/partials/_upgrade_banner.scss @@ -19,7 +19,7 @@ } .UpgradeBanner--normal { - height: 50px; + height: 25px; .UpgradeBanner__text { font-size: 12px; @@ -33,7 +33,7 @@ } .UpgradeBanner--small { - height: 50px; + height: 20px; .UpgradeBanner__text { font-size: 10px; diff --git a/package.json b/package.json index ee161a7d3..ade3fd5ce 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "test": "cross-env BABEL_ENV=test jest", "test.watch": "cross-env BABEL_ENV=test jest --watch", "test.snapshot": "jest --updateSnapshot", - "lint": "eslint --ext .js,.jsx ./app ./src", - "lint.fix": "eslint --ext .js,.jsx ./app ./src --fix", + "lint": "eslint --ext .js,.jsx ./app ./src ./test ./tools", + "lint.fix": "eslint --ext .js,.jsx ./app ./src ./test ./tools --fix", "lint.raw": "eslint --ext .js,.jsx", "docs": "jsdoc -c jsdoc.json -d docs -r", "leet": "node ./tools/leet/leet-en.js", diff --git a/src/background.js b/src/background.js index 67172433d..5c9865c89 100644 --- a/src/background.js +++ b/src/background.js @@ -23,6 +23,7 @@ import moment from 'moment/min/moment-with-locales.min'; import cliqz from './classes/Cliqz'; // object class import Events from './classes/EventHandlers'; +import Policy from './classes/Policy'; // static classes import panelData from './classes/PanelData'; import bugDb from './classes/BugDb'; @@ -39,7 +40,7 @@ import globals from './classes/Globals'; import surrogatedb from './classes/SurrogateDb'; import tabInfo from './classes/TabInfo'; import metrics from './classes/Metrics'; -import rewards from './classes/Rewards'; +import Rewards from './classes/Rewards'; import account from './classes/Account'; import GhosteryModule from './classes/Module'; import promoModals from './classes/PromoModals'; @@ -491,14 +492,14 @@ function handleBlockedRedirect(name, message, tab_id, callback) { function handleRewards(name, message, callback) { switch (name) { case 'rewardSignal': // e.g. hub_open | hub_closed - rewards.sendSignal(message); + Rewards.sendSignal(message); break; case 'ping': metrics.ping(message); break; case 'setPanelData': if (Object.prototype.hasOwnProperty.call(message, 'enable_offers')) { - rewards.sendSignal(message.signal); + Rewards.sendSignal(message.signal); panelData.set({ enable_offers: message.enable_offers }); } return callback(); @@ -598,7 +599,7 @@ function handleGhosteryHub(name, message, callback) { } case 'SET_GHOSTERY_REWARDS': { const { enable_ghostery_rewards = true } = message; - rewards.sendSignal({ + Rewards.sendSignal({ actionId: `rewards_${enable_ghostery_rewards ? 'on' : 'off'}`, origin: 'ghostery-setup-flow', type: 'action-signal', @@ -1012,7 +1013,7 @@ function initializeDispatcher() { const { db } = bugDb; db.noneSelected = (num_selected === 0); // can't simply compare num_selected and size(db.apps) since apps get removed sometimes - db.allSelected = (!!num_selected && every(db.apps, (app, app_id) => Object.property.hasOwnProperty.call(appIds, app_id))); + db.allSelected = (!!num_selected && every(db.apps, (app, app_id) => Object.prototype.hasOwnProperty.call(appIds, app_id))); }); dispatcher.on('conf.save.site_whitelist', () => { // TODO debounce with below @@ -1203,7 +1204,7 @@ function initialiseWebRequestPipeline() { */ function isWhitelisted(state) { // state.ghosteryWhitelisted is sometimes undefined so force to bool - return Boolean(globals.SESSION.paused_blocking || events.policy.getSitePolicy(state.tabUrl, state.url) === 2 || state.ghosteryWhitelisted); + return Boolean(globals.SESSION.paused_blocking || Policy.getSitePolicy(state.tabUrl, state.url) === 2 || state.ghosteryWhitelisted); } /** * Set listener for 'enabled' event for Antitracking module which replaces @@ -1592,7 +1593,7 @@ function initializeGhosteryModules() { cliqz.events.subscribe('myoffrz:turnoff', () => { panelData.set({ enable_offers: false }); - rewards.sendSignal({ + Rewards.sendSignal({ actionId: 'rewards_off', type: 'action-signal', }); diff --git a/src/classes/BrowserButton.js b/src/classes/BrowserButton.js index 5037166d1..279cbfd09 100644 --- a/src/classes/BrowserButton.js +++ b/src/classes/BrowserButton.js @@ -31,7 +31,6 @@ class BrowserButton { alert: [255, 157, 0, 230], default: [51, 0, 51, 230] }; - this.policy = new Policy(); } /** @@ -140,7 +139,7 @@ class BrowserButton { return; } - const { appsCount, appsAlertCount } = this._getTrackerCount(tabId); + const { appsCount, appsAlertCount } = BrowserButton._getTrackerCount(tabId); const adBlockingCount = getCliqzData(tabId, tabHostUrl).trackerCount; const antiTrackingCount = getCliqzData(tabId, tabHostUrl, true).trackerCount; @@ -151,7 +150,7 @@ class BrowserButton { if (trackerCount === '') { this._setIcon(false, tabId, trackerCount, alert); } else { - this._setIcon(!globals.SESSION.paused_blocking && !this.policy.checkSiteWhitelist(tab.url), tabId, trackerCount, alert); + this._setIcon(!globals.SESSION.paused_blocking && !Policy.checkSiteWhitelist(tab.url), tabId, trackerCount, alert); } } @@ -161,7 +160,7 @@ class BrowserButton { * @param {string} tabUrl the Tab URL * @return {Object} the number of total trackers and alerted trackers in an Object */ - _getTrackerCount(tabId, tabUrl) { + static _getTrackerCount(tabId, tabUrl) { const apps = foundBugs.getAppsCountByIssues(tabId, tabUrl); return { appsCount: apps.all, diff --git a/src/classes/Click2PlayDb.js b/src/classes/Click2PlayDb.js index cdeefa4c7..09d3629d6 100644 --- a/src/classes/Click2PlayDb.js +++ b/src/classes/Click2PlayDb.js @@ -43,7 +43,7 @@ class Click2PlayDb extends Updatable { log('processing c2p...'); try { - db = this._buildDb(data.click2play, data.click2playVersion); + db = Click2PlayDb._buildDb(data.click2play, data.click2playVersion); } catch (e) { log('Click2PlayDb processList() error', e); return false; @@ -108,7 +108,7 @@ class Click2PlayDb extends Updatable { * @param {string} version database version * @return {Object} reconfigured database object */ - _buildDb(entries, version) { + static _buildDb(entries, version) { const apps = {}; let allow; diff --git a/src/classes/CompatibilityDb.js b/src/classes/CompatibilityDb.js index bb81c334a..163281bd8 100644 --- a/src/classes/CompatibilityDb.js +++ b/src/classes/CompatibilityDb.js @@ -36,7 +36,7 @@ class CompatibilityDb extends Updatable { log('processing comp...'); try { - db = this._buildDb(comp.compatibility, comp.compatibilityVersion); + db = CompatibilityDb._buildDb(comp.compatibility, comp.compatibilityVersion); } catch (e) { log('CompatibilityDb processList() error', e); return false; @@ -79,7 +79,7 @@ class CompatibilityDb extends Updatable { * @param {string} version database version * @return {Object} Refactored database */ - _buildDb(bugs, version) { + static _buildDb(bugs, version) { const map = {}; bugs.forEach((s) => { diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 23dcaa013..69c4e20e3 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -37,7 +37,7 @@ const IS_FIREFOX = (BROWSER_INFO.name === 'firefox'); class ConfData { constructor() { // language does not get persisted - this.language = this._getDefaultLanguage(); + this.language = ConfData._getDefaultLanguage(); this.SYNC_SET = new Set(globals.SYNC_ARRAY); } @@ -166,7 +166,7 @@ class ConfData { }); } - _getDefaultLanguage() { + static _getDefaultLanguage() { const SUPPORTED_LANGUAGES = { de: 'Deutsch', en: 'English', diff --git a/src/classes/EventHandlers.js b/src/classes/EventHandlers.js index b58f9f055..8bee374b0 100644 --- a/src/classes/EventHandlers.js +++ b/src/classes/EventHandlers.js @@ -372,7 +372,7 @@ class EventHandlers { /* ** SMART BLOCKING - Privacy ** */ // block HTTP request on HTTPS page - if (this.policySmartBlock.isInsecureRequest(tab_id, page_protocol, processed.scheme, processed.hostname)) { + if (PolicySmartBlock.isInsecureRequest(tab_id, page_protocol, processed.scheme, processed.hostname)) { return this._blockHelper(details, tab_id, null, null, request_id, from_redirect, true); } @@ -393,7 +393,7 @@ class EventHandlers { /* ** SMART BLOCKING - Breakage ** */ // allow first party trackers - if (this.policySmartBlock.isFirstPartyRequest(tab_id, page_domain, processed.generalDomain)) { + if (PolicySmartBlock.isFirstPartyRequest(tab_id, page_domain, processed.generalDomain)) { return { cancel: false }; } @@ -838,7 +838,7 @@ class EventHandlers { if (fromRedirect && globals.LET_REDIRECTS_THROUGH) { block = { block: false, reason: BLOCK_REASON_C2P_ALLOWED_THROUGH }; } else { - block = this.policy.shouldBlock(app_id, cat_id, tab_id, tab_host, page_url); + block = Policy.shouldBlock(app_id, cat_id, tab_id, tab_host, page_url); } return block; diff --git a/src/classes/ExtMessenger.js b/src/classes/ExtMessenger.js index 3dbf267a2..ecd068438 100644 --- a/src/classes/ExtMessenger.js +++ b/src/classes/ExtMessenger.js @@ -21,15 +21,15 @@ import { log } from '../utils/common'; * @memberOf BackgroundClasses */ export class ExtMessenger { - addListener(fn) { + static addListener(fn) { chrome.runtime.onMessageExternal.addListener(fn); } - removeListener(fn) { + static removeListener(fn) { chrome.runtime.onMessageExternal.removeListener(fn); } - sendMessage(extensionId, message) { + static sendMessage(extensionId, message) { chrome.runtime.sendMessage(extensionId, message, () => { if (chrome.runtime.lastError) { log('ExtMessenger sendMessage error:', chrome.runtime.lastError); @@ -44,18 +44,17 @@ export class ExtMessenger { */ export default class KordInjector { constructor() { - this.messenger = new ExtMessenger(); this.extensionId = 'cliqz@cliqz.com'; this.moduleWrappers = new Map(); this._messageHandler = this._messageHandler.bind(this); } init() { - this.messenger.addListener(this._messageHandler); + ExtMessenger.addListener(this._messageHandler); } unload() { - this.messenger.removeListener(this._messageHandler); + ExtMessenger.removeListener(this._messageHandler); } module(moduleName) { @@ -70,7 +69,7 @@ export default class KordInjector { return new Spanan((m) => { const message = m; message.moduleName = moduleName; - this.messenger.sendMessage(this.extensionId, message); + ExtMessenger.sendMessage(this.extensionId, message); }); } diff --git a/src/classes/Metrics.js b/src/classes/Metrics.js index 20e75aeb3..7843f6774 100644 --- a/src/classes/Metrics.js +++ b/src/classes/Metrics.js @@ -346,21 +346,21 @@ class Metrics { // Type of blocking selected during setup `&sb=${encodeURIComponent(conf.setup_block.toString())}` + // Recency, days since last active daily ping - `&rc=${encodeURIComponent(this._getRecencyActive(type, frequency).toString())}` + + `&rc=${encodeURIComponent(Metrics._getRecencyActive(type, frequency).toString())}` + // New parameters to Ghostery 8.3 // Subscription Type - `&st=${encodeURIComponent(this._getSubscriptionType().toString())}` + + `&st=${encodeURIComponent(Metrics._getSubscriptionType().toString())}` + // Whether the computer ever had a Paid Subscription `&ps=${encodeURIComponent(conf.paid_subscription ? '1' : '0')}` + // Active Velocity - `&va=${encodeURIComponent(this._getVelocityActive(type).toString())}` + + `&va=${encodeURIComponent(Metrics._getVelocityActive(type).toString())}` + // Engaged Recency - `&re=${encodeURIComponent(this._getRecencyEngaged(type, frequency).toString())}` + + `&re=${encodeURIComponent(Metrics._getRecencyEngaged(type, frequency).toString())}` + // Engaged Velocity - `&ve=${encodeURIComponent(this._getVelocityEngaged(type).toString())}` + + `&ve=${encodeURIComponent(Metrics._getVelocityEngaged(type).toString())}` + // Theme - `&th=${encodeURIComponent(this._getThemeValue().toString())}`; + `&th=${encodeURIComponent(Metrics._getThemeValue().toString())}`; if (CAMPAIGN_METRICS.includes(type)) { // only send campaign attribution when necessary @@ -439,7 +439,7 @@ class Metrics { * * @return {number} in days since the last daily active ping */ - _getRecencyActive(type, frequency) { + static _getRecencyActive(type, frequency) { if (conf.metrics.active_daily && (type === 'active' || type === 'engaged') && frequency === 'daily') { return Math.floor((Number(new Date().getTime()) - conf.metrics.active_daily) / 86400000); } @@ -453,7 +453,7 @@ class Metrics { * * @return {number} in days since the last daily engaged ping */ - _getRecencyEngaged(type, frequency) { + static _getRecencyEngaged(type, frequency) { if (conf.metrics.engaged_daily && (type === 'active' || type === 'engaged') && frequency === 'daily') { return Math.floor((Number(new Date().getTime()) - conf.metrics.engaged_daily) / 86400000); } @@ -465,7 +465,7 @@ class Metrics { * @private * @return {number} The Active Velocity */ - _getVelocityActive(type) { + static _getVelocityActive(type) { if (type !== 'active' && type !== 'engaged') { return -1; } @@ -479,7 +479,7 @@ class Metrics { * @private * @return {number} The Engaged Velocity */ - _getVelocityEngaged(type) { + static _getVelocityEngaged(type) { if (type !== 'active' && type !== 'engaged') { return -1; } @@ -492,7 +492,7 @@ class Metrics { * Get the Subscription Type * @return {string} Subscription Name */ - _getSubscriptionType() { + static _getSubscriptionType() { if (!conf.account) { return -1; } @@ -508,7 +508,7 @@ class Metrics { * @private * @return {number} value associated with the Current Theme */ - _getThemeValue() { + static _getThemeValue() { const { current_theme } = conf; switch (current_theme) { case 'midnight-theme': @@ -531,7 +531,7 @@ class Metrics { * @param {string} frequency one of 'all', 'daily', 'weekly' * @return {number} number in milliseconds over the frequency since the last ping */ - _timeToExpired(type, frequency) { + static _timeToExpired(type, frequency) { if (frequency === 'all') { return 0; } @@ -552,7 +552,7 @@ class Metrics { * @return {boolean} true/false */ _checkPing(type, frequency) { - const result = this._timeToExpired(type, frequency); + const result = Metrics._timeToExpired(type, frequency); if (result > 0) { return false; } @@ -629,7 +629,7 @@ class Metrics { } conf.metrics.active_daily_velocity = active_daily_velocity; - const daily = this._timeToExpired('active', 'daily'); + const daily = Metrics._timeToExpired('active', 'daily'); if (daily > 0) { setTimeout(() => { this._sendReq('active', ['daily']); @@ -644,7 +644,7 @@ class Metrics { }, FREQUENCIES.daily); } - const weekly = this._timeToExpired('active', 'weekly'); + const weekly = Metrics._timeToExpired('active', 'weekly'); if (weekly > 0) { setTimeout(() => { this._sendReq('active', ['weekly']); @@ -659,7 +659,7 @@ class Metrics { }, FREQUENCIES.weekly); } - const monthly = this._timeToExpired('active', 'monthly'); + const monthly = Metrics._timeToExpired('active', 'monthly'); if (monthly > 0) { if (monthly <= FREQUENCIES.biweekly) { setTimeout(() => { diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 4d3e07e38..09bcdee66 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -23,7 +23,7 @@ import globals from './Globals'; import metrics from './Metrics'; import Policy from './Policy'; import tabInfo from './TabInfo'; -import rewards from './Rewards'; +import Rewards from './Rewards'; import account from './Account'; import dispatcher from './Dispatcher'; import promoModals from './PromoModals'; @@ -33,7 +33,6 @@ import { objectEntries, log } from '../utils/common'; const SYNC_SET = new Set(globals.SYNC_ARRAY); const { IS_CLIQZ } = globals; -const policy = new Policy(); /** * PanelData coordinates the assembly and transmission of data to the extension panel @@ -118,16 +117,16 @@ class PanelData { break; case 'RewardsComponentDidMount': this._mountedComponents.rewards = true; - this._panelPort.onDisconnect.addListener(rewards.panelHubClosedListener); + this._panelPort.onDisconnect.addListener(Rewards.panelHubClosedListener); this._postRewardsData(); break; case 'RewardsComponentWillUnmount': this._mountedComponents.rewards = false; - this._panelPort.onDisconnect.removeListener(rewards.panelHubClosedListener); + this._panelPort.onDisconnect.removeListener(Rewards.panelHubClosedListener); break; case 'SettingsComponentDidMount': this._mountedComponents.settings = true; - this._postMessage('settings', this._getSettingsData()); + this._postMessage('settings', PanelData._getSettingsData()); break; case 'SettingsComponentWillUnmount': this._mountedComponents.settings = false; @@ -167,7 +166,7 @@ class PanelData { get(view, tab) { // Hub and Android panel if (view === 'settings') { - return this._getSettingsData(); + return PanelData._getSettingsData(); } // Android panel only @@ -262,7 +261,7 @@ class PanelData { * Helper that retrieves the current account information * @return {Object|null} the current account object or null */ - _getCurrentAccount() { + static _getCurrentAccount() { const currentAccount = conf.account; if (currentAccount && currentAccount.user) { currentAccount.user.subscriptionsPlus = account.hasScopesUnverified(['subscriptions:plus']); @@ -302,7 +301,7 @@ class PanelData { return { needsReload: needsReload || { changes: {} }, smartBlock, - account: this._getCurrentAccount(), + account: PanelData._getCurrentAccount(), }; } @@ -371,7 +370,7 @@ class PanelData { * Get rewards data for the Rewards View * @return {Object} Rewards view data */ - _getRewardsData() { + static _getRewardsData() { return { enable_offers: conf.enable_offers, }; @@ -382,7 +381,7 @@ class PanelData { * Called when and only when the Settings component is mounted * @return {Object} Settings View data */ - _getSettingsData() { + static _getSettingsData() { const { bugs_last_updated, language, new_app_ids, settings_last_exported, settings_last_imported @@ -391,13 +390,13 @@ class PanelData { return { bugs_last_updated, - categories: this._buildGlobalCategories(), + categories: PanelData._buildGlobalCategories(), language, // required for the setup page that does not have access to panelView data new_app_ids, offer_human_web: true, settings_last_exported, settings_last_imported, - ...this._getUserSettingsForSettingsView(conf), + ...PanelData._getUserSettingsForSettingsView(conf), }; } @@ -421,7 +420,7 @@ class PanelData { pageHost, pageUrl: url || '', siteNotScanned: !this._trackerList || false, - sitePolicy: policy.getSitePolicy(url) || false, + sitePolicy: Policy.getSitePolicy(url) || false, ...this._getDynamicSummaryData() }; } @@ -431,7 +430,7 @@ class PanelData { * Invoked if Blocking component is mounted when account.getUserSettings() resolves, max one time per panel open. * @param {Object} userSettings the settings retrieved by account.getUserSettings() in _initPort */ - _getUserSettingsForBlockingView(userSettings) { + static _getUserSettingsForBlockingView(userSettings) { const { expand_all_trackers, selected_app_ids, show_tracker_urls, site_specific_blocks, site_specific_unblocks, toggle_individual_trackers, @@ -452,7 +451,7 @@ class PanelData { * Invoked if Panel is still open account.getUserSettings() resolves, max one time per panel open. * @param {Object} userSettings the settings retrieved by account.getUserSettings() in _initPort */ - _getUserSettingsForPanelView(userSettings) { + static _getUserSettingsForPanelView(userSettings) { const { current_theme, enable_ad_block, enable_anti_tracking, enable_smart_block, enable_offers, is_expanded, is_expert, reload_banner_status, trackers_banner_status, @@ -468,7 +467,7 @@ class PanelData { is_expert, reload_banner_status, trackers_banner_status, - account: this._getCurrentAccount(), + account: PanelData._getCurrentAccount(), }; } @@ -477,7 +476,7 @@ class PanelData { * Invoked if Settings component is mounted when account.getUserSettings() resolves, max one time per panel open. * @param {Object} userSettings the settings retrieved by account.getUserSettings() in _initPort, or the conf object provided by getSettings */ - _getUserSettingsForSettingsView(userSettingsSource) { + static _getUserSettingsForSettingsView(userSettingsSource) { const { alert_bubble_pos, alert_bubble_timeout, block_by_default, cliqz_adb_mode, enable_autoupdate, enable_click2play, enable_click2play_social, enable_human_web, enable_offers, @@ -543,7 +542,7 @@ class PanelData { * @private */ _postRewardsData() { - this._postMessage('rewards', this._getRewardsData()); + this._postMessage('rewards', PanelData._getRewardsData()); } /** @@ -554,16 +553,16 @@ class PanelData { _postUserSettings(userSettings) { if (!this._panelPort || !this._activeTab) { return; } - this._postMessage('panel', this._getUserSettingsForPanelView(userSettings)); + this._postMessage('panel', PanelData._getUserSettingsForPanelView(userSettings)); const { blocking, settings } = this._mountedComponents; if (blocking) { - this._postMessage('blocking', this._getUserSettingsForBlockingView(userSettings)); + this._postMessage('blocking', PanelData._getUserSettingsForBlockingView(userSettings)); } if (settings) { - this._postMessage('settings', this._getUserSettingsForSettingsView(userSettings)); + this._postMessage('settings', PanelData._getUserSettingsForSettingsView(userSettings)); } } // [/DATA TRANSFER] @@ -601,13 +600,13 @@ class PanelData { setTimeout(() => { globals.SESSION.paused_blocking = false; - this._toggleBlockingHelper(); + PanelData._toggleBlockingHelper(); }, value); } else { globals.SESSION.paused_blocking = value; globals.SESSION.paused_blocking_timeout = 0; } - this._toggleBlockingHelper(); + PanelData._toggleBlockingHelper(); } } @@ -632,7 +631,7 @@ class PanelData { /** * Notifies interested parties when blocking is paused / unpaused */ - _toggleBlockingHelper() { + static _toggleBlockingHelper() { button.update(); flushChromeMemoryCache(); dispatcher.trigger('globals.save.paused_blocking'); @@ -660,11 +659,11 @@ class PanelData { if (Object.prototype.hasOwnProperty.call(categories, cat)) { categories[cat].num_total++; - if (this._addsUpToBlocked(trackerState)) { categories[cat].num_blocked++; } + if (PanelData._addsUpToBlocked(trackerState)) { categories[cat].num_blocked++; } } else { - categories[cat] = this._buildCategory(cat, trackerState); + categories[cat] = PanelData._buildCategory(cat, trackerState); } - categories[cat].trackers.push(this._buildTracker(tracker, trackerState, smartBlock)); + categories[cat].trackers.push(PanelData._buildTracker(tracker, trackerState, smartBlock)); }); const categoryArray = Object.values(categories); @@ -683,7 +682,7 @@ class PanelData { * @param {Object} trackerState object containing various block/allow states of a tracker * @return {boolean} is the tracker blocked in one of the possible ways? */ - _addsUpToBlocked({ + static _addsUpToBlocked({ ss_blocked, sb_blocked, blocked, ss_allowed, sb_allowed }) { return (ss_blocked || sb_blocked || (blocked && !ss_allowed && !sb_allowed)); @@ -695,7 +694,7 @@ class PanelData { * @param {Object} trackerState object containing various block/allow states of a tracker * @return {Object} an object with data for a new category */ - _buildCategory(category, trackerState) { + static _buildCategory(category, trackerState) { return { id: category, name: t(`category_${category}`), @@ -703,7 +702,7 @@ class PanelData { img_name: (category === 'advertising') ? 'adv' : // Because AdBlock blocks images with 'advertising' in the name. (category === 'social_media') ? 'smed' : category, // Because AdBlock blocks images with 'social' in the name. num_total: 1, - num_blocked: this._addsUpToBlocked(trackerState) ? 1 : 0, + num_blocked: PanelData._addsUpToBlocked(trackerState) ? 1 : 0, trackers: [] }; } @@ -717,7 +716,7 @@ class PanelData { * @param {Object} smartBlock smart blocking stats for the active tab * @return {Object} object of tracker data */ - _buildTracker(tracker, trackerState, smartBlock) { + static _buildTracker(tracker, trackerState, smartBlock) { const { cat, cliqzAdCount, @@ -783,7 +782,7 @@ class PanelData { * @private * @return {array} array of categories */ - _buildGlobalCategories() { + static _buildGlobalCategories() { const categories = bugDb.db.categories || []; const selectedApps = conf.selected_app_ids || {}; categories.forEach((categoryEl) => { diff --git a/src/classes/Policy.js b/src/classes/Policy.js index 294bfc1c5..50b9f23a8 100644 --- a/src/classes/Policy.js +++ b/src/classes/Policy.js @@ -45,11 +45,11 @@ class Policy { * @param {string} url site url * @return {boolean} */ - getSitePolicy(hostUrl, trackerUrl) { - if (this.blacklisted(hostUrl)) { + static getSitePolicy(hostUrl, trackerUrl) { + if (Policy.blacklisted(hostUrl)) { return globals.BLACKLISTED; } - if (this.checkSiteWhitelist(hostUrl) || this.checkCliqzModuleWhitelist(hostUrl, trackerUrl)) { + if (Policy.checkSiteWhitelist(hostUrl) || Policy.checkCliqzModuleWhitelist(hostUrl, trackerUrl)) { return globals.WHITELISTED; } return false; @@ -60,7 +60,7 @@ class Policy { * @param {string} url site url * @return {string|boolean} corresponding whitelist entry or false, if none */ - checkSiteWhitelist(url) { + static checkSiteWhitelist(url) { const hostUrl = processUrl(url).host; if (hostUrl) { const replacedUrl = hostUrl.replace(/^www\./, ''); @@ -73,7 +73,7 @@ class Policy { if (!sites[i].includes('*') && replacedUrl === sites[i]) { return sites[i]; } - if (this.matchesWildcard(replacedUrl, sites[i])) { + if (Policy.matchesWildcard(replacedUrl, sites[i])) { return sites[i]; } } @@ -87,7 +87,7 @@ class Policy { * @param {string} url site url * @return {string|boolean} corresponding whitelist entry or false, if none */ - checkCliqzModuleWhitelist(hostUrl, trackerUrl) { + static checkCliqzModuleWhitelist(hostUrl, trackerUrl) { let isWhitelisted = false; const processedHostUrl = processUrl(hostUrl).host; const processedTrackerUrl = processUrl(trackerUrl).host; @@ -111,7 +111,7 @@ class Policy { * @param {string} url site url * @return {string|boolean} corresponding blacklist entry or false, if none */ - blacklisted(url) { + static blacklisted(url) { const hostUrl = processUrl(url).host; if (hostUrl) { const replacedUrl = hostUrl.replace(/^www\./, ''); @@ -124,7 +124,7 @@ class Policy { if (!sites[i].includes('*') && replacedUrl === sites[i]) { return sites[i]; } - if (this.matchesWildcard(replacedUrl, sites[i])) { + if (Policy.matchesWildcard(replacedUrl, sites[i])) { return sites[i]; } } @@ -149,7 +149,7 @@ class Policy { * @param {string} tab_url tab url * @return {BlockWithReason} block result with reason */ - shouldBlock(app_id, cat_id, tab_id, tab_host, tab_url) { + static shouldBlock(app_id, cat_id, tab_id, tab_host, tab_url) { if (globals.SESSION.paused_blocking) { return { block: false, reason: BLOCK_REASON_BLOCK_PAUSED }; } @@ -160,13 +160,13 @@ class Policy { // The app_id is on the site-specific allow list for this tab_host if (conf.toggle_individual_trackers && Object.prototype.hasOwnProperty.call(conf.site_specific_unblocks, tab_host) && conf.site_specific_unblocks[tab_host].includes(+app_id)) { // Site blacklist overrides all block settings except C2P allow once - if (this.blacklisted(tab_url)) { + if (Policy.blacklisted(tab_url)) { return { block: !allowedOnce, reason: allowedOnce ? BLOCK_REASON_C2P_ALLOWED_ONCE : BLOCK_REASON_BLACKLISTED }; } return { block: false, reason: BLOCK_REASON_SS_UNBLOCKED }; } // Check for site white-listing - if (this.checkSiteWhitelist(tab_url)) { + if (Policy.checkSiteWhitelist(tab_url)) { return { block: false, reason: BLOCK_REASON_WHITELISTED }; } // The app_id is globally blocked @@ -177,7 +177,7 @@ class Policy { // Check to see if the app_id is on the site-specific block list for this tab_host if (conf.toggle_individual_trackers && Object.prototype.hasOwnProperty.call(conf.site_specific_blocks, tab_host) && conf.site_specific_blocks[tab_host].includes(+app_id)) { // Site white-listing overrides blocking settings - if (this.checkSiteWhitelist(tab_url)) { + if (Policy.checkSiteWhitelist(tab_url)) { return { block: false, reason: BLOCK_REASON_WHITELISTED }; } return { block: !allowedOnce, reason: allowedOnce ? BLOCK_REASON_C2P_ALLOWED_ONCE : BLOCK_REASON_SS_BLOCKED }; @@ -185,13 +185,13 @@ class Policy { // Check to see if the app_id is on the site-specific allow list for this tab_host if (conf.toggle_individual_trackers && Object.prototype.hasOwnProperty.call(conf.site_specific_unblocks, tab_host) && conf.site_specific_unblocks[tab_host].includes(+app_id)) { // Site blacklist overrides all block settings except C2P allow once - if (this.blacklisted(tab_url)) { + if (Policy.blacklisted(tab_url)) { return { block: !allowedOnce, reason: allowedOnce ? BLOCK_REASON_C2P_ALLOWED_ONCE : BLOCK_REASON_BLACKLISTED }; } return { block: false, reason: BLOCK_REASON_SS_UNBLOCKED }; } // Check for site black-listing - if (this.blacklisted(tab_url)) { + if (Policy.blacklisted(tab_url)) { return { block: !allowedOnce, reason: allowedOnce ? BLOCK_REASON_C2P_ALLOWED_ONCE : BLOCK_REASON_BLACKLISTED }; } // The app_id is globally unblocked @@ -204,7 +204,7 @@ class Policy { * @param {string} pattern regex pattern * @return {boolean} */ - matchesWildcard(url, pattern) { + static matchesWildcard(url, pattern) { if (pattern && pattern.includes('*')) { const wildcardPattern = pattern.replace(/\*/g, '.*'); try { diff --git a/src/classes/PolicySmartBlock.js b/src/classes/PolicySmartBlock.js index 65a95020f..df0a47bbe 100644 --- a/src/classes/PolicySmartBlock.js +++ b/src/classes/PolicySmartBlock.js @@ -51,17 +51,17 @@ class PolicySmartBlock { * applicable to this url, or none are met. */ shouldUnblock(appId, catId, tabId, pageURL, requestType) { - if (!this.shouldCheck(tabId, appId)) { return false; } + if (!PolicySmartBlock.shouldCheck(tabId, appId)) { return false; } let reason; - if (this._appHasKnownIssue(tabId, appId, pageURL)) { + if (PolicySmartBlock._appHasKnownIssue(tabId, appId, pageURL)) { reason = 'hasIssue'; // allow if tracker is in compatibility list } else if (this._allowedCategories(tabId, appId, catId)) { reason = 'allowedCategory'; // allow if tracker is in breaking category } else if (this._allowedTypes(tabId, appId, requestType)) { reason = 'allowedType'; // allow if tracker is in breaking type - } else if (this._pageWasReloaded(tabId, appId)) { + } else if (PolicySmartBlock._pageWasReloaded(tabId, appId)) { reason = 'pageReloaded'; // allow if page has been reloaded recently } @@ -85,21 +85,21 @@ class PolicySmartBlock { * applicable to this url, or none are met. */ shouldBlock(appId, catId, tabId, pageURL, requestType, requestTimestamp) { - if (!this.shouldCheck(tabId, appId)) { return false; } + if (!PolicySmartBlock.shouldCheck(tabId, appId)) { return false; } let reason; // Block all trackers that load after 5 seconds from when page load started - if (this._requestWasSlow(tabId, appId, requestTimestamp)) { + if (PolicySmartBlock._requestWasSlow(tabId, appId, requestTimestamp)) { reason = 'slow'; - if (this._appHasKnownIssue(tabId, appId, pageURL)) { + if (PolicySmartBlock._appHasKnownIssue(tabId, appId, pageURL)) { reason = 'hasIssue'; // allow if tracker is in compatibility list } else if (this._allowedCategories(tabId, appId, catId)) { reason = 'allowedCategory'; // allow if tracker is in breaking category } else if (this._allowedTypes(tabId, appId, requestType)) { reason = 'allowedType'; // allow if tracker is in breaking type - } else if (this._pageWasReloaded(tabId, appId)) { + } else if (PolicySmartBlock._pageWasReloaded(tabId, appId)) { reason = 'pageReloaded'; // allow if page has been reloaded recently } } @@ -127,14 +127,14 @@ class PolicySmartBlock { * @param {string | boolean} appId tracker id * @return {boolean} */ - shouldCheck(tabId, appId = false) { + static shouldCheck(tabId, appId = false) { const tabUrl = tabInfo.getTabInfo(tabId, 'url'); const tabHost = tabInfo.getTabInfo(tabId, 'host'); return ( conf.enable_smart_block && !globals.SESSION.paused_blocking && - !this.policy.getSitePolicy(tabUrl) && + !Policy.getSitePolicy(tabUrl) && ((appId && (!Object.prototype.hasOwnProperty.call(conf.site_specific_unblocks, tabHost) || !conf.site_specific_unblocks[tabHost].includes(+appId))) || appId === false) && ((appId && (!Object.prototype.hasOwnProperty.call(conf.site_specific_blocks, tabHost) || !conf.site_specific_blocks[tabHost].includes(+appId))) || appId === false) && (c2pDb.db.apps && !Object.prototype.hasOwnProperty.call(c2pDb.db.apps, appId)) @@ -148,8 +148,8 @@ class PolicySmartBlock { * @param {string} requestDomain domain of the request * @return {boolean} */ - isFirstPartyRequest(tabId, pageDomain = '', requestDomain = '') { - if (!this.shouldCheck(tabId)) { return false; } + static isFirstPartyRequest(tabId, pageDomain = '', requestDomain = '') { + if (!PolicySmartBlock.shouldCheck(tabId)) { return false; } return pageDomain === requestDomain; } @@ -159,7 +159,7 @@ class PolicySmartBlock { * @param {number} tabId tab id * @return {boolean} */ - _pageWasReloaded(tabId) { + static _pageWasReloaded(tabId) { return tabInfo.getTabInfo(tabId, 'reloaded') || false; } @@ -170,7 +170,7 @@ class PolicySmartBlock { * @param {string} pageURL tab url * @return {boolean} */ - _appHasKnownIssue(tabId, appId, pageURL) { + static _appHasKnownIssue(tabId, appId, pageURL) { return compDb.hasIssue(appId, pageURL); } @@ -182,8 +182,8 @@ class PolicySmartBlock { * @param {string} requestHost host of the request url * @return {boolean} */ - isInsecureRequest(tabId, pageProtocol, requestProtocol, requestHost) { - if (!this.shouldCheck(tabId)) { return false; } + static isInsecureRequest(tabId, pageProtocol, requestProtocol, requestHost) { + if (!PolicySmartBlock.shouldCheck(tabId)) { return false; } // don't block mixed content from localhost if (requestHost === 'localhost' || requestHost === '127.0.0.1' || requestHost === '[::1]') { @@ -223,8 +223,8 @@ class PolicySmartBlock { * @param {string} tabId tab id * @return {boolean} */ - checkReloadThreshold(tabId) { - if (!this.shouldCheck(tabId)) { return false; } + static checkReloadThreshold(tabId) { + if (!PolicySmartBlock.shouldCheck(tabId)) { return false; } // Note that this threshold is different from the broken page ping threshold in Metrics, which is 60 seconds // see GH-1797 for more details @@ -243,7 +243,7 @@ class PolicySmartBlock { * @param {number} requestTimestamp timestamp of the request * @return {boolean} */ - _requestWasSlow(tabId, appId, requestTimestamp) { + static _requestWasSlow(tabId, appId, requestTimestamp) { const THRESHHOLD = 5000; // 5 seconds const pageTimestamp = tabInfo.getTabInfo(tabId, 'timestamp'); // TODO: account for lazy-load or widgets triggered by user interaction beyond 5sec diff --git a/src/classes/PurpleBox.js b/src/classes/PurpleBox.js index 8f3cf7bbb..baa62631f 100644 --- a/src/classes/PurpleBox.js +++ b/src/classes/PurpleBox.js @@ -30,7 +30,6 @@ const t = chrome.i18n.getMessage; */ class PurpleBox { constructor() { - this.policy = new Policy(); this.channelsSupported = (typeof chrome.runtime.onConnect === 'object'); this.ports = new Map(); } @@ -48,7 +47,7 @@ class PurpleBox { // Skip in the event of pause, trust, prefetching, newtab page, or Firefox about:pages if (!conf.show_alert || globals.SESSION.paused_blocking || - (conf.hide_alert_trusted && !!this.policy.checkSiteWhitelist(tab.url)) || + (conf.hide_alert_trusted && !!Policy.checkSiteWhitelist(tab.url)) || !tab || tab.purplebox || tab.path.includes('_/chrome/newtab') || tab.protocol === 'about' || globals.EXCLUDES.includes(tab.host)) { return Promise.resolve(false); } diff --git a/src/classes/Rewards.js b/src/classes/Rewards.js index b38c06cbb..e8182648a 100644 --- a/src/classes/Rewards.js +++ b/src/classes/Rewards.js @@ -20,11 +20,7 @@ import { log } from '../utils/common'; * @memberOf BackgroundClasses */ class Rewards { - constructor() { - this.panelHubClosedListener = this.panelHubClosedListener.bind(this); - } - - sendSignal(message) { + static sendSignal(message) { if (!conf.enable_offers) { return; } @@ -45,8 +41,8 @@ class Rewards { cliqz.modules['offers-v2'].background.actions.processRealEstateMessage(signal); } - panelHubClosedListener() { - this.sendSignal({ + static panelHubClosedListener() { + Rewards.sendSignal({ offerId: null, actionId: 'hub_closed', origin: 'rewards-hub', @@ -55,4 +51,4 @@ class Rewards { } } -export default new Rewards(); +export default Rewards; diff --git a/src/classes/SurrogateDb.js b/src/classes/SurrogateDb.js index 535a01b61..f5856f102 100644 --- a/src/classes/SurrogateDb.js +++ b/src/classes/SurrogateDb.js @@ -47,7 +47,7 @@ class SurrogateDb extends Updatable { * @override * */ - update() {} + static update() {} /** * Process surrogates from fetched json diff --git a/src/classes/TabInfo.js b/src/classes/TabInfo.js index 4375f663c..647ccfd62 100644 --- a/src/classes/TabInfo.js +++ b/src/classes/TabInfo.js @@ -37,8 +37,6 @@ import { processUrl } from '../utils/utils'; */ class TabInfo { constructor() { - this.policySmartBlock = new PolicySmartBlock(); - // @private this._tabInfo = {}; this._tabInfoPersist = {}; @@ -60,8 +58,8 @@ class TabInfo { timestamp: Date.now(), // assign only when smartBlock is enabled so avoid false positives // when enabling smartBlock is enabled for the first time - firstLoadTimestamp: this.policySmartBlock.shouldCheck(tab_id) && (numOfReloads === 0 ? Date.now() : (this.getTabInfoPersist(tab_id, 'firstLoadTimestamp') || 0)) || 0, - reloaded: this.policySmartBlock.checkReloadThreshold(tab_id), + firstLoadTimestamp: PolicySmartBlock.shouldCheck(tab_id) && (numOfReloads === 0 ? Date.now() : (this.getTabInfoPersist(tab_id, 'firstLoadTimestamp') || 0)) || 0, + reloaded: PolicySmartBlock.checkReloadThreshold(tab_id), numOfReloads, smartBlock: { blocked: {}, diff --git a/src/utils/api.js b/src/utils/api.js index f2e3865f5..14de96365 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -61,7 +61,7 @@ class Api { })); } - _processResponse(res) { + static _processResponse(res) { return new Promise((resolve, reject) => { const { status } = res; if (status === 204) { @@ -95,7 +95,7 @@ class Api { _sendAuthenticatedRequest(method, path, body) { return new Promise((resolve, reject) => { this._sendReq(method, path, body) - .then(this._processResponse) + .then(Api._processResponse) .then(dataFirstTry => resolve(dataFirstTry)) .catch((data) => { let shouldRefresh = false; @@ -122,7 +122,7 @@ class Api { )).catch(err => reject(err)); } this._sendReq(method, path, body) - .then(this._processResponse) + .then(Api._processResponse) .then(dataSecondTry => resolve(dataSecondTry)) .catch((data3) => { this._errorHandler(data3.errors) diff --git a/src/utils/click2play.js b/src/utils/click2play.js index 6dc0f0327..9d093f24a 100644 --- a/src/utils/click2play.js +++ b/src/utils/click2play.js @@ -25,8 +25,6 @@ import { sendMessage, processUrl, injectScript } from './utils'; import c2p_tpl from '../../app/templates/click2play.html'; import c2p_images from '../../app/data-images/click2play'; -const policy = new Policy(); - /** * Builds Click2Play templates for a given tab_id. * @@ -57,7 +55,7 @@ export function buildC2P(details, app_id) { const app_name = bugDb.db.apps[app_id].name; const c2pHtml = []; const tab_host = tabInfo.getTabInfo(tab_id, 'host'); - const blacklisted = !!policy.blacklisted(tab_host); + const blacklisted = !!Policy.blacklisted(tab_host); // Generate the templates for each c2p definition (could be multiple for an app ID) c2pApp.forEach((c2pAppDef) => { @@ -122,7 +120,7 @@ export function buildRedirectC2P(requestId, redirectUrls, app_id) { globals.BLOCKED_REDIRECT_DATA = {}; globals.BLOCKED_REDIRECT_DATA.app_id = app_id; globals.BLOCKED_REDIRECT_DATA.url = redirectUrls.redirectUrl; - globals.BLOCKED_REDIRECT_DATA.blacklisted = !!policy.blacklisted(host_url); + globals.BLOCKED_REDIRECT_DATA.blacklisted = !!Policy.blacklisted(host_url); globals.BLOCKED_REDIRECT_DATA.translations = { blocked_redirect_page_title: t('blocked_redirect_page_title'), diff --git a/test/src/Policy.test.js b/test/src/Policy.test.js index fc1619bb4..d08dc5b8b 100644 --- a/test/src/Policy.test.js +++ b/test/src/Policy.test.js @@ -77,22 +77,22 @@ describe('src/classes/Policy.js', () => { globals.SESSION.paused_blocking = false; }); test('a blocked tracker is unblocked with reason BLOCK_REASON_BLOCK_PAUSED', () => { - const { block, reason } = policy.shouldBlock(41, 'advertising', 1, 'www.cnn.com', 'https://www.cnn.com/'); + const { block, reason } = Policy.shouldBlock(41, 'advertising', 1, 'www.cnn.com', 'https://www.cnn.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_BLOCK_PAUSED); }); test('an unblocked tracker remains unblocked with reason BLOCK_REASON_BLOCK_PAUSED', () => { - const { block, reason } = policy.shouldBlock(50, 'essential', 1, 'www.cnn.com', 'https://www.cnn.com/'); + const { block, reason } = Policy.shouldBlock(50, 'essential', 1, 'www.cnn.com', 'https://www.cnn.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_BLOCK_PAUSED); }); test('a tracker on a white-listed site is unblocked with reason BLOCK_REASON_BLOCK_PAUSED', () => { - const { block, reason } = policy.shouldBlock(41, 'advertising', 1, 'www.ghostery.com', 'https://www.ghostery.com/'); + const { block, reason } = Policy.shouldBlock(41, 'advertising', 1, 'www.ghostery.com', 'https://www.ghostery.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_BLOCK_PAUSED); }); test('a tracker on a black-listed site is unblocked with reason BLOCK_REASON_BLOCK_PAUSED', () => { - const { block, reason } = policy.shouldBlock(50, 'essential', 1, 'www.tmz.com', 'https://www.tmz.com/'); + const { block, reason } = Policy.shouldBlock(50, 'essential', 1, 'www.tmz.com', 'https://www.tmz.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_BLOCK_PAUSED); }); @@ -106,32 +106,32 @@ describe('src/classes/Policy.js', () => { c2pDb.allowedOnce.mockReturnValue(false); }); test('a blocked tracker on the site-specific allow list on a black-listed site is unblocked with reason BLOCK_REASON_C2P_ALLOWED_ONCE', () => { - const { block, reason } = policy.shouldBlock(41, 'advertising', 1, 'www.tmz.com', 'https://www.tmz.com/'); + const { block, reason } = Policy.shouldBlock(41, 'advertising', 1, 'www.tmz.com', 'https://www.tmz.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_C2P_ALLOWED_ONCE); }); test('a blocked tracker is unblocked with reason BLOCK_REASON_C2P_ALLOWED_ONCE', () => { - const { block, reason } = policy.shouldBlock(41, 'advertising', 1, 'www.cnn.com', 'https://www.cnn.com/'); + const { block, reason } = Policy.shouldBlock(41, 'advertising', 1, 'www.cnn.com', 'https://www.cnn.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_C2P_ALLOWED_ONCE); }); test('an unblocked tracker on the site-specific block list remains unblocked with reason BLOCK_REASON_C2P_ALLOWED_ONCE', () => { - const { block, reason } = policy.shouldBlock(50, 'essential', 1, 'www.espn.com', 'https://www.espn.com/'); + const { block, reason } = Policy.shouldBlock(50, 'essential', 1, 'www.espn.com', 'https://www.espn.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_C2P_ALLOWED_ONCE); }); test('an unblocked tracker on the site-specific allow list on a black-listed site remains unblocked with reason BLOCK_REASON_C2P_ALLOWED_ONCE', () => { - const { block, reason } = policy.shouldBlock(50, 'essential', 1, 'www.tmz.com', 'https://www.tmz.com/'); + const { block, reason } = Policy.shouldBlock(50, 'essential', 1, 'www.tmz.com', 'https://www.tmz.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_C2P_ALLOWED_ONCE); }); test('an unblocked tracker on a black-listed site remains unblocked with reason BLOCK_REASON_C2P_ALLOWED_ONCE', () => { - const { block, reason } = policy.shouldBlock(55, 'essential', 1, 'www.tmz.com', 'https://www.tmz.com/'); + const { block, reason } = Policy.shouldBlock(55, 'essential', 1, 'www.tmz.com', 'https://www.tmz.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_C2P_ALLOWED_ONCE); }); test('an unblocked tracker remains unblocked with reason BLOCK_REASON_C2P_ALLOWED_ONCE', () => { - const { block, reason } = policy.shouldBlock(55, 'essential', 1, 'www.cnn.com', 'https://www.cnn.com/'); + const { block, reason } = Policy.shouldBlock(55, 'essential', 1, 'www.cnn.com', 'https://www.cnn.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_C2P_ALLOWED_ONCE); }); @@ -139,22 +139,22 @@ describe('src/classes/Policy.js', () => { describe('with a globally blocked tracker', () => { test('a tracker on the site-specific allow list is unblocked with reason BLOCK_REASON_SS_UNBLOCKED', () => { - const { block, reason } = policy.shouldBlock(15, 'site_analytics', 1, 'www.cnn.com', 'https://www.cnn.com/'); + const { block, reason } = Policy.shouldBlock(15, 'site_analytics', 1, 'www.cnn.com', 'https://www.cnn.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_SS_UNBLOCKED); }); test('a tracker on the site-specific allow list on a black-listed site remains blocked with reason BLOCK_REASON_BLACKLISTED', () => { - const { block, reason } = policy.shouldBlock(41, 'advertising', 1, 'www.tmz.com', 'https://www.tmz.com/'); + const { block, reason } = Policy.shouldBlock(41, 'advertising', 1, 'www.tmz.com', 'https://www.tmz.com/'); expect(block).toBeTruthy(); expect(reason).toBe(BLOCK_REASON_BLACKLISTED); }); test('a tracker on a white-listed site is unblocked with reason BLOCK_REASON_WHITELISTED', () => { - const { block, reason } = policy.shouldBlock(41, 'advertising', 1, 'www.ghostery.com', 'https://www.ghostery.com/'); + const { block, reason } = Policy.shouldBlock(41, 'advertising', 1, 'www.ghostery.com', 'https://www.ghostery.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_WHITELISTED); }); test('a tracker is blocked with reason BLOCK_REASON_GLOBAL_BLOCKED', () => { - const { block, reason } = policy.shouldBlock(41, 'advertising', 1, 'www.cnn.com', 'https://www.cnn.com/'); + const { block, reason } = Policy.shouldBlock(41, 'advertising', 1, 'www.cnn.com', 'https://www.cnn.com/'); expect(block).toBeTruthy(); expect(reason).toBe(BLOCK_REASON_GLOBAL_BLOCKED); }); @@ -162,32 +162,32 @@ describe('src/classes/Policy.js', () => { describe('with a globally unblocked tracker', () => { test('a tracker on the site-specific block list is blocked with reason BLOCK_REASON_SS_BLOCKED', () => { - const { block, reason } = policy.shouldBlock(13, 'site_analytics', 1, 'www.cnn.com', 'https://www.cnn.com/'); + const { block, reason } = Policy.shouldBlock(13, 'site_analytics', 1, 'www.cnn.com', 'https://www.cnn.com/'); expect(block).toBeTruthy(); expect(reason).toBe(BLOCK_REASON_SS_BLOCKED); }); test('a tracker on the site-specific block list on a white-listed site is unblocked with reason BLOCK_REASON_WHITELISTED', () => { - const { block, reason } = policy.shouldBlock(15, 'site_analytics', 1, 'www.ghostery.com', 'https://www.ghostery.com/'); + const { block, reason } = Policy.shouldBlock(15, 'site_analytics', 1, 'www.ghostery.com', 'https://www.ghostery.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_WHITELISTED); }); test('a tracker on the site-specific allow list is unblocked with reason BLOCK_REASON_SS_UNBLOCKED', () => { - const { block, reason } = policy.shouldBlock(50, 'essential', 1, 'www.cnn.com', 'https://www.cnn.com/'); + const { block, reason } = Policy.shouldBlock(50, 'essential', 1, 'www.cnn.com', 'https://www.cnn.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_SS_UNBLOCKED); }); test('a tracker on the site-specific allow list on a black-listed site is blocked with reason BLOCK_REASON_BLACKLISTED', () => { - const { block, reason } = policy.shouldBlock(50, 'essential', 1, 'www.tmz.com', 'https://www.tmz.com/'); + const { block, reason } = Policy.shouldBlock(50, 'essential', 1, 'www.tmz.com', 'https://www.tmz.com/'); expect(block).toBeTruthy(); expect(reason).toBe(BLOCK_REASON_BLACKLISTED); }); test('a tracker on a black-listed site is blocked with reason BLOCK_REASON_BLACKLISTED', () => { - const { block, reason } = policy.shouldBlock(55, 'essential', 1, 'www.tmz.com', 'https://www.tmz.com/'); + const { block, reason } = Policy.shouldBlock(55, 'essential', 1, 'www.tmz.com', 'https://www.tmz.com/'); expect(block).toBeTruthy(); expect(reason).toBe(BLOCK_REASON_BLACKLISTED); }); test('a tracker is unblocked with reason BLOCK_REASON_GLOBAL_UNBLOCKED', () => { - const { block, reason } = policy.shouldBlock(55, 'essential', 1, 'www.cnn.com', 'https://www.cnn.com/'); + const { block, reason } = Policy.shouldBlock(55, 'essential', 1, 'www.cnn.com', 'https://www.cnn.com/'); expect(block).toBeFalsy(); expect(reason).toBe(BLOCK_REASON_GLOBAL_UNBLOCKED); }); @@ -198,63 +198,63 @@ describe('src/classes/Policy.js', () => { test('matchesWildcard should return true with wildcard entered ', () => { let url = 'developer.mozilla.org'; let input = 'developer.*.org'; - expect(policy.matchesWildcard(url, input)).toBeTruthy(); + expect(Policy.matchesWildcard(url, input)).toBeTruthy(); url = 'ghostery.com'; input = '*.com'; - expect(policy.matchesWildcard(url, input)).toBeTruthy(); + expect(Policy.matchesWildcard(url, input)).toBeTruthy(); url = 'ghostery.com' input = '*'; - expect(policy.matchesWildcard(url, input)).toBeTruthy(); + expect(Policy.matchesWildcard(url, input)).toBeTruthy(); url = 'developer.mozilla.org'; input = 'developer.*'; - expect(policy.matchesWildcard(url , input)).toBeTruthy(); + expect(Policy.matchesWildcard(url , input)).toBeTruthy(); url = 'developer.mozilla.org'; input = '****'; - expect(policy.matchesWildcard(url, input)).toBeTruthy(); + expect(Policy.matchesWildcard(url, input)).toBeTruthy(); }); test('matchesWildcard should return false with wildcard entered ', () => { let url = 'developer.mozilla.org'; let input = ''; - expect(policy.matchesWildcard(url, input)).toBeFalsy(); + expect(Policy.matchesWildcard(url, input)).toBeFalsy(); url = 'ghostery.com'; input = '+$@@#$*'; - expect(policy.matchesWildcard(url, input)).toBeFalsy(); + expect(Policy.matchesWildcard(url, input)).toBeFalsy(); url = 'ghostery.com' input = 'αράδειγμα.δοκιμ.*'; - expect(policy.matchesWildcard(url, input)).toBeFalsy(); + expect(Policy.matchesWildcard(url, input)).toBeFalsy(); url = 'SELECT * FROM USERS'; input = 'developer.*'; - expect(policy.matchesWildcard(url , input)).toBeFalsy(); + expect(Policy.matchesWildcard(url , input)).toBeFalsy(); }); test('matchesWildcard should return false with regex entered', () => { let url = 'foo.com'; let input = '/foo)]/'; - expect(policy.matchesWildcard(url, input)).toBeFalsy(); + expect(Policy.matchesWildcard(url, input)).toBeFalsy(); url = 'foo.com'; input = 'test\\'; - expect(policy.matchesWildcard(url, input)).toBeFalsy(); + expect(Policy.matchesWildcard(url, input)).toBeFalsy(); url = 'foo.com'; input = '/(?<=x*)foo/'; - expect(policy.matchesWildcard(url, input)).toBeFalsy(); + expect(Policy.matchesWildcard(url, input)).toBeFalsy(); url = 'foo.com'; input = '/foo(?)/'; - expect(policy.matchesWildcard(url, input)).toBeFalsy(); + expect(Policy.matchesWildcard(url, input)).toBeFalsy(); url = 'foo.com'; input = ''; - expect(policy.matchesWildcard(url, input)).toBeFalsy(); + expect(Policy.matchesWildcard(url, input)).toBeFalsy(); }); }) }); diff --git a/test/src/PolicySmartBlock.test.js b/test/src/PolicySmartBlock.test.js index 7534e9f6b..342f7acea 100644 --- a/test/src/PolicySmartBlock.test.js +++ b/test/src/PolicySmartBlock.test.js @@ -45,7 +45,7 @@ describe('src/classes/PolicySmartBlock.js', () => { describe('PolicySmartBlock isFirstPartyRequest tests', () => { beforeAll(() => { - policySmartBlock.shouldCheck = jest.fn(() => true); + PolicySmartBlock.shouldCheck = jest.fn(() => true); }); afterAll(() => { @@ -53,18 +53,18 @@ describe('src/classes/PolicySmartBlock.js', () => { }); test('PolicySmartBlock isFirstPartyRequest truthy assertion', () => { - expect(policySmartBlock.isFirstPartyRequest('tabId', 'example.com', 'example.com')).toBeTruthy(); + expect(PolicySmartBlock.isFirstPartyRequest('tabId', 'example.com', 'example.com')).toBeTruthy(); // isFirstPartyRequest() expects pre-parsed domains, so we should parse the test urls const parsedPage = processUrl('https://checkout.ghostery.com/insights'); const parsedRequest = processUrl('https://analytics.ghostery.com/piwik.js'); - expect(policySmartBlock.isFirstPartyRequest('tabId', parsedPage.generalDomain, parsedRequest.generalDomain)).toBeTruthy(); + expect(PolicySmartBlock.isFirstPartyRequest('tabId', parsedPage.generalDomain, parsedRequest.generalDomain)).toBeTruthy(); }); test('PolicySmartBlock isFirstPartyRequest falsy assertion', () => { - expect(policySmartBlock.isFirstPartyRequest('tabId', 'www.example.com', 'example.com')).toBeFalsy(); - expect(policySmartBlock.isFirstPartyRequest('tabId', 'sub.example.com', 'example.com')).toBeFalsy(); - expect(policySmartBlock.isFirstPartyRequest('tabId', 'example.com', 'test.com')).toBeFalsy(); - expect(policySmartBlock.isFirstPartyRequest('tabId', 'www.example.com', 'www.test.com')).toBeFalsy(); + expect(PolicySmartBlock.isFirstPartyRequest('tabId', 'www.example.com', 'example.com')).toBeFalsy(); + expect(PolicySmartBlock.isFirstPartyRequest('tabId', 'sub.example.com', 'example.com')).toBeFalsy(); + expect(PolicySmartBlock.isFirstPartyRequest('tabId', 'example.com', 'test.com')).toBeFalsy(); + expect(PolicySmartBlock.isFirstPartyRequest('tabId', 'www.example.com', 'www.test.com')).toBeFalsy(); }); }); }); diff --git a/tools/i18n-checker.js b/tools/i18n-checker.js index 0f5d96f36..9d7eb181f 100644 --- a/tools/i18n-checker.js +++ b/tools/i18n-checker.js @@ -138,7 +138,7 @@ function findDuplicates(paths) { duplicates[locale] = []; oboe(fs.createReadStream(path)).node('{message}', (val, keys) => { const key = keys[0]; - if (foundKeys.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(foundKeys, key)) { hasDuplicates = true; duplicates[locale].push(key); return; @@ -179,7 +179,7 @@ function findMissingKeys(paths) { const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; missingKeys[locale] = []; Object.keys(defaultLocaleJson).forEach((key) => { - if (!localeJson.hasOwnProperty(key)) { + if (!Object.prototype.hasOwnProperty.call(localeJson, key)) { hasMissingKeys = true; missingKeys[locale].push(key); } @@ -214,7 +214,7 @@ function findExtraKeys(paths) { const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; extraKeys[locale] = []; Object.keys(localeJson).forEach((key) => { - if (!defaultLocaleJson.hasOwnProperty(key)) { + if (!Object.prototype.hasOwnProperty.call(defaultLocaleJson, key)) { hasExtraKeys = true; extraKeys[locale].push(key); } @@ -247,7 +247,7 @@ function findMalformedKeys(paths) { const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; malformedKeys[locale] = []; Object.keys(localeJson).forEach((key) => { - if (!localeJson[key].hasOwnProperty('message')) { + if (!Object.prototype.hasOwnProperty.call(localeJson[key], 'message')) { hasMalformedKeys = true; malformedKeys[locale].push(key); } @@ -282,8 +282,8 @@ function findMissingPlaceholders(paths) { const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; missingPlaceholders[locale] = []; Object.keys(defaultLocaleJson).forEach((key) => { - if (defaultLocaleJson[key].hasOwnProperty('placeholders')) { - if (!localeJson[key] || !localeJson[key].hasOwnProperty('placeholders')) { + if (Object.prototype.hasOwnProperty.call(defaultLocaleJson[key], 'placeholders')) { + if (!localeJson[key] || !Object.prototype.hasOwnProperty.call(localeJson[key], 'placeholders')) { hasMissingPlaceholders = true; missingPlaceholders[locale].push(`${key}: missing ${Object.keys(defaultLocaleJson[key].placeholders).length} placeholder(s)`); return; @@ -326,8 +326,8 @@ function findExtraPlaceholders(paths) { const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; extraPlaceholders[locale] = []; Object.keys(localeJson).forEach((key) => { - if (localeJson[key].hasOwnProperty('placeholders')) { - if (!defaultLocaleJson[key] || !defaultLocaleJson[key].hasOwnProperty('placeholders')) { + if (Object.prototype.hasOwnProperty.call(localeJson[key], 'placeholders')) { + if (!defaultLocaleJson[key] || !Object.prototype.hasOwnProperty.call(defaultLocaleJson[key], 'placeholders')) { hasExtraPlaceholders = true; extraPlaceholders[locale].push(`${key}: has ${Object.keys(localeJson[key].placeholders).length} extra placeholder(s)`); return; @@ -374,7 +374,7 @@ function findMalformedPlaceholders(paths) { if (matchedPlaceholders) { matchedPlaceholders.forEach((p) => { const placeholder = p.toLowerCase().slice(1, -1); - if (!placeholders.hasOwnProperty(placeholder)) { + if (!Object.prototype.hasOwnProperty.call(placeholders, placeholder)) { hasMalformedPlaceholders = true; malformedPlaceholders[locale].push(`${key}: needs placeholder "${placeholder}"`); } diff --git a/tools/leet/leet-en.js b/tools/leet/leet-en.js index f1c236894..af946f111 100644 --- a/tools/leet/leet-en.js +++ b/tools/leet/leet-en.js @@ -67,7 +67,7 @@ const leet_convert = function(string) { output = output.replace(/cks/g, 'x'); for (letter in characterMap) { - if (characterMap.hasOwnProperty(letter)) { + if (Object.prototype.hasOwnProperty.call(characterMap, letter)) { output = output.replace(new RegExp(letter, 'g'), characterMap[letter]); } } @@ -87,7 +87,7 @@ if (!fs.existsSync('./tools/leet/messages.en.copy.json')) { // Create a LEETed version of the messages.json file for (key in en) { - if (en[key].hasOwnProperty('message')) { + if (Object.prototype.hasOwnProperty.call(en[key], 'message')) { const message = leet_convert(en[key].message); const { placeholders } = en[key]; leet[key] = { message, placeholders }; diff --git a/tools/transifex.js b/tools/transifex.js index d8086e72a..a888ebdc1 100644 --- a/tools/transifex.js +++ b/tools/transifex.js @@ -102,8 +102,8 @@ function fixMissingPlaceholders(paths) { let dirty = false; const localeJson = jsonfile.readFileSync(`.${path}`); Object.keys(defaultLocaleJson).forEach((key) => { - if (defaultLocaleJson[key].hasOwnProperty('placeholders')) { - if (localeJson[key] && !localeJson[key].hasOwnProperty('placeholders')) { + if (Object.prototype.hasOwnProperty.call(defaultLocaleJson[key], 'placeholders')) { + if (localeJson[key] && !Object.prototype.hasOwnProperty.call(localeJson[key], 'placeholders')) { dirty = true; localeJson[key].placeholders = defaultLocaleJson[key].placeholders; } From 5f3471ef6303743b58dcb8059065a89d9b4abc3a Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Wed, 6 May 2020 16:09:20 +0200 Subject: [PATCH 06/89] finish linting for class-methods-use-this --- src/background.js | 16 +++++----- src/classes/BugDb.js | 16 +++++----- src/classes/EventHandlers.js | 60 ++++++++++++++++++------------------ src/classes/Module.js | 2 +- 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/background.js b/src/background.js index 5c9865c89..21ef769f3 100644 --- a/src/background.js +++ b/src/background.js @@ -72,7 +72,7 @@ const IS_FIREFOX = (BROWSER_INFO.name === 'firefox'); const VERSION_CHECK_URL = `${CDN_BASE_URL}/update/version`; const REAL_ESTATE_ID = 'ghostery'; const onBeforeRequest = events.onBeforeRequest.bind(events); -const onHeadersReceived = events.onHeadersReceived.bind(events); +const { onHeadersReceived } = Events; // Cliqz Modules const moduleMock = { @@ -1188,7 +1188,7 @@ function initialiseWebRequestPipeline() { spec: 'collect', before: existingSteps.onHeadersReceived, fn: (state) => { - events.onHeadersReceived(state); + Events.onHeadersReceived(state); return true; } }) @@ -1344,10 +1344,10 @@ function initializeEventListeners() { chrome.webNavigation.onCommitted.addListener(events.onCommitted.bind(events)); // Fired when the page's DOM is fully constructed, but the referenced resources may not finish loading - chrome.webNavigation.onDOMContentLoaded.addListener(events.onDOMContentLoaded.bind(events)); + chrome.webNavigation.onDOMContentLoaded.addListener(Events.onDOMContentLoaded.bind(events)); // Fired when a document, including the resources it refers to, is completely loaded and initialized - chrome.webNavigation.onCompleted.addListener(events.onNavigationCompleted.bind(events)); + chrome.webNavigation.onCompleted.addListener(Events.onNavigationCompleted.bind(events)); // Fired when a new window, or a new tab in an existing window, is created to host a navigation. // chrome.webNavigation.onCreatedNavigationTarget @@ -1372,7 +1372,7 @@ function initializeEventListeners() { // chrome.webRequest.onBeforeRequest // Fires when a request is about to send headers - chrome.webRequest.onBeforeSendHeaders.addListener(events.onBeforeSendHeaders.bind(events), { + chrome.webRequest.onBeforeSendHeaders.addListener(Events.onBeforeSendHeaders.bind(events), { urls: [ 'https://l.ghostery.com/*', 'https://d.ghostery.com/*', @@ -1400,17 +1400,17 @@ function initializeEventListeners() { }); // Fires when a request could not be processed successfully - chrome.webRequest.onErrorOccurred.addListener(events.onRequestErrorOccurred.bind(events), { + chrome.webRequest.onErrorOccurred.addListener(Events.onRequestErrorOccurred.bind(events), { urls: ['http://*/*', 'https://*/*'] }); /** * TABS ** */ // Fired when a new tab is created by user or internally - chrome.tabs.onCreated.addListener(events.onTabCreated.bind(events)); + chrome.tabs.onCreated.addListener(Events.onTabCreated.bind(events)); // Fires when the active tab in a window changes - chrome.tabs.onActivated.addListener(events.onTabActivated.bind(events)); + chrome.tabs.onActivated.addListener(Events.onTabActivated.bind(events)); // Fired when a tab is replaced with another tab due to prerendering chrome.tabs.onReplaced.addListener(events.onTabReplaced.bind(events)); diff --git a/src/classes/BugDb.js b/src/classes/BugDb.js index aeee2f505..1166bfa34 100644 --- a/src/classes/BugDb.js +++ b/src/classes/BugDb.js @@ -37,7 +37,7 @@ class BugDb extends Updatable { * @param {Object} old_apps trackers in the original database * @return {Object} list of all new trackers */ - updateNewAppIds(new_apps, old_apps) { + static updateNewAppIds(new_apps, old_apps) { log('updating newAppIds...'); const new_app_ids = difference( @@ -54,7 +54,7 @@ class BugDb extends Updatable { * Apply block to all new trackers * @param {Object} new_app_ids list of new trackers */ - applyBlockByDefault(new_app_ids) { + static applyBlockByDefault(new_app_ids) { if (conf.block_by_default) { log('applying block-by-default...'); const { selected_app_ids } = conf; @@ -74,7 +74,7 @@ class BugDb extends Updatable { * @param {Object} db bugs database object * @return {array} array of categories */ - _buildCategories(db) { + static _buildCategories(db) { const selectedApps = conf.selected_app_ids || {}; let appId; let category; @@ -207,10 +207,10 @@ class BugDb extends Updatable { // update newAppIds and apply block-by-default if (old_bugs) { if (Object.prototype.hasOwnProperty.call(old_bugs, 'version') && bugs.version > old_bugs.version) { - new_app_ids = this.updateNewAppIds(bugs.apps, old_bugs.apps); + new_app_ids = BugDb.updateNewAppIds(bugs.apps, old_bugs.apps); if (new_app_ids.length) { - this.applyBlockByDefault(new_app_ids); + BugDb.applyBlockByDefault(new_app_ids); db.JUST_UPDATED_WITH_NEW_TRACKERS = true; } @@ -221,10 +221,10 @@ class BugDb extends Updatable { return memo; }, {}); - new_app_ids = this.updateNewAppIds(bugs.apps, old_apps); + new_app_ids = BugDb.updateNewAppIds(bugs.apps, old_apps); if (new_app_ids.length) { - this.applyBlockByDefault(new_app_ids); + BugDb.applyBlockByDefault(new_app_ids); // don't claim new trackers when db got downgraded by version if (bugs.version > old_bugs.bugsVersion) { @@ -237,7 +237,7 @@ class BugDb extends Updatable { conf.bugs = bugs; } - db.categories = this._buildCategories(db); + db.categories = BugDb._buildCategories(db); this.db = db; diff --git a/src/classes/EventHandlers.js b/src/classes/EventHandlers.js index 8bee374b0..544d9bf9d 100644 --- a/src/classes/EventHandlers.js +++ b/src/classes/EventHandlers.js @@ -72,7 +72,7 @@ class EventHandlers { log(`❤ ❤ ❤ Tab ${tabId} navigating to ${url} ❤ ❤ ❤`); this._clearTabData(tabId); - this._resetNotifications(); + EventHandlers._resetNotifications(); // TODO understand why this does not work when placed in the 'reload' branch in onCommitted panelData.clearPageLoadTime(tabId); @@ -80,7 +80,7 @@ class EventHandlers { tabInfo.create(tabId, url); foundBugs.update(tabId); button.update(tabId); - this._eventReset(details.tabId); + EventHandlers._eventReset(details.tabId); // Workaround for foundBugs/tabInfo memory leak when the user triggers // prefetching/prerendering but never loads the page. Wait two minutes @@ -89,7 +89,7 @@ class EventHandlers { utils.getTab(tabId, null, () => { log('Clearing orphan tab data for tab', tabId); this._clearTabData(tabId); - this._resetNotifications(); + EventHandlers._resetNotifications(); }); }, 120000); } @@ -143,7 +143,7 @@ class EventHandlers { * * @param {Object} details event data */ - onDOMContentLoaded(details) { + static onDOMContentLoaded(details) { const tab_id = details.tabId; // ignore if this is a sub-frame @@ -280,7 +280,7 @@ class EventHandlers { * * @param {Object} details event data */ - onNavigationCompleted(details) { + static onNavigationCompleted(details) { if (!utils.isValidTopLevelNavigation(details)) { return; } @@ -289,7 +289,7 @@ class EventHandlers { log(`foundBugs: ${foundBugs.getAppsCount(details.tabId)}, tab_id: ${details.tabId}`); // inject page_performance script to display page latency on Summary view - if (this._isValidUrl(utils.processUrl(details.url))) { + if (EventHandlers._isValidUrl(utils.processUrl(details.url))) { utils.injectScript(details.tabId, 'dist/page_performance.js', '', 'document_idle').catch((err) => { log('onNavigationCompleted injectScript error', err); }); @@ -310,13 +310,13 @@ class EventHandlers { // TODO what other webRequest-restricted pages are out there? if (details.url.startsWith('https://chrome.google.com/webstore/')) { this._clearTabData(tab_id); - this._resetNotifications(); + EventHandlers._resetNotifications(); } return; } - this._eventReset(tab_id); + EventHandlers._eventReset(tab_id); } /** @@ -362,7 +362,7 @@ class EventHandlers { }); } - if (!this._checkRedirect(details.type, request_id)) { + if (!EventHandlers._checkRedirect(details.type, request_id)) { return { cancel: false }; } @@ -373,7 +373,7 @@ class EventHandlers { /* ** SMART BLOCKING - Privacy ** */ // block HTTP request on HTTPS page if (PolicySmartBlock.isInsecureRequest(tab_id, page_protocol, processed.scheme, processed.hostname)) { - return this._blockHelper(details, tab_id, null, null, request_id, from_redirect, true); + return EventHandlers._blockHelper(details, tab_id, null, null, request_id, from_redirect, true); } // TODO fuse this into a single call to improve performance @@ -402,7 +402,7 @@ class EventHandlers { const incognito = tabInfo.getTabInfo(tab_id, 'incognito'); const tab_host = tabInfo.getTabInfo(tab_id, 'host'); const fromRedirect = globals.REDIRECT_MAP.has(request_id); - const { block, reason } = this._checkBlocking(app_id, cat_id, tab_id, tab_host, page_url, request_id); + const { block, reason } = EventHandlers._checkBlocking(app_id, cat_id, tab_id, tab_host, page_url, request_id); if (!block && reason === BLOCK_REASON_SS_UNBLOCKED) { // The way to pass this flag to Cliqz handlers details.ghosteryWhitelisted = true; @@ -441,7 +441,7 @@ class EventHandlers { }, 1); if ((block && !smartUnblocked) || smartBlocked) { - return this._blockHelper(details, tab_id, app_id, bug_id, request_id, fromRedirect); + return EventHandlers._blockHelper(details, tab_id, app_id, bug_id, request_id, fromRedirect); } return { cancel: false }; @@ -454,7 +454,7 @@ class EventHandlers { * @param {Object} d event data * @return {Object} optionally return headers to send */ - onBeforeSendHeaders(d) { + static onBeforeSendHeaders(d) { const details = d; for (let i = 0; i < details.requestHeaders.length; ++i) { // Fetch requests in Firefox web-extension has a flaw. They attach @@ -477,7 +477,7 @@ class EventHandlers { * * @param {Object} details event data */ - onHeadersReceived(details) { + static onHeadersReceived(details) { // Skip content-length collection if it's a 3XX (redirect) if (details.statusCode >> 8 === 1) { } // eslint-disable-line } @@ -511,7 +511,7 @@ class EventHandlers { if (!details || details.tabId <= 0) { return; } - this._clearRedirects(details.requestId); + EventHandlers._clearRedirects(details.requestId); if (details.type !== 'main_frame') { const appWithLatencyId = latency.logLatency(details); @@ -529,9 +529,9 @@ class EventHandlers { * * @param {Object} details event data */ - onRequestErrorOccurred(details) { + static onRequestErrorOccurred(details) { latency.logLatency(details); - this._clearRedirects(details.requestId); + EventHandlers._clearRedirects(details.requestId); } /** @@ -540,7 +540,7 @@ class EventHandlers { * * @param {Object} tab Details of the tab that was created */ - onTabCreated(tab) { + static onTabCreated(tab) { const { url } = tab; metrics.handleBrokenPageTrigger(globals.BROKEN_PAGE_NEW_TAB, url); @@ -552,9 +552,9 @@ class EventHandlers { * * @param {Object} activeInfo tab data */ - onTabActivated(activeInfo) { + static onTabActivated(activeInfo) { button.update(activeInfo.tabId); - this._resetNotifications(); + EventHandlers._resetNotifications(); } /** @@ -586,7 +586,7 @@ class EventHandlers { */ onTabRemoved(tab_id) { this._clearTabData(tab_id); - this._resetNotifications(); + EventHandlers._resetNotifications(); } /** @@ -646,7 +646,7 @@ class EventHandlers { * @param {boolean} fromRedirect * @return {string|boolean} */ - _blockHelper(details, tabId, appId, bugId, requestId, fromRedirect, upgradeInsecure) { + static _blockHelper(details, tabId, appId, bugId, requestId, fromRedirect, upgradeInsecure) { if (upgradeInsecure) { // attempt to redirect request to HTTPS. NOTE: Redirects from URLs // with ws:// and wss:// schemes are ignored. @@ -675,7 +675,7 @@ class EventHandlers { if (details.type === 'script' && bugId) { let code = ''; if (appId === 2575) { // Hubspot - code = this._getHubspotFormSurrogate(details.url); + code = EventHandlers._getHubspotFormSurrogate(details.url); } else { const ti = tabInfo.getTabInfo(tabId); const surrogates = surrogatedb.getForTracker(details.url, appId, bugId, ti.host); @@ -717,7 +717,7 @@ class EventHandlers { * @param {URL} parsedURL * @return {Boolean} */ - _isValidUrl(parsedURL) { + static _isValidUrl(parsedURL) { if (parsedURL && parsedURL.protocol.startsWith('http') && parsedURL.isValidHost() && !parsedURL.pathname.includes('_/chrome/newtab')) { return true; } @@ -733,7 +733,7 @@ class EventHandlers { * @param {string} form request url * @return {string} surrogate code */ - _getHubspotFormSurrogate(url) { + static _getHubspotFormSurrogate(url) { // Hubspot url has a fixed format // https://forms.hubspot.com/embed/v3/form/532040/95b5de3a-6d4a-4729-bebf-07c41268d773?callback=hs_reqwest_0&hutk=941df50e9277ee76755310cd78647a08 // The following three parameters are privacy-safe: @@ -786,7 +786,7 @@ class EventHandlers { * * @param {number} requestId */ - _clearRedirects(requestId) { + static _clearRedirects(requestId) { globals.REDIRECT_MAP.delete(requestId); globals.LET_REDIRECTS_THROUGH = false; } @@ -800,7 +800,7 @@ class EventHandlers { * @param {number} request_id request id * @return {boolean} */ - _checkRedirect(type, request_id) { + static _checkRedirect(type, request_id) { const fromRedirect = globals.REDIRECT_MAP.has(request_id); // if the request is part of the main_frame and not a redirect, we don't proceed if (type === 'main_frame' && !fromRedirect) { @@ -829,7 +829,7 @@ class EventHandlers { * @param {number} request_id request id * @return {BlockWithReason} block result with reason */ - _checkBlocking(app_id, cat_id, tab_id, tab_host, page_url, request_id) { + static _checkBlocking(app_id, cat_id, tab_id, tab_host, page_url, request_id) { const fromRedirect = globals.REDIRECT_MAP.has(request_id); let block; @@ -851,7 +851,7 @@ class EventHandlers { * * @param {number} tab_id tab id */ - _eventReset(tab_id) { + static _eventReset(tab_id) { c2pDb.reset(tab_id); globals.REDIRECT_MAP.clear(); globals.LET_REDIRECTS_THROUGH = false; @@ -879,7 +879,7 @@ class EventHandlers { * @private * */ - _resetNotifications() { + static _resetNotifications() { globals.C2P_LOADED = globals.NOTIFICATIONS_LOADED = false; // eslint-disable-line no-multi-assign } } diff --git a/src/classes/Module.js b/src/classes/Module.js index 60170f87b..e1ec1a13a 100644 --- a/src/classes/Module.js +++ b/src/classes/Module.js @@ -30,7 +30,7 @@ const background = baseBackground({ }); class GhosteryModule extends Module { - get _module() { + static get _module() { return background; } } From d4d84dcefbebf0e02049b4daecd2e4c9c86d9b20 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Wed, 6 May 2020 16:51:12 +0200 Subject: [PATCH 07/89] add linting for no-mixed-operators and fix resulting linting errors --- .eslintrc.js | 2 +- .../CreateAccountView/CreateAccountViewContainer.jsx | 2 +- app/hub/Views/HomeView/HomeViewContainer.jsx | 2 +- app/hub/Views/LogInView/LogInViewContainer.jsx | 2 +- app/hub/Views/PlusView/PlusViewContainer.jsx | 2 +- app/panel-android/actions/trackerActions.js | 4 ++-- app/panel-android/components/content/FixedMenu.jsx | 2 +- app/panel-android/components/content/Path.jsx | 2 +- app/panel-android/utils/chart.js | 2 +- app/panel/components/Blocking.jsx | 2 +- app/panel/components/Blocking/BlockingHeader.jsx | 4 ++-- app/panel/components/BuildingBlocks/DonutGraph.jsx | 2 +- app/panel/components/Header.jsx | 2 +- app/panel/components/Summary.jsx | 10 +++++----- app/panel/reducers/blocking.js | 4 ++-- app/panel/utils/blocking.js | 6 +++--- app/panel/utils/utils.js | 2 +- src/classes/PanelData.js | 4 ++-- src/classes/PolicySmartBlock.js | 4 ++-- src/classes/TabInfo.js | 2 +- src/utils/common.js | 2 +- src/utils/utils.js | 2 +- 22 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 739230bb0..80d59f5c8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -54,7 +54,7 @@ module.exports = { 'lines-between-class-members': [1], 'max-len': [0], 'newline-per-chained-call': [0, { 'ignoreChainWithDepth': 2 }], - 'no-mixed-operators': [0], // TODO: enable this check + 'no-mixed-operators': [1], 'no-nested-ternary': [0], 'no-param-reassign': ['error', { props: true, diff --git a/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx b/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx index f0099d3b2..a495f0317 100644 --- a/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx +++ b/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx @@ -189,7 +189,7 @@ class CreateAccountViewContainer extends Component { handleSubmit: this._handleCreateAccountAttempt }; const signedInChildProps = { - email: user && user.email || email, + email: (user && user.email) || email, }; return loggedIn ? ( diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index c98e55b78..aa3a8ed3e 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -117,7 +117,7 @@ class HomeViewContainer extends Component { _render() { const { justInstalled } = this.state; const { home, user } = this.props; - const isPlus = user && user.subscriptionsPlus || false; + const isPlus = (user && user.subscriptionsPlus) || false; const { premium_promo_modal_shown, setup_complete, diff --git a/app/hub/Views/LogInView/LogInViewContainer.jsx b/app/hub/Views/LogInView/LogInViewContainer.jsx index fb9cd1289..e4efeb2a3 100644 --- a/app/hub/Views/LogInView/LogInViewContainer.jsx +++ b/app/hub/Views/LogInView/LogInViewContainer.jsx @@ -137,7 +137,7 @@ class LogInViewContainer extends Component { handleSubmit: this._handleLoginAttempt, }; const signedInChildProps = { - email: user && user.email || 'email', + email: (user && user.email) || 'email', }; return loggedIn ? ( diff --git a/app/hub/Views/PlusView/PlusViewContainer.jsx b/app/hub/Views/PlusView/PlusViewContainer.jsx index 5d8332ddc..c3183ba44 100644 --- a/app/hub/Views/PlusView/PlusViewContainer.jsx +++ b/app/hub/Views/PlusView/PlusViewContainer.jsx @@ -44,7 +44,7 @@ class PlusViewContainer extends Component { */ render() { const childProps = { - isPlus: this.props.user && this.props.user.subscriptionsPlus || false, + isPlus: (this.props.user && this.props.user.subscriptionsPlus) || false, onPlusClick: this._sendPlusPing, }; diff --git a/app/panel-android/actions/trackerActions.js b/app/panel-android/actions/trackerActions.js index 9594a6d6a..f5ea7cf84 100644 --- a/app/panel-android/actions/trackerActions.js +++ b/app/panel-android/actions/trackerActions.js @@ -21,8 +21,8 @@ function trustRestrictTracker({ const siteSpecificUnblocks = blocking.site_specific_unblocks; const siteSpecificBlocks = blocking.site_specific_blocks; - const pageUnblocks = siteSpecificUnblocks[pageHost] && siteSpecificUnblocks[pageHost].slice(0) || []; // clone - const pageBlocks = siteSpecificBlocks[pageHost] && siteSpecificBlocks[pageHost].slice(0) || []; // clone + const pageUnblocks = (siteSpecificUnblocks[pageHost] && siteSpecificUnblocks[pageHost].slice(0)) || []; // clone + const pageBlocks = (siteSpecificBlocks[pageHost] && siteSpecificBlocks[pageHost].slice(0)) || []; // clone let updated_site_specific_unblocks = {}; let updated_site_specific_blocks = {}; diff --git a/app/panel-android/components/content/FixedMenu.jsx b/app/panel-android/components/content/FixedMenu.jsx index 4a87001d1..61d826b63 100644 --- a/app/panel-android/components/content/FixedMenu.jsx +++ b/app/panel-android/components/content/FixedMenu.jsx @@ -64,7 +64,7 @@ export default class FixedMenu extends React.Component { return total; } case 'enable_ad_block': - return this.adBlockData && this.adBlockData.totalCount || 0; + return (this.adBlockData && this.adBlockData.totalCount) || 0; case 'enable_smart_block': Object.keys(this.smartBlockData.blocked || {}).forEach(() => { total++; diff --git a/app/panel-android/components/content/Path.jsx b/app/panel-android/components/content/Path.jsx index 340d1209c..2ee06c8fd 100644 --- a/app/panel-android/components/content/Path.jsx +++ b/app/panel-android/components/content/Path.jsx @@ -43,7 +43,7 @@ export default class Path extends React.Component { } static polarToCartesian(centerX, centerY, radius, angleInDegrees) { - const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0; + const angleInRadians = (angleInDegrees - 90) * (Math.PI / 180.0); return { x: centerX + (radius * Math.cos(angleInRadians)), diff --git a/app/panel-android/utils/chart.js b/app/panel-android/utils/chart.js index 6a86f9da6..2562e0afd 100644 --- a/app/panel-android/utils/chart.js +++ b/app/panel-android/utils/chart.js @@ -28,7 +28,7 @@ export function fromTrackersToChartData(trackers) { const sum = trackers.map(tracker => tracker.numTotal).reduce((a, b) => a + b, 0); for (let i = 0; i < trackers.length; i += 1) { - const endAngle = startAngle + (trackers[i].numTotal * 360 / sum); + const endAngle = startAngle + (trackers[i].numTotal * (360 / sum)); arcs.push({ start: startAngle, diff --git a/app/panel/components/Blocking.jsx b/app/panel/components/Blocking.jsx index 37b6e79aa..c3b869f02 100644 --- a/app/panel/components/Blocking.jsx +++ b/app/panel/components/Blocking.jsx @@ -75,7 +75,7 @@ class Blocking extends React.Component { // Update the summary blocking count whenever the blocking component updated. // This will also show pending blocking changes if the panel is re-opened // before a page refresh - const smartBlock = this.props.smartBlockActive && this.props.smartBlock || { blocked: {}, unblocked: {} }; + const smartBlock = (this.props.smartBlockActive && this.props.smartBlock) || { blocked: {}, unblocked: {} }; updateSummaryBlockingCount(this.props.categories, smartBlock, this.props.actions.updateTrackerCounts); } diff --git a/app/panel/components/Blocking/BlockingHeader.jsx b/app/panel/components/Blocking/BlockingHeader.jsx index 6f778e11e..2147b911d 100644 --- a/app/panel/components/Blocking/BlockingHeader.jsx +++ b/app/panel/components/Blocking/BlockingHeader.jsx @@ -56,7 +56,7 @@ class BlockingHeader extends React.Component { if (typeof this.props.actions.updateTrackerCounts === 'function') { // if we're on GlobalSettings, we don't need to run this function - const smartBlock = this.props.smartBlockActive && this.props.smartBlock || { blocked: {}, unblocked: {} }; + const smartBlock = (this.props.smartBlockActive && this.props.smartBlock) || { blocked: {}, unblocked: {} }; updateSummaryBlockingCount(this.props.categories, smartBlock, this.props.actions.updateTrackerCounts); } } @@ -133,7 +133,7 @@ class BlockingHeader extends React.Component { if (typeof this.props.actions.updateTrackerCounts === 'function') { // if we're on GlobalSettings, we don't need to run this function - const smartBlock = this.props.smartBlockActive && this.props.smartBlock || { blocked: {}, unblocked: {} }; + const smartBlock = (this.props.smartBlockActive && this.props.smartBlock) || { blocked: {}, unblocked: {} }; updateSummaryBlockingCount(this.props.categories, smartBlock, this.props.actions.updateTrackerCounts); } diff --git a/app/panel/components/BuildingBlocks/DonutGraph.jsx b/app/panel/components/BuildingBlocks/DonutGraph.jsx index 17284a75a..dec7cb5da 100644 --- a/app/panel/components/BuildingBlocks/DonutGraph.jsx +++ b/app/panel/components/BuildingBlocks/DonutGraph.jsx @@ -143,7 +143,7 @@ class DonutGraph extends React.Component { * Helper function that calculates domain value for greyscale / redscale rendering */ static getTone(catCount, catIndex) { - return catCount > 1 ? 100 / (catCount - 1) * catIndex * 0.01 : 0; + return catCount > 1 ? (100 / (catCount - 1)) * catIndex * 0.01 : 0; } /** diff --git a/app/panel/components/Header.jsx b/app/panel/components/Header.jsx index 5f31d701a..eb31218f8 100644 --- a/app/panel/components/Header.jsx +++ b/app/panel/components/Header.jsx @@ -109,7 +109,7 @@ class Header extends React.Component { } let accountIcon; - if (!loggedIn || loggedIn && user && !user.emailValidated) { + if (!loggedIn || (loggedIn && user && !user.emailValidated)) { accountIcon = (
diff --git a/app/panel/components/Summary.jsx b/app/panel/components/Summary.jsx index 4b9463999..8c299eca9 100644 --- a/app/panel/components/Summary.jsx +++ b/app/panel/components/Summary.jsx @@ -328,7 +328,7 @@ class Summary extends React.Component { enable_ad_block, } = this.props; - return enable_ad_block && adBlock && adBlock.trackerCount || 0; + return (enable_ad_block && adBlock && adBlock.trackerCount) || 0; } _antiTrackUnsafe() { @@ -337,7 +337,7 @@ class Summary extends React.Component { enable_anti_tracking, } = this.props; - return enable_anti_tracking && antiTracking && antiTracking.trackerCount || 0; + return (enable_anti_tracking && antiTracking && antiTracking.trackerCount) || 0; } _requestsModifiedCount() { @@ -353,7 +353,7 @@ class Summary extends React.Component { _sbBlocked() { const { smartBlock, trackerCounts } = this.props; - let sbBlocked = smartBlock && smartBlock.blocked && Object.keys(smartBlock.blocked).length || 0; + let sbBlocked = (smartBlock && smartBlock.blocked && Object.keys(smartBlock.blocked).length) || 0; if (sbBlocked === trackerCounts.sbBlocked) { sbBlocked = 0; } @@ -364,7 +364,7 @@ class Summary extends React.Component { _sbAllowed() { const { smartBlock, trackerCounts } = this.props; - let sbAllowed = smartBlock && smartBlock.unblocked && Object.keys(smartBlock.unblocked).length || 0; + let sbAllowed = (smartBlock && smartBlock.unblocked && Object.keys(smartBlock.unblocked).length) || 0; if (sbAllowed === trackerCounts.sbAllowed) { sbAllowed = 0; } @@ -375,7 +375,7 @@ class Summary extends React.Component { _sbAdjust() { const { enable_smart_block } = this.props; - return enable_smart_block && (this._sbBlocked() - this._sbAllowed()) || 0; + return enable_smart_block && ((this._sbBlocked() - this._sbAllowed()) || 0); } _totalTrackersBlockedCount() { diff --git a/app/panel/reducers/blocking.js b/app/panel/reducers/blocking.js index fba6086b5..213b15bc7 100644 --- a/app/panel/reducers/blocking.js +++ b/app/panel/reducers/blocking.js @@ -142,8 +142,8 @@ const _updateTrackerTrustRestrict = (state, action) => { const { pageHost } = action; const siteSpecificUnblocks = state.site_specific_unblocks; const siteSpecificBlocks = state.site_specific_blocks; - const pageUnblocks = siteSpecificUnblocks[pageHost] && siteSpecificUnblocks[pageHost].slice(0) || []; // clone - const pageBlocks = siteSpecificBlocks[pageHost] && siteSpecificBlocks[pageHost].slice(0) || []; // clone + const pageUnblocks = (siteSpecificUnblocks[pageHost] && siteSpecificUnblocks[pageHost].slice(0)) || []; // clone + const pageBlocks = (siteSpecificBlocks[pageHost] && siteSpecificBlocks[pageHost].slice(0)) || []; // clone // Site specific un-blocking if (msg.trust) { diff --git a/app/panel/utils/blocking.js b/app/panel/utils/blocking.js index 46669f5ef..b710fa3ce 100644 --- a/app/panel/utils/blocking.js +++ b/app/panel/utils/blocking.js @@ -76,7 +76,7 @@ export function updateBlockAllTrackers(state, action) { const updated_app_ids = JSON.parse(JSON.stringify(state.selected_app_ids)) || {}; const updated_categories = JSON.parse(JSON.stringify(state.categories)) || []; const { smartBlockActive } = action.data; - const smartBlock = smartBlockActive && action.data.smartBlock || { blocked: {}, unblocked: {} }; + const smartBlock = (smartBlockActive && action.data.smartBlock) || { blocked: {}, unblocked: {} }; updated_categories.forEach((categoryEl) => { categoryEl.num_blocked = 0; @@ -117,7 +117,7 @@ export function updateBlockAllTrackers(state, action) { */ export function updateCategoryBlocked(state, action) { const { blocked, smartBlockActive } = action.data; - const smartBlock = smartBlockActive && action.data.smartBlock || { blocked: {}, unblocked: {} }; + const smartBlock = (smartBlockActive && action.data.smartBlock) || { blocked: {}, unblocked: {} }; const updated_app_ids = JSON.parse(JSON.stringify(state.selected_app_ids)) || {}; const updated_categories = JSON.parse(JSON.stringify(state.categories)); // deep clone const catIndex = updated_categories.findIndex(item => item.id === action.data.category); @@ -186,7 +186,7 @@ export function updateTrackerBlocked(state, action) { } const { blocked, smartBlockActive } = action.data; - const smartBlock = smartBlockActive && action.data.smartBlock || { blocked: {}, unblocked: {} }; + const smartBlock = (smartBlockActive && action.data.smartBlock) || { blocked: {}, unblocked: {} }; const updated_app_ids = JSON.parse(JSON.stringify(state.selected_app_ids)) || {}; const updated_categories = JSON.parse(JSON.stringify(state.categories)) || []; // deep clone const catIndex = updated_categories.findIndex(item => item.id === action.data.cat_id); diff --git a/app/panel/utils/utils.js b/app/panel/utils/utils.js index cf1c27cb7..ce438df33 100644 --- a/app/panel/utils/utils.js +++ b/app/panel/utils/utils.js @@ -135,7 +135,7 @@ export function validateConfirmEmail(email, confirmEmail) { } const lEmail = email.toLowerCase(); const lConfirmEmail = confirmEmail.toLowerCase(); - return validateEmail(confirmEmail) && (lEmail === lConfirmEmail) || false; + return (validateEmail(confirmEmail) && (lEmail === lConfirmEmail)) || false; } /** diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 09bcdee66..be365f17b 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -75,7 +75,7 @@ class PanelData { const { url } = tab; this._activeTab = tab; - this._activeTab.pageHost = url && processUrl(url).hostname || ''; + this._activeTab.pageHost = (url && processUrl(url).hostname) || ''; this._attachListeners(); @@ -172,7 +172,7 @@ class PanelData { // Android panel only const { url } = tab; this._activeTab = tab; - this._activeTab.pageHost = url && processUrl(url).hostname || ''; + this._activeTab.pageHost = (url && processUrl(url).hostname) || ''; this._setTrackerListAndCategories(); switch (view) { case 'panel': diff --git a/src/classes/PolicySmartBlock.js b/src/classes/PolicySmartBlock.js index df0a47bbe..7aa1cf834 100644 --- a/src/classes/PolicySmartBlock.js +++ b/src/classes/PolicySmartBlock.js @@ -192,7 +192,7 @@ class PolicySmartBlock { return ( pageProtocol === 'https' && - (requestProtocol === 'http' || requestProtocol === 'ws') || false + ((requestProtocol === 'http' || requestProtocol === 'ws') || false) ); } @@ -232,7 +232,7 @@ class PolicySmartBlock { return ( tabInfo.getTabInfoPersist(tabId, 'numOfReloads') > 1 && - ((Date.now() - tabInfo.getTabInfoPersist(tabId, 'firstLoadTimestamp')) < SMART_BLOCK_BEHAVIOR_THRESHOLD) || false + (((Date.now() - tabInfo.getTabInfoPersist(tabId, 'firstLoadTimestamp')) < SMART_BLOCK_BEHAVIOR_THRESHOLD) || false) ); } diff --git a/src/classes/TabInfo.js b/src/classes/TabInfo.js index 647ccfd62..e0014365b 100644 --- a/src/classes/TabInfo.js +++ b/src/classes/TabInfo.js @@ -58,7 +58,7 @@ class TabInfo { timestamp: Date.now(), // assign only when smartBlock is enabled so avoid false positives // when enabling smartBlock is enabled for the first time - firstLoadTimestamp: PolicySmartBlock.shouldCheck(tab_id) && (numOfReloads === 0 ? Date.now() : (this.getTabInfoPersist(tab_id, 'firstLoadTimestamp') || 0)) || 0, + firstLoadTimestamp: PolicySmartBlock.shouldCheck(tab_id) && (numOfReloads === 0 ? Date.now() : ((this.getTabInfoPersist(tab_id, 'firstLoadTimestamp') || 0)) || 0), reloaded: PolicySmartBlock.checkReloadThreshold(tab_id), numOfReloads, smartBlock: { diff --git a/src/utils/common.js b/src/utils/common.js index e870cfb0a..db625b8a9 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -184,7 +184,7 @@ export function* objectEntries(obj) { * @return {string} unescaped str */ function _base64urlUnescape(str) { - str += new Array(5 - str.length % 4).join('='); // eslint-disable-line no-param-reassign + str += new Array(5 - (str.length % 4)).join('='); // eslint-disable-line no-param-reassign return str.replace(/\-/g, '+').replace(/_/g, '/'); // eslint-disable-line no-useless-escape } diff --git a/src/utils/utils.js b/src/utils/utils.js index faa6bae12..28002a6b6 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -593,7 +593,7 @@ export function injectNotifications(tab_id, importExport = false) { } const tab = tabInfo.getTabInfo(tab_id); // check for prefetching, chrome new tab page and Firefox about:pages - if (tab && tab.prefetched === true || tab.path.includes('_/chrome/newtab') || tab.protocol === 'about' || (!importExport && globals.EXCLUDES.includes(tab.host))) { + if (tab && (tab.prefetched === true || tab.path.includes('_/chrome/newtab') || tab.protocol === 'about' || (!importExport && globals.EXCLUDES.includes(tab.host)))) { // return false to prevent sendMessage calls return Promise.resolve(false); } From 17f90ed75df1752fbedbcd021fcbf60b3abe4436 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Fri, 8 May 2020 17:52:42 +0200 Subject: [PATCH 08/89] add linting for import/prefer-default-export and fix resulting linting errors --- .eslintrc.js | 2 +- app/hub/Views/SetupView/index.js | 6 +++--- .../SetupBlockingView/SetupBlockingViewActions.js | 2 +- .../__tests__/SetupBlockingViewActions.test.js | 4 ++-- app/hub/Views/SetupViews/SetupBlockingView/index.js | 4 ++-- .../Views/SetupViews/SetupDoneView/SetupDoneViewActions.js | 2 +- .../SetupDoneView/__tests__/SetupDoneViewActions.test.js | 4 ++-- app/hub/Views/SetupViews/SetupDoneView/index.js | 4 ++-- .../SetupHumanWebView/SetupHumanWebViewActions.js | 2 +- .../__tests__/SetupHumanWebViewActions.test.js | 4 ++-- app/hub/Views/SetupViews/SetupHumanWebView/index.js | 4 ++-- .../TutorialAntiSuiteView/TutorialAntiSuiteViewActions.js | 2 +- .../__test__/TutorialAntiSuiteViewActions.test.js | 4 ++-- app/hub/Views/TutorialViews/TutorialAntiSuiteView/index.js | 4 ++-- app/panel-android/actions/cliqzActions.js | 2 +- app/panel-android/components/Overview.jsx | 2 +- app/panel-android/components/Panel.jsx | 4 ++-- app/panel-android/utils/chart.js | 2 +- app/panel/actions/DetailActions.js | 2 +- app/panel/components/Blocking.jsx | 2 +- app/panel/components/Blocking/Category.jsx | 2 +- app/panel/components/Blocking/Tracker.jsx | 2 +- app/panel/components/BuildingBlocks/StatsGraph.jsx | 2 +- app/panel/components/Panel.jsx | 4 ++-- app/panel/components/Rewards.jsx | 2 +- app/panel/components/Settings.jsx | 2 +- app/panel/components/Summary.jsx | 2 +- app/panel/components/__tests__/Rewards.jsx | 2 +- app/panel/containers/DetailContainer.js | 4 ++-- app/panel/containers/SettingsContainer.js | 2 +- app/panel/contexts/DynamicUIPortContext.js | 3 ++- app/panel/contexts/ThemeContext.js | 3 ++- src/background.js | 2 +- src/utils/cliqzSettingImport.js | 2 +- 34 files changed, 49 insertions(+), 47 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 80d59f5c8..129c144c5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -77,7 +77,7 @@ module.exports = { // Plugin: Import 'import/no-cycle': [0], - 'import/prefer-default-export': [0], // TODO: enable this check + 'import/prefer-default-export': [1], // Plugin: React 'react/destructuring-assignment': [0], diff --git a/app/hub/Views/SetupView/index.js b/app/hub/Views/SetupView/index.js index 595c53903..5bd10681d 100644 --- a/app/hub/Views/SetupView/index.js +++ b/app/hub/Views/SetupView/index.js @@ -18,15 +18,15 @@ import { withRouter } from 'react-router-dom'; import SetupViewContainer from './SetupViewContainer'; import SetupViewReducer from './SetupViewReducer'; import * as SetupViewActions from './SetupViewActions'; -import { setBlockingPolicy } from '../SetupViews/SetupBlockingView/SetupBlockingViewActions'; +import setBlockingPolicy from '../SetupViews/SetupBlockingView/SetupBlockingViewActions'; import { setAntiTracking, setAdBlock, setSmartBlocking, setGhosteryRewards } from '../SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewActions'; -import { setHumanWeb } from '../SetupViews/SetupHumanWebView/SetupHumanWebViewActions'; -import { setSetupComplete } from '../SetupViews/SetupDoneView/SetupDoneViewActions'; +import setHumanWeb from '../SetupViews/SetupHumanWebView/SetupHumanWebViewActions'; +import setSetupComplete from '../SetupViews/SetupDoneView/SetupDoneViewActions'; /** * Map redux store state properties to the component's own properties. diff --git a/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewActions.js b/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewActions.js index ce9eb31b9..de4b436ed 100644 --- a/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewActions.js +++ b/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewActions.js @@ -14,7 +14,7 @@ import { log, sendMessageInPromise } from '../../../utils'; import { SET_BLOCKING_POLICY } from '../../SetupView/SetupViewConstants'; -export function setBlockingPolicy(actionData) { +export default function setBlockingPolicy(actionData) { return function(dispatch) { return sendMessageInPromise(SET_BLOCKING_POLICY, actionData).then((data) => { dispatch({ diff --git a/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingViewActions.test.js b/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingViewActions.test.js index 23504c3db..cfa80c6f0 100644 --- a/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingViewActions.test.js +++ b/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingViewActions.test.js @@ -14,7 +14,7 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import * as utils from '../../../../utils'; -import * as SetupBlockingViewActions from '../SetupBlockingViewActions'; +import setBlockingPolicy from '../SetupBlockingViewActions'; import { SET_BLOCKING_POLICY } from '../../../SetupView/SetupViewConstants'; const middlewares = [thunk]; @@ -39,7 +39,7 @@ describe('app/hub/Views/SetupViews/SetupBlockingView actions', () => { const data = testData; const expectedPayload = { data, type: SET_BLOCKING_POLICY }; - return store.dispatch(SetupBlockingViewActions.setBlockingPolicy(data)).then(() => { + return store.dispatch(setBlockingPolicy(data)).then(() => { const actions = store.getActions(); expect(actions).toEqual([expectedPayload]); }); diff --git a/app/hub/Views/SetupViews/SetupBlockingView/index.js b/app/hub/Views/SetupViews/SetupBlockingView/index.js index 2d0825f6a..383bda7ff 100644 --- a/app/hub/Views/SetupViews/SetupBlockingView/index.js +++ b/app/hub/Views/SetupViews/SetupBlockingView/index.js @@ -16,7 +16,7 @@ import { bindActionCreators } from 'redux'; import { withRouter } from 'react-router-dom'; import SetupBlockingViewContainer from './SetupBlockingViewContainer'; -import * as SetupBlockingViewActions from './SetupBlockingViewActions'; +import setBlockingPolicy from './SetupBlockingViewActions'; import { setSetupStep, setSetupNavigation } from '../../SetupView/SetupViewActions'; /** @@ -35,7 +35,7 @@ const mapStateToProps = state => ({ ...state.setup }); */ const mapDispatchToProps = dispatch => ({ actions: bindActionCreators({ - ...SetupBlockingViewActions, + setBlockingPolicy, setSetupStep, setSetupNavigation }, dispatch), diff --git a/app/hub/Views/SetupViews/SetupDoneView/SetupDoneViewActions.js b/app/hub/Views/SetupViews/SetupDoneView/SetupDoneViewActions.js index 10ce3e7b4..8c11c0d6e 100644 --- a/app/hub/Views/SetupViews/SetupDoneView/SetupDoneViewActions.js +++ b/app/hub/Views/SetupViews/SetupDoneView/SetupDoneViewActions.js @@ -14,7 +14,7 @@ import { log, sendMessageInPromise } from '../../../utils'; import { SET_SETUP_COMPLETE } from '../../SetupView/SetupViewConstants'; -export function setSetupComplete(actionData) { +export default function setSetupComplete(actionData) { return function(dispatch) { return sendMessageInPromise(SET_SETUP_COMPLETE, actionData).then((data) => { dispatch({ diff --git a/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneViewActions.test.js b/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneViewActions.test.js index 5c66ecd65..1e8a99a74 100644 --- a/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneViewActions.test.js +++ b/app/hub/Views/SetupViews/SetupDoneView/__tests__/SetupDoneViewActions.test.js @@ -14,7 +14,7 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import * as utils from '../../../../utils'; -import * as SetupDoneViewActions from '../SetupDoneViewActions'; +import setSetupComplete from '../SetupDoneViewActions'; import { SET_SETUP_COMPLETE } from '../../../SetupView/SetupViewConstants'; const middlewares = [thunk]; @@ -39,7 +39,7 @@ describe('app/hub/Views/SetupViews/SetupDoneView actions', () => { const data = testData; const expectedPayload = { data, type: SET_SETUP_COMPLETE }; - return store.dispatch(SetupDoneViewActions.setSetupComplete(data)).then(() => { + return store.dispatch(setSetupComplete(data)).then(() => { const actions = store.getActions(); expect(actions).toEqual([expectedPayload]); }); diff --git a/app/hub/Views/SetupViews/SetupDoneView/index.js b/app/hub/Views/SetupViews/SetupDoneView/index.js index 1ed399b91..47199b305 100644 --- a/app/hub/Views/SetupViews/SetupDoneView/index.js +++ b/app/hub/Views/SetupViews/SetupDoneView/index.js @@ -15,7 +15,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import SetupDoneViewContainer from './SetupDoneViewContainer'; -import * as SetupDoneViewActions from './SetupDoneViewActions'; +import setSetupComplete from './SetupDoneViewActions'; import { setSetupStep, setSetupNavigation } from '../../SetupView/SetupViewActions'; /** @@ -34,7 +34,7 @@ const mapStateToProps = state => ({ ...state.setup }); */ const mapDispatchToProps = dispatch => ({ actions: bindActionCreators({ - ...SetupDoneViewActions, + setSetupComplete, setSetupStep, setSetupNavigation }, dispatch), diff --git a/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewActions.js b/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewActions.js index 16cf28b1a..b18500b8a 100644 --- a/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewActions.js +++ b/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewActions.js @@ -14,7 +14,7 @@ import { log, sendMessageInPromise } from '../../../utils'; import { SET_HUMAN_WEB } from '../../SetupView/SetupViewConstants'; -export function setHumanWeb(actionData) { +export default function setHumanWeb(actionData) { return function(dispatch) { return sendMessageInPromise(SET_HUMAN_WEB, actionData).then((data) => { dispatch({ diff --git a/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebViewActions.test.js b/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebViewActions.test.js index e00a40c89..3b21c84be 100644 --- a/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebViewActions.test.js +++ b/app/hub/Views/SetupViews/SetupHumanWebView/__tests__/SetupHumanWebViewActions.test.js @@ -14,7 +14,7 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import * as utils from '../../../../utils'; -import * as SetupHumanWebViewActions from '../SetupHumanWebViewActions'; +import setHumanWeb from '../SetupHumanWebViewActions'; import { SET_HUMAN_WEB } from '../../../SetupView/SetupViewConstants'; const middlewares = [thunk]; @@ -39,7 +39,7 @@ describe('app/hub/Views/SetupViews/SetupHumanWebView actions', () => { const data = testData; const expectedPayload = { data, type: SET_HUMAN_WEB }; - return store.dispatch(SetupHumanWebViewActions.setHumanWeb(data)).then(() => { + return store.dispatch(setHumanWeb(data)).then(() => { const actions = store.getActions(); expect(actions).toEqual([expectedPayload]); }); diff --git a/app/hub/Views/SetupViews/SetupHumanWebView/index.js b/app/hub/Views/SetupViews/SetupHumanWebView/index.js index e5c9110e8..745d9904e 100644 --- a/app/hub/Views/SetupViews/SetupHumanWebView/index.js +++ b/app/hub/Views/SetupViews/SetupHumanWebView/index.js @@ -15,7 +15,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import SetupHumanWebViewContainer from './SetupHumanWebViewContainer'; -import * as SetupHumanWebViewActions from './SetupHumanWebViewActions'; +import setHumanWeb from './SetupHumanWebViewActions'; import { setSetupStep, setSetupNavigation } from '../../SetupView/SetupViewActions'; /** @@ -34,7 +34,7 @@ const mapStateToProps = state => ({ ...state.setup }); */ const mapDispatchToProps = dispatch => ({ actions: bindActionCreators({ - ...SetupHumanWebViewActions, + setHumanWeb, setSetupStep, setSetupNavigation }, dispatch), diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewActions.js b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewActions.js index 019754ba7..ddf87b787 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewActions.js +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewActions.js @@ -14,7 +14,7 @@ import { log, sendMessageInPromise } from '../../../utils'; import { SET_TUTORIAL_COMPLETE } from '../../TutorialView/TutorialViewConstants'; -export function setTutorialComplete(actionData) { +export default function setTutorialComplete(actionData) { return function(dispatch) { return sendMessageInPromise(SET_TUTORIAL_COMPLETE, actionData).then((data) => { dispatch({ diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/TutorialAntiSuiteViewActions.test.js b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/TutorialAntiSuiteViewActions.test.js index 7864a354a..d0866e007 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/TutorialAntiSuiteViewActions.test.js +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/TutorialAntiSuiteViewActions.test.js @@ -14,7 +14,7 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import * as utils from '../../../../utils'; -import * as TutorialAntiSuiteViewActions from '../TutorialAntiSuiteViewActions'; +import setTutorialComplete from '../TutorialAntiSuiteViewActions'; import { SET_TUTORIAL_COMPLETE } from '../../../TutorialView/TutorialViewConstants'; const middlewares = [thunk]; @@ -39,7 +39,7 @@ describe('app/hub/Views/TutorialViews/TutorialAntiSuiteView actions', () => { const data = testData; const expectedPayload = { data, type: SET_TUTORIAL_COMPLETE }; - return store.dispatch(TutorialAntiSuiteViewActions.setTutorialComplete(data)).then(() => { + return store.dispatch(setTutorialComplete(data)).then(() => { const actions = store.getActions(); expect(actions).toEqual([expectedPayload]); }); diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/index.js b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/index.js index 416e6a124..df5d858e3 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/index.js +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/index.js @@ -15,7 +15,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import TutorialAntiSuiteViewContainer from './TutorialAntiSuiteViewContainer'; -import * as TutorialAntiSuiteViewActions from './TutorialAntiSuiteViewActions'; +import setTutorialComplete from './TutorialAntiSuiteViewActions'; import { setTutorialNavigation } from '../../TutorialView/TutorialViewActions'; /** @@ -33,7 +33,7 @@ const mapStateToProps = state => ({ ...state.tutorial }); * @memberof TutorialContainers */ const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators({ ...TutorialAntiSuiteViewActions, setTutorialNavigation }, dispatch), + actions: bindActionCreators({ setTutorialComplete, setTutorialNavigation }, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(TutorialAntiSuiteViewContainer); diff --git a/app/panel-android/actions/cliqzActions.js b/app/panel-android/actions/cliqzActions.js index 478d36bd1..f020abdaf 100644 --- a/app/panel-android/actions/cliqzActions.js +++ b/app/panel-android/actions/cliqzActions.js @@ -13,7 +13,7 @@ import { sendMessageInPromise } from '../../panel/utils/msg'; -export function getCliqzModuleData(tabId) { +export default function getCliqzModuleData(tabId) { return sendMessageInPromise('getCliqzModuleData', { tabId, }); diff --git a/app/panel-android/components/Overview.jsx b/app/panel-android/components/Overview.jsx index 3a0913e1f..ce30ea60b 100644 --- a/app/panel-android/components/Overview.jsx +++ b/app/panel-android/components/Overview.jsx @@ -14,7 +14,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import TrackersChart from './content/TrackersChart'; -import { fromTrackersToChartData } from '../utils/chart'; +import fromTrackersToChartData from '../utils/chart'; export default class Overview extends React.Component { get isTrusted() { diff --git a/app/panel-android/components/Panel.jsx b/app/panel-android/components/Panel.jsx index 204f4ce21..01ea37368 100644 --- a/app/panel-android/components/Panel.jsx +++ b/app/panel-android/components/Panel.jsx @@ -23,9 +23,9 @@ import TrackersChart from './content/TrackersChart'; import { getPanelData, getSummaryData, getSettingsData, getBlockingData } from '../actions/panelActions'; -import { getCliqzModuleData } from '../actions/cliqzActions'; +import getCliqzModuleData from '../actions/cliqzActions'; import handleAllActions from '../actions/handler'; -import { fromTrackersToChartData } from '../utils/chart'; +import fromTrackersToChartData from '../utils/chart'; export default class Panel extends React.Component { constructor(props) { diff --git a/app/panel-android/utils/chart.js b/app/panel-android/utils/chart.js index 2562e0afd..18b314b98 100644 --- a/app/panel-android/utils/chart.js +++ b/app/panel-android/utils/chart.js @@ -14,7 +14,7 @@ * @namespace PanelAndroidUtils */ -export function fromTrackersToChartData(trackers) { +export default function fromTrackersToChartData(trackers) { if (trackers.length < 1) { return { sum: 0, diff --git a/app/panel/actions/DetailActions.js b/app/panel/actions/DetailActions.js index 00dc5aa7c..20fba18ed 100644 --- a/app/panel/actions/DetailActions.js +++ b/app/panel/actions/DetailActions.js @@ -17,7 +17,7 @@ import { TOGGLE_EXPANDED } from '../constants/constants'; * Called from Detail and picked up by Panel reducer * @return {Object} */ -export function toggleExpanded() { +export default function toggleExpanded() { return { type: TOGGLE_EXPANDED, }; diff --git a/app/panel/components/Blocking.jsx b/app/panel/components/Blocking.jsx index c3b869f02..07c9b2e30 100644 --- a/app/panel/components/Blocking.jsx +++ b/app/panel/components/Blocking.jsx @@ -15,7 +15,7 @@ import React from 'react'; import Categories from './Blocking/Categories'; import BlockingHeader from './Blocking/BlockingHeader'; import NotScanned from './BuildingBlocks/NotScanned'; -import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; +import DynamicUIPortContext from '../contexts/DynamicUIPortContext'; import { updateSummaryBlockingCount } from '../utils/blocking'; /** diff --git a/app/panel/components/Blocking/Category.jsx b/app/panel/components/Blocking/Category.jsx index fe9be3483..5111a8b4c 100644 --- a/app/panel/components/Blocking/Category.jsx +++ b/app/panel/components/Blocking/Category.jsx @@ -13,7 +13,7 @@ import React from 'react'; import ClassNames from 'classnames'; -import { ThemeContext } from '../../contexts/ThemeContext'; +import ThemeContext from '../../contexts/ThemeContext'; import Trackers from './Trackers'; /** diff --git a/app/panel/components/Blocking/Tracker.jsx b/app/panel/components/Blocking/Tracker.jsx index 6fc341800..62871aac8 100644 --- a/app/panel/components/Blocking/Tracker.jsx +++ b/app/panel/components/Blocking/Tracker.jsx @@ -16,7 +16,7 @@ import React from 'react'; import ClassNames from 'classnames'; -import { ThemeContext } from '../../contexts/ThemeContext'; +import ThemeContext from '../../contexts/ThemeContext'; import globals from '../../../../src/classes/Globals'; import { log } from '../../../../src/utils/common'; import { sendMessageInPromise } from '../../utils/msg'; diff --git a/app/panel/components/BuildingBlocks/StatsGraph.jsx b/app/panel/components/BuildingBlocks/StatsGraph.jsx index e919018ea..5b9b34088 100644 --- a/app/panel/components/BuildingBlocks/StatsGraph.jsx +++ b/app/panel/components/BuildingBlocks/StatsGraph.jsx @@ -14,7 +14,7 @@ import { isEqual } from 'underscore'; import React from 'react'; import * as D3 from 'd3'; -import { ThemeContext } from '../../contexts/ThemeContext'; +import ThemeContext from '../../contexts/ThemeContext'; /** * Generates an animated graph displaying locally stored stats * @memberof PanelBuildingBlocks diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index 77055f519..dc4321cf8 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -15,8 +15,8 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; import Header from '../containers/HeaderContainer'; import PromoModalContainer from '../../shared-components/PromoModal/PromoModalContainer'; -import { ThemeContext } from '../contexts/ThemeContext'; -import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; +import ThemeContext from '../contexts/ThemeContext'; +import DynamicUIPortContext from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import { setTheme } from '../utils/utils'; diff --git a/app/panel/components/Rewards.jsx b/app/panel/components/Rewards.jsx index dee9602d6..9f8cd6c1e 100644 --- a/app/panel/components/Rewards.jsx +++ b/app/panel/components/Rewards.jsx @@ -15,7 +15,7 @@ import React, { Fragment } from 'react'; import ClassNames from 'classnames'; import { Route } from 'react-router-dom'; import { ToggleSlider } from './BuildingBlocks'; -import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; +import DynamicUIPortContext from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import globals from '../../../src/classes/Globals'; import { log } from '../../../src/utils/common'; diff --git a/app/panel/components/Settings.jsx b/app/panel/components/Settings.jsx index 52f528caa..25e613b63 100644 --- a/app/panel/components/Settings.jsx +++ b/app/panel/components/Settings.jsx @@ -24,7 +24,7 @@ import Notifications from './Settings/Notifications'; import OptIn from './Settings/OptIn'; import Purplebox from './Settings/Purplebox'; import Account from './Settings/Account'; -import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; +import DynamicUIPortContext from '../contexts/DynamicUIPortContext'; /** * @class Implement base Settings view which routes navigation to all settings subviews * @memberof PanelClasses diff --git a/app/panel/components/Summary.jsx b/app/panel/components/Summary.jsx index 8c299eca9..7f71a51fc 100644 --- a/app/panel/components/Summary.jsx +++ b/app/panel/components/Summary.jsx @@ -15,7 +15,7 @@ import React from 'react'; import { ReactSVG } from 'react-svg'; import ClassNames from 'classnames'; import Tooltip from './Tooltip'; -import { DynamicUIPortContext } from '../contexts/DynamicUIPortContext'; +import DynamicUIPortContext from '../contexts/DynamicUIPortContext'; import { sendMessage } from '../utils/msg'; import globals from '../../../src/classes/Globals'; import { diff --git a/app/panel/components/__tests__/Rewards.jsx b/app/panel/components/__tests__/Rewards.jsx index 5af0ace52..281b34088 100644 --- a/app/panel/components/__tests__/Rewards.jsx +++ b/app/panel/components/__tests__/Rewards.jsx @@ -15,7 +15,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { MemoryRouter } from 'react-router'; import Rewards from '../Rewards'; -import { DynamicUIPortContext } from '../../contexts/DynamicUIPortContext'; +import DynamicUIPortContext from '../../contexts/DynamicUIPortContext'; // Fake the translation function to only return the translation key diff --git a/app/panel/containers/DetailContainer.js b/app/panel/containers/DetailContainer.js index 6d103facf..f114667da 100644 --- a/app/panel/containers/DetailContainer.js +++ b/app/panel/containers/DetailContainer.js @@ -14,7 +14,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import Detail from '../components/Detail'; -import * as actions from '../actions/DetailActions'; +import toggleExpanded from '../actions/DetailActions'; /** * Map redux store state properties to Detailed view own properties. * @memberOf PanelContainers @@ -37,7 +37,7 @@ const mapStateToProps = state => ({ * @param {Object} ownProps Detailed view component own props * @return {function} to be used as an argument in redux connect call */ -const mapDispatchToProps = dispatch => ({ actions: bindActionCreators(actions, dispatch) }); +const mapDispatchToProps = dispatch => ({ actions: bindActionCreators({ toggleExpanded }, dispatch) }); /** * Connects Detailed view component to the Redux store. * @memberOf PanelContainers diff --git a/app/panel/containers/SettingsContainer.js b/app/panel/containers/SettingsContainer.js index 1b01a747c..b2c8c1d50 100644 --- a/app/panel/containers/SettingsContainer.js +++ b/app/panel/containers/SettingsContainer.js @@ -14,7 +14,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import Settings from '../components/Settings'; import * as settingsActions from '../actions/SettingsActions'; -import { toggleExpanded } from '../actions/DetailActions'; +import toggleExpanded from '../actions/DetailActions'; import { updateSitePolicy } from '../actions/SummaryActions'; import { sendSignal } from '../actions/RewardsActions'; /** diff --git a/app/panel/contexts/DynamicUIPortContext.js b/app/panel/contexts/DynamicUIPortContext.js index a43890801..370f0ab78 100644 --- a/app/panel/contexts/DynamicUIPortContext.js +++ b/app/panel/contexts/DynamicUIPortContext.js @@ -13,4 +13,5 @@ import React from 'react'; -export const DynamicUIPortContext = React.createContext(null); +const DynamicUIPortContext = React.createContext(null); +export default DynamicUIPortContext; diff --git a/app/panel/contexts/ThemeContext.js b/app/panel/contexts/ThemeContext.js index f47652914..92bf5e6fa 100644 --- a/app/panel/contexts/ThemeContext.js +++ b/app/panel/contexts/ThemeContext.js @@ -13,4 +13,5 @@ import React from 'react'; -export const ThemeContext = React.createContext(null); +const ThemeContext = React.createContext(null); +export default ThemeContext; diff --git a/src/background.js b/src/background.js index 21ef769f3..bef0f797c 100644 --- a/src/background.js +++ b/src/background.js @@ -50,7 +50,7 @@ import { allowAllwaysC2P } from './utils/click2play'; import * as common from './utils/common'; import * as utils from './utils/utils'; import { _getJSONAPIErrorsObject } from './utils/api'; -import { importCliqzSettings } from './utils/cliqzSettingImport'; +import importCliqzSettings from './utils/cliqzSettingImport'; import { sendCliqzModuleCounts } from './utils/cliqzModulesData'; // For debug purposes, provide Access to the internals of `browser-core` diff --git a/src/utils/cliqzSettingImport.js b/src/utils/cliqzSettingImport.js index ba3427e41..800601716 100644 --- a/src/utils/cliqzSettingImport.js +++ b/src/utils/cliqzSettingImport.js @@ -93,7 +93,7 @@ function _runCliqzSettingsImport(cliqz, c) { * @param {Object} cliqz * @param {Object} c conf */ -export function importCliqzSettings(cliqz, c) { +export default function importCliqzSettings(cliqz, c) { const conf = c; log('checking cliqz import', conf.cliqz_import_state); if (!conf.cliqz_import_state) { From a77cd1b72bba037fa83f1b933c20dfe4feb8cfac Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Fri, 8 May 2020 18:42:26 +0200 Subject: [PATCH 09/89] add linting for react/no-access-state-in-setstate and fix resulting linting errors --- .eslintrc.js | 2 +- app/licenses/License.jsx | 2 +- app/panel-android/components/content/Accordion.jsx | 6 +++--- app/panel/components/Blocking/BlockingHeader.jsx | 2 +- app/panel/components/Blocking/GlobalTracker.jsx | 2 +- app/panel/components/Blocking/Tracker.jsx | 2 +- .../components/BuildingBlocks/ToggleSlider.jsx | 4 +--- app/panel/components/DetailMenu.jsx | 10 ++++++---- app/panel/components/Header.jsx | 2 +- app/panel/components/Settings/TrustAndRestrict.jsx | 14 +++++--------- 10 files changed, 21 insertions(+), 25 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 129c144c5..44e51193a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -86,7 +86,7 @@ module.exports = { 'react/jsx-indent': [1, 'tab'], 'react/jsx-indent-props': [1, 'tab'], 'react/jsx-props-no-spreading': [0], // TODO: enable this check - 'react/no-access-state-in-setstate': [0], // TODO: enable this check + 'react/no-access-state-in-setstate': [1], 'react/no-danger': [0], 'react/prop-types': [0], 'react/jsx-fragments': [1, 'element'], diff --git a/app/licenses/License.jsx b/app/licenses/License.jsx index e6951aab5..8f2e737d2 100644 --- a/app/licenses/License.jsx +++ b/app/licenses/License.jsx @@ -33,7 +33,7 @@ class License extends React.Component { * Toggle expansion of a license full text. */ toggleLicenseText() { - this.setState({ expanded: !this.state.expanded }); + this.setState(prevState => ({ expanded: !prevState.expanded })); } /** diff --git a/app/panel-android/components/content/Accordion.jsx b/app/panel-android/components/content/Accordion.jsx index c21e8f9bc..7ec893cee 100644 --- a/app/panel-android/components/content/Accordion.jsx +++ b/app/panel-android/components/content/Accordion.jsx @@ -99,9 +99,9 @@ export default class Accordion extends React.Component { const boundingRect = accordionContentNode.getBoundingClientRect(); // Try lo load more when needed if (scrollTop + window.innerHeight - (accordionContentNode.offsetTop + boundingRect.height) > -needToUpdateHeight) { - const itemsLength = Math.min(this.state.currentItemsLength + this.nExtraItems, this.props.numTotal); - this.setState({ - currentItemsLength: itemsLength, + this.setState((prevState) => { + const itemsLength = Math.min(prevState.currentItemsLength + this.nExtraItems, this.props.numTotal); + return { currentItemsLength: itemsLength }; }); } } diff --git a/app/panel/components/Blocking/BlockingHeader.jsx b/app/panel/components/Blocking/BlockingHeader.jsx index 2147b911d..a03645407 100644 --- a/app/panel/components/Blocking/BlockingHeader.jsx +++ b/app/panel/components/Blocking/BlockingHeader.jsx @@ -186,7 +186,7 @@ class BlockingHeader extends React.Component { * @param {Object} event mouseclick event */ clickFilterText() { - this.setState({ filterMenuOpened: !this.state.filterMenuOpened }); + this.setState(prevState => ({ filterMenuOpened: !prevState.filterMenuOpened })); } /** diff --git a/app/panel/components/Blocking/GlobalTracker.jsx b/app/panel/components/Blocking/GlobalTracker.jsx index 9169c3d1a..8bade7666 100644 --- a/app/panel/components/Blocking/GlobalTracker.jsx +++ b/app/panel/components/Blocking/GlobalTracker.jsx @@ -47,7 +47,7 @@ class GlobalTracker extends React.Component { */ toggleDescription() { const { tracker } = this.props; - this.setState({ showMoreInfo: !this.state.showMoreInfo }); + this.setState(prevState => ({ showMoreInfo: !prevState.showMoreInfo })); if (this.state.description) { return; diff --git a/app/panel/components/Blocking/Tracker.jsx b/app/panel/components/Blocking/Tracker.jsx index 62871aac8..b0a73a710 100644 --- a/app/panel/components/Blocking/Tracker.jsx +++ b/app/panel/components/Blocking/Tracker.jsx @@ -82,7 +82,7 @@ class Tracker extends React.Component { */ toggleDescription() { const { tracker } = this.props; - this.setState({ showMoreInfo: !this.state.showMoreInfo }); + this.setState(prevState => ({ showMoreInfo: !prevState.showMoreInfo })); if (this.state.description) { return; diff --git a/app/panel/components/BuildingBlocks/ToggleSlider.jsx b/app/panel/components/BuildingBlocks/ToggleSlider.jsx index 1f467cbca..185e4ee5b 100644 --- a/app/panel/components/BuildingBlocks/ToggleSlider.jsx +++ b/app/panel/components/BuildingBlocks/ToggleSlider.jsx @@ -50,9 +50,7 @@ class ToggleSlider extends React.Component { if (typeof this.props.onChange === 'function') { this.props.onChange(event); } else { - this.setState({ - checked: !this.state.checked, - }); + this.setState(prevState => ({ checked: !prevState.checked })); } } diff --git a/app/panel/components/DetailMenu.jsx b/app/panel/components/DetailMenu.jsx index 123781a9e..275174694 100644 --- a/app/panel/components/DetailMenu.jsx +++ b/app/panel/components/DetailMenu.jsx @@ -41,12 +41,14 @@ class DetailMenu extends React.Component { * @param {Object} event click event */ setActiveTab(event) { - const menu = { ...this.state.menu }; const selectionId = event.currentTarget.id; - - Object.keys(menu).forEach((key) => { menu[key] = selectionId === key; }); sendMessage('ping', DetailMenu.pings[selectionId]); - this.setState({ menu }); + + this.setState((prevState) => { + const menu = { ...prevState.menu }; + Object.keys(menu).forEach((key) => { menu[key] = selectionId === key; }); + return { menu }; + }); } /** diff --git a/app/panel/components/Header.jsx b/app/panel/components/Header.jsx index eb31218f8..ac3e19448 100644 --- a/app/panel/components/Header.jsx +++ b/app/panel/components/Header.jsx @@ -65,7 +65,7 @@ class Header extends React.Component { * Handles toggling the drop-down pane open/closed */ toggleDropdown = () => { - this.setState({ dropdownOpen: !this.state.dropdownOpen }); + this.setState(prevState => ({ dropdownOpen: !prevState.dropdownOpen })); } handleSignin = () => { diff --git a/app/panel/components/Settings/TrustAndRestrict.jsx b/app/panel/components/Settings/TrustAndRestrict.jsx index 3f5805918..183092754 100644 --- a/app/panel/components/Settings/TrustAndRestrict.jsx +++ b/app/panel/components/Settings/TrustAndRestrict.jsx @@ -43,15 +43,11 @@ class TrustAndRestrict extends React.Component { */ setActivePane(event) { this.showWarning(''); - const newMenuState = { ...this.state.menu }; - Object.keys(newMenuState).forEach((key) => { - if (key === event.currentTarget.id) { - newMenuState[key] = true; - } else { - newMenuState[key] = false; - } - }); - this.setState({ menu: newMenuState }); + const menu = { + showTrustedSites: event.currentTarget.id === 'showTrustedSites', + showRestrictedSites: event.currentTarget.id === 'showRestrictedSites', + }; + this.setState({ menu }); } /** From c31dbaf10160462fe7b4df1944291e62dcca6541 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Mon, 11 May 2020 17:04:56 +0200 Subject: [PATCH 10/89] add linting for react/jsx-props-no-spreading and fix resulting linting errors --- .eslintrc.js | 2 +- app/hub/Views/AppView/AppViewContainer.jsx | 12 +- .../CreateAccountViewContainer.jsx | 38 ++-- app/hub/Views/HomeView/HomeViewContainer.jsx | 19 +- .../Views/LogInView/LogInViewContainer.jsx | 22 +-- app/hub/Views/PlusView/PlusViewContainer.jsx | 12 +- app/hub/Views/SetupView/SetupView.jsx | 2 +- .../SetupHumanWebViewContainer.jsx | 11 +- .../SetupNavigationContainer.jsx | 17 +- .../SideNavigationViewContainer.jsx | 13 +- .../TutorialNavigationContainer.jsx | 18 +- .../components/__tests__/PauseButton.jsx | 168 ++++++++++-------- app/panel/components/__tests__/Rewards.jsx | 46 ++--- .../PromoModal/PromoModal.jsx | 17 +- src/background.js | 6 +- 15 files changed, 218 insertions(+), 185 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 44e51193a..ad9fbdf82 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -85,7 +85,7 @@ module.exports = { 'react/jsx-curly-newline': [0], 'react/jsx-indent': [1, 'tab'], 'react/jsx-indent-props': [1, 'tab'], - 'react/jsx-props-no-spreading': [0], // TODO: enable this check + 'react/jsx-props-no-spreading': [1], 'react/no-access-state-in-setstate': [1], 'react/no-danger': [0], 'react/prop-types': [0], diff --git a/app/hub/Views/AppView/AppViewContainer.jsx b/app/hub/Views/AppView/AppViewContainer.jsx index 2d98688d3..8cf81288e 100644 --- a/app/hub/Views/AppView/AppViewContainer.jsx +++ b/app/hub/Views/AppView/AppViewContainer.jsx @@ -36,11 +36,13 @@ class AppViewContainer extends Component { * @return {JSX} JSX for rendering the Home View of the Hub app */ render() { - const childProps = { - ...this.props, - exitToast: this._exitToast, - }; - return ; + const { app, children } = this.props; + + return ( + + {children} + + ); } } diff --git a/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx b/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx index a495f0317..f71c46f97 100644 --- a/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx +++ b/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx @@ -172,30 +172,26 @@ class CreateAccountViewContainer extends Component { passwordInvalidError, passwordLengthError, } = this.state; - const createAccountChildProps = { - email, - emailError, - confirmEmail, - confirmEmailError, - firstName, - lastName, - legalConsentChecked, - legalConsentNotCheckedError, - password, - passwordInvalidError, - passwordLengthError, - handleInputChange: this._handleInputChange, - handleLegalConsentCheckboxChange: this._handleLegalConsentCheckboxChange, - handleSubmit: this._handleCreateAccountAttempt - }; - const signedInChildProps = { - email: (user && user.email) || email, - }; return loggedIn ? ( - + ) : ( - + ); } } diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index aa3a8ed3e..719651a5d 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -124,15 +124,6 @@ class HomeViewContainer extends Component { tutorial_complete, enable_metrics, } = home; - const childProps = { - justInstalled, - setup_complete, - tutorial_complete, - enable_metrics, - changeMetrics: this._handleToggleMetrics, - email: user ? user.email : '', - isPlus, - }; const showPromoModal = !premium_promo_modal_shown && !this._premiumSubscriber(); @@ -147,7 +138,15 @@ class HomeViewContainer extends Component { handleGetPlusClick={this._handleGetPlusClick} handleTryMidnightClick={this._handleTryMidnightClick} /> - +
); } diff --git a/app/hub/Views/LogInView/LogInViewContainer.jsx b/app/hub/Views/LogInView/LogInViewContainer.jsx index e4efeb2a3..6c5ef5859 100644 --- a/app/hub/Views/LogInView/LogInViewContainer.jsx +++ b/app/hub/Views/LogInView/LogInViewContainer.jsx @@ -128,22 +128,18 @@ class LogInViewContainer extends Component { emailError, passwordError, } = this.state; - const logInChildProps = { - email, - password, - emailError, - passwordError, - handleInputChange: this._handleInputChange, - handleSubmit: this._handleLoginAttempt, - }; - const signedInChildProps = { - email: (user && user.email) || 'email', - }; return loggedIn ? ( - + ) : ( - + ); } } diff --git a/app/hub/Views/PlusView/PlusViewContainer.jsx b/app/hub/Views/PlusView/PlusViewContainer.jsx index c3183ba44..bcd3b0d67 100644 --- a/app/hub/Views/PlusView/PlusViewContainer.jsx +++ b/app/hub/Views/PlusView/PlusViewContainer.jsx @@ -43,12 +43,12 @@ class PlusViewContainer extends Component { * @return {JSX} JSX for rendering the Plus View of the Hub app */ render() { - const childProps = { - isPlus: (this.props.user && this.props.user.subscriptionsPlus) || false, - onPlusClick: this._sendPlusPing, - }; - - return ; + return ( + + ); } } diff --git a/app/hub/Views/SetupView/SetupView.jsx b/app/hub/Views/SetupView/SetupView.jsx index 690d2b475..8db394a37 100644 --- a/app/hub/Views/SetupView/SetupView.jsx +++ b/app/hub/Views/SetupView/SetupView.jsx @@ -36,7 +36,7 @@ const SetupView = (props) => { path={step.path} render={() => (
- +
)} diff --git a/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx b/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx index 3e6415082..e6bce8b7b 100644 --- a/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx +++ b/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx @@ -58,11 +58,12 @@ class SetupHumanWebViewContainer extends Component { * @return {JSX} JSX for rendering the Setup Human Web View of the Hub app */ render() { - const childProps = { - enableHumanWeb: this.props.setup.enable_human_web, - changeHumanWeb: this._handleToggle, - }; - return ; + return ( + + ); } } diff --git a/app/hub/Views/SetupViews/SetupNavigation/SetupNavigationContainer.jsx b/app/hub/Views/SetupViews/SetupNavigation/SetupNavigationContainer.jsx index 3a55f163a..072ba9d07 100644 --- a/app/hub/Views/SetupViews/SetupNavigation/SetupNavigationContainer.jsx +++ b/app/hub/Views/SetupViews/SetupNavigation/SetupNavigationContainer.jsx @@ -22,12 +22,19 @@ import { SteppedNavigation } from '../../../../shared-components'; */ const SetupNavigationContainer = (props) => { const { totalSteps, setup } = props; - const childProps = { - totalSteps, - ...setup.navigation, - }; - return ; + return ( + + ); }; // PropTypes ensure we pass required props of the correct type diff --git a/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx b/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx index 1388a5ea7..c91e2cda3 100644 --- a/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx +++ b/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx @@ -68,13 +68,14 @@ class SideNavigationViewContainer extends Component { icon: 'profile', }, ]; - const childProps = { - menuItems, - bottomItems, - disableNav: disableRegEx.test(location.pathname), - }; - return ; + return ( + + ); } } diff --git a/app/hub/Views/TutorialViews/TutorialNavigation/TutorialNavigationContainer.jsx b/app/hub/Views/TutorialViews/TutorialNavigation/TutorialNavigationContainer.jsx index f64c4f6d1..ed57d61eb 100644 --- a/app/hub/Views/TutorialViews/TutorialNavigation/TutorialNavigationContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialNavigation/TutorialNavigationContainer.jsx @@ -22,11 +22,19 @@ import { SteppedNavigation } from '../../../../shared-components'; */ const TutorialNavigationContainer = (props) => { const { totalSteps, tutorial } = props; - const childProps = { - totalSteps, - ...tutorial.navigation, - }; - return ; + + return ( + + ); }; // PropTypes ensure we pass required props of the correct type diff --git a/app/panel/components/__tests__/PauseButton.jsx b/app/panel/components/__tests__/PauseButton.jsx index 5c7ea3660..4d89ba7b0 100644 --- a/app/panel/components/__tests__/PauseButton.jsx +++ b/app/panel/components/__tests__/PauseButton.jsx @@ -27,72 +27,80 @@ jest.mock('../Tooltip'); describe('app/panel/components/BuildingBlocks/PauseButton.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { test('unpaused state in simple view', () => { - const initialState = { - isPaused: false, - isPausedTimeout: null, - clickPause: () => {}, - dropdownItems: [ - { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, - { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, - { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, - ], - isCentered: true, - isCondensed: false, - }; - const component = renderer.create().toJSON(); + const dropdownItems = [ + { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, + { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, + { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, + ]; + const component = renderer.create( + {}} + dropdownItems={dropdownItems} + isCentered + isCondensed={false} + /> + ).toJSON(); expect(component).toMatchSnapshot(); }); test('paused state in detailed view', () => { - const initialState = { - isPaused: true, - isPausedTimeout: null, - clickPause: () => {}, - dropdownItems: [ - { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, - { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, - { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, - ], - isCentered: false, - isCondensed: false, - }; - const component = renderer.create().toJSON(); + const dropdownItems = [ + { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, + { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, + { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, + ]; + const component = renderer.create( + {}} + dropdownItems={dropdownItems} + isCentered={false} + isCondensed={false} + /> + ).toJSON(); expect(component).toMatchSnapshot(); }); test('paused state in detailed condensed view', () => { - const initialState = { - isPaused: true, - isPausedTimeout: null, - clickPause: () => {}, - dropdownItems: [ - { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, - { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, - { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, - ], - isCentered: false, - isCondensed: true, - }; - const component = renderer.create().toJSON(); + const dropdownItems = [ + { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, + { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, + { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, + ]; + const component = renderer.create( + {}} + dropdownItems={dropdownItems} + isCentered={false} + isCondensed + /> + ).toJSON(); expect(component).toMatchSnapshot(); }); }); describe('Shallow snapshot tests rendered with Enzyme', () => { test('the state of the pause button correctly when Ghostery is not paused', () => { - const initialState = { - isPaused: false, - isPausedTimeout: null, - clickPause: () => {}, - dropdownItems: [ - { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, - { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, - { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, - ], - isCentered: false, - isCondensed: false, - }; - const component = shallow(); + const dropdownItems = [ + { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, + { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, + { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, + ]; + const component = shallow( + {}} + dropdownItems={dropdownItems} + isCentered={false} + isCondensed={false} + /> + ); expect(component.find('.button').length).toBe(2); expect(component.find('.button.button-pause').length).toBe(1); expect(component.find('.button.button-pause.active').length).toBe(0); @@ -110,19 +118,21 @@ describe('app/panel/components/BuildingBlocks/PauseButton.jsx', () => { }); test('the state of the pause button correctly when Ghostery is paused', () => { - const initialState = { - isPaused: true, - isPausedTimeout: 1800000, - clickPause: () => {}, - dropdownItems: [ - { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, - { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, - { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, - ], - isCentered: true, - isCondensed: false, - }; - const component = shallow(); + const dropdownItems = [ + { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, + { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, + { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, + ]; + const component = shallow( + {}} + dropdownItems={dropdownItems} + isCentered + isCondensed={false} + /> + ); expect(component.find('.button').length).toBe(2); expect(component.find('.button.button-pause').length).toBe(1); expect(component.find('.button.button-pause.active').length).toBe(1); @@ -140,19 +150,21 @@ describe('app/panel/components/BuildingBlocks/PauseButton.jsx', () => { }); test('the pause button correctly it is centered and condensed', () => { - const initialState = { - isPaused: false, - isPausedTimeout: null, - clickPause: () => {}, - dropdownItems: [ - { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, - { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, - { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, - ], - isCentered: true, - isCondensed: true, - }; - const component = shallow(); + const dropdownItems = [ + { name: t('pause_30_min'), name_condensed: t('pause_30_min_condensed'), val: 30 }, + { name: t('pause_1_hour'), name_condensed: t('pause_1_hour_condensed'), val: 60 }, + { name: t('pause_24_hours'), name_condensed: t('pause_24_hours_condensed'), val: 1440 }, + ]; + const component = shallow( + {}} + dropdownItems={dropdownItems} + isCentered + isCondensed + /> + ); expect(component.find('.button').length).toBe(2); expect(component.find('.button.button-pause').length).toBe(1); expect(component.find('.button.button-pause.smaller').length).toBe(0); diff --git a/app/panel/components/__tests__/Rewards.jsx b/app/panel/components/__tests__/Rewards.jsx index 281b34088..13fe9fbcf 100644 --- a/app/panel/components/__tests__/Rewards.jsx +++ b/app/panel/components/__tests__/Rewards.jsx @@ -31,21 +31,22 @@ describe('app/panel/components/Rewards.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { test('rewards is rendered correctly when rewards is on and rewards is null', () => { - const initialState = { - actions: { - updateRewardsData: () => {}, - sendSignal: () => {}, - }, - location: { - pathname: '/detail/rewards/list', - }, - enable_offers: true, - is_expanded: false + const actions = { + updateRewardsData: () => {}, + sendSignal: () => {}, + }; + const location = { + pathname: '/detail/rewards/list', }; const component = renderer.create( - + ).toJSON(); @@ -53,21 +54,22 @@ describe('app/panel/components/Rewards.jsx', () => { }); test('rewards is rendered correctly when rewards is off and rewards is null', () => { - const initialState = { - actions: { - updateRewardsData: () => {}, - sendSignal: () => {}, - }, - location: { - pathname: '/detail/rewards/list', - }, - enable_offers: false, - is_expanded: false + const actions = { + updateRewardsData: () => {}, + sendSignal: () => {}, + }; + const location = { + pathname: '/detail/rewards/list', }; const component = renderer.create( - + ).toJSON(); diff --git a/app/shared-components/PromoModal/PromoModal.jsx b/app/shared-components/PromoModal/PromoModal.jsx index 603de8cae..b968cbaf2 100644 --- a/app/shared-components/PromoModal/PromoModal.jsx +++ b/app/shared-components/PromoModal/PromoModal.jsx @@ -124,7 +124,9 @@ class PromoModal extends React.Component { }; renderModalContent() { - const { type, loggedIn } = this.props; + const { + type, loggedIn, location, isPlus, handleKeepBasicClick + } = this.props; switch (type) { case INSIGHTS: return ( @@ -132,7 +134,6 @@ class PromoModal extends React.Component { handleGoAwayClick={() => this._handlePromoGoAwayClick(INSIGHTS)} handleTryInsightsClick={() => this._handlePromoTryProductClick(INSIGHTS, 'in_app_upgrade')} handleSignInClick={this._handlePromoSignInClick} - {...this.props} /> ); case PLUS: @@ -150,11 +151,19 @@ class PromoModal extends React.Component { handleGoAwayClick={() => this._handlePromoGoAwayClick(PREMIUM)} handleTryMidnightClick={() => this._handlePromoTryProductClick(PREMIUM, 'in_app')} handleGetPlusClick={() => this._handlePromoTryProductClick(PLUS, 'in_app')} - {...this.props} + handleKeepBasicClick={handleKeepBasicClick} + location={location} + isPlus={isPlus} /> ); default: - return ; + return ( + this._handlePromoGoAwayClick(INSIGHTS)} + handleTryInsightsClick={() => this._handlePromoTryProductClick(INSIGHTS, 'in_app_upgrade')} + handleSignInClick={this._handlePromoSignInClick} + /> + ); } } diff --git a/src/background.js b/src/background.js index bef0f797c..0fe321ba9 100644 --- a/src/background.js +++ b/src/background.js @@ -776,7 +776,7 @@ function onMessageHandler(request, sender, callback) { const { email, password } = message; account.login(email, password) .then((response) => { - if (!Object.property.hasOwnProperty.call(response, 'errors')) { + if (!Object.prototype.hasOwnProperty.call(response, 'errors')) { metrics.ping('sign_in_success'); } callback(response); @@ -793,7 +793,7 @@ function onMessageHandler(request, sender, callback) { } = message; account.register(email, confirmEmail, password, firstName, lastName) .then((response) => { - if (!Object.property.hasOwnProperty.call(response, 'errors')) { + if (!Object.prototype.hasOwnProperty.call(response, 'errors')) { metrics.ping('create_account_success'); } callback(response); @@ -888,7 +888,7 @@ function onMessageHandler(request, sender, callback) { if (foundUser) { foundUser.subscriptionsPlus = account.hasScopesUnverified(['subscriptions:plus']); } - callback({ foundUser }); + callback({ user: foundUser }); }) .catch((err) => { callback({ errors: _getJSONAPIErrorsObject(err) }); From 3d50aff04aeeef662eaad1d68504e2d044144f79 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Tue, 12 May 2020 15:10:31 +0200 Subject: [PATCH 11/89] finish linting errors for no-restricted-syntax. 1 remains: couldn't resolve removing iterator loops for urlSearchParams --- .eslintrc.js | 2 +- app/panel/reducers/settings.js | 7 ++++--- src/classes/Account.js | 9 ++++++--- src/classes/BugDb.js | 8 ++++++-- src/classes/Click2PlayDb.js | 6 ++++-- src/classes/PanelData.js | 8 +++++--- src/utils/cliqzModulesData.js | 6 ++++-- src/utils/common.js | 19 ------------------- src/utils/utils.js | 12 +++++++++--- test/src/Foundbugs.test.js | 2 +- test/utils/matcher.test.js | 2 ++ tools/leet/leet-en.js | 10 ++++++---- 12 files changed, 48 insertions(+), 43 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ad9fbdf82..98d8b4329 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -66,7 +66,7 @@ module.exports = { }], 'no-plusplus': [0], 'no-prototype-builtins': [1], - 'no-restricted-syntax': [0], // TODO: enable this check + 'no-restricted-syntax': [1], 'no-tabs': [0], 'no-underscore-dangle': [0], 'no-unused-vars': [1], diff --git a/app/panel/reducers/settings.js b/app/panel/reducers/settings.js index 0e5d26394..6ce7b4252 100644 --- a/app/panel/reducers/settings.js +++ b/app/panel/reducers/settings.js @@ -34,7 +34,6 @@ import { updateTrackerBlocked, updateCategoryBlocked, updateBlockAllTrackers, toggleExpandAll } from '../utils/blocking'; import { sendMessage } from '../utils/msg'; -import { objectEntries } from '../../../src/utils/common'; const initialState = { expand_all_trackers: false, @@ -195,8 +194,10 @@ const _importSettingsDialog = (state, action) => { const _importSettingsNative = (state, action) => { const { settings } = action; const updated_state = {}; - // eslint-disable-next-line prefer-const - for (let [key, value] of objectEntries(settings)) { + const settingsKeys = Object.keys(settings); + for (let i = 0; i < settingsKeys.length; i++) { + const key = settingsKeys[i]; + let value = settings[key]; if (key === 'alert_bubble_timeout') { value = (value > 30) ? 30 : value; } diff --git a/src/classes/Account.js b/src/classes/Account.js index 5140b786c..51fea7f2e 100644 --- a/src/classes/Account.js +++ b/src/classes/Account.js @@ -40,7 +40,8 @@ class Account { const opts = { errorHandler: errors => ( new Promise((resolve, reject) => { - for (const err of errors) { + for (let i = 0; i < errors.length; i++) { + const err = errors[i]; switch (err.code) { case '10020': // token is not valid case '10060': // user id does not match @@ -346,10 +347,12 @@ class Account { // check scopes if (userScopes.indexOf('god') >= 0) { return true; } - for (const sArr of required) { + for (let i = 0; i < required.length; i++) { + const sArr = required[i]; let matches = true; if (sArr.length > 0) { - for (const s of sArr) { + for (let j = 0; j < sArr.length; j++) { + const s = sArr[j]; if (userScopes.indexOf(s) === -1) { matches = false; break; diff --git a/src/classes/BugDb.js b/src/classes/BugDb.js index 1166bfa34..612974771 100644 --- a/src/classes/BugDb.js +++ b/src/classes/BugDb.js @@ -84,7 +84,9 @@ class BugDb extends Updatable { const categoryArray = []; const categories = {}; - for (appId in db.apps) { + const appIds = Object.keys(db.apps); + for (let i = 0; i < appIds.length; i++) { + appId = appIds[i]; if (Object.prototype.hasOwnProperty.call(db.apps, appId)) { category = db.apps[appId].cat; if (t(`category_${category}`) === `category_${category}`) { @@ -181,7 +183,9 @@ class BugDb extends Updatable { log('initializing bugdb regexes...'); - for (const id in regexes) { + const regexesKeys = Object.keys(regexes); + for (let i = 0; i < regexesKeys.length; i++) { + const id = regexesKeys[i]; if (Object.prototype.hasOwnProperty.call(regexes, id)) { db.patterns.regex[id] = new RegExp(regexes[id], 'i'); } diff --git a/src/classes/Click2PlayDb.js b/src/classes/Click2PlayDb.js index 09d3629d6..43d892ad5 100644 --- a/src/classes/Click2PlayDb.js +++ b/src/classes/Click2PlayDb.js @@ -68,9 +68,11 @@ class Click2PlayDb extends Updatable { reset(tab_id) { if (!Object.prototype.hasOwnProperty.call(this.allowOnceList, tab_id)) { return; } - const entries = Object.entries(this.allowOnceList[tab_id]); let keep = false; - for (const [appID, count] of entries) { + const allowKeys = Object.keys(this.allowOnceList[tab_id]); + for (let i = 0; i < allowKeys.length; i++) { + const appID = allowKeys[i]; + const count = this.allowOnceList[tab_id][appID]; const newCount = count - 1; this.allowOnceList[tab_id][appID] = newCount; if (newCount > 0) { diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index be365f17b..9b38ee70b 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -29,7 +29,7 @@ import dispatcher from './Dispatcher'; import promoModals from './PromoModals'; import { getCliqzGhosteryBugs, sendCliqzModuleCounts } from '../utils/cliqzModulesData'; import { getActiveTab, flushChromeMemoryCache, processUrl } from '../utils/utils'; -import { objectEntries, log } from '../utils/common'; +import { log } from '../utils/common'; const SYNC_SET = new Set(globals.SYNC_ARRAY); const { IS_CLIQZ } = globals; @@ -586,8 +586,10 @@ class PanelData { } // Set the conf from data - // TODO can this now be replaced by Object.entries? - for (const [key, value] of objectEntries(data)) { + const dataKeys = Object.keys(data); + for (let i = 0; i < dataKeys.length; i++) { + const key = dataKeys[i]; + const value = data[key]; if (Object.prototype.hasOwnProperty.call(conf, key) && !isEqual(conf[key], value)) { conf[key] = value; syncSetDataChanged = SYNC_SET.has(key) ? true : syncSetDataChanged; diff --git a/src/utils/cliqzModulesData.js b/src/utils/cliqzModulesData.js index b28e91a32..1e4555ad4 100644 --- a/src/utils/cliqzModulesData.js +++ b/src/utils/cliqzModulesData.js @@ -58,7 +58,8 @@ export function getCliqzData(tabId, tabHostUrl, antiTracking) { return tracker.ads; }; - for (const bug of bugsValues) { + for (let i = 0; i < bugsValues.length; i++) { + const bug = bugsValues[i]; const dataPoints = getDataPoints(bug); if (dataPoints) { totalUnsafeCount += dataPoints; @@ -66,7 +67,8 @@ export function getCliqzData(tabId, tabHostUrl, antiTracking) { } } - for (const other of othersValues) { + for (let i = 0; i < othersValues.length; i++) { + const other = othersValues[i]; let whitelisted = false; const dataPoints = getDataPoints(other); diff --git a/src/utils/common.js b/src/utils/common.js index db625b8a9..0225fc2a5 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -157,25 +157,6 @@ export function hashCode(str) { return hash; } -/** - * Generator which makes object iterable with for...of loop - * @memberOf BackgroundUtils - * - * @param {Object} object over which own enumerable properties we want to iterate - * @return {Object} Generator object - */ - -export function* objectEntries(obj) { - const propKeys = Object.keys(obj); - - for (const propKey of propKeys) { - // `yield` returns a value and then pauses - // the generator. Later, execution continues - // where it was previously paused. - yield [propKey, obj[propKey]]; - } -} - /** * Unescape base64-encoded string. * @private diff --git a/src/utils/utils.js b/src/utils/utils.js index 28002a6b6..31d10beb7 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -22,7 +22,7 @@ import { debounce } from 'underscore'; import { URL } from '@cliqz/url-parser'; import tabInfo from '../classes/TabInfo'; import globals from '../classes/Globals'; -import { log, objectEntries } from './common'; +import { log } from './common'; const { BROWSER_INFO } = globals; const IS_FIREFOX = (BROWSER_INFO.name === 'firefox'); @@ -355,7 +355,10 @@ function _fetchJson(method, url, query, extraHeaders, referrer = 'no-referrer', Accept: 'application/json' }); if (extraHeaders) { - for (const [key, value] of objectEntries(extraHeaders)) { + const extraHeadersKeys = Object.keys(extraHeaders); + for (let i = 0; i < extraHeadersKeys.length; i++) { + const key = extraHeadersKeys[i]; + const value = extraHeaders[key]; headers.append(key, value); } } @@ -448,7 +451,10 @@ function _fetchJson(method, url, query, extraHeaders, referrer = 'no-referrer', xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Accept', 'application/json'); if (extraHeaders) { - for (const [key, value] of objectEntries(extraHeaders)) { + const extraHeadersKeys = Object.keys(extraHeaders); + for (let i = 0; i < extraHeadersKeys.length; i++) { + const key = extraHeadersKeys[i]; + const value = extraHeaders[key]; xhr.setRequestHeader(key, value); } } diff --git a/test/src/Foundbugs.test.js b/test/src/Foundbugs.test.js index e9a822e71..71b0567b6 100644 --- a/test/src/Foundbugs.test.js +++ b/test/src/Foundbugs.test.js @@ -47,7 +47,7 @@ describe('src/classes/FoundBugs.js', () => { "614": {"name": "New Relic","cat": "site_analytics","tags": [48]}}, "bugs": {"2": {"aid": 13},"935": {"aid": 13},"1982": {"aid": 13},"719": {"aid": 464},"1009": {"aid": 614}}, "firstPartyExceptions": {'something': true}, - "patterns": {'something': true}, + "patterns": { regex: { 'something': true} }, "version":416 }); // Mock bugDb fetch response diff --git a/test/utils/matcher.test.js b/test/utils/matcher.test.js index a2286d925..37dfb1834 100644 --- a/test/utils/matcher.test.js +++ b/test/utils/matcher.test.js @@ -19,6 +19,7 @@ describe('src/utils/matcher.js', () => { beforeAll(done => { // Fake the XMLHttpRequest for fetchJson(/daabases/bugs.json) const bugsJson = JSON.stringify({ + "apps": {}, "firstPartyExceptions": { "101": [ "google.com/ig" @@ -32,6 +33,7 @@ describe('src/utils/matcher.js', () => { ] }, "patterns": { + "regex": {}, "host": { "com": { "gmodules": { diff --git a/tools/leet/leet-en.js b/tools/leet/leet-en.js index af946f111..2ff8dd504 100644 --- a/tools/leet/leet-en.js +++ b/tools/leet/leet-en.js @@ -62,11 +62,12 @@ const leet_convert = function(string) { // 'z': 'z', }; - let letter; let output = string || ''; output = output.replace(/cks/g, 'x'); - for (letter in characterMap) { + const characterKeys = Object.keys(characterMap); + for (let i = 0; i < characterKeys.length; i++) { + const letter = characterKeys[i]; if (Object.prototype.hasOwnProperty.call(characterMap, letter)) { output = output.replace(new RegExp(letter, 'g'), characterMap[letter]); } @@ -82,11 +83,12 @@ if (!fs.existsSync('./tools/leet/messages.en.copy.json')) { // Import the copied messages file const leet = {}; - let key; const en = jsonfile.readFileSync('./tools/leet/messages.en.copy.json'); // Create a LEETed version of the messages.json file - for (key in en) { + const enKeys = Object.keys(en); + for (let i = 0; i < enKeys.length; i++) { + const key = enKeys[i]; if (Object.prototype.hasOwnProperty.call(en[key], 'message')) { const message = leet_convert(en[key].message); const { placeholders } = en[key]; From 0988a293828b4815c64c41867d912ba9d428ee45 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Tue, 12 May 2020 17:26:43 +0200 Subject: [PATCH 12/89] Fix linting errors resulting from the merge with develop --- app/content-scripts/click_to_play.js | 6 ++++-- src/utils/click2play.js | 15 ++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/content-scripts/click_to_play.js b/app/content-scripts/click_to_play.js index 9f463b637..7a41c9c82 100644 --- a/app/content-scripts/click_to_play.js +++ b/app/content-scripts/click_to_play.js @@ -161,8 +161,10 @@ const Click2PlayContentScript = (function(win, doc) { if (name === 'c2p') { if (message) { // Dequeue C2P data stored while the script injection was taking place - for (const app_id in message) { - if (message.hasOwnProperty(app_id)) { + const messageKeys = Object.keys(message); + for (let i = 0; i < messageKeys.length; i++) { + const app_id = messageKeys[i]; + if (Object.prototype.hasOwnProperty.call(message, app_id)) { applyC2P(app_id, message[app_id].data, message[app_id].html); delete message[app_id]; } diff --git a/src/utils/click2play.js b/src/utils/click2play.js index 46f601b01..7e6b8d94a 100644 --- a/src/utils/click2play.js +++ b/src/utils/click2play.js @@ -102,13 +102,14 @@ export function buildC2P(details, app_id) { case 'none': tabInfo.setTabInfo(tab_id, 'c2pStatus', 'loading'); // Push current C2P data into existing queue - if (!tab.c2pQueue.hasOwnProperty(app_id)) { - tabInfo.setTabInfo(tab_id, 'c2pQueue', Object.assign({}, tab.c2pQueue, { + if (!Object.prototype.hasOwnProperty.call(tab.c2pQueue, app_id)) { + tabInfo.setTabInfo(tab_id, 'c2pQueue', { + ...tab.c2pQueue, [app_id]: { data: c2pApp, html: c2pHtml } - })); + }); } // Scripts injected at document_idle are guaranteed to run after the DOM is complete injectScript(tab_id, 'dist/click_to_play.js', '', 'document_idle').then(() => { @@ -122,13 +123,14 @@ export function buildC2P(details, app_id) { break; case 'loading': // Push C2P data to a holding queue until click_to_play.js has finished loading on the page - if (!tab.c2pQueue.hasOwnProperty(app_id)) { - tabInfo.setTabInfo(tab_id, 'c2pQueue', Object.assign({}, tab.c2pQueue, { + if (!Object.prototype.hasOwnProperty.call(tab.c2pQueue, app_id)) { + tabInfo.setTabInfo(tab_id, 'c2pQueue', { + ...tab.c2pQueue, [app_id]: { data: c2pApp, html: c2pHtml } - })); + }); } break; case 'done': @@ -194,7 +196,6 @@ export function allowAllwaysC2P(app_id, tab_host) { // Remove fron site-specific-blocked if (Object.prototype.hasOwnProperty.call(conf.site_specific_blocks, tab_host) && conf.site_specific_blocks[tab_host].includes(+app_id)) { - const index = conf.site_specific_blocks[tab_host].indexOf(+app_id); const { site_specific_blocks } = conf; site_specific_blocks[tab_host].splice(0, 1); conf.site_specific_blocks = site_specific_blocks; From b55be217dc10b1f9db929ca84b97ef8466f82176 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Thu, 14 May 2020 13:58:57 +0200 Subject: [PATCH 13/89] Refactor UNSAFE_componentWillMount into either constructor or componentDidMount, leave notes for how decision was made. --- app/panel/components/Blocking.jsx | 72 +++++++++++++------ app/panel/components/Blocking/Tracker.jsx | 16 ++++- app/panel/components/Detail.jsx | 26 +++++-- app/panel/components/Settings.jsx | 25 +++++-- .../components/Settings/GeneralSettings.jsx | 24 ++++++- 5 files changed, 126 insertions(+), 37 deletions(-) diff --git a/app/panel/components/Blocking.jsx b/app/panel/components/Blocking.jsx index 07c9b2e30..684cda55a 100644 --- a/app/panel/components/Blocking.jsx +++ b/app/panel/components/Blocking.jsx @@ -26,23 +26,31 @@ import { updateSummaryBlockingCount } from '../utils/blocking'; class Blocking extends React.Component { static contextType = DynamicUIPortContext; + /** + * Refactoring UNSAFE_componentWillMount into Constructor + * Stats: + * Constructor runtime before refactor: 0.038ms + * Constructor + UNSAFE_componentWillMount runtime before refactor: 0.333ms + * Constructor runtime after refactor: 0.129ms + * + * Notes: + * calling buildBlockingClasses and computeSiteNotScanned in the constructor takes 0.018ms. + * + * Conclusion: Refactor using constructor as the added computation is minimal + */ constructor(props) { super(props); - this.state = { - blockingClasses: '', - disableBlocking: false, - }; - + // event bindings this.handlePortMessage = this.handlePortMessage.bind(this); - } - /** - * Lifecycle event - */ - UNSAFE_componentWillMount() { - this.updateBlockingClasses(this.props); - this.updateSiteNotScanned(this.props); + const classes = Blocking.buildBlockingClasses(this.props); + const disableBlocking = Blocking.computeSiteNotScanned(this.props); + + this.state = { + blockingClasses: classes.join(' '), + disableBlocking + }; } /** @@ -223,31 +231,55 @@ class Blocking extends React.Component { } /** - * Set dynamic classes on .blocking-trackers. Set state. + * Build dynamic classes on .blocking-trackers. Return classes * @param {Object} props */ - updateBlockingClasses(props) { + static buildBlockingClasses(props) { const classes = []; classes.push((props.toggle_individual_trackers) ? 'show-individual' : ''); classes.push((props.paused_blocking) ? 'paused' : ''); classes.push((props.sitePolicy) ? (props.sitePolicy === 2) ? 'trusted' : 'restricted' : ''); - this.setState({ blockingClasses: classes.join(' ') }); + return classes; } /** - * Disable controls for a site that cannot be scanned by Ghostery. Set state. + * Set dynamic classes on .blocking-trackers. Set state. + * @param {Object} props + */ + updateBlockingClasses(props) { + const classes = Blocking.buildBlockingClasses(props); + const blockingClasses = classes.join(' '); + + if (this.state.blockingClasses !== blockingClasses) { + this.setState({ blockingClasses }); + } + } + + /** + * Compute whether a site cannot be scanned by Ghostery. * @param {Object} props nextProps */ - updateSiteNotScanned(props) { + static computeSiteNotScanned(props) { const { siteNotScanned, categories } = props; const pageUrl = props.pageUrl || ''; if (siteNotScanned || !categories || pageUrl.search('http') === -1) { - this.setState({ disableBlocking: true }); - } else { - this.setState({ disableBlocking: false }); + return true; + } + return false; + } + + /** + * Disable controls for a site that cannot be scanned by Ghostery. Set state. + * @param {Object} props nextProps + */ + updateSiteNotScanned(props) { + const disableBlocking = Blocking.computeSiteNotScanned(props); + + if (this.state.disableBlocking !== disableBlocking) { + this.setState({ disableBlocking }); } } diff --git a/app/panel/components/Blocking/Tracker.jsx b/app/panel/components/Blocking/Tracker.jsx index b0a73a710..c45e97a18 100644 --- a/app/panel/components/Blocking/Tracker.jsx +++ b/app/panel/components/Blocking/Tracker.jsx @@ -29,6 +29,20 @@ import { renderKnownTrackerButtons, renderUnknownTrackerButtons } from './tracke class Tracker extends React.Component { static contextType = ThemeContext; + /** + * Refactoring UNSAFE_componentWillMount into Constructor + * Stats: + * Constructor runtime before refactor: 0.037ms + * Constructor + UNSAFE_componentWillMount runtime before refactor: 0.415ms + * Constructor runtime after refactor: 0.215ms + * + * Refactoring UNSAFE_componentWillMount into componentDidMount + * Stats: + * Constructor runtime after refactor: 0.020ms + * Constructor + componentDidMount runtime after refactor: 14.205ms + * + * Conclusion: Refactor using componentDidMount + */ constructor(props) { super(props); this.state = { @@ -50,7 +64,7 @@ class Tracker extends React.Component { /** * Lifecycle event. */ - UNSAFE_componentWillMount() { + componentDidMount() { this.updateTrackerClasses(this.props.tracker); } diff --git a/app/panel/components/Detail.jsx b/app/panel/components/Detail.jsx index dfb9bf494..e694aeeb9 100644 --- a/app/panel/components/Detail.jsx +++ b/app/panel/components/Detail.jsx @@ -22,22 +22,34 @@ import Rewards from '../containers/RewardsContainer'; * @memberOf PanelClasses */ class Detail extends React.Component { + /** + * Refactoring UNSAFE_componentWillMount into Constructor + * Stats: + * Constructor runtime before refactor: 0.085ms + * Constructor + UNSAFE_componentWillMount runtime before refactor: 0.345ms + * Constructor runtime after refactor: 0.163ms + * + * Refactoring UNSAFE_componentWillMount into componentDidMount + * Stats: + * Constructor runtime with no componentDidMount: 0.163ms + * Constructor runtime with componentDidMount: 0.078ms + * Constructor + componentDidMount runtime: 8.313ms + * Notes: + * Noticably slower when refactoring using componentDidMount + * + * Conclusion: Refactor using constructor + */ constructor(props) { super(props); // event bindings this.toggleExpanded = this.toggleExpanded.bind(this); - } - /** - * Lifecycle event - */ - UNSAFE_componentWillMount() { // set default tab / route based on how we got to this view: // did the user click the Rewards icon? Or the donut number / Detailed View tab in the header? - const location = this.props.history.location.pathname; + const location = props.history.location.pathname; if (!location.includes('rewards')) { - this.props.history.push('/detail/blocking'); + props.history.push('/detail/blocking'); } } diff --git a/app/panel/components/Settings.jsx b/app/panel/components/Settings.jsx index 25e613b63..db5d63722 100644 --- a/app/panel/components/Settings.jsx +++ b/app/panel/components/Settings.jsx @@ -32,6 +32,22 @@ import DynamicUIPortContext from '../contexts/DynamicUIPortContext'; class Settings extends React.Component { static contextType = DynamicUIPortContext; + /** + * Refactoring UNSAFE_componentWillMount into Constructor + * Stats: + * Constructor runtime before refactor: 0.145ms + * Constructor + UNSAFE_componentWillMount runtime before refactor: 0.330ms + * Constructor runtime after refactor: 0.144ms + * + * Refactoring UNSAFE_componentWillMount into componentDidMount + * Stats: + * Constructor runtime with no componentDidMount: 0.123ms + * Constructor runtime with componentDidMount: 0.147ms + * + * Notes: Negligible difference using componentDidMount. + * + * Conclusion: Refactor using constructor to avoid re-render + */ constructor(props) { super(props); this.state = { @@ -45,15 +61,10 @@ class Settings extends React.Component { this.showToast = this.showToast.bind(this); this.hideToast = this.hideToast.bind(this); this.handlePortMessage = this.handlePortMessage.bind(this); - } - /** - * Lifecycle event. Default sub view is set here. - */ - UNSAFE_componentWillMount() { // Do not redirect to the default if we are trying to access a specific other subview - if (this.props.history[this.props.history.length - 1] === '/settings') { - this.props.history.push('/settings/globalblocking'); + if (props.history[props.history.length - 1] === '/settings') { + props.history.push('/settings/globalblocking'); } } diff --git a/app/panel/components/Settings/GeneralSettings.jsx b/app/panel/components/Settings/GeneralSettings.jsx index 67b7099b2..1065d5fbd 100644 --- a/app/panel/components/Settings/GeneralSettings.jsx +++ b/app/panel/components/Settings/GeneralSettings.jsx @@ -19,6 +19,23 @@ import moment from 'moment/min/moment-with-locales.min'; * @memberOf SettingsComponents */ class GeneralSettings extends React.Component { + /** + * Refactoring UNSAFE_componentWillMount into Constructor + * Stats: + * Constructor runtime before refactor: 0.026ms + * Constructor + UNSAFE_componentWillMount runtime before refactor: 2.410ms + * Constructor runtime after refactor: 1.631ms + * + * Refactoring UNSAFE_componentWillMount into componentDidMount + * Stats: + * Constructor runtime with no componentDidMount: 0.208ms + * Constructor runtime with componentDidMount: 0.074ms + * + * Notes: + * updateDbLastUpdated takes ~2ms to run the firt time and then 0.139ms subsequent times. + * + * Conclusion: Refactor using componentDidMount as to not do computations in the constructor + */ constructor(props) { super(props); this.state = { @@ -32,7 +49,7 @@ class GeneralSettings extends React.Component { /** * Lifecycle event. */ - UNSAFE_componentWillMount() { + componentDidMount() { this.updateDbLastUpdated(this.props); } @@ -56,7 +73,10 @@ class GeneralSettings extends React.Component { */ updateDbLastUpdated(props) { moment.locale(props.language).toLowerCase().replace('_', '-'); - this.setState({ dbLastUpdated: moment(props.bugs_last_updated).format('LLL') }); + const dbLastUpdated = moment(props.bugs_last_updated).format('LLL'); + if (this.state.dbLastUpdated !== dbLastUpdated) { + this.setState({ dbLastUpdated }); + } } /** From 241747cc53d268c1d8c0a3b929f42d7549ab1cf6 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Mon, 18 May 2020 17:32:26 +0200 Subject: [PATCH 14/89] Refactor UNSAFE_componentWillReceiveProps to componentDidUpdate or getDerivedStateFromProps --- app/panel/components/Blocking.jsx | 7 +-- .../components/Blocking/BlockingHeader.jsx | 32 ++++++++---- app/panel/components/Blocking/Category.jsx | 43 +++++++++++----- app/panel/components/Blocking/Tracker.jsx | 23 +++++++-- .../components/BuildingBlocks/DonutGraph.jsx | 36 ++++++------- .../BuildingBlocks/RadioButtonGroup.jsx | 2 +- .../BuildingBlocks/ToggleSlider.jsx | 9 ++-- .../components/Settings/GeneralSettings.jsx | 32 +++++++++--- app/panel/components/Stats.jsx | 14 +++-- app/panel/components/Subscription.jsx | 4 +- app/panel/components/Summary.jsx | 51 +++++++++++++------ 11 files changed, 169 insertions(+), 84 deletions(-) diff --git a/app/panel/components/Blocking.jsx b/app/panel/components/Blocking.jsx index 684cda55a..72edbbb8b 100644 --- a/app/panel/components/Blocking.jsx +++ b/app/panel/components/Blocking.jsx @@ -65,9 +65,10 @@ class Blocking extends React.Component { /** * Lifecycle event */ - UNSAFE_componentWillReceiveProps(nextProps) { - this.updateBlockingClasses(nextProps); - this.updateSiteNotScanned(nextProps); + static getDerivedStateFromProps(nextProps) { + const blockingClasses = Blocking.buildBlockingClasses(nextProps).join(' '); + const disableBlocking = Blocking.computeSiteNotScanned(nextProps); + return { blockingClasses, disableBlocking }; } /** diff --git a/app/panel/components/Blocking/BlockingHeader.jsx b/app/panel/components/Blocking/BlockingHeader.jsx index a03645407..19366709b 100644 --- a/app/panel/components/Blocking/BlockingHeader.jsx +++ b/app/panel/components/Blocking/BlockingHeader.jsx @@ -51,7 +51,15 @@ class BlockingHeader extends React.Component { */ componentDidMount() { if (this.props.categories) { - this.updateBlockAll(this.props.categories); + const updates = BlockingHeader.updateBlockAll(this.props.categories, this.state.fromHere); + if (updates) { + const { + allBlocked, + fromHere, + filtered + } = updates; + this.setState({ allBlocked, fromHere, filtered }); + } } if (typeof this.props.actions.updateTrackerCounts === 'function') { @@ -63,18 +71,19 @@ class BlockingHeader extends React.Component { /** * Lifecycle event + * Refactor Notes: + * Refactor UNSAFE_componentWillReceiveProps using getDerivedStateFromProps + * because we are only manipulating state. */ - UNSAFE_componentWillReceiveProps(nextProps) { - if (nextProps.categories) { - this.updateBlockAll(nextProps.categories); - } + static getDerivedStateFromProps(prevProps, prevState) { + return BlockingHeader.updateBlockAll(prevProps.categories, prevState.fromHere); } /** - * Set appropriate initial text ("Block All" or "Unblock All") in Blocking header - * when Blocking or Global Blocking view opens. Save 'allBlocked' property in state. + * Get appropriate initial text ("Block All" or "Unblock All") in Blocking header + * when Blocking or Global Blocking view opens. Return object to set in state. */ - updateBlockAll(categories) { + static updateBlockAll(categories, fromHere) { if (categories) { let totalShown = 0; let totalBlocked = 0; @@ -92,14 +101,15 @@ class BlockingHeader extends React.Component { } }); }); - if (this.state.fromHere || totalShown === totalBlocked || totalBlocked === 0) { - this.setState({ + if (fromHere || totalShown === totalBlocked || totalBlocked === 0) { + return { allBlocked: (totalShown === totalBlocked), fromHere: false, filtered - }); + }; } } + return null; } /** diff --git a/app/panel/components/Blocking/Category.jsx b/app/panel/components/Blocking/Category.jsx index 29905d940..14296aa23 100644 --- a/app/panel/components/Blocking/Category.jsx +++ b/app/panel/components/Blocking/Category.jsx @@ -47,17 +47,34 @@ class Category extends React.Component { */ componentDidMount() { if (this.props.category) { - this.updateCategoryCheckbox(this.props.category); + const { + allShownBlocked, + totalShownBlocked, + } = Category.updateCategoryCheckbox(this.props.category); + this.setState({ allShownBlocked, totalShownBlocked }); } } /** - * Lifecycle event. When props changed we save in state new values - * to ensure correct rendering. + * Lifecycle event */ - UNSAFE_componentWillReceiveProps(nextProps) { - this.updateCategoryExpanded(nextProps.expandAll); - this.updateCategoryCheckbox(nextProps.category); + componentDidUpdate(prevProps) { + this.updateCategoryExpanded(prevProps); + } + + /** + * Lifecycle event. + */ + static getDerivedStateFromProps(prevProps) { + const { + allShownBlocked, + totalShownBlocked, + } = Category.updateCategoryCheckbox(prevProps.category); + + return { + allShownBlocked, + totalShownBlocked + }; } /** @@ -85,7 +102,7 @@ class Category extends React.Component { * Called in lifecycle events. * @param {Object} category object containg the list of tracker objects */ - updateCategoryCheckbox(category) { + static updateCategoryCheckbox(category) { let totalShownBlocked = 0; let allShownBlocked = true; let shownCount = 0; @@ -99,10 +116,10 @@ class Category extends React.Component { }); allShownBlocked = (shownCount === totalShownBlocked); - this.setState({ + return { allShownBlocked, totalShownBlocked, - }); + }; } /** @@ -151,9 +168,11 @@ class Category extends React.Component { * Called in lifecycle events. * @param {boolean} global expanded state */ - updateCategoryExpanded(expandAll) { - if (expandAll !== this.props.expandAll && expandAll !== this.state.isExpanded) { - this.setState({ isExpanded: expandAll }); + updateCategoryExpanded(prevProps) { + if (this.props.expandAll !== prevProps.expandAll && this.props.expandAll !== this.state.isExpanded) { + this.setState({ + isExpanded: this.props.expandAll + }); } } diff --git a/app/panel/components/Blocking/Tracker.jsx b/app/panel/components/Blocking/Tracker.jsx index c45e97a18..aa61ac3b0 100644 --- a/app/panel/components/Blocking/Tracker.jsx +++ b/app/panel/components/Blocking/Tracker.jsx @@ -71,8 +71,8 @@ class Tracker extends React.Component { /** * Lifecycle event. */ - UNSAFE_componentWillReceiveProps(nextProps) { - this.updateTrackerClasses(nextProps.tracker); + static getDerivedStateFromProps(nextProps) { + return Tracker.computeTrackerClasses(nextProps.tracker); } /** @@ -122,10 +122,23 @@ class Tracker extends React.Component { } /** - * Set dynamic classes on .blocking-trk and save it in state. + * Set dynamic classes on .blocking-trk to state. * @param {Object} tracker tracker object */ updateTrackerClasses(tracker) { + const { + trackerClasses, + warningImageTitle + } = Tracker.computeTrackerClasses(tracker); + + this.setState({ trackerClasses, warningImageTitle }); + } + + /** + * Compute dynamic classes on .blocking-trk and return it as an object. + * @param {Object} tracker tracker object + */ + static computeTrackerClasses(tracker) { const classes = []; let updated_title = ''; @@ -155,10 +168,10 @@ class Tracker extends React.Component { updated_title = t('panel_tracker_warning_slow_tooltip'); } - this.setState({ + return { trackerClasses: classes.join(' '), warningImageTitle: updated_title, - }); + }; } /** diff --git a/app/panel/components/BuildingBlocks/DonutGraph.jsx b/app/panel/components/BuildingBlocks/DonutGraph.jsx index dec7cb5da..a7edc2e1a 100644 --- a/app/panel/components/BuildingBlocks/DonutGraph.jsx +++ b/app/panel/components/BuildingBlocks/DonutGraph.jsx @@ -100,7 +100,7 @@ class DonutGraph extends React.Component { /** * Lifecycle event */ - UNSAFE_componentWillReceiveProps(nextProps) { + componentDidUpdate(prevProps) { const { categories, adBlock, @@ -109,33 +109,33 @@ class DonutGraph extends React.Component { renderGreyscale, ghosteryFeatureSelect, isSmall - } = this.props; + } = prevProps; - if (isSmall !== nextProps.isSmall || - renderRedscale !== nextProps.renderRedscale || - renderGreyscale !== nextProps.renderGreyscale || - ghosteryFeatureSelect !== nextProps.ghosteryFeatureSelect + if (isSmall !== this.props.isSmall || + renderRedscale !== this.props.renderRedscale || + renderGreyscale !== this.props.renderGreyscale || + ghosteryFeatureSelect !== this.props.ghosteryFeatureSelect ) { - this.prepareDonutContainer(nextProps.isSmall); - this.nextPropsDonut(nextProps); + this.prepareDonutContainer(this.props.isSmall); + this.nextPropsDonut(this.props); return; } // componentWillReceiveProps gets called many times during page load as new trackers or unsafe data points are found // so only compare tracker totals if we don't already have to redraw anyway as a result of the cheaper checks above - const trackerTotal = categories.reduce((total, category) => total + category.num_total, 0); - const nextTrackerTotal = nextProps.categories.reduce((total, category) => total + category.num_total, 0); - if (trackerTotal !== nextTrackerTotal) { - this.nextPropsDonut(nextProps); + const prevTrackerTotal = categories.reduce((total, category) => total + category.num_total, 0); + const trackerTotal = this.props.categories.reduce((total, category) => total + category.num_total, 0); + if (prevTrackerTotal !== trackerTotal) { + this.nextPropsDonut(this.props); return; } - if (!antiTracking.unknownTrackerCount && !nextProps.antiTracking.unknownTrackerCount - && !adBlock.unknownTrackerCount && !nextProps.adBlock.unknownTrackerCount) { return; } - const unknownDataPoints = antiTracking.unknownTrackerCount + adBlock.unknownTrackerCount; - const nextUnknownDataPoints = nextProps.antiTracking.unknownTrackerCount + nextProps.adBlock.unknownTrackerCount; - if (unknownDataPoints !== nextUnknownDataPoints) { - this.nextPropsDonut(nextProps); + if (!antiTracking.unknownTrackerCount && !this.props.antiTracking.unknownTrackerCount + && !adBlock.unknownTrackerCount && !this.props.adBlock.unknownTrackerCount) { return; } + const prevUnknownDataPoints = antiTracking.unknownTrackerCount + adBlock.unknownTrackerCount; + const unknownDataPoints = this.props.antiTracking.unknownTrackerCount + this.props.adBlock.unknownTrackerCount; + if (prevUnknownDataPoints !== unknownDataPoints) { + this.nextPropsDonut(this.props); } } diff --git a/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx b/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx index 75363b5e2..c0d95edf1 100644 --- a/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx +++ b/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx @@ -31,7 +31,7 @@ const RadioButtonGroup = (props) => { )); const buttons = props.labels.map((label, index) => ( -
+
handleItemClick(index)} diff --git a/app/panel/components/BuildingBlocks/ToggleSlider.jsx b/app/panel/components/BuildingBlocks/ToggleSlider.jsx index 185e4ee5b..f3eabe62a 100644 --- a/app/panel/components/BuildingBlocks/ToggleSlider.jsx +++ b/app/panel/components/BuildingBlocks/ToggleSlider.jsx @@ -35,10 +35,11 @@ class ToggleSlider extends React.Component { /** * Lifecycle event */ - UNSAFE_componentWillReceiveProps(nextProps) { - this.setState({ - checked: nextProps.isChecked, - }); + static getDerivedStateFromProps(prevProps, prevState) { + if (prevState.checked !== prevProps.isChecked) { + return { checked: prevProps.isChecked }; + } + return null; } /** diff --git a/app/panel/components/Settings/GeneralSettings.jsx b/app/panel/components/Settings/GeneralSettings.jsx index 1065d5fbd..b97460633 100644 --- a/app/panel/components/Settings/GeneralSettings.jsx +++ b/app/panel/components/Settings/GeneralSettings.jsx @@ -50,14 +50,19 @@ class GeneralSettings extends React.Component { * Lifecycle event. */ componentDidMount() { - this.updateDbLastUpdated(this.props); + this.updateDbLastUpdated(this.props.settingsData); } /** * Lifecycle event. */ - UNSAFE_componentWillReceiveProps(nextProps) { - this.updateDbLastUpdated(nextProps); + static getDerivedStateFromProps(prevProps, prevState) { + const dbLastUpdated = GeneralSettings.getDbLastUpdated(prevProps.settingsData); + + if (dbLastUpdated && dbLastUpdated !== prevState.dbLastUpdated) { + return { dbLastUpdated }; + } + return null; } /** @@ -67,14 +72,25 @@ class GeneralSettings extends React.Component { this.props.actions.updateDatabase(); } + /** + * Get DB check timestamp and return it. + * @param {Object} settingsData + */ + static getDbLastUpdated(settingsData) { + const { language, bugs_last_updated } = settingsData; + moment.locale(language).toLowerCase().replace('_', '-'); + const dbLastUpdated = moment(bugs_last_updated).format('LLL'); + return dbLastUpdated; + } + /** * Update DB check timestamp and save it in state. - * @param {Object} props + * @param {Object} settingsData */ - updateDbLastUpdated(props) { - moment.locale(props.language).toLowerCase().replace('_', '-'); - const dbLastUpdated = moment(props.bugs_last_updated).format('LLL'); - if (this.state.dbLastUpdated !== dbLastUpdated) { + updateDbLastUpdated(settingsData) { + const dbLastUpdated = GeneralSettings.getDbLastUpdated(settingsData); + + if (dbLastUpdated && dbLastUpdated !== this.state.dbLastUpdated) { this.setState({ dbLastUpdated }); } } diff --git a/app/panel/components/Stats.jsx b/app/panel/components/Stats.jsx index 18a06d8a6..16e685149 100644 --- a/app/panel/components/Stats.jsx +++ b/app/panel/components/Stats.jsx @@ -42,14 +42,14 @@ class Stats extends React.Component { /** * Lifecycle event */ - UNSAFE_componentWillReceiveProps(nextProps) { - const nextPlus = this._isPlus(nextProps); + componentDidUpdate(prevProps) { const thisPlus = this._isPlus(this.props); - if (nextPlus !== thisPlus) { - if (nextPlus) { + const prevPlus = this._isPlus(prevProps); + if (thisPlus !== prevPlus) { + if (thisPlus) { this._init(); } else { - this.setState(this._reset(true)); + this._resetState(); } } } @@ -290,6 +290,10 @@ class Stats extends React.Component { this.props.history.push('/login'); } + _resetState = () => { + this.setState(this._reset(true)); + } + _reset = (demo) => { const demoData = [ { date: '2018-12-28', amount: 300, index: 0 }, diff --git a/app/panel/components/Subscription.jsx b/app/panel/components/Subscription.jsx index d39afad65..718f0e2af 100644 --- a/app/panel/components/Subscription.jsx +++ b/app/panel/components/Subscription.jsx @@ -44,8 +44,8 @@ class Subscription extends React.Component { /** * Lifecycle event. */ - UNSAFE_componentWillReceiveProps = (nextProps) => { - if (!nextProps.loggedIn) { + componentDidUpdate = () => { + if (!this.props.loggedIn) { this.props.history.push('/detail'); } } diff --git a/app/panel/components/Summary.jsx b/app/panel/components/Summary.jsx index 7f71a51fc..f247a6ec9 100644 --- a/app/panel/components/Summary.jsx +++ b/app/panel/components/Summary.jsx @@ -83,12 +83,14 @@ class Summary extends React.Component { /** * Lifecycle event */ - UNSAFE_componentWillReceiveProps(nextProps) { - this._setTrackerLatency(nextProps); - this._updateSiteNotScanned(nextProps); + static getDerivedStateFromProps(nextProps) { + const trackerLatencyTotal = Summary._computeTrackerLatency(nextProps); + const disableBlocking = Summary._computeSiteNotScanned(nextProps); // Set page title for Firefox for Android - window.document.title = `Ghostery's findings for ${this.props.pageUrl}`; + window.document.title = `Ghostery's findings for ${nextProps.pageUrl}`; + + return { trackerLatencyTotal, disableBlocking }; } /** @@ -246,12 +248,11 @@ class Summary extends React.Component { } } - /** * Calculates total tracker latency and sets it to state * @param {Object} props Summary's props, either this.props or nextProps. */ - _setTrackerLatency(props) { + static _computeTrackerLatency(props) { const { performanceData } = props; let pageLatency = 0; @@ -268,26 +269,46 @@ class Summary extends React.Component { } else if (unfixedLatency < 10 && unfixedLatency >= 0) { // < 10s use two decimals pageLatency = unfixedLatency.toFixed(2); } - this.setState({ trackerLatencyTotal: pageLatency }); - // reset page load value if page is reloaded while panel is open - } else if (this.props.performanceData && !performanceData) { - this.setState({ trackerLatencyTotal: pageLatency }); + return pageLatency; + // reset page load value if page is reloaded while panel is open } + return null; } /** - * Disable controls when Ghostery cannot or has not yet scanned a page. + * Calculates total tracker latency and sets it to state * @param {Object} props Summary's props, either this.props or nextProps. */ - _updateSiteNotScanned(props) { + _setTrackerLatency(props) { + const trackerLatencyTotal = Summary._computeTrackerLatency(props); + const { performanceData } = props; + + if (performanceData || this.props.performanceData !== performanceData) { + this.setState({ trackerLatencyTotal }); + } + } + + /** + * Compute whether controls should be disabled. + * @param {Object} props Summary's props, either this.props or nextProps. + */ + static _computeSiteNotScanned(props) { const { siteNotScanned, categories } = props; const pageUrl = props.pageUrl || ''; if (siteNotScanned || !categories || pageUrl.search(/http|chrome-extension|moz-extension|ms-browser-extension|newtab|chrome:\/\/startpage\//) === -1) { - this.setState({ disableBlocking: true }); - } else { - this.setState({ disableBlocking: false }); + return true; } + return false; + } + + /** + * Disable controls when Ghostery cannot or has not yet scanned a page. + * @param {Object} props Summary's props, either this.props or nextProps. + */ + _updateSiteNotScanned(props) { + const disableBlocking = Summary._computeSiteNotScanned(props); + this.setState({ disableBlocking }); } /** From eb413fbe5807a2bcaf2e2fa4d47edd5872c0d157 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Wed, 20 May 2020 11:00:38 +0200 Subject: [PATCH 15/89] re-enable lint exception for no-prototype-builtins and revert calls back to hasOwnProperty --- .eslintrc.js | 4 ++-- app/content-scripts/click_to_play.js | 2 +- .../components/content/FixedMenu.jsx | 2 +- app/panel/reducers/blocking.js | 2 +- app/panel/reducers/panel.js | 6 ++--- app/panel/utils/blocking.js | 16 +++++++------- src/background.js | 14 ++++++------ src/classes/ABTest.js | 2 +- src/classes/BugDb.js | 16 +++++++------- src/classes/Click2PlayDb.js | 8 +++---- src/classes/CompatibilityDb.js | 2 +- src/classes/ConfData.js | 4 ++-- src/classes/FoundBugs.js | 18 +++++++-------- src/classes/Latency.js | 4 ++-- src/classes/PanelData.js | 14 ++++++------ src/classes/Policy.js | 8 +++---- src/classes/PolicySmartBlock.js | 6 ++--- src/classes/SurrogateDb.js | 22 +++++++++---------- src/classes/TabInfo.js | 10 ++++----- src/classes/Updatable.js | 2 +- src/utils/click2play.js | 8 +++---- src/utils/common.js | 4 ++-- src/utils/matcher.js | 8 +++---- tools/i18n-checker.js | 18 +++++++-------- tools/leet/leet-en.js | 4 ++-- tools/transifex.js | 4 ++-- 26 files changed, 104 insertions(+), 104 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 98d8b4329..0cca44a35 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -65,7 +65,7 @@ module.exports = { ] }], 'no-plusplus': [0], - 'no-prototype-builtins': [1], + 'no-prototype-builtins': [0], 'no-restricted-syntax': [1], 'no-tabs': [0], 'no-underscore-dangle': [0], @@ -80,7 +80,7 @@ module.exports = { 'import/prefer-default-export': [1], // Plugin: React - 'react/destructuring-assignment': [0], + 'react/destructuring-assignment': [0], // ToDo 'react/static-property-placement': [0], 'react/jsx-curly-newline': [0], 'react/jsx-indent': [1, 'tab'], diff --git a/app/content-scripts/click_to_play.js b/app/content-scripts/click_to_play.js index 7a41c9c82..9d3e6cbd5 100644 --- a/app/content-scripts/click_to_play.js +++ b/app/content-scripts/click_to_play.js @@ -164,7 +164,7 @@ const Click2PlayContentScript = (function(win, doc) { const messageKeys = Object.keys(message); for (let i = 0; i < messageKeys.length; i++) { const app_id = messageKeys[i]; - if (Object.prototype.hasOwnProperty.call(message, app_id)) { + if (message.hasOwnProperty(app_id)) { applyC2P(app_id, message[app_id].data, message[app_id].html); delete message[app_id]; } diff --git a/app/panel-android/components/content/FixedMenu.jsx b/app/panel-android/components/content/FixedMenu.jsx index 61d826b63..d90dab464 100644 --- a/app/panel-android/components/content/FixedMenu.jsx +++ b/app/panel-android/components/content/FixedMenu.jsx @@ -51,7 +51,7 @@ export default class FixedMenu extends React.Component { const categories = Object.keys(this.antiTrackingData); for (let i = 0; i < categories.length; i++) { const category = categories[i]; - if (Object.prototype.hasOwnProperty.call(this.antiTrackingData, category)) { + if (this.antiTrackingData.hasOwnProperty(category)) { const apps = Object.keys(this.antiTrackingData[category]); for (let j = 0; j < apps.length; j++) { const app = apps[j]; diff --git a/app/panel/reducers/blocking.js b/app/panel/reducers/blocking.js index 213b15bc7..d0d440ed7 100644 --- a/app/panel/reducers/blocking.js +++ b/app/panel/reducers/blocking.js @@ -206,7 +206,7 @@ const _updateCliqzModuleWhitelist = (state, action) => { const addToWhitelist = () => { unknownTracker.domains.forEach((domain) => { - if (Object.prototype.hasOwnProperty.call(whitelistedUrls, domain)) { + if (whitelistedUrls.hasOwnProperty(domain)) { whitelistedUrls[domain].name = unknownTracker.name; whitelistedUrls[domain].hosts.push(pageHost); } else { diff --git a/app/panel/reducers/panel.js b/app/panel/reducers/panel.js index b6f644fc2..e814d834e 100644 --- a/app/panel/reducers/panel.js +++ b/app/panel/reducers/panel.js @@ -303,14 +303,14 @@ const _showNotification = (state, action) => { updated_needsReload = { ...state.needsReload, changes: { ...state.needsReload.changes } }; // handle case where user clicks 'whitelist' then 'blacklist', or inverse - if (msg.updated === 'blacklist' && Object.prototype.hasOwnProperty.call(updated_needsReload.changes, 'whitelist')) { + if (msg.updated === 'blacklist' && updated_needsReload.changes.hasOwnProperty('whitelist')) { delete updated_needsReload.changes.whitelist; - } else if (msg.updated === 'whitelist' && Object.prototype.hasOwnProperty.call(updated_needsReload.changes, 'blacklist')) { + } else if (msg.updated === 'whitelist' && updated_needsReload.changes.hasOwnProperty('blacklist')) { delete updated_needsReload.changes.blacklist; } // update the 'changes' object. if the changed item already exists, remove it to signal a disable has occurred - if (Object.prototype.hasOwnProperty.call(updated_needsReload.changes, msg.updated)) { + if (updated_needsReload.changes.hasOwnProperty(msg.updated)) { delete updated_needsReload.changes[msg.updated]; } else if (msg.updated !== 'init') { // ignore the 'init' change, which comes from Panel.jsx to persist banners updated_needsReload.changes[msg.updated] = true; diff --git a/app/panel/utils/blocking.js b/app/panel/utils/blocking.js index b710fa3ce..ab2d7b0c9 100644 --- a/app/panel/utils/blocking.js +++ b/app/panel/utils/blocking.js @@ -33,8 +33,8 @@ export function updateSummaryBlockingCount(categories = [], smartBlock, updateTr categories.forEach((categoryEl) => { categoryEl.trackers.forEach((trackerEl) => { numTotal++; - const sbBlocked = Object.prototype.hasOwnProperty.call(smartBlock.blocked, trackerEl.id); - const sbUnblocked = Object.prototype.hasOwnProperty.call(smartBlock.unblocked, trackerEl.id); + const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); + const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); if (trackerEl.ss_blocked || sbBlocked || (trackerEl.blocked && !trackerEl.ss_allowed && !sbUnblocked)) { numTotalBlocked++; @@ -81,8 +81,8 @@ export function updateBlockAllTrackers(state, action) { updated_categories.forEach((categoryEl) => { categoryEl.num_blocked = 0; categoryEl.trackers.forEach((trackerEl) => { - const sbBlocked = Object.prototype.hasOwnProperty.call(smartBlock.blocked, trackerEl.id); - const sbUnblocked = Object.prototype.hasOwnProperty.call(smartBlock.unblocked, trackerEl.id); + const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); + const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); if (trackerEl.shouldShow) { trackerEl.blocked = blocked; @@ -124,8 +124,8 @@ export function updateCategoryBlocked(state, action) { const updated_category = updated_categories[catIndex]; updated_category.num_blocked = 0; updated_category.trackers.forEach((trackerEl) => { - const sbBlocked = Object.prototype.hasOwnProperty.call(smartBlock.blocked, trackerEl.id); - const sbUnblocked = Object.prototype.hasOwnProperty.call(smartBlock.unblocked, trackerEl.id); + const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); + const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); if (trackerEl.shouldShow) { trackerEl.blocked = blocked; @@ -194,8 +194,8 @@ export function updateTrackerBlocked(state, action) { updated_category.num_blocked = 0; updated_category.trackers.forEach((trackerEl) => { - const sbBlocked = Object.prototype.hasOwnProperty.call(smartBlock.blocked, trackerEl.id); - const sbUnblocked = Object.prototype.hasOwnProperty.call(smartBlock.unblocked, trackerEl.id); + const sbBlocked = smartBlock.blocked.hasOwnProperty(trackerEl.id); + const sbUnblocked = smartBlock.unblocked.hasOwnProperty(trackerEl.id); if (trackerEl.shouldShow) { if (trackerEl.id === action.data.app_id) { diff --git a/src/background.js b/src/background.js index 5bf91a15c..c77b09004 100644 --- a/src/background.js +++ b/src/background.js @@ -129,10 +129,10 @@ function setGhosteryDefaultBlocking() { const app_ids = Object.keys(bugDb.db.apps); for (let i = 0; i < app_ids.length; i++) { const app_id = app_ids[i]; - if (Object.prototype.hasOwnProperty.call(bugDb.db.apps, app_id)) { + if (bugDb.db.apps.hasOwnProperty(app_id)) { const category = bugDb.db.apps[app_id].cat; if (categoriesBlock.indexOf(category) >= 0 && - !Object.prototype.hasOwnProperty.call(selected_app_ids, app_id)) { + !selected_app_ids.hasOwnProperty(app_id)) { selected_app_ids[app_id] = 1; } } @@ -498,7 +498,7 @@ function handleRewards(name, message, callback) { metrics.ping(message); break; case 'setPanelData': - if (Object.prototype.hasOwnProperty.call(message, 'enable_offers')) { + if (message.hasOwnProperty('enable_offers')) { Rewards.sendSignal(message.signal); panelData.set({ enable_offers: message.enable_offers }); } @@ -580,7 +580,7 @@ function handleGhosteryHub(name, message, callback) { const app_ids = Object.keys(bugDb.db.apps); for (let i = 0; i < app_ids.length; i++) { const app_id = app_ids[i]; - if (!Object.prototype.hasOwnProperty.call(selected_app_ids, app_id)) { + if (!selected_app_ids.hasOwnProperty(app_id)) { selected_app_ids[app_id] = 1; } } @@ -776,7 +776,7 @@ function onMessageHandler(request, sender, callback) { const { email, password } = message; account.login(email, password) .then((response) => { - if (!Object.prototype.hasOwnProperty.call(response, 'errors')) { + if (!response.hasOwnProperty('errors')) { metrics.ping('sign_in_success'); } callback(response); @@ -793,7 +793,7 @@ function onMessageHandler(request, sender, callback) { } = message; account.register(email, confirmEmail, password, firstName, lastName) .then((response) => { - if (!Object.prototype.hasOwnProperty.call(response, 'errors')) { + if (!response.hasOwnProperty('errors')) { metrics.ping('create_account_success'); } callback(response); @@ -1013,7 +1013,7 @@ function initializeDispatcher() { const { db } = bugDb; db.noneSelected = (num_selected === 0); // can't simply compare num_selected and size(db.apps) since apps get removed sometimes - db.allSelected = (!!num_selected && every(db.apps, (app, app_id) => Object.prototype.hasOwnProperty.call(appIds, app_id))); + db.allSelected = (!!num_selected && every(db.apps, (app, app_id) => appIds.hasOwnProperty(app_id))); }); dispatcher.on('conf.save.site_whitelist', () => { // TODO debounce with below diff --git a/src/classes/ABTest.js b/src/classes/ABTest.js index 51a47110c..df7a235fb 100644 --- a/src/classes/ABTest.js +++ b/src/classes/ABTest.js @@ -34,7 +34,7 @@ class ABTest { * @param {string} name test name */ hasTest(name) { - return Object.prototype.hasOwnProperty.call(this.tests, name); + return this.tests.hasOwnProperty(name); } /** diff --git a/src/classes/BugDb.js b/src/classes/BugDb.js index 612974771..497ac53e1 100644 --- a/src/classes/BugDb.js +++ b/src/classes/BugDb.js @@ -87,12 +87,12 @@ class BugDb extends Updatable { const appIds = Object.keys(db.apps); for (let i = 0; i < appIds.length; i++) { appId = appIds[i]; - if (Object.prototype.hasOwnProperty.call(db.apps, appId)) { + if (db.apps.hasOwnProperty(appId)) { category = db.apps[appId].cat; if (t(`category_${category}`) === `category_${category}`) { category = 'uncategorized'; } - blocked = Object.prototype.hasOwnProperty.call(selectedApps, appId); + blocked = selectedApps.hasOwnProperty(appId); // Because we have two trackers in the DB with the same name if ((categories[category] && categories[category].trackers[db.apps[appId].name])) { @@ -100,7 +100,7 @@ class BugDb extends Updatable { continue; } - if (Object.prototype.hasOwnProperty.call(categories, category)) { + if (categories.hasOwnProperty(category)) { categories[category].num_total++; if (blocked) { categories[category].num_blocked++; @@ -131,7 +131,7 @@ class BugDb extends Updatable { const categoryNames = Object.keys(categories); for (let i = 0; i < categoryNames.length; i++) { categoryName = categoryNames[i]; - if (Object.prototype.hasOwnProperty.call(categories, categoryName)) { + if (categories.hasOwnProperty(categoryName)) { const category = categories[categoryName]; if (category.trackers) { category.trackers.sort((a, b) => { @@ -186,7 +186,7 @@ class BugDb extends Updatable { const regexesKeys = Object.keys(regexes); for (let i = 0; i < regexesKeys.length; i++) { const id = regexesKeys[i]; - if (Object.prototype.hasOwnProperty.call(regexes, id)) { + if (regexes.hasOwnProperty(id)) { db.patterns.regex[id] = new RegExp(regexes[id], 'i'); } } @@ -199,7 +199,7 @@ class BugDb extends Updatable { // since allSelected is slow to eval, make it lazy defineLazyProperty(db, 'allSelected', () => { const num_selected = size(conf.selected_app_ids); - return (!!num_selected && every(db.apps, (app, app_id) => Object.prototype.hasOwnProperty.call(conf.selected_app_ids, app_id))); + return (!!num_selected && every(db.apps, (app, app_id) => conf.selected_app_ids.hasOwnProperty(app_id))); }); log('processed bugdb...'); @@ -210,7 +210,7 @@ class BugDb extends Updatable { // if there is an older bugs object in storage, // update newAppIds and apply block-by-default if (old_bugs) { - if (Object.prototype.hasOwnProperty.call(old_bugs, 'version') && bugs.version > old_bugs.version) { + if (old_bugs.hasOwnProperty('version') && bugs.version > old_bugs.version) { new_app_ids = BugDb.updateNewAppIds(bugs.apps, old_bugs.apps); if (new_app_ids.length) { @@ -219,7 +219,7 @@ class BugDb extends Updatable { } // pre-trie/legacy db - } else if (Object.prototype.hasOwnProperty.call(old_bugs, 'bugsVersion') && bugs.version !== old_bugs.bugsVersion) { + } else if (old_bugs.hasOwnProperty('bugsVersion') && bugs.version !== old_bugs.bugsVersion) { const old_apps = reduce(old_bugs.bugs, (memo, bug) => { memo[bug.aid] = true; return memo; diff --git a/src/classes/Click2PlayDb.js b/src/classes/Click2PlayDb.js index 43d892ad5..51e458b30 100644 --- a/src/classes/Click2PlayDb.js +++ b/src/classes/Click2PlayDb.js @@ -66,7 +66,7 @@ class Click2PlayDb extends Updatable { // TODO memory leak when you close tabs before reset() can run? reset(tab_id) { - if (!Object.prototype.hasOwnProperty.call(this.allowOnceList, tab_id)) { return; } + if (!this.allowOnceList.hasOwnProperty(tab_id)) { return; } let keep = false; const allowKeys = Object.keys(this.allowOnceList[tab_id]); @@ -86,8 +86,8 @@ class Click2PlayDb extends Updatable { allowedOnce(tab_id, aid) { return ( - Object.prototype.hasOwnProperty.call(this.allowOnceList, tab_id) && - Object.prototype.hasOwnProperty.call(this.allowOnceList[tab_id], aid) && + this.allowOnceList.hasOwnProperty(tab_id) && + this.allowOnceList[tab_id].hasOwnProperty(aid) && this.allowOnceList[tab_id][aid] > 0 ); } @@ -115,7 +115,7 @@ class Click2PlayDb extends Updatable { let allow; entries.forEach((entry) => { - if (!Object.prototype.hasOwnProperty.call(apps, entry.aid)) { + if (!apps.hasOwnProperty(entry.aid)) { apps[entry.aid] = []; } diff --git a/src/classes/CompatibilityDb.js b/src/classes/CompatibilityDb.js index 163281bd8..94afc446e 100644 --- a/src/classes/CompatibilityDb.js +++ b/src/classes/CompatibilityDb.js @@ -66,7 +66,7 @@ class CompatibilityDb extends Updatable { * @return {Boolean} */ hasIssue(aid, tab_url) { - return this.db.list && Object.prototype.hasOwnProperty.call(this.db.list, aid) && fuzzyUrlMatcher(tab_url, this.db.list[aid]); + return this.db.list && this.db.list.hasOwnProperty(aid) && fuzzyUrlMatcher(tab_url, this.db.list[aid]); } /** diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 69c4e20e3..b080ed117 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -186,12 +186,12 @@ class ConfData { let lang = window.navigator.language.replace('-', '_'); - if (Object.prototype.hasOwnProperty.call(SUPPORTED_LANGUAGES, lang)) { + if (SUPPORTED_LANGUAGES.hasOwnProperty(lang)) { return lang; } lang = lang.slice(0, 2); - if (Object.prototype.hasOwnProperty.call(SUPPORTED_LANGUAGES, lang)) { + if (SUPPORTED_LANGUAGES.hasOwnProperty(lang)) { return lang; } diff --git a/src/classes/FoundBugs.js b/src/classes/FoundBugs.js index 1d956743e..1404106c8 100644 --- a/src/classes/FoundBugs.js +++ b/src/classes/FoundBugs.js @@ -139,7 +139,7 @@ class FoundBugs { if (app_id) { const { appsById } = this._foundApps[tab_id]; - if (Object.prototype.hasOwnProperty.call(appsById, app_id)) { + if (appsById.hasOwnProperty(app_id)) { apps_arr.push(apps[appsById[app_id]]); } } else { @@ -203,11 +203,11 @@ class FoundBugs { const ids = Object.keys(bugs); for (let i = 0; i < ids.length; i++) { id = ids[i]; - if (Object.prototype.hasOwnProperty.call(bugs, id)) { + if (bugs.hasOwnProperty(id)) { aid = db.bugs[id].aid; // eslint-disable-line prefer-destructuring cid = db.apps[aid].cat; - if (Object.prototype.hasOwnProperty.call(cats_obj, cid)) { + if (cats_obj.hasOwnProperty(cid)) { if (!cats_obj[cid].appIds.includes(aid)) { cats_obj[cid].appIds.push(aid); cats_obj[cid].trackers.push({ @@ -244,7 +244,7 @@ class FoundBugs { const cids = Object.keys(cats_obj); for (let i = 0; i < cids.length; i++) { cid = cids[i]; - if (Object.prototype.hasOwnProperty.call(cats_obj, cid)) { + if (cats_obj.hasOwnProperty(cid)) { cats_arr.push(cats_obj[cid]); } } @@ -364,7 +364,7 @@ class FoundBugs { const { aid } = bugDb.db.bugs[bug_id]; const { apps, appsById, issueCounts } = this._foundApps[tab_id]; - if (Object.prototype.hasOwnProperty.call(appsById, aid)) { + if (appsById.hasOwnProperty(aid)) { const app = apps[appsById[aid]]; if (!app.hasLatencyIssue) { issueCounts.latency++; @@ -395,11 +395,11 @@ class FoundBugs { return false; } - if (!Object.prototype.hasOwnProperty.call(this._foundBugs, tab_id)) { + if (!this._foundBugs.hasOwnProperty(tab_id)) { this._foundBugs[tab_id] = {}; } - if (!Object.prototype.hasOwnProperty.call(this._foundApps, tab_id)) { + if (!this._foundApps.hasOwnProperty(tab_id)) { this._foundApps[tab_id] = { apps: [], appsMetadata: {}, @@ -445,7 +445,7 @@ class FoundBugs { * @param {string} type */ _updateFoundBugs(tab_id, bug_id, src, blocked, type) { - if (!Object.prototype.hasOwnProperty.call(this._foundBugs[tab_id], bug_id)) { + if (!this._foundBugs[tab_id].hasOwnProperty(bug_id)) { this._foundBugs[tab_id][bug_id] = { sources: [], hasLatencyIssue: false, @@ -487,7 +487,7 @@ class FoundBugs { apps, appsMetadata, appsById, issueCounts } = this._foundApps[tab_id]; - if (Object.prototype.hasOwnProperty.call(appsById, aid)) { + if (appsById.hasOwnProperty(aid)) { const app = apps[appsById[aid]]; if (!app.hasLatencyIssue && hasLatencyIssue) { issueCounts.latency++; } diff --git a/src/classes/Latency.js b/src/classes/Latency.js index f8acc509f..f5b331111 100644 --- a/src/classes/Latency.js +++ b/src/classes/Latency.js @@ -33,7 +33,7 @@ class Latency { const request_id = details.requestId; const tab_id = details.tabId; - if (!Object.prototype.hasOwnProperty.call(this.latencies, request_id)) { + if (!this.latencies.hasOwnProperty(request_id)) { return 0; } // If the latencies object for this request id is empty then this is @@ -46,7 +46,7 @@ class Latency { // TRACKER1 --> NON-TRACKER --> TRACKER2 // TRACKER2's onBeforeRequest sync callback could maybe fire before // NON-TRACKER's onBeforeRedirect async callback - if (!Object.prototype.hasOwnProperty.call(this.latencies[request_id], details.url)) { + if (!this.latencies[request_id].hasOwnProperty(details.url)) { return 0; } diff --git a/src/classes/PanelData.js b/src/classes/PanelData.js index 9b38ee70b..f6b403b03 100644 --- a/src/classes/PanelData.js +++ b/src/classes/PanelData.js @@ -590,7 +590,7 @@ class PanelData { for (let i = 0; i < dataKeys.length; i++) { const key = dataKeys[i]; const value = data[key]; - if (Object.prototype.hasOwnProperty.call(conf, key) && !isEqual(conf[key], value)) { + if (conf.hasOwnProperty(key) && !isEqual(conf[key], value)) { conf[key] = value; syncSetDataChanged = SYNC_SET.has(key) ? true : syncSetDataChanged; // TODO refactor - this work should probably be the direct responsibility of Globals @@ -659,7 +659,7 @@ class PanelData { cat = 'uncategorized'; } - if (Object.prototype.hasOwnProperty.call(categories, cat)) { + if (categories.hasOwnProperty(cat)) { categories[cat].num_total++; if (PanelData._addsUpToBlocked(trackerState)) { categories[cat].num_blocked++; } } else { @@ -746,7 +746,7 @@ class PanelData { warningCompatibility: hasCompatibilityIssue, warningInsecure: hasInsecureIssue, warningSlow: hasLatencyIssue, - warningSmartBlock: (Object.prototype.hasOwnProperty.call(smartBlock.blocked, id) && 'blocked') || (Object.prototype.hasOwnProperty.call(smartBlock.unblocked, id) && 'unblocked') || false, + warningSmartBlock: (smartBlock.blocked.hasOwnProperty(id) && 'blocked') || (smartBlock.unblocked.hasOwnProperty(id) && 'unblocked') || false, cliqzAdCount, cliqzCookieCount, cliqzFingerprintCount, @@ -771,11 +771,11 @@ class PanelData { const pageBlocks = (pageHost && conf.site_specific_blocks[pageHost]) || []; return { - blocked: Object.prototype.hasOwnProperty.call(selectedAppIds, trackerId), + blocked: selectedAppIds.hasOwnProperty(trackerId), ss_allowed: pageUnblocks.includes(+trackerId), ss_blocked: pageBlocks.includes(+trackerId), - sb_blocked: smartBlockActive && Object.prototype.hasOwnProperty.call(smartBlock.blocked, `${trackerId}`), - sb_allowed: smartBlockActive && Object.prototype.hasOwnProperty.call(smartBlock.unblocked, `${trackerId}`) + sb_blocked: smartBlockActive && smartBlock.blocked.hasOwnProperty(`${trackerId}`), + sb_allowed: smartBlockActive && smartBlock.unblocked.hasOwnProperty(`${trackerId}`) }; } @@ -791,7 +791,7 @@ class PanelData { const { trackers } = categoryEl; categoryEl.num_blocked = 0; trackers.forEach((trackerEl) => { - trackerEl.blocked = Object.prototype.hasOwnProperty.call(selectedApps, trackerEl.id); + trackerEl.blocked = selectedApps.hasOwnProperty(trackerEl.id); if (trackerEl.blocked) { categoryEl.num_blocked++; } diff --git a/src/classes/Policy.js b/src/classes/Policy.js index 50b9f23a8..35db075dc 100644 --- a/src/classes/Policy.js +++ b/src/classes/Policy.js @@ -156,9 +156,9 @@ class Policy { const allowedOnce = c2pDb.allowedOnce(tab_id, app_id); // The app_id has been globally blocked - if (Object.prototype.hasOwnProperty.call(conf.selected_app_ids, app_id)) { + if (conf.selected_app_ids.hasOwnProperty(app_id)) { // The app_id is on the site-specific allow list for this tab_host - if (conf.toggle_individual_trackers && Object.prototype.hasOwnProperty.call(conf.site_specific_unblocks, tab_host) && conf.site_specific_unblocks[tab_host].includes(+app_id)) { + if (conf.toggle_individual_trackers && conf.site_specific_unblocks.hasOwnProperty(tab_host) && conf.site_specific_unblocks[tab_host].includes(+app_id)) { // Site blacklist overrides all block settings except C2P allow once if (Policy.blacklisted(tab_url)) { return { block: !allowedOnce, reason: allowedOnce ? BLOCK_REASON_C2P_ALLOWED_ONCE : BLOCK_REASON_BLACKLISTED }; @@ -175,7 +175,7 @@ class Policy { // The app_id has not been globally blocked // Check to see if the app_id is on the site-specific block list for this tab_host - if (conf.toggle_individual_trackers && Object.prototype.hasOwnProperty.call(conf.site_specific_blocks, tab_host) && conf.site_specific_blocks[tab_host].includes(+app_id)) { + if (conf.toggle_individual_trackers && conf.site_specific_blocks.hasOwnProperty(tab_host) && conf.site_specific_blocks[tab_host].includes(+app_id)) { // Site white-listing overrides blocking settings if (Policy.checkSiteWhitelist(tab_url)) { return { block: false, reason: BLOCK_REASON_WHITELISTED }; @@ -183,7 +183,7 @@ class Policy { return { block: !allowedOnce, reason: allowedOnce ? BLOCK_REASON_C2P_ALLOWED_ONCE : BLOCK_REASON_SS_BLOCKED }; } // Check to see if the app_id is on the site-specific allow list for this tab_host - if (conf.toggle_individual_trackers && Object.prototype.hasOwnProperty.call(conf.site_specific_unblocks, tab_host) && conf.site_specific_unblocks[tab_host].includes(+app_id)) { + if (conf.toggle_individual_trackers && conf.site_specific_unblocks.hasOwnProperty(tab_host) && conf.site_specific_unblocks[tab_host].includes(+app_id)) { // Site blacklist overrides all block settings except C2P allow once if (Policy.blacklisted(tab_url)) { return { block: !allowedOnce, reason: allowedOnce ? BLOCK_REASON_C2P_ALLOWED_ONCE : BLOCK_REASON_BLACKLISTED }; diff --git a/src/classes/PolicySmartBlock.js b/src/classes/PolicySmartBlock.js index 7aa1cf834..9df337c35 100644 --- a/src/classes/PolicySmartBlock.js +++ b/src/classes/PolicySmartBlock.js @@ -135,9 +135,9 @@ class PolicySmartBlock { conf.enable_smart_block && !globals.SESSION.paused_blocking && !Policy.getSitePolicy(tabUrl) && - ((appId && (!Object.prototype.hasOwnProperty.call(conf.site_specific_unblocks, tabHost) || !conf.site_specific_unblocks[tabHost].includes(+appId))) || appId === false) && - ((appId && (!Object.prototype.hasOwnProperty.call(conf.site_specific_blocks, tabHost) || !conf.site_specific_blocks[tabHost].includes(+appId))) || appId === false) && - (c2pDb.db.apps && !Object.prototype.hasOwnProperty.call(c2pDb.db.apps, appId)) + ((appId && (!conf.site_specific_unblocks.hasOwnProperty(tabHost) || !conf.site_specific_unblocks[tabHost].includes(+appId))) || appId === false) && + ((appId && (!conf.site_specific_blocks.hasOwnProperty(tabHost) || !conf.site_specific_blocks[tabHost].includes(+appId))) || appId === false) && + (c2pDb.db.apps && !c2pDb.db.apps.hasOwnProperty(appId)) ); } diff --git a/src/classes/SurrogateDb.js b/src/classes/SurrogateDb.js index f5856f102..d8e5912ed 100644 --- a/src/classes/SurrogateDb.js +++ b/src/classes/SurrogateDb.js @@ -63,24 +63,24 @@ class SurrogateDb extends Updatable { // convert single values to arrays first ['pattern_id', 'app_id', 'sites', 'match'].forEach((prop) => { - if (Object.prototype.hasOwnProperty.call(s, prop) && !isArray(s[prop])) { + if (s.hasOwnProperty(prop) && !isArray(s[prop])) { s[prop] = [s[prop]]; } }); // initialize regexes - if (Object.prototype.hasOwnProperty.call(s, 'match')) { + if (s.hasOwnProperty('match')) { s.match = map(s.match, match => new RegExp(match, '')); } - if (Object.prototype.hasOwnProperty.call(s, 'pattern_id') || Object.prototype.hasOwnProperty.call(s, 'app_id')) { + if (s.hasOwnProperty('pattern_id') || s.hasOwnProperty('app_id')) { // tracker-level surrogate - if (Object.prototype.hasOwnProperty.call(s, 'pattern_id')) { + if (s.hasOwnProperty('pattern_id')) { this._buildDb(s, 'pattern_id', 'pattern_ids'); - } else if (Object.prototype.hasOwnProperty.call(s, 'app_id')) { + } else if (s.hasOwnProperty('app_id')) { this._buildDb(s, 'app_id', 'app_ids'); } - } else if (Object.prototype.hasOwnProperty.call(s, 'sites')) { + } else if (s.hasOwnProperty('sites')) { // we have a "sites" property, but not pattern_id/app_id: // it's a site surrogate this._buildDb(s, 'sites', 'site_surrogates'); @@ -104,23 +104,23 @@ class SurrogateDb extends Updatable { getForTracker(script_src, app_id, pattern_id, host_name) { let candidates = []; - if (Object.prototype.hasOwnProperty.call(this.db.app_ids, app_id)) { + if (this.db.app_ids.hasOwnProperty(app_id)) { candidates = candidates.concat(this.db.app_ids[app_id]); } - if (Object.prototype.hasOwnProperty.call(this.db.pattern_ids, pattern_id)) { + if (this.db.pattern_ids.hasOwnProperty(pattern_id)) { candidates = candidates.concat(this.db.pattern_ids[pattern_id]); } return filter(candidates, (surrogate) => { // note: does not support *.example.com (exact matches only) - if (Object.prototype.hasOwnProperty.call(surrogate, 'sites')) { // array of site hosts + if (surrogate.hasOwnProperty('sites')) { // array of site hosts if (!surrogate.sites.includes(host_name)) { return false; } } - if (Object.prototype.hasOwnProperty.call(surrogate, 'match')) { + if (surrogate.hasOwnProperty('match')) { if (!any(surrogate.match, match => script_src.match(match))) { return false; } @@ -142,7 +142,7 @@ class SurrogateDb extends Updatable { */ _buildDb(surrogate, property, db_name) { surrogate[property].forEach((val) => { - if (!Object.prototype.hasOwnProperty.call(this.db[db_name], val)) { + if (!this.db[db_name].hasOwnProperty(val)) { this.db[db_name][val] = []; } this.db[db_name][val].push(surrogate); diff --git a/src/classes/TabInfo.js b/src/classes/TabInfo.js index 17ea679b3..24731c7fb 100644 --- a/src/classes/TabInfo.js +++ b/src/classes/TabInfo.js @@ -85,7 +85,7 @@ class TabInfo { // TODO consider improving handling of what happens if we mistype the property name. Always // returning an object where property would otherwise have returned false could result in subtle bugs. getTabInfo(tab_id, property) { - if (Object.prototype.hasOwnProperty.call(this._tabInfo, tab_id)) { + if (this._tabInfo.hasOwnProperty(tab_id)) { if (property) { return this._tabInfo[tab_id][property]; } @@ -101,7 +101,7 @@ class TabInfo { * @return {Object} persistent data for this tab */ getTabInfoPersist(tab_id, property) { - if (Object.prototype.hasOwnProperty.call(this._tabInfoPersist, tab_id)) { + if (this._tabInfoPersist.hasOwnProperty(tab_id)) { if (property) { return this._tabInfoPersist[tab_id][property]; } @@ -117,7 +117,7 @@ class TabInfo { * @param {*} value property value */ setTabInfo(tab_id, property, value) { - if (Object.prototype.hasOwnProperty.call(this._tabInfo, tab_id)) { + if (this._tabInfo.hasOwnProperty(tab_id)) { // check for 'url' property case if (property === 'url') { this._updateUrl(tab_id, value); @@ -135,7 +135,7 @@ class TabInfo { * @param {boolean} blocked kind of policy to set */ setTabSmartBlockAppInfo(tabId, appId, rule, blocked) { - if (!Object.prototype.hasOwnProperty.call(this._tabInfo, tabId)) { return; } + if (!this._tabInfo.hasOwnProperty(tabId)) { return; } const policy = blocked ? 'blocked' : 'unblocked'; if (typeof this._tabInfo[tabId].smartBlock[policy][appId] === 'undefined') { @@ -153,7 +153,7 @@ class TabInfo { * @param {number} tab_id tab id */ clear(tab_id) { - if (!Object.prototype.hasOwnProperty.call(this._tabInfo, tab_id)) { return; } + if (!this._tabInfo.hasOwnProperty(tab_id)) { return; } const { numOfReloads, firstLoadTimestamp } = this._tabInfo[tab_id]; // TODO potential memory leak? diff --git a/src/classes/Updatable.js b/src/classes/Updatable.js index cb191d47a..b8a5ba576 100644 --- a/src/classes/Updatable.js +++ b/src/classes/Updatable.js @@ -77,7 +77,7 @@ class Updatable { const version_property = (this.type === 'bugs' || this.type === 'surrogates' ? 'version' : (`${this.type}Version`)); // nothing in storage, or it's so old it doesn't have a version - if (!memory || !Object.prototype.hasOwnProperty.call(memory, version_property)) { + if (!memory || !memory.hasOwnProperty(version_property)) { // return what's on disk log(`fetching ${this.type} from disk`); diff --git a/src/utils/click2play.js b/src/utils/click2play.js index 7e6b8d94a..9979eb9e4 100644 --- a/src/utils/click2play.js +++ b/src/utils/click2play.js @@ -102,7 +102,7 @@ export function buildC2P(details, app_id) { case 'none': tabInfo.setTabInfo(tab_id, 'c2pStatus', 'loading'); // Push current C2P data into existing queue - if (!Object.prototype.hasOwnProperty.call(tab.c2pQueue, app_id)) { + if (!tab.c2pQueue.hasOwnProperty(app_id)) { tabInfo.setTabInfo(tab_id, 'c2pQueue', { ...tab.c2pQueue, [app_id]: { @@ -123,7 +123,7 @@ export function buildC2P(details, app_id) { break; case 'loading': // Push C2P data to a holding queue until click_to_play.js has finished loading on the page - if (!Object.prototype.hasOwnProperty.call(tab.c2pQueue, app_id)) { + if (!tab.c2pQueue.hasOwnProperty(app_id)) { tabInfo.setTabInfo(tab_id, 'c2pQueue', { ...tab.c2pQueue, [app_id]: { @@ -195,7 +195,7 @@ export function allowAllwaysC2P(app_id, tab_host) { conf.selected_app_ids = selected_app_ids; // Remove fron site-specific-blocked - if (Object.prototype.hasOwnProperty.call(conf.site_specific_blocks, tab_host) && conf.site_specific_blocks[tab_host].includes(+app_id)) { + if (conf.site_specific_blocks.hasOwnProperty(tab_host) && conf.site_specific_blocks[tab_host].includes(+app_id)) { const { site_specific_blocks } = conf; site_specific_blocks[tab_host].splice(0, 1); conf.site_specific_blocks = site_specific_blocks; @@ -203,7 +203,7 @@ export function allowAllwaysC2P(app_id, tab_host) { // Add tracker to site-specific-allowed const { site_specific_unblocks } = conf; - if (!Object.prototype.hasOwnProperty.call(site_specific_unblocks, tab_host)) { + if (!site_specific_unblocks.hasOwnProperty(tab_host)) { // create new array of unblocks for this host site_specific_unblocks[tab_host] = []; } diff --git a/src/utils/common.js b/src/utils/common.js index 0225fc2a5..fd5aa1279 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -65,14 +65,14 @@ export function prefsGet(...args) { result = items; } else if (args.length === 1) { const key = args[0]; // extract value from array - if (items && Object.prototype.hasOwnProperty.call(items, key)) { + if (items && items.hasOwnProperty(key)) { result = items[key]; } } else { result = {}; // instantiate an empty object args.forEach((key) => { result[key] = null; - if (items && Object.prototype.hasOwnProperty.call(items, key)) { + if (items && items.hasOwnProperty(key)) { result[key] = items[key]; } }); diff --git a/src/utils/matcher.js b/src/utils/matcher.js index 7e562e43c..92bea4eac 100644 --- a/src/utils/matcher.js +++ b/src/utils/matcher.js @@ -119,7 +119,7 @@ function _matchesHostPath(roots, src_path) { for (i = 0; i < roots.length; i++) { root = roots[i]; - if (Object.prototype.hasOwnProperty.call(root, '$')) { + if (root.hasOwnProperty('$')) { paths = root.$; for (j = 0; j < paths.length; j++) { if (src_path.startsWith(paths[j].path)) { @@ -153,14 +153,14 @@ function _matchesHost(root, src_host, src_path) { for (let i = 0; i < host_rev_arr.length; i++) { host_part = host_rev_arr[i]; // if node has domain, advance and try to update bug_id - if (Object.prototype.hasOwnProperty.call(node, host_part)) { + if (node.hasOwnProperty(host_part)) { // advance node node = node[host_part]; - bug_id = (Object.prototype.hasOwnProperty.call(node, '$') ? node.$ : bug_id); + bug_id = (node.hasOwnProperty('$') ? node.$ : bug_id); // we store all traversed nodes that contained paths in case the final // node does not have the matching path - if (src_path !== undefined && Object.prototype.hasOwnProperty.call(node, '$')) { + if (src_path !== undefined && node.hasOwnProperty('$')) { nodes_with_paths.push(node); } diff --git a/tools/i18n-checker.js b/tools/i18n-checker.js index 9d7eb181f..0f5d96f36 100644 --- a/tools/i18n-checker.js +++ b/tools/i18n-checker.js @@ -138,7 +138,7 @@ function findDuplicates(paths) { duplicates[locale] = []; oboe(fs.createReadStream(path)).node('{message}', (val, keys) => { const key = keys[0]; - if (Object.prototype.hasOwnProperty.call(foundKeys, key)) { + if (foundKeys.hasOwnProperty(key)) { hasDuplicates = true; duplicates[locale].push(key); return; @@ -179,7 +179,7 @@ function findMissingKeys(paths) { const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; missingKeys[locale] = []; Object.keys(defaultLocaleJson).forEach((key) => { - if (!Object.prototype.hasOwnProperty.call(localeJson, key)) { + if (!localeJson.hasOwnProperty(key)) { hasMissingKeys = true; missingKeys[locale].push(key); } @@ -214,7 +214,7 @@ function findExtraKeys(paths) { const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; extraKeys[locale] = []; Object.keys(localeJson).forEach((key) => { - if (!Object.prototype.hasOwnProperty.call(defaultLocaleJson, key)) { + if (!defaultLocaleJson.hasOwnProperty(key)) { hasExtraKeys = true; extraKeys[locale].push(key); } @@ -247,7 +247,7 @@ function findMalformedKeys(paths) { const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; malformedKeys[locale] = []; Object.keys(localeJson).forEach((key) => { - if (!Object.prototype.hasOwnProperty.call(localeJson[key], 'message')) { + if (!localeJson[key].hasOwnProperty('message')) { hasMalformedKeys = true; malformedKeys[locale].push(key); } @@ -282,8 +282,8 @@ function findMissingPlaceholders(paths) { const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; missingPlaceholders[locale] = []; Object.keys(defaultLocaleJson).forEach((key) => { - if (Object.prototype.hasOwnProperty.call(defaultLocaleJson[key], 'placeholders')) { - if (!localeJson[key] || !Object.prototype.hasOwnProperty.call(localeJson[key], 'placeholders')) { + if (defaultLocaleJson[key].hasOwnProperty('placeholders')) { + if (!localeJson[key] || !localeJson[key].hasOwnProperty('placeholders')) { hasMissingPlaceholders = true; missingPlaceholders[locale].push(`${key}: missing ${Object.keys(defaultLocaleJson[key].placeholders).length} placeholder(s)`); return; @@ -326,8 +326,8 @@ function findExtraPlaceholders(paths) { const locale = path.match(/_locales\/(.*)\/messages.json/)[1]; extraPlaceholders[locale] = []; Object.keys(localeJson).forEach((key) => { - if (Object.prototype.hasOwnProperty.call(localeJson[key], 'placeholders')) { - if (!defaultLocaleJson[key] || !Object.prototype.hasOwnProperty.call(defaultLocaleJson[key], 'placeholders')) { + if (localeJson[key].hasOwnProperty('placeholders')) { + if (!defaultLocaleJson[key] || !defaultLocaleJson[key].hasOwnProperty('placeholders')) { hasExtraPlaceholders = true; extraPlaceholders[locale].push(`${key}: has ${Object.keys(localeJson[key].placeholders).length} extra placeholder(s)`); return; @@ -374,7 +374,7 @@ function findMalformedPlaceholders(paths) { if (matchedPlaceholders) { matchedPlaceholders.forEach((p) => { const placeholder = p.toLowerCase().slice(1, -1); - if (!Object.prototype.hasOwnProperty.call(placeholders, placeholder)) { + if (!placeholders.hasOwnProperty(placeholder)) { hasMalformedPlaceholders = true; malformedPlaceholders[locale].push(`${key}: needs placeholder "${placeholder}"`); } diff --git a/tools/leet/leet-en.js b/tools/leet/leet-en.js index 2ff8dd504..e00bcb62b 100644 --- a/tools/leet/leet-en.js +++ b/tools/leet/leet-en.js @@ -68,7 +68,7 @@ const leet_convert = function(string) { const characterKeys = Object.keys(characterMap); for (let i = 0; i < characterKeys.length; i++) { const letter = characterKeys[i]; - if (Object.prototype.hasOwnProperty.call(characterMap, letter)) { + if (characterMap.hasOwnProperty(letter)) { output = output.replace(new RegExp(letter, 'g'), characterMap[letter]); } } @@ -89,7 +89,7 @@ if (!fs.existsSync('./tools/leet/messages.en.copy.json')) { const enKeys = Object.keys(en); for (let i = 0; i < enKeys.length; i++) { const key = enKeys[i]; - if (Object.prototype.hasOwnProperty.call(en[key], 'message')) { + if (en[key].hasOwnProperty('message')) { const message = leet_convert(en[key].message); const { placeholders } = en[key]; leet[key] = { message, placeholders }; diff --git a/tools/transifex.js b/tools/transifex.js index a888ebdc1..d8086e72a 100644 --- a/tools/transifex.js +++ b/tools/transifex.js @@ -102,8 +102,8 @@ function fixMissingPlaceholders(paths) { let dirty = false; const localeJson = jsonfile.readFileSync(`.${path}`); Object.keys(defaultLocaleJson).forEach((key) => { - if (Object.prototype.hasOwnProperty.call(defaultLocaleJson[key], 'placeholders')) { - if (localeJson[key] && !Object.prototype.hasOwnProperty.call(localeJson[key], 'placeholders')) { + if (defaultLocaleJson[key].hasOwnProperty('placeholders')) { + if (localeJson[key] && !localeJson[key].hasOwnProperty('placeholders')) { dirty = true; localeJson[key].placeholders = defaultLocaleJson[key].placeholders; } From ac0ec936b00936c28f8b315b81d9fc6b0b6e2917 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Wed, 20 May 2020 11:04:39 +0200 Subject: [PATCH 16/89] add single line exception for no-restricted-syntax linting rule --- src/utils/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/utils.js b/src/utils/utils.js index da02d0c80..a6490f7f7 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -194,7 +194,7 @@ export function processUrlQuery(src) { try { const res = {}; - for (const [key, value] of new URL(src).searchParams.entries()) { + for (const [key, value] of new URL(src).searchParams.entries()) { // eslint-disable-line no-restricted-syntax res[key] = value; } return res; From fd954ebbdd321f7642a9cd9f90d6887b5fc4f9d8 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Thu, 21 May 2020 22:21:30 +0200 Subject: [PATCH 17/89] add linting for react/destructuring-assignment and fix errors. ToDo: test code and check for errors --- .eslintrc.js | 2 +- app/hub/Views/AppView/AppView.jsx | 3 +- app/hub/Views/AppView/AppViewContainer.jsx | 3 +- .../CreateAccountViewContainer.jsx | 19 ++- app/hub/Views/HomeView/HomeViewContainer.jsx | 8 +- .../Views/LogInView/LogInViewContainer.jsx | 23 +-- app/hub/Views/PlusView/PlusViewContainer.jsx | 6 +- app/hub/Views/ProductsView/ProductsView.jsx | 8 +- .../ProductsView/ProductsViewContainer.jsx | 7 +- .../Views/SetupView/SetupViewContainer.jsx | 34 ++-- .../SetupAntiSuiteView/SetupAntiSuiteView.jsx | 4 +- .../SetupAntiSuiteViewContainer.jsx | 20 +-- .../SetupBlockingDropdownContainer.jsx | 6 +- .../SetupBlockingView/SetupBlockingView.jsx | 18 ++- .../SetupBlockingViewContainer.jsx | 9 +- .../__tests__/SetupBlockingView.test.jsx | 4 + .../SetupDoneView/SetupDoneView.jsx | 4 +- .../SetupViews/SetupHeader/SetupHeader.jsx | 6 +- .../SetupHumanWebView/SetupHumanWebView.jsx | 8 +- .../SetupHumanWebViewContainer.jsx | 8 +- .../SideNavigationViewContainer.jsx | 5 +- app/hub/Views/SignedInView/SignedInView.jsx | 4 +- .../TutorialView/TutorialViewContainer.jsx | 7 +- .../TutorialAntiSuiteViewContainer.jsx | 3 +- app/licenses/License.jsx | 3 +- .../components/GlobalTrackers.jsx | 12 +- app/panel-android/components/Overview.jsx | 27 ++-- app/panel-android/components/Panel.jsx | 23 +-- app/panel-android/components/SiteTrackers.jsx | 9 +- .../components/content/Accordion.jsx | 93 +++++++---- .../components/content/Accordions.jsx | 16 +- .../components/content/ChartSVG.jsx | 17 +- .../components/content/DotsMenu.jsx | 12 +- .../components/content/FixedMenu.jsx | 21 ++- .../components/content/MenuItem.jsx | 39 +++-- app/panel-android/components/content/Path.jsx | 6 +- app/panel-android/components/content/Tab.jsx | 8 +- app/panel-android/components/content/Tabs.jsx | 17 +- .../components/content/TrackerItem.jsx | 88 ++++++----- .../components/content/TrackersChart.jsx | 6 +- app/panel/components/AccountSuccess.jsx | 6 +- app/panel/components/Blocking.jsx | 59 ++++--- .../components/Blocking/BlockingHeader.jsx | 121 ++++++++------ app/panel/components/Blocking/Categories.jsx | 33 ++-- app/panel/components/Blocking/Category.jsx | 97 ++++++++---- .../components/Blocking/GlobalTracker.jsx | 33 ++-- app/panel/components/Blocking/Tracker.jsx | 104 ++++++++----- app/panel/components/Blocking/Trackers.jsx | 43 +++-- .../BuildingBlocks/ClickOutside.jsx | 13 +- .../components/BuildingBlocks/DonutGraph.jsx | 46 +++--- .../BuildingBlocks/GhosteryFeature.jsx | 5 +- .../components/BuildingBlocks/NotScanned.jsx | 4 +- .../components/BuildingBlocks/PauseButton.jsx | 15 +- .../BuildingBlocks/RadioButtonGroup.jsx | 10 +- .../components/BuildingBlocks/StatsGraph.jsx | 45 +++--- .../BuildingBlocks/ToggleSlider.jsx | 17 +- app/panel/components/CreateAccount.jsx | 10 +- app/panel/components/Detail.jsx | 12 +- app/panel/components/DetailMenu.jsx | 13 +- app/panel/components/Header.jsx | 49 +++--- app/panel/components/HeaderMenu.jsx | 45 ++++-- app/panel/components/Login.jsx | 16 +- app/panel/components/Panel.jsx | 62 ++++---- app/panel/components/Rewards.jsx | 15 +- app/panel/components/Settings.jsx | 108 +++++++++++-- app/panel/components/Settings/Account.jsx | 12 +- .../components/Settings/GeneralSettings.jsx | 31 ++-- .../components/Settings/GlobalBlocking.jsx | 29 ++-- .../components/Settings/Notifications.jsx | 93 ++++++----- app/panel/components/Settings/OptIn.jsx | 83 +++++----- app/panel/components/Settings/Purplebox.jsx | 147 +++++++++--------- .../components/Settings/SettingsMenu.jsx | 4 +- app/panel/components/Settings/Site.jsx | 3 +- .../components/Settings/TrustAndRestrict.jsx | 53 ++++--- app/panel/components/Stats.jsx | 6 +- app/panel/components/Subscribe.jsx | 4 +- app/panel/components/Subscription.jsx | 27 +++- .../Subscription/SubscriptionInfo.jsx | 4 +- .../Subscription/SubscriptionThemes.jsx | 8 +- app/panel/components/Summary.jsx | 75 +++++---- app/panel/components/Tooltip.jsx | 30 ++-- .../ForgotPassword/ForgotPassword.jsx | 10 +- app/shared-components/Modal/Modal.jsx | 8 +- .../PromoModal/PromoModal.jsx | 14 +- .../ToastMessage/ToastMessage.jsx | 12 +- .../ToggleCheckbox/ToggleCheckbox.jsx | 10 +- .../ToggleSwitch/ToggleSwitch.jsx | 8 +- 87 files changed, 1365 insertions(+), 903 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 0cca44a35..4e67ea50d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -80,7 +80,7 @@ module.exports = { 'import/prefer-default-export': [1], // Plugin: React - 'react/destructuring-assignment': [0], // ToDo + 'react/destructuring-assignment': [1], 'react/static-property-placement': [0], 'react/jsx-curly-newline': [0], 'react/jsx-indent': [1, 'tab'], diff --git a/app/hub/Views/AppView/AppView.jsx b/app/hub/Views/AppView/AppView.jsx index e46108282..4ae9db2c9 100644 --- a/app/hub/Views/AppView/AppView.jsx +++ b/app/hub/Views/AppView/AppView.jsx @@ -31,7 +31,8 @@ class AppView extends Component { * Lifecycle Event */ componentDidUpdate(prevProps) { - if (prevProps.children !== this.props.children) { + const { children } = this.props; + if (prevProps.children !== children) { this.mainContent.current.scrollTop = 0; } } diff --git a/app/hub/Views/AppView/AppViewContainer.jsx b/app/hub/Views/AppView/AppViewContainer.jsx index 8cf81288e..012b24f89 100644 --- a/app/hub/Views/AppView/AppViewContainer.jsx +++ b/app/hub/Views/AppView/AppViewContainer.jsx @@ -25,7 +25,8 @@ class AppViewContainer extends Component { * Handle clicking to exit the Toast Message. */ _exitToast = () => { - this.props.actions.setToast({ + const { actions } = this.props; + actions.setToast({ toastMessage: '', toastClass: '', }); diff --git a/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx b/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx index f71c46f97..fdbad32c0 100644 --- a/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx +++ b/app/hub/Views/CreateAccountView/CreateAccountViewContainer.jsx @@ -45,7 +45,8 @@ class CreateAccountViewContainer extends Component { validateInput: false, }; - this.props.actions.setToast({ + const { actions } = this.props; + actions.setToast({ toastMessage: '', toastClass: '', }); @@ -59,7 +60,8 @@ class CreateAccountViewContainer extends Component { const { name, value } = event.target; this.setState({ [name]: value }); - if (!this.state.validateInput) { + const { validateInput } = this.state; + if (!validateInput) { return; } @@ -132,20 +134,21 @@ class CreateAccountViewContainer extends Component { if (!emailIsValid || !confirmIsValid || !legalConsentChecked || !passwordIsValid) { return; } - this.props.actions.setToast({ + const { actions, history } = this.props; + actions.setToast({ toastMessage: '', toastClass: '' }); - this.props.actions.register(email, confirmEmail, firstName, lastName, password).then((success) => { + actions.register(email, confirmEmail, firstName, lastName, password).then((success) => { if (success) { - this.props.actions.getUser(); - this.props.actions.setToast({ + actions.getUser(); + actions.setToast({ toastMessage: t('hub_create_account_toast_success'), toastClass: 'success' }); - this.props.history.push('/'); + history.push('/'); } else { - this.props.actions.setToast({ + actions.setToast({ toastMessage: t('hub_create_account_toast_error'), toastClass: 'alert' }); diff --git a/app/hub/Views/HomeView/HomeViewContainer.jsx b/app/hub/Views/HomeView/HomeViewContainer.jsx index 719651a5d..3e2416c82 100644 --- a/app/hub/Views/HomeView/HomeViewContainer.jsx +++ b/app/hub/Views/HomeView/HomeViewContainer.jsx @@ -56,8 +56,9 @@ class HomeViewContainer extends Component { * Function to handle toggling Metrics Opt-In */ _handleToggleMetrics = () => { - const enable_metrics = !this.props.home.enable_metrics; - this.props.actions.setMetrics({ enable_metrics }); + const { actions, home } = this.props; + const enable_metrics = !home.enable_metrics; + actions.setMetrics({ enable_metrics }); } /** @@ -68,7 +69,8 @@ class HomeViewContainer extends Component { _handlePremiumPromoModalClick = (type = 'basic') => { // GH-1777 // we want to show the promo modal exactly once per Hub visit - this.props.actions.markPremiumPromoModalShown(); + const { actions } = this.props; + actions.markPremiumPromoModalShown(); sendMessage('SET_PREMIUM_PROMO_MODAL_SEEN', {}); diff --git a/app/hub/Views/LogInView/LogInViewContainer.jsx b/app/hub/Views/LogInView/LogInViewContainer.jsx index 6c5ef5859..00e711b2c 100644 --- a/app/hub/Views/LogInView/LogInViewContainer.jsx +++ b/app/hub/Views/LogInView/LogInViewContainer.jsx @@ -32,7 +32,8 @@ class LogInViewContainer extends Component { validateInput: false, }; - this.props.actions.setToast({ + const { actions } = this.props; + actions.setToast({ toastMessage: '', toastClass: '', }); @@ -46,7 +47,8 @@ class LogInViewContainer extends Component { const { name, value } = event.target; this.setState({ [name]: value }); - if (!this.state.validateInput) { + const { validateInput } = this.state; + if (!validateInput) { return; } @@ -87,28 +89,29 @@ class LogInViewContainer extends Component { return; } - this.props.actions.setToast({ + const { actions, history } = this.props; + actions.setToast({ toastMessage: '', toastClass: '' }); - this.props.actions.login(email, password).then((success) => { + actions.login(email, password).then((success) => { if (success) { const { origin, pathname, hash } = window.location; window.history.pushState({}, '', `${origin}${pathname}${hash}`); - this.props.actions.getUser(); - this.props.actions.getUserSettings() + actions.getUser(); + actions.getUserSettings() .then((settings) => { const { current_theme } = settings; - return this.props.actions.getTheme(current_theme); + return actions.getTheme(current_theme); }); - this.props.actions.setToast({ + actions.setToast({ toastMessage: t('hub_login_toast_success'), toastClass: 'success' }); - this.props.history.push('/'); + history.push('/'); } else { - this.props.actions.setToast({ + actions.setToast({ toastMessage: t('no_such_email_password_combo'), toastClass: 'alert' }); diff --git a/app/hub/Views/PlusView/PlusViewContainer.jsx b/app/hub/Views/PlusView/PlusViewContainer.jsx index bcd3b0d67..5192768cf 100644 --- a/app/hub/Views/PlusView/PlusViewContainer.jsx +++ b/app/hub/Views/PlusView/PlusViewContainer.jsx @@ -35,7 +35,8 @@ class PlusViewContainer extends Component { * Sends the necessary ping to background */ _sendPlusPing = () => { - this.props.actions.sendPing({ type: 'plus_cta_hub' }); + const { actions } = this.props; + actions.sendPing({ type: 'plus_cta_hub' }); } /** @@ -43,9 +44,10 @@ class PlusViewContainer extends Component { * @return {JSX} JSX for rendering the Plus View of the Hub app */ render() { + const { user } = this.props; return ( ); diff --git a/app/hub/Views/ProductsView/ProductsView.jsx b/app/hub/Views/ProductsView/ProductsView.jsx index 64caa889f..699612982 100644 --- a/app/hub/Views/ProductsView/ProductsView.jsx +++ b/app/hub/Views/ProductsView/ProductsView.jsx @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; * @return {JSX} JSX for rendering the Products View * @memberof HubComponents */ -const ProductsView = props => ( +const ProductsView = ({ onAndroidClick, onIosClick, onLiteClick }) => (
@@ -44,13 +44,13 @@ const ProductsView = props => ( {t('hub_products_main_promo_description')}
- + { this.props.actions.sendPing({ type: 'products_cta_android' }); }} - onIosClick={() => { this.props.actions.sendPing({ type: 'products_cta_ios' }); }} - onLiteClick={() => { this.props.actions.sendPing({ type: 'products_cta_lite' }); }} + onAndroidClick={() => { actions.sendPing({ type: 'products_cta_android' }); }} + onIosClick={() => { actions.sendPing({ type: 'products_cta_ios' }); }} + onLiteClick={() => { actions.sendPing({ type: 'products_cta_lite' }); }} /> ); } diff --git a/app/hub/Views/SetupView/SetupViewContainer.jsx b/app/hub/Views/SetupView/SetupViewContainer.jsx index 0902c1ae7..a025a1fec 100644 --- a/app/hub/Views/SetupView/SetupViewContainer.jsx +++ b/app/hub/Views/SetupView/SetupViewContainer.jsx @@ -42,16 +42,19 @@ class SetupViewContainer extends Component { sendMountActions: false, showModal: false, }; + + const { history } = this.props; if (!props.preventRedirect) { - this.props.history.push('/setup/1'); + history.push('/setup/1'); } const title = t('hub_setup_page_title'); window.document.title = title; - this.props.actions.setSetupStep({ setup_step: 7 }); - this.props.actions.initSetupProps(this.props.setup); - this.props.actions.getSetupShowWarningOverride().then((data) => { + const { actions, setup } = this.props; + actions.setSetupStep({ setup_step: 7 }); + actions.initSetupProps(setup); + actions.getSetupShowWarningOverride().then((data) => { const { setup_show_warning_override } = data; const { justInstalled } = QueryString.parse(window.location.search); const { user } = props; @@ -88,8 +91,9 @@ class SetupViewContainer extends Component { * Function to toggle the Ask Again Checkbox */ _toggleCheckbox = () => { - const { setup_show_warning_override } = this.props.setup; - this.props.actions.setSetupShowWarningOverride({ + const { actions, setup } = this.props; + const { setup_show_warning_override } = setup; + actions.setSetupShowWarningOverride({ setup_show_warning_override: !setup_show_warning_override, }); } @@ -99,13 +103,14 @@ class SetupViewContainer extends Component { */ _setDefaultSettings() { this.setState({ sendMountActions: true }); - this.props.actions.setSetupStep({ setup_step: 8 }); - this.props.actions.setBlockingPolicy({ blockingPolicy: BLOCKING_POLICY_RECOMMENDED }); - this.props.actions.setAntiTracking({ enable_anti_tracking: true }); - this.props.actions.setAdBlock({ enable_ad_block: true }); - this.props.actions.setSmartBlocking({ enable_smart_block: true }); - this.props.actions.setGhosteryRewards({ enable_ghostery_rewards: !IS_FIREFOX }); - this.props.actions.setHumanWeb({ enable_human_web: !IS_FIREFOX }); + const { actions } = this.props; + actions.setSetupStep({ setup_step: 8 }); + actions.setBlockingPolicy({ blockingPolicy: BLOCKING_POLICY_RECOMMENDED }); + actions.setAntiTracking({ enable_anti_tracking: true }); + actions.setAdBlock({ enable_ad_block: true }); + actions.setSmartBlocking({ enable_smart_block: true }); + actions.setGhosteryRewards({ enable_ghostery_rewards: !IS_FIREFOX }); + actions.setHumanWeb({ enable_human_web: !IS_FIREFOX }); } /** @@ -113,7 +118,8 @@ class SetupViewContainer extends Component { * @return {JSX} JSX of the Setup Modal's Children */ _renderModalChildren() { - const { setup_show_warning_override } = this.props.setup; + const { setup } = this.props; + const { setup_show_warning_override } = setup; return (
diff --git a/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteView.jsx b/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteView.jsx index 530d74dff..8143950d2 100644 --- a/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteView.jsx +++ b/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteView.jsx @@ -21,10 +21,10 @@ import { ToggleSwitch } from '../../../../shared-components'; * @return {JSX} JSX for rendering the Setup Anti-Suite View of the Hub app * @memberof HubComponents */ -const SetupAntiSuiteView = props => ( +const SetupAntiSuiteView = ({ features }) => (
- {props.features.map((feature) => { + {features.map((feature) => { const iconClassNames = ClassNames(feature.id, { SetupAntiSuite__icon: true, active: feature.enabled, diff --git a/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx b/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx index b4c25379b..f0ed52027 100644 --- a/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx +++ b/app/hub/Views/SetupViews/SetupAntiSuiteView/SetupAntiSuiteViewContainer.jsx @@ -62,25 +62,26 @@ class SetupAntiSuiteViewContainer extends Component { * @param {Object} featureName the name of the feature being toggled */ _handleToggle = (featureName) => { + const { actions, setup } = this.props; switch (featureName) { case 'anti-tracking': { - const enable_anti_tracking = !this.props.setup.enable_anti_tracking; - this.props.actions.setAntiTracking({ enable_anti_tracking }); + const enable_anti_tracking = !setup.enable_anti_tracking; + actions.setAntiTracking({ enable_anti_tracking }); break; } case 'ad-block': { - const enable_ad_block = !this.props.setup.enable_ad_block; - this.props.actions.setAdBlock({ enable_ad_block }); + const enable_ad_block = !setup.enable_ad_block; + actions.setAdBlock({ enable_ad_block }); break; } case 'smart-blocking': { - const enable_smart_block = !this.props.setup.enable_smart_block; - this.props.actions.setSmartBlocking({ enable_smart_block }); + const enable_smart_block = !setup.enable_smart_block; + actions.setSmartBlocking({ enable_smart_block }); break; } case 'ghostery-rewards': { - const enable_ghostery_rewards = !this.props.setup.enable_ghostery_rewards; - this.props.actions.setGhosteryRewards({ enable_ghostery_rewards }); + const enable_ghostery_rewards = !setup.enable_ghostery_rewards; + actions.setGhosteryRewards({ enable_ghostery_rewards }); break; } default: break; @@ -92,12 +93,13 @@ class SetupAntiSuiteViewContainer extends Component { * @return {JSX} JSX for rendering the Setup Anti-Suite View of the Hub app */ render() { + const { setup } = this.props; const { enable_anti_tracking, enable_ad_block, enable_smart_block, enable_ghostery_rewards, - } = this.props.setup; + } = setup; const anti_tracking_enabled = IS_CLIQZ ? false : enable_anti_tracking; const ad_block_enabled = IS_CLIQZ ? false : enable_ad_block; diff --git a/app/hub/Views/SetupViews/SetupBlockingDropdown/SetupBlockingDropdownContainer.jsx b/app/hub/Views/SetupViews/SetupBlockingDropdown/SetupBlockingDropdownContainer.jsx index 3da70ea20..16424cceb 100644 --- a/app/hub/Views/SetupViews/SetupBlockingDropdown/SetupBlockingDropdownContainer.jsx +++ b/app/hub/Views/SetupViews/SetupBlockingDropdown/SetupBlockingDropdownContainer.jsx @@ -35,7 +35,8 @@ class SetupBlockingDropdownContainer extends Component { * Function to handle navigating back to the Blocking route */ _closeModal = () => { - this.props.history.push('/setup/1'); + const { history } = this.props; + history.push('/setup/1'); } /** @@ -43,9 +44,10 @@ class SetupBlockingDropdownContainer extends Component { * @return {JSX} JSX for rendering the Blocking Dropdown */ render() { + const { actions } = this.props; return ( - + ); } diff --git a/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingView.jsx b/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingView.jsx index 073c6d3d8..5a9e2000e 100644 --- a/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingView.jsx +++ b/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingView.jsx @@ -20,13 +20,18 @@ import ClassNames from 'classnames'; * @return {JSX} JSX for rendering the Setup Blocking View of the Hub app * @memberof HubComponents */ -const SetupBlockingView = props => ( +const SetupBlockingView = ({ + blockingPolicy, + handleSelection, + handleCustomClick, + choices +}) => (
- {props.choices.map((choice) => { - const choiceSelected = choice.name === props.blockingPolicy; + {choices.map((choice) => { + const choiceSelected = choice.name === blockingPolicy; const bigCheckSrc = '/app/images/hub/setup/block-selected.svg'; const choiceBoxClassNames = ClassNames('clickable', 'flex-container', 'flex-dir-column', { SetupBlocking__choiceBox: true, @@ -41,7 +46,7 @@ const SetupBlockingView = props => ( className={choiceBoxClassNames} onClick={() => { if (choice.name === 'BLOCKING_POLICY_CUSTOM') { - props.handleCustomClick(); + handleCustomClick(); } }} > @@ -60,8 +65,8 @@ const SetupBlockingView = props => ( name={choice.name} value={choice.name} id={`input-block-${choice.name}`} - checked={props.blockingPolicy === choice.name} - onChange={props.handleSelection} + checked={blockingPolicy === choice.name} + onChange={handleSelection} />
); @@ -76,6 +81,7 @@ const SetupBlockingView = props => ( SetupBlockingView.propTypes = { blockingPolicy: PropTypes.string.isRequired, handleSelection: PropTypes.func.isRequired, + handleCustomClick: PropTypes.func.isRequired, choices: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string.isRequired, image: PropTypes.string.isRequired, diff --git a/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewContainer.jsx b/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewContainer.jsx index e1b7ac71c..b50fa624f 100644 --- a/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewContainer.jsx +++ b/app/hub/Views/SetupViews/SetupBlockingView/SetupBlockingViewContainer.jsx @@ -57,14 +57,16 @@ class SetupBlockingViewContainer extends Component { */ _handleChange = (event) => { const blockingPolicy = event.target.value; - this.props.actions.setBlockingPolicy({ blockingPolicy }); + const { actions } = this.props; + actions.setBlockingPolicy({ blockingPolicy }); } /** * Function to handle switching to the Custom Blocking route */ _handleCustomClick = () => { - this.props.history.push('/setup/1/custom'); + const { history } = this.props; + history.push('/setup/1/custom'); } /** @@ -72,7 +74,8 @@ class SetupBlockingViewContainer extends Component { * @return {JSX} JSX for rendering the Setup Blocking View of the Hub app */ render() { - const { blockingPolicy } = this.props.setup; + const { setup } = this.props; + const { blockingPolicy } = setup; const choices = [ { name: BLOCKING_POLICY_RECOMMENDED, diff --git a/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingView.test.jsx b/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingView.test.jsx index ae581b658..fa76c3e6f 100644 --- a/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingView.test.jsx +++ b/app/hub/Views/SetupViews/SetupBlockingView/__tests__/SetupBlockingView.test.jsx @@ -36,6 +36,7 @@ describe('app/hub/Views/SetupViews/SetupBlockingView component', () => { }, ], handleSelection: () => {}, + handleCustomClick: () => {}, }; const component = renderer.create( @@ -51,6 +52,7 @@ describe('app/hub/Views/SetupViews/SetupBlockingView component', () => { blockingPolicy: 'test', choices: [], handleSelection: () => {}, + handleCustomClick: () => {}, }; const component = renderer.create( @@ -79,6 +81,7 @@ describe('app/hub/Views/SetupViews/SetupBlockingView component', () => { }, ], handleSelection: () => {}, + handleCustomClick: () => {}, }; const component = shallow(); @@ -94,6 +97,7 @@ describe('app/hub/Views/SetupViews/SetupBlockingView component', () => { blockingPolicy: 'test', choices: [], handleSelection: () => {}, + handleCustomClick: () => {}, }; const component = shallow(); diff --git a/app/hub/Views/SetupViews/SetupDoneView/SetupDoneView.jsx b/app/hub/Views/SetupViews/SetupDoneView/SetupDoneView.jsx index 062edaf23..ca29cded4 100644 --- a/app/hub/Views/SetupViews/SetupDoneView/SetupDoneView.jsx +++ b/app/hub/Views/SetupViews/SetupDoneView/SetupDoneView.jsx @@ -20,7 +20,7 @@ import { NavLink } from 'react-router-dom'; * @return {JSX} JSX for rendering the Setup Done View of the Hub app * @memberof HubComponents */ -const SetupDoneView = props => ( +const SetupDoneView = ({ features }) => (
@@ -33,7 +33,7 @@ const SetupDoneView = props => (
- {props.features.map((feature) => { + {features.map((feature) => { const iconClassNames = `SetupDone__featureIcon feature-${feature.id}`; return ( diff --git a/app/hub/Views/SetupViews/SetupHeader/SetupHeader.jsx b/app/hub/Views/SetupViews/SetupHeader/SetupHeader.jsx index 97f5c2984..02738b5da 100644 --- a/app/hub/Views/SetupViews/SetupHeader/SetupHeader.jsx +++ b/app/hub/Views/SetupViews/SetupHeader/SetupHeader.jsx @@ -19,13 +19,13 @@ import PropTypes from 'prop-types'; * @return {JSX} JSX for rendering the Setup Header View of the Hub app * @memberof HubComponents */ -const SetupHeader = props => ( +const SetupHeader = ({ title, titleImage }) => (
- +
-

+

diff --git a/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebView.jsx b/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebView.jsx index fee183740..700ce3f04 100644 --- a/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebView.jsx +++ b/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebView.jsx @@ -20,17 +20,17 @@ import { ToggleCheckbox } from '../../../../shared-components'; * @return {JSX} JSX for rendering the Setup Human Web View of the Hub app * @memberof HubComponents */ -const SetupHumanWebView = props => ( +const SetupHumanWebView = ({ enableHumanWeb, changeHumanWeb }) => (
- + { t('hub_setup_humanweb_label') }
diff --git a/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx b/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx index e6bce8b7b..9657fc595 100644 --- a/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx +++ b/app/hub/Views/SetupViews/SetupHumanWebView/SetupHumanWebViewContainer.jsx @@ -49,8 +49,9 @@ class SetupHumanWebViewContainer extends Component { * Function to handle toggling Human Web Opt-In */ _handleToggle = () => { - const enable_human_web = !this.props.setup.enable_human_web; - this.props.actions.setHumanWeb({ enable_human_web }); + const { actions, setup } = this.props; + const enable_human_web = !setup.enable_human_web; + actions.setHumanWeb({ enable_human_web }); } /** @@ -58,9 +59,10 @@ class SetupHumanWebViewContainer extends Component { * @return {JSX} JSX for rendering the Setup Human Web View of the Hub app */ render() { + const { setup } = this.props; return ( ); diff --git a/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx b/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx index c91e2cda3..77492fdcf 100644 --- a/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx +++ b/app/hub/Views/SideNavigationView/SideNavigationViewContainer.jsx @@ -33,11 +33,12 @@ class SideNavigationViewContainer extends Component { * Function to handle clicking Log Out */ _handleLogoutClick = () => { - this.props.actions.setToast({ + const { actions } = this.props; + actions.setToast({ toastMessage: '', toastClass: '', }); - this.props.actions.logout(); + actions.logout(); } /** diff --git a/app/hub/Views/SignedInView/SignedInView.jsx b/app/hub/Views/SignedInView/SignedInView.jsx index a1057d938..22dc1c4aa 100644 --- a/app/hub/Views/SignedInView/SignedInView.jsx +++ b/app/hub/Views/SignedInView/SignedInView.jsx @@ -20,7 +20,7 @@ import { ExitButton } from '../../../shared-components'; * @return {JSX} JSX for rendering the Login View of the Hub app * @memberof HubComponents */ -const SignedInView = (props) => { +const SignedInView = ({ email }) => { const signedInAsString = t('hub_signedin_as_email'); return ( @@ -41,7 +41,7 @@ const SignedInView = (props) => {

- {`${signedInAsString} ${props.email}`} + {`${signedInAsString} ${email}`}

diff --git a/app/hub/Views/TutorialView/TutorialViewContainer.jsx b/app/hub/Views/TutorialView/TutorialViewContainer.jsx index 97af3c48c..b60f30ab2 100644 --- a/app/hub/Views/TutorialView/TutorialViewContainer.jsx +++ b/app/hub/Views/TutorialView/TutorialViewContainer.jsx @@ -35,16 +35,17 @@ class TutorialViewContainer extends Component { sendMountActions: false, }; + const { actions, history, tutorial } = this.props; if (!props.preventRedirect) { - this.props.history.push('/tutorial/1'); + history.push('/tutorial/1'); } const title = t('hub_tutorial_page_title'); window.document.title = title; - this.props.actions.initTutorialProps(this.props.tutorial).then(() => { + actions.initTutorialProps(tutorial).then(() => { this.setState({ sendMountActions: true }); - this.props.actions.sendPing({ type: 'tutorial_start' }); + actions.sendPing({ type: 'tutorial_start' }); }); } diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx index a08f8708c..fccaca98e 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx @@ -39,7 +39,8 @@ class TutorialAntiSuiteViewContainer extends Component { }); if (sendMountActions) { - this.props.actions.setTutorialComplete({ + const { actions } = this.props; + actions.setTutorialComplete({ tutorial_complete: true, }); } diff --git a/app/licenses/License.jsx b/app/licenses/License.jsx index 8f2e737d2..46d83883e 100644 --- a/app/licenses/License.jsx +++ b/app/licenses/License.jsx @@ -42,6 +42,7 @@ class License extends React.Component { */ render() { const { license } = this.props; + const { expanded } = this.state; return (
{`${t('license_module')}: ${license.name}`}
@@ -56,7 +57,7 @@ class License extends React.Component {
{t('license_text')} { - this.state.expanded && ( + expanded && (
diff --git a/app/panel-android/components/GlobalTrackers.jsx b/app/panel-android/components/GlobalTrackers.jsx index 103c41183..e2f3e034f 100644 --- a/app/panel-android/components/GlobalTrackers.jsx +++ b/app/panel-android/components/GlobalTrackers.jsx @@ -22,7 +22,8 @@ export default class GlobalTrackers extends React.Component { id: 'blockAllGlobal', name: 'Block All', callback: () => { - this.context.callGlobalAction({ + const { callGlobalAction } = this.context; + callGlobalAction({ actionName: 'blockUnBlockAllTrackers', actionData: { block: true, @@ -35,7 +36,8 @@ export default class GlobalTrackers extends React.Component { id: 'unblockAllGlobal', name: 'Unblock All', callback: () => { - this.context.callGlobalAction({ + const { callGlobalAction } = this.context; + callGlobalAction({ actionName: 'blockUnBlockAllTrackers', actionData: { block: false, @@ -48,7 +50,8 @@ export default class GlobalTrackers extends React.Component { id: 'resetSettings', name: 'Reset Settings', callback: () => { - this.context.callGlobalAction({ + const { callGlobalAction } = this.context; + callGlobalAction({ actionName: 'resetSettings', }); }, @@ -56,7 +59,8 @@ export default class GlobalTrackers extends React.Component { ]; get categories() { - return this.props.categories; + const { categories } = this.props; + return categories; } render() { diff --git a/app/panel-android/components/Overview.jsx b/app/panel-android/components/Overview.jsx index ce30ea60b..bb5aea0dd 100644 --- a/app/panel-android/components/Overview.jsx +++ b/app/panel-android/components/Overview.jsx @@ -18,19 +18,23 @@ import fromTrackersToChartData from '../utils/chart'; export default class Overview extends React.Component { get isTrusted() { - return this.context.siteProps.isTrusted; + const { siteProps } = this.context; + return siteProps.isTrusted; } get isRestricted() { - return this.context.siteProps.isRestricted; + const { siteProps } = this.context; + return siteProps.isRestricted; } get isPaused() { - return this.context.siteProps.isPaused; + const { siteProps } = this.context; + return siteProps.isPaused; } get categories() { - return this.props.categories || []; + const { categories } = this.props; + return categories || []; } get chartData() { @@ -43,27 +47,32 @@ export default class Overview extends React.Component { } get hostName() { - return this.context.siteProps.hostName; + const { siteProps } = this.context; + return siteProps.hostName; } get nTrackersBlocked() { - return this.context.siteProps.nTrackersBlocked; + const { siteProps } = this.context; + return siteProps.nTrackersBlocked; } handleTrustButtonClick = () => { - this.context.callGlobalAction({ + const { callGlobalAction } = this.context; + callGlobalAction({ actionName: 'handleTrustButtonClick', }); } handleRestrictButtonClick = () => { - this.context.callGlobalAction({ + const { callGlobalAction } = this.context; + callGlobalAction({ actionName: 'handleRestrictButtonClick', }); } handlePauseButtonClick = () => { - this.context.callGlobalAction({ + const { callGlobalAction } = this.context; + callGlobalAction({ actionName: 'handlePauseButtonClick', }); } diff --git a/app/panel-android/components/Panel.jsx b/app/panel-android/components/Panel.jsx index 01ea37368..50f53dca5 100644 --- a/app/panel-android/components/Panel.jsx +++ b/app/panel-android/components/Panel.jsx @@ -54,11 +54,13 @@ export default class Panel extends React.Component { } get siteCategories() { - return this.state.blocking.categories || []; + const { blocking } = this.state; + return blocking.categories || []; } get globalCategories() { - return this.state.settings.categories || []; + const { settings } = this.state; + return settings.categories || []; } get chartData() { @@ -71,17 +73,18 @@ export default class Panel extends React.Component { } get siteProps() { - const hostName = this.state.summary.pageHost || ''; + const { summary } = this.state; + const hostName = summary.pageHost || ''; const pageHost = hostName.toLowerCase().replace(/^(http[s]?:\/\/)?(www\.)?/, ''); - const siteWhitelist = this.state.summary.site_whitelist || []; - const siteBlacklist = this.state.summary.site_blacklist || []; + const siteWhitelist = summary.site_whitelist || []; + const siteBlacklist = summary.site_blacklist || []; const isTrusted = siteWhitelist.indexOf(pageHost) !== -1; const isRestricted = siteBlacklist.indexOf(pageHost) !== -1; - const isPaused = this.state.summary.paused_blocking; + const isPaused = summary.paused_blocking; - const nTrackersBlocked = (this.state.summary.trackerCounts || {}).blocked || 0; + const nTrackersBlocked = (summary.trackerCounts || {}).blocked || 0; return { hostName, pageHost, isTrusted, isRestricted, isPaused, nTrackersBlocked @@ -131,7 +134,7 @@ export default class Panel extends React.Component { setGlobalState = (updated) => { const newState = {}; Object.keys(updated).forEach((key) => { - newState[key] = { ...this.state[key], ...updated[key] }; + newState[key] = { ...this.state[key], ...updated[key] }; // eslint-disable-line react/destructuring-assignment }); this.setState(newState); @@ -145,6 +148,8 @@ export default class Panel extends React.Component { } render() { + const { panel } = this.props; + const { cliqzModuleData } = this.state; return (
@@ -164,7 +169,7 @@ export default class Panel extends React.Component { - + diff --git a/app/panel-android/components/SiteTrackers.jsx b/app/panel-android/components/SiteTrackers.jsx index 5ab25770f..be4937e1a 100644 --- a/app/panel-android/components/SiteTrackers.jsx +++ b/app/panel-android/components/SiteTrackers.jsx @@ -22,7 +22,8 @@ export default class SiteTrackers extends React.Component { id: 'blockAllSite', name: 'Block All', callback: () => { - this.context.callGlobalAction({ + const { callGlobalAction } = this.context; + callGlobalAction({ actionName: 'blockUnBlockAllTrackers', actionData: { block: true, @@ -35,7 +36,8 @@ export default class SiteTrackers extends React.Component { id: 'unblockAllSite', name: 'Unblock All', callback: () => { - this.context.callGlobalAction({ + const { callGlobalAction } = this.context; + callGlobalAction({ actionName: 'blockUnBlockAllTrackers', actionData: { block: false, @@ -47,13 +49,14 @@ export default class SiteTrackers extends React.Component { ] render() { + const { categories } = this.props; return (

Trackers on this site

- +
); } diff --git a/app/panel-android/components/content/Accordion.jsx b/app/panel-android/components/content/Accordion.jsx index 7ec893cee..aaa463d6a 100644 --- a/app/panel-android/components/content/Accordion.jsx +++ b/app/panel-android/components/content/Accordion.jsx @@ -47,12 +47,14 @@ export default class Accordion extends React.Component { } get blockingStatus() { - if (this.props.type === 'site-trackers') { - if (this.context.siteProps.isTrusted) { + const { type, numBlocked, numTotal } = this.props; + const { siteProps } = this.context; + if (type === 'site-trackers') { + if (siteProps.isTrusted) { return 'trusted'; } - if (this.context.siteProps.isRestricted) { + if (siteProps.isRestricted) { return 'restricted'; } @@ -70,7 +72,7 @@ export default class Accordion extends React.Component { } } - if (this.props.numBlocked === this.props.numTotal) { + if (numBlocked === numTotal) { return 'blocked'; } @@ -78,17 +80,24 @@ export default class Accordion extends React.Component { } getTrackers = (force = false) => { - if (!this.state.isActive && !force) { + const { id, getTrackersFromCategory } = this.props; + const { isActive } = this.state; + if (!isActive && !force) { return []; } - return this.props.getTrackersFromCategory(this.props.id); + return getTrackersFromCategory(id); } - getMenuOpenStatus = index => index === this.state.openMenuIndex; + getMenuOpenStatus = (index) => { + const { openMenuIndex } = this.state; + return index === openMenuIndex; + } checkAndUpdateData = () => { - if (this.unMounted || !this.state.isActive || this.state.currentItemsLength >= this.props.numTotal) { + const { numTotal } = this.props; + const { isActive, currentItemsLength } = this.state; + if (this.unMounted || !isActive || currentItemsLength >= numTotal) { return; } @@ -100,14 +109,15 @@ export default class Accordion extends React.Component { // Try lo load more when needed if (scrollTop + window.innerHeight - (accordionContentNode.offsetTop + boundingRect.height) > -needToUpdateHeight) { this.setState((prevState) => { - const itemsLength = Math.min(prevState.currentItemsLength + this.nExtraItems, this.props.numTotal); + const itemsLength = Math.min(prevState.currentItemsLength + this.nExtraItems, numTotal); return { currentItemsLength: itemsLength }; }); } } toggleMenu = (index) => { - if (this.state.openMenuIndex === index) { + const { openMenuIndex } = this.state; + if (openMenuIndex === index) { this.setState({ openMenuIndex: -1 }); } else { this.setState({ openMenuIndex: index }); @@ -129,11 +139,13 @@ export default class Accordion extends React.Component { } toggleContent = () => { - this.props.toggleAccordion(this.props.index); + const { index, toggleAccordion, numTotal } = this.props; + const { isActive } = this.state; + toggleAccordion(index); // Show some trackers when this category is expanded - const currentState = this.state.isActive; - const itemsLength = Math.min(this.nExtraItems, this.props.numTotal); + const currentState = isActive; + const itemsLength = Math.min(this.nExtraItems, numTotal); this.setState({ isActive: !currentState, currentItemsLength: itemsLength, @@ -141,47 +153,60 @@ export default class Accordion extends React.Component { } handleCategoryClicked = () => { + const { id, type } = this.props; + const { callGlobalAction } = this.context; if (!this.blockingStatus) { - const type = this.props.type === 'site-trackers' ? 'site' : 'global'; - this.context.callGlobalAction({ + const blockingType = type === 'site-trackers' ? 'site' : 'global'; + callGlobalAction({ actionName: 'blockUnBlockAllTrackers', actionData: { block: true, - type, - categoryId: this.props.id, + type: blockingType, + categoryId: id, } }); } else if (this.blockingStatus === 'blocked') { - const type = this.props.type === 'site-trackers' ? 'site' : 'global'; - this.context.callGlobalAction({ + const blockingType = type === 'site-trackers' ? 'site' : 'global'; + callGlobalAction({ actionName: 'blockUnBlockAllTrackers', actionData: { block: false, - type, - categoryId: this.props.id, + type: blockingType, + categoryId: id, } }); } } render() { - const titleStyle = { backgroundImage: `url(/app/images/panel-android/categories/${this.props.logo}.svg)` }; - const contentStyle = { '--trackers-length': `${this.props.open ? (this.state.currentItemsLength * this.itemHeight) + this.headerheight : 0}px` }; + const { + index, + open, + numBlocked, + name, + numTotal, + logo, + id, + type + } = this.props; + const { isActive, currentItemsLength } = this.state; + const titleStyle = { backgroundImage: `url(/app/images/panel-android/categories/${logo}.svg)` }; + const contentStyle = { '--trackers-length': `${open ? (currentItemsLength * this.itemHeight) + this.headerheight : 0}px` }; return ( -
+
-
-

{this.props.name}

+
+

{name}

- {this.props.numTotal} + {numTotal} {' '} TRACKERS - {!!this.props.numBlocked && ( + {!!numBlocked && ( - {this.props.numBlocked} + {numBlocked} {' '} Blocked @@ -197,15 +222,15 @@ export default class Accordion extends React.Component { Blocked

    - {this.getTrackers(true).slice(0, this.state.currentItemsLength).map((tracker, index) => ( + {this.getTrackers(true).slice(0, currentItemsLength).map((tracker, ind) => ( ))}
diff --git a/app/panel-android/components/content/Accordions.jsx b/app/panel-android/components/content/Accordions.jsx index 031f009e7..99e4b7347 100644 --- a/app/panel-android/components/content/Accordions.jsx +++ b/app/panel-android/components/content/Accordions.jsx @@ -24,15 +24,20 @@ class Accordions extends React.Component { }; } - getOpenStatus = index => index === this.state.openAccordionIndex; + getOpenStatus = (index) => { + const { openAccordionIndex } = this.state; + return index === openAccordionIndex; + } getTrackersFromCategory = (categoryId) => { - const category = this.props.categories[this.props.categories.findIndex(cat => cat.id === categoryId)]; + const { categories } = this.props; + const category = categories[categories.findIndex(cat => cat.id === categoryId)]; return category.trackers; } toggleAccordion = (index) => { - if (this.state.openAccordionIndex === index) { + const { openAccordionIndex } = this.state; + if (openAccordionIndex === index) { this.setState({ openAccordionIndex: -1 }); } else { this.setState({ openAccordionIndex: index }); @@ -40,10 +45,11 @@ class Accordions extends React.Component { } render() { + const { categories, type } = this.props; return (
{ - this.props.categories.map((category, index) => ( + categories.map((category, index) => ( )) } diff --git a/app/panel-android/components/content/ChartSVG.jsx b/app/panel-android/components/content/ChartSVG.jsx index c047d2120..9c436ce1b 100644 --- a/app/panel-android/components/content/ChartSVG.jsx +++ b/app/panel-android/components/content/ChartSVG.jsx @@ -24,8 +24,10 @@ export default class ChartSVG extends React.Component { } increaseN = () => { - let currentN = this.state.nItem; - if (currentN < this.props.paths.length) { + const { paths } = this.props; + const { nItem } = this.state; + let currentN = nItem; + if (currentN < paths.length) { this.setState({ nItem: currentN += 1, }); @@ -33,13 +35,14 @@ export default class ChartSVG extends React.Component { } render() { - const { radius } = this.props; - let paths = this.props.paths.slice(0, this.state.nItem).map((element, index) => ( + const { paths, radius } = this.props; + const { nItem } = this.state; + let computedPaths = paths.slice(0, nItem).map((element, index) => ( // eslint-disable-next-line react/no-array-index-key )); - if (paths.length === 0) { + if (computedPaths.length === 0) { // When there is no tracker const defaultElement = { start: 0, @@ -47,7 +50,7 @@ export default class ChartSVG extends React.Component { category: 'default', }; - paths = ( + computedPaths = ( - {paths} + {computedPaths} ); diff --git a/app/panel-android/components/content/DotsMenu.jsx b/app/panel-android/components/content/DotsMenu.jsx index 2a3b44833..7cc17d440 100644 --- a/app/panel-android/components/content/DotsMenu.jsx +++ b/app/panel-android/components/content/DotsMenu.jsx @@ -33,7 +33,8 @@ export default class DotsMenu extends React.Component { /* Close the menu if user clicks anywhere on the window */ handleClick = (event) => { - if (this.state.opening && event.target.className.indexOf('dots-menu-btn') === -1) { + const { opening } = this.state; + if (opening && event.target.className.indexOf('dots-menu-btn') === -1) { this.setState({ opening: false, }); @@ -42,7 +43,8 @@ export default class DotsMenu extends React.Component { /* Toggle menu */ dotsButtonClicked = () => { - const currentState = this.state.opening; + const { opening } = this.state; + const currentState = opening; this.setState({ opening: !currentState, @@ -50,12 +52,14 @@ export default class DotsMenu extends React.Component { } render() { + const { actions } = this.props; + const { opening } = this.state; return (
diff --git a/app/panel-android/components/content/FixedMenu.jsx b/app/panel-android/components/content/FixedMenu.jsx index d90dab464..7a6a153e8 100644 --- a/app/panel-android/components/content/FixedMenu.jsx +++ b/app/panel-android/components/content/FixedMenu.jsx @@ -29,7 +29,8 @@ export default class FixedMenu extends React.Component { } get cliqzModuleData() { - return this.props.cliqzModuleData || {}; + const { cliqzModuleData } = this.props; + return cliqzModuleData || {}; } get antiTrackingData() { @@ -41,7 +42,8 @@ export default class FixedMenu extends React.Component { } get smartBlockData() { - return this.props.panel.smartBlock || {}; + const { panel } = this.props; + return panel.smartBlock || {}; } getCount = (type) => { @@ -79,7 +81,8 @@ export default class FixedMenu extends React.Component { } toggleMenu = () => { - const currentState = this.state.open; + const { open } = this.state; + const currentState = open; this.setState({ open: !currentState, }); @@ -94,15 +97,17 @@ export default class FixedMenu extends React.Component { } render() { + const { panel } = this.props; + const { open, currentMenuItemText } = this.state; return ( -
+
-

{this.state.currentMenuItemText}

+

{currentMenuItemText}

  • { + const { updateHeadeText, title } = this.props; this.setState({ opening: true, }); - this.props.updateHeadeText(this.props.title); + updateHeadeText(title); } closeButtonClicked = () => { + const { updateHeadeText } = this.props; this.setState({ opening: false, }); - this.props.updateHeadeText(''); + updateHeadeText(''); } switcherClicked = () => { - this.context.callGlobalAction({ + const { active, type } = this.props; + const { callGlobalAction } = this.context; + callGlobalAction({ actionName: 'cliqzFeatureToggle', actionData: { - currentState: this.props.active, - type: this.props.type, + currentState: active, + type, }, }); } render() { + const { + type, + numData, + title, + description, + active, + headline, + } = this.props; + const { opening } = this.state; return (
    - {this.props.numData} - {this.props.title} -

    {this.props.description}

    + {numData} + {title} +

    {description}

    - -
    - {this.props.numData} -

    {this.props.headline}

    -

    {this.props.description}

    + +
    + {numData} +

    {headline}

    +

    {description}

    diff --git a/app/panel-android/components/content/Path.jsx b/app/panel-android/components/content/Path.jsx index 2ee06c8fd..1534c973c 100644 --- a/app/panel-android/components/content/Path.jsx +++ b/app/panel-android/components/content/Path.jsx @@ -29,7 +29,8 @@ export default class Path extends React.Component { // Check and call props.handler() if the animationEnd event doesn't get fired somehow this.timer = setInterval(() => { clearInterval(this.timer); // Run this only once - this.props.handler(); + const { handler } = this.props; + handler(); }, INTERVAL); } @@ -39,7 +40,8 @@ export default class Path extends React.Component { onAnimationEndHandler = () => { clearInterval(this.timer); - this.props.handler(); + const { handler } = this.props; + handler(); } static polarToCartesian(centerX, centerY, radius, angleInDegrees) { diff --git a/app/panel-android/components/content/Tab.jsx b/app/panel-android/components/content/Tab.jsx index c5ddfb740..07ee41c0c 100644 --- a/app/panel-android/components/content/Tab.jsx +++ b/app/panel-android/components/content/Tab.jsx @@ -17,17 +17,19 @@ import PropTypes from 'prop-types'; export default class Tab extends React.Component { handleTabClick = (event) => { event.preventDefault(); - this.props.onClick(this.props.tabIndex); + const { onClick, tabIndex } = this.props; + onClick(tabIndex); } render() { + const { isActive, tabLabel, linkClassName } = this.props; return (
  • - {this.props.tabLabel} + {tabLabel}
  • ); diff --git a/app/panel-android/components/content/Tabs.jsx b/app/panel-android/components/content/Tabs.jsx index c37eefdf5..50c0a52fb 100644 --- a/app/panel-android/components/content/Tabs.jsx +++ b/app/panel-android/components/content/Tabs.jsx @@ -23,7 +23,8 @@ export default class Tabs extends React.Component { } handleTabClick = (tabIndex) => { - if (tabIndex === this.state.activeTabIndex) { + const { activeTabIndex } = this.state; + if (tabIndex === activeTabIndex) { return; } @@ -32,11 +33,15 @@ export default class Tabs extends React.Component { }); } - renderTabsNav = () => React.Children.map(this.props.children, (child, index) => React.cloneElement(child, { - onClick: this.handleTabClick, - tabIndex: index, - isActive: index === this.state.activeTabIndex - })); + renderTabsNav = () => { + const { children } = this.props; + const { activeTabIndex } = this.state; + return React.Children.map(children, (child, index) => React.cloneElement(child, { + onClick: this.handleTabClick, + tabIndex: index, + isActive: index === activeTabIndex + })); + } renderActiveTabContent = () => { const { children } = this.props; diff --git a/app/panel-android/components/content/TrackerItem.jsx b/app/panel-android/components/content/TrackerItem.jsx index 78e178239..aec6ebfef 100644 --- a/app/panel-android/components/content/TrackerItem.jsx +++ b/app/panel-android/components/content/TrackerItem.jsx @@ -17,26 +17,28 @@ import getUrlFromTrackerId from '../../utils/tracker-info'; export default class TrackerItem extends React.Component { get trackerSelectStatus() { + const { type, tracker } = this.props; + const { siteProps } = this.context; // Only for site trackers - if (this.props.type === 'site-trackers') { - if (this.context.siteProps.isTrusted) { + if (type === 'site-trackers') { + if (siteProps.isTrusted) { return 'trusted'; } - if (this.context.siteProps.isRestricted) { + if (siteProps.isRestricted) { return 'restricted'; } } - if (this.props.tracker.ss_allowed) { + if (tracker.ss_allowed) { return 'trusted'; } - if (this.props.tracker.ss_blocked) { + if (tracker.ss_blocked) { return 'restricted'; } - if (this.props.tracker.blocked) { + if (tracker.blocked) { return 'blocked'; } @@ -44,7 +46,8 @@ export default class TrackerItem extends React.Component { } get showMenu() { - return this.props.showMenu; + const { showMenu } = this.props; + return showMenu; } get disabledStatus() { @@ -52,100 +55,115 @@ export default class TrackerItem extends React.Component { } clickButtonTrust = () => { - const ss_allowed = !this.props.tracker.ss_allowed; + const { + tracker, categoryId, index, toggleMenu + } = this.props; + const { callGlobalAction } = this.context; + const ss_allowed = !tracker.ss_allowed; - this.context.callGlobalAction({ + callGlobalAction({ actionName: 'trustRestrictBlockSiteTracker', actionData: { - app_id: this.props.tracker.id, - cat_id: this.props.categoryId, + app_id: tracker.id, + cat_id: categoryId, trust: ss_allowed, restrict: false, - block: this.props.tracker.blocked, // Keep blocking + block: tracker.blocked, // Keep blocking } }); - this.props.toggleMenu(this.props.index); // Hide menu + toggleMenu(index); // Hide menu } clickButtonRestrict = () => { - const ss_blocked = !this.props.tracker.ss_blocked; - this.context.callGlobalAction({ + const { + tracker, categoryId, index, toggleMenu + } = this.props; + const { callGlobalAction } = this.context; + const ss_blocked = !tracker.ss_blocked; + callGlobalAction({ actionName: 'trustRestrictBlockSiteTracker', actionData: { - app_id: this.props.tracker.id, - cat_id: this.props.categoryId, + app_id: tracker.id, + cat_id: categoryId, restrict: ss_blocked, trust: false, - block: this.props.tracker.blocked, // Keep blocking + block: tracker.blocked, // Keep blocking } }); - this.props.toggleMenu(this.props.index); + toggleMenu(index); } clickButtonBlock = (hideMenu = true) => { // onClick={(e) => { e.stopPropagation(); this.clickButtonBlock(false); }} + const { + tracker, type, categoryId, index, toggleMenu + } = this.props; + const { callGlobalAction } = this.context; if (this.disabledStatus) { return; } - const blocked = !this.props.tracker.blocked; + const blocked = !tracker.blocked; - if (this.props.type === 'site-trackers') { - this.context.callGlobalAction({ + if (type === 'site-trackers') { + callGlobalAction({ actionName: 'trustRestrictBlockSiteTracker', actionData: { - app_id: this.props.tracker.id, - cat_id: this.props.categoryId, + app_id: tracker.id, + cat_id: categoryId, block: blocked, trust: false, restrict: false, } }); } else { - this.context.callGlobalAction({ + callGlobalAction({ actionName: 'blockUnblockGlobalTracker', actionData: { - app_id: this.props.tracker.id, - cat_id: this.props.categoryId, + app_id: tracker.id, + cat_id: categoryId, block: blocked, } }); } if (hideMenu) { - this.props.toggleMenu(this.props.index); + toggleMenu(index); } } openTrackerLink = () => { - const url = getUrlFromTrackerId(this.props.tracker.id); + const { tracker } = this.props; + const url = getUrlFromTrackerId(tracker.id); const win = window.open(url, '_blank'); win.focus(); } toggleMenu = () => { - this.props.toggleMenu(this.props.index); + const { index, toggleMenu } = this.props; + toggleMenu(index); } render() { + const { tracker, type } = this.props; return (
diff --git a/app/panel-android/components/content/TrackersChart.jsx b/app/panel-android/components/content/TrackersChart.jsx index 89879086b..8781aa20f 100644 --- a/app/panel-android/components/content/TrackersChart.jsx +++ b/app/panel-android/components/content/TrackersChart.jsx @@ -27,12 +27,14 @@ class TrackersChart extends React.Component { } render() { + const { num, paths } = this.props; + const { config } = this.state; return (
- +

- {this.props.num} + {num} {' '} Trackers found diff --git a/app/panel/components/AccountSuccess.jsx b/app/panel/components/AccountSuccess.jsx index 66061f210..7d02f66ab 100644 --- a/app/panel/components/AccountSuccess.jsx +++ b/app/panel/components/AccountSuccess.jsx @@ -18,15 +18,15 @@ import { Link } from 'react-router-dom'; * in place of Sign In view on successful signing. * @memberof PanelClasses */ -const AccountSuccess = (props) => ( // eslint-disable-line arrow-parens +const AccountSuccess = ({ email, is_expert }) => ( // eslint-disable-line arrow-parens

{ t('panel_signin_success_title') }

{ t('panel_signin_success') }

-

{ props.email }

- +

{ email }

+ { t('panel_view_trackers') }
diff --git a/app/panel/components/Blocking.jsx b/app/panel/components/Blocking.jsx index 72edbbb8b..31328d1a1 100644 --- a/app/panel/components/Blocking.jsx +++ b/app/panel/components/Blocking.jsx @@ -75,17 +75,20 @@ class Blocking extends React.Component { * Lifecycle event */ componentDidUpdate(prevProps) { + const { + actions, filter, categories, smartBlock, smartBlockActive + } = this.props; // methods here will run after categories is assigned - if (prevProps.filter.type !== this.props.filter.type - || prevProps.filter.name !== this.props.filter.name) { + if (prevProps.filter.type !== filter.type + || prevProps.filter.name !== filter.name) { this.filterTrackers(); } // Update the summary blocking count whenever the blocking component updated. // This will also show pending blocking changes if the panel is re-opened // before a page refresh - const smartBlock = (this.props.smartBlockActive && this.props.smartBlock) || { blocked: {}, unblocked: {} }; - updateSummaryBlockingCount(this.props.categories, smartBlock, this.props.actions.updateTrackerCounts); + const computedSmartBlock = (smartBlockActive && smartBlock) || { blocked: {}, unblocked: {} }; + updateSummaryBlockingCount(categories, computedSmartBlock, actions.updateTrackerCounts); } /** @@ -101,8 +104,9 @@ class Blocking extends React.Component { * @param {string} filterName */ setShow(filterName) { - const updated_categories = JSON.parse(JSON.stringify(this.props.categories)); // deep clone - const updatedUnknownCategory = JSON.parse(JSON.stringify(this.props.unknownCategory)); // deep clone + const { actions, categories, unknownCategory } = this.props; + const updated_categories = JSON.parse(JSON.stringify(categories)); // deep clone + const updatedUnknownCategory = JSON.parse(JSON.stringify(unknownCategory)); // deep clone updated_categories.forEach((categoryEl) => { let count = 0; @@ -122,8 +126,8 @@ class Blocking extends React.Component { }); updatedUnknownCategory.hide = !(filterName === 'all' || filterName === 'unknown'); - this.props.actions.updateCategories(updated_categories); - this.props.actions.updateUnknownCategoryHide(updatedUnknownCategory); + actions.updateCategories(updated_categories); + actions.updateUnknownCategoryHide(updatedUnknownCategory); } /** @@ -131,12 +135,13 @@ class Blocking extends React.Component { * those that are blocked. Trigger action. */ setBlockedShow() { - const updated_categories = JSON.parse(JSON.stringify(this.props.categories)); // deep clone + const { actions, categories, smartBlockActive } = this.props; + const updated_categories = JSON.parse(JSON.stringify(categories)); // deep clone updated_categories.forEach((categoryEl) => { let count = 0; categoryEl.trackers.forEach((trackerEl) => { - const isSbBlocked = this.props.smartBlockActive && trackerEl.warningSmartBlock; + const isSbBlocked = smartBlockActive && trackerEl.warningSmartBlock; if ((trackerEl.blocked && !trackerEl.ss_allowed) || isSbBlocked || trackerEl.ss_blocked) { trackerEl.shouldShow = true; count++; @@ -147,7 +152,7 @@ class Blocking extends React.Component { categoryEl.num_shown = count; }); - this.props.actions.updateCategories(updated_categories); + actions.updateCategories(updated_categories); } /** @@ -155,7 +160,8 @@ class Blocking extends React.Component { * those that have warnings. Trigger action. */ setWarningShow() { - const updated_categories = JSON.parse(JSON.stringify(this.props.categories)); // deep clone + const { actions, categories } = this.props; + const updated_categories = JSON.parse(JSON.stringify(categories)); // deep clone updated_categories.forEach((categoryEl) => { let count = 0; @@ -171,7 +177,7 @@ class Blocking extends React.Component { categoryEl.num_shown = count; }); - this.props.actions.updateCategories(updated_categories); + actions.updateCategories(updated_categories); } /** @@ -179,7 +185,8 @@ class Blocking extends React.Component { * that have compatibility warnings. Trigger action. */ setWarningCompatibilityShow() { - const updated_categories = JSON.parse(JSON.stringify(this.props.categories)); // deep clone + const { actions, categories } = this.props; + const updated_categories = JSON.parse(JSON.stringify(categories)); // deep clone updated_categories.forEach((categoryEl) => { let count = 0; @@ -195,7 +202,7 @@ class Blocking extends React.Component { categoryEl.num_shown = count; }); - this.props.actions.updateCategories(updated_categories); + actions.updateCategories(updated_categories); } /** @@ -203,7 +210,8 @@ class Blocking extends React.Component { * have slow/insecure warnings. Trigger action. */ setWarningSlowInsecureShow() { - const updated_categories = JSON.parse(JSON.stringify(this.props.categories)); // deep clone + const { actions, categories } = this.props; + const updated_categories = JSON.parse(JSON.stringify(categories)); // deep clone updated_categories.forEach((categoryEl) => { let count = 0; @@ -219,16 +227,17 @@ class Blocking extends React.Component { categoryEl.num_shown = count; }); - this.props.actions.updateCategories(updated_categories); + actions.updateCategories(updated_categories); } /** * Handles messages from dynamic UI port to background */ handlePortMessage(msg) { + const { actions } = this.props; if (msg.to !== 'blocking' || !msg.body) { return; } - this.props.actions.updateBlockingData(msg.body); + actions.updateBlockingData(msg.body); } /** @@ -250,11 +259,12 @@ class Blocking extends React.Component { * @param {Object} props */ updateBlockingClasses(props) { + const { blockingClasses } = this.state; const classes = Blocking.buildBlockingClasses(props); - const blockingClasses = classes.join(' '); + const joinedBlockingClasses = classes.join(' '); - if (this.state.blockingClasses !== blockingClasses) { - this.setState({ blockingClasses }); + if (blockingClasses !== joinedBlockingClasses) { + this.setState({ blockingClasses: joinedBlockingClasses }); } } @@ -277,10 +287,11 @@ class Blocking extends React.Component { * @param {Object} props nextProps */ updateSiteNotScanned(props) { - const disableBlocking = Blocking.computeSiteNotScanned(props); + const { disableBlocking } = this.state; + const computeDisableBlocking = Blocking.computeSiteNotScanned(props); - if (this.state.disableBlocking !== disableBlocking) { - this.setState({ disableBlocking }); + if (disableBlocking !== computeDisableBlocking) { + this.setState({ disableBlocking: computeDisableBlocking }); } } diff --git a/app/panel/components/Blocking/BlockingHeader.jsx b/app/panel/components/Blocking/BlockingHeader.jsx index 19366709b..5bf74cc81 100644 --- a/app/panel/components/Blocking/BlockingHeader.jsx +++ b/app/panel/components/Blocking/BlockingHeader.jsx @@ -50,22 +50,25 @@ class BlockingHeader extends React.Component { * Lifecycle event */ componentDidMount() { - if (this.props.categories) { - const updates = BlockingHeader.updateBlockAll(this.props.categories, this.state.fromHere); + const { + actions, categories, smartBlock, smartBlockActive + } = this.props; + const { fromHere } = this.state; + if (categories) { + const updates = BlockingHeader.updateBlockAll(categories, fromHere); if (updates) { - const { - allBlocked, - fromHere, - filtered - } = updates; - this.setState({ allBlocked, fromHere, filtered }); + this.setState({ + allBlocked: updates.allBlocked, + fromHere: updates.fromHere, + filtered: updates.filtered + }); } } - if (typeof this.props.actions.updateTrackerCounts === 'function') { + if (typeof actions.updateTrackerCounts === 'function') { // if we're on GlobalSettings, we don't need to run this function - const smartBlock = (this.props.smartBlockActive && this.props.smartBlock) || { blocked: {}, unblocked: {} }; - updateSummaryBlockingCount(this.props.categories, smartBlock, this.props.actions.updateTrackerCounts); + const calcSmartBlock = (smartBlockActive && smartBlock) || { blocked: {}, unblocked: {} }; + updateSummaryBlockingCount(categories, calcSmartBlock, actions.updateTrackerCounts); } } @@ -117,8 +120,9 @@ class BlockingHeader extends React.Component { * Trigger action which expands/contracts all categories. */ clickExpandAll() { - const newState = !this.props.expandAll; - this.props.actions.toggleExpandAll(newState); + const { actions, expandAll } = this.props; + const newState = !expandAll; + actions.toggleExpandAll(newState); } /** @@ -127,33 +131,43 @@ class BlockingHeader extends React.Component { * blocking view. Update counters in Summary view accordingly. */ clickBlockAll() { - const globalBlocking = !!this.props.globalBlocking; - if (this.props.categories) { + const { + actions, + categories, + globalBlocking, + paused_blocking, + sitePolicy, + smartBlock, + smartBlockActive, + showToast + } = this.props; + const globalBlockingBool = !!globalBlocking; + if (categories) { this.setState({ fromHere: true }, () => { const { allBlocked } = this.state; - if ((this.props.paused_blocking || this.props.sitePolicy) && !globalBlocking) { + if ((paused_blocking || sitePolicy) && !globalBlockingBool) { return; } - this.props.actions.updateBlockAllTrackers({ - smartBlockActive: this.props.smartBlockActive, - smartBlock: this.props.smartBlock, + actions.updateBlockAllTrackers({ + smartBlockActive, + smartBlock, allBlocked, }); - if (typeof this.props.actions.updateTrackerCounts === 'function') { + if (typeof actions.updateTrackerCounts === 'function') { // if we're on GlobalSettings, we don't need to run this function - const smartBlock = (this.props.smartBlockActive && this.props.smartBlock) || { blocked: {}, unblocked: {} }; - updateSummaryBlockingCount(this.props.categories, smartBlock, this.props.actions.updateTrackerCounts); + const calcSmartBlock = (smartBlockActive && smartBlock) || { blocked: {}, unblocked: {} }; + updateSummaryBlockingCount(categories, calcSmartBlock, actions.updateTrackerCounts); } - this.props.actions.showNotification({ + actions.showNotification({ updated: 'globalBLockAll', reload: true, }); - if (globalBlocking) { - this.props.showToast({ + if (globalBlockingBool) { + showToast({ text: t('global_settings_saved') }); } @@ -166,9 +180,11 @@ class BlockingHeader extends React.Component { * Applicable to Global Tracking view in Settings. */ handleSubmit(event) { + const { actions } = this.props; + const { searchValue } = this.state; if (event.keyCode === 13) { - if (this.state.searchValue) { - this.props.actions.toggleExpandAll(true); + if (searchValue) { + actions.toggleExpandAll(true); } } } @@ -182,9 +198,10 @@ class BlockingHeader extends React.Component { * @param {Object} event keyboard event */ updateValue(event) { + const { actions } = this.props; const query = event.currentTarget.value ? event.currentTarget.value.toLowerCase() : ''; this.setState({ searchValue: query }); - this.props.actions.updateSearchValue(query); + actions.updateSearchValue(query); } /** @@ -207,7 +224,8 @@ class BlockingHeader extends React.Component { * @param {Object} event mouseclick event */ filterAll() { - this.props.actions.filter('all'); + const { actions } = this.props; + actions.filter('all'); this.setState({ filterMenuOpened: false }); } @@ -220,7 +238,8 @@ class BlockingHeader extends React.Component { * @param {Object} event mouseclick event */ filterBlocked() { - this.props.actions.filter('blocked'); + const { actions } = this.props; + actions.filter('blocked'); this.setState({ filterMenuOpened: false }); } @@ -233,7 +252,8 @@ class BlockingHeader extends React.Component { * @param {Object} event mouseclick event */ filterUnblocked() { - this.props.actions.filter('unblocked'); + const { actions } = this.props; + actions.filter('unblocked'); this.setState({ filterMenuOpened: false }); } @@ -246,7 +266,8 @@ class BlockingHeader extends React.Component { * @param {Object} event mouseclick event */ filterNew() { - this.props.actions.filter('new'); + const { actions } = this.props; + actions.filter('new'); this.setState({ filterMenuOpened: false }); } @@ -255,22 +276,28 @@ class BlockingHeader extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - const globalBlocking = !!this.props.globalBlocking; - const blockText = this.state.allBlocked ? - (this.state.filtered ? t('blocking_unblock_shown') : t('blocking_unblock_all')) : - (this.state.filtered ? t('blocking_block_shown') : t('blocking_block_all')); + const { + globalBlocking, categories, expandAll, filterText + } = this.props; + const { + allBlocked, filtered, searchValue, filterMenuOpened + } = this.state; + const globalBlockingBool = !!globalBlocking; + const blockText = allBlocked ? + (filtered ? t('blocking_unblock_shown') : t('blocking_unblock_all')) : + (filtered ? t('blocking_block_shown') : t('blocking_block_all')); return (
- { globalBlocking ? t('settings_global_blocking') : t('blocking_trackers') } + { globalBlockingBool ? t('settings_global_blocking') : t('blocking_trackers') } {' '}
- {this.props.categories && this.props.categories.length > 0 && ( + {categories && categories.length > 0 && (
{ - globalBlocking && ( + globalBlockingBool && (
- +
@@ -294,23 +321,23 @@ class BlockingHeader extends React.Component { }
- {this.props.categories && this.props.categories.length > 0 && ( + {categories && categories.length > 0 && ( - { (!this.props.expandAll) ? t('expand_all') : t('collapse_all') } + { (!expandAll) ? t('expand_all') : t('collapse_all') } )}
{ - globalBlocking && ( + globalBlockingBool && (
{}} + onClickOutside={filterMenuOpened ? this.clickFilterText : () => {}} > -
- {this.props.filterText} +
+ {filterText}
-
+
{t('settings_filter_all')}
{t('settings_filter_blocked')}
{t('settings_filter_unblocked')}
diff --git a/app/panel/components/Blocking/Categories.jsx b/app/panel/components/Blocking/Categories.jsx index a51b58882..3c5dd05b7 100644 --- a/app/panel/components/Blocking/Categories.jsx +++ b/app/panel/components/Blocking/Categories.jsx @@ -30,14 +30,23 @@ class Categories extends React.Component { */ render() { const { + actions, categories, expandAll, unknownCategory, enable_anti_tracking, sitePolicy, + globalBlocking, + filtered, + showToast, + show_tracker_urls, + paused_blocking, + language, + smartBlockActive, + smartBlock, } = this.props; - const globalBlocking = !!this.props.globalBlocking; - const filtered = !!this.props.filtered; + const globalBlockingBool = !!globalBlocking; + const filteredBool = !!filtered; const renderCategory = (category, index, isUnknown) => { let whitelistedTotal = 0; @@ -75,19 +84,19 @@ class Categories extends React.Component { return ( diff --git a/app/panel/components/Blocking/Category.jsx b/app/panel/components/Blocking/Category.jsx index 14296aa23..636f30e82 100644 --- a/app/panel/components/Blocking/Category.jsx +++ b/app/panel/components/Blocking/Category.jsx @@ -27,11 +27,13 @@ class Category extends React.Component { constructor(props) { super(props); + + const { expandAll } = this.props; this.state = { allShownBlocked: false, totalShownBlocked: false, showTooltip: false, - isExpanded: this.props.expandAll + isExpanded: expandAll }; // event bindings @@ -46,11 +48,12 @@ class Category extends React.Component { * new values related to tracker blocking to ensure correct rendering. */ componentDidMount() { - if (this.props.category) { + const { category } = this.props; + if (category) { const { allShownBlocked, totalShownBlocked, - } = Category.updateCategoryCheckbox(this.props.category); + } = Category.updateCategoryCheckbox(category); this.setState({ allShownBlocked, totalShownBlocked }); } } @@ -137,27 +140,38 @@ class Category extends React.Component { * Display alert that new settings were successfully persisted. */ clickCategoryStatus() { - const globalBlocking = !!this.props.globalBlocking; - const blocked = !this.state.allShownBlocked; + const { + actions, + category, + globalBlocking, + sitePolicy, + paused_blocking, + smartBlock, + smartBlockActive, + showToast + } = this.props; + const { allShownBlocked } = this.state; + const globalBlockingBool = !!globalBlocking; + const blocked = !allShownBlocked; - if ((this.props.paused_blocking || this.props.sitePolicy) && !globalBlocking) { + if ((paused_blocking || sitePolicy) && !globalBlockingBool) { return; } - this.props.actions.updateCategoryBlocked({ - smartBlockActive: this.props.smartBlockActive, - smartBlock: this.props.smartBlock, - category: this.props.category.id, + actions.updateCategoryBlocked({ + smartBlockActive, + smartBlock, + category: category.id, blocked, }); - this.props.actions.showNotification({ - updated: `cat_${this.props.category.id}_blocked`, + actions.showNotification({ + updated: `cat_${category.id}_blocked`, reload: true, }); - if (globalBlocking) { - this.props.showToast({ + if (globalBlockingBool) { + showToast({ text: t('global_settings_saved_category') }); } @@ -169,9 +183,11 @@ class Category extends React.Component { * @param {boolean} global expanded state */ updateCategoryExpanded(prevProps) { - if (this.props.expandAll !== prevProps.expandAll && this.props.expandAll !== this.state.isExpanded) { + const { expandAll } = this.props; + const { isExpanded } = this.state; + if (expandAll !== prevProps.expandAll && expandAll !== isExpanded) { this.setState({ - isExpanded: this.props.expandAll + isExpanded: expandAll }); } } @@ -198,15 +214,30 @@ class Category extends React.Component { */ render() { const { + actions, category, paused_blocking, sitePolicy, isUnknown, + globalBlocking, + index, + filtered, + showToast, + show_tracker_urls, + language, + smartBlockActive, + smartBlock, } = this.props; + const { + totalShownBlocked, + allShownBlocked, + showTooltip, + isExpanded, + } = this.state; - const globalBlocking = !!this.props.globalBlocking; + const globalBlockingBool = !!globalBlocking; - const checkBoxStyle = `${(this.state.totalShownBlocked && this.state.allShownBlocked) ? 'all-blocked ' : (this.state.totalShownBlocked ? 'some-blocked ' : '')} checkbox-container`; + const checkBoxStyle = `${(totalShownBlocked && allShownBlocked) ? 'all-blocked ' : (totalShownBlocked ? 'some-blocked ' : '')} checkbox-container`; const filteredText = { color: 'red' }; let trackersBlockedCount; @@ -220,7 +251,7 @@ class Category extends React.Component { return (
-
+
{isUnknown && (
)} @@ -229,15 +260,15 @@ class Category extends React.Component {
-
+
{category.name}
-
+
- {this.props.filtered && ( + {filtered && ( {t('blocking_category_tracker_found')} @@ -264,7 +295,7 @@ class Category extends React.Component { { this._renderCaret() } {!isUnknown && (
- + @@ -293,19 +324,19 @@ class Category extends React.Component {
- {this.state.isExpanded && ( + {isExpanded && ( )} diff --git a/app/panel/components/Blocking/GlobalTracker.jsx b/app/panel/components/Blocking/GlobalTracker.jsx index 8bade7666..df2221d95 100644 --- a/app/panel/components/Blocking/GlobalTracker.jsx +++ b/app/panel/components/Blocking/GlobalTracker.jsx @@ -46,17 +46,18 @@ class GlobalTracker extends React.Component { * description from https://apps.ghostery.com and sets it in state. */ toggleDescription() { - const { tracker } = this.props; + const { tracker, language } = this.props; + const { description } = this.state; this.setState(prevState => ({ showMoreInfo: !prevState.showMoreInfo })); - if (this.state.description) { + if (description) { return; } this.setState({ description: t('tracker_description_getting') }); sendMessageInPromise('getTrackerDescription', { - url: `${globals.APPS_BASE_URL}/${this.props.language}/apps/${ + url: `${globals.APPS_BASE_URL}/${language}/apps/${ encodeURIComponent(tracker.name.replace(/\s+/g, '_').toLowerCase())}?format=json`, }).then((data) => { if (data) { @@ -79,13 +80,16 @@ class GlobalTracker extends React.Component { * @todo Toast shows always. It does not reflect actual success. */ clickTrackerStatus() { - const isBlocked = !this.props.tracker.blocked; - this.props.actions.updateTrackerBlocked({ - app_id: this.props.tracker.id, - cat_id: this.props.cat_id, + const { + actions, tracker, cat_id, showToast + } = this.props; + const isBlocked = !tracker.blocked; + actions.updateTrackerBlocked({ + app_id: tracker.id, + cat_id, blocked: isBlocked, }); - this.props.showToast({ + showToast({ text: t('global_settings_saved_tracker') }); } @@ -95,7 +99,8 @@ class GlobalTracker extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - const { tracker } = this.props; + const { tracker, language } = this.props; + const { showMoreInfo, description, showTrackerLearnMore } = this.state; return (
@@ -107,14 +112,14 @@ class GlobalTracker extends React.Component {
{ - this.state.showMoreInfo && ( + showMoreInfo && (
- { this.state.description } + { description } { - this.state.showTrackerLearnMore && ( -
- + showTrackerLearnMore && ( + diff --git a/app/panel/components/Blocking/Tracker.jsx b/app/panel/components/Blocking/Tracker.jsx index aa61ac3b0..d19c2f5e7 100644 --- a/app/panel/components/Blocking/Tracker.jsx +++ b/app/panel/components/Blocking/Tracker.jsx @@ -65,7 +65,8 @@ class Tracker extends React.Component { * Lifecycle event. */ componentDidMount() { - this.updateTrackerClasses(this.props.tracker); + const { tracker } = this.props; + this.updateTrackerClasses(tracker); } /** @@ -95,17 +96,18 @@ class Tracker extends React.Component { * description from https://apps.ghostery.com and sets it in state. */ toggleDescription() { - const { tracker } = this.props; + const { tracker, language } = this.props; this.setState(prevState => ({ showMoreInfo: !prevState.showMoreInfo })); - if (this.state.description) { + const { description } = this.state; + if (description) { return; } this.setState({ description: t('tracker_description_getting') }); sendMessageInPromise('getTrackerDescription', { - url: `${globals.APPS_BASE_URL}/${this.props.language}/apps/${ + url: `${globals.APPS_BASE_URL}/${language}/apps/${ encodeURIComponent(tracker.name.replace(/\s+/g, '_').toLowerCase())}?format=json`, }).then((data) => { if (data) { @@ -181,22 +183,31 @@ class Tracker extends React.Component { * user that the page should be reloaded. */ clickTrackerStatus() { - const blocked = !this.props.tracker.blocked; - - if (this.props.paused_blocking || this.props.sitePolicy) { + const { + actions, + tracker, + paused_blocking, + sitePolicy, + smartBlockActive, + smartBlock, + cat_id, + } = this.props; + const blocked = !tracker.blocked; + + if (paused_blocking || sitePolicy) { return; } - this.props.actions.updateTrackerBlocked({ - smartBlockActive: this.props.smartBlockActive, - smartBlock: this.props.smartBlock, - app_id: this.props.tracker.id, - cat_id: this.props.cat_id, + actions.updateTrackerBlocked({ + smartBlockActive, + smartBlock, + app_id: tracker.id, + cat_id, blocked, }); - this.props.actions.showNotification({ - updated: `${this.props.tracker.id}_blocked`, + actions.showNotification({ + updated: `${tracker.id}_blocked`, reload: true, }); } @@ -207,16 +218,17 @@ class Tracker extends React.Component { * that the page should be reloaded. */ clickTrackerTrust() { - const ss_allowed = !this.props.tracker.ss_allowed; - this.props.actions.updateTrackerTrustRestrict({ - app_id: this.props.tracker.id, - cat_id: this.props.cat_id, + const { actions, tracker, cat_id } = this.props; + const ss_allowed = !tracker.ss_allowed; + actions.updateTrackerTrustRestrict({ + app_id: tracker.id, + cat_id, trust: ss_allowed, restrict: false, }); - this.props.actions.showNotification({ - updated: `${this.props.tracker.id}_ss_allowed`, + actions.showNotification({ + updated: `${tracker.id}_ss_allowed`, reload: true, }); } @@ -227,16 +239,17 @@ class Tracker extends React.Component { * that the page should be reloaded. */ clickTrackerRestrict() { - const ss_blocked = !this.props.tracker.ss_blocked; - this.props.actions.updateTrackerTrustRestrict({ - app_id: this.props.tracker.id, - cat_id: this.props.cat_id, + const { actions, tracker, cat_id } = this.props; + const ss_blocked = !tracker.ss_blocked; + actions.updateTrackerTrustRestrict({ + app_id: tracker.id, + cat_id, trust: false, restrict: ss_blocked, }); - this.props.actions.showNotification({ - updated: `${this.props.tracker.id}_ss_blocked`, + actions.showNotification({ + updated: `${tracker.id}_ss_blocked`, reload: true, }); } @@ -247,10 +260,10 @@ class Tracker extends React.Component { * that the page should be reloaded. */ handleCliqzTrackerWhitelist() { - const { tracker } = this.props; + const { actions, tracker } = this.props; - this.props.actions.updateCliqzModuleWhitelist(tracker); - this.props.actions.showNotification({ + actions.updateCliqzModuleWhitelist(tracker); + actions.showNotification({ updated: `${tracker.name}-whitelisting-status-changed`, reload: true, }); @@ -329,7 +342,16 @@ class Tracker extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - const { tracker, isUnknown } = this.props; + const { + tracker, isUnknown, language, show_tracker_urls + } = this.props; + const { + trackerClasses, + description, + warningImageTitle, + showMoreInfo, + showTrackerLearnMore, + } = this.state; let sources; if (tracker.sources) { @@ -340,7 +362,7 @@ class Tracker extends React.Component { className="trk-src-link" title={source.src} key={index} - href={`${globals.GCACHE_BASE_URL}/${encodeURIComponent(this.props.language)}/gcache/?n=${encodeURIComponent(tracker.name)}&s=${encodeURIComponent(source.src)}&v=2&t=${source.type}`} + href={`${globals.GCACHE_BASE_URL}/${encodeURIComponent(language)}/gcache/?n=${encodeURIComponent(tracker.name)}&s=${encodeURIComponent(source.src)}&v=2&t=${source.type}`} > { source.src } @@ -356,10 +378,10 @@ class Tracker extends React.Component { }); return ( -
+
-
+
{!isUnknown && renderKnownTrackerButtons( - this.props.tracker.ss_allowed, - this.props.tracker.ss_blocked, + tracker.ss_allowed, + tracker.ss_blocked, this.clickTrackerTrust, this.clickTrackerRestrict, this.clickTrackerStatus, @@ -387,20 +409,20 @@ class Tracker extends React.Component { )}
- {this.state.showMoreInfo && ( -
+ {showMoreInfo && ( +
{!isUnknown && (
- {this.state.description} - )} -
+
{t('panel_tracker_found_sources_title')}
{sources}
diff --git a/app/panel/components/Blocking/Trackers.jsx b/app/panel/components/Blocking/Trackers.jsx index 5b9bc0dce..9bb8ce9ff 100644 --- a/app/panel/components/Blocking/Trackers.jsx +++ b/app/panel/components/Blocking/Trackers.jsx @@ -40,9 +40,22 @@ class Trackers extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - const { trackers, isUnknown } = this.props; + const { + actions, + trackers, + isUnknown, + globalBlocking, + showToast, + language, + cat_id, + show_tracker_urls, + sitePolicy, + paused_blocking, + smartBlockActive, + smartBlock, + } = this.props; let trackerList; - if (this.props.globalBlocking) { + if (globalBlocking) { const trackersToShow = []; trackers.forEach((tracker) => { if (tracker.shouldShow) { @@ -52,13 +65,13 @@ class Trackers extends React.Component { trackerList = trackersToShow.map((tracker, index) => ( )); } else { @@ -66,14 +79,14 @@ class Trackers extends React.Component { )); diff --git a/app/panel/components/BuildingBlocks/ClickOutside.jsx b/app/panel/components/BuildingBlocks/ClickOutside.jsx index cd5634513..0d4b01bd9 100644 --- a/app/panel/components/BuildingBlocks/ClickOutside.jsx +++ b/app/panel/components/BuildingBlocks/ClickOutside.jsx @@ -22,9 +22,10 @@ class ClickOutside extends React.Component { super(props); // event bindings + const { offsetParent } = this.props; this.getContainer = this.getContainer.bind(this); this.clickHandler = this.clickHandler.bind(this); - this.listenerEl = this.props.offsetParent || document; + this.listenerEl = offsetParent || document; } /** @@ -55,6 +56,7 @@ class ClickOutside extends React.Component { * @param {Object} e mouseclick event */ clickHandler(e) { + const { excludeEl, onClickOutside } = this.props; // Simple polyfill for Event.composedPath if (!('composedPath' in Event.prototype)) { Event.prototype.composedPath = function() { @@ -76,11 +78,11 @@ class ClickOutside extends React.Component { const ePath = e.path || (e.composedPath && e.composedPath()); if ( !el.contains(e.target) - && e.target !== this.props.excludeEl + && e.target !== excludeEl && !el.contains(ePath[0]) - && ePath[0] !== this.props.excludeEl + && ePath[0] !== excludeEl ) { - this.props.onClickOutside(e); + onClickOutside(e); } } @@ -89,7 +91,8 @@ class ClickOutside extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - return
{ this.props.children }
; + const { children } = this.props; + return
{ children }
; } } diff --git a/app/panel/components/BuildingBlocks/DonutGraph.jsx b/app/panel/components/BuildingBlocks/DonutGraph.jsx index a7edc2e1a..dd843b98a 100644 --- a/app/panel/components/BuildingBlocks/DonutGraph.jsx +++ b/app/panel/components/BuildingBlocks/DonutGraph.jsx @@ -101,39 +101,47 @@ class DonutGraph extends React.Component { * Lifecycle event */ componentDidUpdate(prevProps) { + const prevCategories = prevProps.categories; + const prevAdBlock = prevProps.adBlock; + const prevAntiTracking = prevProps.antiTracking; + const prevRenderRedscale = prevProps.renderRedscale; + const prevRenderGreyscale = prevProps.renderGreyscale; + const prevGhosteryFeatureSelect = prevProps.ghosteryFeatureSelect; + const prevIsSmall = prevProps.isSmall; + const { - categories, - adBlock, - antiTracking, + isSmall, renderRedscale, renderGreyscale, ghosteryFeatureSelect, - isSmall - } = prevProps; + categories, + antiTracking, + adBlock, + } = this.props; - if (isSmall !== this.props.isSmall || - renderRedscale !== this.props.renderRedscale || - renderGreyscale !== this.props.renderGreyscale || - ghosteryFeatureSelect !== this.props.ghosteryFeatureSelect + if (prevIsSmall !== isSmall || + prevRenderRedscale !== renderRedscale || + prevRenderGreyscale !== renderGreyscale || + prevGhosteryFeatureSelect !== ghosteryFeatureSelect ) { - this.prepareDonutContainer(this.props.isSmall); + this.prepareDonutContainer(isSmall); this.nextPropsDonut(this.props); return; } // componentWillReceiveProps gets called many times during page load as new trackers or unsafe data points are found // so only compare tracker totals if we don't already have to redraw anyway as a result of the cheaper checks above - const prevTrackerTotal = categories.reduce((total, category) => total + category.num_total, 0); - const trackerTotal = this.props.categories.reduce((total, category) => total + category.num_total, 0); + const prevTrackerTotal = prevCategories.reduce((total, category) => total + category.num_total, 0); + const trackerTotal = categories.reduce((total, category) => total + category.num_total, 0); if (prevTrackerTotal !== trackerTotal) { this.nextPropsDonut(this.props); return; } - if (!antiTracking.unknownTrackerCount && !this.props.antiTracking.unknownTrackerCount - && !adBlock.unknownTrackerCount && !this.props.adBlock.unknownTrackerCount) { return; } - const prevUnknownDataPoints = antiTracking.unknownTrackerCount + adBlock.unknownTrackerCount; - const unknownDataPoints = this.props.antiTracking.unknownTrackerCount + this.props.adBlock.unknownTrackerCount; + if (!prevAntiTracking.unknownTrackerCount && !antiTracking.unknownTrackerCount + && !prevAdBlock.unknownTrackerCount && !adBlock.unknownTrackerCount) { return; } + const prevUnknownDataPoints = prevAntiTracking.unknownTrackerCount + prevAdBlock.unknownTrackerCount; + const unknownDataPoints = antiTracking.unknownTrackerCount + adBlock.unknownTrackerCount; if (prevUnknownDataPoints !== unknownDataPoints) { this.nextPropsDonut(this.props); } @@ -309,8 +317,9 @@ class DonutGraph extends React.Component { } }) .on('click', (d) => { + const { clickDonut } = this.props; if (d.data.name && isSmall) { - this.props.clickDonut({ type: 'category', name: d.data.id }); + clickDonut({ type: 'category', name: d.data.id }); } }) .transition() @@ -338,7 +347,8 @@ class DonutGraph extends React.Component { * Handle click event for graph text. Filters to show all categories. */ clickGraphText() { - this.props.clickDonut({ type: 'trackers', name: 'all' }); + const { clickDonut } = this.props; + clickDonut({ type: 'trackers', name: 'all' }); } /** diff --git a/app/panel/components/BuildingBlocks/GhosteryFeature.jsx b/app/panel/components/BuildingBlocks/GhosteryFeature.jsx index 8d127474e..690d128b9 100644 --- a/app/panel/components/BuildingBlocks/GhosteryFeature.jsx +++ b/app/panel/components/BuildingBlocks/GhosteryFeature.jsx @@ -33,11 +33,12 @@ class GhosteryFeature extends React.Component { * Handles user click on the Ghostery Feature button */ handleClick() { - if (this.props.blockingPausedOrDisabled) { + const { blockingPausedOrDisabled, handleClick, type } = this.props; + if (blockingPausedOrDisabled) { return; } - this.props.handleClick(this.props.type); + handleClick(type); } static _getButtonText(sitePolicy, showText, type) { diff --git a/app/panel/components/BuildingBlocks/NotScanned.jsx b/app/panel/components/BuildingBlocks/NotScanned.jsx index a91bb21b9..000f5054e 100644 --- a/app/panel/components/BuildingBlocks/NotScanned.jsx +++ b/app/panel/components/BuildingBlocks/NotScanned.jsx @@ -19,9 +19,9 @@ import ClassNames from 'classnames'; * when a site is not scannable or has not yet been scanned. * @memberof PanelBuildingBlocks */ -const NotScanned = (props) => { +const NotScanned = ({ isSmall }) => { const notScannedClassNames = ClassNames('sub-component', 'not-scanned', { - small: props.isSmall, + small: isSmall, }); return ( // eslint-disable-line arrow-parens diff --git a/app/panel/components/BuildingBlocks/PauseButton.jsx b/app/panel/components/BuildingBlocks/PauseButton.jsx index ad0f21cfb..217588c87 100644 --- a/app/panel/components/BuildingBlocks/PauseButton.jsx +++ b/app/panel/components/BuildingBlocks/PauseButton.jsx @@ -36,7 +36,8 @@ class PauseButton extends React.Component { * Handles the click event for the Dropdown Caret */ clickDropdownCaret() { - if (!this.state.showDropdown) { + const { showDropdown } = this.state; + if (!showDropdown) { this.setState({ showDropdown: true }); document.body.addEventListener('click', this.clickOutside); } else { @@ -63,9 +64,10 @@ class PauseButton extends React.Component { * @param {number} time The time in minutes that Ghostery should be paused` */ clickDropdownPause(time) { + const { clickPause } = this.props; this.setState({ showDropdown: false }); document.body.removeEventListener('click', this.clickOutside); - this.props.clickPause(time); + clickPause(time); } /** @@ -73,7 +75,7 @@ class PauseButton extends React.Component { * @return {JSX} JSX for the dropdown list */ renderDropdown() { - const { isCondensed, isPausedTimeout } = this.props; + const { isCondensed, isPausedTimeout, dropdownItems } = this.props; function dropdownItemClassName(value) { return ClassNames('dropdown-item', 'clickable', 'dropdown-clickable', { @@ -87,7 +89,7 @@ class PauseButton extends React.Component { return (
- {this.props.dropdownItems.map(item => ( + {dropdownItems.map(item => (
{ this.clickDropdownPause(item.val); }}> {!isCondensed ? item.name : item.name_condensed} @@ -132,7 +134,8 @@ class PauseButton extends React.Component { const { isPaused, isCentered, - isCondensed + isCondensed, + clickPause } = this.props; const { showDropdown } = this.state; const centeredAndCondensed = isCentered && isCondensed; @@ -157,7 +160,7 @@ class PauseButton extends React.Component { const togglePauseButton = (
{ this.pauseWidth = node && node.clientWidth; }} > {this.renderPauseButtonText()} diff --git a/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx b/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx index c0d95edf1..ec6314cd4 100644 --- a/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx +++ b/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx @@ -21,16 +21,14 @@ import RadioButton from './RadioButton'; * @class Implements a radio button group * @memberof PanelBuildingBlocks */ -const RadioButtonGroup = (props) => { - const { indexClicked, handleItemClick } = props; - - const labels = props.labels.map(label => ( +const RadioButtonGroup = ({ indexClicked, handleItemClick, labels }) => { + const labelsEl = labels.map(label => (
{t(label)}
)); - const buttons = props.labels.map((label, index) => ( + const buttons = labels.map((label, index) => (
{ return (
- {labels} + {labelsEl}
{buttons} diff --git a/app/panel/components/BuildingBlocks/StatsGraph.jsx b/app/panel/components/BuildingBlocks/StatsGraph.jsx index 5b9b34088..82a95664e 100644 --- a/app/panel/components/BuildingBlocks/StatsGraph.jsx +++ b/app/panel/components/BuildingBlocks/StatsGraph.jsx @@ -44,7 +44,9 @@ class StatsGraph extends React.Component { * Add tooltips for each data point */ generateGraph() { - const { demo } = this.props; + const { + data, demo, dailyOrMonthly, view, tooltipText + } = this.props; // Clear graph D3.select(this.node).selectAll('*').remove(); @@ -70,7 +72,7 @@ class StatsGraph extends React.Component { let formatLabelDate; let formatTooltipDate; - if (this.props.dailyOrMonthly === 'daily') { + if (dailyOrMonthly === 'daily') { formatLabelDate = D3.timeFormat('%b %d'); formatTooltipDate = D3.timeFormat('%b %d, %Y'); } else { @@ -78,24 +80,24 @@ class StatsGraph extends React.Component { formatTooltipDate = D3.timeFormat('%b %Y'); } - const data = JSON.parse(JSON.stringify(this.props.data)); - data.forEach((e) => { + const dataJson = JSON.parse(JSON.stringify(data)); + dataJson.forEach((e) => { const entry = e; entry.date = parseMonth(entry.date); }); let tickAmount; - switch (data.length) { + switch (dataJson.length) { case 0: case 1: case 6: - tickAmount = data.length; + tickAmount = dataJson.length; break; case 2: case 3: case 4: case 5: - tickAmount = data.length - 1; + tickAmount = dataJson.length - 1; break; default: tickAmount = 6; @@ -104,22 +106,22 @@ class StatsGraph extends React.Component { // Set scales const x = D3.scaleLinear() .range([0, width]) - .domain(D3.extent(data, d => d.index)); + .domain(D3.extent(dataJson, d => d.index)); const y = D3.scaleLinear() .range([height, 0]); // ~ Handle axis styling for edge case of only one data point ~ - if (data.length === 1) { - y.domain([0, D3.max(data, d => d.amount) * 2]); + if (dataJson.length === 1) { + y.domain([0, D3.max(dataJson, d => d.amount) * 2]); } else { - y.domain(D3.extent(data, d => d.amount)); + y.domain(D3.extent(dataJson, d => d.amount)); } // Add axes const xAxis = D3.axisBottom() .ticks(tickAmount) .tickSize(0) - .tickFormat(d => formatLabelDate(data[d].date)) + .tickFormat(d => formatLabelDate(dataJson[d].date)) .scale(x); const yAxis = D3.axisLeft() @@ -142,7 +144,7 @@ class StatsGraph extends React.Component { .call(yAxis.tickSize(-width).tickFormat('')); // Add data path - const pathGroup = canvas.append('g').datum(data); + const pathGroup = canvas.append('g').datum(dataJson); const line = D3.line() .x(d => x(d.index)) @@ -194,7 +196,7 @@ class StatsGraph extends React.Component { canvas.append('g') .attr('class', 'point-group') .selectAll('circle') - .data(data) + .data(dataJson) .enter() .append('circle') .attr('class', (d, i) => `point point-${i}`) @@ -230,7 +232,7 @@ class StatsGraph extends React.Component { tooltipFlipped = true; tooltipPositionY += 130; tooltipPositionX += 0; - } else if (this.props.view === 'trackersAnonymized') { + } else if (view === 'trackersAnonymized') { tooltipPositionY -= 16; } @@ -241,7 +243,7 @@ class StatsGraph extends React.Component { .style('left', `${tooltipPositionX}px`) .style('top', `${tooltipPositionY}px`); - if (this.props.view === 'trackersAnonymized') { + if (view === 'trackersAnonymized') { div.classed('long-text', true); } @@ -252,7 +254,7 @@ class StatsGraph extends React.Component { div.append('p') .attr('class', 'tooltip-text-top') - .html(`${formatTooltipDate(d.date)}
${this.props.tooltipText}`); + .html(`${formatTooltipDate(d.date)}
${tooltipText}`); div.append('p') .attr('class', 'tooltip-text-bottom') .html(D3.format(',')(d.amount)); @@ -294,19 +296,20 @@ class StatsGraph extends React.Component { * @return {JSX} JSX for rendering the Historical Stats Graph in the Stats View component */ render() { + const { timeframeSelectors, selectTimeframe } = this.props; return (
{ this.node = node; }} />
); diff --git a/app/panel/components/BuildingBlocks/ToggleSlider.jsx b/app/panel/components/BuildingBlocks/ToggleSlider.jsx index f3eabe62a..975c2238f 100644 --- a/app/panel/components/BuildingBlocks/ToggleSlider.jsx +++ b/app/panel/components/BuildingBlocks/ToggleSlider.jsx @@ -24,8 +24,10 @@ import ClassNames from 'classnames'; class ToggleSlider extends React.Component { constructor(props) { super(props); + + const { isChecked } = this.props; this.state = { - checked: this.props.isChecked, + checked: isChecked, }; // Event Bindings @@ -48,8 +50,9 @@ class ToggleSlider extends React.Component { * property in the parent. Or it can just set the state directly. */ _handleChange(event) { - if (typeof this.props.onChange === 'function') { - this.props.onChange(event); + const { onChange } = this.props; + if (typeof onChange === 'function') { + onChange(event); } else { this.setState(prevState => ({ checked: !prevState.checked })); } @@ -60,9 +63,11 @@ class ToggleSlider extends React.Component { * @return {JSX} JSX for rendering the Toggle Slider used throughout the extension */ render() { - const compClassNames = ClassNames('ToggleSlider', this.props.className); + const { className, isDisabled } = this.props; + const { checked } = this.state; + const compClassNames = ClassNames('ToggleSlider', className); const labelClassNames = ClassNames('ToggleSlider__switch', { - disabled: this.props.isDisabled, + disabled: isDisabled, }); return (
@@ -70,7 +75,7 @@ class ToggleSlider extends React.Component { diff --git a/app/panel/components/CreateAccount.jsx b/app/panel/components/CreateAccount.jsx index 7d0120504..eb08eb37d 100644 --- a/app/panel/components/CreateAccount.jsx +++ b/app/panel/components/CreateAccount.jsx @@ -115,15 +115,16 @@ class CreateAccount extends React.Component { passwordInvalidError: false, passwordLengthError: false, }, () => { - this.props.actions.register(email, confirmEmail, firstName, lastName, password).then((success) => { + const { actions, history } = this.props; + actions.register(email, confirmEmail, firstName, lastName, password).then((success) => { this.setState({ loading: false }); if (success) { new RSVP.Promise((resolve) => { - this.props.actions.getUser() + actions.getUser() .then(() => resolve()) .catch(() => resolve()); }).finally(() => { - this.props.history.push('/account-success'); + history.push('/account-success'); }); } }); @@ -137,6 +138,7 @@ class CreateAccount extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { + const { is_expert } = this.props; const { email, confirmEmail, firstName, lastName, password, legalConsentChecked, loading, emailError, confirmEmailError, legalConsentNotCheckedError, passwordInvalidError, passwordLengthError } = this.state; @@ -224,7 +226,7 @@ class CreateAccount extends React.Component {
- + { t('button_cancel') }
diff --git a/app/panel/components/Detail.jsx b/app/panel/components/Detail.jsx index e694aeeb9..3a2782032 100644 --- a/app/panel/components/Detail.jsx +++ b/app/panel/components/Detail.jsx @@ -61,7 +61,8 @@ class Detail extends React.Component { * Click "expertTab" to enable detailed (expert) mode. Trigger action. */ toggleExpanded() { - this.props.actions.toggleExpanded(); + const { actions } = this.props; + actions.toggleExpanded(); } /** @@ -72,13 +73,14 @@ class Detail extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { + const { is_expanded, history, user } = this.props; const condensedToggleClassNames = ClassNames('condensed-toggle', { - condensed: this.props.is_expanded, + condensed: is_expanded, }); - const activeTab = this.props.history.location.pathname.includes('rewards') ? 'rewards' : 'blocking'; + const activeTab = history.location.pathname.includes('rewards') ? 'rewards' : 'blocking'; const contentDetailsClassNames = ClassNames({ - expanded: this.props.is_expanded, + expanded: is_expanded, rewardsView: activeTab === 'rewards', }); @@ -92,7 +94,7 @@ class Detail extends React.Component {
diff --git a/app/panel/components/DetailMenu.jsx b/app/panel/components/DetailMenu.jsx index 275174694..b104ca5b2 100644 --- a/app/panel/components/DetailMenu.jsx +++ b/app/panel/components/DetailMenu.jsx @@ -25,10 +25,11 @@ class DetailMenu extends React.Component { constructor(props) { super(props); + const { activeTab } = this.props; this.state = { menu: { - showBlocking: this.props.activeTab === 'blocking', - showRewards: this.props.activeTab === 'rewards' + showBlocking: activeTab === 'blocking', + showRewards: activeTab === 'rewards' } }; @@ -56,11 +57,13 @@ class DetailMenu extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { + const { hasReward } = this.props; + const { menu } = this.state; return (
- + @@ -80,11 +83,11 @@ class DetailMenu extends React.Component {
- + - {this.props.hasReward && ( + {hasReward && ( )} diff --git a/app/panel/components/Header.jsx b/app/panel/components/Header.jsx index ac3e19448..44ef3fb43 100644 --- a/app/panel/components/Header.jsx +++ b/app/panel/components/Header.jsx @@ -35,7 +35,8 @@ class Header extends React.Component { * Handles clicking on the Simple View tab */ clickSimpleTab = () => { - if (this.props.is_expert) { + const { is_expert } = this.props; + if (is_expert) { this.toggleExpert(); } } @@ -44,7 +45,8 @@ class Header extends React.Component { * Handles clicking on the Detailed View tab */ clickDetailedTab = () => { - if (!this.props.is_expert) { + const { is_expert } = this.props; + if (!is_expert) { this.toggleExpert(); } } @@ -53,11 +55,12 @@ class Header extends React.Component { * Toggle between Simple and Detailed Views. */ toggleExpert = () => { - this.props.actions.toggleExpert(); - if (this.props.is_expert) { - this.props.history.push('/'); + const { actions, history, is_expert } = this.props; + actions.toggleExpert(); + if (is_expert) { + history.push('/'); } else { - this.props.history.push('/detail'); + history.push('/detail'); } } @@ -69,26 +72,27 @@ class Header extends React.Component { } handleSignin = () => { - this.props.history.push('/login'); + const { history } = this.props; + history.push('/login'); } handleSendValidateAccountEmail = () => { - const { user } = this.props; + const { actions, user } = this.props; sendMessageInPromise('account.sendValidateAccountEmail').then((success) => { if (success) { - this.props.actions.showNotification({ + actions.showNotification({ classes: 'success', text: t('panel_email_verification_sent', user && user.email), }); } else { - this.props.actions.showNotification({ + actions.showNotification({ classes: 'alert', text: t('server_error_message'), }); } }).catch((err) => { log('sendVerificationEmail Error', err); - this.props.actions.showNotification({ + actions.showNotification({ classes: 'alert', text: t('server_error_message'), }); @@ -136,21 +140,23 @@ class Header extends React.Component { } determineBackPath = () => { - const { entries, location } = this.props.history; + const { history, is_expert } = this.props; + const { entries, location } = history; const subscriptionRegEx = /^(\/subscription)/; if (location.pathname === '/stats' && (entries.length > 1 && subscriptionRegEx.test(entries[entries.length - 2].pathname))) { return 'subscription/info'; } - return this.props.is_expert ? '/detail/blocking' : '/'; + return is_expert ? '/detail/blocking' : '/'; } clickUpgradeBannerOrGoldPlusIcon = () => { // TODO check whether this is the message we want to be sending now + const { history } = this.props; sendMessage('ping', 'plus_panel_from_badge'); const { user } = this.props; const subscriber = user && user.subscriptionsPlus; - this.props.history.push(subscriber ? '/subscription/info' : `/subscribe/${!!user}`); + history.push(subscriber ? '/subscription/info' : `/subscribe/${!!user}`); } /** @@ -159,12 +165,17 @@ class Header extends React.Component { */ render() { const { + actions, is_expanded, is_expert, location, loggedIn, user, + language, + tab_id, + history, } = this.props; + const { dropdownOpen } = this.state; const { pathname } = location; const showTabs = pathname === '/' || pathname.startsWith('/detail'); const headerArrowClasses = ClassNames('back-arrow', { @@ -239,11 +250,11 @@ class Header extends React.Component { loggedIn={loggedIn} subscriber={subscriber} email={user && user.email} - language={this.props.language} - tab_id={this.props.tab_id} + language={language} + tab_id={tab_id} location={location} - history={this.props.history} - actions={this.props.actions} + history={history} + actions={actions} toggleDropdown={this.toggleDropdown} kebab={this.kebab} /> @@ -262,7 +273,7 @@ class Header extends React.Component { {((is_expert && is_expanded) || !showTabs) && plusUpgradeBannerOrSubscriberBadgeLogolink } {headerMenuKebab}
- { this.state.dropdownOpen && headerMenu } + { dropdownOpen && headerMenu }
diff --git a/app/panel/components/HeaderMenu.jsx b/app/panel/components/HeaderMenu.jsx index 6467aa295..5a31f6406 100644 --- a/app/panel/components/HeaderMenu.jsx +++ b/app/panel/components/HeaderMenu.jsx @@ -32,9 +32,10 @@ class HeaderMenu extends React.Component { * @param {Object} e mouseclick event */ handleClickOutside = (e) => { + const { toggleDropdown } = this.props; // eslint-disable-next-line react/no-find-dom-node if (!ReactDOM.findDOMNode(this).contains(e.target)) { - this.props.toggleDropdown(); + toggleDropdown(); } } @@ -42,8 +43,9 @@ class HeaderMenu extends React.Component { * Trigger action which open Settings panel from drop-down menu Settings item. */ clickSettings = () => { - this.props.toggleDropdown(); - this.props.history.push('/settings/globalblocking'); + const { history, toggleDropdown } = this.props; + toggleDropdown(); + history.push('/settings/globalblocking'); } /** @@ -107,8 +109,9 @@ class HeaderMenu extends React.Component { * It should open Help view. */ clickHelp = () => { - this.props.toggleDropdown(); - this.props.history.push('/help'); + const { history, toggleDropdown } = this.props; + toggleDropdown(); + history.push('/help'); } /** @@ -116,8 +119,9 @@ class HeaderMenu extends React.Component { * It should open About view. */ clickAbout = () => { - this.props.toggleDropdown(); - this.props.history.push('/about'); + const { history, toggleDropdown } = this.props; + toggleDropdown(); + history.push('/about'); } /** @@ -136,25 +140,30 @@ class HeaderMenu extends React.Component { * Handle click on 'Sign in' menu item and navigate to Login panel. */ clickSignIn = () => { - this.props.toggleDropdown(); - this.props.history.push('/login'); + const { history, toggleDropdown } = this.props; + toggleDropdown(); + history.push('/login'); } /** * Handle click on 'Sign out' menu item (if user is in logged in state) and log out the user. */ clickSignOut = () => { - this.props.toggleDropdown(); - this.props.actions.logout(); + const { actions, toggleDropdown } = this.props; + toggleDropdown(); + actions.logout(); } /** * Handle click on Subscriber menu item. */ clickSubscriber = () => { + const { + history, toggleDropdown, subscriber, loggedIn + } = this.props; sendMessage('ping', 'plus_panel_from_menu'); - this.props.toggleDropdown(); - this.props.history.push(this.props.subscriber ? '/subscription/info' : `/subscribe/${this.props.loggedIn}`); + toggleDropdown(); + history.push(subscriber ? '/subscription/info' : `/subscribe/${loggedIn}`); } /** @@ -162,11 +171,13 @@ class HeaderMenu extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - const { loggedIn, email } = this.props; - const optionClasses = ClassNames({ 'menu-option': this.props.subscriber, 'menu-option-non-subscriber': !this.props.subscriber }); - const iconClasses = ClassNames('menu-icon-container', { subscriber: this.props.subscriber }, { 'non-subscriber': !this.props.subscriber }); + const { + loggedIn, email, subscriber, kebab + } = this.props; + const optionClasses = ClassNames({ 'menu-option': subscriber, 'menu-option-non-subscriber': !subscriber }); + const iconClasses = ClassNames('menu-icon-container', { subscriber }, { 'non-subscriber': !subscriber }); return ( - +
  • diff --git a/app/panel/components/Login.jsx b/app/panel/components/Login.jsx index a7b7562d4..2f787fe73 100644 --- a/app/panel/components/Login.jsx +++ b/app/panel/components/Login.jsx @@ -49,6 +49,7 @@ class Login extends React.Component { * Validate entered login data and, if it is good, trigger Login action. */ handleSubmit = (e) => { + const { actions, is_expert } = this.props; e.preventDefault(); const { email, password } = this.state; const emailIsValid = email && validateEmail(email); @@ -60,22 +61,22 @@ class Login extends React.Component { if (!emailIsValid || !password) { return; } this.setState({ loading: true }, () => { - this.props.actions.login(email, password) + actions.login(email, password) .then((success) => { if (success) { RSVP.all([ - this.props.actions.getUser(), - this.props.actions.getUserSettings(), + actions.getUser(), + actions.getUserSettings(), ]) .then((res) => { const { current_theme = 'default' } = res[1]; - return this.props.actions.getTheme(current_theme); + return actions.getTheme(current_theme); }) .finally(() => { this.setState({ loading: false }, () => { - this.props.actions.togglePromoModal(); + actions.togglePromoModal(); history.push({ - pathname: this.props.is_expert ? '/detail/blocking' : '/' + pathname: is_expert ? '/detail/blocking' : '/' }); }); }); @@ -92,6 +93,7 @@ class Login extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { + const { is_expert } = this.props; const { email, password, emailError, passwordError, loading } = this.state; @@ -119,7 +121,7 @@ class Login extends React.Component {
- + { t('button_cancel') }
diff --git a/app/panel/components/Panel.jsx b/app/panel/components/Panel.jsx index dc4321cf8..c75ded9ef 100644 --- a/app/panel/components/Panel.jsx +++ b/app/panel/components/Panel.jsx @@ -42,6 +42,7 @@ class Panel extends React.Component { * Lifecycle event */ componentDidMount() { + const { actions } = this.props; sendMessage('ping', 'engaged'); this._dynamicUIDataInitialized = false; this._dynamicUIPort = chrome.runtime.connect({ name: 'dynamicUIPanelPort' }); @@ -53,7 +54,7 @@ class Panel extends React.Component { if (body.panel) { this._initializeData(body); } else if (this._dynamicUIDataInitialized) { - this.props.actions.updatePanelData(body); + actions.updatePanelData(body); } }); } @@ -71,7 +72,8 @@ class Panel extends React.Component { * @todo Why do we need explicit argument here? */ clickReloadBanner() { - sendMessage('reloadTab', { tab_id: +this.props.tab_id }); + const { tab_id } = this.props; + sendMessage('reloadTab', { tab_id: +tab_id }); window.close(); } @@ -81,7 +83,7 @@ class Panel extends React.Component { * @todo Why do we need explicit argument here? */ closeNotification() { - const { notificationClasses } = this.props; + const { actions, notificationClasses } = this.props; let banner_status_name = ''; if (notificationClasses.includes('hideous')) { @@ -92,7 +94,7 @@ class Panel extends React.Component { banner_status_name = 'temp_banner_status'; } - this.props.actions.closeNotification({ + actions.closeNotification({ banner_status_name, }); } @@ -103,17 +105,18 @@ class Panel extends React.Component { * @param {Object} event */ filterTrackers(event) { + const { actions, is_expert } = this.props; const classes = event.target.className; - if (!this.props.is_expert) { + if (!is_expert) { return; } if (classes.includes('slow-insecure')) { - this.props.actions.filterTrackers({ type: 'trackers', name: 'warning-slow-insecure' }); + actions.filterTrackers({ type: 'trackers', name: 'warning-slow-insecure' }); } else if (classes.includes('compatibility')) { - this.props.actions.filterTrackers({ type: 'trackers', name: 'warning-compatibility' }); + actions.filterTrackers({ type: 'trackers', name: 'warning-compatibility' }); } else { - this.props.actions.filterTrackers({ type: 'trackers', name: 'warning' }); + actions.filterTrackers({ type: 'trackers', name: 'warning' }); } this.closeNotification(); @@ -125,6 +128,7 @@ class Panel extends React.Component { */ _initializeData(payload) { + const { actions, history } = this.props; this._dynamicUIDataInitialized = true; const { panel, summary, blocking } = payload; @@ -132,19 +136,19 @@ class Panel extends React.Component { setTheme(document, current_theme, account); - this.props.actions.updatePanelData(panel); - this.props.actions.updateSummaryData(summary); - if (blocking) { this.props.actions.updateBlockingData(blocking); } + actions.updatePanelData(panel); + actions.updateSummaryData(summary); + if (blocking) { actions.updateBlockingData(blocking); } if (panel.is_expert) { // load Detail component - this.props.history.push('/detail'); + history.push('/detail'); } // persist whitelist/blacklist/paused_blocking notifications in the event that the // panel is opened without a page reload if (Object.keys(panel.needsReload.changes).length) { - this.props.actions.showNotification({ + actions.showNotification({ updated: 'init', reload: true }); @@ -156,22 +160,23 @@ class Panel extends React.Component { * @return {JSX} JSX for the notification callout */ renderNotification() { - const needsReload = !!Object.keys(this.props.needsReload.changes).length; + const { needsReload, notificationText, notificationFilter } = this.props; + const needsReloadBool = !!Object.keys(needsReload.changes).length; - if (this.props.notificationText) { + if (notificationText) { return ( - - {this.props.notificationText === t('promos_turned_off_notification') && ( + + {notificationText === t('promos_turned_off_notification') && ( {t('settings')} )} - {needsReload && ( + {needsReloadBool && (
{ t('alert_reload') }
)}
); } - if (needsReload) { + if (needsReloadBool) { return ( {t('panel_needs_reload')} @@ -179,18 +184,18 @@ class Panel extends React.Component { ); } - if (this.props.notificationFilter === 'slow') { + if (notificationFilter === 'slow') { return ( - + { t('panel_tracker_slow_non_secure_end') } ); } - if (this.props.notificationFilter === 'compatibility') { + if (notificationFilter === 'compatibility') { return ( - + { t('panel_tracker_breaking_page_end') } ); @@ -325,18 +330,21 @@ class Panel extends React.Component { * @return {JSX} JSX for rendering the Panel */ render() { + const { + initialized, notificationShown, notificationClasses, children + } = this.props; // this prevents double rendering when waiting for getPanelData() to finish - if (!this.props.initialized) { + if (!initialized) { return null; } - const notificationText = this.props.notificationShown && this.renderNotification(); + const notificationText = notificationShown && this.renderNotification(); const { current_theme } = this.props; return (
{this._renderPromoModal()}
-
+
@@ -350,7 +358,7 @@ class Panel extends React.Component {
- { this.props.children } + { children } diff --git a/app/panel/components/Rewards.jsx b/app/panel/components/Rewards.jsx index 9f8cd6c1e..d5cf1cb7a 100644 --- a/app/panel/components/Rewards.jsx +++ b/app/panel/components/Rewards.jsx @@ -57,12 +57,13 @@ class Rewards extends React.Component { * Lifecycle event */ componentDidMount() { + const { actions } = this.props; this._dynamicUIPort = this.context; this._dynamicUIPort.onMessage.addListener(this.handlePortMessage); window.addEventListener('message', this.handleMyoffrzMessage); this._dynamicUIPort.postMessage({ name: 'RewardsComponentDidMount' }); - this.props.actions.sendSignal('hub_open'); + actions.sendSignal('hub_open'); } /** @@ -70,7 +71,8 @@ class Rewards extends React.Component { */ componentWillUnmount() { /* @TODO send message to background to remove port onDisconnect event */ - this.props.actions.sendSignal('hub_closed'); + const { actions } = this.props; + actions.sendSignal('hub_closed'); this._dynamicUIPort.postMessage({ name: 'RewardsComponentWillUnmount' }); this._dynamicUIPort.onMessage.removeListener(this.handlePortMessage); window.removeEventListener('message', this.handleMyoffrzMessage); @@ -83,7 +85,8 @@ class Rewards extends React.Component { if (msg.to !== 'rewards' || !msg.body) { return; } // msg.body can contain enable_offers prop - this.props.actions.updateRewardsData(msg.body); + const { actions } = this.props; + actions.updateRewardsData(msg.body); } iframeResize(data = {}) { @@ -152,12 +155,12 @@ class Rewards extends React.Component { * Handles toggling rewards on/off */ toggleOffers() { - const { enable_offers } = this.props; - this.props.actions.showNotification({ + const { actions, enable_offers } = this.props; + actions.showNotification({ text: !enable_offers ? t('rewards_on_toast_notification') : t('rewards_off_toast_notification'), classes: 'purple', }); - this.props.actions.toggleOffersEnabled(!enable_offers); + actions.toggleOffersEnabled(!enable_offers); const signal = { actionId: enable_offers ? 'rewards_off' : 'rewards_on', origin: 'rewards-hub', diff --git a/app/panel/components/Settings.jsx b/app/panel/components/Settings.jsx index db5d63722..26a90c858 100644 --- a/app/panel/components/Settings.jsx +++ b/app/panel/components/Settings.jsx @@ -85,29 +85,105 @@ class Settings extends React.Component { this._dynamicUIPort.onMessage.removeListener(this.handlePortMessage); } - GlobalBlockingComponent = () => (); + GlobalBlockingComponent = () => { + const { actions, language } = this.props; + return ( + + ); + } - TrustAndRestrictComponent = () => () + TrustAndRestrictComponent = () => { + const { actions, site_whitelist, site_blacklist } = this.props; + return ( + + ); + } - GeneralSettingsComponent = () => (); + GeneralSettingsComponent = () => { + const { actions } = this.props; + return ( + + ); + } - AdBlockerComponent = () => (); + AdBlockerComponent = () => { + const { actions } = this.props; + return ( + + ); + } - PurpleboxComponent = () => (); + PurpleboxComponent = () => { + const { actions } = this.props; + return ( + + ); + } - NotificationsComponent = () => (); + NotificationsComponent = () => { + const { actions } = this.props; + return ( + + ); + } - OptInComponent = () => (); + OptInComponent = () => { + const { actions } = this.props; + return ( + + ); + } - AccountComponent = () => (); + AccountComponent = () => { + const { actions } = this.props; + return ( + + ); + } /** * Handles messages from dynamic UI port to background */ handlePortMessage(msg) { + const { actions } = this.props; if (msg.to !== 'settings' || !msg.body) { return; } - this.props.actions.updateSettingsData(msg.body); + actions.updateSettingsData(msg.body); } /** @@ -115,6 +191,7 @@ class Settings extends React.Component { * @param {Object} event checking a checkbox event */ toggleCheckbox(event) { + const { actions } = this.props; if (event.currentTarget.name === 'enable_offers') { const signal = { actionId: !event.currentTarget.checked ? 'rewards_off' : 'rewards_on', @@ -124,7 +201,7 @@ class Settings extends React.Component { sendMessage('setPanelData', { enable_offers: event.currentTarget.checked, signal }, 'rewardsPanel'); sendMessage('ping', event.currentTarget.checked ? 'rewards_on' : 'rewards_off'); } - this.props.actions.toggleCheckbox({ + actions.toggleCheckbox({ event: event.currentTarget.name, checked: event.currentTarget.checked, }); @@ -134,7 +211,8 @@ class Settings extends React.Component { * Trigger parameterized selection action. */ selectItem(event) { - this.props.actions.selectItem({ + const { actions } = this.props; + actions.selectItem({ event: event.currentTarget.name, value: event.currentTarget.value, }); @@ -167,15 +245,17 @@ class Settings extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { + const { is_expanded } = this.props; + const { showToast, toastText } = this.state; return (
-
-
{this.state.toastText}
+
+
{toastText}
- + diff --git a/app/panel/components/Settings/Account.jsx b/app/panel/components/Settings/Account.jsx index 99337bf16..15273c354 100644 --- a/app/panel/components/Settings/Account.jsx +++ b/app/panel/components/Settings/Account.jsx @@ -27,7 +27,8 @@ class Account extends React.Component { * view to open Sign In (Login) view. */ clickSigninCreate = () => { - this.props.settingsData.history.push('/login'); + const { settingsData } = this.props; + settingsData.history.push('/login'); } /** @@ -47,17 +48,19 @@ class Account extends React.Component { * Trigger action to export settings in JSON format and save it to a file. */ clickExportSettings = () => { - this.props.actions.exportSettings(this.props.settingsData.pageUrl); + const { actions, settingsData } = this.props; + actions.exportSettings(settingsData.pageUrl); } /** * Trigger custom Import dialog or a native Open File dialog depending on browser. */ clickImportSettings = () => { + const { actions, settingsData } = this.props; const browserName = globals.BROWSER_INFO.name; if (browserName === 'firefox') { // show ghostery dialog window for import - this.props.actions.importSettingsDialog(this.props.settingsData.pageUrl); + actions.importSettingsDialog(settingsData.pageUrl); } else { // for chrome and opera, use the native File Dialog this.selectedFile.click(); @@ -68,7 +71,8 @@ class Account extends React.Component { * Parse settings file imported via native browser window. Called via input#select-file onChange. */ validateImportFile = () => { - this.props.actions.importSettingsNative(this.selectedFile.files[0]); + const { actions } = this.props; + actions.importSettingsNative(this.selectedFile.files[0]); } /** diff --git a/app/panel/components/Settings/GeneralSettings.jsx b/app/panel/components/Settings/GeneralSettings.jsx index b97460633..a0930b2d2 100644 --- a/app/panel/components/Settings/GeneralSettings.jsx +++ b/app/panel/components/Settings/GeneralSettings.jsx @@ -50,7 +50,8 @@ class GeneralSettings extends React.Component { * Lifecycle event. */ componentDidMount() { - this.updateDbLastUpdated(this.props.settingsData); + const { settingsData } = this.props; + this.updateDbLastUpdated(settingsData); } /** @@ -69,7 +70,8 @@ class GeneralSettings extends React.Component { * Trigger action to check for new DB updates. */ updateDatabase() { - this.props.actions.updateDatabase(); + const { actions } = this.props; + actions.updateDatabase(); } /** @@ -88,10 +90,11 @@ class GeneralSettings extends React.Component { * @param {Object} settingsData */ updateDbLastUpdated(settingsData) { - const dbLastUpdated = GeneralSettings.getDbLastUpdated(settingsData); + const { dbLastUpdated } = this.state; + const calcDbLastUpdated = GeneralSettings.getDbLastUpdated(settingsData); - if (dbLastUpdated && dbLastUpdated !== this.state.dbLastUpdated) { - this.setState({ dbLastUpdated }); + if (calcDbLastUpdated && calcDbLastUpdated !== dbLastUpdated) { + this.setState({ dbLastUpdated: calcDbLastUpdated }); } } @@ -100,7 +103,7 @@ class GeneralSettings extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - const { settingsData } = this.props; + const { settingsData, toggleCheckbox, dbLastUpdated } = this.props; return (
@@ -108,7 +111,7 @@ class GeneralSettings extends React.Component {

{ t('settings_trackers') }

- + @@ -117,7 +120,7 @@ class GeneralSettings extends React.Component { { t('settings_last_update') } {' '} - { this.state.dbLastUpdated } + { dbLastUpdated } {' '} { settingsData.dbUpdateText } @@ -127,7 +130,7 @@ class GeneralSettings extends React.Component {
- + @@ -142,7 +145,7 @@ class GeneralSettings extends React.Component {
- + @@ -150,14 +153,14 @@ class GeneralSettings extends React.Component {
- +

{ t('settings_blocking') }

- + @@ -168,7 +171,7 @@ class GeneralSettings extends React.Component {
- + @@ -179,7 +182,7 @@ class GeneralSettings extends React.Component {
- + diff --git a/app/panel/components/Settings/GlobalBlocking.jsx b/app/panel/components/Settings/GlobalBlocking.jsx index df48a10c8..3a7249679 100644 --- a/app/panel/components/Settings/GlobalBlocking.jsx +++ b/app/panel/components/Settings/GlobalBlocking.jsx @@ -31,7 +31,8 @@ class GlobalBlocking extends React.Component { * Trigger action which toggles expanded state. */ toggleExpanded() { - this.props.actions.toggleExpanded(); + const { actions } = this.props; + actions.toggleExpanded(); } /** @@ -39,10 +40,16 @@ class GlobalBlocking extends React.Component { * @return {ReactComponent} ReactComponent instance */ render() { - const { settingsData } = this.props; - const categories = this.props.settingsData ? this.props.settingsData.categories : []; - const filterText = this.props.settingsData ? this.props.settingsData.filterText : t('settings_filter_all_label'); - const expandAll = this.props.settingsData ? this.props.settingsData.expand_all_trackers : false; + const { + actions, + settingsData, + showToast, + filtered, + language, + } = this.props; + const categories = settingsData ? settingsData.categories : []; + const filterText = settingsData ? settingsData.filterText : t('settings_filter_all_label'); + const expandAll = settingsData ? settingsData.expand_all_trackers : false; const condensedToggleClassNames = ClassNames('condensed-toggle', { condensed: settingsData.is_expanded, }); @@ -53,8 +60,8 @@ class GlobalBlocking extends React.Component { categories={categories} filterText={filterText} expandAll={expandAll} - actions={this.props.actions} - showToast={this.props.showToast} + actions={actions} + showToast={showToast} sitePolicy={settingsData.sitePolicy} paused_blocking={settingsData.paused_blocking} selected_app_ids={settingsData.selected_app_ids} @@ -65,10 +72,10 @@ class GlobalBlocking extends React.Component { )} diff --git a/app/panel/components/Settings/Notifications.jsx b/app/panel/components/Settings/Notifications.jsx index 6b658001b..b8047f727 100644 --- a/app/panel/components/Settings/Notifications.jsx +++ b/app/panel/components/Settings/Notifications.jsx @@ -18,63 +18,60 @@ import React from 'react'; * Settings view. * @memberOf SettingsComponents */ -const Notifications = (props) => { - const { settingsData } = props; - return ( -
-
-
-

{ t('settings_notifications') }

-
{ t('settings_notify_me') }
-
- -
-
-
- - -
+const Notifications = ({ settingsData, toggleCheckbox }) => ( +
+
+
+

{ t('settings_notifications') }

+
{ t('settings_notify_me') }
+
+ +
+
+
+ +
-
-
- - -
+
+
+
+ +
-
-
- - -
+
+
+
+ +
-
-
- - -
+
+
+
+ +
-
-
- - -
+
+
+
+ +
-
-
- - -
+
+
+
+ +
-
-
- - -
+
+
+
+ +
- ); -}; +
+); export default Notifications; diff --git a/app/panel/components/Settings/OptIn.jsx b/app/panel/components/Settings/OptIn.jsx index 04598f99f..5f7fbd80a 100644 --- a/app/panel/components/Settings/OptIn.jsx +++ b/app/panel/components/Settings/OptIn.jsx @@ -22,58 +22,55 @@ const { IS_CLIQZ } = globals; * It invites user to opt in for telemetry options, human web and offers * @memberOf SettingsComponents */ -const OptIn = (props) => { - const { settingsData } = props; - return ( -
-
-
-

{ t('settings_support_ghostery') }

-
- { t('settings_support_ghostery_by') } - : -
-
+const OptIn = ({ settingsData, toggleCheckbox }) => ( +
+
+
+

{ t('settings_support_ghostery') }

+
+ { t('settings_support_ghostery_by') } + : +
+
+
+ + +
+ +
+
+
+ {!IS_CLIQZ && ( +
- -
- {!IS_CLIQZ && ( -
-
- - -
- -
-
-
- )} - {!IS_CLIQZ && ( -
-
- - -
- -
+ )} + {!IS_CLIQZ && ( +
+
+ + +
+
- )} -
+
+ )}
- ); -}; +
+); export default OptIn; diff --git a/app/panel/components/Settings/Purplebox.jsx b/app/panel/components/Settings/Purplebox.jsx index c8812a1c2..a1b6c1a7b 100644 --- a/app/panel/components/Settings/Purplebox.jsx +++ b/app/panel/components/Settings/Purplebox.jsx @@ -17,88 +17,85 @@ import React from 'react'; * The view allows to set parameters for Ghostery purplebox. * @memberOf SettingsComponents */ -const Purplebox = (props) => { - const { settingsData } = props; - return ( -
-
-
-

{ t('settings_purple_box') }

-
-
- - -
- -
+const Purplebox = ({ settingsData, toggleCheckbox, selectItem }) => ( +
+
+
+

{ t('settings_purple_box') }

+
+
+ + +
+
-
-
- { t('settings_dismiss_after') } -
- +
+
+
+ { t('settings_dismiss_after') }
-
-
- { t('settings_display_in') } -
- + +
+
+
+ { t('settings_display_in') }
-
-
- - -
+ +
+
+
+ +
- ); -}; +
+); export default Purplebox; diff --git a/app/panel/components/Settings/SettingsMenu.jsx b/app/panel/components/Settings/SettingsMenu.jsx index f0264b4c2..da041584d 100644 --- a/app/panel/components/Settings/SettingsMenu.jsx +++ b/app/panel/components/Settings/SettingsMenu.jsx @@ -24,9 +24,9 @@ const { IS_CLIQZ } = globals; * The view allows to set parameters for Ghostery purplebox. * @memberOf SettingsComponents */ -const SettingsMenu = (props) => { +const SettingsMenu = ({ is_expanded }) => { const listClassNames = ClassNames('content-settings-menu menu vertical no-bullet', { - 's-hide': props.is_expanded, + 's-hide': is_expanded, }); return (
+
+
+
+
+
+
+ +
+

+ Test1 +

+
+ + 1 blocking_category_tracker + + + 1 blocking_category_blocked + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+

+ Test2 +

+
+ + 5 blocking_category_trackers + + + 3 blocking_category_blocked + +
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`app/panel-android/components/content/BlockingTab.jsx Snapshot tests with react-test-renderer BlockingTab component as site with falsy props 1`] = ` +
+
+

+ android_site_blocking_header +

+
+ + +
  • + +
  • + +
    +
    +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTab.jsx Snapshot tests with react-test-renderer BlockingTab component as site with tracker falsy props 1`] = ` +
    +
    +

    + android_site_blocking_header +

    +
    + + +
  • + +
  • + +
    +
    +
    +
    +
    +
    + +
    +

    + Test1 +

    +
    + + 1 blocking_category_tracker + + + 1 blocking_category_blocked + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/app/panel-android/components/content/__tests__/__snapshots__/BlockingTracker.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/BlockingTracker.jsx.snap new file mode 100644 index 000000000..af14b3f9d --- /dev/null +++ b/app/panel-android/components/content/__tests__/__snapshots__/BlockingTracker.jsx.snap @@ -0,0 +1,323 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when site Paused 1`] = ` +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    + android_block +
    +
    + android_restrict +
    +
    + android_trust +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when site Restricted 1`] = ` +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    + android_block +
    +
    + android_restrict +
    +
    + android_trust +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when site Trusted 1`] = ` +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    + android_block +
    +
    + android_restrict +
    +
    + android_trust +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when tracker allowed 1`] = ` +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    + android_block +
    +
    + android_restrict +
    +
    + android_untrust +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when tracker blocked 1`] = ` +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    + android_unblock +
    +
    + android_restrict +
    +
    + android_trust +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component when tracker restricted 1`] = ` +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    + android_block +
    +
    + android_unrestrict +
    +
    + android_trust +
    +
    +
    +`; + +exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests with react-test-renderer BlockingTracker component with falsy props 1`] = ` +
    +
    +
    +
    +
    + Tracker 1 +
    +
    +
    +
    + android_block +
    +
    + android_restrict +
    +
    + android_trust +
    +
    +
    +`; diff --git a/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap index 69727248d..d345de9e4 100644 --- a/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap +++ b/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap @@ -1,75 +1,508 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`app/panel-android/components/content/OverviewTab.jsx Snapshot tests with react-test-renderer OverviewTab component with DonutGraph 1`] = ` +exports[`app/panel-android/components/content/OverviewTab.jsx Snapshot tests with react-test-renderer OverviewTab component with falsy props and SiteNotScanned 1`] = `
    -
    +
    -
    - Test Element +
    + + + + + + + +
    -
    -
    -
    - Test Element +
    + + + + +
    +
    +
    -
    - Test Element +
    + summary_page_not_scanned
    -
    - Test Element +
    + summary_description_not_scanned_1 +
    +
    + summary_description_not_scanned_2
    -
    - Test Element +
    +
    +
    + + + summary_trust_site + + +
    +
    +
    +
    + + + summary_restrict_site + + +
    +
    +
    +
    +
    +
    + + + summary_pause_ghostery + + +
    +
    + + summary_show_menu + +
    +
    +
    +
    - Test Element +
    +
    +
    + off +
    +
    +
    + enhanced_anti_tracking +
    +
    +
    +
    +
    +
    + off +
    +
    +
    + enhanced_ad_blocking +
    +
    +
    +
    +
    +
    + off +
    +
    +
    + smart_blocking +
    +
    +
    `; -exports[`app/panel-android/components/content/OverviewTab.jsx Snapshot tests with react-test-renderer OverviewTab component with SiteNotScanned 1`] = ` +exports[`app/panel-android/components/content/OverviewTab.jsx Snapshot tests with react-test-renderer OverviewTab component with truthy props and no SiteNotScanned 1`] = `
    -
    - Test Element +
    +
    + + + + + + + + +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + 29 +
    +
    +
    +
    +
    + + page_host + +
    +
    +
    + + trackers_blocked + + + + 0 + +
    +
    + + requests_modified + + + + 21 + +
    -
    - Test Element +
    +
    +
    + + + summary_trust_site + + +
    +
    +
    +
    + + + summary_restrict_site + + +
    +
    +
    +
    +
    +
    + + + summary_resume_ghostery + + +
    +
    + + summary_show_menu + +
    +
    +
    +
    - Test Element +
    +
    +
    + on +
    +
    +
    + enhanced_anti_tracking +
    +
    +
    +
    +
    +
    + on +
    +
    +
    + enhanced_ad_blocking +
    +
    +
    +
    +
    +
    + on +
    +
    +
    + smart_blocking +
    +
    +
    diff --git a/app/panel/components/Settings/TrustAndRestrict.jsx b/app/panel/components/Settings/TrustAndRestrict.jsx index e3027002b..2d6511b65 100644 --- a/app/panel/components/Settings/TrustAndRestrict.jsx +++ b/app/panel/components/Settings/TrustAndRestrict.jsx @@ -192,12 +192,14 @@ class TrustAndRestrict extends React.Component {

    { t('settings_trusted_restricted_sites') }

    -
    -
    - {t('settings_trusted_sites')} -
    -
    - {t('settings_restricted_sites')} +
    +
    +
    + {t('settings_trusted_sites')} +
    +
    + {t('settings_restricted_sites')} +
    diff --git a/app/panel/components/Settings/__tests__/__snapshots__/TrustAndRestrict.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/TrustAndRestrict.jsx.snap index 0403041cf..96eddffb5 100644 --- a/app/panel/components/Settings/__tests__/__snapshots__/TrustAndRestrict.jsx.snap +++ b/app/panel/components/Settings/__tests__/__snapshots__/TrustAndRestrict.jsx.snap @@ -16,25 +16,29 @@ exports[`app/panel/components/Settings/TrustAndRestrict Snapshot test with react
    - - settings_trusted_sites - -
    -
    - - settings_restricted_sites - +
    + + settings_trusted_sites + +
    +
    + + settings_restricted_sites + +
    Date: Wed, 1 Jul 2020 16:52:51 -0400 Subject: [PATCH 51/89] Add actions to PanelAndroid Settings --- app/panel-android/actions/handler.js | 23 +++++- app/panel-android/actions/settingsActions.js | 69 ++++++++++++++++++ app/panel-android/actions/summaryActions.js | 51 +++++++++++++ app/panel-android/components/PanelAndroid.jsx | 32 ++++++--- .../components/content/Settings.jsx | 71 ++++++++++++++----- .../components/Settings/GeneralSettings.jsx | 1 + 6 files changed, 217 insertions(+), 30 deletions(-) create mode 100644 app/panel-android/actions/settingsActions.js diff --git a/app/panel-android/actions/handler.js b/app/panel-android/actions/handler.js index f04da2a7a..210c53140 100644 --- a/app/panel-android/actions/handler.js +++ b/app/panel-android/actions/handler.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 @@ -12,11 +12,14 @@ */ import { - handleTrustButtonClick, handleRestrictButtonClick, handlePauseButtonClick, cliqzFeatureToggle + handleTrustButtonClick, handleRestrictButtonClick, handlePauseButtonClick, cliqzFeatureToggle, updateSitePolicy } from './summaryActions'; import { trustRestrictBlockSiteTracker, blockUnblockGlobalTracker, blockUnBlockAllTrackers, resetSettings } from './trackerActions'; +import { + updateDatabase, updateSettingCheckbox, selectItem +} from './settingsActions'; // Handle all actions in Panel.jsx export default function handleAllActions({ actionName, actionData, state }) { @@ -55,6 +58,22 @@ export default function handleAllActions({ actionName, actionData, state }) { updated = resetSettings({ state }); break; + case 'updateSitePolicy': + updated = updateSitePolicy({ actionData, state }); + break; + + case 'updateDatabase': + updated = updateDatabase({ actionData, state }); + break; + + case 'updateSettingCheckbox': + updated = updateSettingCheckbox({ actionData, state }); + break; + + case 'selectItem': + updated = selectItem({ actionData, state }); + break; + default: updated = {}; } diff --git a/app/panel-android/actions/settingsActions.js b/app/panel-android/actions/settingsActions.js new file mode 100644 index 000000000..946e5ca6b --- /dev/null +++ b/app/panel-android/actions/settingsActions.js @@ -0,0 +1,69 @@ +/** + * Tracker Action creators + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * 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 + */ + +import { sendMessage, sendMessageInPromise } from '../../panel/utils/msg'; + +export function updateDatabase() { + // Send Message to Background + return sendMessageInPromise('update_database').then((result) => { + let resultText; + if (result && result.success === true) { + if (result.updated === true) { + resultText = t('settings_update_success'); + } else { + resultText = t('settings_update_up_to_date'); + } + } else { + resultText = t('settings_update_failed'); + } + + // Update State for PanelAndroid UI + return { + settings: { + dbUpdateText: resultText, + ...result.confData, + } + }; + }); +} + +export function updateSettingCheckbox({ actionData }) { + const { name, checked } = actionData; + const updatedState = {}; + + if (name === 'trackers_banner_status' || name === 'reload_banner_status') { + updatedState.panel = { [name]: checked }; + } else { + updatedState.settings = { [name]: checked }; + } + + // Send Message to Background + sendMessage('setPanelData', { [name]: checked }); + + // Update State for PanelAndroid UI + return updatedState; +} + +export function selectItem({ actionData }) { + const { event, value } = actionData; + + // Send Message to Background + sendMessage('setPanelData', { [event]: value }); + + // Update State for PanelAndroid UI + return { + settings: { + [event]: value, + }, + }; +} diff --git a/app/panel-android/actions/summaryActions.js b/app/panel-android/actions/summaryActions.js index 7dfb02f46..819c91c74 100644 --- a/app/panel-android/actions/summaryActions.js +++ b/app/panel-android/actions/summaryActions.js @@ -18,6 +18,57 @@ function getPageHostFromSummary(summary) { return summary.pageHost.toLowerCase().replace(/^(http[s]?:\/\/)?(www\.)?/, ''); } +export function updateSitePolicy({ actionData, state }) { + const { type, pageHost } = actionData; + const { summary } = state; + const { site_blacklist, site_whitelist } = summary; + + const host = pageHost.replace(/^www\./, ''); + + let updated_blacklist = site_blacklist.slice(0); + let updated_whitelist = site_whitelist.slice(0); + + if (type === 'whitelist') { + if (site_blacklist.includes(host)) { + // remove from backlist if site is whitelisted + updated_blacklist = removeFromArray(site_blacklist, site_blacklist.indexOf(host)); + } + if (!site_whitelist.includes(host)) { + // add to whitelist + updated_whitelist = addToArray(site_whitelist, host); + } else { + // remove from whitelist + updated_whitelist = removeFromArray(site_whitelist, site_whitelist.indexOf(host)); + } + } else { + if (site_whitelist.includes(host)) { + // remove from whitelisted if site is blacklisted + updated_whitelist = removeFromArray(site_whitelist, site_whitelist.indexOf(host)); + } + if (!site_blacklist.includes(host)) { + // add to blacklist + updated_blacklist = addToArray(site_blacklist, host); + } else { + // remove from blacklist + updated_blacklist = removeFromArray(site_blacklist, site_blacklist.indexOf(host)); + } + } + + // Send Message to Background + sendMessage('setPanelData', { + site_whitelist: updated_whitelist, + site_blacklist: updated_blacklist, + }); + + // Update State for PanelAndroid UI + return { + summary: { + site_whitelist: updated_whitelist, + site_blacklist: updated_blacklist, + }, + }; +} + export function handleTrustButtonClick({ state }) { const { summary } = state; // This pageHost has to be cleaned. diff --git a/app/panel-android/components/PanelAndroid.jsx b/app/panel-android/components/PanelAndroid.jsx index 3fd5643fb..e233582ce 100644 --- a/app/panel-android/components/PanelAndroid.jsx +++ b/app/panel-android/components/PanelAndroid.jsx @@ -90,7 +90,14 @@ class PanelAndroid extends React.Component { setPanelState = (tabId) => { getPanelData(tabId).then((data) => { - this.setState({ panel: data.panel }); + this.setState(prevState => ({ + panel: data.panel, + settings: { + ...prevState.settings, + reload_banner_status: data.panel.reload_banner_status, + trackers_banner_status: data.panel.trackers_banner_status, + } + })); }); } @@ -102,7 +109,13 @@ class PanelAndroid extends React.Component { setSettingsState = () => { getSettingsData().then((data) => { - this.setState({ settings: data }); + this.setState(prevState => ({ + settings: { + ...prevState.settings, + ...data, + dbUpdateText: t('settings_update_now'), + } + })); }); } @@ -129,7 +142,13 @@ class PanelAndroid extends React.Component { callGlobalAction = ({ actionName, actionData = {} }) => { const updated = handleAllActions({ actionName, actionData, state: this.state }); - if (Object.keys(updated).length !== 0) { + if (updated instanceof Promise) { + updated.then((result) => { + if (Object.keys(result).length !== 0) { + this.setGlobalState(result); + } + }); + } else if (Object.keys(updated).length !== 0) { this.setGlobalState(updated); } } @@ -151,18 +170,13 @@ class PanelAndroid extends React.Component { _renderSettings() { const { summary, settings } = this.state; - const actions = { - updateSitePolicy: () => { console.log('updateSitePolicy'); }, - updateDatabase: () => { console.log('updateDatabase'); }, - selectItem: () => { console.log('selectItem'); }, - }; return ( { this.changeView('overview'); }} + callGlobalAction={this.callGlobalAction} /> ); } diff --git a/app/panel-android/components/content/Settings.jsx b/app/panel-android/components/content/Settings.jsx index 1fe2ba5cf..63f4bb5fe 100644 --- a/app/panel-android/components/content/Settings.jsx +++ b/app/panel-android/components/content/Settings.jsx @@ -33,17 +33,9 @@ class Account extends React.Component { }; } - toggleCheckbox = (a, b, c, d, e) => { - console.log('bloink toggleCheckbox', a, b, c, d, e); - } - - showToast = (a, b, c, d, e) => { - console.log('bloink showToast', a, b, c, d, e); - } - - selectItem = (a, b, c, d, e) => { - console.log('bloink selectItem', a, b, c, d, e); - } + // showToast = (a, b, c, d, e) => { + // console.log('bloink showToast', a, b, c, d, e); + // } clickBack = () => { const { clickHome } = this.props; @@ -56,6 +48,42 @@ class Account extends React.Component { } } + updateSitePolicy = ({ type, pageHost }) => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'updateSitePolicy', + actionData: { type, pageHost }, + }); + } + + updateDatabase = () => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'updateDatabase', + }); + } + + toggleCheckbox = (event) => { + const { callGlobalAction } = this.props; + const { name, checked } = event.currentTarget; + + callGlobalAction({ + actionName: 'updateSettingCheckbox', + actionData: { name, checked }, + }); + } + + selectItem = ({ event, value }) => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'selectItem', + actionData: { event, value }, + }); + } + _renderSettingsHeader() { const { view } = this.state; @@ -122,8 +150,11 @@ class Account extends React.Component { } _renderSettingsTrustRestrict() { - const { actions, summary } = this.props; + const { summary } = this.props; const { site_whitelist, site_blacklist } = summary; + const actions = { + updateSitePolicy: this.updateSitePolicy, + }; return (
    From 48ff206424ae9b6f67f09ad51230d4461af947a9 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Wed, 1 Jul 2020 20:06:12 -0400 Subject: [PATCH 52/89] Update BlockingTracker to render in a List compoent to better performance --- .../components/content/BlockingCategory.jsx | 71 ++++--- .../components/content/FixedMenu.jsx | 147 ------------- app/panel-android/components/content/Path.jsx | 98 --------- .../components/content/TrackerItem.jsx | 194 ------------------ app/panel-android/utils/tracker-info.js | 2 +- .../components/Settings/TrustAndRestrict.jsx | 1 - package.json | 1 + src/classes/GhosteryDebug.js | 1 - yarn.lock | 20 ++ 9 files changed, 69 insertions(+), 466 deletions(-) delete mode 100644 app/panel-android/components/content/FixedMenu.jsx delete mode 100644 app/panel-android/components/content/Path.jsx delete mode 100644 app/panel-android/components/content/TrackerItem.jsx diff --git a/app/panel-android/components/content/BlockingCategory.jsx b/app/panel-android/components/content/BlockingCategory.jsx index b1d022877..b25289837 100644 --- a/app/panel-android/components/content/BlockingCategory.jsx +++ b/app/panel-android/components/content/BlockingCategory.jsx @@ -14,6 +14,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ClassNames from 'classnames'; +import { FixedSizeList as List } from 'react-window'; import BlockingTracker from './BlockingTracker'; class BlockingCategory extends React.Component { @@ -26,10 +27,15 @@ class BlockingCategory extends React.Component { this.heightTracker = 50; this.heightListHeader = 30; + this.maxListHeight = 750; } - getHeightTrackerList(count) { - return this.heightListHeader + count * this.heightTracker; + getListHeight(count) { + return Math.min(this.maxListHeight, count * this.heightTracker); + } + + getListHeightWithHeader(count) { + return this.heightListHeader + this.getListHeight(count); } get categorySelectStatus() { @@ -151,23 +157,46 @@ class BlockingCategory extends React.Component { ); } + renderBlockingTracker = ({ index, style }) => { + const { + category, + type, + siteProps, + callGlobalAction, + } = this.props; + const { id, trackers } = category; + const tracker = trackers[index]; + + return ( +
    + { this.toggleTrackerSelectOpen(tracker.id); }} + open={this.getTrackerOpenStatus(tracker.id)} + siteProps={siteProps} + callGlobalAction={callGlobalAction} + /> +
    +
    + ); + } + render() { + const { openTrackerIndex } = this.state; const { index, category, open, toggleCategoryOpen, - type, - siteProps, - callGlobalAction, } = this.props; const { - id, name, img_name, num_total, num_blocked, - trackers, } = category; const categoryImage = `/app/images/panel-android/categories/${img_name}.svg`; @@ -189,28 +218,22 @@ class BlockingCategory extends React.Component { {this.renderToggleArrow()}
    -
    +
    {open && (
    -
    +
    {t('blocking_category_trackers')} {t('blocking_category_blocked')}
    - {trackers.map((tracker, trackerIndex) => ( -
    - -
    -
    - ))} + + {this.renderBlockingTracker} +
    )}
    diff --git a/app/panel-android/components/content/FixedMenu.jsx b/app/panel-android/components/content/FixedMenu.jsx deleted file mode 100644 index 3301bf4ff..000000000 --- a/app/panel-android/components/content/FixedMenu.jsx +++ /dev/null @@ -1,147 +0,0 @@ -/** - * FixedMenu Component - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2019 Ghostery, Inc. All rights reserved. - * - * 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 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import MenuItem from './MenuItem'; - -export default class FixedMenu extends React.Component { - constructor(props) { - super(props); - this.state = { - open: false, - currentMenuItemText: FixedMenu.defaultHeaderText, - }; - } - - static get defaultHeaderText() { - return 'Enhanced Options'; - } - - get cliqzModuleData() { - const { cliqzModuleData } = this.props; - return cliqzModuleData || {}; - } - - get antiTrackingData() { - return this.cliqzModuleData.antitracking || {}; - } - - get adBlockData() { - return this.cliqzModuleData.adblock || {}; - } - - get smartBlockData() { - const { panel } = this.props; - return panel.smartBlock || {}; - } - - getCount = (type) => { - let total = 0; - switch (type) { - case 'enable_anti_tracking': { - const categories = Object.keys(this.antiTrackingData); - categories.forEach((category) => { - const apps = Object.keys(this.antiTrackingData[category]); - apps.forEach((app) => { - if (this.antiTrackingData[category][app] === 'unsafe') { - total++; - } - }); - }); - return total; - } - case 'enable_ad_block': - return (this.adBlockData && this.adBlockData.totalCount) || 0; - case 'enable_smart_block': - Object.keys(this.smartBlockData.blocked || {}).forEach(() => { - total++; - }); - Object.keys(this.smartBlockData.unblocked || {}).forEach(() => { - total++; - }); - return total; - default: - return 0; - } - } - - toggleMenu = () => { - this.setState(prevState => ({ open: !prevState.open })); - } - - updateHeaderText = (text) => { - const textToShow = text || FixedMenu.defaultHeaderText; - - this.setState({ - currentMenuItemText: textToShow, - }); - } - - render() { - const { panel } = this.props; - const { open, currentMenuItemText } = this.state; - return ( -
    -
    -

    {currentMenuItemText}

    -
    -
      -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    -
    - ); - } -} - -FixedMenu.propTypes = { - panel: PropTypes.shape, - cliqzModuleData: PropTypes.shape, -}; - -FixedMenu.defaultProps = { - panel: {}, - cliqzModuleData: {}, -}; diff --git a/app/panel-android/components/content/Path.jsx b/app/panel-android/components/content/Path.jsx deleted file mode 100644 index 8f37a2d4d..000000000 --- a/app/panel-android/components/content/Path.jsx +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Path Component - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2019 Ghostery, Inc. All rights reserved. - * - * 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 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; - -const INTERVAL = 1000; // Define the maximum rendering time for this path - -export default class Path extends React.Component { - constructor(props) { - super(props); - this.myRef = React.createRef(); - this.timer = null; - } - - static polarToCartesian(centerX, centerY, radius, angleInDegrees) { - const angleInRadians = (angleInDegrees - 90) * (Math.PI / 180.0); - - return { - x: centerX + (radius * Math.cos(angleInRadians)), - y: centerY + (radius * Math.sin(angleInRadians)) - }; - } - - static describeArc(x, y, radius, startAngle, endAngle) { - const start = Path.polarToCartesian(x, y, radius, startAngle); - const end = Path.polarToCartesian(x, y, radius, endAngle); - - const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1'; - - const d = [ - 'M', start.x, start.y, - 'A', radius, radius, 0, largeArcFlag, 1, end.x, end.y - ].join(' '); - - return d; - } - - componentDidMount() { - const node = this.myRef.current; - node.style.setProperty('--stroke-length', `${node.getTotalLength()}`); - // Check and call props.handler() if the animationEnd event doesn't get fired somehow - this.timer = setInterval(() => { - clearInterval(this.timer); // Run this only once - const { handler } = this.props; - handler(); - }, INTERVAL); - } - - componentWillUnmount() { - clearInterval(this.timer); - } - - onAnimationEndHandler = () => { - clearInterval(this.timer); - const { handler } = this.props; - handler(); - } - - render() { - const { radius, path } = this.props; - const { start, category } = path; - // Fix error for single path - const end = path.end === 360 ? 359.9999 : path.end; - - const d = Path.describeArc(0, 0, radius, start, end); - - return ( - - ); - } -} - -Path.propTypes = { - radius: PropTypes.number.isRequired, - path: PropTypes.shape, - handler: PropTypes.func.isRequired, -}; - -Path.defaultProps = { - path: {}, -}; diff --git a/app/panel-android/components/content/TrackerItem.jsx b/app/panel-android/components/content/TrackerItem.jsx deleted file mode 100644 index 329ad1fde..000000000 --- a/app/panel-android/components/content/TrackerItem.jsx +++ /dev/null @@ -1,194 +0,0 @@ -/** - * TrackerItem Component - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2019 Ghostery, Inc. All rights reserved. - * - * 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 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import getUrlFromTrackerId from '../../utils/tracker-info'; - -export default class TrackerItem extends React.Component { - get trackerSelectStatus() { - const { type, tracker } = this.props; - const { siteProps } = this.context; - // Only for site trackers - if (type === 'site-trackers') { - if (siteProps.isTrusted) { - return 'trusted'; - } - - if (siteProps.isRestricted) { - return 'restricted'; - } - } - - if (tracker.ss_allowed) { - return 'trusted'; - } - - if (tracker.ss_blocked) { - return 'restricted'; - } - - if (tracker.blocked) { - return 'blocked'; - } - - return ''; - } - - get showMenu() { - const { showMenu } = this.props; - return showMenu; - } - - get disabledStatus() { - return ['trusted', 'restricted'].includes(this.trackerSelectStatus) ? 'disabled' : ''; - } - - clickButtonTrust = () => { - const { - tracker, categoryId, index, toggleMenu - } = this.props; - const { callGlobalAction } = this.context; - const ss_allowed = !tracker.ss_allowed; - - callGlobalAction({ - actionName: 'trustRestrictBlockSiteTracker', - actionData: { - app_id: tracker.id, - cat_id: categoryId, - trust: ss_allowed, - restrict: false, - block: tracker.blocked, // Keep blocking - } - }); - toggleMenu(index); // Hide menu - } - - clickButtonRestrict = () => { - const { - tracker, categoryId, index, toggleMenu - } = this.props; - const { callGlobalAction } = this.context; - const ss_blocked = !tracker.ss_blocked; - callGlobalAction({ - actionName: 'trustRestrictBlockSiteTracker', - actionData: { - app_id: tracker.id, - cat_id: categoryId, - restrict: ss_blocked, - trust: false, - block: tracker.blocked, // Keep blocking - } - }); - toggleMenu(index); - } - - clickButtonBlock = (hideMenu = true) => { - // onClick={(e) => { e.stopPropagation(); this.clickButtonBlock(false); }} - const { - tracker, type, categoryId, index, toggleMenu - } = this.props; - const { callGlobalAction } = this.context; - if (this.disabledStatus) { - return; - } - - const blocked = !tracker.blocked; - - if (type === 'site-trackers') { - callGlobalAction({ - actionName: 'trustRestrictBlockSiteTracker', - actionData: { - app_id: tracker.id, - cat_id: categoryId, - block: blocked, - trust: false, - restrict: false, - } - }); - } else { - callGlobalAction({ - actionName: 'blockUnblockGlobalTracker', - actionData: { - app_id: tracker.id, - cat_id: categoryId, - block: blocked, - } - }); - } - - if (hideMenu) { - toggleMenu(index); - } - } - - openTrackerLink = () => { - const { tracker } = this.props; - const url = getUrlFromTrackerId(tracker.id); - const win = window.open(url, '_blank'); - win.focus(); - } - - toggleMenu = () => { - const { index, toggleMenu } = this.props; - toggleMenu(index); - } - - render() { - const { tracker, type } = this.props; - return ( -
  • -
    - - - -
    -
  • - - ); - } -} - -TrackerItem.propTypes = { - toggleMenu: PropTypes.func.isRequired, - index: PropTypes.number.isRequired, - showMenu: PropTypes.bool, - tracker: PropTypes.shape, - categoryId: PropTypes.string, - type: PropTypes.string, -}; - -TrackerItem.defaultProps = { - showMenu: false, - tracker: {}, - categoryId: '', - type: '', -}; - -TrackerItem.contextTypes = { - callGlobalAction: PropTypes.func, - siteProps: PropTypes.shape, -}; diff --git a/app/panel-android/utils/tracker-info.js b/app/panel-android/utils/tracker-info.js index 9749513fc..1ea1d2253 100644 --- a/app/panel-android/utils/tracker-info.js +++ b/app/panel-android/utils/tracker-info.js @@ -14,7 +14,7 @@ * @namespace PanelAndroidUtils */ -import { apps } from '../../../cliqz/core/tracker_db_v2.json'; +import { apps } from '../../../cliqz/antitracking/tracker_db_v2.json'; // Link to whotracks.me website export default function getUrlFromTrackerId(id) { diff --git a/app/panel/components/Settings/TrustAndRestrict.jsx b/app/panel/components/Settings/TrustAndRestrict.jsx index 8f49532d0..68f4b237f 100644 --- a/app/panel/components/Settings/TrustAndRestrict.jsx +++ b/app/panel/components/Settings/TrustAndRestrict.jsx @@ -245,5 +245,4 @@ TrustAndRestrict.propTypes = { site_blacklist: PropTypes.arrayOf(PropTypes.string).isRequired, }; - export default TrustAndRestrict; diff --git a/package.json b/package.json index 43374ce6d..807bc1954 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "react-redux": "^7.2.0", "react-router-dom": "^5.2.0", "react-svg": "^11.0.26", + "react-window": "^1.8.5", "redux": "^4.0.5", "redux-object": "^0.5.10", "redux-thunk": "^2.2.0", diff --git a/src/classes/GhosteryDebug.js b/src/classes/GhosteryDebug.js index 544467160..5c4304e74 100644 --- a/src/classes/GhosteryDebug.js +++ b/src/classes/GhosteryDebug.js @@ -124,7 +124,6 @@ class GhosteryDebug { }); } - return new Promise((resolve) => { Promise.all([ _getUserCookies(), diff --git a/yarn.lock b/yarn.lock index 4be63ed12..bb2caa0dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -375,6 +375,13 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" +"@babel/runtime@^7.0.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99" + integrity sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.5.5": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" @@ -5726,6 +5733,11 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= +"memoize-one@>=3.1.1 <6": + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== + memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -7068,6 +7080,14 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.13.1: react-is "^16.8.6" scheduler "^0.19.1" +react-window@^1.8.5: + version "1.8.5" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1" + integrity sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" From 44cfe9070bf76df57f8173da6008101c15d10f03 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Thu, 2 Jul 2020 19:45:59 -0400 Subject: [PATCH 53/89] Update PausedButton actions to work with dropdown --- app/panel-android/actions/handler.js | 2 +- app/panel-android/actions/summaryActions.js | 12 ++++++------ app/panel-android/components/content/OverviewTab.jsx | 9 +++++++-- .../content/__tests__/BlockingCategory.jsx | 4 ++-- app/scss/android/_overview_tab.scss | 1 + 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/panel-android/actions/handler.js b/app/panel-android/actions/handler.js index 210c53140..6f32b7112 100644 --- a/app/panel-android/actions/handler.js +++ b/app/panel-android/actions/handler.js @@ -35,7 +35,7 @@ export default function handleAllActions({ actionName, actionData, state }) { break; case 'handlePauseButtonClick': - updated = handlePauseButtonClick({ state }); + updated = handlePauseButtonClick({ actionData, state }); break; case 'cliqzFeatureToggle': diff --git a/app/panel-android/actions/summaryActions.js b/app/panel-android/actions/summaryActions.js index 819c91c74..66e45e0d5 100644 --- a/app/panel-android/actions/summaryActions.js +++ b/app/panel-android/actions/summaryActions.js @@ -152,18 +152,18 @@ export function handleRestrictButtonClick({ state }) { }; } -export function handlePauseButtonClick({ state }) { - const { summary } = state; - const currentState = summary.paused_blocking; +export function handlePauseButtonClick({ actionData }) { + const { paused_blocking, time } = actionData; sendMessage('setPanelData', { - paused_blocking: !currentState, + paused_blocking: time || paused_blocking, }); return { summary: { - paused_blocking: !currentState, - } + paused_blocking, + paused_blocking_timeout: time, + }, }; } diff --git a/app/panel-android/components/content/OverviewTab.jsx b/app/panel-android/components/content/OverviewTab.jsx index 2428e24bb..bc1a65d4b 100644 --- a/app/panel-android/components/content/OverviewTab.jsx +++ b/app/panel-android/components/content/OverviewTab.jsx @@ -145,11 +145,16 @@ class OverviewTab extends React.Component { }); } - handlePauseButtonClick = () => { - const { callGlobalAction } = this.props; + handlePauseButtonClick = (time) => { + const { summary, callGlobalAction } = this.props; + const { paused_blocking } = summary; callGlobalAction({ actionName: 'handlePauseButtonClick', + actionData: { + paused_blocking: (typeof time === 'number' ? true : !paused_blocking), + time: typeof time === 'number' ? time * 60000 : 0, + }, }); } diff --git a/app/panel-android/components/content/__tests__/BlockingCategory.jsx b/app/panel-android/components/content/__tests__/BlockingCategory.jsx index 9d3981ef6..beaeb142b 100644 --- a/app/panel-android/components/content/__tests__/BlockingCategory.jsx +++ b/app/panel-android/components/content/__tests__/BlockingCategory.jsx @@ -341,11 +341,11 @@ describe('app/panel-android/components/content/BlockingCategory.jsx', () => { /> ); - expect(component.find('.BlockingCategory__tracker').length).toBe(0); + expect(component.find('.BlockingCategory__listHeader').length).toBe(0); component.find('.BlockingCategory__details').simulate('click'); component.setProps({ open: true }); expect(toggleCategoryOpen.mock.calls.length).toBe(1); - expect(component.find('.BlockingCategory__tracker').length).toBe(2); + expect(component.find('.BlockingCategory__listHeader').length).toBe(1); expect(callGlobalAction.mock.calls.length).toBe(0); component.find('.BlockingSelectButton').simulate('click', { stopPropagation: () => {} }); diff --git a/app/scss/android/_overview_tab.scss b/app/scss/android/_overview_tab.scss index 114afb924..16ccb95f5 100644 --- a/app/scss/android/_overview_tab.scss +++ b/app/scss/android/_overview_tab.scss @@ -20,6 +20,7 @@ .OverviewTab__NavigationLink { cursor: pointer; + margin: 0 10px; } } From 935e32fc74def5c84f4df83d510326cd26834893 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Thu, 9 Jul 2020 13:09:21 -0400 Subject: [PATCH 54/89] Update path to cliqz trackerdb. Update compyright year for touched files in this PR --- app/content-scripts/debug_information.js | 16 ++++++++++++++++ app/content-scripts/notifications.js | 2 +- app/panel-android/actions/summaryActions.js | 2 +- .../components/content/Settings.jsx | 4 ---- app/panel-android/utils/tracker-info.js | 4 ++-- .../components/BuildingBlocks/CliqzFeature.jsx | 2 +- .../BuildingBlocks/GhosteryFeature.jsx | 2 +- .../components/BuildingBlocks/NotScanned.jsx | 2 +- .../components/BuildingBlocks/PauseButton.jsx | 2 +- app/panel/components/Header.jsx | 2 +- app/panel/components/Help.jsx | 2 +- app/panel/components/Settings/AdBlocker.jsx | 2 +- .../components/Settings/GeneralSettings.jsx | 2 +- app/panel/components/Settings/Notifications.jsx | 2 +- app/panel/components/Settings/OptIn.jsx | 2 +- .../components/Settings/TrustAndRestrict.jsx | 2 +- .../Settings/__tests__/TrustAndRestrict.jsx | 2 +- app/panel/components/__tests__/PauseButton.jsx | 2 +- app/scss/panel.scss | 2 +- app/scss/partials/_header.scss | 2 +- app/scss/partials/_pause_button.scss | 2 +- app/scss/partials/_settings.scss | 2 +- app/scss/partials/_subscribe.scss | 2 +- app/scss/partials/_summary.scss | 2 +- app/templates/debug_information.html | 12 ++++++++++++ src/background.js | 2 +- src/classes/Account.js | 2 +- webpack.config.js | 2 +- 28 files changed, 54 insertions(+), 30 deletions(-) diff --git a/app/content-scripts/debug_information.js b/app/content-scripts/debug_information.js index 9825b164a..47031fb3a 100644 --- a/app/content-scripts/debug_information.js +++ b/app/content-scripts/debug_information.js @@ -1,3 +1,19 @@ +/** + * Debug Information + * + * This file asks the background for Ghostery Debug information + * and writes the returned JSON to the body innerText. + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * 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 + */ + chrome.runtime.sendMessage({ name: 'debug_information' }, (response) => { document.body.innerText = response; }); diff --git a/app/content-scripts/notifications.js b/app/content-scripts/notifications.js index f7807bbdd..dd0d50195 100644 --- a/app/content-scripts/notifications.js +++ b/app/content-scripts/notifications.js @@ -7,7 +7,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel-android/actions/summaryActions.js b/app/panel-android/actions/summaryActions.js index 66e45e0d5..fbf2103ad 100644 --- a/app/panel-android/actions/summaryActions.js +++ b/app/panel-android/actions/summaryActions.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel-android/components/content/Settings.jsx b/app/panel-android/components/content/Settings.jsx index 63f4bb5fe..b8605581e 100644 --- a/app/panel-android/components/content/Settings.jsx +++ b/app/panel-android/components/content/Settings.jsx @@ -33,10 +33,6 @@ class Account extends React.Component { }; } - // showToast = (a, b, c, d, e) => { - // console.log('bloink showToast', a, b, c, d, e); - // } - clickBack = () => { const { clickHome } = this.props; const { view } = this.state; diff --git a/app/panel-android/utils/tracker-info.js b/app/panel-android/utils/tracker-info.js index 1ea1d2253..58ae9b2a1 100644 --- a/app/panel-android/utils/tracker-info.js +++ b/app/panel-android/utils/tracker-info.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 @@ -14,7 +14,7 @@ * @namespace PanelAndroidUtils */ -import { apps } from '../../../cliqz/antitracking/tracker_db_v2.json'; +import { apps } from '../../../cliqz/core/tracker_db_v2.json'; // Link to whotracks.me website export default function getUrlFromTrackerId(id) { diff --git a/app/panel/components/BuildingBlocks/CliqzFeature.jsx b/app/panel/components/BuildingBlocks/CliqzFeature.jsx index 56eae26e4..3abc03dc9 100644 --- a/app/panel/components/BuildingBlocks/CliqzFeature.jsx +++ b/app/panel/components/BuildingBlocks/CliqzFeature.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/BuildingBlocks/GhosteryFeature.jsx b/app/panel/components/BuildingBlocks/GhosteryFeature.jsx index f0ff60354..d66e943bd 100644 --- a/app/panel/components/BuildingBlocks/GhosteryFeature.jsx +++ b/app/panel/components/BuildingBlocks/GhosteryFeature.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/BuildingBlocks/NotScanned.jsx b/app/panel/components/BuildingBlocks/NotScanned.jsx index 8499df78a..74eb6dd34 100644 --- a/app/panel/components/BuildingBlocks/NotScanned.jsx +++ b/app/panel/components/BuildingBlocks/NotScanned.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/BuildingBlocks/PauseButton.jsx b/app/panel/components/BuildingBlocks/PauseButton.jsx index c76934ddb..d7ab0e287 100644 --- a/app/panel/components/BuildingBlocks/PauseButton.jsx +++ b/app/panel/components/BuildingBlocks/PauseButton.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/Header.jsx b/app/panel/components/Header.jsx index d9620e96c..f20bb7ac5 100644 --- a/app/panel/components/Header.jsx +++ b/app/panel/components/Header.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/Help.jsx b/app/panel/components/Help.jsx index c97b45ddc..0062034ce 100644 --- a/app/panel/components/Help.jsx +++ b/app/panel/components/Help.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/Settings/AdBlocker.jsx b/app/panel/components/Settings/AdBlocker.jsx index e86bd6d4c..2a5466602 100644 --- a/app/panel/components/Settings/AdBlocker.jsx +++ b/app/panel/components/Settings/AdBlocker.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/Settings/GeneralSettings.jsx b/app/panel/components/Settings/GeneralSettings.jsx index 0a147a732..02e5704b0 100644 --- a/app/panel/components/Settings/GeneralSettings.jsx +++ b/app/panel/components/Settings/GeneralSettings.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/Settings/Notifications.jsx b/app/panel/components/Settings/Notifications.jsx index 5a49035d7..9efbef082 100644 --- a/app/panel/components/Settings/Notifications.jsx +++ b/app/panel/components/Settings/Notifications.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/Settings/OptIn.jsx b/app/panel/components/Settings/OptIn.jsx index 532260039..a816cd1ea 100644 --- a/app/panel/components/Settings/OptIn.jsx +++ b/app/panel/components/Settings/OptIn.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/Settings/TrustAndRestrict.jsx b/app/panel/components/Settings/TrustAndRestrict.jsx index 68f4b237f..03ed86e6b 100644 --- a/app/panel/components/Settings/TrustAndRestrict.jsx +++ b/app/panel/components/Settings/TrustAndRestrict.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx b/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx index eb70ac5bd..39ef2b5da 100644 --- a/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx +++ b/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel/components/__tests__/PauseButton.jsx b/app/panel/components/__tests__/PauseButton.jsx index 10115c797..0aeafae24 100644 --- a/app/panel/components/__tests__/PauseButton.jsx +++ b/app/panel/components/__tests__/PauseButton.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/scss/panel.scss b/app/scss/panel.scss index 699d662c5..88bffcdff 100644 --- a/app/scss/panel.scss +++ b/app/scss/panel.scss @@ -7,7 +7,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/scss/partials/_header.scss b/app/scss/partials/_header.scss index a37f89837..c7ca1357e 100644 --- a/app/scss/partials/_header.scss +++ b/app/scss/partials/_header.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/scss/partials/_pause_button.scss b/app/scss/partials/_pause_button.scss index 7d9ae27c2..5f546a924 100644 --- a/app/scss/partials/_pause_button.scss +++ b/app/scss/partials/_pause_button.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/scss/partials/_settings.scss b/app/scss/partials/_settings.scss index 50c49f7d1..c9452812a 100644 --- a/app/scss/partials/_settings.scss +++ b/app/scss/partials/_settings.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/scss/partials/_subscribe.scss b/app/scss/partials/_subscribe.scss index 5103155eb..59a440ca5 100644 --- a/app/scss/partials/_subscribe.scss +++ b/app/scss/partials/_subscribe.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/scss/partials/_summary.scss b/app/scss/partials/_summary.scss index 2ec34e218..b5d87bacf 100644 --- a/app/scss/partials/_summary.scss +++ b/app/scss/partials/_summary.scss @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/templates/debug_information.html b/app/templates/debug_information.html index e61e8e992..1fce4e0d4 100644 --- a/app/templates/debug_information.html +++ b/app/templates/debug_information.html @@ -1,4 +1,16 @@ + diff --git a/src/background.js b/src/background.js index f39e4ee3d..71db2789b 100644 --- a/src/background.js +++ b/src/background.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/src/classes/Account.js b/src/classes/Account.js index bac384fe0..abb656079 100644 --- a/src/classes/Account.js +++ b/src/classes/Account.js @@ -6,7 +6,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/webpack.config.js b/webpack.config.js index 640298548..526cdf878 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 From f07805c833e6d170ce34290b98a5dd0a1f9bf103 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Tue, 14 Jul 2020 13:04:57 -0400 Subject: [PATCH 55/89] Fix linting error --- src/classes/GhosteryDebug.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/classes/GhosteryDebug.js b/src/classes/GhosteryDebug.js index 544467160..5c4304e74 100644 --- a/src/classes/GhosteryDebug.js +++ b/src/classes/GhosteryDebug.js @@ -124,7 +124,6 @@ class GhosteryDebug { }); } - return new Promise((resolve) => { Promise.all([ _getUserCookies(), From 7e0d83f723423725f61ca451a2cf0c21280c0c81 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Sun, 19 Jul 2020 19:04:05 -0400 Subject: [PATCH 56/89] Add CliqzFeatures to PanelAndroid TrackerList --- .../{trackerActions.js => blockingActions.js} | 4 +- app/panel-android/actions/handler.js | 2 +- app/panel-android/actions/settingsActions.js | 3 + app/panel-android/components/PanelAndroid.jsx | 7 +- .../components/content/BlockingCategories.jsx | 3 + .../components/content/BlockingCategory.jsx | 5 +- .../components/content/BlockingTab.jsx | 4 + .../components/content/BlockingTracker.jsx | 97 ++++++++++++++++--- app/scss/android/_blocking_tab.scss | 63 ++++++++++++ src/classes/PromoModals.js | 2 +- 10 files changed, 170 insertions(+), 20 deletions(-) rename app/panel-android/actions/{trackerActions.js => blockingActions.js} (99%) diff --git a/app/panel-android/actions/trackerActions.js b/app/panel-android/actions/blockingActions.js similarity index 99% rename from app/panel-android/actions/trackerActions.js rename to app/panel-android/actions/blockingActions.js index f5ea7cf84..0fbf2567e 100644 --- a/app/panel-android/actions/trackerActions.js +++ b/app/panel-android/actions/blockingActions.js @@ -1,10 +1,10 @@ /** - * Tracker Action creators + * Blocking Action creators * * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2019 Ghostery, Inc. All rights reserved. + * Copyright 2020 Ghostery, Inc. All rights reserved. * * 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 diff --git a/app/panel-android/actions/handler.js b/app/panel-android/actions/handler.js index 6f32b7112..12950eb4e 100644 --- a/app/panel-android/actions/handler.js +++ b/app/panel-android/actions/handler.js @@ -16,7 +16,7 @@ import { } from './summaryActions'; import { trustRestrictBlockSiteTracker, blockUnblockGlobalTracker, blockUnBlockAllTrackers, resetSettings -} from './trackerActions'; +} from './blockingActions'; import { updateDatabase, updateSettingCheckbox, selectItem } from './settingsActions'; diff --git a/app/panel-android/actions/settingsActions.js b/app/panel-android/actions/settingsActions.js index 946e5ca6b..0129886a0 100644 --- a/app/panel-android/actions/settingsActions.js +++ b/app/panel-android/actions/settingsActions.js @@ -43,6 +43,9 @@ export function updateSettingCheckbox({ actionData }) { if (name === 'trackers_banner_status' || name === 'reload_banner_status') { updatedState.panel = { [name]: checked }; + } else if (name === 'toggle_individual_trackers') { + updatedState.blocking = { [name]: checked }; + updatedState.settings = { [name]: checked }; } else { updatedState.settings = { [name]: checked }; } diff --git a/app/panel-android/components/PanelAndroid.jsx b/app/panel-android/components/PanelAndroid.jsx index e233582ce..c68473bf2 100644 --- a/app/panel-android/components/PanelAndroid.jsx +++ b/app/panel-android/components/PanelAndroid.jsx @@ -51,8 +51,8 @@ class PanelAndroid extends React.Component { pageUrl: '', }, cliqzModuleData: { - adBlock: { trackerCount: 0 }, - antiTracking: { trackerCount: 0 }, + adBlock: { trackerCount: 0, unknownTrackers: [] }, + antiTracking: { trackerCount: 0, unknownTrackers: [] }, }, }; } @@ -189,7 +189,7 @@ class PanelAndroid extends React.Component { settings, cliqzModuleData, } = this.state; - const { categories } = blocking; + const { categories, toggle_individual_trackers } = blocking; return ( @@ -209,6 +209,7 @@ class PanelAndroid extends React.Component { diff --git a/app/panel-android/components/content/BlockingCategories.jsx b/app/panel-android/components/content/BlockingCategories.jsx index dc0c53ad6..f367b8dcf 100644 --- a/app/panel-android/components/content/BlockingCategories.jsx +++ b/app/panel-android/components/content/BlockingCategories.jsx @@ -57,6 +57,7 @@ class BlockingCategories extends React.Component { categories, type, siteProps, + settings, callGlobalAction, } = this.props; @@ -68,6 +69,7 @@ class BlockingCategories extends React.Component { key={category.id} index={index} category={category} + settings={settings} toggleCategoryOpen={this.toggleCategoryOpen} open={this.getOpenStatus(index)} type={type} @@ -89,6 +91,7 @@ BlockingCategories.propTypes = { ]).isRequired, siteProps: PropTypes.shape({}).isRequired, callGlobalAction: PropTypes.func.isRequired, + settings: PropTypes.shape({}).isRequired, }; export default BlockingCategories; diff --git a/app/panel-android/components/content/BlockingCategory.jsx b/app/panel-android/components/content/BlockingCategory.jsx index b25289837..5f5ac358a 100644 --- a/app/panel-android/components/content/BlockingCategory.jsx +++ b/app/panel-android/components/content/BlockingCategory.jsx @@ -162,6 +162,7 @@ class BlockingCategory extends React.Component { category, type, siteProps, + settings, callGlobalAction, } = this.props; const { id, trackers } = category; @@ -177,6 +178,7 @@ class BlockingCategory extends React.Component { toggleTrackerSelectOpen={() => { this.toggleTrackerSelectOpen(tracker.id); }} open={this.getTrackerOpenStatus(tracker.id)} siteProps={siteProps} + settings={settings} callGlobalAction={callGlobalAction} />
    @@ -221,7 +223,7 @@ class BlockingCategory extends React.Component {
    {open && (
    -
    +
    {t('blocking_category_trackers')} {t('blocking_category_blocked')}
    @@ -270,6 +272,7 @@ BlockingCategory.propTypes = { isRestricted: PropTypes.bool.isRequired, isPaused: PropTypes.bool.isRequired, }).isRequired, + settings: PropTypes.shape({}).isRequired, callGlobalAction: PropTypes.func.isRequired, }; diff --git a/app/panel-android/components/content/BlockingTab.jsx b/app/panel-android/components/content/BlockingTab.jsx index cc1edde8d..a60566d59 100644 --- a/app/panel-android/components/content/BlockingTab.jsx +++ b/app/panel-android/components/content/BlockingTab.jsx @@ -92,6 +92,7 @@ class BlockingTab extends React.Component { const { type, categories, + settings, siteProps, callGlobalAction, } = this.props; @@ -105,6 +106,7 @@ class BlockingTab extends React.Component { @@ -121,10 +123,12 @@ BlockingTab.propTypes = { callGlobalAction: PropTypes.func.isRequired, siteProps: PropTypes.shape({}).isRequired, categories: PropTypes.arrayOf(PropTypes.shape({})), + settings: PropTypes.shape({}), }; BlockingTab.defaultProps = { categories: [], + settings: {}, }; export default BlockingTab; diff --git a/app/panel-android/components/content/BlockingTracker.jsx b/app/panel-android/components/content/BlockingTracker.jsx index d13c1bcc5..7f1d2f307 100644 --- a/app/panel-android/components/content/BlockingTracker.jsx +++ b/app/panel-android/components/content/BlockingTracker.jsx @@ -20,9 +20,18 @@ class BlockingTracker extends React.Component { get trackerSelectStatus() { const { type, siteProps, tracker } = this.props; const { isTrusted, isRestricted } = siteProps; - const { blocked, ss_allowed = false, ss_blocked = false } = tracker; + const { + blocked, + ss_allowed = false, + ss_blocked = false, + warningSmartBlock = false, + } = tracker; if (type === 'site') { + if (warningSmartBlock) { + return 'override-sb'; + } + if (isTrusted) { return 'trusted'; } @@ -149,26 +158,77 @@ class BlockingTracker extends React.Component { }); } - renderTrackerSelect() { + renderTrackerModified() { + const { type, tracker } = this.props; + const { cliqzAdCount, cliqzCookieCount, cliqzFingerprintCount } = tracker; + + if (type === 'global') { + return null; + } + + return ( +
    + {cliqzAdCount > 0 && ( + + {`${cliqzAdCount} ${cliqzAdCount === 1 ? t('ad') : t('ads')}`} + + )} + {cliqzCookieCount > 0 && ( + + {`${cliqzCookieCount} ${cliqzCookieCount === 1 ? t('cookie') : t('cookies')}`} + + )} + {cliqzFingerprintCount > 0 && ( + + {`${cliqzFingerprintCount} ${cliqzFingerprintCount === 1 ? t('fingerprint') : t('fingerprints')}`} + + )} +
    + ); + } + + renderTrackerStatus() { const trackerSelect = this.trackerSelectStatus; - const trackerSelectClassNames = ClassNames('BlockingSelectButton', { + const trackerSelectClassNames = ClassNames({ + OverrideSmartBlock: trackerSelect === 'override-sb', + BlockingSelectButton: trackerSelect.indexOf('override-') === -1, BlockingSelectButton__blocked: trackerSelect === 'blocked', BlockingSelectButton__trusted: trackerSelect === 'trusted', BlockingSelectButton__restricted: trackerSelect === 'restricted', }); return ( -
    +
    ); } - renderBlockingSelectGroup() { - const { type, open, tracker } = this.props; + renderSmartBlockOverflow() { + const { open, tracker } = this.props; + const { warningSmartBlock } = tracker; + const selectGroupClassNames = ClassNames('OverrideText full-height', + 'flex-container align-center-middle', { + 'OverrideText--open': open, + }); + const text = (warningSmartBlock && warningSmartBlock === 'blocked') ? + t('panel_tracker_warning_smartblock_tooltip') : + t('panel_tracker_warning_smartunblock_tooltip'); + + return ( +
    + {text} +
    + ); + } + + renderBlockingOverflow() { + const { type, open, tracker, settings } = this.props; const { ss_allowed = false, ss_blocked = false, blocked } = tracker; + const { toggle_individual_trackers = false } = settings; + const selectGroupClassNames = ClassNames('BlockingSelectGroup full-height', 'flex-container flex-dir-row-reverse', { 'BlockingSelectGroup--open': open, - 'BlockingSelectGroup--wide': type === 'site', + 'BlockingSelectGroup--wide': type === 'site' && toggle_individual_trackers, 'BlockingSelectGroup--disabled': this.selectDisabled, }); const selectBlockClassNames = ClassNames('BlockingSelect BlockingSelect__block', @@ -181,12 +241,12 @@ class BlockingTracker extends React.Component {
    {blocked ? t('android_unblock') : t('android_block')}
    - {type === 'site' && ( + {type === 'site' && toggle_individual_trackers && (
    {ss_blocked ? t('android_unrestrict') : t('android_restrict')}
    )} - {type === 'site' && ( + {type === 'site' && toggle_individual_trackers && (
    {ss_allowed ? t('android_untrust') : t('android_trust')}
    @@ -195,6 +255,15 @@ class BlockingTracker extends React.Component { ); } + renderTrackerOverflow() { + const trackerSelect = this.trackerSelectStatus; + if (trackerSelect === 'override-sb') { + return this.renderSmartBlockOverflow(); + } + + return this.renderBlockingOverflow(); + } + render() { const { index, tracker, toggleTrackerSelectOpen } = this.props; const { name } = tracker; @@ -204,9 +273,12 @@ class BlockingTracker extends React.Component {
    -
    {name}
    - {this.renderTrackerSelect()} - {this.renderBlockingSelectGroup()} +
    +
    {name}
    + {this.renderTrackerModified()} +
    + {this.renderTrackerStatus()} + {this.renderTrackerOverflow()}
    ); } @@ -236,6 +308,7 @@ BlockingTracker.propTypes = { isRestricted: PropTypes.bool.isRequired, isPaused: PropTypes.bool.isRequired, }).isRequired, + settings: PropTypes.shape({}).isRequired, callGlobalAction: PropTypes.func.isRequired, }; diff --git a/app/scss/android/_blocking_tab.scss b/app/scss/android/_blocking_tab.scss index 08f648d1f..39116834f 100644 --- a/app/scss/android/_blocking_tab.scss +++ b/app/scss/android/_blocking_tab.scss @@ -142,6 +142,7 @@ .BlockingSelectGroup { position: absolute; overflow: hidden; + word-break: break-all; right: 0px; width: 0px; margin-right: -10px; @@ -192,6 +193,68 @@ background-image: buildIconTrust($white); } } + + .OverrideSmartBlock { + position: relative; + cursor: pointer; + height: 25px; + width: 25px; + margin-right: 5px; + background-image: buildIconSmartBlocking($ghosty-blue); + background-size: 40px 40px; + background-position: center; + } + + .OverrideText { + position: absolute; + overflow: hidden; + word-break: keep-all; + right: 0px; + width: 0px; + margin-right: -10px; + color: $white; + background-color: $ghosty-blue; + font-size: 13px; + text-align: center; + @include prefix('transition', 'width 300ms ease'); + + &.OverrideText--open { + width: 250px; + } + + span { + min-width: 250px; + } + } + + .RequestModified { + color: $ghosty-blue; + text-transform: capitalize; + font-size: 11px; + font-style: normal; + padding-right: 5px; + + &:before { + content: ""; + position: relative; + display: inline-block; + top: 2px; + height: 13px; + width: 13px; + margin-right: 2px; + background-size: 21px 21px; + background-position: center; + background-repeat: no-repeat; + + } + &.RequestModified--ad:before { + background-image: buildIconAdBlocking($ghosty-blue); + } + &.RequestModified--cookie:before, + &.RequestModified--fingerprint:before { + background-image: buildIconAntiTracking($ghosty-blue); + } + } } .BlockingSelectButton { diff --git a/src/classes/PromoModals.js b/src/classes/PromoModals.js index 0c096ee27..cc48730f9 100644 --- a/src/classes/PromoModals.js +++ b/src/classes/PromoModals.js @@ -100,7 +100,7 @@ class PromoModals { * @return {Boolean} */ static _hasEngagedFrequently() { - const { engaged_daily_count } = conf.metrics || []; + const { engaged_daily_count = [] } = conf.metrics; const very_engaged_days = engaged_daily_count.reduce((acc, count) => (count >= DAILY_INSIGHTS_TARGET ? acc + 1 : acc), 0); From bd8a0ef63c762676e50ba9f4d0b4c076145553d8 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Sun, 19 Jul 2020 19:23:47 -0400 Subject: [PATCH 57/89] Fix linting and testing errors --- .../components/content/BlockingTracker.jsx | 7 ++- .../content/__tests__/BlockingCategories.jsx | 3 + .../content/__tests__/BlockingCategory.jsx | 9 +++ .../content/__tests__/BlockingTracker.jsx | 8 +++ .../__snapshots__/BlockingTracker.jsx.snap | 63 +++++++++---------- .../Settings/__tests__/TrustAndRestrict.jsx | 6 +- 6 files changed, 61 insertions(+), 35 deletions(-) diff --git a/app/panel-android/components/content/BlockingTracker.jsx b/app/panel-android/components/content/BlockingTracker.jsx index 7f1d2f307..31c4194f2 100644 --- a/app/panel-android/components/content/BlockingTracker.jsx +++ b/app/panel-android/components/content/BlockingTracker.jsx @@ -221,7 +221,12 @@ class BlockingTracker extends React.Component { } renderBlockingOverflow() { - const { type, open, tracker, settings } = this.props; + const { + type, + open, + tracker, + settings, + } = this.props; const { ss_allowed = false, ss_blocked = false, blocked } = tracker; const { toggle_individual_trackers = false } = settings; diff --git a/app/panel-android/components/content/__tests__/BlockingCategories.jsx b/app/panel-android/components/content/__tests__/BlockingCategories.jsx index 31703760b..8496a46af 100644 --- a/app/panel-android/components/content/__tests__/BlockingCategories.jsx +++ b/app/panel-android/components/content/__tests__/BlockingCategories.jsx @@ -91,6 +91,7 @@ describe('app/panel-android/components/content/BlockingCategories.jsx', () => { {}} /> @@ -159,6 +160,7 @@ describe('app/panel-android/components/content/BlockingCategories.jsx', () => { {}} /> @@ -229,6 +231,7 @@ describe('app/panel-android/components/content/BlockingCategories.jsx', () => { {}} /> diff --git a/app/panel-android/components/content/__tests__/BlockingCategory.jsx b/app/panel-android/components/content/__tests__/BlockingCategory.jsx index beaeb142b..05571129f 100644 --- a/app/panel-android/components/content/__tests__/BlockingCategory.jsx +++ b/app/panel-android/components/content/__tests__/BlockingCategory.jsx @@ -38,6 +38,7 @@ describe('app/panel-android/components/content/BlockingCategory.jsx', () => { key="cat-1" index={1} category={category} + settings={{}} toggleCategoryOpen={() => {}} open={false} type="site" @@ -68,6 +69,7 @@ describe('app/panel-android/components/content/BlockingCategory.jsx', () => { key="cat-1" index={1} category={category} + settings={{}} toggleCategoryOpen={() => {}} open={false} type="site" @@ -98,6 +100,7 @@ describe('app/panel-android/components/content/BlockingCategory.jsx', () => { key="cat-1" index={1} category={category} + settings={{}} toggleCategoryOpen={() => {}} open={false} type="site" @@ -136,6 +139,7 @@ describe('app/panel-android/components/content/BlockingCategory.jsx', () => { key="cat-1" index={1} category={category} + settings={{}} toggleCategoryOpen={() => {}} open={false} type="site" @@ -174,6 +178,7 @@ describe('app/panel-android/components/content/BlockingCategory.jsx', () => { key="cat-1" index={1} category={category} + settings={{}} toggleCategoryOpen={() => {}} open={false} type="site" @@ -210,6 +215,7 @@ describe('app/panel-android/components/content/BlockingCategory.jsx', () => { key="cat-1" index={1} category={category} + settings={{}} toggleCategoryOpen={() => {}} open={false} type="global" @@ -246,6 +252,7 @@ describe('app/panel-android/components/content/BlockingCategory.jsx', () => { key="cat-1" index={1} category={category} + settings={{}} toggleCategoryOpen={() => {}} open={false} type="global" @@ -287,6 +294,7 @@ describe('app/panel-android/components/content/BlockingCategory.jsx', () => { key="cat-1" index={1} category={category} + settings={{}} toggleCategoryOpen={() => {}} open={false} type="global" @@ -333,6 +341,7 @@ describe('app/panel-android/components/content/BlockingCategory.jsx', () => { key="cat-1" index={1} category={category} + settings={{}} toggleCategoryOpen={toggleCategoryOpen} open={false} type="site" diff --git a/app/panel-android/components/content/__tests__/BlockingTracker.jsx b/app/panel-android/components/content/__tests__/BlockingTracker.jsx index f7aa6a518..97bb43659 100644 --- a/app/panel-android/components/content/__tests__/BlockingTracker.jsx +++ b/app/panel-android/components/content/__tests__/BlockingTracker.jsx @@ -40,6 +40,7 @@ describe('app/panel-android/components/content/BlockingTracker.jsx', () => { type="site" toggleTrackerSelectOpen={() => {}} open={false} + settings={{}} siteProps={siteProps} callGlobalAction={() => {}} /> @@ -69,6 +70,7 @@ describe('app/panel-android/components/content/BlockingTracker.jsx', () => { type="site" toggleTrackerSelectOpen={() => {}} open={false} + settings={{ toggle_individual_trackers: false }} siteProps={siteProps} callGlobalAction={() => {}} /> @@ -98,6 +100,7 @@ describe('app/panel-android/components/content/BlockingTracker.jsx', () => { type="site" toggleTrackerSelectOpen={() => {}} open={false} + settings={{ toggle_individual_trackers: true }} siteProps={siteProps} callGlobalAction={() => {}} /> @@ -127,6 +130,7 @@ describe('app/panel-android/components/content/BlockingTracker.jsx', () => { type="site" toggleTrackerSelectOpen={() => {}} open={false} + settings={{ toggle_individual_trackers: true }} siteProps={siteProps} callGlobalAction={() => {}} /> @@ -156,6 +160,7 @@ describe('app/panel-android/components/content/BlockingTracker.jsx', () => { type="site" toggleTrackerSelectOpen={() => {}} open={false} + settings={{ toggle_individual_trackers: true }} siteProps={siteProps} callGlobalAction={() => {}} /> @@ -185,6 +190,7 @@ describe('app/panel-android/components/content/BlockingTracker.jsx', () => { type="site" toggleTrackerSelectOpen={() => {}} open={false} + settings={{ toggle_individual_trackers: true }} siteProps={siteProps} callGlobalAction={() => {}} /> @@ -214,6 +220,7 @@ describe('app/panel-android/components/content/BlockingTracker.jsx', () => { type="site" toggleTrackerSelectOpen={() => {}} open={false} + settings={{ toggle_individual_trackers: true }} siteProps={siteProps} callGlobalAction={() => {}} /> @@ -248,6 +255,7 @@ describe('app/panel-android/components/content/BlockingTracker.jsx', () => { type="global" toggleTrackerSelectOpen={toggleTrackerSelectOpen} open={false} + settings={{ toggle_individual_trackers: true }} siteProps={siteProps} callGlobalAction={callGlobalAction} /> diff --git a/app/panel-android/components/content/__tests__/__snapshots__/BlockingTracker.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/BlockingTracker.jsx.snap index af14b3f9d..9b60d4d84 100644 --- a/app/panel-android/components/content/__tests__/__snapshots__/BlockingTracker.jsx.snap +++ b/app/panel-android/components/content/__tests__/__snapshots__/BlockingTracker.jsx.snap @@ -16,7 +16,10 @@ exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests
    - Tracker 1 +
    + Tracker 1 +
    +
    - Tracker 1 +
    + Tracker 1 +
    +
    - Tracker 1 +
    + Tracker 1 +
    +
    - Tracker 1 +
    + Tracker 1 +
    +
    - Tracker 1 +
    + Tracker 1 +
    +
    android_unblock
    -
    - android_restrict -
    -
    - android_trust -
    `; @@ -246,7 +249,10 @@ exports[`app/panel-android/components/content/BlockingTracker.jsx Snapshot tests
    - Tracker 1 +
    + Tracker 1 +
    +
    - Tracker 1 +
    + Tracker 1 +
    +
    android_block
    -
    - android_restrict -
    -
    - android_trust -
    `; diff --git a/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx b/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx index 39ef2b5da..823ae3848 100644 --- a/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx +++ b/app/panel/components/Settings/__tests__/TrustAndRestrict.jsx @@ -20,7 +20,11 @@ describe('app/panel/components/Settings/TrustAndRestrict', () => { describe('Snapshot test with react-test-renderer', () => { test('Testing TrustAndRestrict is rendering', () => { const wrapper = renderer.create( - + {} }} + site_whitelist={[]} + site_blacklist={[]} + /> ).toJSON(); expect(wrapper).toMatchSnapshot(); }); From 5df17fda13953e0574377958a8d01758caa5ef2c Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Sun, 19 Jul 2020 23:10:23 -0400 Subject: [PATCH 58/89] Add unknown category in tracker list --- .../panel-android/categories/unknown.svg | 7 +++++ app/panel-android/components/PanelAndroid.jsx | 30 ++++++++++++++++++- .../components/content/BlockingCategory.jsx | 7 ++++- app/panel-android/index.jsx | 15 ++++++++++ app/panel-android/utils/tracker-info.js | 2 +- app/scss/android/_blocking_tab.scss | 18 ++++++++++- 6 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 app/images/panel-android/categories/unknown.svg diff --git a/app/images/panel-android/categories/unknown.svg b/app/images/panel-android/categories/unknown.svg new file mode 100644 index 000000000..1408249f7 --- /dev/null +++ b/app/images/panel-android/categories/unknown.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/panel-android/components/PanelAndroid.jsx b/app/panel-android/components/PanelAndroid.jsx index c68473bf2..bbe2053a3 100644 --- a/app/panel-android/components/PanelAndroid.jsx +++ b/app/panel-android/components/PanelAndroid.jsx @@ -49,6 +49,7 @@ class PanelAndroid extends React.Component { blocking: { siteNotScanned: false, pageUrl: '', + categories: [], }, cliqzModuleData: { adBlock: { trackerCount: 0, unknownTrackers: [] }, @@ -157,6 +158,18 @@ class PanelAndroid extends React.Component { this.setState({ view: newView }); } + massageCliqzTrackers = tracker => ({ + id: tracker.name, + catId: tracker.type, + cliqzAdCount: tracker.ads, + cliqzCookieCount: tracker.cookies, + cliqzFingerprintCount: tracker.fingerprints, + name: tracker.name, + sources: tracker.domains, + whitelisted: tracker.whitelisted, + blocked: false, // To appease BlockingTracker PropTypes + }) + _renderAccount() { const { summary, settings } = this.state; return ( @@ -190,6 +203,21 @@ class PanelAndroid extends React.Component { cliqzModuleData, } = this.state; const { categories, toggle_individual_trackers } = blocking; + const { adBlock, antiTracking } = cliqzModuleData; + + const unknownTrackers = Array.from(new Set([ + ...antiTracking.unknownTrackers.map(this.massageCliqzTrackers), + ...adBlock.unknownTrackers.map(this.massageCliqzTrackers), + ])); + const unknownCategory = { + id: 'unknown', + name: t('unknown'), + description: t('unknown_description'), + img_name: 'unknown', + num_total: unknownTrackers.length, + num_blocked: 0, // We don't want to see the Trackers Blocked text + trackers: unknownTrackers, + }; return ( @@ -208,7 +236,7 @@ class PanelAndroid extends React.Component { +
    { toggleCategoryOpen(index); }}>
    diff --git a/app/panel-android/index.jsx b/app/panel-android/index.jsx index e1a755e4e..c6a27a1ca 100644 --- a/app/panel-android/index.jsx +++ b/app/panel-android/index.jsx @@ -9,6 +9,21 @@ * 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 + * + * ToDo: + * - [-] Move Settings Icon to OverviewTab Header, change icon to a Hamburger, + * have it open the Menu with settings as a menu item, and make all + * menu sub-views mobile friendly. + * - [|] Add Unknown Category to TrackerList for AdBlock and AntiTracking Unknown Trackers + * - [-] Check that all strings are localized + * - [ ] Add tests for PanelAndroid Settings and Panel Settings sub-components + * - [ ] Add tests for PanelAndroid Menu and Panel Menu Sub-Components + * - [ ] See if Tino is OK with the react-window List library for rendering lists + * - [ ] See if Vinny likes what I did with SmartBlock & CliqzFeatures + * - [ ] Move Account Icon to OverviewTab Header and add Account Views + * - [ ] Replace hidden tooltips on the OverviewTab with a Help Screen + * - [ ] Make a landscape mode: OverviewTab on left, Site/Global Blocking on right. + * * @namespace PanelAndroidClasses */ diff --git a/app/panel-android/utils/tracker-info.js b/app/panel-android/utils/tracker-info.js index 58ae9b2a1..bacb66d90 100644 --- a/app/panel-android/utils/tracker-info.js +++ b/app/panel-android/utils/tracker-info.js @@ -18,7 +18,7 @@ import { apps } from '../../../cliqz/core/tracker_db_v2.json'; // Link to whotracks.me website export default function getUrlFromTrackerId(id) { - const trackerName = apps[id].name; + const trackerName = apps[id] && apps[id].name; const trackerWtm = (Object.values(apps).find(app => app.wtm && app.name === trackerName) || {}).wtm; const slug = trackerWtm || '../tracker-not-found'; return `https://whotracks.me/trackers/${slug}.html`; diff --git a/app/scss/android/_blocking_tab.scss b/app/scss/android/_blocking_tab.scss index 39116834f..06cd9dec7 100644 --- a/app/scss/android/_blocking_tab.scss +++ b/app/scss/android/_blocking_tab.scss @@ -44,8 +44,24 @@ text-transform: uppercase; } + &.BlockingCategory__unknown:before { + content: ""; + position: relative; + top: -1px; + display: block; + width: 100%; + height: 5px; + border-top: 1px solid $ghosty-blue; + border-bottom: 1px solid $ghosty-blue; + } + + &.BlockingCategory__unknown .BlockingCategory__numBlocked, + &.BlockingCategory__unknown .BlockingSelectButton { + display: none; + } + .BlockingCategory__details { - min-height: 88px; // ToDo: Check This + min-height: 80px; padding: 15px 12px; cursor: pointer; } From 839cff12d297af1f0bbc147b5a27811ae6890edf Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Sun, 19 Jul 2020 23:35:10 -0400 Subject: [PATCH 59/89] sort unknown trackers and hide category when there are no trackers --- app/panel-android/components/PanelAndroid.jsx | 14 ++++++++++++-- app/panel-android/index.jsx | 4 +--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/panel-android/components/PanelAndroid.jsx b/app/panel-android/components/PanelAndroid.jsx index bbe2053a3..cc997c966 100644 --- a/app/panel-android/components/PanelAndroid.jsx +++ b/app/panel-android/components/PanelAndroid.jsx @@ -208,7 +208,17 @@ class PanelAndroid extends React.Component { const unknownTrackers = Array.from(new Set([ ...antiTracking.unknownTrackers.map(this.massageCliqzTrackers), ...adBlock.unknownTrackers.map(this.massageCliqzTrackers), - ])); + ])).sort((a, b) => { + const nameA = a.name.toLowerCase(); + const nameB = b.name.toLowerCase(); + if (nameA < nameB) { + return -1; + } + if (nameA > nameB) { + return 1; + } + return 0; + }); const unknownCategory = { id: 'unknown', name: t('unknown'), @@ -236,7 +246,7 @@ class PanelAndroid extends React.Component { Date: Tue, 21 Jul 2020 10:35:56 -0400 Subject: [PATCH 60/89] Add Help and About to PanelAndroid Settings Menu. UI Tweak for Settings and make clicking RadioButton Labels switch Radios. --- .../components/content/Settings.jsx | 17 +++++++++++++++++ app/panel-android/index.jsx | 5 +---- .../BuildingBlocks/RadioButtonGroup.jsx | 8 ++++++-- app/scss/android/_settings.scss | 2 ++ app/scss/panel_android.scss | 1 + 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app/panel-android/components/content/Settings.jsx b/app/panel-android/components/content/Settings.jsx index b8605581e..fd4da29f9 100644 --- a/app/panel-android/components/content/Settings.jsx +++ b/app/panel-android/components/content/Settings.jsx @@ -20,6 +20,9 @@ import GeneralSettings from '../../../panel/components/Settings/GeneralSettings' import AdBlocker from '../../../panel/components/Settings/AdBlocker'; import Notifications from '../../../panel/components/Settings/Notifications'; import OptIn from '../../../panel/components/Settings/OptIn'; +import Help from '../../../panel/components/Help'; +import About from '../../../panel/components/About'; + import globals from '../../../../src/classes/Globals'; const { IS_CLIQZ } = globals; @@ -103,6 +106,12 @@ class Account extends React.Component { case 'settings-opt-in': headerText = t('settings_opt_in'); break; + case 'settings-help': + headerText = t('panel_menu_help'); + break; + case 'settings-about': + headerText = t('panel_menu_about'); + break; default: headerText = ''; } @@ -140,6 +149,12 @@ class Account extends React.Component {
    { this.setState({ view: 'settings-opt-in' }); }}> { t('settings_opt_in') }
    +
    { this.setState({ view: 'settings-help' }); }}> + { t('panel_menu_help') } +
    +
    { this.setState({ view: 'settings-about' }); }}> + { t('panel_menu_about') } +
    ); @@ -224,6 +239,8 @@ class Account extends React.Component { {view === 'settings-adblocker' && this._renderSettingsAdBlocker()} {view === 'settings-notifications' && this._renderSettingsNotification()} {view === 'settings-opt-in' && this._renderSettingsOptIn()} + {view === 'settings-help' && ()} + {view === 'settings-about' && ()}
    ); } diff --git a/app/panel-android/index.jsx b/app/panel-android/index.jsx index 748dc7995..4ea25db32 100644 --- a/app/panel-android/index.jsx +++ b/app/panel-android/index.jsx @@ -11,14 +11,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 * * ToDo: - * - [next] Move Settings Icon to OverviewTab Header, change icon to a Hamburger, - * have it open the Menu with settings as a menu item, and make all - * menu sub-views mobile friendly. * - [ ] Add tests for PanelAndroid Settings and Panel Settings sub-components * - [ ] Add tests for PanelAndroid Menu and Panel Menu Sub-Components * - [ ] See if Tino is OK with the react-window List library for rendering lists * - [ ] See if Vinny likes what I did with SmartBlock & CliqzFeatures - * - [ ] Move Account Icon to OverviewTab Header and add Account Views + * - [ ] Add Account Views * - [ ] Replace hidden tooltips on the OverviewTab with a Help Screen * - [ ] Make a landscape mode: OverviewTab on left, Site/Global Blocking on right. diff --git a/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx b/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx index 376544392..2722ceddc 100644 --- a/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx +++ b/app/panel/components/BuildingBlocks/RadioButtonGroup.jsx @@ -20,8 +20,12 @@ import RadioButton from './RadioButton'; * @memberof PanelBuildingBlocks */ const RadioButtonGroup = ({ indexClicked, handleItemClick, labels }) => { - const labelsEl = labels.map(label => ( -
    + const labelsEl = labels.map((label, index) => ( +
    handleItemClick(index)} + > {t(label)}
    )); diff --git a/app/scss/android/_settings.scss b/app/scss/android/_settings.scss index c93ab9df4..303669a52 100644 --- a/app/scss/android/_settings.scss +++ b/app/scss/android/_settings.scss @@ -128,6 +128,8 @@ } .s-option-group { + margin-right: 0; + .s-checkbox-label { margin-left: 24px; max-width: 100%; diff --git a/app/scss/panel_android.scss b/app/scss/panel_android.scss index 9a24a0a48..fb306c30b 100644 --- a/app/scss/panel_android.scss +++ b/app/scss/panel_android.scss @@ -29,6 +29,7 @@ // Panel Shared Component Styles @import './partials/_settings'; +@import './partials/_help'; @import './partials/_radio_button'; @import './partials/_not_scanned'; @import './partials/_donut_graph'; From 00b655767e09b9aa3a471b63f7bb6c078f5a3185 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Tue, 21 Jul 2020 12:59:39 -0400 Subject: [PATCH 61/89] Hide account icon, try to unhide tab label --- app/panel-android/components/content/OverviewTab.jsx | 5 +++-- app/panel-android/index.jsx | 3 +-- app/scss/android/_tabs.scss | 7 +++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/panel-android/components/content/OverviewTab.jsx b/app/panel-android/components/content/OverviewTab.jsx index bc1a65d4b..fcbec213e 100644 --- a/app/panel-android/components/content/OverviewTab.jsx +++ b/app/panel-android/components/content/OverviewTab.jsx @@ -191,10 +191,11 @@ class OverviewTab extends React.Component { ); + // Remove `flex-dir-row-reverse` & `hide` classes when you add back the accountIcon return (
    -
    -
    +
    +
    {accountIcon}
    diff --git a/app/panel-android/index.jsx b/app/panel-android/index.jsx index 4ea25db32..2bf04d6ac 100644 --- a/app/panel-android/index.jsx +++ b/app/panel-android/index.jsx @@ -11,14 +11,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 * * ToDo: + * - [next] Add a Close & Reload notification above blue navbar. * - [ ] Add tests for PanelAndroid Settings and Panel Settings sub-components * - [ ] Add tests for PanelAndroid Menu and Panel Menu Sub-Components - * - [ ] See if Tino is OK with the react-window List library for rendering lists * - [ ] See if Vinny likes what I did with SmartBlock & CliqzFeatures * - [ ] Add Account Views * - [ ] Replace hidden tooltips on the OverviewTab with a Help Screen * - [ ] Make a landscape mode: OverviewTab on left, Site/Global Blocking on right. - * * @namespace PanelAndroidClasses */ diff --git a/app/scss/android/_tabs.scss b/app/scss/android/_tabs.scss index a78e4b49f..533f1c3fd 100644 --- a/app/scss/android/_tabs.scss +++ b/app/scss/android/_tabs.scss @@ -33,16 +33,15 @@ } .Tab__navigation_link { padding: 0 10px 2px; - color: $white; + color: rgba($white, 0.8); font-size: 12px; font-weight: 500; text-transform: uppercase; - opacity: 0.8; - @include prefix('transition', 'opacity 300ms ease-in'); + @include prefix('transition', 'color 300ms ease-in'); } .Tab__navigation_link.Tab--active { padding-bottom: 0; - opacity: 1; + color: $white; } .Tabs__active_content {} From 52d2ddb1afce2d075cf4f337e41149c2d004c612 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Tue, 21 Jul 2020 13:09:03 -0400 Subject: [PATCH 62/89] Update tests --- .../content/__tests__/__snapshots__/OverviewTab.jsx.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap index d345de9e4..2ff8423a9 100644 --- a/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap +++ b/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap @@ -8,10 +8,10 @@ exports[`app/panel-android/components/content/OverviewTab.jsx Snapshot tests wit className="OverviewTab__NavigationLinks full-width" >
    Date: Fri, 24 Jul 2020 11:48:17 -0400 Subject: [PATCH 63/89] fix android icons and bump FF min version --- manifest.json | 6 +++--- src/classes/BrowserButton.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/manifest.json b/manifest.json index 20a595fd8..581d00c7d 100644 --- a/manifest.json +++ b/manifest.json @@ -14,8 +14,8 @@ }, "browser_action": { "default_icon": { - "19": "app/images/icon19_off.png", - "38": "app/images/icon38_off.png" + "19": "app/images/icon19.png", + "38": "app/images/icon38.png" }, "default_title": "Ghostery" }, @@ -101,7 +101,7 @@ "browser_specific_settings": { "gecko": { "id": "firefox@ghostery.com", - "strict_min_version": "52.0" + "strict_min_version": "68.0" } }, "minimum_edge_version": "79.0.309", diff --git a/src/classes/BrowserButton.js b/src/classes/BrowserButton.js index e702fa214..ee42462fd 100644 --- a/src/classes/BrowserButton.js +++ b/src/classes/BrowserButton.js @@ -38,6 +38,7 @@ class BrowserButton { * @param {number} tabId tab id */ update(tabId) { + if (globals.BROWSER_INFO.os === 'android') { return; } // Update this specific tab if (tabId) { // In ES6 classes, we need to bind context to callback function @@ -73,7 +74,6 @@ class BrowserButton { * @param {boolean} alert is it a special case which requires button to change its background color? */ _setIcon(active, tabId, trackerCount, alert) { - if (globals.BROWSER_INFO.os === 'android') { return; } if (tabId <= 0) { return; } const iconAlt = (!active) ? '_off' : ''; From 860c35f7e6b9f475e4c2dbf00bc4ba5892ab247e Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Mon, 27 Jul 2020 11:52:56 -0400 Subject: [PATCH 64/89] remove console debugging code --- app/content-scripts/debug_information.js | 19 --- app/content-scripts/notifications.js | 2 +- app/panel/components/Help.jsx | 72 ++++------- app/templates/debug_information.html | 22 ---- src/background.js | 22 +--- src/classes/Account.js | 14 +-- src/classes/GhosteryDebug.js | 146 ----------------------- webpack.config.js | 1 - 8 files changed, 25 insertions(+), 273 deletions(-) delete mode 100644 app/content-scripts/debug_information.js delete mode 100644 app/templates/debug_information.html delete mode 100644 src/classes/GhosteryDebug.js diff --git a/app/content-scripts/debug_information.js b/app/content-scripts/debug_information.js deleted file mode 100644 index 47031fb3a..000000000 --- a/app/content-scripts/debug_information.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Debug Information - * - * This file asks the background for Ghostery Debug information - * and writes the returned JSON to the body innerText. - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2020 Ghostery, Inc. All rights reserved. - * - * 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 - */ - -chrome.runtime.sendMessage({ name: 'debug_information' }, (response) => { - document.body.innerText = response; -}); diff --git a/app/content-scripts/notifications.js b/app/content-scripts/notifications.js index dd0d50195..c9ecc7c56 100644 --- a/app/content-scripts/notifications.js +++ b/app/content-scripts/notifications.js @@ -1,5 +1,5 @@ /** - * Ghostery NotificationsContentScript + * Ghostery Notifications Content Script * * This file provides notification alerts for the CMP, update dialogs * and import/export functionality diff --git a/app/panel/components/Help.jsx b/app/panel/components/Help.jsx index 0062034ce..ef5f442b9 100644 --- a/app/panel/components/Help.jsx +++ b/app/panel/components/Help.jsx @@ -4,7 +4,7 @@ * Ghostery Browser Extension * https://www.ghostery.com/ * - * Copyright 2020 Ghostery, Inc. All rights reserved. + * Copyright 2019 Ghostery, Inc. All rights reserved. * * 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 @@ -12,61 +12,33 @@ */ import React from 'react'; -import { handleClickOnNewTabLink, openSupportPage } from '../utils/msg'; +import { openSupportPage, openHubPage } from '../utils/msg'; import PanelToTabLink from './BuildingBlocks/PanelToTabLink'; /** * Render Help view that user can open from the header drop-down menu */ -class Help extends React.Component { - constructor(props) { - super(props); - - this.updateCount = this.updateCount.bind(this); - - this.state = { - clickCount: 0, - }; - } - - updateCount() { - this.setState((prevState) => { - let { clickCount } = prevState; - clickCount++; - return { clickCount }; - }); - } - - render() { - const { clickCount } = this.state; - const hubUrl = chrome.runtime.getURL('./app/templates/hub.html'); - - return ( -
    -
    -
    -

    { t('panel_help_panel_header') }

    -
    - {t('panel_help_setup')} - {clickCount >= 5 && ( - Open Debug Information - )} -
    -
    -

    { t('panel_help_questions_header') }

    - {t('panel_help_faq')} - {t('panel_help_feedback')} - { t('support') } -
    -
    -

    { t('panel_help_contact_header') }

    - info@ghostery.com -
    -
    +const Help = () => ( +
    +
    +
    +

    {t('panel_help_panel_header')}

    + +
    +

    {t('panel_help_questions_header')}

    + {t('panel_help_faq')} + {t('panel_help_feedback')} + {t('support')} +
    +
    +

    {t('panel_help_contact_header')}

    + info@ghostery.com
    - ); - } -} +
    +
    +); export default Help; diff --git a/app/templates/debug_information.html b/app/templates/debug_information.html deleted file mode 100644 index 1fce4e0d4..000000000 --- a/app/templates/debug_information.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - -

    Loading Debug Information...

    - - diff --git a/src/background.js b/src/background.js index 71db2789b..1776a8cc7 100644 --- a/src/background.js +++ b/src/background.js @@ -17,8 +17,7 @@ import { debounce, every, size } from 'underscore'; import moment from 'moment/min/moment-with-locales.min'; import cliqz, { HUMANWEB_MODULE, HPN_MODULE } from './classes/Cliqz'; -import ghosteryDebug from './classes/GhosteryDebug'; -// object class +// object classes import Events from './classes/EventHandlers'; import Policy from './classes/Policy'; // static classes @@ -54,9 +53,6 @@ import { sendCliqzModuleCounts } from './utils/cliqzModulesData'; // module from Developer Tools Console. window.CLIQZ = cliqz; -// For debug purposes, provide access to Ghostery's internal data. -window.GHOSTERY = ghosteryDebug; - // class instantiation const events = new Events(); // function shortcuts @@ -1058,18 +1054,6 @@ function onMessageHandler(request, sender, callback) { promoModals.turnOffPromos(); return false; } - if (name === 'debug_information') { - Promise.all([ - ghosteryDebug.getTabInfo(), - ghosteryDebug.getUserData(), - ]).then(() => { - const debugInfo = JSON.stringify(window.GHOSTERY); - const msg = { type: 'Ghostery-Debug', content: debugInfo }; - sendMessage(sender.tab.id, 'exportFile', msg); - callback(debugInfo); - }); - return true; - } return false; } @@ -1751,12 +1735,9 @@ function init() { initializeEventListeners(); initializeVersioning(); return metrics.init(globals.JUST_INSTALLED).then(() => initializeGhosteryModules().then(() => { - ghosteryDebug.init(); - ghosteryDebug.addAccountEvent('migrate', 'migrate start'); account.migrate() .then(() => { if (conf.account !== null) { - ghosteryDebug.addAccountEvent('app started', 'signed in', conf.account); return account.getUser() .then(account.getUserSettings) .then(() => { @@ -1766,7 +1747,6 @@ function init() { return false; }); } - ghosteryDebug.addAccountEvent('app started', 'not signed in'); if (globals.JUST_INSTALLED) { setGhosteryDefaultBlocking(); } diff --git a/src/classes/Account.js b/src/classes/Account.js index abb656079..9f2e55a34 100644 --- a/src/classes/Account.js +++ b/src/classes/Account.js @@ -22,7 +22,6 @@ import conf from './Conf'; import dispatcher from './Dispatcher'; import { log } from '../utils/common'; import Api from '../utils/api'; -import ghosteryDebug from './GhosteryDebug'; const api = new Api(); const { @@ -84,7 +83,6 @@ class Account { if (res.status >= 400) { return res.json(); } - ghosteryDebug.addAccountEvent('login', 'cookie set by fetch POST'); this._getUserIDFromCookie().then((userID) => { this._setAccountInfo(userID); this.getUserSubscriptionData(); @@ -105,7 +103,6 @@ class Account { credentials: 'include', }).then((res) => { if (res.status >= 400) { - ghosteryDebug.addAccountEvent('register', 'cookie set by fetch POST'); return res.json(); } this._getUserIDFromCookie().then((userID) => { @@ -127,10 +124,7 @@ class Account { credentials: 'include', headers: { 'X-CSRF-Token': cookie.value }, }).then((res) => { - if (res.status < 400) { - ghosteryDebug.addAccountEvent('logout', 'cookie set by fetch POST'); - return resolve(); - } + if (res.status < 400) { return resolve(); } return res.json().then(json => reject(json)); }).catch(err => reject(err)); }); @@ -263,14 +257,12 @@ class Account { const legacyLoginInfoKey = 'login_info'; chrome.storage.local.get(legacyLoginInfoKey, (items) => { if (chrome.runtime.lastError) { - ghosteryDebug.addAccountEvent('migrate', 'runtime error'); resolve(new Error(chrome.runtime.lastError)); return; } const { login_info } = items; if (!items || !login_info) { - ghosteryDebug.addAccountEvent('migrate', 'no items found'); resolve(); return; } @@ -278,7 +270,6 @@ class Account { // ensure we have all the necessary info const { decoded_user_token, user_token } = login_info; if (!decoded_user_token || !user_token) { - ghosteryDebug.addAccountEvent('migrate', 'found items, not enough info I'); chrome.storage.local.remove(legacyLoginInfoKey, () => resolve()); return; } @@ -286,7 +277,6 @@ class Account { UserId, csrf_token, RefreshToken, exp } = decoded_user_token; if (!UserId || !csrf_token || !RefreshToken || !exp) { - ghosteryDebug.addAccountEvent('migrate', 'found items, not enough info II'); chrome.storage.local.remove(legacyLoginInfoKey, () => resolve()); return; } @@ -321,10 +311,8 @@ class Account { // login this._setAccountInfo(UserId); this.getUserSubscriptionData(); - ghosteryDebug.addAccountEvent('migrate', 'remove legacy items'); chrome.storage.local.remove(legacyLoginInfoKey, () => resolve()); }).catch((err) => { - ghosteryDebug.addAccountEvent('migrate', 'cookies set error'); resolve(err); }); }); diff --git a/src/classes/GhosteryDebug.js b/src/classes/GhosteryDebug.js deleted file mode 100644 index 5c4304e74..000000000 --- a/src/classes/GhosteryDebug.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Ghostery Debug Class - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2020 Ghostery, Inc. All rights reserved. - * - * 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 - */ - -import account from './Account'; -import confData from './ConfData'; -import globals from './Globals'; -import tabInfo from './TabInfo'; -import foundBugs from './FoundBugs'; - -/** - * @class for debugging Ghostery via the background.js console. - * @memberof BackgroundClasses - */ -class GhosteryDebug { - constructor() { - this.accountEvents = []; - this.browserInfo = globals.BROWSER_INFO; - this.extensionInfo = { - name: globals.EXTENSION_NAME, - version: globals.EXTENSION_VERSION, - }; - - const _cookieChangeEvent = (changeInfo) => { - const { removed, cookie, cause } = changeInfo; - const { domain, name } = cookie; - if (domain.includes(globals.GHOSTERY_ROOT_DOMAIN)) { - const type = `Cookie ${name} ${removed ? 'Removed' : 'Added'}`; - this.addAccountEvent(type, cause, cookie); - } - }; - - // Chrome Documentation: https://developer.chrome.com/extensions/cookies#event-onChanged - // Mozilla Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies/onChanged - chrome.cookies.onChanged.addListener(_cookieChangeEvent); - } - - init() { - this.extensionInfo.installDate = confData.install_date; - this.extensionInfo.versionHistory = confData.version_history; - } - - addAccountEvent(type, event, details) { - const timestamp = new Date(); - const pushObj = { type, event, timestamp }; - if (details) { - pushObj.details = details; - } - - this.accountEvents.push(pushObj); - } - - /** - * this.tabInfo[tabId] properties update without re-calling getTabInfo. - * this.foundApps[tabId].foundApps properties update without re-calling getTabInfo. - * this.foundApps[tabId].foundBugs properties update without re-calling getTabInfo. - * No need to call `window.GHOSTERY.getTabInfo()` to see changes to object properties. - * You will need to call `window.GHOSTERY` again to see changes as console object - * properties are fixed to when the object was read by the console. - * Only object properties will update, no new tabIds will be added. - * Reloading the tab will end these updates. - */ - getTabInfo() { - function _getActiveTabIds() { - return new Promise((resolve) => { - chrome.tabs.query({ - active: true, - }, (tabs) => { - if (chrome.runtime.lastError) { - return resolve(chrome.runtime.lastError.message); - } - const tabIds = tabs.map(tab => tab.id); - return resolve(tabIds); - }); - }); - } - - return new Promise((resolve) => { - _getActiveTabIds().then((tabIds) => { - this.activeTabIds = tabIds; - this.tabInfo = { ...tabInfo._tabInfo }; - this.foundBugs = { - foundApps: { ...foundBugs._foundApps }, - foundBugs: { ...foundBugs._foundBugs }, - }; - resolve(tabIds); - }); - }); - } - - getUserData() { - function _getUserCookies() { - return new Promise((resolve) => { - chrome.cookies.getAll({ - url: globals.COOKIE_URL, - }, resolve); - }); - } - - function _getUser() { - return new Promise((resolve) => { - account.getUser().then(resolve).catch(resolve); - }); - } - - function _getUserSettings() { - return new Promise((resolve) => { - account.getUserSettings().then(resolve).catch(resolve); - }); - } - - function _getUserSubscriptionData() { - return new Promise((resolve) => { - account.getUserSubscriptionData().then(resolve).catch(resolve); - }); - } - - return new Promise((resolve) => { - Promise.all([ - _getUserCookies(), - _getUser(), - _getUserSettings(), - _getUserSubscriptionData(), - ]).then(([userCookies, userData, syncedUserSettings, userSubscriptionData]) => { - this.user = { - userCookies, - userData, - syncedUserSettings, - userSubscriptionData, - }; - resolve(this.user); - }); - }); - } -} - -export default new GhosteryDebug(); diff --git a/webpack.config.js b/webpack.config.js index 526cdf878..95dac4452 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -59,7 +59,6 @@ module.exports = { panel_react: [`${PANEL_DIR}/index.jsx`], purplebox: [`${CONTENT_SCRIPTS_DIR}/purplebox.js`], shared_comp_react: [`${SHARED_COMP_DIR}/index.js`], - debug_information: [`${CONTENT_SCRIPTS_DIR}/debug_information.js`], // Sass foundation: [`${SASS_DIR}/vendor/foundation.scss`], foundation_hub: [`${SASS_DIR}/vendor/foundation_hub.scss`], From 9a2d7a3e90c953f2ed9886c028e508ad00f07786 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Tue, 28 Jul 2020 17:01:55 -0400 Subject: [PATCH 65/89] clean up hub components on Android --- _locales/en/messages.json | 6 +- app/hub/Views/SetupView/SetupView.jsx | 2 +- .../SetupAntiSuiteViewContainer.jsx | 5 +- .../SideNavigationViewContainer.jsx | 7 +- app/hub/Views/TutorialView/TutorialView.jsx | 6 +- .../TutorialView/TutorialViewContainer.jsx | 6 +- .../TutorialAntiSuiteView.jsx | 29 +++--- .../TutorialAntiSuiteViewContainer.jsx | 3 +- .../TutorialBlockingView.jsx | 29 +++--- .../TutorialBlockingViewContainer.jsx | 3 +- .../TutorialLayoutView/TutorialLayoutView.jsx | 11 ++- .../TutorialLayoutViewContainer.jsx | 3 +- .../TutorialTrackerListView.jsx | 9 +- .../TutorialTrackerListViewContainer.jsx | 4 +- .../TutorialTrustView/TutorialTrustView.jsx | 29 +++--- .../TutorialTrustViewContainer.jsx | 3 +- .../UpgradePlanView/UpgradePlanView.scss | 1 + .../components/content/Account.jsx | 4 +- app/panel/components/Settings/OptIn.jsx | 5 +- app/scss/hub.scss | 1 + .../ForgotPassword/ForgotPassword.jsx | 88 ++++++++++--------- .../ForgotPassword/ForgotPassword.scss | 6 +- manifest.json | 4 +- src/background.js | 8 +- 24 files changed, 161 insertions(+), 111 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 56898ca98..cc9c30132 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1256,19 +1256,19 @@ "message": "Block" }, "android_unblock": { - "message": "unBlock" + "message": "Unblock" }, "android_restrict": { "message": "Restrict" }, "android_unrestrict": { - "message": "unRestrict" + "message": "Undo" }, "android_trust": { "message": "Trust" }, "android_untrust": { - "message": "unTrust" + "message": "Undo" }, "hub_side_navigation_home": { "message": "Home" diff --git a/app/hub/Views/SetupView/SetupView.jsx b/app/hub/Views/SetupView/SetupView.jsx index 8db394a37..f70cecabd 100644 --- a/app/hub/Views/SetupView/SetupView.jsx +++ b/app/hub/Views/SetupView/SetupView.jsx @@ -28,7 +28,7 @@ const SetupView = (props) => { const { extraRoutes, sendMountActions, steps } = props; return ( -
    +
    {steps.map(step => ( { - const { sendMountActions, steps } = props; + const { sendMountActions, steps, isAndroid } = props; return ( -
    +
    {steps.map(step => ( } + render={() => } /> ))}
    diff --git a/app/hub/Views/TutorialView/TutorialViewContainer.jsx b/app/hub/Views/TutorialView/TutorialViewContainer.jsx index f42b3985f..db683ea04 100644 --- a/app/hub/Views/TutorialView/TutorialViewContainer.jsx +++ b/app/hub/Views/TutorialView/TutorialViewContainer.jsx @@ -14,6 +14,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import TutorialView from './TutorialView'; +import globals from '../../../../src/classes/Globals'; // Component Views import TutorialVideoView from '../TutorialViews/TutorialVideoView'; @@ -23,6 +24,9 @@ import TutorialLayoutView from '../TutorialViews/TutorialLayoutView'; import TutorialTrustView from '../TutorialViews/TutorialTrustView'; import TutorialAntiSuiteView from '../TutorialViews/TutorialAntiSuiteView'; +const { BROWSER_INFO } = globals; +const IS_ANDROID = (BROWSER_INFO.os === 'android'); + /** * @class Implement the Tutorial View for the Ghostery Hub * @extends Component @@ -88,7 +92,7 @@ class TutorialViewContainer extends Component { }, ]; - return ; + return ; } } diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx index 5b4ec4e55..886994943 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteView.jsx @@ -12,13 +12,14 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; /** * A Functional React component for rendering the Tutorial Anti Suite View * @return {JSX} JSX for rendering the Tutorial Anti Suite View of the Hub app * @memberof HubComponents */ -const TutorialAntiSuiteView = () => ( +const TutorialAntiSuiteView = ({ isAndroid }) => (
    @@ -26,17 +27,21 @@ const TutorialAntiSuiteView = () => (
    {t('simple_view')} -
    - {t('detailed_view')} -
    - {t('detailed_view')} + { !isAndroid && ( +
    +
    + {t('detailed_view')} +
    + {t('detailed_view')} +
    + )}
    @@ -82,6 +87,8 @@ const TutorialAntiSuiteView = () => (
    ); -// No need for PropTypes. The SideNavigationViewContainer has no props. +TutorialAntiSuiteView.propTypes = { + isAndroid: PropTypes.bool.isRequired +}; export default TutorialAntiSuiteView; diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx index 5313f9b07..0f322785c 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/TutorialAntiSuiteViewContainer.jsx @@ -51,7 +51,8 @@ class TutorialAntiSuiteViewContainer extends Component { * @return {JSX} JSX for rendering the Tutorial Anti Suite View of the Hub app */ render() { - return ; + const { isAndroid } = this.props; + return ; } } diff --git a/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingView.jsx b/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingView.jsx index ca0199702..616e018d8 100644 --- a/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingView.jsx +++ b/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingView.jsx @@ -12,13 +12,14 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; /** * A Functional React component for rendering the Tutorial Blocking View * @return {JSX} JSX for rendering the Tutorial Blocking View of the Hub app * @memberof HubComponents */ -const TutorialBlockingView = () => ( +const TutorialBlockingView = ({ isAndroid }) => (
    @@ -26,17 +27,21 @@ const TutorialBlockingView = () => (
    {t('detailed_view')} -
    - {t('hub_tutorial_detailed_expanded_view')} -
    - {t('hub_tutorial_detailed_expanded_view')} + { !isAndroid && ( +
    +
    + {t('hub_tutorial_detailed_expanded_view')} +
    + {t('hub_tutorial_detailed_expanded_view')} +
    + )}
    @@ -60,6 +65,8 @@ const TutorialBlockingView = () => (
    ); -// No need for PropTypes. The SideNavigationViewContainer has no props. +TutorialBlockingView.propTypes = { + isAndroid: PropTypes.bool.isRequired +}; export default TutorialBlockingView; diff --git a/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx index 5210ca461..856309f8a 100644 --- a/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialBlockingView/TutorialBlockingViewContainer.jsx @@ -44,7 +44,8 @@ class TutorialBlockingViewContainer extends Component { * @return {JSX} JSX for rendering the Tutorial Blocking View of the Hub app */ render() { - return ; + const { isAndroid } = this.props; + return ; } } diff --git a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx index 56a7bac57..7c8518116 100644 --- a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx +++ b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx @@ -12,23 +12,24 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; /** * A Functional React component for rendering the Tutorial Layout View * @return {JSX} JSX for rendering the Tutorial Layout View of the Hub app * @memberof HubComponents */ -const TutorialLayoutView = () => ( +const TutorialLayoutView = ({ isAndroid }) => (
    {t('simple_view')} {t('detailed_view')}
    @@ -43,6 +44,8 @@ const TutorialLayoutView = () => (
    ); -// No need for PropTypes. The SideNavigationViewContainer has no props. +TutorialLayoutView.propTypes = { + isAndroid: PropTypes.bool.isRequired +}; export default TutorialLayoutView; diff --git a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx index 59c5f0208..384bcfaa5 100644 --- a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutViewContainer.jsx @@ -44,7 +44,8 @@ class TutorialLayoutViewContainer extends Component { * @return {JSX} JSX for rendering the Tutorial Layout View of the Hub app */ render() { - return ; + const { isAndroid } = this.props; + return ; } } diff --git a/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListView.jsx b/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListView.jsx index 9bfbaee8e..3a5e96c03 100644 --- a/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListView.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListView.jsx @@ -12,18 +12,19 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; /** * A Functional React component for rendering the Tutorial Tracker List View * @return {JSX} JSX for rendering the Tutorial Tracker List View of the Hub app * @memberof HubComponents */ -const TutorialTrackerListView = () => ( +const TutorialTrackerListView = ({ isAndroid }) => (
    {t('hub_tutorial_trackerlist_title')}
    @@ -38,6 +39,8 @@ const TutorialTrackerListView = () => (
    ); -// No need for PropTypes. The SideNavigationViewContainer has no props. +TutorialTrackerListView.propTypes = { + isAndroid: PropTypes.bool.isRequired +}; export default TutorialTrackerListView; diff --git a/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx index 9aeab240b..dfe809712 100644 --- a/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrackerListView/TutorialTrackerListViewContainer.jsx @@ -44,13 +44,15 @@ class TutorialTrackerListViewContainer extends Component { * @return {JSX} JSX for rendering the Tutorial Tracker List View of the Hub app */ render() { - return ; + const { isAndroid } = this.props; + return ; } } // PropTypes ensure we pass required props of the correct type TutorialTrackerListViewContainer.propTypes = { index: PropTypes.number.isRequired, + isAndroid: PropTypes.bool.isRequired, actions: PropTypes.shape({ setTutorialNavigation: PropTypes.func.isRequired, }).isRequired, diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx index 605aaa9cb..d477d66fb 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustView.jsx @@ -12,13 +12,14 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; /** * A Functional React component for rendering the Tutorial Trust and Restrict View * @return {JSX} JSX for rendering the Tutorial Trust and Restrict View of the Hub app * @memberof HubComponents */ -const TutorialTrustView = () => ( +const TutorialTrustView = ({ isAndroid }) => (
    @@ -26,17 +27,21 @@ const TutorialTrustView = () => (
    {t('simple_view')} -
    - {t('detailed_view')} -
    - {t('detailed_view')} + { !isAndroid && ( +
    +
    + {t('detailed_view')} +
    + {t('detailed_view')} +
    + )}
    @@ -63,6 +68,8 @@ const TutorialTrustView = () => (
    ); -// No need for PropTypes. The SideNavigationViewContainer has no props. +TutorialTrustView.propTypes = { + isAndroid: PropTypes.bool.isRequired +}; export default TutorialTrustView; diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx index 42d8e341e..949d361af 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrustView/TutorialTrustViewContainer.jsx @@ -44,7 +44,8 @@ class TutorialTrustViewContainer extends Component { * @return {JSX} JSX for rendering the Tutorial Trust View of the Hub app */ render() { - return ; + const { isAndroid } = this.props; + return ; } } diff --git a/app/hub/Views/UpgradePlanView/UpgradePlanView.scss b/app/hub/Views/UpgradePlanView/UpgradePlanView.scss index e6e73f46d..58d6fda86 100644 --- a/app/hub/Views/UpgradePlanView/UpgradePlanView.scss +++ b/app/hub/Views/UpgradePlanView/UpgradePlanView.scss @@ -749,6 +749,7 @@ section.home-template .section.section-pricing { align-items: center; } @include breakpoint($medium-large-breakpoint down) { + max-width: 100%; padding-left: rem-calc(20); padding-right: rem-calc(20); } diff --git a/app/panel-android/components/content/Account.jsx b/app/panel-android/components/content/Account.jsx index bded9ff3b..6ebb741c1 100644 --- a/app/panel-android/components/content/Account.jsx +++ b/app/panel-android/components/content/Account.jsx @@ -143,9 +143,7 @@ class Account extends React.Component { ); } - _renderCreateAccount() { - console.log('createAccount', this.props); - + _renderCreateAccount() { // eslint-disable-line class-methods-use-this return (
    Create Account
    ); } diff --git a/app/panel/components/Settings/OptIn.jsx b/app/panel/components/Settings/OptIn.jsx index a816cd1ea..f74262c0b 100644 --- a/app/panel/components/Settings/OptIn.jsx +++ b/app/panel/components/Settings/OptIn.jsx @@ -15,7 +15,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import globals from '../../../../src/classes/Globals'; -const { IS_CLIQZ } = globals; +const { IS_CLIQZ, BROWSER_INFO } = globals; +const IS_ANDROID = (BROWSER_INFO.os === 'android'); /** * @class Implement Opt In subview as a React component. @@ -56,7 +57,7 @@ const OptIn = ({ settingsData, toggleCheckbox }) => (
    )} - {!IS_CLIQZ && ( + {!IS_CLIQZ && !IS_ANDROID && (
    diff --git a/app/scss/hub.scss b/app/scss/hub.scss index 60394aad7..3cd030117 100644 --- a/app/scss/hub.scss +++ b/app/scss/hub.scss @@ -31,6 +31,7 @@ html, body, #root { .App__mainContent { margin-left: 54px; } + .android-relative {position: relative;} } // Foundation Overrides diff --git a/app/shared-components/ForgotPassword/ForgotPassword.jsx b/app/shared-components/ForgotPassword/ForgotPassword.jsx index 058cef3fb..f13d8c7b1 100644 --- a/app/shared-components/ForgotPassword/ForgotPassword.jsx +++ b/app/shared-components/ForgotPassword/ForgotPassword.jsx @@ -114,51 +114,53 @@ class ForgotPassword extends React.Component { return (
    -
    - {panel && ( -

    - {t('forgot_password_message')} -

    - )} - {hub && ( -

    - {t('forgot_password_message')} -

    - )} -
    - -

    - {t('invalid_email_forgot')} -

    -

    - {t('error_email_forgot')} -

    -
    -
    -
    - {panel && ( - - {t('button_cancel')} - - )} - {hub && ( -
    - {t('button_cancel')} -
    - )} +
    + + {panel && ( +

    + {t('forgot_password_message')} +

    + )} + {hub && ( +

    + {t('forgot_password_message')} +

    + )} +
    + +

    + {t('invalid_email_forgot')} +

    +

    + {t('error_email_forgot')} +

    -
    - +
    +
    + {panel && ( + + {t('button_cancel')} + + )} + {hub && ( +
    + {t('button_cancel')} +
    + )} +
    +
    + +
    -
    - + +
    ); diff --git a/app/shared-components/ForgotPassword/ForgotPassword.scss b/app/shared-components/ForgotPassword/ForgotPassword.scss index 1e272e2d2..0b1d84d89 100644 --- a/app/shared-components/ForgotPassword/ForgotPassword.scss +++ b/app/shared-components/ForgotPassword/ForgotPassword.scss @@ -37,7 +37,8 @@ margin: 40px auto; } .ForgotPasswordForm { - width: 420px; + max-width: 420px; + margin: 0 auto; } #ForgotPasswordMessage { font-size: 1.25rem; @@ -60,6 +61,9 @@ } #send-button { width: 150px; + @include breakpoint(medium down) { + width: 100px; + } } } diff --git a/manifest.json b/manifest.json index d82dfb614..e326a8bd3 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,5 @@ { "manifest_version": 2, - "debug": true, - "log": true, "author": "Ghostery", "name": "__MSG_name__", "short_name": "Ghostery", @@ -9,6 +7,8 @@ "version_name": "8.5.2", "default_locale": "en", "description": "__MSG_short_description__", + "debug": true, + "log": true, "icons": { "16": "app/images/icon16.png", "48": "app/images/icon48.png", diff --git a/src/background.js b/src/background.js index bb3fb6b33..cb9c02b13 100644 --- a/src/background.js +++ b/src/background.js @@ -1746,8 +1746,12 @@ function initializeGhosteryModules() { // we need to do this after running scheduledTasks for the first time // because of an A/B test that determines which promo variant is shown in the Hub on install if (globals.JUST_INSTALLED) { - const route = (conf.hub_promo_variant === 'upgrade' || conf.hub_promo_variant === 'not_yet_set') ? '' : '#home'; - const showPremiumPromoModal = conf.hub_promo_variant === 'midnight'; + let route = (conf.hub_promo_variant === 'upgrade' || conf.hub_promo_variant === 'not_yet_set') ? '' : '#home'; + let showPremiumPromoModal = conf.hub_promo_variant === 'midnight'; + if (BROWSER_INFO.os === 'android') { + route = '#home'; + showPremiumPromoModal = false; + } chrome.tabs.create({ url: chrome.runtime.getURL(`./app/templates/hub.html?$justInstalled=true&pm=${showPremiumPromoModal}${route}`), active: true From 1d09f3d58066e854d4023143a224521a5a36f057 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Wed, 29 Jul 2020 00:35:13 -0400 Subject: [PATCH 66/89] fixing issues with unknown trackers on android --- _locales/en/messages.json | 3 ++ app/panel-android/components/PanelAndroid.jsx | 1 + .../components/content/BlockingCategory.jsx | 10 +++- .../components/content/BlockingTracker.jsx | 51 +++++++++++++++++-- app/panel-android/utils/tracker-info.js | 11 ++-- app/scss/android/_blocking_tab.scss | 5 -- src/background.js | 6 ++- src/utils/cliqzModulesData.js | 4 +- 8 files changed, 74 insertions(+), 17 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index cc9c30132..af9f2ebf9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1270,6 +1270,9 @@ "android_untrust": { "message": "Undo" }, + "android_anonymized": { + "message": "Anonymized" + }, "hub_side_navigation_home": { "message": "Home" }, diff --git a/app/panel-android/components/PanelAndroid.jsx b/app/panel-android/components/PanelAndroid.jsx index cc997c966..520f79058 100644 --- a/app/panel-android/components/PanelAndroid.jsx +++ b/app/panel-android/components/PanelAndroid.jsx @@ -168,6 +168,7 @@ class PanelAndroid extends React.Component { sources: tracker.domains, whitelisted: tracker.whitelisted, blocked: false, // To appease BlockingTracker PropTypes + wtm: tracker.wtm, }) _renderAccount() { diff --git a/app/panel-android/components/content/BlockingCategory.jsx b/app/panel-android/components/content/BlockingCategory.jsx index 91d265c2a..c0b0b7dac 100644 --- a/app/panel-android/components/content/BlockingCategory.jsx +++ b/app/panel-android/components/content/BlockingCategory.jsx @@ -51,6 +51,10 @@ class BlockingCategory extends React.Component { return 'restricted'; } + if (category.id === 'unknown') { + return 'unknown'; + } + if (trackers.every(tracker => tracker.ss_allowed)) { return 'trusted'; } @@ -132,6 +136,10 @@ class BlockingCategory extends React.Component { renderCategorySelect() { const categorySelect = this.categorySelectStatus; + // Hide category blocking for Unknown trackers + if (categorySelect === 'unknown') { + return false; + } const categorySelectClassNames = ClassNames('BlockingSelectButton', { BlockingSelectButton__mixed: categorySelect === 'mixed' || categorySelect === 'ss_mixed', BlockingSelectButton__blocked: categorySelect === 'blocked', @@ -230,7 +238,7 @@ class BlockingCategory extends React.Component {
    {t('blocking_category_trackers')} - {t('blocking_category_blocked')} + {category.id === 'unknown' ? t('android_anonymized') : t('blocking_category_blocked')}
    { event.stopPropagation(); const { tracker } = this.props; - const url = getUrlFromTrackerId(tracker.id); - const tab = window.open(url, '_blank'); + const slug = (tracker.wtm) ? tracker.wtm : getSlugFromTrackerId(tracker.id); + const tab = window.open(`https://whotracks.me/trackers/${slug}.html`, '_blank'); tab.focus(); } @@ -260,8 +265,48 @@ class BlockingTracker extends React.Component { ); } + renderUnknownOverflow() { + const { + type, + open, + tracker, + settings, + } = this.props; + const { ss_allowed = false, ss_blocked = false, blocked } = tracker; + const { toggle_individual_trackers = false } = settings; + + const selectGroupClassNames = ClassNames('BlockingSelectGroup full-height', + 'flex-container flex-dir-row-reverse', { + 'BlockingSelectGroup--open': open, + 'BlockingSelectGroup--wide': type === 'site' && toggle_individual_trackers, + 'BlockingSelectGroup--disabled': this.selectDisabled, + }); + const selectBlockClassNames = ClassNames('BlockingSelect BlockingSelect__block', + 'full-height flex-child-grow', { + 'BlockingSelect--disabled': this.selectBlockDisabled, + }); + + return ( +
    + {type === 'site' && toggle_individual_trackers && ( +
    + {ss_blocked ? t('android_unrestrict') : t('android_restrict')} +
    + )} + {type === 'site' && toggle_individual_trackers && ( +
    + {ss_allowed ? t('android_untrust') : t('android_trust')} +
    + )} +
    + ); + } + renderTrackerOverflow() { const trackerSelect = this.trackerSelectStatus; + if (trackerSelect === 'antiTracking' || trackerSelect === 'adBlock') { + return this.renderUnknownOverflow(); + } if (trackerSelect === 'override-sb') { return this.renderSmartBlockOverflow(); } diff --git a/app/panel-android/utils/tracker-info.js b/app/panel-android/utils/tracker-info.js index bacb66d90..df8e1a3a2 100644 --- a/app/panel-android/utils/tracker-info.js +++ b/app/panel-android/utils/tracker-info.js @@ -16,10 +16,13 @@ import { apps } from '../../../cliqz/core/tracker_db_v2.json'; -// Link to whotracks.me website -export default function getUrlFromTrackerId(id) { +/** + * Look up WhoTracksMe url slug + * @param {Int} id Ghostery tracker ID + * @return {String} WTM slug + */ +export default function getSlugFromTrackerId(id) { const trackerName = apps[id] && apps[id].name; const trackerWtm = (Object.values(apps).find(app => app.wtm && app.name === trackerName) || {}).wtm; - const slug = trackerWtm || '../tracker-not-found'; - return `https://whotracks.me/trackers/${slug}.html`; + return trackerWtm || '../tracker-not-found'; } diff --git a/app/scss/android/_blocking_tab.scss b/app/scss/android/_blocking_tab.scss index 06cd9dec7..38d8a5a52 100644 --- a/app/scss/android/_blocking_tab.scss +++ b/app/scss/android/_blocking_tab.scss @@ -55,11 +55,6 @@ border-bottom: 1px solid $ghosty-blue; } - &.BlockingCategory__unknown .BlockingCategory__numBlocked, - &.BlockingCategory__unknown .BlockingSelectButton { - display: none; - } - .BlockingCategory__details { min-height: 80px; padding: 15px 12px; diff --git a/src/background.js b/src/background.js index cb9c02b13..6e65cd5e7 100644 --- a/src/background.js +++ b/src/background.js @@ -800,11 +800,13 @@ function onMessageHandler(request, sender, callback) { if (name === 'getCliqzModuleData') { // panel-android only if (!message.tabId) { utils.getActiveTab((activeTab) => { - sendCliqzModuleCounts(activeTab.id, activeTab.pageHost, callback); + const pageHost = (activeTab.url && utils.processUrl(activeTab.url).hostname) || ''; + sendCliqzModuleCounts(activeTab.id, pageHost, callback); }); } else { chrome.tabs.get(+message.tabId, (messageTab) => { - sendCliqzModuleCounts(messageTab.id, messageTab.pageHost, callback); + const pageHost = (messageTab.url && utils.processUrl(messageTab.url).hostname) || ''; + sendCliqzModuleCounts(messageTab.id, pageHost, callback); }); } return true; diff --git a/src/utils/cliqzModulesData.js b/src/utils/cliqzModulesData.js index 4a882a34e..ed0cc52cd 100644 --- a/src/utils/cliqzModulesData.js +++ b/src/utils/cliqzModulesData.js @@ -89,11 +89,11 @@ export function getCliqzData(tabId, tabHostUrl, antiTracking) { if (dataPoints || whitelisted) { const type = antiTracking ? 'antiTracking' : 'adBlock'; const { - name, domains, ads, cookies, fingerprints + name, domains, ads, cookies, fingerprints, wtm } = other; unknownTrackers.push({ - name, domains, ads, cookies, fingerprints, whitelisted, type + name, domains, ads, cookies, fingerprints, whitelisted, type, wtm }); } }); From ddde80efe7999accceeff7daf761ef803d9c1ce0 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Wed, 29 Jul 2020 00:39:07 -0400 Subject: [PATCH 67/89] update snapshots --- .../__snapshots__/SetupView.test.jsx.snap | 10 +++++----- .../SetupViewContainer.test.jsx.snap | 10 +++++----- .../__snapshots__/TutorialView.test.jsx.snap | 2 +- .../TutorialViewContainer.test.jsx.snap | 12 +++++------ .../TutorialAntiSuiteView.test.jsx.snap | 20 ++++++++++--------- .../TutorialBlockingView.test.jsx.snap | 20 ++++++++++--------- .../TutorialTrustView.test.jsx.snap | 20 ++++++++++--------- 7 files changed, 50 insertions(+), 44 deletions(-) diff --git a/app/hub/Views/SetupView/__tests__/__snapshots__/SetupView.test.jsx.snap b/app/hub/Views/SetupView/__tests__/__snapshots__/SetupView.test.jsx.snap index 3be46c161..dd6b69d36 100644 --- a/app/hub/Views/SetupView/__tests__/__snapshots__/SetupView.test.jsx.snap +++ b/app/hub/Views/SetupView/__tests__/__snapshots__/SetupView.test.jsx.snap @@ -2,7 +2,7 @@ exports[`app/hub/Views/SetupView component More Snapshot tests with react-test-renderer, but for edge cases edge case where activeIndex not in steps index 1`] = `
    -
    - detailed_view +
    +
    + detailed_view +
    + detailed_view
    - detailed_view
    -
    - hub_tutorial_detailed_expanded_view +
    +
    + hub_tutorial_detailed_expanded_view +
    + hub_tutorial_detailed_expanded_view
    - hub_tutorial_detailed_expanded_view
    -
    - detailed_view +
    +
    + detailed_view +
    + detailed_view
    - detailed_view
    Date: Wed, 29 Jul 2020 09:40:52 -0400 Subject: [PATCH 68/89] Fix broken tests --- .../__snapshots__/SetupView.test.jsx.snap | 10 +++++----- .../SetupViewContainer.test.jsx.snap | 10 +++++----- .../__snapshots__/TutorialView.test.jsx.snap | 2 +- .../TutorialViewContainer.test.jsx.snap | 12 +++++------ .../__test__/TutorialAntiSuiteView.test.jsx | 4 ++-- .../TutorialAntiSuiteView.test.jsx.snap | 20 ++++++++++--------- .../__tests__/TutorialBlockingView.test.jsx | 4 ++-- .../TutorialBlockingView.test.jsx.snap | 20 ++++++++++--------- .../__tests__/TutorialLayoutView.test.jsx | 4 ++-- .../TutorialTrackerListView.test.jsx | 4 ++-- .../__tests__/TutorialTrustView.test.jsx | 4 ++-- .../TutorialTrustView.test.jsx.snap | 20 ++++++++++--------- 12 files changed, 60 insertions(+), 54 deletions(-) diff --git a/app/hub/Views/SetupView/__tests__/__snapshots__/SetupView.test.jsx.snap b/app/hub/Views/SetupView/__tests__/__snapshots__/SetupView.test.jsx.snap index 3be46c161..dd6b69d36 100644 --- a/app/hub/Views/SetupView/__tests__/__snapshots__/SetupView.test.jsx.snap +++ b/app/hub/Views/SetupView/__tests__/__snapshots__/SetupView.test.jsx.snap @@ -2,7 +2,7 @@ exports[`app/hub/Views/SetupView component More Snapshot tests with react-test-renderer, but for edge cases edge case where activeIndex not in steps index 1`] = `
    { describe('Snapshot tests with react-test-renderer', () => { test('tutorial anti-suite view is rendered correctly', () => { - const component = renderer.create().toJSON(); + const component = renderer.create().toJSON(); expect(component).toMatchSnapshot(); }); }); describe('Shallow snapshot tests rendered with Enzyme', () => { test('the happy path of the component', () => { - const component = shallow(); + const component = shallow(); expect(component.find('.TutorialAntiSuiteView').length).toBe(1); expect(component.find('img').length).toBe(2); expect(component.find('.TutorialView__keyItem').length).toBe(3); diff --git a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap index 83a6fdc3e..bed91e7ce 100644 --- a/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap +++ b/app/hub/Views/TutorialViews/TutorialAntiSuiteView/__test__/__snapshots__/TutorialAntiSuiteView.test.jsx.snap @@ -17,16 +17,18 @@ exports[`app/hub/Views/TutorialViews/TutorialAntiSuiteView component Snapshot te className="TutorialAntiSuiteView__image antisuite-simple" src="/app/images/hub/tutorial/antisuite-simple.png" /> -
    - detailed_view +
    +
    + detailed_view +
    + detailed_view
    - detailed_view
    { describe('Snapshot tests with react-test-renderer', () => { test('tutorial blocking view is rendered correctly', () => { - const component = renderer.create().toJSON(); + const component = renderer.create().toJSON(); expect(component).toMatchSnapshot(); }); }); describe('Shallow snapshot tests rendered with Enzyme', () => { test('the happy path of the component', () => { - const component = shallow(); + const component = shallow(); expect(component.find('.TutorialBlockingView').length).toBe(1); expect(component.find('.TutorialBlockingView__image').length).toBe(2); expect(component.find('.TutorialView__keyText').length).toBe(3); diff --git a/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap b/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap index f314105f1..6a430821b 100644 --- a/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap +++ b/app/hub/Views/TutorialViews/TutorialBlockingView/__tests__/__snapshots__/TutorialBlockingView.test.jsx.snap @@ -17,16 +17,18 @@ exports[`app/hub/Views/TutorialViews/TutorialBlockingView component Snapshot tes className="TutorialBlockingView__image blocking-detailed" src="/app/images/hub/tutorial/blocking-detailed.png" /> -
    - hub_tutorial_detailed_expanded_view +
    +
    + hub_tutorial_detailed_expanded_view +
    + hub_tutorial_detailed_expanded_view
    - hub_tutorial_detailed_expanded_view
    { describe('Snapshot tests with react-test-renderer', () => { test('tutorial layout view is rendered correctly', () => { - const component = renderer.create().toJSON(); + const component = renderer.create().toJSON(); expect(component).toMatchSnapshot(); }); }); describe('Shallow snapshot tests rendered with Enzyme', () => { test('the happy path of the component', () => { - const component = shallow(); + const component = shallow(); expect(component.find('.TutorialLayoutView').length).toBe(1); expect(component.find('.TutorialLayoutView__image').length).toBe(2); }); diff --git a/app/hub/Views/TutorialViews/TutorialTrackerListView/__tests__/TutorialTrackerListView.test.jsx b/app/hub/Views/TutorialViews/TutorialTrackerListView/__tests__/TutorialTrackerListView.test.jsx index 93662c432..c31fde285 100644 --- a/app/hub/Views/TutorialViews/TutorialTrackerListView/__tests__/TutorialTrackerListView.test.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrackerListView/__tests__/TutorialTrackerListView.test.jsx @@ -19,14 +19,14 @@ import TutorialTrackerListView from '../TutorialTrackerListView'; describe('app/hub/Views/TutorialViews/TutorialTrackerListView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('tutorial tracker list view is rendered correctly', () => { - const component = renderer.create().toJSON(); + const component = renderer.create().toJSON(); expect(component).toMatchSnapshot(); }); }); describe('Shallow snapshot tests rendered with Enzyme', () => { test('the happy path of the component', () => { - const component = shallow(); + const component = shallow(); expect(component.find('.TutorialTrackerListView').length).toBe(1); expect(component.find('.TutorialTrackerListView__image').length).toBe(1); }); diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/TutorialTrustView.test.jsx b/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/TutorialTrustView.test.jsx index 92c050bfa..2cf8e90ec 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/TutorialTrustView.test.jsx +++ b/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/TutorialTrustView.test.jsx @@ -19,14 +19,14 @@ import TutorialTrustView from '../TutorialTrustView'; describe('app/hub/Views/TutorialViews/TutorialTrustView component', () => { describe('Snapshot tests with react-test-renderer', () => { test('tutorial trust view is rendered correctly', () => { - const component = renderer.create().toJSON(); + const component = renderer.create().toJSON(); expect(component).toMatchSnapshot(); }); }); describe('Shallow snapshot tests rendered with Enzyme', () => { test('the happy path of the component', () => { - const component = shallow(); + const component = shallow(); expect(component.find('.TutorialTrustView').length).toBe(1); expect(component.find('.TutorialTrustView__image').length).toBe(2); expect(component.find('.TutorialTrustView__key').length).toBe(1); diff --git a/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap b/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap index 27e9a3973..f34f2f7cd 100644 --- a/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap +++ b/app/hub/Views/TutorialViews/TutorialTrustView/__tests__/__snapshots__/TutorialTrustView.test.jsx.snap @@ -17,16 +17,18 @@ exports[`app/hub/Views/TutorialViews/TutorialTrustView component Snapshot tests className="TutorialTrustView__image trustrestrict-simple" src="/app/images/hub/tutorial/trustrestrict-simple.png" /> -
    - detailed_view +
    +
    + detailed_view +
    + detailed_view
    - detailed_view
    Date: Wed, 29 Jul 2020 10:18:11 -0400 Subject: [PATCH 69/89] update hub tutorial images for android --- app/hub/Views/TutorialView/TutorialView.scss | 11 ++++++++++- .../TutorialLayoutView/TutorialLayoutView.jsx | 2 +- .../TutorialLayoutView.test.jsx.snap | 2 +- .../hub/tutorial/antisuite-simple-android.png | Bin 0 -> 41151 bytes .../hub/tutorial/blocking-detailed-android.png | Bin 0 -> 57832 bytes .../hub/tutorial/layout-detailed-android.png | Bin 0 -> 53753 bytes .../hub/tutorial/layout-simple-android.png | Bin 0 -> 40643 bytes .../hub/tutorial/tracker-list-android.png | Bin 0 -> 55296 bytes .../tutorial/trustrestrict-simple-android.png | Bin 0 -> 41104 bytes .../components/content/BlockingTracker.jsx | 1 + 10 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 app/images/hub/tutorial/antisuite-simple-android.png create mode 100644 app/images/hub/tutorial/blocking-detailed-android.png create mode 100644 app/images/hub/tutorial/layout-detailed-android.png create mode 100644 app/images/hub/tutorial/layout-simple-android.png create mode 100644 app/images/hub/tutorial/tracker-list-android.png create mode 100644 app/images/hub/tutorial/trustrestrict-simple-android.png diff --git a/app/hub/Views/TutorialView/TutorialView.scss b/app/hub/Views/TutorialView/TutorialView.scss index ae47ef2ec..eebb3226e 100644 --- a/app/hub/Views/TutorialView/TutorialView.scss +++ b/app/hub/Views/TutorialView/TutorialView.scss @@ -149,8 +149,17 @@ left: 1%; padding-right: 12%; margin-bottom: 30px; + @media only screen and (max-width: 740px) { + left: auto; + padding-right: 0; + } + } + &.layout-detailed { + left: -11%; + @media only screen and (max-width: 740px) { + left: auto; + } } - &.layout-detailed { left: -11%; } } @media only screen and (max-width: 960px) { .TutorialLayoutView .TutorialView__tagline { diff --git a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx index 7c8518116..5764a49d8 100644 --- a/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx +++ b/app/hub/Views/TutorialViews/TutorialLayoutView/TutorialLayoutView.jsx @@ -21,7 +21,7 @@ import PropTypes from 'prop-types'; */ const TutorialLayoutView = ({ isAndroid }) => (
    -
    +
    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

    k|J?LYIKG=CntU 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 literal 0 HcmV?d00001 diff --git a/app/images/hub/tutorial/blocking-detailed-android.png b/app/images/hub/tutorial/blocking-detailed-android.png new file mode 100644 index 0000000000000000000000000000000000000000..c4b03659a401292152caf385e6b3ebff799dbdf8 GIT binary patch literal 57832 zcmbSzV~}J`v~AnAZQIi}s%>l9p4PNAZQJ&=ZQHhOa~iL{d*l6mKVC#dorpY{`|Pa5 z$-UOv6|Mx3LWIME0|5a+l#v!!0RaJp2LS;?gMk8;Y?z&A0WY8~DpH~#)zbuLARxjZ zGU6g?9-x={&_+oT>%MLcXURmrZzLGBMCvqaU;cDuZ!M_K7|#4|Zpg|y0$hT**>WhO z;OCV|-*g2FOW$;SKTKYy+KS>)WHRu8XV%Djon)t5Y`TPAZP%;T+8((VEC6k2kkLlEWF(+5Cmyo^egft_d*WA~P*G9gg+3n_T)m$| z#6=4Gg#%me=1we+l9DExKE;Oai|Kl0B7w{PLk^tr_T`sw?2lp2|V#^Z+1 z0D(p>8O4*f;sS$G-t5c>%>XSr0SY$qGjPZ&*XManQ2`XM0}=}&{0kxs{`CS&=XrJU zze;p2{&{r?J?^l0@O1w|?mbg`_a7G&9rU%dJa~)4eMDuF6gHfDo%7s~l-gf^@bUeg z93l40((pQc4=rQY%KCj?{uvP&=|HJOu_&aQ83^=GSj^GiKOP+y_iZv579ERA{5NN@ za_L%lqN3m%P6Yus8jXpy?Bu&Pk`@vNA9TSgbLjK=61~k@y*a&Dx>vN(T`xFt>%soM zWPDn~n@eZ{N@g?AYVWm^uuo#oCk&1Zwt>f9=h;GOs98mmWGxKHkW+^y1pdT-UdHpz z`Wy^LhbOW4!b@~71%{|0%V6Y>E|ysF8u6mAA^p19>Tv%_qn1FY4v)*Oouo`kAEU4e zY)O(tfP;BDfS54?NC+vBij0W3E><&+PDdQK&P+QJK5!J~LI6l#3K{kKYe!-;PurF0 zxhF7yUC>-UT8cTXmYDL+LJ*J`QIs)rn=NDZiqf;EhwD2o#St`HX(Mr zPgzY`AeKrX}y(20QCegAhiF==clhDKsY)KTA`LC`e@vm9Z<_tl(H5 zIObYD==FF14RC)dIO5z$Q%TlY>of9>iO%?c*3TzEpYGl($n3AuWmt#oLo|2RYk{@< z)4!sbLKjV9jQYp;5ob^?w@i{?~{Tf8Lt;B%U$nf9LJf`2~@VjAgK%gL1%@d`s?UuwqOpYvD)8+gn7CLRe@qgog_EAH?`44K@L1<0V3ow zo75#^v!a}?l4&~he7+|Sveju}zgj?_M;Vv*(d)F!5!Y$wN>hbf!7YHG)J_wViHGoH zJG}Dbe7zODxm)vCy@q-02SXMB4!-DMGapCOYPu+&mVX3kLVkk3A5Z&0N#_^)kcB?N zVhH&QeEzvXL1ZN@8QkAHvwA%{Xm$N=vT~T4t2>>|2Zur?n&bnBI5~x*cdl11a1|63 z&@Hy9erIw!a9xRCY1skn`042Nax#@0GFPz`SzVu3D}e zo0yQb|2i}2SF`t9LhFbml5a8a(TAF@2tCrLC~DtDMYaE{RR<=Z082|dW@gO=;J@Nf zT@&>Q4KUfWNtd4PkL6X zd2ulLe3_LiEMX5sJ6c*=o`Wd%xU4Kajg;|DB{5}i7>r@LHKVZ@~^vu#JYJ zaG`9UHynf(x;a>^dR^B2*URr^r4e) zbdw=?C4w%!H&8bzU$^D@Z?B5?*W1bH9*>LAz{_yW_j$xUP=qPoyr$RG}>iaOPi-XF@87=L{I z6HqNj`_bWk1E82OKKjdE#-PmJibhBU$D&3W1Ji0Mg_V8<4J+mStDml0ynApE!ged; zkKIP|K`=UiC9E3hMw$xXp|76)#dFvCw5(fay+o&)|Muo-i?U8#W(}c4uIg#v_vyL- zQqy@47iesjYz{L$v;EJL$?7Hq+koXfMAEpNY(>u{8W&?RhoF@2$91bI&#Je>;`~9Z zP+u8oU7ZuuDJPC43N3B<`f~oeev5sV6*4~8U#Xd#AK67y$)>5eBoq?kO%6NTjj)K0 zn+hA!B)@CiXkb;X6N(UJTH3L%5=w%g(LuHC?Q>`|7bFRLm2~z_MUk-R8u{MFlPf37 z7(TDd3S&%0V%VZm-dX*+ZIOm>_#Rne7JJfeD|$aZyuF{-O2z+$Ky_g@({-sPJCDr?B0p|53!SCi`Cm3&Xie<64y35d?!XLd> zMo6PD+5YcGkW}05B!#-n=X_YP248S#RLFgostq zb6YdN^!Q3gr7pRI!~0}z-32S$Z?@ZrSz&BUuQJoQ+x1O4T_}yUBCilCC%Ra180_?X zkRNfMFZWhoU0iJbDV*`ct6ACDyfvvyPPh&@y3H33j(0=YnOtOXatd}+BKGm|Oi4BC z+2chbc8acFo?|E~HGtv5rCPHwlb~H|5HJ{C86=a-cqt-tI;FgV2xWpbUXNS)191{k z(!MAr(bsW6w)*fwv0Un`pt7`+bJbZdT@i`SP~N9dZq_k*6^UXs%=)E0Colx z27$p##pzHaHm;*_!Z0miv`GTqxD-?S>0on%QV?5VmX#t*6p*-G$=HKed`|7 z$CZOS{!u%xO6*EX#?FZxm0L5QpN>D|w|Dq*JNED8F7HbN+DC^KJjrgoDOO zy%;xY@p;0knx?O8a_{YxAfUjgVXnan@FES?T0@y-8VC@X5}b;M?wYr4hgsDyYB25*a}Urcv9|Rg(i= zLGSl{hr7MwaWEx0lq8~DQS_70#KeStvE!97S%S{o%1UO%{DAs&bGAm6f`#-O9HTPMyQkOtp`Ji==7x>vFd#uchfyh5+9&`57qZemvD z`sIk?;?h;L#;ZKwbbW-HUm+n9B~xV&^o+%oTz}mT8}S38FL8X;mxcUkeVW+U7?N^@ z?8~(`3R{!(-yT>2!SNpEWi{n`m~w3os)##(jP?h^Pi_bb?H&FbYj-O36n_!%#V%;Xb1aa zzts+HVSJt3jQ*2)hRS!2#weeSPac8EL0!K5quQAgdc^&je#B{e#ZA3)H=>5I^U2{i z(qi!s2vu#>4a=~`Bwq#3u$1%uMGg?CR8qa`%9N^s5I(uk|Dvx|$KF7RRkaYxL1&Uq zUZXy;&WjNh3tkj1f?6X~E_GwrIszC6B&}MQoAXX5 z;mZI#!5a^N!ebe$Vy2(~LylvA$jIAjV!7=9AM>JLOSC;>ViervOTHjOoAe+*3)Ovk zh^(rc-(~9=tK_x-B zd`-#iI10F_Zu{+}V-pSf_x)(AOyIJ+WABGG^rRQ+q1fNy8ESK{?&Cer?w#FLstxUG zA%Fpe7KOrESNS~2{puaE*oq25+D2xM@`R|no4a+UCMj(Q&JzfrSet7d$>&oQF+jC% zy*K!{-D24M9?j7&%Zdpw8&O;qY{N2GXeO&RJ=dF z%)Gy+W?H{ezt$Omj@3K4I@%r@zdQKo3rZDJ* zqe|t5G7Emex$nI5*qdgB_%ZasN@Ycd=lVuV-!t}KiL4)A@mvKfdt4>EBi;u42XV;Z*LQBH9t<7w3=tBS-Zhj9|MEA$AK<8zNm;I|*&dK6O`FT#1Y zF6S`fCQAw_F{D**1Wx4T$f5B-(Ton@h4Xp82xdSY?z|_Gjihh|QKj~aToanIcn>1-)Eg zCrmjeho+MKrs^Z5r9@?Q5=hS$eqMNKaC#bQK?f;N?vHU_$kyC{_Nvb3QwdybuDa{@ z`wl;5L}z&{6N^f^{kI7zK$VAYfcuHW)bk}OaQ~QHf^(wh$Orgw%_i`@Sn2UT1RZCn z=HEJKiLl;64B)>Ie}x&SpUd*vla_^5bsB2IHoA=y)ao)J-}b#C&-yja%HwT}oE%Ivs0 zOS<~vA3$^bHwL)><{t}#2ZlubW@~=4#BT*Z(`_-m&I<8(9ty3S`SK}0!LHMN)$L}H z*eftCR5nBKMKqM3BkK@uvA?<-vM?mKo8bC&Isa#u1Ewa{FJ5xScGGv^6EvuB5=U?y zX&)7vSSCF98E&1z#I%*2V=2AKhKr1w8d-{neG)}Sd_&wsmH`R3omeoFCA~wO`e1Di zdH2n)OBT}vgHLIx?e2Mkb$`-SdGhMO9CeEMk@#<;`GUs!54f>R&$J##!4^hpO&Yq# z(*;CN$hAZGhCD&{L)K87yjivCOcM9|a;bwgzpqqw-b56I-G8pn0s?m*J{)9TFwIyD zc-@d|81ycApnak;|Dx?_853B3Pnx#ACv-S2G=2AW5DUio2K{3D)!Rx-?(ZLxt&)d| zHe#0iyoMds+u9;D@SxZo;V1aQUDRx2<-4)KhDz(X?Z8aI>6;m%hMg>C7GAG1zuGKD zFH5DI_W8-ob37;}56w8q+`pN7Qm7x={1`bj${x$!gE_Ih7gB0C!nh*n?^7N)s&q!V zD-Jo`YP(cYT*sfqc8~LMwTB;7K|yWTGm>KzCuV1TdtmY%=6Hhc(*p9 z8rRGTUR6d#=(Udu3SJea!Egb@Wk&*_px}k_!9pl1Os`vi#tk6%>c4FH`w6~+A-k>xFH8%jG$0DDE0x@2P7%H3|ewgVT=ZIvn{Qi1%{4o3AdqIx!JhA$#SBK@k^@t4Y zgVrPd>Vi-co(KsABCJ4FSCUoF_jIYih%0^!pFPcV&m%{a?;dAs8_HkJ9i~_7ara}G zp^@CS5i`SnZwbZPg~<_5?C8M`2Zdg*8{e1<`|dE11QjlPbb{q=U8LYZa8gisRGt3B zl!OHIyPg)Jk!0+xfJ(`&2Xd z=%sw_H(?frU(nM6ClKTTjkZfs3pr|-VBn3CZsq7Qw0w(DtuK9{|T%ys+Na1?eO7Rb%?hTizZOj%_* zz1{{n-U!)Cf|bhcc%7gpTHu$szHy$!dB~DL#KmN9s1VM>>Kq--ooW5}=Q=cN3~jW3 zVt9Zc)^gxb3>S+2jK_x*{P8M^yaHvrl@OyX^ciuWh%@~JVZHhe`4l{xJd?}T5J^T@ z&uP1p)3>>mM3bRP=s5Q?U$vm~I7@bo?iW^ZmKsiWad{JBMj+;v&Py#hdFped-mszw zgF3pY>a461kwaSY*uq)^4A$6P7G2E4zjRt{Zabb056Pcwno%kh`+j~p+M^SU;tj{L z^a+hh?fFisT+wBxGz|j!-t+BIyh5J&@;F!u<3XwCt05z(9TA& zKe}s!p(e2yGffYzM(Q6h>aScX8tb4%_Y?Sts40 zuAH3_zPVtKKs#iQxMC`+U@BJ?meEsY<7_S#Kee} zq)8dUSgxD#9XmV?C`05%Nv=Zx+HdLj!od9WpJ1z+P)l#70q*jLz>~F-^Gb=`|EoL?se$we>#eJ7^S?GJ;2om@eEyK`dL#z7 z17mR044E*UupzlArVs;aVuzZl8JzERS$*aRB@zr}%MRQVN~Rf{M=kKa<~5~;M~z9EVr@Eo{R2xuCdYv4 z3U>g>#ff9VK4;b|*?50Ak(}I6^S;0pouM_{O;KCsicX)2dG(vW33p=4+8%Q zIa`%qSDkI57+xw0xqzqFo$(QPK%?i_3GA0_L;iY1ppdB+I$)I2P zBxLe5-+%NC)d@&PiJKy9a;19x?TtwOCE+O?8KuV;^=VUA6OdD}gh0a4GwMP3H5JkB zJaA&juO(eyHXWjJT+XEUZ~H%{Dhymag3Y+#l4`VP&)x<)(pk1*jI$9!BeC4aju!K@*ozu8v49g_HA#|vw|%wOuUF~DeVr7 z>oFr4>%?@_nqo-aphsfUHf`dAz`bRQ=B0IYK^!b&A7mjZhapr!?Zv6q*gS2PY1VVq zANA%81Plr*Zeg&{b&4$QXBxw*$(d9#LMUc%*h@J{%n2pV)HOQsFy-p@$w`duIcQCU z)n8>vxyJOffyw2Hb2lq5@~}1$7-hBYxiuM~+H;zmS^Yw?E!wM(%hq`YYy0e;&bK4& zk>ul2j!?=nW*q1DBy)Jl7q5>X*=>#G!a5+5#TfBPHC>GP%#_IEydocAt&!uC*b55U zpUZiUQtm`{*EE=SEM1%j254kRWi}Jfj`}N>3aPj>JXw#l%cLnUCa$+(9{LvHTdIL4g#o!I^bKb z;>$kRr|o_*`zK@iZ%wOQ50Iv$&nd_H+mS&~kY{_g!nqSGZAmF-OGb0;|2V30F)J)df=Qxi}Pk&?~i=_RMorn0oW@=iKN7)`_HP~vB z+^=2R(KkXe-X9lLY|M#H=-*Z>wXHRZ9M#uocETdrjz8di?y>|H@~}}jkw2(1Vg6o& zZZ4AJQ8(W0j;6Q8N$~z5G-byRyCWA<$DN2(?2p~>$lx(Nu9MK`GSr**w7u6eds`VnUXqS@*8imq35es0gs-*Wpb7?XD59>oA&a(5BAx*AD0 z>#g+w6GdrvGCf`FYhZm|Gmx4dH>ddKb_g@)VBHKVcJ%q63~P(dTT)oN>hZVdK8y`Y z^v8_xn*Fi7h$*k@KK!Zyo_^S-KzSM3jsf#8=3)O{XX+ayak((f1_ClRRi2asBu%QFnkFU|GINK{|DZ+oAXXUzlwL>QNimhrmFLmO@vq4kYc)1>z@tdadscWxT{r4XUW&jT9%%mb_zvA zdqCY(-e~mE-hBB`afbCR)YnnXglbDs?M~;+btI*wA~=s1S+!N@p-xnJ&zE03%0=Wg zb-i0xS6!Q>N+Z{Es!iX(fVEvRm9|vkwPxLtJ;nIP)CdG!XeXqw+j&C2v8G$E6WkBC z-|h=PRas)6F5a)^i>OLR@Fhh{Losn$;L->kk2*m~=y1}7FZTf^jJ{*{dwl!mWxmU` zTa$lRN|J}puFt~(G*toPS+{1VXad6L>Vu2d!}L*Mt(t4jhyst{!H(h=_L(!8?6?VnF^PR z2~?ru-PPrIo@8qkg==&qyn3lEIsCAw)tJQmjr8UZcsmU-i=>Mi0FIa4ZzLX{T7#s{ z31vg_*-s84ukPln!>DuVHLi()y;sy&C-Bm=a}3uN8f4w6S_Nfo12<59YJJ$ zRULqU{*&_G#A*tHw)C@NC~uC)ymV;BsJY1?ad%Luvg}2cDZEju#$-qz%_7{Pzj{=O zT9|&%`R&E(f7lB60K;Oqj8CdMO-oxI6>*GKwZ3@{QLG=~zkHPk-cm+F?^M0^_1LZ zvB~JwjO&j*feGF}h%FDKErq0^K?K$iqd1g+a3Yqe2Lo5hGP0g5?5VZe;S@Ec@xqu4 zPZM17flwCVoZ4yO2TuhRcP_h$DGz%x$s$~99f56*o{m2?)Swam#*Bj$wTXz)@g(em zlBU9ewSG`ZDWrVNLe;jWtxrr1F^Tz#^SV)6>^GpfS%d&4i-W%OL}0?uFon*K_r#}G zS!=4(rFSQ{op=>f3S2l3pT{8D%{AL&Kit%2+-^Vl1}G=;GZ$($H^ohPH`ae5PzowX z$XE)sC&*nt2dIeM;sq)xEk%LLGD(FQxi*qMq)*W5`fwS%$tk%mPsYkNF3!n2{6Mh-u#vn}JifJ)(@GD0#x+R1iNk?Pw25lvm?DMf;Z`yi z7M*x+U%xgVVe~8?=?QZ0tbS&_C^*?U)>=l0t_m%r5I5!Nm)N6F%JFI z?gN>&AG7L;#+u?RNgOxJKI+Yl0AKQf)xkvP_^c^EH^TlfFHhI07O^;&$UB`j{1&E0 z28^_=>a>20u0b%n=yaQ!hvZbxI;tqt#?4VRfmwomzk%a( z>P9H_tBPsKi&x0@fI)!*cncd801YS`oM5XfPq{a~QX zj&jpeu0cM-E%kw(3=lTlp@AG8Efq91=A5U@`=8?gXhT;~6cQ-&lHg@o1^M6UFT=DK}km2MgU+n#QqW3|+ReT)8vqkUV0$VO1?nhW%z2 z>;O|gV+&=u3ulyxrd?I9zRdU#+oRSAT&N1qQF^8Q-u-1$AYnYvw*&z}C#Ud*DALo+ z33GWki!yfQjHiLPW}dP{8aPv<{@#g(xfT_M)kj!Zfnj}*9Fl7ZBhikSNR>}+8DgRS zQGbJ%3*HnYF$*nJ;tGijYW}G4RzLYN<92W$^T1@tT75wjf|Nv9nQ8dla97-XUx6uO zywSV&AXEv_Xq)pcpicB$CeM~%UkR5Xwt{T~87Za;Hz7!^(UlxO0-amcJKkiGQ|us> z58?hRR-%PMyH`vzKpm~Nv|H_ftWv`QdoQ+;0D}Y#)z-p5v)5FSQM2rni$pescZz<{ zy8@G;glLYypl0PmR!&ZN+$-1fdEgHy%BK?ymg$FqFBW9aj_Rkl`~Tn&#kpWo1Nv(I zjgMfmxPANSWLhog(1EX&igUxr;l)ckm`40FQ0^W?7}G_2ohxEAA9SVoS%wO}oO3Fw zo%(mCTIL62I}}3{V=>Pm`k9V8g<^) z4Bm@Nih3~BsqMp+v^^Q~f9Gmccmu7J=L(R>;^Npf&mFJXSO+@oXN8&qL*~fF zNL#pYjgH=e5$wM_IA25#R{^Ml2;1)#VuP4|AsUZOikwcp4roX?5sd#T%#z97#~Sq7 zVF(+5P)Il@804pTT=g}bzbJ$z6mnO?^ndUJ2Z3V|T=DfL|BBL~7QGfva2U)Q)32sH zl!%I2D;=c8SnzxZIAslbEeP~oY24{1Ux|jg|8%H$3E+J`O{5F1^XrtgFe!9_H`#-i zOaF;>?|?L5oTIE7&=_d8jVbE!p+(JqFc+swUfug4Q;-D9TC{|jW6tw`@JF5%Pu^0~ z@_*`KEu_V9AW7X+KMGX~L{3nEME0ti5fEZiq6YHYW$2ltotq=GQ$^x3@0;-24 z31+!aW`4X7RJWhg*>1ISM$uFbP@fx1R8bB)8!?{<>*(NJcmK0LJUKB1$~Idc=j8@U z2A|&J7;I(#W8GKfcxHhr(=7LG&sU(l&*`=gI#j(JNWQDPYMFdp5`;xW^rx|!_9q}N z|3pn$G6{cga26i16SyBG=lTA4sIyuqsWl0LjI6CdlAFxU20uGH^8hLrS%Jb7`3w%) zzpKVeasT&Ty|yKlI9DWj3=bKwypQZ}#Q@ra#fh+)8AXoY*BcubkCbClmt{YGo#cWTyM7JYJFWdBIzSXg*)560@_ylhem?3Kj8J+9~Xw-{i%k& z;XrTH(JQPBbUBNxUhQ3)2+9(`t8|+B?OL{e+G33mxX33PXPg&9MwaY%POUiS!6Au@ zi|@`!5m)t>o*8O$7M)%6ssPbYDWF_oh)7u>MS3Ejbbt>(@vh_YZnT$Q1A; z7vUfO$yQ2JYxkLgZ#QZ`E1oK?#obDa9b)*Q7c$R1zqwAJ5=3z&Ri zFkUvivor*bI{)sucS0-6V5DWByt_dpYb|K}dlGL;l7zY)JCy4M5oLmY7d(_xXWS|P z!MfanZKbSV=_)Bsh4~3hXIe;?$^^IK66AG?4j5CfsPUKR2G}A2Dj{EE<+-geWTLVC zuQU`ywa5|9F44Il5^3{l)yfo+^I*&o?6S&Px(Mz?nI_55$}*Gfv~t^O1TAl0w7k0r ziGs24keMxcj~6$)5wSrnwJ4|t^%S$%-o*w<;u2*266IucVAt3ByAiiuT(0g`!nL>L zg(&kwLDQWf2DWd$&|JRpF@ePriwiW?$+NMPF=2bTT!M`}43LD1*5R)J*29$jNE_6T ztB41e-w($e;vQtP73T1g8ahYKx}^o(-+R=y#uC|1!{s8lqnbh^ymwjdu9XFxHY@4x z*Nf$aiL-!}rOFi?JpN{fnc+H@cBJe-L=UwtY1wE^%+Cx+wFcqf7zBD@y#Fv>3ku`; ze+m?oIrpAO7>hEf*muK0vRe%Bzr+f&JFN$}*kzFM|0o1?))b)-Z>xLDyn4Ofht%?( z(ee1E5w?v&4lgg3K06hY4OJQBd?2G*2zhjGHeDWqXT7OYr`aa`!B`;LQiZRKaTJE_ z=eKJt@I1)=2Wk%WGOKiGBITfzR|wh*vOf}Wni+>8D=oR;*6H8#6%O~;kJChxHlUUl zZ{hWUpE|nNbeGNiTx~Kz!9 zY>kGFNyJzlBV)^rQMTbhNx+K;8mmdBsqcKl*z`URloFonMTHpaM-LSKh&f5ID}_MU zYs}`|o9t_(aJU@dMCAOdo-Fp_R)XaoV~prB_S);-i3HAvv?j%i%<9m6N3G=E1xGwVsLp&SYkCa!7D61o>IgZ@Ia6D+ql5mFf+DX|5EYrV(EU9Ua|0o|(&o z$60vGz<nyUX?=+RETCyWSYj5ay>bII&+LjXX+=Vq)IcpKZlPM9tgvS z7A>af*Fa^wpeMAB>r3wiQ;D+XRPNU~$>NYV1RSOsA)Z}BpZHCo?U_P8u=7C~RmXlODRh>4ma|H$*5iBSKMf%eA=EkEcycLBrlocv z3RY*QJ1xk~XfQET=U|F^n?iapzqS?xxYii-uR;KIunxwHF#x;wz65cm-sShfxi&GdecNxY7T#fo;2(RLx?bDNyjk~=EiBI_p%ShZ_gz0GZzJ?K#+_4|af{h08PW-1mJ z;SY(OuTFY*bV_h>+jz5U^*z*mK8LdXCPSaRE?QGWFrK9+rf<;>3%9=oKa(l5=sgz`3Lc#1e&Y74hCwI4L=r=Scg>-TwJ<6m! zC~(+K<|qy3vG-B-?LSez?-<+W#wGGx!mRefMJZ3oqxnX6fD;Bi8IS{ z*y{Dg<-5Nul6iPCom~NMt<7_Eikam_x4ES72W1{Lu^$5Rrk_v=;1h36JE_(;BP7@F zYv}OdwJaK)Qy6sf{Fz9Nha+$fT zOpe)-V0#gsVUh#+Y?D<8`eJ7@qUc(MQ%g8Efswgr@RDn!s9P1NY zZ|=$0p8?*2QVMF)j6X%bKJO^4ve9&ogSP{>SCcnNdU{bu6Dj+z2Qhsgz*jRlb!x&e zL8m0G@cD%EI&VZoM7gQt?k?#+^6+#2Nio(2o+$v%i4G~TG>4=G{`%aStkkukAxr6r+6dkmiCWb2G){t9?cEl4i10jk9pN( zGWrrHtSIiO^1CuCCf&@KCe1DWW-A*i6Rx&8q1(580yce5wpJU>=}Y7@?Pb;EcjL-# zc!=XSMa~D1iLEd37kWOC7+d+3JDST!rzT8arQJ4hz?ri7+y-4&^x@ru2Stb@z=!3F zPlS!tF}6Jhnp(dyD$AaW_PU{o`Y#;1O{E7zRfG#zqy5>5r-$6tWm~}l;nOF{l?fJYP6r#!%mj2uALpVsCn$Xoio8sI z8_mfK!Xgwf$}D}ro@HRzqdBK%l=SX_7;Fw__l<{*KXsOnVwl#CNhsEHiN}oHFA@Nd zI{l#_58*;2iODwKve1p+jBSp@+iXX~ulw}|C-Ag7V7bjO6s)JX>v4Hk8ke1DN)ZlJ z7lUf*{jR%{D?PoDFL$$DA+l3dz3Y$s9>Pq0`|NPL7RMY5Jt*^SH2QsBfS(px1cx0k zk~CIySR=?xNrmjrb-g!KZ0sXErc~c!|Cpiz>(YnKIgFz$$Q6NN&&Ip zdT26TlbX1V3Bb1bNz^8uk?6@jVmfw^**esQbK0zsR`cK zRINhqYBRVtvXH~FcIzsYnI89@Jna%$W;Np?KEu4lm2i?yM<~IpnN+Wpc}Au=ee`3& zQ9L8KgZ?P7~6W;adc}Om;YDu2d z%Jk_$E1kMXYmm$`8$p&O0w>z)+B z?^}^$ePC{cQ-9sa?y;`J$Zp?1qooSY)!dZ{<_-wTzsBlW2RhA`#=VDqp~M4(En8+4 zN^~(rfv2Zho4zAkII}&(SWE_i2Qh-<;t%sXQvUI(T8SVxYgDg#fhHCFQi#NVn5hAS z3N70K&y7_d_ZtqUi^UD$rr(!s1h>dJ1WH<0b5*(px4k|JT~dzzcd!^>IC;%RSXOmq z*TI$^XlTkw*jjyK?h4SDlm7^+x845pD-a16sTcYHn*kC>==eX9iAoh zr~x$^2PurY0pb8tIFj+-L|(h?Px#S{1-dKMqX;;Ni3wL09rpc>Ag>d*j-Yi^Ic@%8{R55BhA5)q5T&~cw9Hw9Ph_`>sBMf8(adVN=M_qhHrK^CQY`|{_L;)7dqxY zC=zZ$>#Y$h4kDsA)2GhIv|qO|zYU3gmv$rq_^dVo%;3!bgR8mvHg(msm?_sb!Nkw0 zi71)bBA}%&9`&IvVrvEhN?oayUMVAr<*>+OY0mCh5Q35hF-WiDC=TuE{zTFXeEv}; zV-zV&BKO=*3B>Cwu$Z|PIa0-@T6r_H!8)Y4qC}?v6x}&nNX5rqQ2lJibozUGz15zUn{&YDsQ$B8rfbz<$7gcJv((0-g4a1i^m^h3oB&$} z0cv*g?J&OB=rH#KmdOg>!zHdUW%9M-zViw2tDsbM3e)P+$Z#m|w>MxG&w z9W34b1Ko1^oMx^HyfPu1MPV@LcMxR6M?1IEtf8RxKWz;<3`_oc!i0Bp{{Qv%=`B`q~Jr zF%UtB9tyBN+-tYk@m6Akdsv&HO_8fuB}k4gm&uGfnvlRkbVcvZ2(3zt+|3@=CPGxZp46W`{^xP(N^H#}nGG;LxV zu@#8MBy+1Acw32!tL^U&JR?`jT!t!bm;>K6*g7{Nh{bu|l zTuBA4(ix075!nIaS-n{GEqg!hcg>};I@GhimUEtOS3D;1h9KTl=&;LiphhUnG)fZ3 z$k^M~EZtN{LnpYS##esW%g{mWa%3H=FZqzwPi<62I+kJeOUe8ijBF_=hF(nXE3qpA zB?kuglEnMDHaJJOQ7-SI|c)>;7|41 zuVu1j4=?QnKYTxYQBCg4xCSu}NEERzGiT#lHt#}r6gX$(#5L5Yt2YWN`m|#rrqXLB zC&7B)_e`QDIfpF0G~g+I#d4c20}!k6h`z>S$GGk<6sqxAbJOXykTjiqff~a<(&aqltgbiu^>52n72M)xTPA8UQAh^`9*|^M@mqZq=K^1G7|H;o6uCy9su@ zAELFZeSm3|Cf}bg+*ZJ>FWjwm*8{rA?VtJ~00tXOf&cqo0Lk+rEjpLQpG=R05n0vX zPzbwxk#4WR#7_mjcaCnUSRyJ$AFqPqUG|@oz5OBZ@d*imz|^&r3mFyqSyY@IZRigQ z-{%{eeVtg15g_Af#k*2}%p^P)Ncl$)8K|OZW#0ey=pV~br0|7x*VRFqZ=tD54ftD7 zwNYz2?oPAWPIw3IMP~j=`g5$K-4B*KbL(yi;jm^qI1R}z&iRr>{C*=-uNr?$zZ>rd2M5vJ&L|__pj}fW4*f1VBC!6;DJvlyQNo*m*vyqQHvB{xT zf_k5=|(gVWRJnK`o9 z^Q`}B&IDJG|vV51Iygn4+h$Ib1+jym?>$r5tI|iW&I7nnjkqym&Sko&%qP-ZHstJeEms zf*!EmE}Rty;dPN2BAbtLx<>OdJgmZ3i?@W;+ieS~f7>wF_ZfzddI>}a>d+AVwrSx?e*C8{`)VS@9 zdDTj^doLV!vdZmz*=V;nNO#vY%S*q>IxUH$rga~-@fa`xH0O!nE8}+nOGC|O+bmFw zEt^mQVCNBmv-9;Rn==!4d(;2@k^(=dPs6}3tT>)~yyWN4pAlLO=Gb*F?>pb|`d!}G zfpZoYE!*BUkYi%=;ruQuK2m79dKMu2K!e{N%LCzLIQM_HQWEr!Yc z`dL*R^ay)!?WMKGA_zv~zsY_l@Ir~%a1>|K%K3D@aFVF#5SuMHX}YcqjB2%o8a%qreXvX2W6h zO8;y!5}Q98JRjw@HxlF>1j*bV3kC*>&ETWAL6Ec==`j@0OvF4rMZSw?{sP0@LSzQA z{l3A`R)CSi5~*B0bx&-pS39lNMa;~7BZ@`B~QSByggqhfFpADO43>6`!#FPFfd^=8;fe8j%xrC za=1S}fc`d*E8SQew($yXdBI8o<)}uUbk+C)B!SJ)pL)rzC12ILZS%JA#Z15*Hr8+) z?g;rQBo@0Z*4I-@50_L{m1nIH?`M@mQV%KG9|Zx>4KKn$Q?%)k%MEF2dfR+%G;*sA zrG7sSZG!fm*ZHNYw_m3(ZFHwSj(ezN7r$mf!T_{o4jQ85!^M_sIR9!kA}gNoxm1>- z)dtyE>~xB*U66VQ;w*k)(uHTw_a9LjgqN3K5>JJzRkp37Y)`RnoX-*+&@5vnVwJq?XQ_8JOO1P+nADSHMyx)6`5^{VU|{u8aRK;NI!eo+3YCxrgD2;1*_Ab zLa7olFI0av==}Cn#2_zN8V^9xG47dvn=tt5$4LECfZ#@kuj{epwhFxEvE22Rb0n_U z+gl*=rlzJ^nC&Z98p>V}nDu5Sxj=ynH6v)+1xs2-F<5+k=7>$?$DFK|Vn^VPIWOeyPi;Jy-ce663Nv#gJ~mp0pXAu0n_e;*Oj`Sh zGWMiy@p?i1zHC7%c0PYQg0cL**;?f_r&|_yrfKK%EtlHSP>n7z2`^k4D$jO(9H}7w zpdum?u77K@SsZ31yV&TVo6O%62VVv(O(dlc>m95E+WwT+ zSaBH-gt%8Jl)1MI+r0&OtDqx^-=q4S&eIpVY;cpCBG_ipH=AZk@R_$LTB(7b_j>P{$ zRPrWynA9cGdxT}61~o-pipLHim8<1nFULcB=!8jHsF486s~Xj%i>Hy<9Jd07{$~wD z!6IJYm33vc}XEKCMUYIk8V zn#qDD6@edkacxd&$NG%ncdAr2v{s#p8HvA&YxnJpt-k$+WLvzru)mp-g<+FHsDPo2 znygKtBL?A7zYMZBHCkfi1LrQA7ez9kXA0zkc76V{klsb*lIO08!`aaL$)`H2KslEFekvq z!>?5?&)@m1wMwIQxG;i1+e~zgB)D8`>9vHP6U2E=?5B_*F>a(P4z~C+5FnDGF5r(s z^SBS3p7jB_CD!H|WaLx`*i;Vfrxa-N%E8<{IrBPohM+(w2R@$P!<_ghP$NS*&fV23 zrUHm2O-wR+=!at*+;Hfz6O+u^kSd zGpXPc?VDhX=GpdVvr~dO%J5!#UFCg3v;EwKOPxnt_91a*{VY%SX#;$wSC8*d~61p)!8fi)qY? z#g1z7I{%66N=pJD+=}kP?Gf?NJ>q;9#wUjVs7VyzDj+Olz%ki!5_T2h-4C7U7KquDONzxijHsx7>C*%gPgPMzz{to-=>9-qkd{0DB&gI)na<2=~um# zvMKSN<6S>YkS?N(J^&2;h?I2`@% z0x?iWd-jy&)1?M(HFs^eV9-*SeKmJ3bq0qohboz0if5u}LDGQ*`8h6+5e(nBIpy=S zBn%_e@v0^3Is0C#Cl4;%*=QpoQDQx?@_x%xq-3;d21bixYz-J|_1L-})O?RlW1-3D zkNpd=M1A==i^Os>*1s`W6rsM7$ObcC?w&c^OUW(rGA+@A#+-%~fFz{c6`xBrx8-`X zbDD{j&bEdIT~B6FyWaM$K=8BCGAk?o`ubmdO86TOZoxvkVuy~hLUEa}#kaTRG2U$Qa2)t9=#uE!<90btZich6=Xok|f%?$ZsR39$b1oRp%9bO&l* z-4`{=Z}PB0%$IhVjs=~cp)f+}@c)mc^`HIbruwH$8qnAdwB?Acszs5HIur5!=bPs} zA5u2sLU#YpH}9@y3{#H>t?-|3e#ZN+D$pc=|DSJOFHJKT=$lXZ&o|$oPp1a-&5!)& zo7W&uCj$EB|F6Hk*Zw-S)b7E}vX(tt~);CuR5a*f}f7Jw9^VGCB7c#DZdthDVmy1#q=Xh9-U;n6Y;bqBT zciF7B)1&JV{X5~-4ET@wYOODNZ9Fo1Oy;6ZUyT-Eat7WJ3USTbu*<{ybW|4Ay z2Cb~K3^jYk*wps$;M(xo5Z_xdjqqZ!8+i61-%pLCdR+dzdB~JKB28}++9 z(3P2hx{6oE<1J?424J>)d}3t!^C_?^)GD?Fr0;JyhlhgKxZ3-9e#W(a`BA=Zsf&ab zoF#HIX>RPDp9tVj7Sq2zex>W!Gb;yAU#F@P2?E=pFdE9jnlt$8Bn@RUKhBLYvIN}C^ zfTX?b?t-UUe4%haCAOH1}{4`$iyb_Jxr$hnYkSVtRQ zfsnL0$x&`X4L?cc7Q`&=sgOq2Kx@OAX}_DZABS;8$_4W0JOP@gYNZkeI^CSLr50Bim0}1IsPUTH{TRmmJfACOnmBp96)8gi^iUdq1$azpwA0oV&g~s{W8MWuU?_78x&I=z#B-72grPuM zx+vR}By2^a5eRJKS+kwz3lNt|uqdOg8@@nF>?ZbyAb`YnUF#L+88{xlijQw6wh;Is zUdP4%a2K?GW`B9T=i+>-G@0zB*4Yc29BUkTyc3+dWV-=PkflXE@{S?!sHTY9MVewt^hEnirnlAK#nLOJ9DCmET{>;!@V ze~!cXpX4`of+{gX_lvQM=FNZolXX}UP8`(_CbqEL8ej0S7eP2lL~YW2;cA_aLub|-a6iQnXQV7Hi=R&KlWXgd z-0t?$1pj=T>p}4Kc^!aT$xTi^gmqr3Z*#N6)yiF=b9^;UfF>~2Lf&8bF%4;V6j^PjIG_L`7R zw%*FA7W=Jr$t7tCcEPn%)9Q@RKN_|Ai8!Q9I-kORQVsq~TzR!Re@y$Ze`_xy2LRBR z3jdMiEG8(tpCbsFT-ncrzoawo|WC^5*JxYQVl;;@;VpN$6%|5)CFf}3)-Z0>I_ z+Y7{e;go-VV<@zr-WlCw@CQZin?AkwnC&g4gV9jbfe*`P0jmCAxwM>>5m0hoo$)f> zxmt{zFmla4tia|66C@Qxi|>tF`h-)@-jUCLhl7LJjGT5~xfOb%Xm))1RF7p2=_FXa z01-T=>26Tl5&a5Y5as8vyf2X`d9p-T9jL7nzctVvBvCZO(8yuBB}Kj3lvO}hDoU7t znVlR-pSEhcxPdgoD&u}E*@uKJ^Ku%bvU94j_i1w> zf*LKM;dt9s$AT45BFw2tA^b6!X=hJ1v%4}mgc3A;;+c-oHfFy`MqbGY%3PXMGNq6q zqUY!&T5zxbjBqtFl(^JBXzh1)_}>eqR@d$y#X;cj%$~h=l)dPG333I_GMH{^T`m=f zrG#$C91_!#2}lIef<+-NpKQqoiAz`-dm*?u3(>rB@5hs{A-wB}qN7y2Isnq}9o3n`74kuTmRox#D&hw7z3d+Tc(gOw^G zsZ7xP-1W>6o#w}yb*HeGM++`@l;fa9!c)8Hk3vL)LK|+QGk*-u)>uozll^uG@53v1 z`JrD00|I8fMzIKHQY*QFr9=4{QQW}(vYxal5`t}?G18ybGIE`H=pLz(@m(_q_jgYu zZBn^}n%a{2I`Vtv7LSa2nW~Tu5@;)B>woaEthVMpUer{R5`6WB$la*QK`#f@qrH;F)mzy{*%M*4{xJ=SMR900G6K6ynG%_p&Z=3FFV5Zp;LUneQ41+%Bx3rP&GX@dl}z74-^Xvq1T9o1q0iAh)lWg}uI zr{v#~CeGhJR_)7gu3yQjC>V2iKb~9Bv)v~LO2UwgZ<}ZSlGw0x>%78u>pa1}HSrN# zs)Y9*)9a`$aZc`A>S=2S*I48SnRmS__frYo#><+@W=A79^u#%QdsC7WnasV{D^b;0 z63#!Dn_Q32(8h$_F;C|e7c(4gs!K9 z@EDU3r)3KkNSpMYNWhN=Hxf5OSR3Ehy6lb-0zHEHk`qz@@pWxtZ{2rV( zB(61!UWwfIW|GqA_de~v>BGMm9$SH(six?zr=jdEdI|XXvO(7oJG;^FalHmREX(;c z#En_eBv*=otD368$Cwwrg9xuwWc4zPvBy_J<6LUM}W)r$iLU2Srkt5TgMP~1%&1n>XFqgOPIt2W0BSK2gKv0kE~+`^1%w}EZ+ zWKHK*kuco=XyJ=j;s_(?uAZe;MMRZZq!*f5f44Ha$yN!J=8Y&0QF*_k{i_dXY`vxc zh!B|iK5+S}Sdfq?^Q!!S5zt_)V-ia)6h9;}q#I=z6x*>cGUG+{K$IR1qTg4PW_q=< zA^2)wLNl-%9l(_+DbleWu=OhcCEggPrKWRoczm4A#}kqv77_fp?x3^*YW@12-&fgu zAM*?4AWF>D6weN0<0X7*6a-QSY(vCYR|XM%?E*{e;n$yHCC%ba-)av7BhcjareiBE zhU)hla8E4`uvq0Qq$>BlB3ISl*>;w8=>3^eTF*!z;SqJexsEkhc^t^p$O#l#JqS=G zDW%xcyN8QxH0~-r;GQ`k3EdT!TCd(+ALQc)evUMah~*}8zM&6@EL;`<9{`{7gujhR)^1kn@stI#Nf7PvxE#~0 zgX2ax`&vI(P94j36Gj?w->zEU-1UO?(=Udoo-mCCI~HEpjL@Yy!_FQtT-579a+48- ztsA~HbNO)>hoy}DTsFg$>&w$j%?uA_TxR|LjXAnZd`24QEk0doPWnVOr%o^q?Ap|b zuM^%Y)|Dn9fg#p|OTe8WoX3f2{%L%6)c!}$pwSYA|MNk`#Wzrj z>_$SOtFqx_RM^gurX2AKkDka$6}LnhFze*dCx6{!;;d%>bg}jV+;>j?g}l=^UX&!G zByzfXlxnq{g#xs%FifqdYAVO)uoJuUW3SZAtl#Rsa^GTzKf_XLvHH@f8f$=$|Y&y4{{Ktlowa7*ZP{~p`xZ4E==HyPN&R|%nS-i z%uj=1d@Va28>;OAO-RU1Ho{$q;wQX+e!7Te_t1@qRVeT0d*ngyd4_R5nkq*)pg?an z^061y8Lvt0(YKD2#ZVToZ0^%IzjqXV=PXf{NS!TZ^W#8}H(Nj@c;yjo!9ohZ1<8?u zNJqJJMUMaWQ!7s34&}ZQ`>AIdFJx-C0IKhOO77APa2d9?wWR~rLI?|ECFzzHW|!gB z)76&?OHuJ$M_BNL{F`|B75KeNN^ag-*V|ob)lTc#CT|a?N28))bM^<^tB1fI7+ZPS z=g0kwct3C&0s-9ibAcm|iyQ;a7TNG&i|w`z{2tX7tkOoBR`hL}y;tUbtpC*l@mSIG zz)nmeLq$U~0YX>C%SdsKC$mg?B^qf0Mk)u+J;Ro&G;ke(OxbHW&JlygDerGe7x>_K z-_(UR+I4+>icxg6?S z`@F5zg?2YOxvpN}Q96pz*fy?mXpxP#p18CdsY|%;H#=JG5z*aR@HRWH3BZ)H4vp=@?jLbRU^W0!ci*%jl6WFF?COrYdmF>C9#0_)X z3hY*ww3hl@=d-x0xYPPi0S(f@ea7pJR!6I#_?Bcz2J#~$0Fen8h7S3)*8nCJrfkQ28(a0%510DJW96J8KH+C!O z2U1F{n$rUg55JRYY+DaJYJb@zI-)SCTlgm+z#S1Y1JYh$f7M#zUSjLCNr*#igh9m( zd+=)`^L<=wPY4-*nS2jMFIR%B=y_JsVBY3NoH;}MU}YN_UQCpgp*(2Bsy?~L#xsf8 zizhZj5p1;N56@mp?R>`OLA0>zcV#zy)|M${FW9H32b;fd%}Jl`VL;H-Ko5#z37>!z z?RJf~z9m>n%_%UqKqX_S5;h}PO z#A3^)wGg9e=X^h2ZpHu1Rh)CK;ZewD8tzx#*BJC{N8Tx;xk5$fEJB0EmmpFK%$58i zOwG5P^Qw`Sc|xs(=T5)+Uh6>YtpHSiPRV*x!Vr@|pn6LHB8j45gR5cL(S5FM9IArN z{+v5V=yhl+{_H3UE+Uak+wN#|m2^<~y5GvkkflhI#rdb7IqCDf<$aWUo#$fDbph_j zm-z(cuR=p|&oV2Nd>WK(SMba(EpA?bMll~ku(}E*t<&kQVE`?e8VJn|!5EBpZd%YN zMu_1n&Snz?A{Xx&T>N&Vif6Lynz_RO3XzRLo~~<(el6`)dbYuas8N{9Ug=738%*=VN|X8QBTF6niF+Mn8^#PnFAi{;9 z!#64nPRlq<-;;=tluaJ(Cg=3$7s+!JrFu z2vSl~(xnBeu4k3y0OzSEmHL8vob-V^80S=8O{h~b>X^qIfkR*N!qCwN<4n4UBVeuq zq}RDwIW=)mZO0Ocb7)SCP?LCGYgn|0Jrd4k@H=Y&+0R?Ce$2ZMv@;6uTLuNiX3MO3k$J!0GS(kge06r2NK5V)M;W*8yN<3+p&H- zJ`&ZfV~;xt=p=Gp+ zBXgoI6r*nlYPj0sVyCd9O?|Wo zg#s@RS(&z0vly_f8cVx^KP6>lRd=nVWdMKgBIXf+%CRD)jL!me(aK=EvQQE5i&zbF zBFS~s4Sx003v+*m9@_8d?er5n|CyF?s+m~&LoEkdtH2MnrV>CaR0RMu`evLI`w$kK zZWR^otAzupg+aP6Y-_;L&%mua`$#p9EW(zz^Xe7ptAGsKC8#j z%7WARUL>3eTtaeq%`~-OFLA|Gd7(&QcURKkEs%_fiC)Z;h4LpNb0+B@AG`@;-i7SG zF8*{Gihm_-+l714Ew&(4|QeYU@Z0?lqrpri>?!8SMgyO|QSKgcdw zdopBoqRkQ*G_-zfd&J&J9<32Pzhx!%N>)ahRj)lWf2}`vFPcs6X4+X!JJd%F9R+{; z)2??CFI+iS6YL;8*05R(Y@Qvx6F4A=N8(jkG)`U`!L@Y`f6rE*{bsS4&Vd8?9ARId zCROK<@~JHEZS$HgNuCXI;Rho#7&6(JQFL!6(RQHvExvU%^8P>-@w@38bHXBrYl1-h zhfif@ICc^L(-c0XaFXKYkR3acXb&5jjzaM!D{DEHbfFI3f0DBSex@DkTHip;LdSbJ z>yeppL%6`uk#VGfK*d=RM&>(n0T>E$W!At8<=>HE5uBAZGkKsdkdG3HF7g}Zv8Aln zNU@i;uA=v_sKXh8Q|$ss(nlDK4q=H)*DH$U?&|ktW@SMJ^<5h(6T;UJ3iZXV{WfZB zC6JP%sFG066;mnbqc*&K4v|?7iagQsS!0nY3cirGyV}LqRJHDM)&Kf2(YOvXCv?Y* zmWnDCD13QvY^PEXs@3&X!vRSW!d>jM;Z(oCDynXW897ynJn@93G7+I-SrI_z91YO7 z^Wi)s%W;AV9<0-&qcJa|lbt|)SJx9RSapOQwEu_QZ>l~{GrvomrFIX$ba*SPvEcXe z_>=YkK+5xD2qu%rLfejL$0ck z$sMt5TM$7UeTn%?@0Z8)0s0Wfr&A(A82ZD# z7vM)oOuVmYEBT75B&0V-L4^`P_<=UStxww1g2403UnI~0!h0kyN)H>Z?x>eipN)x( z8b)U@^E^?^6`a5ytEyZs#2}kplK0#9CDO>FwpwW{eaLmIwnu-V932HvkFIj4q>}DCv*(HwEq79<^ zxu0rJ6(iTmhDz$`fr2k-$9>D}utV9+q#jRrb0*jvko=b>i2~P1D1!((844qu5DB<( z)g1f`6WfbF5nwUHrRB(OM8VhLVr#tQYuMZ(-2ApxdPgt=NqpEbFFY}gK=F5NeqBgH zAQM&ztcN{`I8SRsGE`5kVA2WB57`tj>1?Z;rv#feM-lbT+CNxcBvtMSXI))69mG-5 zY;iQ~tyxUb!Gf?)@Dqe%WgTie-%r)W5OZsnO~PQZgZ?JU!ZCP%IiQJ4n8XPiUu@wJ z>))>>2a^6iK5q z95mDn21ju}<>GvKurXgYe`P;r!>w~xD6s4;a#NxY2YYW&SGSirjjZFa!6pIYF%sRU zzj>z4rKrtm|6*oYSMFb>tkTEsH(T>BqO(7z1`&Km?;|l-9(wTE1ud=jR2Q`h5!@r_ z7sEI575O1COiZp-vUQuOEHExES23?FV3GlamM8>S1!o4{Y@6?l&T8geoU)egc!eH6 z+aFY~TS~d7J==^&x~O`BFk%Ju*(-E4ql5bylj3Rvqt*8*j@5_#7|He1grh#^D}J_NLXo>v|AU+ehg=Cb=;Cedtp~ckHH8eb)tF! zn>1Ua<24fh6BHM2_HzFfqCXUZnr5K5Y7wSdZ8Bs1#5Rtwq8L(XeDpUc__>KvgmDG?OQd;VhN~pyO%4}&E@O$-$Cr`dtk8_ z@~SrIr2TMpMfhtm&6X5v{1->jHM#w;(|K(*M-y_{W$DKOGHFMmivT`C_LCyIb=sWG zCq0~2M63k;7a-shtBg7u@JBDyL%N0)I}!(WOlyXlU;RsE$5k|Amw!l+tdUK|(9jq-yed zCFkTPcJLX1ciZv!Cd-_vYvprxf1J|hSGCB!Vj%9>WUr0Aa1~!9hS0Wsx2cl9A5(RR zEZgshMbBx^4i_O4UK5ikXRQof6{o$6ss!6Vw;fZkw@RiEzw}P+T>Sr5g6`MP72r~z z%2!ZWW~owgBJJGN!M=ykEPm{EVc6rIm2DgL#mOFpSB6{1GL&DT?>>3|x_PVfhx6Ja zp?6*`ZZdeCJsC^9;+@5sS@mqtT{hFt&P|2^!`H@y1ZwOr!r!My(wF`e_pn`MA9(6r z?*!iksP(0IZ7r}qG|)^`u>c`NZ|zakyBM0RPC9OGal)Wesd2)~#dV5Z!S_@jN^Tjf zkT@Y=ICMB6fOc8ApyEW}ar`Xd5=ZJtJ2vsPV4hcH4;v&-B3MoR6pQiN_?+QfV2Ry$ z(fWg>iuzVwn~!Oh%OOUJ8O%c%aK%*K_3@n-tBU??4Cc-|H4XNlrGPg3FBKe` z!HIrGF20HWDRXO3&C3Q2zm=zR8_dI3K2WrBpJBC5kEWe?{BmhD>7Za4LpqL}hfT<{^7I55~0swR(iW6FnIO33!S4am=$%b2EG++zvl z{?WH65Y3T`@mE&=J{whheo*w{)r%3)o}VgWRy_dd zE-*W59WD>7)GDdfrM<4t`TT3!^kS0sOW-Ab)Ik%ItWcWPiM*SA6>2QmcTRBqVgJpe zA+>%{P5D+vss=~Xq+zzBpaU&p6_hN7=W}cav z8BYNz+Weczq2~qm@$YU)CKFzOF|xgGa?QMNfHfV7w1R3)Jjla^80FA|lk{-u_YAQ} zcxy}7@Hs%-uAQ%>M}II1-Cp%$uDJ=?@Gz z8UM?Sk1}UK^O@`o)h@t$Klv*SNu9q6@F&m9Qz5ai*wP-axK=e-RarUNc}5afe1A~> zOhru}f29+hY0dXuXJeNTD=@h+7rMuh<-)nf{PtKNl1J*V%qT;Tuu;xz6=SI16oqNb zMqux~L_c{!GN+eiE)SF6X`?le8q%epQ0pQ@05i7mh*T0-FQR#KT0`d)Z!-WsA_}AE z*iisCu>e~~(V(aob1%TlPKBx^#^Bp9gHX5FZJMsaJR8Hk1(E!hG1UbfD7( ziJ)ua1S7hAUxB7d|HFkwm*eSHpgkbXVGfunDCz}6iS=J{+0U`I=$+_VXl|7<%^pc# zAzha#HXtB_unriN<$AG-GLcR{!1ugajFluGJ{DLuZ~zI1x!ZnTLwoOF!e8MXZ?-2%5R={eW?1T7N=xV)5hMB6X zd%=qZJIl_3=LE}Zway42dxdBA2|*1;2%0>dm6Du{Zd4KfR}O-~+wzz#$TB^LHY+o; zg12b~1)56^IWcJ==3 zU&8;%Vtn06U`&(<9~!{N`qE^xLZh$` zOh{8MV3Na|aD2$;_v3Dkx{d@o0}g;;X{vw$pp>*^ACo~hvg4{!slO_Xrf`h*(<#?$YIZi6!gcSt!-e!24 zU7!k0*?PlCF-qtI$+RibJn`d|;hxj2c))sx%*4k zoC_ikZSExw zAcCMD6ei7K@VV`aw;!CM4DZNw*l1y74V4uxWE}f@>MF{nsPz;57EKAzo0p3Nb(vqD&2v?Eps0y3pBWd4-3D9 z2><~CPMR5Bl_N7jS)#}PbG$du>orzZIw~xA7#NSJnw!d0aR-mbsR;UWLtbz!_oRR_Ueheew3)T04@g*%z{2ZJZ^}Ui4&GGGAWs(os zlJB;9E55fbik8c~%=wKv8l!(nf@mD9OkQ%F+y8O%)r@zMF{D}Eh@`wLm}VlW3bGzk zs(rG-pI z%rE?`tbLxWSKv5F-3bEy?1+^Fm3K7N^!L~2d(~aVf=UG7PjPJvDpEWk;FDO7h={Ou-MPKj#A)sxTzyng^UKqmh~1 zEVW^8_+~WQn^0G+MYho`6o2YIkTn9-1 zSqP-3y&h(>8HVvu)Frd1y6ZAqVjo{QJq&uDaib6SI#p|4a58i`#|;0wZKBf_cvh_S z&|WCuu60F6Ps>eepwmt6SPMyw1#PopefB6hHMIo31Ojj2N@?eg?|k7oLIKnnhCGbk zVJf&MuuU%q3ew#&A-Njw<=^;4!K5$M;~;UeVXQ1XC;ccRP2lz6aTGJUm~OWtVQ*vWu{ zh5~se4>jScuRiM!o`JPp0i_~S2=Ft_hhK^?9jFLj-o6KeE*sD)sH8{m?44BxP|RSR z)L3!NbITO$^TY|GepHMufDoY`g@2^qnXK(OR{(YME`*o=c#49KD3&R@3Ga-!1lx-| z3{zrXa*L_B^F4x%QAf;{fd!IRQOzi-0z^7T*FX%7mZxi4X9X4s2@|3RnmdgzP}2r! z=AwwZPf-7r=T(vG3THY;GT(D-a$MpyqBRcpYN5!loz3=B5nX{-IA@q17bH4-uc9fE zeH00X_6nX8YefcGncW2fSy~*If`Y=aC{xFAW1N8Wvu{=+&CISuYUNX^=Rj9D3PGk+ zWuflsQqf=gj|Or<%R->?AuMFFI1&4)vTSVD^xQ6iVgO#Hqkky;mQ)mf5UfS(zaxTlncJQ?Q8sxj;bGmdPjdew|U+ zz*@pCrU0v-GbzqoIhBGYCBYCeoA)8N05-@oDNqE=N3xvhGV0lNs)D|vrqBKMp5I`P zC4o>e-l*~x3+TWZ`mEvHy1&yF@Nar6_g1-KZl)B3lm(UvE~xbSMFprV0Tp>fijGLZ zh`+&pgUKm?UWM9Q_f~Ld))gitCCMp}>lOcZc7vke2Ho$li*{wXAku{Ys^sjS!f@yW1lyI|(lxSl(V{(CMPQq_xpqZz`-J>w<))EOUAc+u2 z#0Z8=)d~E`PS78AuX1w)bIHU&Avx4>Odm(}UL=C&jdMVN?me@A=gj@ejDIqFu5K4} zdWs+GuXsusTJv%8Hsh*sl9#_ds%fsqFSWUFY9j|)OGPvt3Y(&`_hH6dZA9#djvgaI zJYdBcW}m?cPks9wmLQ4Y?!;hvngtbat&ALZsN95OmMA_zOIKMF}Dt{&iy_u+>n9WUgU;IVtXQi{b2&0F5C- zM4cLyAPfm5dpM}qMUuAihw*tp-fOS^LMrlHGuSsB)zBQY!L+Np z@?1k5*cD#6l~IOswALjO-OU(=Vf*(T`BaF7PERn7VW^2rF~4Z(^t+RaTw*?zTb%t! zht@3;F|iV77LvF$x=FN(ggNmZ9HXbzIW~6#lZ9Rs{jty^Bf` zGxWV-Ao5U1#i^P?=V2$xIGnf@j?~|Q23WS&^d0HkWt2bRV5}-HgyZ7FDZh|s|ML0P zwj&p|=tk`?%KqzmLixBrOo~L{x26`og{6}%43rj=F9f#(hsT8s3STo z&86^`^z{tH;L^1&yefe(+ho3)>Sg~iO^|KGGtfwFU9$S@0kj!+kXt~uwY`&&h_mMXZN6!UuW0L5}-J2 zdL+a@U=1$<4*43heQ`dxDT(gmvY;JdB<60YKa6kWCkhfY42?hn;kxH5pY~^5UD7>6 z+~E+b81XQ1ETCzj8ZSl}gsKfH`^s>3OGE{ahc$GaAnm(f%}d>pIjQW($jGSbP8KUu z0MzvKa-b0Rq7o7zJE=s;$;pCN2S5QpzZ7L|3E2pvsbW)28HI<~VE#1@$De-^gDvD&utul-5F0pD>*%f+hY6s++$PBSHv6sl9 zZN@Q@3}vf^AR)C23>vKBI8Hop7cU2b#KYvF*meLY&auv%wgSE2@M&NqE+j-M`siZL z9uZZwUI(U5_5(=B|7QWT*sH-4;^J2EMiRPAGFvx?5Da9+7cl#Oc-n4vYE?3RP#`PS zueLbk(Z+fv^{6H!)9G1oN{z7<(aYb<)YNJXY(u|H1 z1!Hcl{Hvqx^Qs>V$wQ(XthiepUl@tlxc_8EJ(^xg6#m21e2~dgJBfjNbt-1TG$corNCMvm0D%-J_?PJrYZa>I*C*Qa0g5qh4ps@t|Pv%iH~N`X+f+n zu?vPXqsr{C0oa>h&i6^Li?q-dV~y$_w|(eZqyi z;W@^-ZyWwEF;y?;-eQK9sXcUKVCf|o`)P4n(5Y4h&*CHFjQ~wABs9jckH!#H3~O(7 zHE;ZK>RDa%)rTiKh!?2d+c%$^6c^UqHprW#EMztyj$Q<69aU>ma*5xd1g?j68ZF_R zUowZcds;5N&sE(m3Mkrf5ptWcj&TSS(<0HNB1kveD)fIjTSWA98W7^89iGDbfsF^9 z7Di>^3fm_KMl{#0t4pq^#I~ccv3(X8j1Bw~c0q4JWrAqRft+a$fa*~pN`H^j~k zd&@{{+sP3xIj6pyOwytBoFLtW#8jAqRxo`&b%sc41xiqn_s@MP;2Znf{)i>$bLK`u z9U!>eSuCgW5R?%iCYK@a(5H(c0`;_c zJ`EECA0*8AewI#08@td7k`K>tz`S?a-1BcR30%mR?Sf^apd z{a}L)OEzR&XNh|!#gIU;soZ4+Hb_=TNzkf_pFu2elqyaUa>!tDON(lVJRSq4!2Jx9 z>tZQzTO=LitpBqzh{Fw9x;qi_r!`oUz1Eqc^=GnWT?OHmD}!9nKv|%%XPQiFcYLe^ zS(VBDfc@N_S%sVvcjvsTwHzq0v7G+jR%Sh4{eNits;D^HXiHogf;LWYcMI;p-6c4| z9fAgTcXxMpx8M?-V8I~-m*9Gf|31yzJj^U!ibYq|S4~&-Is0sJlT*zzq6JDBqr-C9 zc`&yXu)S0EV(Bsh+z0P3!cWQR;P`Kco172ZK2aS+_HGL*itqcS)AW&JXME56+}$4l ze|Y7G58OK(uo*c}{DT=IEF7l)6;rkVsc5|AsN=ss#Y3%{%|;C_sat~xrGW~~k>ce` zM|B3&Pbr|H`zht6*Id#UF9vK&&eayLRnX2vy_a$SyTd{<)Sp9a7x_)xLtv@^r|y!T z0hLT6?0TSm|2s$j(5M+*LM6wHG~zwB)=!R>s+Rac|1~L&%Mh*l2gt2M&02+2VEuCKKNObcrS{5MzMb4-fj9#VCTrVka z(FAZeri0O#(Cdxw%s0o&*G1~W0!gos6fa$Am%iF<^~*+(Y7ofWmXOfU5=kE@s99#B z(*x6JsZsE zQ?Eij&KUL^z7C>@ikP*MUu+UvCU+^~i$gAL6 z%+C-A3T6^R=C6G{CDf2?1up5M_!eZ1NJkwZ8p~qh3lIe z*nhv=8d3C^0Bj8@JUo2-bA~qU@mD?PYANcURgo?;-eku%VR^-^A$i7MlZ`~Ahe@@5 z*@jelBkP7bZL8Awh{$Q;x$?=7kpmQ=pot#hm~H5N?ed=ijjYZk4su5aMe>Wic!cg0w)8$4j#AdaCVXl9qWC$&Aih24v2=rWZ^1}mp>42 z8r~c&;sUlT-&5k_^My${IKXn|F?-j^-vM<|;2gs4dv>=vF(ovctFdvx)ubI8@7lNL z%aK67xA)xl{i1e2V^hI6DK#<$GmFIBg~r@<5s;ue*+J7j!)*QiJ1EkrXeXT7kZ+eDt%1Ro zeE0GuR6B7F$=a?22Dt&`e8c2>c7b~DxHf4{iVyyZK~E243)59U0wBT(@G!%$4*&de z6_22Mk{<>-?vw+1&WqfTW{c9#xhijW}{?jRDqIlcW>)3B^Ct61%OAz$Kzks(Ak`r5SyOo z0%*(2OK*Tq3q(V6!=TrpR?cnUbqV`;dx{i5x27;grtB9DQTbW6#BUVs`slaCS{sN2zIaPW$tjI0wpgHq2$91&?=A*K|ZPioI;DHHP8 zlgmkgv8@NvN%vfgr|xlO6hloA^yx)lt%|Z}I=J*u=2xJ;Uid5Ln42^GKpL39ip%A& zC5+|vcvwn5Oh%rNugH5y5kYoi624Grm*xhaKG7pQ-4vM`TTPziaJDMVq&3R zuy1HddM6!?Cvlt+qUr>SLeHnoviyGym zS2HpL$b1SGN)-roq?HJWB2MFq zqQX$ifH@-O+GHfCq|!-gXr#)a={2g9Jj#09S5eCn+Mw}^aZ;vTRd=@YMPXzxFA%f< zhY(c^P=DA<$!UOX{+TRo@J)lSO=Ic!*A{u-RzeWB=>Ta>J)Io- zdr$HSnyo<(EhaU#H!3CbO2%P7K#g#X6QcDRfnG0MzV$J zAcgETHR}m@UR%K&&`h%-%BTs@q}R!tXtXRPNtJ=(#w_i(xfV$(7-1mZ&oCd;e>*QF zn;b6OZ+#niQ)i=q#2&MmB(_WxfDak3f&m#SEU_3EqUr0AE9htFR&a&_zYz>KxSm?>VyHtLI7ks`L za9N~G={IwIFVkD2h=pEvGcd-~ekWPws?yM4CNeA=n2anUsn*G>MMri>7`n@`Wc|SH zE#2EzO^3w;?}locSbbX6OiOY_nw=2SE=T7Z(+d5!Lu~hG_4Ma5uV!B(wWh;8){1U@ zPM=Ij5NIjEGs-fg5FR0qDd*ZTJ@$kk8jbh@uI?As=NFUuov6EQdX<{OWm0RptRK)# z$2{V6O+QFaj2f54Ixa%r02-NTBbnt@5HFX_k08Xx3R#Ah8i3%aq0{B2S}H?AlM32- zw!Oa|yuDY#HvGAf9ja);n=BQF_BRk7uC*Vmto|H~KqtkIsHVeBm}jURn%UFx`18Pp zv9-f_aVT?N^G@F7uxVaF)fEk&kLO8Qz0CeWJ-K)nKOTw_1%_(4;6AFJ?VE)%Rkssy znM5}yo*yF<{IfXLC05~gWqluV&ayLcPk3yio0)L}hG$>-Rs>zrTn5%TG#N4&ybllk zbxK-dU(jl9N2~2C3nA1==9jhg@IP)g&??=wOjPDxE`8A|F!`S8r9{g@$s=84r!3Tv zQY|*P^;%pm_fVu#LKO}KkmsEiEMy@PA8ID~1%aGwmpQ}oom3^-sSXQk#8mTEX!DkJ zU!B{pb=yE0c zi9%p8*#g8t4!c0W?CUjz0_hXeH`8ahE#a@iUltXNPxHI1i1`z23@*qW1SKgq;PUys zsmXY6B{EVrIyeCs^}pU@Iah&8S2fS_z4x#@thNSv599aVBkbkSc>sV>|IZzo6|$Y& zivNRZGoYFSU{U>MMO1}>pFE8^mRO(pqB$^7Z?A)AI3s_(JRNUXgBH?(Gakv&Wh<%X zxu^o*@$u`6%Q*lZ%K`0$3stg08SPBR`^jD2UORv`&7}kYk9iYis2Bb_j^S*FukX62 zyg%>zzS+{nm{y=BJJ3Q0O{Ghsu-4|JONE(ELll+m{d6pI%fR z(@TV*RsDoT;8mT8)OGSa;dvLVXrCIb>>X_dDz3Y(+ur8f2fSs9#ppz{-K(acxln|- zUaz92I&$VMlayQz83i;xn(7|pv|MleyR5)wEFvs!7o1#x3E|;O zny{ca4edr?!Ofb3#f#hMAs6B-wJv6ZqAx|3P{z;*ji8J$1kc| zVR^#Pe!*k-I}Sf{LiciW{cRUZEr?H$bS&}`a7P6~0{WVeIcHY}w<8qM0Ig`|Hjj zx8pSpEY0Q{H_o?%*Q$hA0ymHX-wn7J=WL@9{=DlBl8udRw%6A{*J(C*=XN8wb;BFy z)Y!P*{ZJ(-DU9o$o=H;98Jy#}EnZ}1wktF(7`kTHj;_;kDlB7#&+B?ZgMAL=b$v;# z)#(ErFqrpg|M?p*HEhdQ)nk9s+%Wbork$Ci-AXpDh44EU5c+XV`|lNcb1|2Umkf>5 zk^X!N$NBhg(C68KB%FnB^~^z`p=1o+vh~i;R6FfB20zXOJnPNY;ks^aJ=>11WF22U4z`+?Hh(-u zXU5TM99YKk`{xLB?*1XD8K_vazF0AD#G3KqGG}^Cc#16551U%=*f})M6%XUC%4T`x zctA-wcSMLq7yJ|thV6$hONI|N7-<|cIA!@>F*hl}^dw+jmsR(d7w%OsMY1#&%!>!0I%H$CNk ze*Whrm*X7L#3&|>2bkDPw3QL`PQ9s1(|CWslkm%B21IlpgoK1Xxt-CIjImhGr5n%F z05mE%GV--Nsa01NdRYqGfFUE2d^-8JS+Io<$Q>4Ye5{_u+VK}ty>eIdvY`+pB3t03 zQdmX2?pACU8BUc*d>EmNz#Iy88D4z0@6P~Vkq(w}tV>eYdzBrBFKF2K0Ri&H1AkpW zKRR-ZFOq0O)Wqqxc`Ebf7`gOgd%8V4iXFC#5+w?SA+}H5HY9!YnH0D$VRYR$+Hu+3 zSzbNtx>?#;RV79$4MfZ-=91?0^}@8PRmL0Z*K7eK2K}F-aPt*-s)}=r1}e^TxA71B^S; zcAS5$o}=kHKnRj0sIG5~W@-NoRQz>MQl-izCP887X8@lk&+3Ho$1u7u*Y7jaZLMxF z($n%RW5y&6Uhw6Bx8A7ME1qa)6vm$dXJu8iGt57Q@&(3qhKm`Cfd4r)RhBCq3PtHw zh>o44TfMEAxOn7sykBaLH^w0zUfaVjzIY1c^%gz8!RFWzcE#7uDjH5&1AhH^E-#)) zf?qgLf#Mh}8=6HRb{gThOs-82RA7X{IUFfaBvq6p9*Kjr!P0E5^E2yFM>Wq>wa>cF zVFgLK;l@oYjQfm8My!{nA(C6jl^-L4H&UKNHY!H^qJGb4d5ju?1;XHb-9M{9Ejc=B z3~lQiH5b}iDXX2}Re3#n)atdvq=}hfe1y68C1$u1HIfW4;RDjbR)7f=TS)4 z_ocbOl^g6QE+Jv;>%^AhyZ`A~^#r4bjefhX6OHaR)W`d2>m{8y!gd!ciq|KIbQ9s%m6P(cW6Kp@E zzgC?C>Fudd9tB#>&`c-^Jx24Ykgkb(YOscj{E}MGH3ox8wSQHqgXwuPtOjT6Z0c@l z8f~?L5fyyP=|-q=x;PC3%`hqY3nLT#YEZBgStVNaj)gUP{N_k};4}2GH7t@rI^x5~ zAfgli^~uz0qxo~|pyS{~HoiJ``1yYjdgj0pY>+z2ZT;;>B=9}YGxm(`=Whd_Oa!pb$fBlR z%%<-fbfE-cDCEl%DJQ2!^KOieeLo+&(2v${K7*s^1G7Eep0pDYG>{A}+RUR?6`Eb9 ziv#@uvOU&c+RR(h&P_x_{Ff&KewLZNwES;v{pA7xnH0+pfsiEe(QI$>Rk+kZ*yeye zexa_`Vd7n;UhGgA8yY$W4ZnD5)rcgrRq>Fov#G>ijekQDVoH84CH;bC((n&tNbHJP zAhE8bx(E~`edv<@Kr2lPQ5I>^KjpZtl!p5U6`loSI25eVi6*O^JsoG@PQWOE-T^O;3C=$#6vKL16=tgQY z97Vr>T@%2zc{Wj=kW{1Ub;BW5*5=BYNU@#?Z~wwLn-7b&rA`o!|NA(Efchb!6uVi$ zzijL{!oQMraLcmV0=hg+V$%|s`*VDP?Seznaez5#fk02C(Mw_%+y_mOa+`*bt7`CI zN1P?sQbZDRQ|M)e&0Ou-2?JKAG}xITiWbhQ^P)?ow0X2j(6C)lNLOvKa*@2yh1<@6 z<&chtW0Q{4Q1H->n2B1P6t*A&-BqYTo(%;i1R^NuP=YItWlhR*!0uAgP!5${#`m3o z&;$WW`sOBp|C#5u&MuN3K-uv2;}oMy%PNF(W$Y|5*jU& zpyjPNT8U6LD-2fT@E~7V8Iwe9F;tkQV=Hjso7MX<5#FD}W~ic1iekTp?a`|=coOy* zFa9}_4QlxH{(1AOp1_`~qPLRDUuHC+EL>(iX}Ik#<^DkWs*7jo2ixleB1GVKEjVFB zWRG0heCIAO`auERye3P8%&ZZb)Au)M?PD`iG6pGW@V1X@*4jvL){o*k!i4=zxDcB4 zLAoD7vVSz8pP*Rt8(79EC%VN~Y3N49Mb;q0j(t<+lqUE0T=FqXi@3W@&VCvaF%4`0 zqcORj(F9vwr(`1Q@Q(`h|^E zugIVTZ$=fAiCwtH8Ku>)3|k(Nx@f~ghUGwLqzfzKv2jmR6UW!tGzbfwl`qrW!06x$ z8Eh8}(l`tZGJS#Mrr+K|^@Y5FKZy}Gs~m~OFTJ6V8K-`$-^gH@IF_?)C~!m02TC%> zB63jt?pc8PRCAdu$vjhPb_swC4Akg#UI3sR9U99&4B?#Qj|Kb#EUGA@q*4x~>VQX( z3GHor+inaWm>g58P3h0O_y;iH;M}6NH7g(sMi8#ugDrx}%JzFAj zh;;xk2Be3B00k<_ad+4V7?f)PG~15e%VC1WfGPm2)L~pU*3<2${kAP7CDjKwiiZI1 z)vz`&DS*2*K47WJUpXhDnkTLd2-#TnKu{r=f+DuFw^5Suw-4(MM7ps`E9Hkx+$J0c zywY6if$MInnr-Z#6jN-7oy19Zh+|2(?c(}CcL~3kG#&O*#i^_~rd0TNu|Ub_+;ns@ zDPBUwXJb{}rm25TQ}s$*LsfQ2z5(`=*?%d_jV5!24E%qL#*<^Is?w1tp@{);6jTKB z3M%_uz^p%F#lU?V7FH1Eo$guo9YG%k8fvWf?ZrOoBOf*Z*!EYbSE@Zf!4F|5?-mr`?Z$Ph#7x;KnfEYEt$!zdIJ;*>GoW#d&m(JDOp0Y7!-2yzI8QWBNgVZ{?*&q(-_ zCqWad45XB>6)GnO2jE`Fg1aMX@Ey*F1q(3WHsSEvfhU^G>iuD9uG(di(&&9@wrj>0 z^%19<&NE(hA^%Vpw7wjlN6YKeH5^Sn7xiE>Rv{o|ENK}ZslKs?1)*bv-Z$n*{3^+fYuWL=kk>&Kk|gpn$we?1Y4ccTQY{@~ z5?UVUBsCm$A4KKViCKtLd-$-$I?gVuF0OeQTF4GP9bG2t@aB&I?(%&)ZN(}Cl-~N% zsA`C#RY@E`JVu#>hYa)6-MJ=&2+Q`B4*N__KWZ^(Rn}}eCpr)wbqBC5(A?^Gu8Mr1 z5kVtF0ILb52~tzTPYb18w~dw@CnR{-E150kzLDSQclMM@F~oR&Wf zl;2`6G2($iUL{3fv+sGJdb~NN&RpG56G##TC>MgJak%#;Ge0NgMpCLXXGsO~hx~(CIk*;3$t}hVaRv6* z@csIheZp{^NsSdcJ}Q5G3vQv_3?9u-C2yK{gj1s7qkhB1M&n{HqpM_ni&HND@f(OE zidgA=eY93z2gWo7xM_KHl2iL9tIhO)?>X1dGC;G?VlY=t+Xg(2mEaU(z$QL`*1?B@ zii$4WpBXmy5=c-h>-6?qn4F|~(B>kn;K8Up zoD3EW^eXE8fSyA?tUDlfV6|ChfkjT$bqdd@H~g*;uJ5s*oR{G+gr#W+3{ho%<~0f5 z?EliSSI0n{*v0k;@qPW5+jdxzc>#R>$Ll@4lR88Ssk@cy8ObH0l#Y;V4_zYWOR}W% zzi^n0#tp2RmeDQv#&Cn~M?L7)J8y<>ZFTkpzEo1oz|7x32klv_htn^cQ9psjFDQdr_0{3Vr!D)j82=YrS*^$SE*UkpKrcF7SRSY^O3DD6O&_t%i}4}ppprpShi94@M1(gE0V1pu^D z_;+MM4`_qrKo)ftaz{rE07gXyY;#BW$gN|^`wzd92me3ppw=xuUmf@u#sy@|$pWjB z^Z(KxP%3y?!F!NZQ@Wskfpy^Q*jQ+hwMYI@KT4RMXxR!25u6k&B> z0E7Q5C@TJ<=XT@=&jceZYE8Nee`>h4q4vSAdV>&-x{Hl~1AAFG1(Kqb+L_xaE!{!6 zK#8MFb@xVyoFm6|KIx&ONs72)PtpgrS6xc|e_lq7eEVNR6~n#ms|8UWQeq>%^-<+* zk1~t>8JCyAW*xWadF9G5G3wc&D$qDFK2#0#i3mILSn(cOxas_ARD44ivaIQ?_hGf| zsIvT`DE~<*-@$vn2+T5Us^dNZELN`{+lHyUcpDOAt7n75mml$s0mHo^KpLBg5igzs9s;|@(;76Uabj>v#cYG5N-d0YS(Pm3ND17{8{F0$f7vS@@ ztGL``Bv2x7FWkI8Q`z~`Ocf=WosIo+)pzs9&5~Z=XxN3-zrXO3_^>7rXqNqROi;E= z*-=fU`XO&UCfvGggD9t=qm`vQTNCGFI^>pn%yN?c;e*D)bAHUz2^Cm1ukO zAnpY2S)Be|Ih^sB@1}i)+nOE89ZWEt*k3jS@fTc8pXuH2v z^<0RcM97~M47Ae3=e`&9Qm_&P^as_9!*M7wCSqgsXy}a$YIY-0ng%1kJ%lR3udlBa zsINXfF$nsW$cr7hmye-YoM9Aab-v-f-0CZ&f8T44 z2Hs+h4^)avjl2P}J>nojb9;%iphO#c?=g-!?S|fZ?m>`0{D3L>JD%6`c+0mJ$?ZNq zMxAeet`Ab<_YKM5SwSKfjzsWA*(rAqR*}FD}VGR$}TXexlFtsA{$wBlq^blKxVZ{-)sTyaZ4r^KzV!;ENVvMEBwEXZ+qJH@6*{(1VyuUv6d}Q+qxOkpplsXud zJwhm$TUJ#K!?L7gV?bu^c?R8rELe2i`@y=yVmV6p!)@nsn@42CoAS~9 zc4<38V$c0}V_zQx3_=?ruQLL3cu_pFUsAjWn%m|b1Sm2hK5hn5QScvw&&w(5-u|DR zSNIZIhp@=Vfq_`T7$Iy%8&>8tospYj0@zV#WQ|g!AZ;&3;ThGG(O58MoY(bnNS z=lohV>_(~wJo^k71}BaFFiorbU}57&Q4{%sYa{f1Cv4qFd%PJ+vV4Jc*rh40kVWB; zL>l9n#e=nDPYGTXMDK|h3MJ9H-CyPJ95D>+T{#=xvw(-LVC zmz*33q(Kzu+|{sEyai1LhN7)diAu{7KG?V2(j}W$7U^}`buS1ICQhZ%9GdFuKHB|j zro{>gFOrU6A|)loRAV_mFaVEUV_JqJ1l0;XZH3wc zVU4U%Hg8>*yy^yPWi*6R3KAhr)Rdn-epTdw)l1;b*F4`K3s)+xNO!G3dnrw)*-9{) zAse2=;10c(Jo$w4b#s@wUr1l==E+`SJkfZ7J8t+g;VaTxYuFa0Rvk4qcQB&hdL7bK zoRU~*i19#0_QhpuzU;L7@=%6sA4@=(L9ubNy0$xItrSiKE}6d){z?lMYVV1{(0$Re zN?duk0yc$n*vM`yf6!i%8lEN2Rz3AwV5fa2BGAsHJnx&hMkMtShZ?>0crUlO(7$>- zWgGHKo{B)jcc`08mQ21pXu?wUPQHDX7HxlMXR~@Mk1FF(3WkAC-M_>MA7b)09HnO% z4xA@K542`sLouZfERL@ud>qbZgJbT!4UrOqk4uk_*$pACOyCv+Gtx3jD$92W$E`fv z5jkTe4@h!yI1ncP$n_tToGn*nGm!3MC5s zYt1Bjbj-fp>&2MmYb)F$g=2f(4w*jPg`#;&O8F)2_dN24YNg^-LsC;|#TL^)#-}2k z%QVLO8~#mAx-c*+Nf<`zM?r=We|NE`=w)pH1UMJlPE5fEarnb|#HdodSde9TZIksH z_CMERL$y$gpwTj2RjmeR9^j!CAj{mLv=ELf$8&C@%!jgFn)}CSxsDX8p~1Z}HviXs zSkrq+jaOWY1rZO+T2SDS67=sA8B&=7R>Zo|c5WnkZk*pxfeM;C2IkJf=rkTqLw9F> z(h_I9%GF@bh3|B?AN*M_JT&|@03(|7-|b9bt^NSa>+Uc;g8@s=C&3Oxgr?{SHOrv8 z=ytbQ!fmo>6LZtl;V*IINATyg#Zdwe6j?6AVm8Z7u+!Za7*}Gjncw>#h$#QArF@2r ze=>o%!Et%nA|UQ>@#^jh6tN;)8mlY)2m^KiFt zTp8I`2EztlQKVF@kGQq1P0CCe#V8h+PU!7Ohcy7Z;+N{DMp=9~8L|;XL+{u9C2r{j za`t7D592xAo~B1j5OBBsOqGwrVHEXV4+g+^id;o3B7>7f20fs>^m>mGmQ(_1np=P5D9k`A<7$OrSVJMvo{>rd;B( z4I8BenYecC5CqLJ6KBbRvDQ;r!*T_{*8dyg8wtAF9%;O|;WChY=?gRZJY?sH5@C9& zYM`w+Z7CTdZT<{R?9GD7YFtU8rR%-F-w?7buOLiP=`6P^|X8GB{<+LEG{IuwdqLrUK zh7Wsl@&O$>mZzjDi(Mht4@ch%eRKR|_^?2s4)sK(GD>bR?TKBvk$G-)ZUY*rt0JGW{i8uDSw8KA>p7AESRtQIx^R$`ohplt2~~HXAJyE;*&-agRf}};=!J4+3Ynq~ zhZWhSnB=|)f^4N?kEuo6gbZ!l2_xi0s~qKuqO&bjnTe@oB2}76ER%taCis`#{s{ZOhBCWcbHu^N^dMA-k)MKnO zno6rSl`{HW_HJZRKeUaNe}cx`gsnL~x{w7rm;D}O_0+M?ZXDeU0}8ov8n69nE-_y5 zhS3&90zv*}12&TR953hFb3I-uWBT3sG0@CeTT$L})T@sZw>>xk%Jo?d8H4%e^6Pa? zS53s>wca;n;qF8gkDa}UIV-)juTofY>t4Op2RfQdEvdzY6kCOPbqq4$x7uv1bvC(j zx!8D-8i8Was$+5`=xWWNQ7ye3`K@Jo5>l%u^?o~#_8iSWs7k{xq4JUuA(PI_cmkO! z1KvXoh#z&k-A-om(-@9EA54Y+*qlRwoXhw;Romf5NdjpTaqmEic9-4>OztAl1%i{dJM$vQjYl_yrKh0A@@ zMG$yj`erd+2J&G)`Ym)35|e0rb7f$BBQLyr7u)EJgjG!<%-MkCv4Qq1laYfYUrkn1 zxQ}U~TbB=pBTFGv3KfG=B?uCHrPf!NR|et8mR<3mv~VKd&&`muOEFV4w+zi>e{U=q ztl9Bb23w&|vINuH#6Bbqb4Z1mj~15XhmCO-rZ58HRdRBUu~w2hPQU0J3-gi=II_OM zV686YGvzu4z|)7cPxYj)hZu2<3s&=NDVQPhM@vU3rrhcau|+`o&HT*F4&P9qJefXv z|CN>(M71}rm*yuGR-|*Wp37}!!FBz2UfDSTqi7#YqFMp-c>Ww_TccZj%s}PLP$R0+b?T_I-n(IbE~lV&Z9z1d)a?!$S=dW?9}*rP^FM zMC(S(bSHTQdUyOJx(kz#Wb%g^?@MX1YM*Ro@Fl(*0?OcM8_SBoceM2#V-`bq^J}6R zZHkCF97%n$QmGG2Oo}FP%&MmO6x%p|3)K=Z(nZEbM+e)XS0^~C9y$oLkFJ)o&N$|; zV3|?|#Be9IKMXr^2AWGO%i`Ex;@ugz!=;R%mg)!qa>l$pcDq+&!}hEWllEs8hkWeg z1C4Kj-%#ZST+?i~nprcAO4uwa!zgu4uaado?#7_7TXChCubsz5SuEpN@{6uhwd3bF zS*Em2Le+P4i^`i4gVQNjQO9?<>eKJV(6Y3X`e(Xa!HF^!3V-78`91ojIoxB1f@OEeC#3Q`KtO?*<DjC{A;t>aYT>1U$2y|iHay^EQy7#X7TUj= z|8}022XPSj+#6*%{6NIzvdgksDvTA3m{%0~xZdeD0$4QU_xah9_>DedzZ3u8G3ZsJ zao3^2!L5b~rbN7isS3S_%km8AN8J_U+?$D9hhVt;-DI4U=1d${?TnrmbnT?%1b+-6 zNP2Ba3|1p#W2&4e{;^j3VL9gLhCr+g%~)0_6J2{4MA+FHrb-0_mf+>e|tM_cHG=XCPk48A zjk4_@!&7dzQ|er{o)ASR>J<@&2f@ogkFb$fL$VF0jg@P57OhbdRKy@Q?gVTg9>JPQ zpy+-IX3(y?x}I00|HJ37-dIObpa$@uRcwE~M#gBFDE;yVgQt=niA8yAAz4ieUU{t6{RYd_xYNzf1 zz#j$m$u!d|C(EEE(d`;q43$UcJZz!Qm5W9SH?e^2GT{V!0V)Q)GedZ8?V(|Q*hv?%UkK}@G zj}a@Y@|sUJm&V|_H80OTUZ&FE4GuuZWUvZq{ zKFC9|Dlb3ntS!IzzKP#apTqB!amm^c#FG&&|GS>33o}6%a%8wdE$ZtzhcLG9#C{D& zbhzAF8D#;#F;JwGKipy0 zCVpLahPX*aQ4;IiQISuBwM~o2Y2qFpl$Y2t9KUI2WK}eOzSW7cIgYi4b5YlsSH}X; zMmf=|YvcDXM$DHgYWf*Qyq;Oo{=y`ok&de`EG%4f2}Svsq}Inzc*FBUF^gOIlzg-_Kb0&jnCyMh`_!}*<1A|%VOBOGs?}*hUdAuV-kMZ zpE{zJ&`srrp{z^()`Hk9&sji!p^igI`Rgl81)1<*pU>Z$-{fp6sENrn#j(G8#dPx` zwh3?|w`#%>1WHg3%(od}5mI$Cd(nGk*-%M+x7h~uorwZqltlN+9?JevksjZf)=e<2 zoaDX2Vsql#PYyk?W!MHYK(+zM|;><1xhVmBd}VI-f!;6(Vx^}2lDao z;)rDsnqk?fB%Sa2+8!G4B1x{E&U@*%ESR6faR2#Wdt*~9^`xxOzqOZUL*lfg!->}B7<@=d;tQkMMPzJ<`l^MZ5JDt`nPF8ubEsL#OpWx>z|dFh^3uJ7A)4R4sLEV=(SEnA z6$q=$P-Q**0?;)izThw3aqesC4f9KNn{9(W16%Z+v>sDo1{5HpZd3$gZ+4v7QtF?*{OveG8%Ki1 zpx=f2E~Sx^A1KB{;Qk{}ON+&_DM{>20S}L=p)-o7XqW~}2vy{@xFC}*oBU^9>jC*T z<;%=#|AA@$X#+#eTrfm_7@^VUP?Gx~BbwC~%<=|!Qu~P*I&tqx7B)+p_(rjDC~@ra zR71o3pZ?ZYlowo-!yO1rWTX`abV8^i#sF*XJIW6U)Iftyns6NG+Pz4yPPnfwU$dH! zu+N&Pvd-^7*(2wUB7IaiN5y&k8Xkw!<@)T;)Zd-YDbGV8k$+=#JI;R*lN<^g%vdOp znuV~E8hX7b-nJfAObaV6+*#eZdJS(g7gOY?$@Pu?8_XAb8eZOwVZtaL1O*}o-lm0c zK01vrv3zWF<|G#2;&p#^?03}wC8JiIFe_Uz3O>(6)KZeMMbPgy$FJJa4Xp4@Jazm5 zO41m>z|u^cHvqO6yebGcF2ctUN%FaKm^_xhpa^AEvIS6}q=e$YO=KuH zJ*h>p=HN#De(ned&Go_;ygGus2K%&{Y+D;J!!D)x4U=P$Uh8eQ$@wgh_LQ{P!Br{N z={2|UfnQ0FM*Y1~Euw^kk*c z@vQxT?vJ@g5(w9K&9-$r$8!&Y5E5>c+MVhzaO>6L?_W9$&vfi5jDJ zyVT3Yx0QYRMqZ?p{OX0D>B+x6xm6KwT;F$O6kI$hK0n~EFU+`%ykp+OD z)+A7W19zpU!#0=VvP=THziKmoa%Kf=bfFiZhRILh{NR#SH|b8u@2&)d$Oa|A`}@11 zu1afB`|soOOb56kE2LlT+2e~?wwC3^go!CrbX2KRG{@Oxm$N3p8k@xX`H3&UgtGMd zI#U?ef_ICT8M2ItcM`bq`Sf-`)A5<8zS?3qpT2WH?@f+8f6!6)np|jvL5tBeeVl+C zhg5;jT~VDw7j)%V&AvmW-3)-T4D6L+>HoO1O|4olSP-yhYDTWKvloX^?-Y?iDq-ER z-WUQ)E_L$px7*|=Tn(P(vUS)*G?#HHO5%2x{3_<^(|{vM6O*Ja*~6a(>?RL!=HyJ!)+8Iv(r!2 zs^v%Nq^aTPjW#c(+8CcccvKv*YU#GS#mVqAn!^0ytX==79#dU$c12kIFJg`g9_-Wg zf$^3f$)3-ZW6$0qk3^m>xrEWj3<84GM_NooO&bJO7ogwOL^aN^%9m{Fs6I;XKT_|l zqU$EWo{>CwYjvaaQ&!h}GNDQw>tRMqKx?UsfC7aXimbrG6uTT=t^N9?v1X;yyx6N@ zEAB&YRorZ$7xyKZECkIrKX#IH7A%@i`rE0Eyr`aow?7PU6HMxC`F$^XySv)U|9X=l$|fp z$BsT@vYkFHpZbM-<_y82!NP6SB?KRGT1+@G*j$iH&njBDlRR&%V4ZXc&kBYg6H*>K z=i#?=`6sB;7(c2=*HB&aGV}{M7!GasKi}qyfBoMbe5cx9WK%L?|sb=(@IOi4iQm`!hV1fz1L`!O&*Oz*wHVBc87+(> z;>Wk;=+=dV_}ihP{AVK*#YeZ@79MR}f+a)D9ID%7hgPVM-+Xroj-2JRoojM}gf%p< zIWubPW^2Cw<_qCFbQID(b6A?q|2g^tjXEx9^~*<{pJnT2$VW;#-zqb9TX{E^H8?(g zkpK4J`Nbo%GP37a4#EsMFHELNtBERUf;i%BtbqAI`7J}8tiG)T_EdGV^?tog2J}X=)5=vJW8umB z+=6pcf>MU9rN5jFTe3H7b$UY)?43=Grw*9QQ1(`dj`!jExE}}FVoReSCCGLJ%Whr> z*LTc{m;V^<&9z%50g6=>0vrQ&qkzHTVO=m&aB%Py;N&;jWI4YuJSb1eX8(%G_OIiR z&3L(Zie_=~6U_*91@b`sw+`#AkTGJP>-y`VkP%wi7hUF0o?Xs|Ix?m&XKr@MH^)MK zB~yr2yZdJLb2y)%Y=ea96Jb~8BpW8iO&?x&8w3uj!hRYAKnGHg&>-Un$8z_Tl~#Oo z;^@@j!g)gFg8OPar8phpR39qdIFJ*^R3!5RVgsUYgyvZ7r(`Fc#Kg}1~}aa)dQrmZ|!1G(9hXR zT=wfscfarQe!o?AT8|YNwPz_Gf6SVk>Mz-o&}LJ}>SJ0Xy{S1S&WVKRBtJOTcc!Wi zkZNVqz8^h1VQ+9(3`trGEHd}ve>B6*F1FcP8;e{T~k8;>7n_^&ote$G@ z9jo!THaz_?Ku#e*{+ZJj2zr&f5BYcXWW6NohFeypVSUO&%;UnV<9hNtn4{3-{=#UA zN#5rbZ>7V#Q%sUzHQodDQ%N0Kpi+GM$;wl*iHVt1?nasB+v`-MZ2766me*&p^cNcIvA5i&&aH8K(* zIc`f>+x2goT9W422WSkP=O$WyY=o_mT${M``O<*2DyQk`^MpAS3l0S4o+d3$pUYrm z!Wn~<*JImsUXKQ^gI}3NZEada?thIg@BXFcZ5cj{2pOb~5WNVTwT?medP-sUUI(aY zXpl~kV|>J-eSLM+eqJSDdqYK)b#j^>o(QF5eY37fC$aw3#f&M8eG>6eUOsV`T47vW zGe$IHIBsQX-E;5I9DBU)mDY)YTo2AyQ26X&XnJq2T-9mQ{!MeoxRaKA`A@Zu$!gLV zU9XVPlP2C5cE0{5ShJo<{Re`b^#}^Q6K7Qo*j{@#u}L z7FtqPt9hzJaGTL z^4y}o%e(KbkWp7$E;r7ZiB`P396EATRzql?dv2k`cZ*X(`^#V7FHw z6Z2H+i(Y(Lotr#)5;%*+^RfgfihAQ%@W~K;>UU*S%z2#;Q!9*#5wfpwl?~t2s`j))Ae5EAf z<(U82qblC-9(hcvDl647u@IDt-JNcz&-cIoJqZQTty#BT-hJm?O%@6z&-lY z|M3sM58Hr4PG+5?NTU1>NyfdYY3cItALl7m@xb9!88W!}hwA`AMudln@cW14&40Zmq3GtYYm<2*3x?DWvF#7FwG$8GF^0M!E2p z-JYr{gDgm{#KVfd?&y&t*%Bxg%Yp^an%L$iD1yj34`as+HPTPH8!xy5sfEL3uk z<*mo_B)!BUeR_tX+hG{wDB>0$Ux_`sPZB>_DIwXX#LrPC)?OCbJLN~>u=_#7pr}wh zYhd_vNQl2p@&Y4d{oyRRZD=xtl-HXM2QdPYlbtJL#*9XbZhZdq)U)!|vgI;v>=?A> zrl1*VP_^SjB4ay^m?$W-sR3yi+Cq);miz4UFXW|{UXcfWbFXyo-c6o*_HT9fG*Kj0 zrxJP0m@!TM@YsC$_v*D09u_XWdndy-Qs=d2MR#Z7#B0&rh(^t8Aqcd@cQK@_hqhnD~q4ZBlQ3ge}>cu6+%BJ!Z><8~GUm-TwFsLH>Kq%8PGGXuxl|h3B zs&b^EkLgqE>`;R^jXiAMTLodK784Dv;Lm^liyC5OsTtpak&*y^e@RY^myGl@r4e0v z>7|kdt-@yWRYuegKKw{^XIPg52M;N&#=M^|PZ7E;)I^3|c9}%No^k|)S*Lw4rVbi` zwn4yc%_0w@37I985)~f+K{E?2lIWECy+goKSB2)6xxQbltX|SLSek6a5oGz)LN_t|4yDfQ8F+xa@ClT zDp~;Y@VBFDg>dgZKbQGWERehI{xt{$R^6okbUFkYYh=z>$cwq>d}&_H zbssn&z1iH2t5>hSxw-56^9tmdof*?(Gbue z2rXNCUMqm}7BvLd$vsT>OhF-pI#B#TBo5TE6v7@76s)wKB6J^oA?%zK#QfP^Nli@` zKXk`>!nkV&YA45z9m8lys3Ja&ppeW8i%L{}b`S&aPsgw^ziBNmEG$r?Crw$_&9wD* z$0NW7ghtDj42i1u1n*~LWTdjUwY2opr%zwnRNOUd*4)z6wey`nw|P9@9SyD*Rybg% zwBg0&NQZIJ2mSw&pd>l;WunCG`C4NAC@|INq!`1cR&+N4A#jb`Bx=Nl*0})bwlq*1 z2uH|+tGnj-#?S{c9Ta6^a0BZ)l zU4w~4=l>?TU#O{n$ns z&2x>VS=$eDO~83vd(FJv`Q2DL&WkPwMm%S7E?4xS=ODnwfEwV1d|IM~vW*B~aXmI;G*{-Pt~Hj9nV##nYdqIu*EsrE zBhYpTH1!6zILGKfKbRvcVW_KrOAW}PpnHN8OuAk&&Q?e%MmN-KQ%kj!!*`$@3xHVD zQsE;|BUM46@U<`pQ8E08Yu$qV>L;jwX#_L^9g2XPHj8SB4e*m#f2$Oil}aQGar9E8 z!pyFT1DZt6wKG5lfl{?@m4p=Ji5;v^W$}{|xHRWrL?mzAWT}MBg9Z;WS7jCa9N^X2 z7jED>cq0OQJG5rxiBHx|vOxa}&wX@!q%2E4Bas+VWH-WxUYYDs0FiRoAach}6ieS8 zQgr$hv<9sH6CN*Raq&_SOeZdMQSjjlMB8I-VUeVS+d)DcX4EfM(=QcCnK)DM@WFor zE&i?xJjV;f*0`?D;ACAqb)F=TQ_60Hn7pd2NVVsU_&j4Y28LyciMyt2+{KKXY2kh& zD>G%|4l{Q%C!RIV;jbzX&*;#Z5OAv-!e`0{dsRZTMEE!)uNWSh%JrBAPrMwsX0Irb z(EK#aLJg3T-u=bVH$f_T#7G6S4bC570)|dukqT%OrDyVFTHm6`P}4OxsMERws5A6;Ze(pyJi0j)YWN zyvGPedS>2cYDQ^{Smrr{IGAU>!qujuzd9NLw=|2|*G@v(D&~G9NFWTD zoCd~6Ugc|-l0djMV_LccL}Ig+OC_eQ*T5O8#=?;l7J-w>s^uuwrI?l+DgDAR-Q3wC zH4PDmpg#KOBU!g@9o}glnKESxW_wO>-jEMRtVN3!scGmOf}{@v7fVe_N`g!F3|X>d zi5m0f4P4`J)~s1-eu|mE`|rQ6d>-z&a*EY8TZpkR?@MVi^_{D*5W5ocU_Mbi01C_(s%U^WyZ{kEL+q zc4Zr~V7NBy^2;P@^3{-ghaCC)n-Vcpqp--TS2H z?KjKm6>FvHaE4Tq+c0`ojb-JMe(+7ne0#O{oIHuLStPhmFNvBpQT!sp_%@&vCfS%z zLw|QP0&Zy*s_?2+6B!;GCUg46N_yrADXpwlPEOP=93Y7@-yo^L5NQ=$z|Y#?A!)JW z1>xui-yXO@V)E<;&%@*IrX(wQ18P<+aJkEZ6ScyH(nGd3myN z<3<@caG)v+%aEL$EXR)@m(80u!_9k`Tn6{;k3arc-ACuJXP$XR4q(FDjW^z?%J|Sj z56QuU2UR%?L9$3W(*6381#*G2p$}`8VXtUe-Df0aJqCGJwn2gddTTTOC%S9 zOqceGja$W$k!92k%+5zydg!R6KKiuSVKa)IHc2Y>9F)xYi=-N9SHb}=4f$E&R1`P+ zIQxPRROb2m0bgGXHjXn9M8^{v2lU&?IFo(**zmt!`I$8>-sl7 zzPmjB_~T0WsWnh&Ps3Z2>i{qW!kcn3*05lp_K|{lA@FWfzv(4OZDZ1;N$~FMuBHs% zdFP#S>QoklvyT#VYAz%jY7+hW^;4Pz$%9(S?AfzbIvg+m^2;xj#==QzBq);1wr$&F z!GZ;F$&P}zWx6U0H5Y$)ewyI~R==XN6}1VDrE?)yY9m~4gXN>9G-k{g^$Dhfmopd& zy9KV^N#B_(miz)ax$tFNJ(v3}=t}q|c9YQQ*QmRTH}95;W5<<1+I&IRwYTsIQLD&V zv0efaupY@hx2yPmDShPF&mNSs2aZWu8vJ75i5h?JkHiXXCwTZJQu_E`RQ<|mkI6$ubRl@UU8Bb8X#{U$i!6a0*ML@!)#K_Yfw2+wS>k1du9n* znp{lmvN|f{djn!+Jl3#Kvrtv{qh%^+TwFKdhl|bZ#Hv$$`}R|rFbuX^wrquVb3pFA^Daq-_P}68iWMuHof~2%q+VdWK-3uMp?MZ>N-idL(RN|C*wFH@_O-DAMmb(AUJHdw zbvWlz(1w8*7XRzwnTa>^O>W_eL$m`>L!bb1x_QIE!&@a0O6kI+u+A?Sx zpzAb$=j7z5-_#U%O#ghg{#*`?8UR<%stXWO+(SD9{r;II~3(8V7i%eMk=pIV$vH)jH_;uDA-+Mda<#w>YR}$fF#!V!>(KnCXrPX6p0;L3{5}^ zX7w!K;D*tqDy$b;Q^_af5L;NN_+kQD4SZcHa`VLoGV#F@hxD$c0kbL>4sw3TKLiG7 zJ`9w%Dmzasz^0ncG;px9MTH?I?FtNr2IzpsQFW#mNuUdg^lVtBkmS!NQJH&2szI_f zxS!9QW$=Stsyqky!_aMI8IV{No)Mk#Klk zMqJ}(OaF4w910*?etsGY6kZ1NIt7$QP2+*_!5C#RuXCXsUgL-HD9j9Tad9dQ^FDR5 z_QhtG!)y(hjv0ppMsjcsOpj$?SILZL)`7oCW}N13rpNd=T^o!8<1gw9;<5v43L}eR zMq@hGFzitgZu+d$){*f*=zdrQopFr(4cu&)G^f%a;i`mu#MTY*K=Aa!B{gu0zCjU2 zaTtwcVh4$;z%*HZ^P$UQZU=w{KNb(zz=+H{;p5bqA(`_f>A)6r3G9wqt|g{%`{i zsr6($A7q|)oo?#fQ)T=1ZIYUr>YSF(novmS-OBA8lahLNNns%doR!SLNr+~{qE6^P zP6WJ`EZ9HJzT3P|J*rN92h>niL4rS$Pia1@nLowmaTU!;*LE+T; zoSbaA^wObfy$m|h(gT|Wdg#z$S+!~<*7KN+eOD5_76<2B?{YxRa_`=~YJCz;*t5W= zy%EP&mpi)dq1Tdy@klW#2@|q7tCYgQz*dAr!r$zQ@MZDl!1|`8rD1mMNx9*M8?Y%+ z_j5?a;{Nl2*M@W`8SVckrk;5s5#U3HaOQ-pJJbZ8s_JsQ$DE?#q% zEhX3Z;A~pXT&=*;0Ms0;>H`co73W7tDm^yWe zS_p}@n&RRj)fMxa2i-;$>0)HT7NlV-(%88+Lc0T!Wfy}xvX_>gRd$N{`)1PW%ayxC z3JT6($-h9=+IoB6okOFU~(ba+O;5l(riknnu9u$$~Gw>&s8!ssEb-pu<8b}sg?r-eatCT_Sz=3^O z$`7lqt6;1a#`Qsa^yr~R1iaPPq_OAAx*CKrQ&y~4fkmHp%J}i)l?K*W{H}h^Yo&zR z11I-Jq4fG<@n z1Y*Kp+4XP@TzZ~Erd^FFObqz^&HY@uDh4JN&3$_{S+L?11lQ1z z7#jX|jq7d4tZP@Q?}a=+O%mrv%j*rC=Z8SY&e_1rx&{J}b+;4)+AOlW22{G1 z8i6j4K#n^Iup4s1odoo~8Uc-fXCUCNtW&ed^o*+NQfdS=0&XJUmaO|?e#C#dYEn~S z_Uu_po4TgYYXmd`o`V2o-JQ}}FrPmI6Re8dNkreP5zq*9ZUiXfly!GXNt*?Xq&3^! zNkreP5zq*9ZUiXfly!GXsXRE5&}Z%>qVLrRXaqVp0<9tAiY!*IUcC=$N>=Btt}dNM zKqGK52vD{u;}>(U>phArsGi{Smg@z5tP#)%cn1QMZFjpML_(1T53sD*CNR)Z9_yeH z&YBeB&yTU2xtU42LhDo*3c?g8z))t2N>vUB)pr)I%otm z0-lWkWtlSFTD7Q07EIJ`x4(i+id!oKosLF8BhdZ`P=+bXt(o&V8zt=DzrSSApg~74 z)G`*IbI7DMGuLTp1T+Gzj{v$J4y<7RRJt(lol zOCz8WXnO>pRV+sLV{>cg)F@dnlS79N9R*>8;;>I^XR6cF2xtV_1_2PoI`~w)+ZL%e z+s(j!8vkJCZIQ7~QzM`eXk7#-t0PB_T++I!)}_{92Z_21u1D8hcilw%z75OBH>ruZ z#@ELh0gXWWAwU}ia(neZ|M|}cZJBF}gr8j!7(MrcEJmO(U1KxMwnSa0tr2J?1fWe6 z)0o*7Hj0KtY!O-TCK^453>mT+J6px$XSaq4>E9XwjX?V%0HWB9;nAmBd(@+;TC}Je z((vY)GiMHlmT@aSy&GQBzcm6Hfwn~e-HxO5%xq1EtCp1bqR4^?V_4M(q%j)b-vVi9 zO`|1f>o^(#HxK|pWI>yF3nZ}#-)^l4E=kvD)=0p|UH5kM;%M_K-F~cXh>+jGs%eeE>3kVJAt9k7 z+QOZyY2f$5j!pkI55$u$|M(<{A+WkjHX9j(0zx=UE{A&Edce_MRdsomuIt?nC3B;v zv^cK@8fl+A%j32OI4}DS(W+4yFj#21iw%PTql#+7kiV_kNwIE8oL@lAQy}wKOPRj%2y>t~7=g+b{A6Ft< z4;78Ov)oQgI!4FFtRa^7&QhD9je$BqL^T+|Jvb5)l8pDw0J_9ktU`v-gh&*gC)fN_ z7s&aHqu6`^11deL2_wuO&?G^(`{S4{vxU-zh660b>!%IpEG8BfCoW_zY+hr_P|W{5 zy08dP6S@WP;jvwBw*6-`p1k9XNk_2Ft%JXCuSCy|_1zuJKA@USRvW1Rv9F@8W-kz# z-rSvS0k(m+&qxOgp2`c(RyaiR*3IMjIx#nJZHtxjQN9UJCc z9~E%C7ixH=w*=sFo+iv*QQVUvy|VyTBgp%xk`-PERe^GG;jYt~KQTWAfd+xTv4Dq| z>Zvc~!d$;2LS$;z2eA=j(m! z?$N`16XU<0Re?!&T3rtH8ePoPcfljIOwaMj>GqNa(aWg2uXwz3OFjMXNvfzOXpt*z zCIr0uVEjCOaV+5Ge#k)Hpu5K-PL8W550S2r5N@8=JoMgwii(P_5kt51Qi-8Y4(+U! zFR}m>DZd(HC2Zk8*DD8g8O!LseSOp9%aN+~!f~|-&p)rK5<6eB;m@kFT!wa-yc-_K zaU)NbekmvApzw=xP$YIU9vwyh^-Ek*GMwnw zhnSAXLmn0vBV(;FYS31Pd(->Fk~Y9eNC^jmC^|1MuRy!Y>(tEnSGbD2JkiR^N^EpA z0gYOHv{V{Pi5)39xr!rg2Ig2dTPM!e8fShL{h?+bMYYR~-pOJGcCuT5ng2?icS0mC zr}$cFbLRu}K9W^H_e(($%eRjYM%)ykU2 zma&mh>1V&!IL}Zg?ytk>-Cg+vvaA(tJ9~RHAN59yl1h&wIFm%zu|%5Kgap#i(9qcy zdtNg8^B;3Oww)OCAfdrcU$z}(3cNQ_X+kL$K6nB|0p7P7m zXgtN1pICe;gcAp}orD6RKigh7lWbEyH_ET%6JNs2yMqb zvd@dN>&t0F_Bku)wic_$EJ$F2pWxZ?J^Dr4I{{s$q%k;Mww z<=S0Z3}`T9WfRmDcY9I1!e0|A0w>A(kum)K5TlP5YjTxail69OY|;a3&9-sx-z_a= zhk(V%#`wiAb$!Ou?bT(wC%jUlAB>8M3IyrzTxoSQS9(NjFvI}Cmmdi-`$NDYaXt_q&_4U~nJDKme)_4KtE=Ay=T-M1a#s5U-PC-Q6XZUd1 zP-`%P3@+=A0gnR2DbCUI{%8Q48l^Cs#@=77i7=otdOqIO*4k+ZEmms&1euqB26?1u zf{3tXtuB6$%-C zeW->0GfB><595)wHakSFcN45i&(J8ekoI>QJ04^jmci;d!gmxVUT(B4 zWplM5l5)O5YPLCD#OsVsOemz1=Tgzcd4X9KNoT}6jXYW}Rd&egXDdrU7_mzT8e|R( z1nM9!U=akyIA=Zlff*&;_c5ke85wy1;)*pR5eq{NiK1)J!!Owsz{@$}Gwz$bd{1P> zK6$&Z>pld#+YxdKIN$BtgXLu$)(pE*lkXXt2Kq)-)$TVLOIlJ=L4d?)_D6ru$k1@? z{)Bh%JTCKYR-R=?==@)q-JMO#3 zyjWCRjG@6hTNF?dW?)qco^6OmWr!@+fV=>37zT@%}Wi^Rv+9|`9> zBDfXrF&Q0C6@5NDgj}REJwIQL#KCh*WhzI+ns;~gir?4f!C;ll?06!F)S9HLdN!b) zBK?8TY-HbiiUf74TYNABiBzx9uW~Cr(={Dzq-FWgzKDc1}0q|qZ%KSu6fX$^+^&BUYeSoWE#D^ z<#88*6`YiiAn<#wm`{{sKI^)gNhIAlDV?g{t=daXqnB;Y~g!HC7ptfUW?^f3+^yKCLBhJljyWd<}FLt zIb_PRN`|n!mp4CN9}@o2b&4`TmOk9v*eY3?MGY|?9HHy5HOW>W%2w+v)%jnZXdG2zEAPOWgEpFF8|KuULQkG8m1kimni)u-WFOfA31g(eyWp6XjCvg){do zg7b<>88Jb~|A-pA&BBCA^2t>k&W3ErO%T=X#A5pv;qLtr8Oq+F9n2qr6qic>eMbJ< zs_i)UpILw*PUIVeUSbw*g+kF}<6!NgR0~XW%#6_Y`XBru4xyl|9Cxz(G11`-Q(Xb2 z$1(f8JX%MqXnQIRMv}VJ0-lkh;v0Ie=}x2`vM$vB{>`c9HG{&DQ3yG9#e>QEUYMmA zAQ_>P#?i$1nHU{U5EzT2id%!=WIi9g*DoEMPSu*Xf2NOKTL&{oQc=`sBwQBhlYdK}>$nIt3`R@80yS1FCWM|AM) z^r2gg6%Q03*xj2xkN3};qNbcekCWVM;invH^OYwrRB(MKoqHRN_j7fZn>QyzSsOPo zz5cwQ%WI}Vxm#;<*^Av?gN(-x(3iBDt*6^cDvNT#C2pAgX=ET=W9H;Ov|UwU3lAC6 zf<>0zm1o>{M7|Mg&cJQB^;Pl9GTsxm9!V-$p8LCltADn~wSlLn>N0RjR%Dw0y&rXm zgZy~z5ci5}$>E!m?*??5o>r@@5f{!MQRL;>iiKFD06{V4W-`?o)>V)%m}?i?NpkFSUQkr#UO|Mim6 zacbi=KT*8k@J0FlMrMs7H z?yHuz@}XfLO;@Xc;~lngO}_z=2qwI1eIO zZ+K(7I;Ro8t;FHWb^)C_tbXQNIYmWELpC0E1_r#BR_JXr1rpy|LJWIw? z!3{4x$EFw%P|Nl8>}|LoV>}irI2rfVdCz0Oa50*rrGs46^_~-s=@k>swoA=!e4fCl zzlt*e2GxAk`64Eb>l%vVz29Q@3Y|23gzrh-HJ(iV)qZ9x7A_NeR@FUHw)CN}&{Dfj zO2>7Rn9g?3g~{*?+j5+KW;oKj5JpRtSed{ultH)U!6J3p`ohoxZktG}Mwl1LuHFeA z7Y5kqP2Z^{W>S~t)NOBnIei2AgLr6iv_UIym!XlU+33NtY#2=>jnTD&<1)z$hhtGH zkWIzY@upB}yLxF0G<@FoSsU(TNeuYf9|-IMX~8%F$Rkklxalb zIR(JtG0G}E%s^^I8+WTpbD1`;d#okI{v#Y(NZO=ipN0^(;kXJTO*@!*(~f<>W*rGF z3Xk@8bB3IQPFm9bkfj{6`GtR9dsQ?ZyLCjH2;%=rEM*>oh|H1t=ZOjiOs*iB37b`OXU^e7%lb((O*>b`HlAWjo5i2 zIX%5RG^W(MF>AX6p(cS;0Zm93t7cEXb+@Mk*$XNX+yl?ct|-y5c`!`IT~T)US+^bh ze+BMMw9$`OBL_>7alx7Gf=YYoKc$oAgf%IrxNlOq9~ggnHHsMC^d`(&Z};5Jv|Swz{OGD zrgjB^`T`OEblc~Kp@>?ACs@aoyIIPrgW0!iE#}{O^v!C!L53kWQZC)cxG4w2wXz1zTNDB>c z{g{7x4p2=G`Zs6Slg?tbOa%TS#Q5HvQyPxbL{2g^j3fvF7~c~6nM~w$Sn#&LQlu5f zKs+e%7I|XVSCKSAVH^oo=O4it4n2%`+-SX4atQV7y;JlToo0B0xGXqP+Zw-SH<)wT z3-}~2Ebpf%xYOZ5z#{YO{N8?w$QPBi2N$@*zh8r2PjydvlY}_1b2$AaA?Ba+;=b9D zJJ5#GB&1D}@#tGFim&i&qE^kX+O>vr@TM7=loTcOSTU*8G-?ge@%6uuv4i&t%Kms) z7*=Ql@+AuGCc*XdbyQ-wo}SH<=^ez<8cQ&YU=#JDI5miRbwtMf(aICct{21EuQ2g9 z0p4kk6(nS|@?L2l!IVFa@Yyh%bvE^$8)8gM5cH>TNaws$NVF$h)ZL!WNLjT!vn)2D z-t;lH+PayEY$w?@%N9$_aJ)vB_YQ)!d1FqxJgK)P-LwlQQ%TUZ+!9gyNPZu|nn)^e zno(A$6%D5xMI>5xzH4cDoWiB&ZOIE~GLJKzm`HJ*ULTqPx1&J$%w?`x${8uP3=tW3P_M`bq72d9q|f=8CC!x_LX2Us9{{ zseSwEE;HKt8~Y#<`;#z% zF)=qnpFhIpivW+u7gft^;lFOM*fM44 zbKQxK93Nho0I=)va!_SWsMoK~JL?pi9ZS1`XZ3?LQ){Y|sge3Z8!Om+)2ojlPIA$t z%#8g8bh6Q|OINjY#EpzLr&wdH7MV$|BNEaTg&bQ{eQ3!DR#`Uj<;Ub18S%oMA3X-^ z5jyY`w>z9!!;n{Agck%W+MXYB>@V|3u`M(b@VN5`ab|J(CeAS=WJ6c zx_%liBbc`%B3q(|kr#~j%Y6rU`Dj_F2xQlow*eJy`Hm|KN5EKF_v7i3h*_`XDko-* z=?;}3SdbrS?`YIpdYORH;33;+gYR|j0u`#Igo5_(#d-{l{SkFEO19rDeO_Q`>86@H z1vWOQOGrN!$50>pN_v8`xcC@q@gjWqjwxI}X@P7(OG=PY^)VYx_61{Q$J{w0Yp2Z% z6=>0)O}$db>S^BI$U1{8yXO=mg=GKlSIc)=V+M?O<&tDiIQEF602Y*XOL9AWD1$|Bwj3uVDxDPfVt>kw^)%Re*PX&FWOYR>9fB?Xm zY`iZCXn0qlFt_hm*40e_o!CjoLBfF%+ty+UHRnx2!a-)M($Z8|9XXpAx^^YUmf6~O z*xVy7zSY&tr$CS)1-C?VU!=W=kF_(QRx4zS0jd}S;_LXN2@tQ{=T`2jAdD_ZOKI@t zN;5U9r0Hba`Mg>!tf+qsira~UCc`nK_-MW<9_e1ff}0ib&4Fko3~aWT(`g(FFr(7? z?t+)9XS?{mm(Jzu$?v4JAfOUEFf&nHA)sE9Mm`3+rEvVGYd>fEfV3mzCaZjF@>JjZ z@~5T5-w%wa*Zd$qT=4%eRDBETDliz`S5-9Tr-s07e^AHdf~ri4N2ING#&7##IT$Dl z*?s3iTAuD+0bcUKlo=M(8})O-Nf{;l9kc**8sVXSX-4Z;Y96rjvZ){+pb1RT+CGiOEf){Pa!W|9TRUH?-&XUTywil?hEGgf~#=Zoka1NLcn$Ble12 z5%_CP)uXGXSv+1jA#JzXzFZ;J2Id8(pQ0AmVxDJ^P_9~7K5|ajIldKqFwCr(B)iHo zTp{tI@)0UtC1Uv0_n>BP%B#!Rq_gD5wx>wBR~tD}>Bo3RPIo82?>#WM-NU5(Uczql zl?aCp{zv9i6-9zGOTyTV>CUae7RCt=T9AN6rE$B$CMK9aDO6ioFAXlp=38RI@ELY_ z2bDG+W)UDu=`ax<@HM19dJs8~$d62RCwqm_Qf{A*^E;34bKTGu#@ltJ7S2YU^Ory1 z8SsKlC+O=L0ICg0x3{A_sAsRfRTmNA>#D1f#M8)uw}^L|fnRZu&!yhbESc3~M5qGB zJasb%HNwV~2<{$yCB{Qt)5&5ONTxxx7|PXX%rXdkzK<1R%`z&D7l@ajLSJbl8}1f`ys|GlPW*Pf zMaX;HY*|V`Bz9OChzGFPY<9f}SdA8ETL2&%nsWHz1?*OBW7ENH4%Xg-}~5wapf z_?}OqAiZ(=H|C7F253gBU29r|$-!ki&&3N?qYh9OuFeW68XKhIpQBar2_a8Y{<)+G zv#)28<|f%FI_(Ox#QCt8wvS)PqEA`$s2C6berr#o2?iGsqXhJUacH}q)ML5BQEsEe zs6{=~uV|gdN@bFU1CJ3evcJj)#*3Vi$KxH^)q|bgP6Xa8DF$24pi*}*d2jyG8PAi_ zAh!eV@JY04tq4gn{hwln(Gv?uhVp`9=KRE{62e}X;IUIBfH_XMJquzn6L=g9)}`v4 za?-Ct)6MM@2fLooG%IGB$X-G~F4BmN6Suib4Hl@9q(qFM`o;U2p`DJZ#=2l#Q~03Q zl+btPk8?vKRa(uMLD;@Wbp)}Y65ydkt98@N?;jF>)2182BAsky1KaYx&iTGpPL=3z z$rM#4GrN{~g?X@HiSdC{XE#B{jTx>^LM@}xGRiB~f4VN1NwqJZ%fR&SFj9g>*dDPN ze@eezRN^g;wFM81tog4q(lyCjsj5=99J|LK(p5H)y(VvCO`p(>&t`4$fJ%7NX1SJx z`(GC+ui0%$yL`2`?%QAS%h?$o0l_UmO_^Z4X0C)v6|C?xNP@qbvD2e9HTEa*M?FlL)R|gP7|hG?3B`85J#Q{hK~BhKK$>3WXuQ0Y`Tss zV!v{7sy^yeRW}1{f;DQgRW~`Ql^I4pWf7E)AQfDGB8LDnUD%A{D)1g4Ic6>z4e@>{ zEV+Z9W(R!sB<1uaE&%UlD0FW@ zMx8FL18c#Mqb`=bZfSS^{JOgU&~BNpS`ADmJ3mr{9;1Ml*5$3qV^-&8A`r?30+ zyX)YgSC$qwPJ=}Yj(9;IWOP8^{sb?#HG7ISJ`vzmSe?$`ZjyCOMgaJvwF!+(Y!^2a z7!K5bdii@m6I?(PHSed=+KL;R{UeRHp-;8CfhXN)*4<5>w|PNdloLD&YJw5*fWQ=@C;r;JQvT}1Zw4^%~SOmmd2ONx%>aid`2>zw%F z{``=6)fw@Ze?+kmn2wA?pUPpn7HQDrEDhABcV|b%J&ocj>sw4rj2xpHE10{8;PUR{ zpc?C*KqJ?e{EdwTrbOe!;^%kDU0?*>U3=Y^>xUQ?wgIIW5pN~ zkP*~>#$%e$)|m}C8~fC-v=pJyXIWkB^dHYu7AZc&$48=S`cx|KwtA}D>H;lvM6LJP z>%MN^5S7%aNq1Op7$mI$dB#NdfLY#Nxb9~!zi;5;{~Zi?!#Yc&VE}R^b^n^KE?U2S)0`fBli8xd4qInL7YyTU6MQ*Qwefo%AyX( zb|Vea{k81Pw6y4iQ)Ij{xViZP_k-z>tN+B(lL;R;Q>bHt>CiCq(W!7SFR9<^2X|** z^pW%QVDq*@x3^5Oh}L=-TYn>0PqE-M5}Ros?TA>@RCmvmkK1r0|L=v%=7Vc|*@Blk zbLftugRu#WAvwxpzwcq-))Y6-pnYPWoA6V_2Q&3zcH%+0o^w{@LxEDbD!eLR&>sZSl(n`yby;9T^jVsxgXNJNJZ z9q7+{yz%UShJ9BSwe465%-QU>U<+14sjf<&&5BDI3=9H2=ZtXLpFYw(d}AD+HO`17 ziK|Z8rt*2E7*95T;_i+Q(Gsq9kAmD9oE1929624b&D@YfJ0c5#WT+7=6hH^kxUY}N z;bmNEa?>oNWbk5fhs|oX;S;W;+#;{3NUYK=t1u>#4v~v)CX?j6x~b4$bG6?c-9}fU zC~0e3fiA{vWFL{61TQTmjVfXjyW7jAt7+{bqd2=SEIMgucY>u3Cjj&hM?z~Y9dgr8 z1wFl?>mD_X+%fP2!0i6j$lq}&OYV-ia$Wv_3LjEQ9PWDiqyNYw_Q^#H!t}v{#s>-8 zn;;RO@pL##p7PJB{opONL*`9QGV+eA6+!t;)j8e$6w;`2?p|7kcP>l2pTV2sW0PlQ z#ANPL5CZEze{o9M0>A=kTnwHvAP+yaCTU$yN zlq>y(2otz{`U8R;&9S|VhNWw*VyKx%Hbv?cq;bt-~QEA+Ibyg%w`7C z`N<`8i_hDAc|DjvW=mJ4yb+qeDf!_fKH=aVLB@M*49@nUc+Zk@wNA#tqC_ zpqGR=qg&4XKuy|do*BL@O-u)Q{uI;g^+fnw@52al8gjnVNVx%b8r&OVNl!n7`#K;ZtOQ&}dfapZ7p!RlTL z6ek<^5(3fIu6`*aNTO-E8M|x-n;4PipjEe{^W$}fcwGE~L`kiJ54qtaEZ(!DNKl`= z7|VXYt^DmXY0Etp%fsGv+UAK{#iR1$`Bio-V8a^NZ7BAJZqcfTV?P(g+uGD@5R&1u zcTEGrd)WdJo6rV$$&4IJCD#$V5qP%W@qN2~c+U9LXl#d+NL(j${m~cijU3dYTjoZ3(QNlI{K;Kdk&SY02_Z5%lp`l<9ZB zEAVu+J2IoQmjzN*$caY_l|!w#%;m-p`7c$RWWrd4>Bzw+VsneMGgtQqKcW|?vHUEn z5_Wvqgp{Mhu4QLCIGz@%8=G(Mbt)k7x(lSntfdwT#bchPbEI5L`B9p9+I~9e0MyWl z;6;v4_18;tkQr*pYhy>DrsrNDqer%(SfevnzcEmSv3YUI(ZlQ30{_T;sYNX5pq+^q zF$*{I&;}@o>mp#S6I{)C1@UtqABHz^Ak!-QHlR1^Wo-^l@mG)~JS}4K-z2`z9w0Ao z$)ktR=JtNL_?FHrc*t?1@LwzR1zHDig3Zkq&jHx~T~CO50zod-z14^^h2AmV79^ML z)E#UTtluZz7S&%HDGf|AZxt4EKi2(R)P9ONW&X=Ralrn(0?P0>%W+z9o@qe%zTybr z!dfDLnTL5Mjw}DT4zqV(ZZZRX@XTuju4T7Bt%>f($jQ?-IYuPVhNh?d&S;R1xafI! z-7u_mG4_pbCT1YM@Y}fK7~skNK`~3g0Zyh|;q&#mjB4A$=kQKRMOA>T(&W}SeFbsN zyLU!Jp)j;r1 z0NF7nq~em&XM-FCKS%B|978}ZUHy$O5DP|2I<+4HbR6&X-5RqUBhi6fe5Mfhf-e^* zt7qM%D<7pW7l7f7Md#xQ+rgDZl8IYoNAl_D`bDznmr96=8w=}yd|n`HtbE$Uw&y3S zcR!EO!z+aJocUY`}Hwik+F8XRtk4Zp$Arnm~e5vyPl?83vksCW2c zl|^dq669ticwp{)P3f$FH*B{tSbI6zwRG~o#(5_Pb&^5% zB7|zDAJ!1tbPE3$DZN3h_E+CJE9;s!2j?PYMEYclJX!#(;j>S;e|x(kzN_7nvESl` zJCWMz+1X}U>d4ABc-fr>>QwIbc5>tfvPrvf@x;OKf8`oCn0fHyR>cNTEy($$SCY!T zlx0#?u_uk|AjUx*GQV6{8UzY)U5M~qjPJiFm$TIn}>+MULu z1GE9ibpt*DW)=!cg5Ubr9OO6qiUj@){aQE0L#3i&8+g9n!gNe(KCktfois>d0<`f_ z&>4!vwOi737PGajfyHF}L))?+n=RiD;3?i`4#k9Fjs*S2Fzvp1ov|$MPYQoX1S<_a z9zE>tH-a$NZ}hndh=_?vQPuINHeAc9{~R5&NMZq$^@h0yJ_qA_sfxW=t~D<3`l{(+ zbT0`I>{$Y*<57o4M`I!JT=>YO(}43ymK`3BcyvneRL8Q$k)cxmFuWfD*3iXgeo^JXD4^2zuGeS;LTmCBUu=A>9h zzJZ?o=e;Ov7XdFltnN3$aM$m@M^+%>uU}+QT(_dbwJemL!=mQMET|@4-veRoWvM4_ z07PVjVa^ekQJ5XPv>sNQC0Q@0$c8IX*+&-@;*S9b|A-}B3Oc&Dns*QWLqaa@A{k%g z711Xf2eRqc*1-}%)m-#=s0XZ;k>z`OT)6TekyoPyMeKvEgFLDp9tU9IMT&E6gG)9f zS#J`GeY8pxDagq0U{f)r!Coicxcz*5`|~qJ01GC-2UHhHE_S-ED1PEyiO5F_eY^_g z#40L4CA>$dnKPf6RvsA=m#82xu$06AX0!|Gzzn%tC9yf5t4V(J=hy(|PJBqdO=y4B zOo8>E0Vzd6-@<&KUyheH!SM!GNu&5)zeF_3o5jURCEaWJ*Z_H;Z5O=+8&pKS{VNT1 zY`xUUtiR4Bc|v@e?vbu3x0fj@<&!MF*ku}}Cu3t14&?s=c$7hS<2k6Yyw(2Ib8(1q zq^DEb;^xVr4nms8(Z8SWVGI$bq;Qf@b4jWdH1;!ni^Wm%_wR;}RkgS~lWcp6>Jaw_ zGMuj`E8#CfOv3Bwt0`4zh`)sVaW?7j5V!Y3zqqBv10xEE+!h^EK;qsP_EyMP)agB;~w8fCS|p4BE54{;KSa%)YajEPQI&ZU9ZRc3rd?iw`YzrGDq32&6K#%4KC;A|< zu6%SNF$1TXXbH*~N(>q@`ieMZ-JipaW(^2#$7+&?$hy-L?e4|mwhLkNa8)uGr>ye$LQ16S2Wj6zkV!W`U9rqYh(z2 z1SN`>jCb-?Xw&P=GW1_WTx_gf%USDzzRTFJ6}OF!SViB+DBCt{HFC(|9)}3|6Lzg5-d3e5%b0X)Co%CP z8*nF9vw@l4F%l#A3*;Jl2oW)(xAFkNe4=*Mu+>XM5sGG2QnI_*QBn$OwO{KE(xbJM z@nrf1<;XX0MTktvcj)gFLkPUeA6_ZkkjU+dfmsmObbmYLwOlSSU7vg23ha+2aO@!RVua3Qserc)bIvE^@KV5F@w-{%} z*W~@jpLB@JD<~XXwtZHTxO`rM4gfHo8-p{(671+h_f#Vb8DkBFVJJANf4Y8SM6ko9!=@SwnbUqTk4BT=_W;+R*z7FR&Zh z)vt5*A~rTaR4iD$%l;IWVk?$H3_X)#0RG;)H1oK?se%rp!5Vd&U61^0L|ua*ZEKT4 zY@;`)Q8276#p1UHRd#J7509qX`m|;)q>#iunFUFA?jHlMbj1KAQRyW(`Cm#TJ2aZe zoj?2hl2}SxO^uL?bL);iX>Dz70m9}du`}Q+t@~4f-X*}hqc4gfSX`nJu~1|*G|~=u z4yf@W`I3dcNL(V`iPyj1SwDskr3rSs#WA%mBa9D4Ua|rlwR)S?FZmHw19N@oY1OsqLMT ze2O;7z*GleTc)d&p1XnQ_`nhLz5aXx#_n&I$YG20AI&Ns*4TWIzj@jP(#322=|BLZ z{NwhRv^>PW+5*$}hJK?au zN4?=ivu?S{4A|wyG;H>vzdxo#_Tehgk|@I`WZJ{6w(z66<9uvtJZXLtsG25k&vVk< zV45I<=U1Z4L-);7Zs)F{CyR3FB99^%5+aR1rv?{WXY`*p%>Nlay@Q&r@ZiMBP)90> zLMkDLn{LQ1?YRwzNvf6K{tH1!noM$xHViB)up@1RI~)@Is~6LpM{l2%7S7PAu17>{ zFt;>~!Ip@y&0@JiSw?^N`Am!;KK5&ba^J}3YiB4kdIKEXj(+g0>zz@Z7T-JOcCTsB zt))_M&1sgbBclEKw#;tj`Co!}Q6vHLC@)MFDI>R^+s|95;6KhfdNK=Qf%U1~;1i@c zaXyLu0U~(`y2ID~OjDx^fOp*=2ufw3QQZ6sUg@)d`{eM|e?~ltR~plI8akrf?vK$j zyl?VSsPg*N;YiPVRGAhNhFLj2i>fmTijb6F9;J#pc)-$j8$6;qg1^66c|>%F-<5kdcIP3aNHwOtis*uoP;8RN5{J9 zQFedj9a65|#1Dbz7NQ4*21F07(li@O3?p3s+U~P^qhZZGH-U#m2WI5u>g_MCUB>cs zxR1;pP#MhI5-T#JGlQ`vgW>j-g4t9JI$To?C5|Csn0ee?7_{sZt`%smu`nVW#W|m@ ziw!V%KWf6Lf+7+G4gN|)Di>LXEDq+hXYOLlH{<6CO#&7c^Ec(47f${jlpR_UW(?n-VzP$H2bxaA&70r#W zQ7WxT9DMK)6BAQaua2xY78xbqzDQag20A?lurJ^$Za7y+lN@*jDjpsGi~UQj{m-Ia zoTNK-0^ISI3TcPOftlD4m>Of^CG*Tuxh5Tza%JpJUr=%A?@sHj&J43=@Snd%ji@Ue zkEij*^YCYF_J(t%G8x>>%W=vFmpdXzbVn+29MnP!1}ZffNwCIzz?0YC_M%=?DMAv2 zBog&jYy-JIuYb5Bcek$u1NqoE4NmH+{@(F~s(O)WJ*P@LQ##7%0xmG@c@EN=^r@`K zPdpAOlwB5NlwiBOrFmZVLqEB&;htMmsFSL3lc6Ml3VY6__yIJK`)G0pvv*hAq&L|l zK4pLmw-(emb$&}{zaIfR=xG_iz%0G6T%Q^?kf3J_n0-s?xK0_Tg!#VM_k@k1x77K) zbbw0;{!sO5ol$!#Qg%ym_I^jEx}$%RQh!sC5-}=%e4bM_`9ryGR6^$C(voEPSl9C` zl^vrpD4T)H`hX!m=Nxu#uGQpF`jbRLdtEsN7>bJQn_?Q<#1=9bEcJ01VJpU z06XsV-SHkW)OCdN$%Y|Vb3s@!BWO7_Fy7wO-c0@H`9K@UBbq$ebg=F?wpIwkjj-qg z96ZIc9K_6&5ThTi8Phy=AaS_ySlEq{T!)5~-ZLa8N<(nNmb-Dt>vDUURay6CMq-sQ zi5zjeb#>fclUV{KaU?daLZP$*b^vYHx%@aqIA3n&;uCGA>n1rc`%Q{DDDC}%X33;m zq+f4?JNRr53+ukt6oMrwI?;@d~fl$t= zm2b6xoUPnzUlt-4O?m}^3xTh&|1-u3c@u405t0Gb6`4X1cb*}A94W^ zvJRcUMG&o)Uua>uB{nIYJ+Y!o4E)VZZM#j2#lMwE8!+ajx{HU>o9w|>A$D3*1~&fm zm^eiv%S=fj4^%!t}Qm9wCGEG zF3CvLIdDgDje)%=-@k%qkwxP?Z-{ZD67FF3usjiY3r*b;skRcww8sxz;I5TC(7s^x5FyJ5{lp^_+M6AKRIJ;&*o5=yx(z36JlBP#%BR z__!qdP5{b1m80f&9>#dGZ`8M*KIH%bk5fGRdD(WAFOfvMOtYk5Ih*r$BDC9PT+A{9 zpy`{H+nmoTa4?SSf4ZO*8^WWIPAL2VN&ib?vmR5v)+_CgqY2Fjaj@PQbE2|Jc}(wb z?C39sWql-CAqi!%%U#1*x8550>medr>y@3AB9IIMGI@DnT&llaljticWz2oYRvDz9 z*Po^5ZgJvDq~lW4K-mo@tay^44fxPARGN({Q)1V%G5V)plZ35*S{U4;{<>ITDeAxN z$AX3S31gch$vD%{^l2W;5|OGiv|r)Sa+B2yw!;3!rIo#L(wTboN8l-zJU{(GfsqNA znw(Vb5{o0FnB!j88nfPd`AksJS~?bR_2^x75;mpM@P@*vZqWD&ww(G)jDwq-&^B5S?a6A;csKprkjK(M!m<2UCTw|8mi#jLG_~r;x*fw z#U0DaP4`?Z?}>yBig{Y%01Q1YqkiMQ*K0-=q_3OaMLLWPj~MmGD;4=~t{E)!>z^wIO3HW|qsQ1BBcFO| zwkPd0EGszJrY^?BO~>6IEUIrlTm8%kc~W7nPIf`+Hg3=xXI=0gEZnTiX8q#S3p`uB z2Ew2u{_JDs>ez2N1|y2u-V(JBm+M+!QLySmr~M{B*2nDxi&lc>kBFUrKx7KKmaL&+ zPJ3yQ;OE;N7L?cD%N8?MY<{*S`Wsp^m%uOPJS{+d zcUp|Qi}bLGM>qxv8WoXS>#xt_eO1t-C*67)-C>?w%O{8$?X-&0OD~17<4*K6x=Ya^{D^u82TxzINFfADmYy^2_t-=y!D@RNwD8^fqoGT5lH;UR^`CIQ9}y-m z0p8r5OV9M!(^L?0!<0js4xL&8-r{HUi`L-9Y2P=j@LtFKeLJmh<V34-=&sZ6!6( zy^l>iWU)`V2zf>y#iNU7Vx1q6@6M<-2^&1+EnfYm*WIB?ua-T(`>KBGY)IfF&We>x z`>`mrmIo|3^POO7p>0a1=k8&tiKJ*CTczd0i?#j$dltlI+gAmLSu4vLxlD06kCKJa za{2tOH=A#=(ZUKFF|``;u|_9!!MGOw_~|l!J^ea9g)r1PYRfy*+Xe*W=mG{BkUh4% zO8K25^Wklmch_ucG)%mwMgO@1SbtHn3y$gwf=i=PKO)NYWx#7OU^cVirW@o3D1NWh*^p0~()E!#U42N4@Hs@xFvJ1OxVQP$Fca56#&wpl0#CFN{a6jxgy1C5h^ zN}$4WB6yT@l@=HhlKQWHa@*^dHufx6t%FHpTs~5T^@!`Hh*N;7{N#Fu%*DHFWPkSW0W}(}li?rvE!Owl}S-3u4FF@%kgJJOgPV}waZ;dPZ z2)VvAL@L7LDhaHMl!Q0XLvb!tq9-$JoxJ>86Ye68)$W((Bu}fS9fO1y{^9p$Y0CXR zfjA+E*co9AwzieC{soQpw?cJqcU-w-vAcbf61&+if1k<`Tu3*RPk!n3;NS7DYxrDa zoN}LDifKONLFQ;5Ya6opZ|$dU&=|gJzmAiaE%LUn;e|3avBBYdOWg_PUpc-}r0JDz z;*}jk<}C^eC^(>yMiF{8&aJ}@74D#@<5Sy0+wOBAoQ7}sH{DU_cY1tpTu@YV;?4Oy ztE>q|PvLk`pF$CSb4)%)Wa-SKd>xIaNKPunc09=stGIar52-50#bgwb0xIFMFipD& z&ll!uFuM2lul`>3Xbr$3EB)#D^y7-!xemk!rYGhon}Vv18*!b}D9v1fo13c0YdB_R za=x+lT$ZEw9ju1ToAFH5{pfTCu>))Z-&B)|B#$cw#dS|(7%dI*geUQ+kcDNN1>cRv zas`e4WBrjE4c{XxeSkX&C0^7RM|9;+*QRD6vp4s{^l7GN>@jsKp(vGP7!yfTCqb{G z&L^|SfZoQTQ>k7!ot$Zg$ZqQJ;D~}m-u3Iy@v7h#n#OwU>J5XmUz=oJ+wWkHAd~qU zF`X``{+s1;`ksq+11gv5D_Dzz-#UDhTjF43N6!|?}#l+~2$ zDRbxbSDkv(@PE7cpUl3zlMhh|E4`QM$Br}6ejHvLq`qSx^ewoQrI#GgdEu#Z?hP08 zo0l6?G%qBwSiutod_^J0g(5YEA-e7w=DT~CsvQc2eEql=YJUHgZc%b6nhW%5yO0Amy7P>m1mEW;;0<5`BKo*SpoDd0HSEP3r}HLzk_7;_+%)-inE};;K`S{Ewu4 z&sP-y2hrA@)W|ciHD|G~k-A}Ljd%Ml=i8_!rwN%dA4xB-`U02BZnBA2=;3YFbmWzb zi;ITmD~C|(m$uj5HfaBt8+x>6^hEP4W3_!Nr5%JKBlxB)SQcd@lrRO3*A9IwTniI& z=mZ1EFS|rpO-i!LjBQ`S+}qiZQRE!pqoYimo+(J?3jy=(fCN4ev<1eVF9cv(eZ^%~108(kL!oP<~OT zDcakK{k{Yt`edS|K=qmYOD;{&1Qr0&8*2q~)rI<|>(ww+cKWL%qq-C?cpL+Y0Td%| z-PuFRzg|30dNq_$#K?qh8c3siuJ;*usM^Q3Wmggwmb;j7I5Y)LKzI#l^hBL2r{#mc z7(w}u$B1-VnU!*BYx8BNB#msoQ>If3TX_(D#iD8y{z^UNM}I!}RUiIV2yB$O+QFY$ z*fgqPp_AQrA*)a%+-_3@YrU>*V=rvL!~>STll~0PDEjBMltJ@P2X4`krizCU91Nbt zlaW=*EGp-D{gaJ*HAm7V=Zi?w8bgqv*Jp?${r_V5EF)x1j1Gc~n?eDY1jP-F@4JVD z;$4(4M%*YpfCv&%%@p11iv-~&l-A$IN%Irq%ItpuLw08yoWv`G3*WSA3=Q1mIrh;m zz)K!iJZULWZr2;F68W_g!X$2SEptR?2FSejY+{K87s(Pp3^}EP^Y<7^>0HzQ2Js?) zffw5#+T`~f*wpzW(pbjQn|YC#8&7g}r2OS0OQ^u_o7<<8O+zSRyVD4mmSB@GAtor) zYtX#cR;SlEDG*W~VqO*jQR4cR(L-1B5N(nZzmH=>hc0Kd-ExlH3v5%ilpn>3CSfj$ zp2$n@%G{#cMBPEPoO(LRdP|y9m-1s272Ej1R z-Sdd>2U&KXdSi%oa=?oT?JN*;_C05fJ}f`!%oKoW%*p(ff`V{)o)x}W7CL>AcvYUT8lvOU{;AaGBfdEFCJ_YbJ z6)%jGRDv45vPPOe38OFfWUoH!=fI4!(3jM!WHH*S%n+&2j*O3VmM-$7`kSVzT()g51q zznpkLE`31y8pFs*76FAOFKn+D_ez%H*Mb1TOIflkoSpw_v63xGl5pbNf(##`l$QoeLg0l+vq!UU~M+LXN zj)}M$J;5LSAW2P8n^-p`2b+3sU2v;V>rbE(OvAj$)M~ z&7xgr9UjMK#RL~MuI%TzoiCR^TVy$f{)f;W1%@?Itr2Q_8jKo*bNpUUtuGWHN4jO1 zk}#%C0i4RRowczpLe}>ua}_hra3Yr}r;YZTx>I^px=pe#4;R1HbdaevUmq_kYsE7| zKlJlt1Wc^QNtPzTq^s{qfZ;`O>*QFn8Vcwu5t*gA3^EW&%=yQpy8ibt1?grafuFsC z|DL_yF~C%51Yj%x$uMOh6Cp?oGcn=cLIb*917H27iW(c%opiL7sc4RWQa=ycS>xWs z4o9^>Fas1U7et0WtHHPsaO0r*r?$HWQ?5F3L?;XLdq3pC9w6^+ZVH=UpgX~T^GX8s z&XTrPSgjYr&)?oajYkZU1~Pyn2iFhZvV9(UyNgg)c3+96r7uI;+FJ7457% z-A=z3n&Mu6NUj)80uRE{D|Gc*;+0daJXXx}uXo$Nb0-4)-a?2D`i=UgGV$aZG25iU zFud!PLIe5P_b32ry%(fe}(RGPvH3NXrf?J$LDsa||j32xq8EMav$I zJgGVcdIrkL)3}0XV0hb`t&=z>LJ1e#oTXr<4^9-iuJLO?1aVL zlyB7tZHyE$C0VnI35ER!4lW+j@FT2luAtV~Wh-AkV*N3fK3~4?JGm^v{#Vazs3+c8eW+D#`%&i2d?^k^F}FD+sEU|7Syz*sAT*bBCVyk}p=*(sulSs0;v6dSLf4T^A!1 z3f6&DF~~F#*3H^3=*ms|^^Vf$sB)W4aEpU*qJrpr-@x$o{pC3KOyBuCahth3=q3pt zndp3`;p0@>{%Gv}!p%&_vLq60_%K4v!MuHAY07VSX;w&*j=#WB{SC9s-fVCLO5<_Q z?VQSL1J~&b9%V9>nX0Y|oyu`+{AypA>`UmB@t5e}(%1rSSuJHbnCBm8VZJ@Z+KQGq z45#tmDkOdXwur4s1J(V`;cViW=XTjS1vN`-$5Ywt_^k5Bl!rbmB1RP_2A;fFKaZv| zmwep+lXa8ja+l3P$H%~+aOiqS0U&7j=4i|LxBw|*AI&UEb|ZHO!WcNT8_?*M&BQn#^GRN507MufRouCTje5B(E;0>F1#W?pc>4l61M$64IpDnpKobR zF5z^E->2#m`{o9*7mlsEfS+JP*NQ#1cRA)Zaf$u5574XUR0fyx01^+aZWsK5)MTvN z`}+1r$@JIKqh1VUujWs13s-X{=AF(fK2Ti030>P_L*oSXS8lnGMRszUkWNX_Rs=b)kTWI(VlvnoIv`U=nSN7G zymF(=)0uAfRM_P(+RW?h+1g3(%rbRQ`nP6*h8n=|hoMB(D~H82;ZmDdx8O`Q&4RJi zak9lSMjWUjqxpDs4t$|C-9W5NE6N$SKaBzQZ_hX5omew>=GuUul1fkP?oEYUApB4kIl8Js0ydxk@WH){Th&%EE?%clL z$NwkK$gyDhdvwDx^yg8;g;}9%@lii}!xvL;Q{-1^lldPd5u?4*C~FYLZC)EZcy{sm1G*OS?s6I0mY$eObJorbJ2A{3FIv~l=`TJNIvMJRB&aB}(x=}=E8>}VmD-*^Y~)x$`Q?Hp{Yj8& zw@S5WX)09?`)$C_kQBK~{R*p#EjAk1so@y(*vWiz+mj4^yEks&Y%^o3QQ{)-Z2*b- z{DgLl#AsacDq%sudzc#8Mmi7R`Jw!((qFrPM(BERpY&)70q$Dr6Ew2|3V?gDCh}{^ z6uoeupr}M|AT$+T)wpVq)7y+A5Ou*qin@#syXSGuX36JB$}ZpqP23bFDMCI|KIjgT zlR#XK42Tohm*~&ie00695S56-sGYZX4z{kO4VOc8Vh&t`=#%2m;UtvC${l8-i{_KM zzViRnv!YVJY-(c!#EZdN)0-kR=8yQW6V8YA94!~-q)nL5KixacKPERGLfc^y76RM#?a*HA~;g z>~M7Ycq?x4oF&-+dT7tEfsFU%x2jdrA>O!$&0Y*0|3TqYi1We)*uO)nK!1}$rhQOS znyUX79b@V-Ys#m!LHFBJ+A@2E8(@o_Stq-Svz3bV#iVpOCL!X=(jq2N!Xy-(9HYQdM5Gk+lw|e|E0xD?$y}FXx4Mii<%Aj{vK=K z;UNfuXu(9tRQ>TSx%6d$6oJ+Zn=Q5HF>p|1R=xWzNBr@W?C^IRdVG3x(=%%mNJ}RM zuZ}pI>esKYvh^;Md#Y(7VMZ2=yGu*Z^&qwAiuMw7#8L>ohl_-U^%0rd+U46=XujK* zHGoVk(m-&Im^-G1^vJWcmxs6>-WoeL2_SFZd(Z0YiRo$^7wx@(yATpUFudg5vp5%~ z!|!P6BW6ZEEMe_W=iU6St6%r@Wua@608z>n94Nb{Yh&xh65d{Eq{H^Up>*p0&q}CT0Mp4&$Q?au*`Z}SOF3gotRPvg zDk|WnP8({HVz3MVBNkPQN(G&8g|=5KUo$zRqC-#eKSuw>GTii3HAaTJzAEMT9~cw| z*GahH|Ak4=ID7Lm?^k-mlrmE%dn*Uz5y!rKsX}^*sKAS25x)CZRjeknR6p~=lX$@F z*26+zHr7eb*rD2ep$B;)4*#ry}42QWV1gm z6m7?Oc$_Yb-%*h8aQ6#qB=$?fbQm7t*j*AE6_m|`_)nDbmqG^RxQ;mRu%1J+s6+cUehd23W z^bSJc1YS&b^#xcQgqn}H{*NeyQxF=TE7ak2FM|*XjCA3H)g;fu$}@vpn5!e;hNvf& zD~+x?Uj%$S#5V3(dMmh_!UZ>EPjxUHLtL1aV03JUKui#0McJ4HOt48gd|O$18YArE zO`P!086Np3>=U8r*U)5sqboJi6!aW-@Q+n1=yz{Hy-d6V_WL(_I!f)y5 z>bBClNgE42#jdfD;)kAp3@GFbC5T+Wn86#JvRj5po1Q96AOA%VmGue%imev;JV&eM zu@?QkKoKL%BdW1}ePeGu!o@(8SQi7adhg66eZqvfed@sc-a*y3(|H~~S-b-6V2a9I z-Fi92>}G{6qAgt*N{vj_imJgiSzA^`tzt6CoB_oNhNiZ5T;se=UVQ)BTs9>WbkJ71 zMX${d)Ge>M3M8hUYpw0VymV@!{hJ)EhW)wB{;8|{atf$MNe%Am8h%cALF@%G>1%!8)i_frXnm7zWOo! zT_m{!>s%gr)!_VXrLdG72Pc#Shx%lyB#Bi~AI?C2pF~Dt!4&@`*pJ@DEF)Y#;HcQL zeQdq!{c+v-th|&y)8qb*x$>zYMoCiiyt+|tgye2?bX2*d(G}NU%iro1(P1m>C*b7I zcs0Fk&q$WACcW%L@x=lidJM-2{3E)t@V9T=a8b3%lD zQjReMJc_^FOR3cO@imIxdEEcI9L2TlN+vC)Shf1&xm2Ms9*n^Sy;ipHcxKrcFNx)r z!B2!AXo}CtCHSo$Xm)q``MU$Tw=AOS{nDO0a|;9gt4zRbq`Ig$bbL$%(S3`s&($lV zcAZDvQ|PO087X~15S(h!lcCsGZD+H{R)bk-TfvGso~iTdPX9cv!JT^;cq!IUn|;XqcE7q)?sj z-uvLb{KrNJoRAg7MJb9}FH3xIZ6$Ls6E(h>+)6~;H^saC%nxsx-QPH;sIWJB)@Y$E zaN@1rT3i@|eWqc!cxn0_;)tlt%kr(OGVqXTO&laRjmTVts%7&PFqsIYpx*Fo^tl;kpm<|M1;# zTQNWoah;S#RYAviyKho-%{61`XExWv}|(X z%Oepo6#)^RGqHT_?XHrHj0y@@@$z6eMl$Bn6WH=vJ?HPA4yC}mKv-gBC$MN@=(1(I zdeIONaK=*1zsaXoTwK1YwP={MMQUs6IL7^3^SAMs{_rKgM+5dW~i!IX|u%ypOJ z^QVpXv?x)%2lP3+&Qyy2r3g)MHoVAE#(XE#JzJ4xipLbKjyQ~>P|uN-@k!2X*qK2OUjv0 zpMOPOqT8zf{teRv3-g$|20fvASrYoS5V4r930C$p)zDieFhPWSq7IoIi5=~T=2{2k z{r3XtXx;LcEc>|hS`;8tpuF8rS7Tvbzatr0S=G};#?{ihX#pj+3UEe%iSb>Qcv-*yyW>9;zad~z<26_IUksVyKg8NGn0FDgk7Q{J zHkRL3`DuIe`DeJ<@|Hc9yJ{RgUGr#=q*NuoCbaG<038DZixwV;A>tcAR}M$q z&hY^@m&cr?dx7ViF7^L>_kd;xgo@DAeu4)8(3U}QfvTw@MI>GoD?sjI3MdVa6arpH z(ssA=5|IJV=~8$?K(rc+>>Jnwx{{mjl<#|hFM)Gw8x*s+X-USKcJ|q$5XC;)9yQM4(YFE)qa|H$X}9*`>nsCHupm8t+a86`d0Pvr)2+y=5&@Yz!CJu=*O&TIZpf%z%+46y z!*5mWqAR0fFTc#M=l^Pc`*?N~BX-A1`L8S>Hxl(mRP#O@S>$V;XJ1lh3$wgf&Mz^c zMGAaK_7$3rIB2*r9W2_WGIkfgckmrTUx>G~TxT*uAAqsJVLquG$~di%9&7xu8%Baj z%2#SZm=4e|#~&VrfMXPT-t6$uvSvMQuri!NYKj|e%_ zm=Snc|3NJ1t^OFl0KraSEv7$4`riHbk3cbBt$`xes3q=*clolK$y~k_UGFit&BrRR zd+p=F9EwlI;q$ZKy~(pXUbV+!UylDlTPkR_!m5 z6FjTv;mx?8({H7L7}r;Qxz=xr&`Jp#MJZ`$7zT*aL#<$WXJ2iaOZ~5ep}z<&hqyT? z1&ZN1zu1tFDfb#u_dZUfYA~P$h{sRR;e){tVLD}PRp4K$}1CC)<4WXR9x<|jC zNBqT)m@Gz4X6m|2fud}I&(_+Apk_*p)VCCR0>r@+(f}4LhWETy5s-^4Vk3$>XY+Q& zYlg9fsX{HsG*F!q*Uey^NtgycxtoYd@Xw=*!b; z*55Pa+}ZX2osIm3CP?mS7B3nO0mP)!jy4fPk*#HMs=ksAsXk;d6 z&K(q{cenElfPyR}(la$xi@m(s&aA2k)W__+QA_pz%kn@d!?5EN9Y5Du%-MgaZR7ED{HK?{v@SNWg!4iQ`2y@oudEnUaG{+jj zQvnjmvqq+jVHT6umo^X5|9OL9Q%wP~hAt2CiVNNiIs*`f^6xZreTsc5=ytfkUw2XG zSBt3aT;~TUK{{Lv?3xq0h#~j9Jlb)D*1YVF$fwE?aYDx+AmCnE3eD!@|0HwoM1<|U zDOomO%6whrx<&K56C@d4=Us9LXYaC?4nocVNMS+4#j4(zDCaN;p-*bc|9RGMQ(4!z)v*Ehs{PUW zmm$;^L{+|pZ#M-B&*Ww#;(FMA1#-SLGN++JS9jhQmg?!gXPt7-^I&saVw{6NoRAJl zReT8i3VAfRf0jy9G3^fzn@&vww>0n(CYAhc?iEJ9kGOV-^C*YH1Zj|v0dxH8h5YcxE+c(NZfBlhxbVw9`ep0nE{d>Im!YEJtPmddjihPtT3x;H z_lI#yEvn7HsXZ_#=Wns|NQ~yG>O0tiR_otN2Bp?w9)1V!-?>2pgW~}=aOq!6)--s% zTYdDZeq#ggYV~$f7`s>1vejQClbDpt4M-81m-jzy!mPxWc#;L@r&It0vN^z_pv3Vm zwI=yLB=@bd{Le)0%3R>}Ts=-RFl(x_KkY zF8T29c;K)wf&bi(7ZlnovG>Bx`Ni8!hW4WztI?B1KYliLb-5&cnT&8~UZ|tMI8YPV zN~SE0EL*qlPn9nLn6(*WXvNpniRpb;C|DpJaM4!pezyuS zLzL9G2W7?3z?-*QQ{UmT8V9J}lyfPEl;UXcXC1I@gGmd7vrYqRuhsjU(D(%bldQX= z(-On`HKciNfQwWmkrSlULPjQxkecs+Fwa;Gm&di;Ipqhp5+7(!HC{E*83uQ9<^V~4 z7#TFTgB!DSPZPTsvZ3S=Jj8yeU!yK8H1zI$@hfmZ3gM(#AXV4EJOhJZ&HNJkCgZ{Y zJycC%Vq&s|`t-vXDx9;dm#nX>Q&7+MzmN37R6_4nb6xDm7eE0GuaC$3xf1qs;S2z5 zI3TO3LtG7M5<7OHrZd*uA}XLzLC&=Eg^ZB7RP}gi(h%>=cLlx%!r`Yq&c*yoFsi`v z;ZGd-*qz4J-6E`nFCyOq6uiuSK6wz3mBD!)@UYNrp5DI9%dre0@oeO&dj{p+FzGd` zlR9j+H;pSkO#}Ig`9vqKz^xUsjEs!z*|x{_cU+KXZNd&?t#KcKYKf0252o*RR9YV9 zzeiDj8w5gmgtB8ppqi65+#ZWCiBIW!RQ+WKn%k}yJ5tG`-nZw3N+=50H69j7rC$6v zqGfV8VY?YAYr4ZOvZl*w_BQ&&?MV}~0Yko{3+Ddo6#MOigeqv~aLkvewDiy0L0RE5aLiO->)`NOlMPnwi-P&r zBauSl^Yu=zhIvBdq@V3sB_bwt#(5GsK6B1;U!hRF{`xC3Z}}k_y`cV{=_xPr?Jc$_ zRNs(dzzv77%`2~CTFjl1>uum5uQJ0dZ9$N*jU%x)ObaFat=;RGt9{|1ED1uKH&b+e z?{syIX*3bgKwIDVS1rE#f4so{8#X`) zA7{C!Z-H-k*~7yGLJg0KYeoUTH_u|cC_D~H1+=5d^dI2tB*UeZY6F4m!XG8QeWeh) zHlki37=0#EdA<(F2&U7-q;d|BFCn9zK`yT;*HY`8Okj@~P0E^hizeMm4=9 z9SThY;%kyC$eMQZ#;f7(S2Qw#u;+4OUz>n3!I#qwkgC1(=z_|cVW#1hJ@8-6MY*>1F14ZjdSq%4 z!H;%a=!$FlgtfF*9_{#qi~nHN+UrUYHjT&DI4pZt6mf_u>2_QY)Drp$?VE(;|&aqWRWXpgiK>5mf14S8EXVckBczj7B z`v-z2$$d?=-?^tk;6MbRT4nfryElNZu^%M3uZwBlmFlVY(Yp%OjR4je0(Z_NobM1VVdE z*4nzm$g5o(LZIBwXLIAE&KL9C)NNX7(r``Tq|4RlydrFAA&aQ-bE>UmrB|rZELZt< zGa$6-&ZUBmr?V9{;F8-FKK02#?yQk0-IGw~`HD>icbk6IpjSZguv*+7yg4;7Yr}2I zcLsr&{sK@RCD$0jQOV3{qd?PjekTob;-r2>A>_l5A?qk>{+&b1uEgtkPb%AD9v1u2 z!J?t`)*dawX{D6u@xLc{_2UfJkHdr;{47Yt)!O=tYnRrt#mk>z%mq*YK+^t52HzbbUfw^kaHV7zwoaibrbO#Aa65-Ku?;v9IA%(?Uh1djljb^H< z3C>P-+~bcY3SochS7Fu*Fy#2h4jhbtm!1PS>L6)1IOB1S`foaQ$koBTt6stJHRi9Ce;Ne>)l5-!v|T3SsA_+>O8lj1d3r&)wdN_^8j=Iw3yG+T7*M? zlUY9Sz3s5v)54CQd)_ps~^2 z10?+MTnI3*I|S{+S-$2KqI7`8O8~HPfVD&^H>T&~wNk~nw5J2SYM?=OiP=DXA^PDT z8$-m-oK!7eYU8T=FER#=;vo2l0G$T&mxBW`yi`vFYxSO~kCQ9vk^qR58N%cJKpK+R zSv_Vcr#P{VUc}x*BdgA5!OkmhZ2=EdTX--BfkpHW-`6Wj&=QJ9CStDbnA9;G>~PDM zWLz0k$ysK4)V>3K`w~Z;Ojh4UG87{_x?a2N{>}F?AI^Iw95l52x6$-$!yZ4ukB`3& zhdwd$X)$DUZ1C;Z*N%raG3bft?qDQYyWN%UUqapd33o;>G7^%;i+s53apJwYTvY&R z7kHGb;R3EeEWrPlT*FulnzYAheSLi= z%T<}qaWW3AL;~)>Y|8}5guvMbd$9CMz3X+RCj>C;!pw^^+qx*z@>Wwyaj+JN>a^e= z_b^EyqE5T_ryJ(wE~VhVEpqJv)9RYq17fjFWvhGAu7qpm!lyInzjE`Dj{kj)Yn@@E z?5<@C{=1HL=D__;G++?P8vCb&4_Fb1-=MOJs&Fyc|9~ zy1wRu1ZqU!Ezi(y}>(kpF5@$NV48M|K9N;zF?V(|NTu{g9! zy`mv?nJ>jvp#RkCkw}RYe`2W;`6GALPla!Oa;ZgrdsG?!xuTk+@S@oGtQ+Megc}n+ z3{@e^MhGd0)r$M$N~;A_>2Mjz@Wi_S(aJy9n(;YqbOpo2A<2 zi!E(`1e+1YquuGl>H090B8de=IKBR~c2=uNB)#6+k*F z>#16k5#&RboEsq8@$d5T@`%7VNM#ICsQkw6qYRs*2`gE>$ME@_KwXF>w<9w`Jh;Ew z3)sD{^11i-_Uj!U4nvf;x3`!s#-FsO<=Kq-t*E8Be*CXbS19v?K%wa%ecKv~ znH{lYp= zXwM~BFz*dwOPBF>q8`bnK=qU=zPpZbyg25|+|cIN-ij>r#xDo*UlGRr@#cjBQP@bF z0pK7_VfFbK^jwrCtKh=+DVvH;PER&v`rCY(+NrRd74_ZnuO26}kYEaxS}z`t{f5w` zxw*M!iiuCs#-y3S?y0yZcLU)rK$wf2@ll=cMtIrNvF~@JYvK2Mq{OGHmgwDEC19;HR`%g5UszHzNF<WT++HQECSGB$zWhUS2b+|LF zHQPmk!v~QXr(Nu)!ZPo)WIu)?aZJt@%Vo(`!BYzJ=>PEVab{k)@6afb!=QjL#T6EL zrVl`t;{#LDq$427`=pDIA`Avob&%yrVPBlFX%-&uRG#(JN9~tVKo+(qOvO`fk6iGnq%PXJA2>NxK3@ z0#(Hs87x?p5olHzAR@7>_yZ)PG6(mQ3`=?`yIFoGv-zaTctN%!X zCA)2|(jv}=N)K6~Qi{TG?uIV#Rs2mSHp-eZh6b~P(fHiu?M966MI=jf_? z;O5TNaS+?i@}9jO<@=y3%_=xLYf8Rx>u74q-}e;>tVaM+4;9`os+memu$mLVRrI52 zwKFo)5C)RfM&7iDN7Z*DY(d6h5t03`7eKcpi{^WK-1^Cd);$jvxHIU8bVY$lD?Nb^ZeN9*S?6@c7wH z=~|_b;2uht+=KTFzm*Gbl*p6QC!NTcVl5B$_2jW!E>t&cr*WJ^IamI3U z;HRz?+TaZGuRaw=@y6h(+&>A2W^a>KTVo8=A;bS|D%iZ9pjSx&7?hJ~aa1+ik8Jlu z9wLd2Py8yXMLykZmaC*{Oxopy&!@GtyohS(c95Wbzjy{#|K0p;#WmR z@T_b=<<`QZXY|`6SP#6-<6YHz5Vee5K+T#Rc;9+`Cy;e=f~OW@hu7JASYpnKvu6R@ zwmF^_;}rVmZvEBz?RP|NLkG@mohAX>2ktmLsS%r{=s{6I~=_P2ou_|k60M*`?w8r8FTGDri0b-Ov<#au30UXx zUAM3YZUkXz7}Y^WBEboBp>Z@tXE;xa@nts4cD-1shQZU>{L)=}aAHDE#D6l26NaG= zp^H8qK@9&*>)-JDY`B;4awPEZGBHls!>BK+-99~25&fCOmOeHqhPG7 ztYR3jn6A{F|e z_qB6|K5k`_yq>S~u~8&1P|KEw-b+{x`<1Qz ziB9kYLl*=S(~Rq95LR4rT0}{<_c!(}JGw>N9@Ov^l%F!t~D`kv^<>Y|~1hali=ij(=yH|9HwFpQzQJF?{dRCswV}Ezo?v zn{Io4zx&!5|I!YwD~axNWay5p0)Fn7a240LtS9>ya;%+o$Ep5bH9b*DyQ*A0wIE15 ztg!gpjUx|B+Ck*fDLX)IGde>y<~!pD1#R+wpI81j<_WWyG5mM5LPKL)rE#RlsCb3l z1ET#k8V#5cg@nc;O2wYU-RtbTKMWu~pzlXWWBAE2)zTIzu@)xt0)X3!QtvNV+o=^6 zET3;{v(w1<1e8!51J9-^$sFKqqRq(Yfj|1P87#Y5gmE4@v`y?1X#0qFHMy)oXMMad zZ;S{~tG9sxNv^KlfZJQ;mYLC5{ek6Y_IiC%=@b4At;_gG5CF(T!264Y|5I0__1Q*V zevR}K=m}8&S&m>EAxH>5FbMzIePrR+G@+fnbI&4|#OY1-SZo%0^(c{;m_$lLAwDci zu-4nEjQAQ;?0ZGEjj^3%Bmt^zF@=f##{>G0KV%^oW_w~KvRSKz=r{DlR(-^9q50Tf zvDqpxDe9-}Uo4Q(`Cat{7NUmcoaejW)hgX!(DX5?;yAsI@?d^(&-aRY^()vJVK5}I zYkRP@J6|3P;$uM}@u7%`0cg0r;j@~dkQ|x53b!_FJ1atM z_nV;6$x#7kRZLyC3)xocOiIW0#6||18U)1oY}vM%=KF=f zTgw^N&97^;M(Z^(9CQmA^jR3MNj!p$g%m)a69EM#(bPwr6Fve8Gd9mb5L9g0sLTz^ zNI7(O!sch_A`__VOrK%9?Ct1oD{ktln}~LYMoawCjCs7w&CW|;Y#vbe?)Yw97Ez9^`}>;LbuOW4lQ>04 zhhLvv`RJcDUyxW2NfGg#c!b+qO)TOAa76#OEv$d>r|5;R_)PZY?!6X>KiL3^fZ9jv zLox2nR0!eT+X;(ZXCXNmnS_q22>culx9RV$3`b?**J%_{Vip?lZDhTMe<<~^!(PTwV|LG29Sld*#?KsaL^tGkucoZVBC8nCK^2L(xwZUS znNYI>_NM+YGW~ajbSxhAovnzz-<57>LbR)CEFN!Z5q5vPm$O;~d9=g1n53sv8A>w} zZ>7=@g&xb?mI5q0UI#7>O%D^3YLwRzC_YM3If0k;BMk!Mc{r>{u!R;vs{yrbdaJmo zU+BG@L8Dh0V*mByZt-W@8_*feD$EO;?8b7`mw2)Ou68yQYF1@~R+=sTHH!{BQ<33Z z5nZ+HAjYjh^W7E3)%;R!vzjm9H2#~IoD!`oy$z~=;-z3mk{iRqK~YWG5N3%Lee%#^otZvnE^hdGz3f+G@IigfxIQW7h9R*z&En0oNINrb!d=e!cB;_Ki z)sQmbeic#@Q0iqzzlOUu#S-d>5u7@`LLvJ#Kb*M^=Fg*>01VikfP zk%^&%jDlH6=F#fIId!=!q!ri8@)7N5TRzT&D+-dir}kbGW@i3T`t$ln)_~x&@<`n91--G(9zHW=I51@ zhRIPi<$iHZO`=!_)?iuw5EGy7bq7WkrOop{C@4sQq1VE|tI!}e&iC)s!cC>GE4sgb zv(C=WDib{V5jKx_7nYP%xPE6hI#6dgn-x^r*8lf4(4rx_FqkNGNyW972D0m51aEsC z6Wlt_tHO|WWOUONajhl|6x=9ME#@7i@G%p^_O~L;MG$+yRXMS?QU$i@0Q-w`kMn^>zS~;q&zl$6h2$tc0w}(H?t#fjVQZw}?(mc1Lq!KbR%8{XaFVjAs0e;}n^;E#7#8d9y z!)esTzZ=Y3ur8GM!l2N{37WcIl|;GFItcq*n>og;-MvWGnj{)RO4?>IDtl3E1Ne?m=Cy-vw<)>wL;bzBI-WAZ1` ze_N6p?tu{B&t5)VT|+_N+l%}P)vBM7`T#G`0o$y${Aj~vOU2h~CkP82ocNhSKD%<2 zSRdA$IRJ39Df{IdB-3i-?^4KRmSUrl^SEgK$>!CPydNR(kXd->gC^Vt5OpH|RPYn- zh1|~qs%>F7g?D!p7)@0J<-JfDu?9yy0h=`gi9a-AWq9mOcDmD~dGRWZ+7DI#os61@ zTEH?p!qu@i!jnw7f6KE*RKTg}Vr7vY<^o$Vlv0r|J5NhO@I}oc1e?+O)^?p9ziIqU zd9O^Y#d=M(Y>{VnoDoR#k-N!rU$DO4&&g9qpYrx`8xF!Wy@fq!yB#D+^-!YaONAx~oVM_xkjo&4N_M&AXq*CxTJ$iX;ywnJv|P+I%bF1STlASX`Z+^M*8 zlUgc?JvBrzN28*=J()lAi8+<1id?#hQl13u`u$^y`--ww(TR}RIP`qLwi-k{jNfz; zo7~#x{YLzJwMp79^OU27D;RQJH3P^vdGgW~%n8N#hUrOhmFz9I z`nzi&(5kjTr_+wUHyouTGghhRLzsrX&C+*7M&vs;dFq^@&1u_@mH89qnadUO*^7%* z;Lrd&Wq}YSYlNtCPXWECO8lhyfO1MQsHzeMaL9H&JM(}mYJm|~EeutwGuhwPk) z=qxT$j?nF7S<5t!I-3v8MhPo71;9%np@*W{iWy)qnqd121Z=N^IDZLZ5V0F_X5ceo zIpIn`c}sdf^Bn~IT&jTy!FBl3L(i#r$9>dH@>b*;) z_nS&k($*jwBZ$TMs>~z%i{`z>LNy_?}q2z~a+o@;)Wfd^uh-1@!^5d`27jO|w(A2h5 zE@TDs&<`_4# z6AS1G-wh)$C84cp|I^4XR}0|*D25#bjf-B&OoAc>xJ9-)N0FS<#Yx$-Nq%=`$vdd{ zQEu8K1v8PTDIgbHJw8Q6nf8?#NvAoMiwNqP zTYy#ND5_)9#fPM)HpHb1JT~=xD0?lcTm;y)Dz11sZI7-^BVM)sz0Oayja$n;Ukc?Z zj6Qn;xYLky3x|Bizx3jr@)G||85kRZDflBiNH@jvNWggC33$__#iu=~NZr@2$6gCPRP z-NbMwzky{>C>SC^%tDP8m;Yrm!MLKl9~9_xOWScS;3Bj$R>&3X%{Ur~iQG9=1hn>z zf0n+2x6iW~LX&@!u5p`Ig3B4e+xNeHksSvT!HlMOX@B^?iucA=UOL>RC^`)fKE&tM zlYAqg6S#3Be zI!3Aj+6aL?{Y@?2OczT(cagHn;gAZN{Dvsn`#d)omA1oDH_rR! z=klH_={m(FQ=2j-P%kxJ+fa*LCQ3d^;~1XeD%r#s;>n@Fw`S@m7E?lh6c=e`f|t$o zW$`=7$#nf{>Ox7$+uQFJwp(5(3q{4KlqEurW0G=P2ogUU6TM&F6Tdy5#NZE|zn%eS z<&cTp;h|tAL4S=KPbDN|KOVwSVfXia!G!Dc?es|P$&Dnxs1;E#2Ss^wwAxUmc^W{8 z%^sb?6!D2we6>3+#-IUN+ANlycTngkWhV1}p%}&*dqUqI@6;Q##n6@&=*u3LE!xY} z{(hl@fb47w7G{>4Rb1P>^onW$V-eRZli&S2VxVrHB9>a|qC~g(EU}-16L#n81%bI0 z@@gV__Mr8;?s^JBb;t0c-Qj%Q3D>tW89Hk^3V}yhR=q3{&4%{fh{?6sgToZLZbBrx zXz8T&8enkXq>L<05HWu?LgiJhh4V@|5ybG{X}&@e+=UDji7&ga1oz=vr#a7L@Ej+Z zTwQ1PQd{&<@ODv}J(lQU5Li2F`R+%J)Y~kZz_@vPE^(;~hrSE;W+!C@uy1`J6MEiL zjhomw>G$a~{wCK^CppzLSuPQk@xE|+f1i*^8rZoKc(Tz6RBS`!Njg}^VvhFur2wZ? zb;A~MwiOZ%M!zg_ohXwLw614HdO7t=F%}uzY+5d~YgeeS=Y~m}#(0z7(W|wRuPf#M zeO<@sI#ym>Te=M>X*;rb&5=HO+97gB3~;02J&QVxZJ1ke5jz%i*ih4=A_JXackKmk zoEt$fg)!4Q37n#%r9TPI=gUD^9nL#JW;sF%zQUKf4Com9Gu^b4Jt6i?XGht9wfDaZ(Q33K zc)i~LOk1qfifO#>qC^do>AC6hAPDFSkpGELRN4wL=bh;%i_4xTyY?S8n;|yX&lNRxi{sPDOCNCFV@%J zD!U{V{pnv<1GYDC%Rys~)G`S3kJ<$0Z%zdk{FbZ-lkxX>o%hq25F@O@6UTPTqSj@0 z;Us#(2sb71^%w^*=t{;NgQla`@cnl_H3OR@-)U3(q+I9M;9oss5Re8U7<&Bi!90Uu z_ofGmu=D~uJ9#>v$A2-KO#v1>nD;Gi@qu5v;VGyWs46D7%Z`y`*tV%IC;zu8wx{dxj3 zAkn!#tK7P|-Cv@ah|741*!MomdaAYj!-erRH>r_&GdG_>41XH(JxmG2gc#aU^DoH{)U`Rkmamn-jJKAM9(4wde%cFq zl?xhLR&05mSH>R`JWsC~w&I3DJi|F2^ibc9DIOxK1bj`f!uZ}3^Paa-w)96ODe`3< zm^a;@zy}8hx6^6p15{R5n!Rv~4{jf?+0Dd#|;;4D+tw_{`V@6TLl zO+WZ>c?+ZYk3OPyNQ3u8;--2g-!XJdsApyQ5|5q5eSeHp>OtTG8LeZH=mvKm&iPzgq6?O-J?anV7R#^K1X&%cFc=(UxDfy6+Hv`B zvk}So(7423TtQ`=mT<$m)&0K4a43ZzF~txw5TzJa%h@q$T2K1M4*nU6tr3f_IvauEgvv_s4s4 zKUHrU@9dWjMhJNC59??!OK-t!iRqp}MvkAo+J3Ry+vCjN()d!ZzG!IahrEz@0`(=Q zN5?VPvT4c@3Ipulf)VlkQ}jHPSQ9ozCzm5YKVnokGpFDZ$24gJ^3#ne3YI&qxmU;T zdtvAi`0QibE0Lib#ze8UcSZ=rQ~!SLC&>4!pX5-VO;z=LTB9RRDkt`>9$^oL`q5uJ zAfeRSab`xS5vvt@*170LO_u0}(6DRqLUebg=z{6OP<5@uro~`r9GGu#>3Va=WoO6u z5`i|7Y3V>TFXJARHQ8+_8ct-UcKX2O3*i$HQK4oG4eg}Rc`%lm&MnN;HueQc{nk3TM1 z4Gt|<7AgO7&i%@P6H?o1&fhRCm08X;C^bf<4t#8$6GGlO*3tD^yw9TabJ1)qH6?)MMeJ{BMZ>Xc$lPJ zLLnv|{WIn?q8Uq3&f^0u{a%{@y?0+{RBL&`#Y16@G5XuI|Jm`^e6G)@2PM&K3XGt* z6;J)3IHE>vSITx_0 z2DORK(e)KOLyYsHhWfObcUH`!ZRiwc6^&SJQC`pp~`}cc@4P=IA2P9abhq zue)2~pl%4^JzucZFA-%j`JMR?Q@5zkVFY?y58{cmnk3W3Ai@yE3;|21-P$6vujihD zK_k?58p_b3(b6Ly5bJIAV~1H~J_A(cM=J$Ty(Fq!9SDm*4m~ntRrh5Z)uN>$HS6hn z+-><@$wkzL7oCAlBsc=EV?NLKlaf>bgx83K=VGOo>2j$-Nk=291`Kx(ID=W_a3kdT z-jVI=X270_TX*ZFjicV^312SSBOg{o$I#^Ae}*Mu;`sPmZW3J6pC?l}BVp;B*Xfli}a->^u$0d^gs@t$v2NixuBQ}@; zNsdjz4`?T33nhOF{xrH%p1()?Mobh-;hS-CR!O`=IB7$zhv(iBk>7qI_+dCG$%uI~ z8D_P$R_VQoAu;sSN+=t5c#aMPvI?;*nn<`3C;TmI72znIj4O3#sgfzQE*pfL2+m6P zn@xq{O=f35Ofa^ zF@nA+#|+(Ufn{ME?Z=++9}@XZP#6@H15Q1)T~rQXwaD?cZK1FIR*N?;eh~NLXuVVH za`S%mIARKpZBwp{F6xL`AJwYneO=ez^VUO*Fz@x4&e(^ z$~zh?+?v~shH{#1m533Vh(1i(W8hRsR&GJ(7?{^e;C7mb7_If}m+&1R^|cbsI8u@; zSZXY7i(Bg^W>7=W8$&X+%u%zkeT58bbsmXIVVC?6+yQ28cdOJ;Bf(&4F(?StRt3~M z--?QgTu%%SZZ|&8l_Wxb6#DG8#;AX_eLr!+}l5$N~!eM z4RRk^Pc@B<-b^H14y)axziTbBfD5+jL05pFP|FA zd~5!R4rI!oLBqD+E8o{u?Nmu(n$GLdn$%{ciYngCin3)fG@P>1%)5>T=x^S!@dOXI z+G6u9pb4`jmRkJ zr{ML2giD40E|Ho4^k_!ysw1%aXJo+yy>3D>O07%rIYgIJ0L)$}WqcN9aI7IyBPgv9ZTa zW()oI@vKy;2+r)CNIUd$EGhI@F$%cpTmD&N*GCXyTgw z=E_>cfJ6Y<3DIfqp#=fG`+R8Na7qVH6=r`t(}T$MWNtLMvf{!tf77hwjtl2|gM97H zGQ~eCpInt$FJqdNV3gKcF79u&%S5Q5`nOe|z9_Bj1$mZaN$NI#f3 zcxG4$(1XCCtzh1v{=lk=GPzK6z|1ijY*i;_V2*?I;XEde%+iUq=hW!QRQ)}+34K|t z?4pLMqpgbG5gjoRIa@92jH$W*J9idB<=3mcl9K)wg&V)Ses6N`@14d+y1M@yd7a_( zwZ{HaOzGj_Dt51NxISS7m;UZoXK61-5c|gg+gL#e=9SgamgT|qdGZ}F?`f1Lz5pcv z3n&X2Htn$4n>IktW5mCy`^9fk4%xpgzm|Y?k4v_c5gzzcR}3M)Zgh`ol}k6kH?JQv zMI>XP#zvhY&0V_HeCo)B*9au0DVfE_+;5sz|P^J#C z#k@ac(+fdl^&6rAXciGUx#)i@ow;(u_O$l(RnNN-3{YprjOTy)Le|LfrkrTHuYWhdKyFh+syyoN!u&6G0TS|+MAmlniL@3L6}1;BQfLUaG(0Zpo-3(_K1o&eqaM5TltqrH_Os zAKiSM+%N)Imw_Y5UUB-CjkoYxXyw@#1>zf4XrDTZK&7ZDNJ4#u@nh);c4 z9nXIFSIPn!8Sip5mDLJ>UuLukj)6XDjfDrGwt}j-ud1r&<1xc(MRq{SOAFDzDK>WJF%+F{jS#1VnZSv~EhRZl(f^hz<1cDGu6x0e zO54XgM^lp-^s1wKj&sulr$5~I?KW%|UF#6BClt}2DNPJ!) zo^q5PG2TD)mGpj|{dv0&Q^Afw(bKi>z_X5OK{ptPRz^=86&8DtFZ{lA2CMJwYMta- z{_1gX!I5E1_r z>pE?Sp$#cls9>qQs4PYzaS9$+62cf2{FQVFsYm3@CR6ivVfOWWK2#Q`NAeo1(yW1` zVf^KL@wiR-K2WZ$TK=lzvQe}<8p>g!&uz^09-X{b6?xS5MDJkJZ8Q=c&Z%Hzz%9n8 zZtOa3b9}!pOv3b5CM$h8Sk6#BEyUh|SA$@a68JY;XB+jQtsl{$d{Lz_5kEL>g)+4R zTh5^MQD^z_2%JOPmx9m=NrftG* zX)R}B<)X{n>B@PIQ0J zghH$RFO{zcl?26J8rU9rh6B*(B)Xgy{52F7_M%;^oy#)2Qr?5UTR_sFJPMWD`FtTC zqNJb10?)!wbCDO>autr+Dw%R8fg$mB9yLe+2*#pgr#RT+AS1)WyD|Kq3fuCrUJsEu zoQ^+$YA{N(owvQ7uaBDe!9<{M>Ce7)I}VJeH;iksqx=`(+)HF-KYB8z=MiuOM2rfa zNpVEKEzx?C8^8r$`tts?Prv9CAgRNt?T09GtKv7Dz1+KKmiw z-@&(KFxYB^wJ=;7KdI=GQ`F#$9?^tb%(i!E_*gt~<7)u8$@B|xfH2upsWU7`w^L>3 zEj9ev6DAQIKdxxJ7#qJ(p-0Q8{%?7Upy-*~WUR~XgtX2Q^msyOwp7O_AQ|Ed#UZ6y zewWy4Oayc`4EutD&uf{&9=hEf8jQvg!=)a!xj4s8_Pzi4C1u7OCeQaGqFcP8)ny%W zllVE0PT+`-L|V^28W8eglVC6NNij`Y?)v(>^i_x^P-buznH)ZRB;Na2Fb13fi;}EjGEVYWE!!>=Q>8g*W`Er-cPbB% zv;3fJO^^wt*E!uTnSNZVlIH1sXT6biqx! zy=o^XB5)kLMmrP~U*X9$Of+IM^-Z_q`wGvJ-}f=bUMeEJ?U-X(r|QsZgU!2vF=ZRFmSNgt#dSJlY%8f3Moq z8(vsYw|=Qess6%hBd6Pfu-Ea6PXS>8ze7z_F0*LlewbInXtl_E0~ovZQ=CWJ?|0K` zD;8xv2gzE!YPzzijG*%TAC0pRqh#Ihx6+wjk1JCC4M@f$krbr=giVZAPQ4@UN51E6 zaPpxQBsuWZ6qC^f3+Y_3>({fIy)?6I#fLh3`Bgi~DRQw^uYk<+1TIIPw5Z+t--(rL zfj{H${2T(CNm#&>*FDca_i^1K#7fOc4?;Pr8FqhU`YF53Nu_n(CuqH}m`w`3t6=s2 znE1tH7CGE3$zU+D|Fq3;bjH%1?N{0dzJo+K}R3pQg}ih6%267)iIfcQbf;WqB^X31ezUKRI{R^U941?4Y~&6WY436mh$ zIek_~+1?_J8okQ2nYlUkYV#QxSpQMIy5iCgsKSQRf(D*@oiph#_CkOe57@zH3LF3! zPY;aE_jAo=Rvuc)#LF1sr7RS7qvk~&BB|ey2 z8p4OsNgOSwS#R4=L7C-mR6-N4Q!wJ6CxBe^E|r8g*e}0tv*DPG}N@TL)0cU1G~k4pT9*S1Y;{#9Dl3q z@w8XJzn|Y`4vs3`be+2WTBSt3mt zpbE18ZCY72?#C%tS;$q(R8?sDzn9Ye`5QCHsMaQ5Q=XV!T1Jn}X!4cXQ%?Ba#1Qx^ zqN?hmJUM!b#?lKN?%fsl+o_wYEw>9<;uG7lWP;b}ee9zO5liV&oN`pAlb_Cl9y|wP z>*8l`Sv$}5bY2J&;oP=JPM5u;YXD00Yn={t3fk2~uXjS-u0H z7oX=XVO}s@VJVQh+)G ze3EvE?QdDGf`4&~0QB6F0_v+?L{l}Ko8JfHY^f6RbV@#4IY;Ia1N#vC3GqJQQbUgeQ-D{ad~}Mk)TodY<%45WI8eX&!=004r3k zkG-q-S!Gw$GcSCCB#g@nOhdV?)wDTmX)T>V-;)=nL;?&7zN>~njP;fcKf3WXF@ydU z`j?2lu3m5elq+8jH+*<4EgL*;J$#&scJMCTO#=P#wrat88BC z1_jx-i>ECWLiaFe&1nIO1QwsHAZP2f@RVir=|cCK5}6n6GM_J(K42Mf$FK|25agh$ zVN0ATa-$P>ch`e6&*Nx-1W-uvYIt8_ z`To$sL`zRKH2g=aqAjVKTFa@)Z35H&D^m}HSWPbOm7(XgSi*;**z=yx9}Jv&?ubD> zFvF$?+GX9L0QZ{hO(m^qb?EZG%WfPSr}=7~d{b1oqUek;gaNX2mA$b1O|v~glMkvB zQhKi8=Cb~R5*9}6*GrcHt;w1*pfMs7`aJWI^dm6x+}t-jU%s8R?yOc6)rsXbyMd@q~HalPW2m)sood0KN&+aq9P)O zYmXPH?6!Qt2z-hM2RVdh?~#Flu1LaBkRG$MbfLcNd&oz{ ziO+d?X)cub>a|k9*U+s5ADhCO6VQJC8KP)|;d2uZm1;xSMw^nl4d0P-#Y-0rCT1mr z(W~4F+l_aKdkp?^PO9?bH^?IfBjKNXy+De@XA|-@ACP6&rV0KwLkoB!7CTZ|T)U1+j2#4#y1;#(IN@F#^i|5O^ZLiu+x6v|ZEj^J!qfT_ zQGNjl3i5Vu2cftB2!T7um}J$jt0aWT3$)a^lCRJ{FjwXlS*>Qnqv{Rpn}_OR@k9;NFSrZ`?Q@GEZt#A3W6H! z*$4*NVFv?ad}DWD%bByY-Z0hMIw!C4a-ZYF4+(Qimrp)xsF%`|z51&}F6*y$ATXg< z^QCZ=eeiI!0o_Cy+obL0><7QFLpjG3P8TvgY2?zNI)FUJ#M;v-&1I6}5Q+#i;3ggzA1h)WTb9L7Kv2$ArCsRqwG!^G4p5 z^*#_~jj=8Eb-Al@(SNSZV?N3$xDPGKc2!pSJk7AJXf+s+L+Lgy)l^;)m)e|@$1k`= z4sAGkKY`*Y!G}64KhOHzs9wxGg2aIdILPS#lX#=DUejN_$2neT=E{PlX9&QY69!yqXBV_-piL9Q>i1_=qogx5y)=yDEZWo{ zpLRo-9Q_5z#Ewhh;SO$+ejAT%pWICtFHtKpDa%5%k;LM$3^ED{Ue2r4bXyrTlILSz7K0rQqvCFq*6ch_$3Nlz9BM%K;G`Ett7XU6-{s8}9%70=;{$ z5yotM@_ct-d~dtIGVR{&NmLMPQ)vS%<8wBU{!{F7tMVtS$4LR%ie0UbC+bKy{o=#| zIwQVacHjNS_T_yTG^S#5dW;&P^y48KhZpsq?_f(lBk#}v2zKQuBlw#J{Fla+$#ZW! zI5I>z%;DYdCV`1qB7}{|2Fl_(bv63LkwZgCIoS0ij~=y6dw(_u9g#vmfc5n@OPk+= zga*etOmyV-<;6y8WOyNJ9k`!~j}sb-faNBqvVkvh8fbPdFrIQnbez+p(CWzTU+-X` z7wdPug*iZcYzI4AdYIuG$a8IvlBb@T9Z~s{?>ULev}&Q7Ww7_*xUi(wc>^lUg-UpR z^PV*j3OY|~iGG>Xakpq9xs2H-jF^srLG7^XLX`A4tc^tH&O`~^8UeU*fOWsWYAu_{ z$vaJu^a5G55Q%+809YV7u2t)Fh8f2Gx);yWv{4IZ@EwA-DGWPqYMTEmei>7&>@jVN z3iG|!7lf?EG8%uanfgzmcnpEWMiyKLqhI!Hjh^Y0vJ$u@*sq|xH1@0mw=5d3Db%Rk zBc>4|YA!h3CEM%2V9H8Bm`NZDn5OrejdwbU3sU80K*Omcgq@?Z@pbCMyB~Ve-M#2M zSE!SvG$G9ReS;Vi&Zsr!d%Wj|^*|jG7}?;sB${Rg+wWsQrTILZ)D{^QPm^r&e!5zM zPSN!QB}gi2vja8DQ|B%FnZtm}A{J?_rf1KY?R_aK5)R47O(RYFq*mc}6^jPFlQR@y zzem?HKn#Ka3pZ$zqD!kfClnY4TBj}GF+LUm?8h4H0sV$*`z-=am4wI`1|y(QhczhO zFLKzF#r2pm?aOwnTcQtp3(GYTCMi0fw^NP;&~clcabo_&K*PdaFayI0m?FEP^>$>f)lwoufYOR{J)e3rtHcFkQz z(rlJVbajNzflSMX1iVa2mTrNJkV-%6uQB6xS;LzorBIzTUdc~gb-lX{3C_jCN_W-< z!{sbww-rx=$C^ji*LfC_kNlt(GqMsYZeQMB!Vb4lp_C~niABmT-15WUByo9wW?R_> zpNVAaQw5PTRh7Mc_s?Xy43jq#?EWCPI-kCNTEI9*UL!0(r$kKEBb!Y2l0_-jow<&; zP}5G^ofJ5W6_N>t4#qmj=G+#gX^_b1%oh-aSRfXa{!?w!Ye-p#^DK0l#wq;b4?Pkg z-L=>c%^i4uErSm|x*4k#slk`KyqegHrbZhreY6E$3FoE-KJgUJMu{=2vX(kxS~6^d zi?z0f2DJR-h~z$G44gc^(~x&!iOE>9JJV@_U*?6CUg_r{+ftHDXF*^TbNvGYJP~ET z7NswQAjgaw#-fFqY-}3G_}rm78k1(Lqs$W9i-Z5FI`60T?JUA^bm=p`9tzPgQK$QP zE$&?Bg5cbLN?A>tEv_lL^M_zbnkE(fH#PD{U{5WL4TRI1PE-o+?-1%0`pRW|lPf`S z9W1tyXjRMxjjFwdI0f2vK_zAf1jl*epDr8Q#g%8<#RE>-$BJ^>g|jl=s&I1UY3uu$ z$ciet7Q}%D|M6c)Z^0TtuhmuAmN@JDk)BBl3(2A*A7}r9$g);Gd{GNf-&tJcjah$u z@wxfGUyT19dT^Q+h;WpAqaTHZ6}M08)rqIs*GrT5g2Swe zMN=Oarh7=7X+5}dT3X*Q=qZ1yD$jW+QaLDZzpWg(Tb)$Y%Q=&(s3K_Ko3@?y%wEcN zyKgEM_&_f`qpIvl6u0~x+jBlmhqjj>Ua0Lr$4!w3=^)36xE3L$sIK|L;{@{WB3$C= zszca&bJ#(~Y!45I%;GL$n+&I_s%H2z^4BTGsop=Hm9A3n1ZO9_z29djd@XTLE3%@Y z_O1=Hf3juwIo-f!!U#<6(CtS<<&c?Kzu?2>Va)rn>p6zB*I}*!B^oXWBFH|U$pLc+ z)_@B4ibKUfoCGKzzNRpzvdf}$@10c#gJ?<3Ck`Sg8diH5n->X%Wc;Q8F~pYC$u| z83dy0nEun1j_Br4kur|wl3J!(}`@a`s}ts;so$~ za=DjQYz!ei?}e$%C0d7!sm1rh`+_kd5-h19!FHyFDtOaeV}QhLPfvS5MxWg&=YR99 zMT?5a@!ggxLq1z@)7+{7HdT#`4j!G0N}h^D^r~PF)c6e#f$c@v5Bvd+X9S-W5KNi& zGh(?~!tTlN@Mep#ptDGD%xn+K?)}Ml5por6j}BXL_p1}L@v~argQHLgn^!dnrcmW` z_@p*Bsdavk%B|~GUM?++K8YPZ!{;zC((`~764;R@ynq`zOP!b{B7OB~K%eouVgDv$ zx~v!$p3x3cV9RL{u z%NE;qmn+@T{wozB7NugDI1}IjuCnxSwnT&TaWYpzvoO;1Yl#{%%-Ny{?^!S8w7%1tIAtu;R6l7D4K-JH`3qeqj{r=uk=!lq4@ zBc_EK`9*EcR~Z&c#>uXN2&GiU=x=nNe`>B&D@ma+Lkau5Mz&r@a~v}?k`P*u21}aW zB*Y_x$+9`XvSLrx+0O{&YVmeT1)l1N{4g~eeG)gBz*thTxX*>L&*|#K60wy#C$+Ka zaU-@_3v{>Y0lQ5=^+q`>Q78X1ptm+&osYjgSH7Q!^(iC!fhdY}^V!Harn#F4a(X<{&Ku$e8W$#`S_c6E3B8j978w(hqDr7Oa>EQFQQHHM zLGr>L8OlmaVFt&BgG5YufEjmul~&96KUUQD%m8R4>Ju&~({{NWK49Fdq(ln+4bL~# z#!qSjAVZNcFd)IPs2w0D{*u@S7D8_`;)H^DPx4YA$ABD`;z0RAtJkevWPdQ885HQjE zv^un7`qxD%dT2<-)T`R`H(sjL_F-c`eVzlg^%@kRB5{bR&(#DLKJ^}h3AS;%BaBvO{7fV{Eb_i_ zM?ZF)LbY;&-CUuXHNf`mLDHB&e;$sP3pZ#j%En&s(@RyZ{M}ePo6PYm`kVJB%Nf_C z&naBb?Kn+%Z=xT?4JH4-v=s&y8=DFy&>J=7a7EY{&Y4kaxYGi`|tliq?2jmilkGheJ)G9p|U)p@k&95#DWor zW(vej9!?kH)6&WcA5t+NH@k?&EAU~bH!}+mFPmmtiN(pRQd9Y&)lH`|%e*bncVRhx zUE`)Ck_h{BYVYP1*j-Hf1lW!|a)Ykrh6(5D={@5cZ0Y%27q&8Z7fB=~hAR4hp(G=F zJe|h#&Yl@8vpgTbpIS>Ie`|k^*pgCu5w++>XoFq;j{RPdBMByk@AV^p$MZqfqFWX5 z^M@c7Xq1OWP^)T93g`qJd4+j*5o)9uuSvi97^NqGNdOvcOy?2j!^hC}iPx{*Hqsn+ zhPEUws^!VfG^EKnk(VpgMf`2u2y@vSP*0B`+^eg`<&envKLen+aH{yJyQ99%ygc&DE0CPI2IWBh1oz|h%)hNKm^ z>2lD4TYLABJ5}f8;y+8od*g<9+dMLG(Hv}>dc>WY2AK#;{=QXBub|TpG33E9npKa{s;x=8j$=)ChhJ3Cuh)UnpptV#2P6x4vH5JS-ufBta}VF|(hv;oa_=wUUGeL11poKNJ)1O$b3H z_m;%Isj57XU^EoK3D~GL2nbOnWoDr4s7_30jl_J=74Y7Ioj;7U;t^jG6qeY4vm`t> zZ%JztdUSu8GY!dDr)jBXyihno8d$--C?g7RCH9A01~cez$GV{Kvw-yH)~ZXqur~sJ z&n_LFda8I!q=0uN{I+dfBYfx!mY+5{# zVYJg^=QW?_v)Z5GdCecg=><2QM|zJ=2jjDcVch!t^rK^Z`o2zcXddkG>GxYthx|wU zoRgDK9UP`ejMsAh%H+o&j*T#S6a9XMVLU7jd!xy4ZS2zd9)zKKa0lm4gSt=L>|s>- zSWl>qfqn=R2P{=MoYSdyArq8QheG!a76rJ$<6>IbO7(b6Y8Xm z#sETGII;!8@jW5o1DBnqA$*e_n&xot4#aOxqJjXLbK?vG27y>Xzz{xGO4{VpAOHjm z;Y~F#2*eNqhVU^|*e0t60Yi9G2Mhu+gn%J@3>CJ?szKnsZOa*GdnN+GJ^5hz?ECP+ zyXMS^2sobb6j`MZiQdx=6`Y~rO74+6z0#BD%sZv|D3!=-=9yx?+7fO!Spg?j`fzz- zJ-C}(M7ZZ3w0n*xd}-+sSa=<(?7Z`{O~;QPgBt2cB|^)}f9zPPjK)VDDF^RV(o~Xe0TU*Shna2YoI_*| zUhwlMo%T!JLxx1qI_|`}XY^{Tx@uL4v-{KGS5U z5tYXue;keB1T=uoZtTn|y|e2y=IC+Ix~swzCPR&K+)ztqkFS4J>*bJk`0x=*dyvx{ zjtZRa>|~#T&wSDDfm5e+_Of(*4{y(Gte(5wEg*Gd5_7ae)nT8{gWjJ#oTb+5RJ_;- zo;r0xSs|K5Jj)p_eW1z#nr;_8aokmL7%X7ij$y2&eU<~0y z5pm$PCVZr!y5;ZJm3Q!ZhAvtXebJ(Y$`}gQE*cu@)wD+YJNX!6gy*m}bZ8syo^y(v z!}YW@d`i2qNzL0s4RO#m0SRAMcMFzwQdO_#^tmi$GWd1gB}=_G= zd+jt;#;r(p0^F9ot-fd++95a#4M#89aR^H*vD&sAE4p=URcf~Ayyk+HQymFU!J1jK zW~oK0^76Bi3L})->JViQXm5|7F`hg*b{4N#-psp2`XVVQDN);!^XAP{BO#~Ohx=yJ zuBQOj&~O{1hEYaXhh-(F321F3l4-1mfkzsz>^RizDLIHvI@q{T@W$o3qVmjGInrT| zPNAeBLvoc&f)+uJ&RZXhSXlOyL}UZNx?mpxJ{UP@pUQ8#j}$j`#pVys2F;QMG6ncH!(fI~3X!87KYBRx7@PlCSAI&rVV!o)yv9;~@1^{lPIQfh;}`98M$OS6U=T1i?Mxjo2*eZu?BSiQ zEf7=1X)8$%H|`AkZlS;Cd5>^fx*U zWjqZ61_7raP|YFzRj1^}r6~9 z(lh41LBJr;^AI4{jw`NXT#v=Ow$f-tY~nEp7zBDA z0^~Tku0(5b@oCRXqDhfKz#tGU1T@DL;dk!bc@|y%^=L(I;xPyq1bPkvj(`pJ zK<3;aU=Vn)5Fm%i<)0HV6F`U27#W10EY2Cd@=d8 zZQHh83QbuD!ZSd5dHId}{QTP>{KU`)%(X$lAkecBK<~a0L-|ADQW=);40!(h`AU#J z9j7D0J!tM31PlT_4*`&UH}>5B9Vum<4%uO&`yY%T(!=JyLBJr;a}Xd`r%s*vR%a>d z(sy+hFJAm4B6%Jrjw6l;I(v!nGYA+2?jHi2vm^FRAAIn^-n+e}O9bZd9t(M8Ap9gm zXnm*YZZR8wgMdNcz9WEMy@3mKk>>273Fs2x`4AWK#*G_y2wyN6jLU(c5t(a)fI;B@ zivY-e5?gP72)7y^DJ5MR(uY1=R8;gW^!7i(Z+NJmxi$zG1nw^a7{XUlf-Y33rYmp# zT7+lB*s_ZQ@u%bX0}$Wz_+3HUgfR$o0|AiyI(qeYLG*(-w*Q7&7vuffgxB$t_=Saq zW08Q__)P-&`)WUPGzb_3x`zNrUJs6y;`b?4T)!^Sbt<||cpVXsB!6*naW3Zh1-Kl7 zYtHwxkjP#*?~U^=uNfe$aRz}{LBI!&G=U@hC`>iZX-V2e8+0^F)m@U?LHr*`HPelc SbaSx)0000?47VkgOLFpj<<1U=C0Malc5n&Nb&Lyn?=)^QFbc`PhS_!4k62?)_ z72%S~#(hlKs3HtB@Z_@b`KI_TJBf>mio{TD-QF%DE+3FXDX^F8T4Ugi9jWL7g{?*MFbQeFq~MkEC|WmT zFT0eLl?gU|9ybJ@^oXkic=7SkWCVR)?sntK!k@B~G&E9HX1IU22l|QhiuN8mANUzp zTs?dD1$-kQkp1w)$PpKMTz4ATeMOJ!Z150)1`iZAK?8f97kOIV^t$ZYzo+!=f<-An z`8EcOQrsHFo*lG9Wr2O70Nb%2IW$EDc6~Nv_%4TE;pqMr87U|zuqIoSTEeXJ_^*^W zHn{XN=j`mP)28q1xmeSWvZbJ09;XA;n&nr5ID!1;cmm-6+u^a(hTS8@W#{cw$m$=p z3gug;DIRmSg+~IBhqP63t2bOIM2P4RKbTH_(|^7GAf09#kiKd;865Dk>ydYNXUB}J zKl0jP2{ZkF9digm>`@68ioE=xuWmIuFUj@AcgAN1Z2Oqxu0Bby(Bgl!b}yW__3{3S zL3XSOgFujUrM6283jD_Lhy(%cZx$}xBN7O0`c3r`viz7ZPfXd2b7Ch=9`CCjCmGAc z$hOr-xs1C)7ZwiA7TmEN^ig@AsU0pD9age!DFInfPx3J}_z0K2R7z+)tOPin!Y|AA8>-n;MkzIn{f4WdJ^fH(cZRgGM@y5p+BYrpD5^Y)<2 zOZ&b9sR#02G!ER~RKdXr;NKI?4%=u3m8XFKJ@e&s@@&xlVdRErYumqHf_{o)RM6faGf6MtCNR#$BxI^&(M1D2 zz0E#nVqFo%%zH|5K|M_&dAsIfqJPQOW1gMc3X*;v7{vT8G2KaoXH+t-Y7*@>??&jD zzYKZQD!SiIKwoMb=QTx9T|O1Qk3W7{w%yzi$uCmsDf2XLzEx+SOqbi8m zo0ynLG#-hO>2N)VN^uTIV5O#(GBP%%*XLkxhh=b*eSJ6&3?Z`!MJ9rR>zd0CEUGjywIyssm)8#P_E9YBR>uls673L^ljjLwMkC!?FuseyKVd3(JZ?~4mrF9L4@~4 zyUSb;5P0p*f8yiwyq`MPV6J$)R7GD~17IZx=`=F1<=#$ z)bbd#yImIl{IyZuw!!rLuyzSW<+ktE^O~JdP}BXA;W=i*5Q0L&gh8!PDaj(c=5pGd zYM$#OSsQD>iMY}67-zfIJn@%ni-CyGwQ|XEDw9jG*?z0~f?coq<7GdZSU^&N@4V6K z7!{*rK3{+DFc-)~oy{TQd4KW^$XPhFl*^n=vTGF!b~=S3^hP?7%A9Ilola>0>{7|` zz7e`v+xkF`wskYKN6UGt|E1OGUUzpqF9(f6T=_DlhDpnkjzSL#PFGvALX>Xt(~(_rCori$!hlulCn~a+}6zSxF8)Dh8Bh zIYALpkpOUcPYbWCNr|r3=Am;Lv$;yhJk@4z6z%3_f_W^9XBu|0T4t-;r=0WO;N8`> znB$VRd6`>>5WeyEH$1%ba*;!USFfFp&Dw34g<1LM z#hU6=0k4NK`kGF1u@FS%XtZy*1R2ZvUXb_41QNc2#uT+4w`LL+#Xq*i3L1zH4$KM_ zgcuj7w4}IQhs?P)oEz>=mksnzJA-fec7hHu2SMh!Rr+&v#(0i*N6Mwz8^WFBi{u0S z$YI$~oF4g6!~*biAgfY+{i5Of^EI3*vzXeUqH^Le7MuOo`_o8lUnKqNCZd0-J#R$- zDyj}E|d>OEQO7` zSFv;B`x8#t{@UbR0Wa4X_(Pn=zn=iOm2ci}*CT3yQ@p*8zzr86G2;nPwyaCerTfhu z`+g*Iibxf+uuo=jdz|;k*H*zu^;^ z61>#&*iSA;7=K-0ISySNG0$aG_w~FxBEu9u%oS>o6&vy#ZG?xvQEx|_b`&>S4|lLO zj}ALFKzlieG&0x^#?e<}+>9d|@uwkvzK#V(b;}i0>UCfnCXJ6#D=IM(yeE1?{r-4+ z{3iKedNh+yDtG@7Znbro%dD=Fgfx^R;AJvfAW>dKxedXiLDd~3MeHHYDXa!V0(^6D z`RfeH(|of9?9o4%%8tb5k^Ok=`5YTh-g^3ZC`?w#1orxNe0*HKu67?VM8X}??30?D zs}g&e>=IzbhO8zIxUj+ZU9u>jSkPN*pO%bF1TI{?qFXf~i_SZhNpBYHM~Y11 zl;44a*5byO3Zb#ofBxVp)#SnY`?@qDbbs|xt>1n%Tw_Gxz%%1GfW(!z|B5*0jrPJT zr11>^_Zv`6=(g@~66{!OyjZSY-ZRuaNtvo!{=&oDb(f%$@7yz<^P~KyjjcA17G7!p z^*0h;XDy`aBL@ijC{C@BR_*we+>+h(QsUorZ|d(8<6LHwT7MGh?Yo~t63ErwD~9zr z5t|5NU}jM05n;?kpKI{jZ9fYdcRS3w3{%5=^d6i*M0@|%nwIq|fkN-=-ERfzP>YrU~m`cH@zvf@%-s^J0E$wIAS+ zdk~H~s(Oxj!FSTl5Q<~FrspH|=5^~+->z*Rdl74|mo|LbpSyhU<0&!7hNk<~u8ExU z6fIajSJ3AQSphZex5fsz>^%~}_cDKWD=$u-1oxHNd|9PgX7&4^Aa{!huSKnyqEvl| z?S$eTvz!&Iq61#XJ?Z4FbawMnCisqkWRL1q7n5{~b9|Bh)QK=OiaayspgsLE{W*`r zzv3kH2Kqkt>TORuk&Cc&2(nF4Rg@w|LY{v!IB)Ler%OKsUsnOKT@vQF%?pK+(USbB zqY0Fe!5Lr%Pi==&YSCdEJ$CX6LSJedHxRmvu|GvWsp2=meysJQH`(?}H zxqt4TaP1m2TGWjzer*vbuW5Ds1u#(2zkft9cTZ84tUe0gDDYgWZl^iYeF53VtBsaU zVw5b*h}Oq&*f7-G=(!mg5viC}VdG0N%#+B~IgqBf5Cej!?@H>s2#5zy380(PRJpjo z=aMbM)|3MRZP? zpmMB^IL`0wiEGqblC0mJ@wa9u~+_#QWD)kRNub5$2N#krbk9SzutTKG~*-xrqbV_{y`7jq6XTJ(OBS-qxOI-zj9vnv+wS1>vb z8$6+fF>EW36&7yP?+q~T-)kO>eZKE9g@)35u&G4DXDd>+OF9u^3S0uvV`U??=#_I`ly*x(kP}l^&b;(%q`Z z9?7Ey3J0Qw<;fZ_dsiVX9Yf0Boa^pIz^MX2gt!W_|P|0^V7^LwQf3oKM2kL!q;c zH2G*viIHsF9IQX?M^1OA^awbn>N*bbB)~g1T_#a!>N<6IzI>?8*$bmA4*d=gE+Soo zeB`GykLi%6itW)N_PFZ7nwB+GIi7_%9M#5PN|tf=-Au|IeYFQxHm1;UK-qV-D-~zE z2Fs`aPL~&Ycl0<0wO~O5JI_A>WK7z=4u7j0=b1mk*g8axj6D8bjKmL1I37G9>=!us z(rqyql(2xHei*KnrH<;Q7zGL$^^#|Wfos!TA@rRN5Z2d|XJcjr9 zIM&3BX$l^CvR;D?((Vs5VcCD1?KgGYoxV^*cRIT2D}B2q1+n5nt5_Z~-j6$oL65Io z7m^l{8}h-+u&%~p`;fiAJf@V*uvug>u~SI7+LqjFhg#Y2x>_-Bcsd9CB>6Vc$Y(k| z_2GEf#Wmhx?*n+;dQXeO-Qm_x9Kg%kfn{r-5&w zosS3PZO2^8vay?bd%=gB2?pRy$-f&Jay3*QB^d~yN2=S;etijqbJjcB#*G^k>Cw}>N?aMNYC zx9;~ScGBpV)-SOm@Wc4-;T%jX3Ux^j=kntRp$L^u!rP|oAMDAAst_*N=fE5$B~NIM zozdH&#q!~&I7jYt0iV*>U6=3UE-2@!yl|dhcYz6eNm@LFGS%WnSZej*CKOFA0njZdcD1U zJLVqNxD3W{`JN>!C$!IVyO+9A}{3xil?d68O8*Lov z@R}|CBR=ol(R{?a{Vv?WH3s14sRCfqpD#6wg}C{tP<9~-+%`p#`#vb5k-OGyi3b

    &!!iwI<0Cn=nlajrb83FZJ0RU5`%rUGS$sNI?R8ZQ=^4AwJKhoh>_${s5A5dCJFF)9MX^DVNR`WQE99$u z0DK6Td_B_k@aiLMdpkk+gBnUdLu)FdS7)t4WqF>sKSw{6h;jfV|Mb4HJYMZ$pX0$( z^_{GZzY;8+)I@>}QtsC*9_)7HkiM#+z7#X~U~0SBA=qnciyd!Gl#&gvzqk3JqaG60 zi}eSfzQG!o_4sk`=J-fL+^;<6Wjg#VF$pvK^J0WMj8wDT`8*Guc?dUfW0Mgd~+nGXbcd9v3gXBx&i`pzS}3`=)VH%pF@cpcU^#S4O4<*AyK%bF!Ly~ z@(|Fpb!pVJl-isD({Hb)ptfBTp3j|z{R{(Ls#pPjDMttN^SnNvy}}`bJQ4XgpomdJ zI;~{;YEZpoNDp(jH*`C;y^;5?__u=xkOfo%of%xC(5)>hY_PC6>@?KG4E2HYtnuf2 zmWm+h_nKvkV;_?TOtkw=)?HIPj%n;?HN^Cfi}S=6?F;zdOx8f>*5{H+K`j z)P~|+Vd=2joGu3_mf}tBjiGc%cjvU9 zUi$`)FG-1eu*I8Z9~Qil=QtCYM>;KHX+XgTbHdJ9eH91HhkGu4qy%3-PA*f&sY&+z zmN5DzpYqO^H=TO-kCM3t14YWNbXNuzg71!~!w~4nDf#|8ec!rZoKxXSqEdn{fdQb{ zve6b_t7Vuwfv>ri#t09_X6(!gXlsM&?fz2nz(G2v)LY#@R+v*ex|N1B|UOj7aper(1jNfabm?BB!<1U&cb5ud9ki&`dz_UU}nko&!K3}VPU5j#&n|^ zgR>r)cPy}nM5@4O9h6gq)%I%r&?s$XftjY|%}EQy6Ms4^qnM0r3h~>8*~tTzcz3#| zpKUp55qEyV5mpGu=6GL=>@yq9XW&EKZtS31%oP^9w?j|+2_ea8L`H&ys`xXjSmPI@ zFs|L{6J{cL+$vJ-_5+YGva^K&bsvB2heZ;oJB-@pZ)4%udJnCuJTck%33W$C@rDj&_ zt8cIXzk=wbsI1AGIZUs)5vKq?%|niSig*S#^}phJ7vFB=szip6!fkjuGX4%gR;21Y zYl;Pm-1Kw%Op|!*2)azVSpB3kLtj@PAt;CD>Oq7ML9@W@uUNoBlMWcxJgi9FR1oB2 zt$3~Uq!#DK`cc~|8dM?b-g6cAkTMV>=8VQW*rbUhmE1Q}j>qv;n+(l@zrVs}h2D46 zTjD97N<}Xb4vR?*BpQrxKZ#AvbpDEZlHs(wMJfzMCsm3_{J)sER4;1xVvmukaG)?H zM95XLRvP_Rr2PL!nulF@*Brm2ff7oaCz9tvp@h(HtWC>pzcsBOeRgV2ejPP`|4>Xt zcpMzTzQ>-ltS{-MA0{zle$OB_$}Lu;8EP&+ppWc@hrf$AUt3~1Em!LH3q>{6JLIc) zgVHnNJ;Pv!dH4B#smcSi;B9QAB}gwi$Zc&YWeqaJ`ajEkpG=8jTa);24Y{R`KH86% zuM`J(v~YBgkV~I0Y7TD~a=?bp%iN>h_2}c;M`!mmZnA*QpIyXceLxOqV)*HNKT_QD z!{Ma_k4`Aw{UA4YHKnK9qs-ouk&@7X_R(@TG5wgdTCyK!Xi$?acCGozOf-Kg=xo~h zl`7MT`{3`_*q=oO8bZtDo<)c7s?A--6n!Sp=yXdRgW#7}rbYMJ7yg?DRyEnQmJI!a z0ZYNjS8IH&wwPF1F6(rGK$&tWtBe0KKtFmah9nxmA4cqw0{l`2@toa~CrulO3 zihGoQb4yRD3qkI^#Vt!U;aB1nOwd>jix<Q=ymyH@gyn*v+pb#0QsX zm+|4OahUwMa^xzYuhoGOF6?El#l1~BAGH5LjPRjF=$pA(R+O~s_|zMljGs^cXG@i| z{>pCh7?Ix1)s+@rEiAN>t({VUsy;3Tb=_5_I>f{X>17uIg6R|!AwK-$h0Jxtm6Tyk zEzIcWuwc#lgU>Gs#7C6qWNR-klm3#ndfo{u+JGW;G@3n$zq*K!+hPa3{2Z{2uuz-e zJgFq~$+xM8i@ZI6JA{tN{ce)V0kQTWZj`!W_ztHc$`7EY+}sCZFkccOz{_);K{73*gdDA?YjC38mq!gm8ZiVZtgm zbY8n0VF0fS%#O+MKvAnC=Rx;AwLdN{im{!nO#1ElKp}+X=xsWuHQcgv4n&B*sNi#n zKG&zO;uF|WjX?KWD41xCzcfIrrc5>>2p#=(Rfe7;0>;yNIaBuj4R&rxHnOTM#g=wx zUg(g_SrFrpbXei1kiWA*M0M|i>5{vMkTcPDc^OPsj3bDmqFt*aH!Zl_pcQB;@v*)F zHqdTnfA8_c9Ma3QYm~QDXrKIXlo#J`MMb?M^H=!a7T-D_uJ3c7bQH}OsF)yfBCk;b zQVX_F$%R5*iT^|<7VV`eA{om?O8zWUlh6J2RKOFAZn=z6#ft_y9SFBn-QbP#zDXyT z=W$K-e+^FCJT7pN;h&T`ptU?xNXNxb$o*F7p@NXl5dv!<*i`ehQ3HIK+$Dpk-SK7zv z?(KTlc>nzRny!Xnq@jD(wHSo*OxH~+LD@ra-@M#!(`Ktj_Hs~Hj~nPv$z{X9eUz#S zs!GKf8J_qYPMYAtD{`FAfEW|rDC7WocW};(z_x&wxZoD`baoJ^0!_i^Rp3Im6{F23 zd{|!hC#({u^a2a_&ASS*>SX?OjXtT<->Mh!yDQXZJ;@;d6xDEu-$IE>Y2a(yi<-XO zTa$r94;;^qNtKsUg0)!kpuJgC&Pt8(dM?Fk==qEDkZKiiy3HtIrFKU$V7$kM7-B>I zx}5{kW(d5RTQP6nJN0(4U&MopIBPG;79!K{7M^%r{+rA!=7G3N@?8``F$M=aHQ9tw z|BhJ`2lh7e$Z^zV3u{!)cvp?WgMUFx{dty#9|cYvqQ{Y3DzOT`2|oEqV~qo)p={zE za(n}g5%5~X-aV-rO;xw_v=WJVwow$AXTyFOu-D!h+EL)(;bPB9XXB8}pPPkE=@j20 z*E5xPALs53+`us@^JgetgRFXs8{#P!FAxn7OQY?vSp6!K_7Lfsbq;q@|710_e!fS$uRPXawGed48UHSQn&j>EW> zg|TD8OScA5%I%$gI+L3J%34h_Kv?4X_*%?#XD>_89;vxk2G5QI_4MP@;u-1ZLdH(L z=uhAwg&w+e$F-%YqmKb0c{qU{Yw(V!*t`(Dh4_C{y{u7sUWICPLgYHLk}Ki!!mnW+mztHvAYY<@T#*S?u`J9mo3A1tSt zn-aIS8u7lU8Hfhvl@E9)YuJ=oDyL)tL1h*ADbu)DL@ate(BXrN3l-n~OIY$#s-?mEO)SYJVS(k_?bI>W9!#H&uy<}VZ z6xtm@2teW&Cijzd_3=hsuJ7Q| z7=A(VRCCH&R;D*ts2VsI=}0*=#kwDgEJw7Zu!N1jlK1k~)-3v%gFHtgL^u~OTKBH( zO6T5Inu78p8}_-5MwID}qLwgI1@vXDiI|sp=6#UM1fl2NOe(#}w^Iq}{HaijE*N=q z%Uo7=;T}21xUJQ@Cj&gaF5}X8iSqvP=;#%r#6JpO=vav9?s#lRcyqcqv;Ic;4~&j0 zJJ2HB!4uQ;JDp~+v-5N5@1Iv$Z}}v0GFu^7tIDvgr5QIg?^X_pKMrp}Y?O{6o~iLHM`0aA9}UPOlL@ zKpf}uAz{;2&oCoT+h)2<&b%+AvMrSdkU4*hu9-?MdF{Z0ixW-;xJ0 z_up<-M!6CBPpJKSfYH!zEpa=ZbqGEJp|ihTE6j}bc#4Ez*rOv6Au9QhTy6512Sy*8 zaEpZ4D75v5i~=5oPk@Glx*f5?#efC2gpD4dCn0&gB>l7k!LvgUk-7s@Kr0F)pN-m) z=#VMOtLmQAh}SEroQv6htVnc$^YR6TwCix^X0x@75W*uDLCSq12IcL#N&btc7ZL*h z+x+2O92186mayITgTx8{!%TM+Cs*t-aviaU^)z?Ru0dL=>WA{<4F}(6>KSogXtrWS z#RSMhz}O_%$twnK(;>$if0OcK-ssszxae@m zS-88P={k3sj2~5}3lb&%>EX4^ly&?&B2Gu+-K{qM_d}U@HWll(4Tk3Ed0#66Oakqs zo#b+$Cq_vg$&g{geWyHJq7JLybC<1E!YPXWpghL1MELr(L>GQ{5`%5c~su=1mdm0=7zvmc+w4X3%#9%~ z)<|Wr=ESq)?22(rr+gbnum^k#T~F&O-_>vPBf2dP=Plo?2<5kGF5Qp>s$V3n`|+SM zm?75tzJu#k%(LKDy44OrjWNADVxtMg%GTA=kAOMXcxwu$|@GDCs?*|#G4U7&cDtFVQrBX-3 zWti%G3fT(@q{xHs6XHGEB5`c@0R+p>u5AqvP`I$RWSCa2cqCXrSEEWJFeim{83bMD z!r%1#J*Wmrc381OlynBU+3?$5BPEV6h^QowXGLbjQm$G8_JC<{^#y1wd|NhKCjtZI zqNPyeIM(KapTu{`>wk2%%+8#RW1@Msw!D3EV+(75(-^g-5eQa7dCs@u+_!Rg!0vmL zbC^V?0io~T1+~8JBmkh-1iuj^D&d^&PufX#p9+Vm9$0>N6Fh9(&rYk~gCEPR<>yZ- zq;gme5#?mcdv{3QL#fRh0HG)x&uPncJt((a{`;Z2vBTOONZy-atT`9p$$^3o=P%5+ z!Xe|9m8}*Z*^Ns{o!=IC$*NW5fn0dM?lLwcV(^ke?^HEL%CP3U^Tdrii8=;tIoB65!&bl=Oth?Crv}yZs+r)kL_zC;7f4*>c!P!(0`l(cB zQn4mW;~|z};S#@OAA2gtE4{p?8@q;_&5OwHd|J!G3-8<%0e&TT=kE9 zp%G9a1uMi<7x`4di4IbT8$~z*3 zBv@={CogN!(cojOR^ESpJRjjo$z(uCr;#8ogjy)<-Kly;Z=2X)F>_pEophqH;wk6U@rEs}SQTAR4;OGE?H zM9vS%Mjm;VOxH=~O)C9YAW(Rh+>pBMomYv;3cwF}EX{H3A|7&u5BC*^v*3l|5VB4T z#D^oHt5=$~N^HG0{_^TMn7-iJ6ygin`v^_MhnLH7BViUc6iI>rp0ApV5IWFjeeQzy?l8vzwSM@+ID^cmo= z|3E`IT@EC}EGE_$h}1TKSNz?9->J>LK$eo&OnH7MYmQOFxdQ)X*)87Z5UGJf6vqxsOdY$WXmh9~XWo4slv8P}7EHb_H<`>ei}dp~{`)=9mdiMgzi;eBELE%3-l}nNW>3y1brF*vv zD83XQjDZID9jC5Lj!`d_!&$J23fq5uED-pkM>U*%aVgtfPN_msNK#f-w7w{nWp3WQ zd1RfE1t6b9-an`SUM8n5%A1c>c@KiaAbX*Z<%0;UmJUQkk2&1;Z{&=2p+nkwu=vu> z?S}8tMy66JnmJFC^q18?QX?gY#ylug0eu4}CKe>fqX_Wudy&lDagB`}Jf3%@hy>r| zV}w4QprveKw+vw^(EKIzIa#3Vfpr2p2j1~yTP1<(%CkGoP;}^2AWOr>_6Ca*g6Na! zimiMD-r<4!q&m$&7jK?I6yKtWBjaFMsj^0YE5fGDQQ28iv!cdAT)L0@N7Z~i-}>L6JyFlY z*k$?M0dp~OPpkOvTogW!DsMI)>S2VeDGP02tNevGpBxjvpg}cHTi~BTEh5AhWh9(# zMomu6t-s&zB@!=Bmuphf(s0SzT5Le%+S=L)Nl5^&hco4^EuY71=d%^%5JWukFWP#& z^Rdbo*EZy;s;c-mOSGhR&A*sT6!S3uhOv;}Bc+Kck85jQ`D+CMel!H4u ziU4>Tq1(SBdTPO}4Ehl~cS=(W_+Go83o$BYV~hz24;l=v^}A+RBO2hLwyonufA}@@ zf2uQ_KAxv;$^S^q&&K#SFXg=GsRp-V2CP-=_Pn>c&w+yo_dZ);)vh-Qckj~2<9Dx} zOk))jd^w`{kMEgq@kL8Y$QKL!j>}H*rOtk(QGfxFy|b9S^j8?W5Cj}|{#j_FRV~^u zQbLx9lAd} zrk^edmH1-eJ?eIXep2DdVSA$RMTrf{6$?d{na&X?vft{K?YCd471{3HvVkfccbbWT z>ANCo-9X~-HB7Q~);d%W=I*za9`-=T|6F+n}56wr3e116nCI7A{qF=)

    k|J?LYIKG=CntU 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𝔡jM$ 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

    Ns<1aELA2<%{9E<3%*Y3=C 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 Date: Wed, 29 Jul 2020 11:29:26 -0400 Subject: [PATCH 70/89] Remove PanelAndroid account code. Replace with Hub/Account-Web --- app/panel-android/components/PanelAndroid.jsx | 16 +- .../components/content/Account.jsx | 220 ------------------ .../components/content/OverviewTab.jsx | 5 +- app/panel/utils/msg.js | 10 + src/background.js | 9 + 5 files changed, 23 insertions(+), 237 deletions(-) delete mode 100644 app/panel-android/components/content/Account.jsx diff --git a/app/panel-android/components/PanelAndroid.jsx b/app/panel-android/components/PanelAndroid.jsx index 520f79058..0caf7c495 100644 --- a/app/panel-android/components/PanelAndroid.jsx +++ b/app/panel-android/components/PanelAndroid.jsx @@ -12,7 +12,6 @@ */ import React from 'react'; -import Account from './content/Account'; import Settings from './content/Settings'; import Tabs from './content/Tabs'; import Tab from './content/Tab'; @@ -23,6 +22,7 @@ import { } from '../actions/panelActions'; import getCliqzModuleData from '../actions/cliqzActions'; import handleAllActions from '../actions/handler'; +import { openAccountPageAndroid } from '../../panel/utils/msg'; class PanelAndroid extends React.Component { constructor(props) { @@ -171,17 +171,6 @@ class PanelAndroid extends React.Component { wtm: tracker.wtm, }) - _renderAccount() { - const { summary, settings } = this.state; - return ( - { this.changeView('overview'); }} - /> - ); - } - _renderSettings() { const { summary, settings } = this.state; @@ -238,7 +227,7 @@ class PanelAndroid extends React.Component { summary={summary} blocking={blocking} cliqzModuleData={cliqzModuleData} - clickAccount={() => { this.changeView('account'); }} + clickAccount={openAccountPageAndroid} clickSettings={() => { this.changeView('settings'); }} callGlobalAction={this.callGlobalAction} /> @@ -271,7 +260,6 @@ class PanelAndroid extends React.Component { return (

    diff --git a/app/panel-android/components/content/Account.jsx b/app/panel-android/components/content/Account.jsx deleted file mode 100644 index 6ebb741c1..000000000 --- a/app/panel-android/components/content/Account.jsx +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Account Component - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2020 Ghostery, Inc. All rights reserved. - * - * 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 - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { ReactSVG } from 'react-svg'; -import ClassNames from 'classnames'; - -class Account extends React.Component { - constructor(props) { - super(props); - - this.state = { - view: 'login', - }; - } - - _renderAccountHeader() { - const { clickHome } = this.props; - const { view } = this.state; - - let headerText; - switch (view) { - case 'login': - headerText = t('sign_in'); - break; - case 'create-account': - headerText = t('create_account'); - break; - case 'forgot-password': - headerText = t('forgot_password'); - break; - default: - headerText = ''; - } - - return ( -
    - - {headerText} -
    - ); - } - - _renderLogIn() { - const { - email, - password, - emailError, - passwordError, - handleSubmit, - handleInputChange, - } = this.props; - const emailInputClassNames = ClassNames('Account__inputBox', { - error: emailError, - }); - const passwordInputClassNames = ClassNames('Account__inputBox', { - error: passwordError, - }); - - return ( -
    -
    -
    -

    - {t('hub_login_header_title')} -

    -
    -
    -
    -
    - - - {emailError && ( -
    - {t('please_enter_a_valid_email')} -
    - )} - - - {passwordError && ( -
    - {t('hub_login_label_password_invalid')} -
    - )} -
    - { this.setState({ view: 'forgot-password' }); }}> - { t('forgot_password') } - -
    -
    - - { t('hub_login_link_dont_have_account') } -   - { this.setState({ view: 'create-account' }); }}> - { t('hub_login_link_create_account') } - - -
    -
    - -
    -
    -
    -
    - ); - } - - _renderCreateAccount() { // eslint-disable-line class-methods-use-this - return (
    Create Account
    ); - } - - _renderForgotPassword() { - const { - email, - emailError, - handleSubmit, - handleInputChange, - } = this.props; - - return ( -
    -
    -
    -

    - {t('forgot_password_message')} -

    -
    -
    -
    -
    -
    - -

    - {t('invalid_email_forgot')} -

    -

    - {t('error_email_forgot')} -

    -
    -
    -
    -
    { this.setState({ view: 'login' }); }}> - {t('button_cancel')} -
    -
    -
    - -
    -
    -
    -
    -
    - ); - } - - render() { - const { view } = this.state; - - return ( -
    - {this._renderAccountHeader()} - {view === 'login' && this._renderLogIn()} - {view === 'create-account' && this._renderCreateAccount()} - {view === 'forgot-password' && this._renderForgotPassword()} -
    - ); - } -} - -Account.propTypes = { - summary: PropTypes.shape({}).isRequired, - settings: PropTypes.shape({}).isRequired, -}; - -export default Account; diff --git a/app/panel-android/components/content/OverviewTab.jsx b/app/panel-android/components/content/OverviewTab.jsx index fcbec213e..bc1a65d4b 100644 --- a/app/panel-android/components/content/OverviewTab.jsx +++ b/app/panel-android/components/content/OverviewTab.jsx @@ -191,11 +191,10 @@ class OverviewTab extends React.Component { ); - // Remove `flex-dir-row-reverse` & `hide` classes when you add back the accountIcon return (
    -
    -
    +
    +
    {accountIcon}
    diff --git a/app/panel/utils/msg.js b/app/panel/utils/msg.js index c966af4f1..8582ab152 100644 --- a/app/panel/utils/msg.js +++ b/app/panel/utils/msg.js @@ -150,3 +150,13 @@ export function openHubPage(e) { sendMessage('openHubPage'); window.close(); } + +/** + * Send a message to open Account-Web if signed in or the Account Hub page if not + * This should be used for messages that don't require a callback. + * @memberOf PanelUtils + */ +export function openAccountPageAndroid(e) { + e.preventDefault(); + sendMessage('openAccountAndroid'); +} diff --git a/src/background.js b/src/background.js index 6e65cd5e7..f6dc50bd5 100644 --- a/src/background.js +++ b/src/background.js @@ -1040,6 +1040,15 @@ function onMessageHandler(request, sender, callback) { utils.openNewTab({ url: hubUrl, become_active: true }); return false; } + if (name === 'openAccountAndroid') { + if (confData.account) { + utils.openNewTab({ url: `${globals.ACCOUNT_BASE_URL}/`, become_active: true }); + } else { + const hubUrl = chrome.runtime.getURL('./app/templates/hub.html#log-in'); + utils.openNewTab({ url: hubUrl, become_active: true }); + } + return false; + } if (name === 'promoModals.sawPremiumPromo') { promoModals.recordPremiumPromoSighting(); return false; From 6f86ba396c7f3098adb425daecd45066332f893d Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Wed, 29 Jul 2020 11:37:45 -0400 Subject: [PATCH 71/89] Update tests --- .../content/__tests__/__snapshots__/OverviewTab.jsx.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap b/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap index 2ff8423a9..d345de9e4 100644 --- a/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap +++ b/app/panel-android/components/content/__tests__/__snapshots__/OverviewTab.jsx.snap @@ -8,10 +8,10 @@ exports[`app/panel-android/components/content/OverviewTab.jsx Snapshot tests wit className="OverviewTab__NavigationLinks full-width" >
    Date: Wed, 29 Jul 2020 12:06:15 -0400 Subject: [PATCH 72/89] add support for new Ghostery android UA string --- app/hub/Views/SetupView/SetupViewContainer.jsx | 3 ++- src/background.js | 13 +++++++------ src/classes/BrowserButton.js | 4 +++- src/classes/Cliqz.js | 3 ++- src/classes/ConfData.js | 5 +++-- src/classes/Globals.js | 8 +++++++- 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/hub/Views/SetupView/SetupViewContainer.jsx b/app/hub/Views/SetupView/SetupViewContainer.jsx index 4d606bb42..2a0e65021 100644 --- a/app/hub/Views/SetupView/SetupViewContainer.jsx +++ b/app/hub/Views/SetupView/SetupViewContainer.jsx @@ -29,6 +29,7 @@ import SetupDoneView from '../SetupViews/SetupDoneView'; const { BROWSER_INFO } = globals; const IS_FIREFOX = (BROWSER_INFO.name === 'firefox'); +const IS_ANDROID = (BROWSER_INFO.os === 'android'); /** * @class Implement the Setup View for the Ghostery Hub @@ -109,7 +110,7 @@ class SetupViewContainer extends Component { actions.setAntiTracking({ enable_anti_tracking: true }); actions.setAdBlock({ enable_ad_block: true }); actions.setSmartBlocking({ enable_smart_block: true }); - actions.setGhosteryRewards({ enable_ghostery_rewards: !IS_FIREFOX }); + actions.setGhosteryRewards({ enable_ghostery_rewards: !IS_FIREFOX && !IS_ANDROID }); actions.setHumanWeb({ enable_human_web: !IS_FIREFOX }); } diff --git a/src/background.js b/src/background.js index 6e65cd5e7..24073350f 100644 --- a/src/background.js +++ b/src/background.js @@ -65,6 +65,7 @@ const { } = globals; const IS_EDGE = (BROWSER_INFO.name === 'edge'); const IS_FIREFOX = (BROWSER_INFO.name === 'firefox'); +const IS_ANDROID = (BROWSER_INFO.os === 'android'); const VERSION_CHECK_URL = `${CDN_BASE_URL}/update/version`; const REAL_ESTATE_ID = 'ghostery'; const onBeforeRequest = events.onBeforeRequest.bind(events); @@ -208,7 +209,7 @@ function reloadTab(data) { * @memberOf Background */ function closeAndroidPanelTabs() { - if (BROWSER_INFO.os !== 'android') { return; } + if (!IS_ANDROID) { return; } chrome.tabs.query({ active: true, url: chrome.extension.getURL('app/templates/panel_android.html*') @@ -1376,7 +1377,7 @@ function getDataForGhosteryTab(callback) { * @memberOf Background */ function initializePopup() { - if (BROWSER_INFO.os === 'android') { + if (IS_ANDROID) { chrome.browserAction.setPopup({ popup: 'app/templates/panel_android.html', }); @@ -1645,7 +1646,7 @@ function initializeGhosteryModules() { conf.enable_ad_block = !adblocker.isDisabled; conf.enable_anti_tracking = !antitracking.isDisabled; conf.enable_human_web = !humanweb.isDisabled; - conf.enable_offers = !offers.isDisabled; + conf.enable_offers = !offers.isDisabled && !IS_ANDROID; if (IS_FIREFOX) { if (globals.JUST_INSTALLED) { @@ -1685,8 +1686,8 @@ function initializeGhosteryModules() { setCliqzModuleEnabled(offers, false); } - // Disable purplebox for Firefox Android users - if (BROWSER_INFO.os === 'android' && IS_FIREFOX) { + // Disable purplebox for Android users + if (IS_ANDROID) { conf.show_alert = false; } @@ -1750,7 +1751,7 @@ function initializeGhosteryModules() { if (globals.JUST_INSTALLED) { let route = (conf.hub_promo_variant === 'upgrade' || conf.hub_promo_variant === 'not_yet_set') ? '' : '#home'; let showPremiumPromoModal = conf.hub_promo_variant === 'midnight'; - if (BROWSER_INFO.os === 'android') { + if (IS_ANDROID) { route = '#home'; showPremiumPromoModal = false; } diff --git a/src/classes/BrowserButton.js b/src/classes/BrowserButton.js index ee42462fd..96a290055 100644 --- a/src/classes/BrowserButton.js +++ b/src/classes/BrowserButton.js @@ -21,6 +21,8 @@ import { getTab } from '../utils/utils'; import { log } from '../utils/common'; import globals from './Globals'; +const IS_ANDROID = globals.BROWSER_INFO.os === 'android'; + /** * @class for handling Ghostery button. * @memberof BackgroundClasses @@ -38,7 +40,7 @@ class BrowserButton { * @param {number} tabId tab id */ update(tabId) { - if (globals.BROWSER_INFO.os === 'android') { return; } + if (IS_ANDROID) { return; } // Update this specific tab if (tabId) { // In ES6 classes, we need to bind context to callback function diff --git a/src/classes/Cliqz.js b/src/classes/Cliqz.js index 7ce2f20a9..013889dbc 100644 --- a/src/classes/Cliqz.js +++ b/src/classes/Cliqz.js @@ -18,7 +18,8 @@ import globals from './Globals'; const IS_ANDROID = globals.BROWSER_INFO.os === 'android'; export const HUMANWEB_MODULE = IS_ANDROID ? 'human-web-lite' : 'human-web'; export const HPN_MODULE = IS_ANDROID ? 'hpn-lite' : 'hpnv2'; -// override the default prefs based on the platform + +// Override the default prefs based on the platform CLIQZ.config.default_prefs = { ...CLIQZ.config.default_prefs, // the following are enabled by default on non-android platforms diff --git a/src/classes/ConfData.js b/src/classes/ConfData.js index 7c49a60b8..c542d5090 100644 --- a/src/classes/ConfData.js +++ b/src/classes/ConfData.js @@ -21,6 +21,7 @@ import { prefsGet } from '../utils/common'; const { IS_CLIQZ, BROWSER_INFO } = globals; const IS_FIREFOX = (BROWSER_INFO.name === 'firefox'); +const IS_ANDROID = (BROWSER_INFO.os === 'android'); /** * Class for handling user configuration properties synchronously. @@ -111,7 +112,7 @@ class ConfData { _initProperty('enable_click2play_social', true); _initProperty('enable_human_web', !IS_CLIQZ && !IS_FIREFOX); _initProperty('enable_metrics', false); - _initProperty('enable_offers', !IS_CLIQZ && !IS_FIREFOX); + _initProperty('enable_offers', !IS_CLIQZ && !IS_FIREFOX && !IS_ANDROID); _initProperty('enable_smart_block', true); _initProperty('expand_all_trackers', true); _initProperty('hide_alert_trusted', false); @@ -134,7 +135,7 @@ class ConfData { _initProperty('rewards_opted_in', false); // Migrated to Cliqz pref myoffrz.opted_in _initProperty('settings_last_imported', 0); _initProperty('settings_last_exported', 0); - _initProperty('show_alert', BROWSER_INFO.os !== 'android' && !IS_FIREFOX); + _initProperty('show_alert', !IS_ANDROID); _initProperty('show_badge', true); _initProperty('show_cmp', true); _initProperty('show_tracker_urls', true); diff --git a/src/classes/Globals.js b/src/classes/Globals.js index 0d8894f93..5d2786f34 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -148,7 +148,9 @@ class Globals { * @return {Object} */ buildBrowserInfo() { - const ua = parser(navigator.userAgent); + // Extend the UA library to look for Ghostery Android browser's custom user agent + const uaExtension = [[/(ghostery)\/([\w\.]+)/i], [parser.BROWSER.NAME, parser.BROWSER.VERSION]]; // eslint-disable-line no-useless-escape + const ua = parser({ browser: uaExtension }); const browser = ua.browser.name.toLowerCase(); const version = parseInt(ua.browser.version.toString(), 10); // convert to string for Chrome const platform = ua.os.name.toLowerCase(); @@ -158,6 +160,10 @@ class Globals { this.BROWSER_INFO.displayName = 'Cliqz'; this.BROWSER_INFO.name = 'cliqz'; this.BROWSER_INFO.token = 'cl'; + } else if (browser.includes('ghostery')) { + this.BROWSER_INFO.displayName = 'Ghostery Android Browser'; + this.BROWSER_INFO.name = 'ghostery_android'; + this.BROWSER_INFO.token = 'ga'; } else if (browser.includes('edge')) { this.BROWSER_INFO.displayName = 'Edge'; this.BROWSER_INFO.name = 'edge'; From 1cb8b7337d099792a934dda50e40d113aaf1eaa3 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Wed, 29 Jul 2020 12:35:36 -0400 Subject: [PATCH 73/89] Add Import/Export to PanelAndroid. Need actions implemented --- _locales/en/messages.json | 3 + .../components/content/Settings.jsx | 51 ++++++++++- app/panel/components/Settings/Account.jsx | 49 ++--------- .../components/Settings/ImportExport.jsx | 86 +++++++++++++++++++ app/scss/android/_settings.scss | 4 + 5 files changed, 146 insertions(+), 47 deletions(-) create mode 100644 app/panel/components/Settings/ImportExport.jsx diff --git a/_locales/en/messages.json b/_locales/en/messages.json index af9f2ebf9..1f5212f7a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -833,6 +833,9 @@ "settings_account": { "message": "Account" }, + "settings_import_export": { + "message": "Import & Export Settings" + }, "settings_trackers": { "message": "Trackers" }, diff --git a/app/panel-android/components/content/Settings.jsx b/app/panel-android/components/content/Settings.jsx index fd4da29f9..cc41c9da2 100644 --- a/app/panel-android/components/content/Settings.jsx +++ b/app/panel-android/components/content/Settings.jsx @@ -14,12 +14,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { ReactSVG } from 'react-svg'; -// import ClassNames from 'classnames'; import TrustAndRestrict from '../../../panel/components/Settings/TrustAndRestrict'; import GeneralSettings from '../../../panel/components/Settings/GeneralSettings'; import AdBlocker from '../../../panel/components/Settings/AdBlocker'; import Notifications from '../../../panel/components/Settings/Notifications'; import OptIn from '../../../panel/components/Settings/OptIn'; +import ImportExport from '../../../panel/components/Settings/ImportExport'; import Help from '../../../panel/components/Help'; import About from '../../../panel/components/About'; @@ -27,7 +27,7 @@ import globals from '../../../../src/classes/Globals'; const { IS_CLIQZ } = globals; -class Account extends React.Component { +class Settings extends React.Component { constructor(props) { super(props); @@ -106,6 +106,9 @@ class Account extends React.Component { case 'settings-opt-in': headerText = t('settings_opt_in'); break; + case 'settings-import-export': + headerText = t('settings_import_export'); + break; case 'settings-help': headerText = t('panel_menu_help'); break; @@ -149,6 +152,9 @@ class Account extends React.Component {
    { this.setState({ view: 'settings-opt-in' }); }}> { t('settings_opt_in') }
    +
    { this.setState({ view: 'settings-import-export' }); }}> + { t('settings_import_export') } +
    { this.setState({ view: 'settings-help' }); }}> { t('panel_menu_help') }
    @@ -227,6 +233,42 @@ class Account extends React.Component { ); } + _renderSettingsImportExport() { + const { summary, settings } = this.props; + const { pageUrl = '' } = summary; + const { + exportResultText = '', + importResultText = '', + actionSuccess = false, + } = settings; + const settingsData = { + pageUrl, + exportResultText, + importResultText, + actionSuccess, + }; + + const actions = { + exportSettings: () => { console.log('exportSettings'); }, + importSettingsDialog: () => { console.log('importSettingsDialog'); }, + importSettingsNative: () => { console.log('importSettingsNative'); }, + }; + console.log('ToDo: implement actions and add settingsData elements to reducer', this.props, this.state); + + return ( +
    +
    +
    + +
    +
    +
    + ); + } + render() { const { view } = this.state; @@ -239,6 +281,7 @@ class Account extends React.Component { {view === 'settings-adblocker' && this._renderSettingsAdBlocker()} {view === 'settings-notifications' && this._renderSettingsNotification()} {view === 'settings-opt-in' && this._renderSettingsOptIn()} + {view === 'settings-import-export' && this._renderSettingsImportExport()} {view === 'settings-help' && ()} {view === 'settings-about' && ()}
    @@ -246,7 +289,7 @@ class Account extends React.Component { } } -Account.propTypes = { +Settings.propTypes = { summary: PropTypes.shape({ site_whitelist: PropTypes.arrayOf(PropTypes.string).isRequired, site_blacklist: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -256,4 +299,4 @@ Account.propTypes = { callGlobalAction: PropTypes.func.isRequired, }; -export default Account; +export default Settings; diff --git a/app/panel/components/Settings/Account.jsx b/app/panel/components/Settings/Account.jsx index 15273c354..52d4e2e20 100644 --- a/app/panel/components/Settings/Account.jsx +++ b/app/panel/components/Settings/Account.jsx @@ -14,6 +14,7 @@ * @namespace SettingsComponents */ import React from 'react'; +import ImportExport from './ImportExport'; import { sendMessage } from '../../utils/msg'; import globals from '../../../../src/classes/Globals'; /** @@ -44,43 +45,12 @@ class Account extends React.Component { window.close(); } - /** - * Trigger action to export settings in JSON format and save it to a file. - */ - clickExportSettings = () => { - const { actions, settingsData } = this.props; - actions.exportSettings(settingsData.pageUrl); - } - - /** - * Trigger custom Import dialog or a native Open File dialog depending on browser. - */ - clickImportSettings = () => { - const { actions, settingsData } = this.props; - const browserName = globals.BROWSER_INFO.name; - if (browserName === 'firefox') { - // show ghostery dialog window for import - actions.importSettingsDialog(settingsData.pageUrl); - } else { - // for chrome and opera, use the native File Dialog - this.selectedFile.click(); - } - } - - /** - * Parse settings file imported via native browser window. Called via input#select-file onChange. - */ - validateImportFile = () => { - const { actions } = this.props; - actions.importSettingsNative(this.selectedFile.files[0]); - } - /** * Render Account subview. * @return {ReactComponent} ReactComponent instance */ render() { - const { settingsData } = this.props; + const { settingsData, actions } = this.props; const { email, firstName, lastName } = settingsData.user ? settingsData.user : {}; const accountName = (firstName || lastName) ? (firstName ? (`${firstName} ${lastName}`) : lastName) : ''; return ( @@ -116,17 +86,10 @@ class Account extends React.Component {

    { t('settings_edit_account') }

    -
    -

    { t('settings_export_header') }

    -

    { t('settings_export_text') }

    -

    { settingsData.exportResultText }

    -
    -

    { t('settings_import_header') }

    -

    { t('settings_import_text') }

    -

    { t('settings_import_warning') }

    -

    { settingsData.importResultText }

    - { this.selectedFile = input; }} type="file" id="select-file" name="select-file" onChange={this.validateImportFile} /> -
    +
    diff --git a/app/panel/components/Settings/ImportExport.jsx b/app/panel/components/Settings/ImportExport.jsx new file mode 100644 index 000000000..f781200b0 --- /dev/null +++ b/app/panel/components/Settings/ImportExport.jsx @@ -0,0 +1,86 @@ +/** + * Account Settings Import/Export Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * 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 + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import globals from '../../../../src/classes/Globals'; + +class ImportExport extends React.Component { + /** + * Trigger action to export settings in JSON format and save it to a file. + */ + clickExportSettings = () => { + const { actions, settingsData } = this.props; + actions.exportSettings(settingsData.pageUrl); + } + + /** + * Trigger custom Import dialog or a native Open File dialog depending on browser. + */ + clickImportSettings = () => { + const { actions, settingsData } = this.props; + const browserName = globals.BROWSER_INFO.name; + if (browserName === 'firefox') { + // show ghostery dialog window for import + actions.importSettingsDialog(settingsData.pageUrl); + } else { + // for chrome and opera, use the native File Dialog + this.selectedFile.click(); + } + } + + /** + * Parse settings file imported via native browser window. Called via input#select-file onChange. + */ + validateImportFile = () => { + const { actions } = this.props; + actions.importSettingsNative(this.selectedFile.files[0]); + } + + /** + * Render Account subview. + * @return {ReactComponent} ReactComponent instance + */ + render() { + const { settingsData } = this.props; + return ( +
    +

    { t('settings_export_header') }

    +

    { t('settings_export_text') }

    +

    { settingsData.exportResultText }

    +
    +

    { t('settings_import_header') }

    +

    { t('settings_import_text') }

    +

    { t('settings_import_warning') }

    +

    { settingsData.importResultText }

    + { this.selectedFile = input; }} type="file" id="select-file" name="select-file" onChange={this.validateImportFile} /> +
    + ); + } +} + +ImportExport.propTypes = { + settingsData: PropTypes.shape({ + pageUrl: PropTypes.string.isRequired, + exportResultText: PropTypes.string.isRequired, + importResultText: PropTypes.string.isRequired, + actionSuccess: PropTypes.bool.isRequired, + }).isRequired, + actions: PropTypes.shape({ + exportSettings: PropTypes.func.isRequired, + importSettingsDialog: PropTypes.func.isRequired, + importSettingsNative: PropTypes.func.isRequired, + }).isRequired, +}; + +export default ImportExport; diff --git a/app/scss/android/_settings.scss b/app/scss/android/_settings.scss index 303669a52..f13ffe9fb 100644 --- a/app/scss/android/_settings.scss +++ b/app/scss/android/_settings.scss @@ -127,6 +127,10 @@ max-width: 100%; } + p { + font-size: 14px; + } + .s-option-group { margin-right: 0; From ee68cba71e8cc2ba972237343cb77c3da140ac8c Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Wed, 29 Jul 2020 17:23:43 -0400 Subject: [PATCH 74/89] wire up unknown tracker whitelisting --- _locales/en/messages.json | 3 + app/panel-android/actions/blockingActions.js | 62 ++++++++++++ app/panel-android/actions/handler.js | 6 +- .../components/content/BlockingTracker.jsx | 94 ++++++++++++++----- app/scss/android/_blocking_tab.scss | 62 +++++++++++- src/classes/Globals.js | 6 +- 6 files changed, 206 insertions(+), 27 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1f5212f7a..25a272d39 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1273,6 +1273,9 @@ "android_untrust": { "message": "Undo" }, + "android_anonymize": { + "message": "Anonymize" + }, "android_anonymized": { "message": "Anonymized" }, diff --git a/app/panel-android/actions/blockingActions.js b/app/panel-android/actions/blockingActions.js index 0fbf2567e..fdbfa2a74 100644 --- a/app/panel-android/actions/blockingActions.js +++ b/app/panel-android/actions/blockingActions.js @@ -68,6 +68,68 @@ function calculateDelta(oldState, newState) { return 0; } +export function anonymizeSiteTracker({ actionData, state }) { + const updatedcliqzModuleData = JSON.parse(JSON.stringify(state.cliqzModuleData)); + const { antiTracking, adBlock } = state.cliqzModuleData; + const whitelistedUrls = { ...antiTracking.whitelistedUrls, ...adBlock.whitelistedUrls }; + const { unknownTracker, pageHost } = actionData; + + const addToWhitelist = () => { + unknownTracker.sources.forEach((domain) => { + if (whitelistedUrls.hasOwnProperty(domain)) { + whitelistedUrls[domain].name = unknownTracker.name; + whitelistedUrls[domain].hosts.push(pageHost); + } else { + whitelistedUrls[domain] = { + name: unknownTracker.name, + hosts: [pageHost], + }; + } + }); + }; + + const removeFromWhitelist = (domain) => { + if (!whitelistedUrls[domain]) { return; } + + whitelistedUrls[domain].hosts = whitelistedUrls[domain].hosts.filter(hostUrl => ( + hostUrl !== pageHost + )); + + if (whitelistedUrls[domain].hosts.length === 0) { + delete whitelistedUrls[domain]; + } + }; + + if (unknownTracker.whitelisted) { + unknownTracker.sources.forEach(removeFromWhitelist); + + Object.keys(whitelistedUrls).forEach((domain) => { + if (whitelistedUrls[domain].name === unknownTracker.name) { + removeFromWhitelist(domain); + } + }); + } else { + addToWhitelist(); + } + + // Update Ad Blocking trackers + updatedcliqzModuleData.adBlock.unknownTrackers.forEach((trackerEl) => { + if (trackerEl.name === unknownTracker.name) { + trackerEl.whitelisted = !trackerEl.whitelisted; + } + }); + // Update Anti-Tracking trackers + updatedcliqzModuleData.antiTracking.unknownTrackers.forEach((trackerEl) => { + if (trackerEl.name === unknownTracker.name) { + trackerEl.whitelisted = !trackerEl.whitelisted; + } + }); + sendMessage('setPanelData', { cliqz_module_whitelist: whitelistedUrls }); + return { + cliqzModuleData: updatedcliqzModuleData, + }; +} + export function trustRestrictBlockSiteTracker({ actionData, state }) { const { blocking, summary, settings } = state; const { pageHost } = summary; diff --git a/app/panel-android/actions/handler.js b/app/panel-android/actions/handler.js index 12950eb4e..32c27214a 100644 --- a/app/panel-android/actions/handler.js +++ b/app/panel-android/actions/handler.js @@ -15,7 +15,7 @@ import { handleTrustButtonClick, handleRestrictButtonClick, handlePauseButtonClick, cliqzFeatureToggle, updateSitePolicy } from './summaryActions'; import { - trustRestrictBlockSiteTracker, blockUnblockGlobalTracker, blockUnBlockAllTrackers, resetSettings + trustRestrictBlockSiteTracker, anonymizeSiteTracker, blockUnblockGlobalTracker, blockUnBlockAllTrackers, resetSettings } from './blockingActions'; import { updateDatabase, updateSettingCheckbox, selectItem @@ -46,6 +46,10 @@ export default function handleAllActions({ actionName, actionData, state }) { updated = trustRestrictBlockSiteTracker({ actionData, state }); break; + case 'anonymizeSiteTracker': + updated = anonymizeSiteTracker({ actionData, state }); + break; + case 'blockUnblockGlobalTracker': updated = blockUnblockGlobalTracker({ actionData, state }); break; diff --git a/app/panel-android/components/content/BlockingTracker.jsx b/app/panel-android/components/content/BlockingTracker.jsx index fb10a5602..2baebd0e2 100644 --- a/app/panel-android/components/content/BlockingTracker.jsx +++ b/app/panel-android/components/content/BlockingTracker.jsx @@ -163,6 +163,22 @@ class BlockingTracker extends React.Component { }); } + clickAnonymize = () => { + const { tracker, siteProps, callGlobalAction } = this.props; + + if (this.selectDisabled) { + return; + } + + callGlobalAction({ + actionName: 'anonymizeSiteTracker', + actionData: { + unknownTracker: tracker, + pageHost: siteProps.pageHost, + } + }); + } + renderTrackerModified() { const { type, tracker } = this.props; const { cliqzAdCount, cliqzCookieCount, cliqzFingerprintCount } = tracker; @@ -194,7 +210,6 @@ class BlockingTracker extends React.Component { renderTrackerStatus() { const trackerSelect = this.trackerSelectStatus; - // TODO here switch to Anti track icon const trackerSelectClassNames = ClassNames({ OverrideSmartBlock: trackerSelect === 'override-sb', BlockingSelectButton: trackerSelect.indexOf('override-') === -1, @@ -208,6 +223,57 @@ class BlockingTracker extends React.Component { ); } + renderUnknownTrackerStatus() { + const { siteProps, tracker } = this.props; + const trackerSelect = this.trackerSelectStatus; + const svgContainerClasses = ClassNames('UnknownSVGContainer', { + whitelisted: tracker.whitelisted && !siteProps.isRestricted, + siteRestricted: siteProps.isRestricted, + }); + const borderClassNames = ClassNames('border', { + protected: trackerSelect === 'antiTracking', + restricted: trackerSelect !== 'antiTracking', + }); + const backgroundClassNames = ClassNames('background', { + protected: trackerSelect === 'antiTracking', + restricted: trackerSelect !== 'antiTracking', + }); + + return ( +
    + + + + + + + + + + + + + + {trackerSelect === 'antiTracking' ? ( + + + + + + ) : ( + + + + + + + )} + + +
    + ); + } + renderSmartBlockOverflow() { const { open, tracker } = this.props; const { warningSmartBlock } = tracker; @@ -268,37 +334,22 @@ class BlockingTracker extends React.Component { renderUnknownOverflow() { const { - type, open, tracker, - settings, } = this.props; - const { ss_allowed = false, ss_blocked = false, blocked } = tracker; - const { toggle_individual_trackers = false } = settings; + const { whitelisted } = tracker; const selectGroupClassNames = ClassNames('BlockingSelectGroup full-height', 'flex-container flex-dir-row-reverse', { 'BlockingSelectGroup--open': open, - 'BlockingSelectGroup--wide': type === 'site' && toggle_individual_trackers, 'BlockingSelectGroup--disabled': this.selectDisabled, }); - const selectBlockClassNames = ClassNames('BlockingSelect BlockingSelect__block', - 'full-height flex-child-grow', { - 'BlockingSelect--disabled': this.selectBlockDisabled, - }); return (
    - {type === 'site' && toggle_individual_trackers && ( -
    - {ss_blocked ? t('android_unrestrict') : t('android_restrict')} -
    - )} - {type === 'site' && toggle_individual_trackers && ( -
    - {ss_allowed ? t('android_untrust') : t('android_trust')} -
    - )} +
    + {whitelisted ? t('android_anonymize') : t('android_trust')} +
    ); } @@ -316,6 +367,7 @@ class BlockingTracker extends React.Component { } render() { + const trackerSelect = this.trackerSelectStatus; const { index, tracker, toggleTrackerSelectOpen } = this.props; const { name } = tracker; @@ -328,7 +380,7 @@ class BlockingTracker extends React.Component {
    {name}
    {this.renderTrackerModified()}
    - {this.renderTrackerStatus()} + {(trackerSelect === 'antiTracking' || trackerSelect === 'adBlock') ? this.renderUnknownTrackerStatus() : this.renderTrackerStatus()} {this.renderTrackerOverflow()}
    ); diff --git a/app/scss/android/_blocking_tab.scss b/app/scss/android/_blocking_tab.scss index 38d8a5a52..d876f5b93 100644 --- a/app/scss/android/_blocking_tab.scss +++ b/app/scss/android/_blocking_tab.scss @@ -171,7 +171,8 @@ &.BlockingSelectGroup--disabled { .BlockingSelect__block, .BlockingSelect__restrict, - .BlockingSelect__trust { + .BlockingSelect__trust, + .BlockingSelect__anonymize { background-color: #C6C6C6; } } @@ -203,6 +204,11 @@ background-color: $button-trust; background-image: buildIconTrust($white); } + + .BlockingSelect__anonymize { + background-color: $ghosty-blue; + background-image: buildIconTrust($white); + } } .OverrideSmartBlock { @@ -319,3 +325,57 @@ background-image: buildIconRestrict($white); } } + +.UnknownSVGContainer { + position: relative; + margin-right: 7px; + display: flex; + align-items: center; + justify-content: space-between; + &:not(.whitelisted) { + .cliqz-tracker-trust { + visibility: hidden; + cursor: pointer; + .border { stroke: #d8d8d8; } + .background { fill: #f7f7f7; } + .trust-circle { stroke: #9B9B9B; } + } + .cliqz-tracker-trust > g > path:nth-child(1) { + stroke: #d8d8d8; + } + .cliqz-tracker-trust > g > path:nth-child(2) { + fill: #f7f7f7; + } + .cliqz-tracker-scrub > g > .border { + fill: #FFF; + stroke: #00AEF0; + } + .cliqz-tracker-scrub > g > .background { + fill: #00AEF0; + stroke: #FFF; + } + .cliqz-tracker-scrub { + pointer-events: none; + .background.protected { + fill: #00AEF0; + } + .background.restricted { + fill: #00AEF0; + } + } + } + &.whitelisted { + flex-direction: row-reverse; + .cliqz-tracker-trust { + pointer-events: none; + } + .cliqz-tracker-scrub { + visibility: hidden; + pointer-events: auto; + cursor: pointer; + .border { stroke: #d8d8d8; } + .background { fill: #f7f7f7; } + .shield { stroke: #9B9B9B; } + } + } +} diff --git a/src/classes/Globals.js b/src/classes/Globals.js index 5d2786f34..2e0907800 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -148,9 +148,7 @@ class Globals { * @return {Object} */ buildBrowserInfo() { - // Extend the UA library to look for Ghostery Android browser's custom user agent - const uaExtension = [[/(ghostery)\/([\w\.]+)/i], [parser.BROWSER.NAME, parser.BROWSER.VERSION]]; // eslint-disable-line no-useless-escape - const ua = parser({ browser: uaExtension }); + const ua = parser(navigator.userAgent); const browser = ua.browser.name.toLowerCase(); const version = parseInt(ua.browser.version.toString(), 10); // convert to string for Chrome const platform = ua.os.name.toLowerCase(); @@ -160,7 +158,7 @@ class Globals { this.BROWSER_INFO.displayName = 'Cliqz'; this.BROWSER_INFO.name = 'cliqz'; this.BROWSER_INFO.token = 'cl'; - } else if (browser.includes('ghostery')) { + } else if (navigator.userAgent.toLocaleLowerCase().includes('ghostery')) { this.BROWSER_INFO.displayName = 'Ghostery Android Browser'; this.BROWSER_INFO.name = 'ghostery_android'; this.BROWSER_INFO.token = 'ga'; From 38893f4320768cd45be4c08458b052ce603ef0c2 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Wed, 29 Jul 2020 17:56:47 -0400 Subject: [PATCH 75/89] fix issue where block/allow unknown trackers did not persist on android --- app/panel-android/actions/blockingActions.js | 3 ++- app/panel-android/components/content/BlockingTracker.jsx | 3 +-- app/panel-android/index.jsx | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/panel-android/actions/blockingActions.js b/app/panel-android/actions/blockingActions.js index fdbfa2a74..dea07ad77 100644 --- a/app/panel-android/actions/blockingActions.js +++ b/app/panel-android/actions/blockingActions.js @@ -72,7 +72,8 @@ export function anonymizeSiteTracker({ actionData, state }) { const updatedcliqzModuleData = JSON.parse(JSON.stringify(state.cliqzModuleData)); const { antiTracking, adBlock } = state.cliqzModuleData; const whitelistedUrls = { ...antiTracking.whitelistedUrls, ...adBlock.whitelistedUrls }; - const { unknownTracker, pageHost } = actionData; + const { unknownTracker } = actionData; + const { pageHost } = state.summary; const addToWhitelist = () => { unknownTracker.sources.forEach((domain) => { diff --git a/app/panel-android/components/content/BlockingTracker.jsx b/app/panel-android/components/content/BlockingTracker.jsx index 2baebd0e2..572162008 100644 --- a/app/panel-android/components/content/BlockingTracker.jsx +++ b/app/panel-android/components/content/BlockingTracker.jsx @@ -164,7 +164,7 @@ class BlockingTracker extends React.Component { } clickAnonymize = () => { - const { tracker, siteProps, callGlobalAction } = this.props; + const { tracker, callGlobalAction } = this.props; if (this.selectDisabled) { return; @@ -174,7 +174,6 @@ class BlockingTracker extends React.Component { actionName: 'anonymizeSiteTracker', actionData: { unknownTracker: tracker, - pageHost: siteProps.pageHost, } }); } diff --git a/app/panel-android/index.jsx b/app/panel-android/index.jsx index 2bf04d6ac..832019fc8 100644 --- a/app/panel-android/index.jsx +++ b/app/panel-android/index.jsx @@ -11,11 +11,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 * * ToDo: - * - [next] Add a Close & Reload notification above blue navbar. + * - [ ] Add a Close & Reload notification above blue navbar. * - [ ] Add tests for PanelAndroid Settings and Panel Settings sub-components * - [ ] Add tests for PanelAndroid Menu and Panel Menu Sub-Components - * - [ ] See if Vinny likes what I did with SmartBlock & CliqzFeatures - * - [ ] Add Account Views * - [ ] Replace hidden tooltips on the OverviewTab with a Help Screen * - [ ] Make a landscape mode: OverviewTab on left, Site/Global Blocking on right. * From 85c59317a1d50b0ce472d5b80c785cdaffe809a8 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Wed, 29 Jul 2020 21:39:26 -0400 Subject: [PATCH 76/89] Hide ImportExport, add tests for some Settings --- app/panel-android/actions/settingsActions.js | 5 + .../components/content/Settings.jsx | 15 +- .../Settings/__tests__/ImportExport.jsx | 148 +++++++++++ .../components/Settings/__tests__/OptIn.jsx | 77 ++++++ .../__snapshots__/ImportExport.jsx.snap | 184 ++++++++++++++ .../__tests__/__snapshots__/OptIn.jsx.snap | 235 ++++++++++++++++++ 6 files changed, 657 insertions(+), 7 deletions(-) create mode 100644 app/panel/components/Settings/__tests__/ImportExport.jsx create mode 100644 app/panel/components/Settings/__tests__/OptIn.jsx create mode 100644 app/panel/components/Settings/__tests__/__snapshots__/ImportExport.jsx.snap create mode 100644 app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap diff --git a/app/panel-android/actions/settingsActions.js b/app/panel-android/actions/settingsActions.js index 0129886a0..28015510b 100644 --- a/app/panel-android/actions/settingsActions.js +++ b/app/panel-android/actions/settingsActions.js @@ -70,3 +70,8 @@ export function selectItem({ actionData }) { }, }; } + +// ToDo: Implement for Import/Export +export function exportSettings() {} +export function importSettingsDialog() {} +export function importSettingsNative() {} diff --git a/app/panel-android/components/content/Settings.jsx b/app/panel-android/components/content/Settings.jsx index cc41c9da2..986f9e960 100644 --- a/app/panel-android/components/content/Settings.jsx +++ b/app/panel-android/components/content/Settings.jsx @@ -22,6 +22,7 @@ import OptIn from '../../../panel/components/Settings/OptIn'; import ImportExport from '../../../panel/components/Settings/ImportExport'; import Help from '../../../panel/components/Help'; import About from '../../../panel/components/About'; +import { exportSettings, importSettingsDialog, importSettingsNative } from '../../actions/settingsActions'; import globals from '../../../../src/classes/Globals'; @@ -152,9 +153,11 @@ class Settings extends React.Component {
    { this.setState({ view: 'settings-opt-in' }); }}> { t('settings_opt_in') }
    -
    { this.setState({ view: 'settings-import-export' }); }}> + {false && ( // Remove to show Import/Export menu item +
    { this.setState({ view: 'settings-import-export' }); }}> { t('settings_import_export') } -
    +
    + )}
    { this.setState({ view: 'settings-help' }); }}> { t('panel_menu_help') }
    @@ -247,13 +250,11 @@ class Settings extends React.Component { importResultText, actionSuccess, }; - const actions = { - exportSettings: () => { console.log('exportSettings'); }, - importSettingsDialog: () => { console.log('importSettingsDialog'); }, - importSettingsNative: () => { console.log('importSettingsNative'); }, + exportSettings, + importSettingsDialog, + importSettingsNative, }; - console.log('ToDo: implement actions and add settingsData elements to reducer', this.props, this.state); return (
    diff --git a/app/panel/components/Settings/__tests__/ImportExport.jsx b/app/panel/components/Settings/__tests__/ImportExport.jsx new file mode 100644 index 000000000..a09516e4e --- /dev/null +++ b/app/panel/components/Settings/__tests__/ImportExport.jsx @@ -0,0 +1,148 @@ +/** + * Import Export Settings Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * 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 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import ImportExport from '../ImportExport'; + +jest.mock('../../../../../src/classes/Globals', () => ({ + BROWSER_INFO: { + name: 'firefox', + } +})); + +describe('app/panel/Settings/ImportExport.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('ImportExport is rendered correctly with baseline props', () => { + const settingsData = { + pageUrl: '', + exportResultText: '', + importResultText: '', + actionSuccess: false, + }; + const actions = { + exportSettings: () => {}, + importSettingsDialog: () => {}, + importSettingsNative: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('ImportExport is rendered correctly with happy-path props', () => { + const settingsData = { + pageUrl: 'https://example.com', + exportResultText: 'Your settings have been successfully exported to your downloads folder on August 8, 2020 6:08 PM', + importResultText: 'Your settings have been successfully imported on September 12, 2020 6:08 PM', + actionSuccess: true, + }; + const actions = { + exportSettings: () => {}, + importSettingsDialog: () => {}, + importSettingsNative: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('ImportExport is rendered correctly with unhappy-path props', () => { + const settingsData = { + pageUrl: 'chrome://extensions', + exportResultText: 'Ghostery cannot export settings when the current page is one of the reserved browser pages. Please navigate to a different page and try again.', + importResultText: 'That is the incorrect file type. Please choose a .ghost file and try again.', + actionSuccess: false, + }; + const actions = { + exportSettings: () => {}, + importSettingsDialog: () => {}, + importSettingsNative: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('ImportExport functions correctly', () => { + const settingsData = { + pageUrl: '', + exportResultText: '', + importResultText: '', + actionSuccess: false, + }; + const actions = { + exportSettings: jest.fn(), + importSettingsDialog: jest.fn(), + importSettingsNative: jest.fn(), + }; + const component = mount( + + ); + + expect(actions.exportSettings.mock.calls.length).toBe(0); + expect(actions.importSettingsDialog.mock.calls.length).toBe(0); + expect(actions.importSettingsNative.mock.calls.length).toBe(0); + expect(component.find('.export-result').text()).toBe(''); + expect(component.find('.import-result').text()).toBe(''); + + component.find('.export').simulate('click'); + component.setProps({ + settingsData: { + pageUrl: '', + exportResultText: 'export-result-text', + importResultText: '', + actionSuccess: true, + } + }); + expect(actions.exportSettings.mock.calls.length).toBe(1); + expect(component.find('.export-result').text()).toBe('export-result-text'); + + component.find('.import').simulate('click'); + component.setProps({ + settingsData: { + pageUrl: '', + exportResultText: '', + importResultText: 'import-result-text', + actionSuccess: true, + } + }); + expect(actions.importSettingsDialog.mock.calls.length).toBe(1); + expect(component.find('.import-result').text()).toBe('import-result-text'); + + component.find('#select-file').simulate('change'); + expect(actions.importSettingsNative.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/app/panel/components/Settings/__tests__/OptIn.jsx b/app/panel/components/Settings/__tests__/OptIn.jsx new file mode 100644 index 000000000..083c43a4e --- /dev/null +++ b/app/panel/components/Settings/__tests__/OptIn.jsx @@ -0,0 +1,77 @@ +/** + * OptIn Settings Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * 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 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import OptIn from '../OptIn'; + +describe('app/panel/Settings/OptIn.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('OptIn is rendered correctly with falsy props', () => { + const settingsData = { + enable_metrics: false, + enable_human_web: false, + enable_offers: false, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('OptIn is rendered correctly with truthy props', () => { + const settingsData = { + enable_metrics: true, + enable_human_web: true, + enable_offers: true, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('OptIn functions correctly', () => { + const settingsData = { + enable_metrics: true, + enable_human_web: true, + enable_offers: true, + }; + const toggleCheckbox = jest.fn(); + + const component = mount( + + ); + + expect(toggleCheckbox.mock.calls.length).toBe(0); + component.find('#settings-share-usage').simulate('click'); + component.find('#settings-share-human-web').simulate('click'); + component.find('#settings-allow-offers').simulate('click'); + expect(toggleCheckbox.mock.calls.length).toBe(3); + }); + }); +}); diff --git a/app/panel/components/Settings/__tests__/__snapshots__/ImportExport.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/ImportExport.jsx.snap new file mode 100644 index 000000000..fae790743 --- /dev/null +++ b/app/panel/components/Settings/__tests__/__snapshots__/ImportExport.jsx.snap @@ -0,0 +1,184 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/Settings/ImportExport.jsx Snapshot tests with react-test-renderer ImportExport is rendered correctly with baseline props 1`] = ` +
    +

    + settings_export_header +

    +

    + settings_export_text +

    +

    + +

    +
    +

    + settings_import_header +

    +

    + settings_import_text +

    +

    + settings_import_warning +

    +

    + +

    + +
    +`; + +exports[`app/panel/Settings/ImportExport.jsx Snapshot tests with react-test-renderer ImportExport is rendered correctly with happy-path props 1`] = ` +
    +

    + settings_export_header +

    +

    + settings_export_text +

    +

    + Your settings have been successfully exported to your downloads folder on August 8, 2020 6:08 PM +

    +
    +

    + settings_import_header +

    +

    + settings_import_text +

    +

    + settings_import_warning +

    +

    + Your settings have been successfully imported on September 12, 2020 6:08 PM +

    + +
    +`; + +exports[`app/panel/Settings/ImportExport.jsx Snapshot tests with react-test-renderer ImportExport is rendered correctly with unhappy-path props 1`] = ` +
    +

    + settings_export_header +

    +

    + settings_export_text +

    +

    + Ghostery cannot export settings when the current page is one of the reserved browser pages. Please navigate to a different page and try again. +

    +
    +

    + settings_import_header +

    +

    + settings_import_text +

    +

    + settings_import_warning +

    +

    + That is the incorrect file type. Please choose a .ghost file and try again. +

    + +
    +`; diff --git a/app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap new file mode 100644 index 000000000..3953f909d --- /dev/null +++ b/app/panel/components/Settings/__tests__/__snapshots__/OptIn.jsx.snap @@ -0,0 +1,235 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/Settings/OptIn.jsx Snapshot tests with react-test-renderer OptIn is rendered correctly with falsy props 1`] = ` +
    +
    +
    +

    + settings_support_ghostery +

    +
    + settings_support_ghostery_by + : +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +`; + +exports[`app/panel/Settings/OptIn.jsx Snapshot tests with react-test-renderer OptIn is rendered correctly with truthy props 1`] = ` +
    +
    +
    +

    + settings_support_ghostery +

    +
    + settings_support_ghostery_by + : +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +`; From 4df66ecb68cf08860b3e5d72b9f8d59cd1fe4255 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Wed, 29 Jul 2020 22:23:24 -0400 Subject: [PATCH 77/89] Add NeedsReload message to PanelAndroid --- app/panel-android/components/PanelAndroid.jsx | 20 +++- .../components/content/Settings.jsx | 2 +- app/panel-android/index.jsx | 13 +-- app/scss/android/_account.scss | 105 ------------------ app/scss/panel_android.scss | 18 ++- 5 files changed, 38 insertions(+), 120 deletions(-) delete mode 100644 app/scss/android/_account.scss diff --git a/app/panel-android/components/PanelAndroid.jsx b/app/panel-android/components/PanelAndroid.jsx index 0caf7c495..27696fa72 100644 --- a/app/panel-android/components/PanelAndroid.jsx +++ b/app/panel-android/components/PanelAndroid.jsx @@ -12,6 +12,7 @@ */ import React from 'react'; +import ClassNames from 'classnames'; import Settings from './content/Settings'; import Tabs from './content/Tabs'; import Tab from './content/Tab'; @@ -22,13 +23,14 @@ import { } from '../actions/panelActions'; import getCliqzModuleData from '../actions/cliqzActions'; import handleAllActions from '../actions/handler'; -import { openAccountPageAndroid } from '../../panel/utils/msg'; +import { sendMessage, openAccountPageAndroid } from '../../panel/utils/msg'; class PanelAndroid extends React.Component { constructor(props) { super(props); this.state = { + needsReload: false, view: 'overview', panel: { enable_ad_block: false, @@ -133,7 +135,7 @@ class PanelAndroid extends React.Component { } setGlobalState = (updated) => { - const newState = {}; + const newState = { needsReload: true }; Object.keys(updated).forEach((key) => { newState[key] = { ...this.state[key], ...updated[key] }; // eslint-disable-line react/destructuring-assignment }); @@ -171,6 +173,12 @@ class PanelAndroid extends React.Component { wtm: tracker.wtm, }) + reloadTab = () => { + const { panel } = this.state; + sendMessage('reloadTab', { tab_id: +panel.tab_id }); + window.close(); + } + _renderSettings() { const { summary, settings } = this.state; @@ -256,10 +264,16 @@ class PanelAndroid extends React.Component { } render() { - const { view } = this.state; + const { needsReload, view } = this.state; + const needsReloadClassNames = ClassNames('NeedsReload flex-container align-center-middle', { + 'NeedsReload--show': needsReload, + }); return (
    +
    + {t('alert_reload')} +
    {view === 'settings' && this._renderSettings()} {view === 'overview' && this._renderOverview()}
    diff --git a/app/panel-android/components/content/Settings.jsx b/app/panel-android/components/content/Settings.jsx index 986f9e960..f68a577f6 100644 --- a/app/panel-android/components/content/Settings.jsx +++ b/app/panel-android/components/content/Settings.jsx @@ -155,7 +155,7 @@ class Settings extends React.Component {
    {false && ( // Remove to show Import/Export menu item
    { this.setState({ view: 'settings-import-export' }); }}> - { t('settings_import_export') } + { t('settings_import_export') }
    )}
    { this.setState({ view: 'settings-help' }); }}> diff --git a/app/panel-android/index.jsx b/app/panel-android/index.jsx index 2bf04d6ac..90422a32f 100644 --- a/app/panel-android/index.jsx +++ b/app/panel-android/index.jsx @@ -9,16 +9,9 @@ * 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 - * - * ToDo: - * - [next] Add a Close & Reload notification above blue navbar. - * - [ ] Add tests for PanelAndroid Settings and Panel Settings sub-components - * - [ ] Add tests for PanelAndroid Menu and Panel Menu Sub-Components - * - [ ] See if Vinny likes what I did with SmartBlock & CliqzFeatures - * - [ ] Add Account Views - * - [ ] Replace hidden tooltips on the OverviewTab with a Help Screen - * - [ ] Make a landscape mode: OverviewTab on left, Site/Global Blocking on right. - * + */ + +/** * @namespace PanelAndroidClasses */ diff --git a/app/scss/android/_account.scss b/app/scss/android/_account.scss deleted file mode 100644 index 1f4e9bab8..000000000 --- a/app/scss/android/_account.scss +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Account Sass - * - * Ghostery Browser Extension - * https://www.ghostery.com/ - * - * Copyright 2020 Ghostery, Inc. All rights reserved. - * - * 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 - */ - -.Account { - .AccountHeader { - height: $tab--nav-height; - line-height: $tab--nav-height; - background-color: $ghosty-blue; - text-align: center; - position: relative; - @include prefix('box-shadow', '3px 2px 10px rgba(0,0,0,0.15)'); - - .AccountHeader__icon { - position: absolute; - left: 0; - height: 14px; - width: 10px; - padding: 8px 10px; - margin: 10px; - line-height: 0; - cursor: pointer; - } - - .AccountHeader__text { - color: $white; - font-size: 16px; - font-weight: 500; - } - } - - .Account--addPaddingTop { - padding-top: 30px; - } - - .Account__headerImage { - max-width: 180px; - } - - .Account__headerTitle h3 { - font-size: 24px; - line-height: 1.5; - color: #4a4a4a; - margin: 20px 25px 0; - } - - .Account__inputLabel { - font-size: 14px; - font-weight: 500; - line-height: 40px; - color: #4a4a4a; - } - - .Account__inputBox { - font-size: 14; - line-height: 24px; - color: #4a4a4a; - margin-bottom: 35px; - - // Foundation Overrides - border-radius: 0; - box-shadow: none; - border: 1px solid #c8c7c2; - } - .Account__inputBox.error { - margin-bottom: 8px; - border-color: #e74055; - } - .Account__inputBox:focus { - // Foundation Overrides - box-shadow: none; - border-color: #4a4a4a; - } - - .Account__inputError { - font-size: 12; - line-height: 14px; - color: #e74055; - margin-bottom: 13px; - } - - .Account__link { - font-size: 14px; - line-height: 30px; - } - - .Account__button { - min-width: 180px; - } -} - -@media only screen and (max-width: 740px) { - .Account__header { - flex-direction: column; - } -} diff --git a/app/scss/panel_android.scss b/app/scss/panel_android.scss index fb306c30b..c302b9665 100644 --- a/app/scss/panel_android.scss +++ b/app/scss/panel_android.scss @@ -20,7 +20,6 @@ @import './partials/_svgs'; // Android Specific Component Styles -@import './android/_account'; @import './android/_settings'; @import './android/_tabs'; @import './android/_overview_tab'; @@ -39,3 +38,20 @@ // Import shared helpers @import 'shared_helper_classes'; + +// NeedsReload Header +.NeedsReload { + height: 0; + color: #4a4a4a; + font-weight: 500; + font-style: italic; + background-color: #ffae00; + overflow: none; + opacity: 0; + @include prefix('transition', 'all 300ms ease'); + + &.NeedsReload--show { + height: 25px; + opacity: 1; + } +} From 819554961f8b065bf43168343521b16ee3556b17 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Thu, 30 Jul 2020 18:24:13 -0400 Subject: [PATCH 78/89] Fix bug on BlockingTracker --- app/panel-android/components/content/BlockingTracker.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/panel-android/components/content/BlockingTracker.jsx b/app/panel-android/components/content/BlockingTracker.jsx index 572162008..501607a36 100644 --- a/app/panel-android/components/content/BlockingTracker.jsx +++ b/app/panel-android/components/content/BlockingTracker.jsx @@ -49,6 +49,10 @@ class BlockingTracker extends React.Component { return 'restricted'; } + if (blocked) { + return 'blocked'; + } + if (catId !== '') { return catId; } From 82cbf39906614d8a5c0f0653287118b3e4e22e9b Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Thu, 30 Jul 2020 19:39:44 -0400 Subject: [PATCH 79/89] Add tests for components used in PanelAndroid Settings --- .../__tests__/CliqzFeature.jsx | 4 +- .../__tests__/DonutGraph.jsx | 4 +- .../__tests__/GhosteryFeature.jsx | 4 +- .../__tests__/NotScanned.jsx | 2 +- .../__tests__/PauseButton.jsx | 4 +- .../__snapshots__/CliqzFeature.jsx.snap | 0 .../__snapshots__/DonutGraph.jsx.snap | 0 .../__snapshots__/GhosteryFeature.jsx.snap | 0 .../__snapshots__/NotScanned.jsx.snap | 0 .../__snapshots__/PauseButton.jsx.snap | 0 .../Settings/__tests__/AdBlocker.jsx | 95 ++++ .../Settings/__tests__/GeneralSettings.jsx | 112 +++++ .../Settings/__tests__/Notifications.jsx | 93 ++++ .../__snapshots__/AdBlocker.jsx.snap | 280 +++++++++++ .../__snapshots__/GeneralSettings.jsx.snap | 462 ++++++++++++++++++ .../__snapshots__/Notifications.jsx.snap | 355 ++++++++++++++ app/panel/components/__tests__/About.jsx | 27 + app/panel/components/__tests__/Help.jsx | 32 ++ .../__tests__/__snapshots__/About.jsx.snap | 74 +++ .../__tests__/__snapshots__/Help.jsx.snap | 69 +++ 20 files changed, 1608 insertions(+), 9 deletions(-) rename app/panel/components/{ => BuildingBlocks}/__tests__/CliqzFeature.jsx (98%) rename app/panel/components/{ => BuildingBlocks}/__tests__/DonutGraph.jsx (97%) rename app/panel/components/{ => BuildingBlocks}/__tests__/GhosteryFeature.jsx (98%) rename app/panel/components/{ => BuildingBlocks}/__tests__/NotScanned.jsx (95%) rename app/panel/components/{ => BuildingBlocks}/__tests__/PauseButton.jsx (98%) rename app/panel/components/{ => BuildingBlocks}/__tests__/__snapshots__/CliqzFeature.jsx.snap (100%) rename app/panel/components/{ => BuildingBlocks}/__tests__/__snapshots__/DonutGraph.jsx.snap (100%) rename app/panel/components/{ => BuildingBlocks}/__tests__/__snapshots__/GhosteryFeature.jsx.snap (100%) rename app/panel/components/{ => BuildingBlocks}/__tests__/__snapshots__/NotScanned.jsx.snap (100%) rename app/panel/components/{ => BuildingBlocks}/__tests__/__snapshots__/PauseButton.jsx.snap (100%) create mode 100644 app/panel/components/Settings/__tests__/AdBlocker.jsx create mode 100644 app/panel/components/Settings/__tests__/GeneralSettings.jsx create mode 100644 app/panel/components/Settings/__tests__/Notifications.jsx create mode 100644 app/panel/components/Settings/__tests__/__snapshots__/AdBlocker.jsx.snap create mode 100644 app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap create mode 100644 app/panel/components/Settings/__tests__/__snapshots__/Notifications.jsx.snap create mode 100644 app/panel/components/__tests__/About.jsx create mode 100644 app/panel/components/__tests__/Help.jsx create mode 100644 app/panel/components/__tests__/__snapshots__/About.jsx.snap create mode 100644 app/panel/components/__tests__/__snapshots__/Help.jsx.snap diff --git a/app/panel/components/__tests__/CliqzFeature.jsx b/app/panel/components/BuildingBlocks/__tests__/CliqzFeature.jsx similarity index 98% rename from app/panel/components/__tests__/CliqzFeature.jsx rename to app/panel/components/BuildingBlocks/__tests__/CliqzFeature.jsx index 07202ae0a..cba12eac1 100644 --- a/app/panel/components/__tests__/CliqzFeature.jsx +++ b/app/panel/components/BuildingBlocks/__tests__/CliqzFeature.jsx @@ -14,7 +14,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; -import CliqzFeature from '../BuildingBlocks/CliqzFeature'; +import CliqzFeature from '../CliqzFeature'; // Fake the translation function to only return the translation key global.t = function(str) { @@ -22,7 +22,7 @@ global.t = function(str) { }; // Fake the Tooltip implementation -jest.mock('../Tooltip'); +jest.mock('../../Tooltip'); describe('app/panel/components/CliqzFeature.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { diff --git a/app/panel/components/__tests__/DonutGraph.jsx b/app/panel/components/BuildingBlocks/__tests__/DonutGraph.jsx similarity index 97% rename from app/panel/components/__tests__/DonutGraph.jsx rename to app/panel/components/BuildingBlocks/__tests__/DonutGraph.jsx index 47139998c..29d3d9311 100644 --- a/app/panel/components/__tests__/DonutGraph.jsx +++ b/app/panel/components/BuildingBlocks/__tests__/DonutGraph.jsx @@ -14,7 +14,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; -import DonutGraph from '../BuildingBlocks/DonutGraph'; +import DonutGraph from '../DonutGraph'; // Fake the translation function to only return the translation key global.t = function(str) { @@ -22,7 +22,7 @@ global.t = function(str) { }; // Fake the Tooltip implementation -jest.mock('../Tooltip'); +jest.mock('../../Tooltip'); describe('app/panel/components/DonutGraph.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { diff --git a/app/panel/components/__tests__/GhosteryFeature.jsx b/app/panel/components/BuildingBlocks/__tests__/GhosteryFeature.jsx similarity index 98% rename from app/panel/components/__tests__/GhosteryFeature.jsx rename to app/panel/components/BuildingBlocks/__tests__/GhosteryFeature.jsx index 0f3ab5876..e8516f147 100644 --- a/app/panel/components/__tests__/GhosteryFeature.jsx +++ b/app/panel/components/BuildingBlocks/__tests__/GhosteryFeature.jsx @@ -14,7 +14,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; -import GhosteryFeature from '../BuildingBlocks/GhosteryFeature'; +import GhosteryFeature from '../GhosteryFeature'; // Fake the translation function to only return the translation key global.t = function(str) { @@ -22,7 +22,7 @@ global.t = function(str) { }; // Fake the Tooltip implementation -jest.mock('../Tooltip'); +jest.mock('../../Tooltip'); describe('app/panel/components/GhosteryFeature.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { diff --git a/app/panel/components/__tests__/NotScanned.jsx b/app/panel/components/BuildingBlocks/__tests__/NotScanned.jsx similarity index 95% rename from app/panel/components/__tests__/NotScanned.jsx rename to app/panel/components/BuildingBlocks/__tests__/NotScanned.jsx index 799196eaf..ac445df7b 100644 --- a/app/panel/components/__tests__/NotScanned.jsx +++ b/app/panel/components/BuildingBlocks/__tests__/NotScanned.jsx @@ -13,7 +13,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import NotScanned from '../BuildingBlocks/NotScanned'; +import NotScanned from '../NotScanned'; // Fake the translation function to only return the translation key global.t = function(str) { diff --git a/app/panel/components/__tests__/PauseButton.jsx b/app/panel/components/BuildingBlocks/__tests__/PauseButton.jsx similarity index 98% rename from app/panel/components/__tests__/PauseButton.jsx rename to app/panel/components/BuildingBlocks/__tests__/PauseButton.jsx index 0aeafae24..37ba604ee 100644 --- a/app/panel/components/__tests__/PauseButton.jsx +++ b/app/panel/components/BuildingBlocks/__tests__/PauseButton.jsx @@ -14,7 +14,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; -import PauseButton from '../BuildingBlocks/PauseButton'; +import PauseButton from '../PauseButton'; // Fake the translation function to only return the translation key global.t = function(str) { @@ -22,7 +22,7 @@ global.t = function(str) { }; // Fake the Tooltip implementation -jest.mock('../Tooltip'); +jest.mock('../../Tooltip'); describe('app/panel/components/BuildingBlocks/PauseButton.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { diff --git a/app/panel/components/__tests__/__snapshots__/CliqzFeature.jsx.snap b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/CliqzFeature.jsx.snap similarity index 100% rename from app/panel/components/__tests__/__snapshots__/CliqzFeature.jsx.snap rename to app/panel/components/BuildingBlocks/__tests__/__snapshots__/CliqzFeature.jsx.snap diff --git a/app/panel/components/__tests__/__snapshots__/DonutGraph.jsx.snap b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/DonutGraph.jsx.snap similarity index 100% rename from app/panel/components/__tests__/__snapshots__/DonutGraph.jsx.snap rename to app/panel/components/BuildingBlocks/__tests__/__snapshots__/DonutGraph.jsx.snap diff --git a/app/panel/components/__tests__/__snapshots__/GhosteryFeature.jsx.snap b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/GhosteryFeature.jsx.snap similarity index 100% rename from app/panel/components/__tests__/__snapshots__/GhosteryFeature.jsx.snap rename to app/panel/components/BuildingBlocks/__tests__/__snapshots__/GhosteryFeature.jsx.snap diff --git a/app/panel/components/__tests__/__snapshots__/NotScanned.jsx.snap b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/NotScanned.jsx.snap similarity index 100% rename from app/panel/components/__tests__/__snapshots__/NotScanned.jsx.snap rename to app/panel/components/BuildingBlocks/__tests__/__snapshots__/NotScanned.jsx.snap diff --git a/app/panel/components/__tests__/__snapshots__/PauseButton.jsx.snap b/app/panel/components/BuildingBlocks/__tests__/__snapshots__/PauseButton.jsx.snap similarity index 100% rename from app/panel/components/__tests__/__snapshots__/PauseButton.jsx.snap rename to app/panel/components/BuildingBlocks/__tests__/__snapshots__/PauseButton.jsx.snap diff --git a/app/panel/components/Settings/__tests__/AdBlocker.jsx b/app/panel/components/Settings/__tests__/AdBlocker.jsx new file mode 100644 index 000000000..8ef332c2e --- /dev/null +++ b/app/panel/components/Settings/__tests__/AdBlocker.jsx @@ -0,0 +1,95 @@ +/** + * AdBlocker Settings Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * 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 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import AdBlocker from '../AdBlocker'; + +describe('app/panel/Settings/AdBlocker.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('AdBlocker is rendered correctly with AdOnly checked', () => { + const settingsData = { + cliqz_adb_mode: 0, + }; + const actions = { + selectItem: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('AdBlocker is rendered correctly with Ads & Trackers checked', () => { + const settingsData = { + cliqz_adb_mode: 1, + }; + const actions = { + selectItem: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('AdBlocker is rendered correctly with Ads, Trackers, & Annoyances checked', () => { + const settingsData = { + cliqz_adb_mode: 2, + }; + const actions = { + selectItem: () => {}, + }; + + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('AdBlocker functions correctly', () => { + const settingsData = { + cliqz_adb_mode: 0, + }; + const actions = { + selectItem: jest.fn(), + }; + + const component = mount( + + ); + + expect(actions.selectItem.mock.calls.length).toBe(0); + component.find('.RadioButtonGroup__label').at(0).simulate('click'); + component.find('.RadioButton__outerCircle').at(2).simulate('click'); + expect(actions.selectItem.mock.calls.length).toBe(2); + }); + }); +}); diff --git a/app/panel/components/Settings/__tests__/GeneralSettings.jsx b/app/panel/components/Settings/__tests__/GeneralSettings.jsx new file mode 100644 index 000000000..de277beb2 --- /dev/null +++ b/app/panel/components/Settings/__tests__/GeneralSettings.jsx @@ -0,0 +1,112 @@ +/** + * GeneralSettings Settings Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * 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 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import GeneralSettings from '../GeneralSettings'; + +describe('app/panel/Settings/GeneralSettings.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('GeneralSettings is rendered correctly with falsy props', () => { + const settingsData = { + language: 'en', + bugs_last_checked: 0, + enable_autoupdate: false, + show_tracker_urls: false, + enable_click2play: false, + enable_click2play_social: false, + toggle_individual_trackers: false, + ignore_first_party: false, + block_by_default: false, + }; + const actions = { + updateDatabase: () => {}, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('GeneralSettings is rendered correctly with truthy props', () => { + const settingsData = { + language: 'en', + bugs_last_checked: 1000000, + enable_autoupdate: true, + dbUpdateText: 'database-updated-text', + show_tracker_urls: true, + enable_click2play: true, + enable_click2play_social: true, + toggle_individual_trackers: true, + ignore_first_party: true, + block_by_default: true, + }; + const actions = { + updateDatabase: () => {}, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('GeneralSettings functions correctly', () => { + const settingsData = { + language: 'en', + bugs_last_checked: 0, + enable_autoupdate: false, + show_tracker_urls: false, + enable_click2play: false, + enable_click2play_social: false, + toggle_individual_trackers: false, + ignore_first_party: false, + block_by_default: false, + }; + const actions = { + updateDatabase: jest.fn(), + }; + const toggleCheckbox = jest.fn(); + + const component = mount( + + ); + + expect(actions.updateDatabase.mock.calls.length).toBe(0); + component.find('#update-now-span').simulate('click'); + expect(actions.updateDatabase.mock.calls.length).toBe(1); + + expect(toggleCheckbox.mock.calls.length).toBe(0); + component.find('input[type="checkbox"]').at(0).simulate('click'); + component.find('#settings-allow-trackers').simulate('click'); + expect(toggleCheckbox.mock.calls.length).toBe(2); + }); + }); +}); diff --git a/app/panel/components/Settings/__tests__/Notifications.jsx b/app/panel/components/Settings/__tests__/Notifications.jsx new file mode 100644 index 000000000..32ea8e74e --- /dev/null +++ b/app/panel/components/Settings/__tests__/Notifications.jsx @@ -0,0 +1,93 @@ +/** + * Notifications Settings Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2020 Ghostery, Inc. All rights reserved. + * + * 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 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import Notifications from '../Notifications'; + +describe('app/panel/Settings/Notifications.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('Notifications is rendered correctly with falsy props', () => { + const settingsData = { + show_cmp: false, + notify_upgrade_updates: false, + notify_promotions: false, + notify_library_updates: false, + reload_banner_status: false, + trackers_banner_status: false, + show_badge: false, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + + test('Notifications is rendered correctly with truthy props', () => { + const settingsData = { + show_cmp: true, + notify_upgrade_updates: true, + notify_promotions: true, + notify_library_updates: true, + reload_banner_status: true, + trackers_banner_status: true, + show_badge: true, + }; + + const component = renderer.create( + {}} + /> + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('Shallow snapshot tests rendered with Enzyme', () => { + test('Notifications functions correctly', () => { + const settingsData = { + show_cmp: false, + notify_upgrade_updates: false, + notify_promotions: false, + notify_library_updates: false, + reload_banner_status: false, + trackers_banner_status: false, + show_badge: false, + }; + const toggleCheckbox = jest.fn(); + + const component = mount( + + ); + + expect(toggleCheckbox.mock.calls.length).toBe(0); + component.find('#settings-announcements').simulate('click'); + component.find('#settings-new-features').simulate('click'); + component.find('#settings-new-promotions').simulate('click'); + component.find('#settings-new-trackers').simulate('click'); + component.find('#settings-show-reload-banner').simulate('click'); + component.find('#settings-show-trackers-banner').simulate('click'); + component.find('#settings-show-count-badge').simulate('click'); + expect(toggleCheckbox.mock.calls.length).toBe(7); + }); + }); +}); diff --git a/app/panel/components/Settings/__tests__/__snapshots__/AdBlocker.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/AdBlocker.jsx.snap new file mode 100644 index 000000000..72a961de8 --- /dev/null +++ b/app/panel/components/Settings/__tests__/__snapshots__/AdBlocker.jsx.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/Settings/AdBlocker.jsx Snapshot tests with react-test-renderer AdBlocker is rendered correctly with AdOnly checked 1`] = ` +
    +
    +
    +

    + settings_adblocker +

    +
    + settings_adblocker_lists +
    +
    +
    +
    + settings_adblocker_list_1 +
    +
    + settings_adblocker_list_2 +
    +
    + settings_adblocker_list_3 +
    +
    +
    +
    + + + + + +
    +
    + + + + + +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +`; + +exports[`app/panel/Settings/AdBlocker.jsx Snapshot tests with react-test-renderer AdBlocker is rendered correctly with Ads & Trackers checked 1`] = ` +
    +
    +
    +

    + settings_adblocker +

    +
    + settings_adblocker_lists +
    +
    +
    +
    + settings_adblocker_list_1 +
    +
    + settings_adblocker_list_2 +
    +
    + settings_adblocker_list_3 +
    +
    +
    +
    + + + + + +
    +
    + + + + + +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +`; + +exports[`app/panel/Settings/AdBlocker.jsx Snapshot tests with react-test-renderer AdBlocker is rendered correctly with Ads, Trackers, & Annoyances checked 1`] = ` +
    +
    +
    +

    + settings_adblocker +

    +
    + settings_adblocker_lists +
    +
    +
    +
    + settings_adblocker_list_1 +
    +
    + settings_adblocker_list_2 +
    +
    + settings_adblocker_list_3 +
    +
    +
    +
    + + + + + +
    +
    + + + + + +
    +
    + + + + + +
    +
    +
    +
    +
    +
    +`; diff --git a/app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap new file mode 100644 index 000000000..3a68f2101 --- /dev/null +++ b/app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap @@ -0,0 +1,462 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/Settings/GeneralSettings.jsx Snapshot tests with react-test-renderer GeneralSettings is rendered correctly with falsy props 1`] = ` +
    +
    +
    +

    + settings_trackers +

    +
    +
    + + +
    + + settings_last_update + + + + December 31, 1969 7:00 PM + + + + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +

    + settings_highlight_trackers +

    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +

    + settings_blocking +

    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +`; + +exports[`app/panel/Settings/GeneralSettings.jsx Snapshot tests with react-test-renderer GeneralSettings is rendered correctly with truthy props 1`] = ` +
    +
    +
    +

    + settings_trackers +

    +
    +
    + + +
    + + settings_last_update + + + + December 31, 1969 7:16 PM + + + + database-updated-text + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +

    + settings_highlight_trackers +

    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +

    + settings_blocking +

    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +`; diff --git a/app/panel/components/Settings/__tests__/__snapshots__/Notifications.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/Notifications.jsx.snap new file mode 100644 index 000000000..8ffa3a822 --- /dev/null +++ b/app/panel/components/Settings/__tests__/__snapshots__/Notifications.jsx.snap @@ -0,0 +1,355 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/Settings/Notifications.jsx Snapshot tests with react-test-renderer Notifications is rendered correctly with falsy props 1`] = ` +
    +
    +
    +

    + settings_notifications +

    +
    + settings_notify_me +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +`; + +exports[`app/panel/Settings/Notifications.jsx Snapshot tests with react-test-renderer Notifications is rendered correctly with truthy props 1`] = ` +
    +
    +
    +

    + settings_notifications +

    +
    + settings_notify_me +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +`; diff --git a/app/panel/components/__tests__/About.jsx b/app/panel/components/__tests__/About.jsx new file mode 100644 index 000000000..509dacfe8 --- /dev/null +++ b/app/panel/components/__tests__/About.jsx @@ -0,0 +1,27 @@ +/** + * About Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * 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 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import About from '../About'; + +describe('app/panel/components/About.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('About panel is rendered correctly', () => { + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/app/panel/components/__tests__/Help.jsx b/app/panel/components/__tests__/Help.jsx new file mode 100644 index 000000000..bfd93a890 --- /dev/null +++ b/app/panel/components/__tests__/Help.jsx @@ -0,0 +1,32 @@ +/** + * Help Test Component + * + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2019 Ghostery, Inc. All rights reserved. + * + * 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 + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import Help from '../Help'; + +jest.mock('../../utils/msg', () => ({ + openSupportPage: () => {}, + openHubPage: () => {}, +})); + +describe('app/panel/components/Help.jsx', () => { + describe('Snapshot tests with react-test-renderer', () => { + test('Help panel is rendered correctly', () => { + const component = renderer.create( + + ).toJSON(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/app/panel/components/__tests__/__snapshots__/About.jsx.snap b/app/panel/components/__tests__/__snapshots__/About.jsx.snap new file mode 100644 index 000000000..7d943a683 --- /dev/null +++ b/app/panel/components/__tests__/__snapshots__/About.jsx.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/components/About.jsx Snapshot tests with react-test-renderer About panel is rendered correctly 1`] = ` +
    +`; diff --git a/app/panel/components/__tests__/__snapshots__/Help.jsx.snap b/app/panel/components/__tests__/__snapshots__/Help.jsx.snap new file mode 100644 index 000000000..6043581f6 --- /dev/null +++ b/app/panel/components/__tests__/__snapshots__/Help.jsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/panel/components/Help.jsx Snapshot tests with react-test-renderer Help panel is rendered correctly 1`] = ` +
    +
    +
    +

    + panel_help_panel_header +

    + +
    +

    + panel_help_questions_header +

    + + panel_help_faq + + + panel_help_feedback + + + support + +
    +
    +

    + panel_help_contact_header +

    + + info@ghostery.com + +
    +
    +
    +
    +`; From f540226e43690f3544b2af9116f5d395eddace33 Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Thu, 30 Jul 2020 19:45:56 -0400 Subject: [PATCH 80/89] fix linting error --- app/panel/components/Settings/__tests__/GeneralSettings.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/panel/components/Settings/__tests__/GeneralSettings.jsx b/app/panel/components/Settings/__tests__/GeneralSettings.jsx index de277beb2..3a3bee46b 100644 --- a/app/panel/components/Settings/__tests__/GeneralSettings.jsx +++ b/app/panel/components/Settings/__tests__/GeneralSettings.jsx @@ -72,7 +72,6 @@ describe('app/panel/Settings/GeneralSettings.jsx', () => { }); }); - describe('Shallow snapshot tests rendered with Enzyme', () => { test('GeneralSettings functions correctly', () => { const settingsData = { From 47cabf9ec5648c26237b356d967117395c955cad Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Fri, 31 Jul 2020 11:58:29 -0400 Subject: [PATCH 81/89] Update GeneralSettings test to not depend on MomentJS --- app/panel-android/actions/handler.js | 4 ++-- app/panel-android/actions/settingsActions.js | 2 +- app/panel-android/components/content/OverviewTab.jsx | 2 -- app/panel/components/Settings/__tests__/GeneralSettings.jsx | 2 ++ .../Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/panel-android/actions/handler.js b/app/panel-android/actions/handler.js index 32c27214a..34a3744b0 100644 --- a/app/panel-android/actions/handler.js +++ b/app/panel-android/actions/handler.js @@ -12,10 +12,10 @@ */ import { - handleTrustButtonClick, handleRestrictButtonClick, handlePauseButtonClick, cliqzFeatureToggle, updateSitePolicy + updateSitePolicy, handleTrustButtonClick, handleRestrictButtonClick, handlePauseButtonClick, cliqzFeatureToggle } from './summaryActions'; import { - trustRestrictBlockSiteTracker, anonymizeSiteTracker, blockUnblockGlobalTracker, blockUnBlockAllTrackers, resetSettings + anonymizeSiteTracker, trustRestrictBlockSiteTracker, blockUnblockGlobalTracker, blockUnBlockAllTrackers, resetSettings } from './blockingActions'; import { updateDatabase, updateSettingCheckbox, selectItem diff --git a/app/panel-android/actions/settingsActions.js b/app/panel-android/actions/settingsActions.js index 28015510b..af93e84e4 100644 --- a/app/panel-android/actions/settingsActions.js +++ b/app/panel-android/actions/settingsActions.js @@ -71,7 +71,7 @@ export function selectItem({ actionData }) { }; } -// ToDo: Implement for Import/Export +// ToDo: Implement for Import/Export & add them them to handler.js export function exportSettings() {} export function importSettingsDialog() {} export function importSettingsNative() {} diff --git a/app/panel-android/components/content/OverviewTab.jsx b/app/panel-android/components/content/OverviewTab.jsx index bc1a65d4b..07a67b41f 100644 --- a/app/panel-android/components/content/OverviewTab.jsx +++ b/app/panel-android/components/content/OverviewTab.jsx @@ -14,7 +14,6 @@ import React from 'react'; import ClassNames from 'classnames'; import PropTypes from 'prop-types'; - import { NotScanned, DonutGraph, @@ -22,7 +21,6 @@ import { PauseButton, CliqzFeature } from '../../../panel/components/BuildingBlocks'; - import globals from '../../../../src/classes/Globals'; const { diff --git a/app/panel/components/Settings/__tests__/GeneralSettings.jsx b/app/panel/components/Settings/__tests__/GeneralSettings.jsx index 3a3bee46b..84a0fce33 100644 --- a/app/panel/components/Settings/__tests__/GeneralSettings.jsx +++ b/app/panel/components/Settings/__tests__/GeneralSettings.jsx @@ -16,6 +16,8 @@ import renderer from 'react-test-renderer'; import { mount } from 'enzyme'; import GeneralSettings from '../GeneralSettings'; +jest.spyOn(GeneralSettings, 'getDbLastUpdated').mockImplementation((settingsData) => settingsData.bugs_last_checked); + describe('app/panel/Settings/GeneralSettings.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { test('GeneralSettings is rendered correctly with falsy props', () => { diff --git a/app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap b/app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap index 3a68f2101..22e43b4f7 100644 --- a/app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap +++ b/app/panel/components/Settings/__tests__/__snapshots__/GeneralSettings.jsx.snap @@ -43,7 +43,7 @@ exports[`app/panel/Settings/GeneralSettings.jsx Snapshot tests with react-test-r - December 31, 1969 7:00 PM + - December 31, 1969 7:16 PM + 1000000 Date: Fri, 31 Jul 2020 12:04:09 -0400 Subject: [PATCH 82/89] fix linting error --- app/panel/components/Settings/__tests__/GeneralSettings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/panel/components/Settings/__tests__/GeneralSettings.jsx b/app/panel/components/Settings/__tests__/GeneralSettings.jsx index 84a0fce33..d67ab814e 100644 --- a/app/panel/components/Settings/__tests__/GeneralSettings.jsx +++ b/app/panel/components/Settings/__tests__/GeneralSettings.jsx @@ -16,7 +16,7 @@ import renderer from 'react-test-renderer'; import { mount } from 'enzyme'; import GeneralSettings from '../GeneralSettings'; -jest.spyOn(GeneralSettings, 'getDbLastUpdated').mockImplementation((settingsData) => settingsData.bugs_last_checked); +jest.spyOn(GeneralSettings, 'getDbLastUpdated').mockImplementation(settingsData => settingsData.bugs_last_checked); describe('app/panel/Settings/GeneralSettings.jsx', () => { describe('Snapshot tests with react-test-renderer', () => { From a53016d45f8fba275a5bea43da7267dc6eb7d52c Mon Sep 17 00:00:00 2001 From: Caleb Richelson Date: Sat, 1 Aug 2020 00:27:04 -0400 Subject: [PATCH 83/89] Implement Import/Export feature --- app/panel-android/actions/handler.js | 14 ++- app/panel-android/actions/settingsActions.js | 117 +++++++++++++++++- app/panel-android/components/PanelAndroid.jsx | 4 + .../components/content/Settings.jsx | 41 ++++-- src/background.js | 11 ++ 5 files changed, 172 insertions(+), 15 deletions(-) diff --git a/app/panel-android/actions/handler.js b/app/panel-android/actions/handler.js index 34a3744b0..09d1aca99 100644 --- a/app/panel-android/actions/handler.js +++ b/app/panel-android/actions/handler.js @@ -18,7 +18,7 @@ import { anonymizeSiteTracker, trustRestrictBlockSiteTracker, blockUnblockGlobalTracker, blockUnBlockAllTrackers, resetSettings } from './blockingActions'; import { - updateDatabase, updateSettingCheckbox, selectItem + updateDatabase, updateSettingCheckbox, selectItem, exportSettings, importSettingsDialog, importSettingsNative } from './settingsActions'; // Handle all actions in Panel.jsx @@ -78,6 +78,18 @@ export default function handleAllActions({ actionName, actionData, state }) { updated = selectItem({ actionData, state }); break; + case 'exportSettings': + updated = exportSettings({ actionData, state }); + break; + + case 'importSettingsDialog': + updated = importSettingsDialog({ actionData, state }); + break; + + case 'importSettingsNative': + updated = importSettingsNative({ actionData, state }); + break; + default: updated = {}; } diff --git a/app/panel-android/actions/settingsActions.js b/app/panel-android/actions/settingsActions.js index af93e84e4..b92a84e5f 100644 --- a/app/panel-android/actions/settingsActions.js +++ b/app/panel-android/actions/settingsActions.js @@ -11,7 +11,60 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0 */ +import moment from 'moment/min/moment-with-locales.min'; import { sendMessage, sendMessageInPromise } from '../../panel/utils/msg'; +import { hashCode } from '../../../src/utils/common'; + +// Function taken from app/content-scripts/notifications.js +function _saveToFile({ content, type }) { + const textFileAsBlob = new Blob([content], { type: 'text/plain' }); + const ext = type === 'Ghostery-Backup' ? 'ghost' : 'json'; + const d = new Date(); + const dStr = `${d.getMonth() + 1}-${d.getDate()}-${d.getFullYear()}`; + const fileNameToSaveAs = `${type}-${dStr}.${ext}`; + let url = ''; + if (window.URL) { + url = window.URL.createObjectURL(textFileAsBlob); + } else { + url = window.webkitURL.createObjectURL(textFileAsBlob); + } + + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', fileNameToSaveAs); + document.body.appendChild(link); + link.click(); +} + +function _importFromFile(fileToLoad) { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + fileReader.onload = (fileLoadedEvent) => { + try { + const backup = JSON.parse(fileLoadedEvent.target.result); + if (backup.hash !== hashCode(JSON.stringify(backup.settings))) { + throw new Error('Invalid hash'); + } + const settings = (backup.settings || {}).conf || {}; + resolve(settings); + } catch (err) { + reject(err); + } + }; + fileReader.readAsText(fileToLoad, 'UTF-8'); + }); +} + +function _chooseFile() { + return new Promise((resolve) => { + const inputEl = document.createElement('input'); + inputEl.type = 'file'; + inputEl.addEventListener('change', () => { + resolve(inputEl.files[0]); + }); + inputEl.click(); + }); +} export function updateDatabase() { // Send Message to Background @@ -71,7 +124,63 @@ export function selectItem({ actionData }) { }; } -// ToDo: Implement for Import/Export & add them them to handler.js -export function exportSettings() {} -export function importSettingsDialog() {} -export function importSettingsNative() {} +export function exportSettings({ state }) { + return sendMessageInPromise('gather_ghostery_export_data').then((result) => { + const { needsReload } = state; + const settings_last_exported = Number((new Date()).getTime()); + const exportResultText = `${t('settings_export_success')} ${moment(settings_last_exported).format('LLL')}`; + + _saveToFile(result); + + return { + needsReload, + settings: { + actionSuccess: true, + settings_last_exported, + exportResultText, + } + }; + }); +} + +export function importSettingsNative({ actionData, state }) { + return new Promise((resolve) => { + const { needsReload } = state; + const settings_last_imported = Number((new Date()).getTime()); + const importResultText = `${t('settings_import_success')} ${moment(settings_last_imported).format('LLL')}`; + + _importFromFile(actionData).then((settings) => { + // Taken from panel/reducers/settings.js + const imported_settings = { ...settings }; + if (imported_settings.hasOwnProperty('alert_bubble_timeout')) { + imported_settings.alert_bubble_timeout = Math.min(30, imported_settings.alert_bubble_timeout); + } + + imported_settings.settings_last_imported = Number((new Date()).getTime()); + imported_settings.importResultText = `${t('settings_import_success')} ${moment(imported_settings.settings_last_imported).format('LLL')}`; + imported_settings.actionSuccess = true; + + // persist to background + sendMessage('setPanelData', imported_settings); + + return resolve({ + needsReload: true, + settings: { + actionSuccess: true, + settings_last_imported, + importResultText, + } + }); + }).catch(() => resolve({ + needsReload, + settings: { + actionSuccess: false, + importResultText: t('settings_import_file_error'), + } + })); + }); +} + +export function importSettingsDialog({ state }) { + return _chooseFile().then(fileToLoad => importSettingsNative({ actionData: fileToLoad, state })); +} diff --git a/app/panel-android/components/PanelAndroid.jsx b/app/panel-android/components/PanelAndroid.jsx index 27696fa72..0dbd1bfa2 100644 --- a/app/panel-android/components/PanelAndroid.jsx +++ b/app/panel-android/components/PanelAndroid.jsx @@ -140,6 +140,10 @@ class PanelAndroid extends React.Component { newState[key] = { ...this.state[key], ...updated[key] }; // eslint-disable-line react/destructuring-assignment }); + if (updated.needsReload === false) { + newState.needsReload = false; + } + this.setState(newState); } diff --git a/app/panel-android/components/content/Settings.jsx b/app/panel-android/components/content/Settings.jsx index f68a577f6..38698a3cb 100644 --- a/app/panel-android/components/content/Settings.jsx +++ b/app/panel-android/components/content/Settings.jsx @@ -22,8 +22,6 @@ import OptIn from '../../../panel/components/Settings/OptIn'; import ImportExport from '../../../panel/components/Settings/ImportExport'; import Help from '../../../panel/components/Help'; import About from '../../../panel/components/About'; -import { exportSettings, importSettingsDialog, importSettingsNative } from '../../actions/settingsActions'; - import globals from '../../../../src/classes/Globals'; const { IS_CLIQZ } = globals; @@ -84,6 +82,31 @@ class Settings extends React.Component { }); } + exportSettings = () => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'exportSettings', + }); + } + + importSettingsDialog = () => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'importSettingsDialog', + }); + } + + importSettingsNative = (importFile) => { + const { callGlobalAction } = this.props; + + callGlobalAction({ + actionName: 'importSettingsNative', + actionData: importFile, + }); + } + _renderSettingsHeader() { const { view } = this.state; @@ -153,11 +176,9 @@ class Settings extends React.Component {
    { this.setState({ view: 'settings-opt-in' }); }}> { t('settings_opt_in') }
    - {false && ( // Remove to show Import/Export menu item -
    { this.setState({ view: 'settings-import-export' }); }}> - { t('settings_import_export') } -
    - )} +
    { this.setState({ view: 'settings-import-export' }); }}> + { t('settings_import_export') } +
    { this.setState({ view: 'settings-help' }); }}> { t('panel_menu_help') }
    @@ -251,9 +272,9 @@ class Settings extends React.Component { actionSuccess, }; const actions = { - exportSettings, - importSettingsDialog, - importSettingsNative, + exportSettings: this.exportSettings, + importSettingsDialog: this.importSettingsDialog, + importSettingsNative: this.importSettingsNative, }; return ( diff --git a/src/background.js b/src/background.js index 5281790ff..6d2e90f33 100644 --- a/src/background.js +++ b/src/background.js @@ -979,6 +979,17 @@ function onMessageHandler(request, sender, callback) { closeAndroidPanelTabs(); return false; } + if (name === 'gather_ghostery_export_data') { + const settings = account.buildUserSettings(); + settings.site_blacklist = conf.site_blacklist; + settings.site_whitelist = conf.site_whitelist; + + const hash = common.hashCode(JSON.stringify({ conf: settings })); + const backup = JSON.stringify({ hash, settings: { conf: settings } }); + const msg = { type: 'Ghostery-Backup', content: backup }; + callback(msg); + return true; + } if (name === 'getSettingsForExport') { utils.getActiveTab((activeTab) => { if (activeTab && activeTab.id && activeTab.url.startsWith('http')) { From 552923f823debf8750bf9533efba64d0c20966af Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Mon, 3 Aug 2020 11:12:08 -0400 Subject: [PATCH 84/89] fix android UA detection --- src/classes/Globals.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/classes/Globals.js b/src/classes/Globals.js index 2e0907800..328ee2718 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -153,15 +153,24 @@ class Globals { const version = parseInt(ua.browser.version.toString(), 10); // convert to string for Chrome const platform = ua.os.name.toLowerCase(); + // Check for Ghostery Android Browser + if (typeof chrome.runtime.getBrowserInfo === 'function') { + chrome.runtime.getBrowserInfo().then((info) => { + if (info.name === 'Ghostery') { + this.BROWSER_INFO.displayName = 'Ghostery Android Browser'; + this.BROWSER_INFO.name = 'ghostery_android'; + this.BROWSER_INFO.token = 'ga'; + this.BROWSER_INFO.os = 'android'; + this.BROWSER_INFO.version = info.version; + } + }); + } + // Set name and token properties. CMP uses `name` value. Metrics uses `token` if (this.IS_CLIQZ) { this.BROWSER_INFO.displayName = 'Cliqz'; this.BROWSER_INFO.name = 'cliqz'; this.BROWSER_INFO.token = 'cl'; - } else if (navigator.userAgent.toLocaleLowerCase().includes('ghostery')) { - this.BROWSER_INFO.displayName = 'Ghostery Android Browser'; - this.BROWSER_INFO.name = 'ghostery_android'; - this.BROWSER_INFO.token = 'ga'; } else if (browser.includes('edge')) { this.BROWSER_INFO.displayName = 'Edge'; this.BROWSER_INFO.name = 'edge'; @@ -174,7 +183,7 @@ class Globals { this.BROWSER_INFO.displayName = 'Chrome'; this.BROWSER_INFO.name = 'chrome'; this.BROWSER_INFO.token = 'ch'; - } else if (browser.includes('firefox')) { + } else if (browser.includes('firefox') && !this.BROWSER_INFO.name.length) { this.BROWSER_INFO.displayName = 'Firefox'; this.BROWSER_INFO.name = 'firefox'; this.BROWSER_INFO.token = 'ff'; From 9a3459f15a6b8cbc4631facfd7d0351ad91a3bcb Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Tue, 4 Aug 2020 11:20:29 -0400 Subject: [PATCH 85/89] fix hw opt-in for android browser --- src/background.js | 2 +- src/classes/Globals.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/background.js b/src/background.js index 6d2e90f33..c7c1ec9ba 100644 --- a/src/background.js +++ b/src/background.js @@ -1660,7 +1660,7 @@ function initializeGhosteryModules() { conf.enable_human_web = !humanweb.isDisabled; conf.enable_offers = !offers.isDisabled && !IS_ANDROID; - if (IS_FIREFOX) { + if (IS_FIREFOX && BROWSER_INFO.name !== 'ghostery_android') { if (globals.JUST_INSTALLED) { conf.enable_human_web = false; conf.enable_offers = false; diff --git a/src/classes/Globals.js b/src/classes/Globals.js index 328ee2718..f0d85ba1b 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -155,6 +155,8 @@ class Globals { // Check for Ghostery Android Browser if (typeof chrome.runtime.getBrowserInfo === 'function') { + // TODO: This can create a race condition when BROWSER_INFO is evaluated at runtime. + // In background.js, IS_FIREFOX will always be true. chrome.runtime.getBrowserInfo().then((info) => { if (info.name === 'Ghostery') { this.BROWSER_INFO.displayName = 'Ghostery Android Browser'; From 7be82e081dbfce08804b2019efa5e7c4bffa4402 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Tue, 4 Aug 2020 12:33:09 -0400 Subject: [PATCH 86/89] fix humanweb opt-in for hub on android --- app/hub/Views/HomeView/HomeView.jsx | 9 ++++----- app/hub/Views/SetupView/SetupViewContainer.jsx | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/hub/Views/HomeView/HomeView.jsx b/app/hub/Views/HomeView/HomeView.jsx index 91727d0b1..8f911fd88 100644 --- a/app/hub/Views/HomeView/HomeView.jsx +++ b/app/hub/Views/HomeView/HomeView.jsx @@ -18,8 +18,7 @@ import { NavLink } from 'react-router-dom'; import globals from '../../../../src/classes/Globals'; import { ToggleCheckbox } from '../../../shared-components'; -const { IS_CLIQZ } = globals; -const IS_FIREFOX = (globals.BROWSER_INFO.name === 'firefox'); +const { IS_CLIQZ, BROWSER_INFO } = globals; /** * A Functional React component for rendering the Home View @@ -40,10 +39,10 @@ const HomeView = (props) => { const accountHref = globals.ACCOUNT_BASE_URL; let headerInfoText = t('hub_home_header_info'); - if (globals.BROWSER_INFO) { - if (IS_FIREFOX) { + if (BROWSER_INFO) { + if (BROWSER_INFO.name === 'firefox') { headerInfoText = t('hub_home_header_info_opted_out'); - } else if (IS_CLIQZ) { + } else if (IS_CLIQZ || BROWSER_INFO.name === 'ghostery_android') { headerInfoText = t('hub_home_header_info_cliqz'); } } diff --git a/app/hub/Views/SetupView/SetupViewContainer.jsx b/app/hub/Views/SetupView/SetupViewContainer.jsx index 2a0e65021..38e410a23 100644 --- a/app/hub/Views/SetupView/SetupViewContainer.jsx +++ b/app/hub/Views/SetupView/SetupViewContainer.jsx @@ -111,7 +111,7 @@ class SetupViewContainer extends Component { actions.setAdBlock({ enable_ad_block: true }); actions.setSmartBlocking({ enable_smart_block: true }); actions.setGhosteryRewards({ enable_ghostery_rewards: !IS_FIREFOX && !IS_ANDROID }); - actions.setHumanWeb({ enable_human_web: !IS_FIREFOX }); + actions.setHumanWeb({ enable_human_web: !IS_FIREFOX || BROWSER_INFO.name === 'ghostery_android' }); } /** From 63738ee89b9a779382d27a0bda14fb263e655491 Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Thu, 6 Aug 2020 10:43:52 -0400 Subject: [PATCH 87/89] clean up annotation and naming for android --- app/panel-android/actions/blockingActions.js | 1 + app/panel-android/actions/settingsActions.js | 2 +- src/background.js | 6 ++---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/panel-android/actions/blockingActions.js b/app/panel-android/actions/blockingActions.js index dea07ad77..4ff46c5fe 100644 --- a/app/panel-android/actions/blockingActions.js +++ b/app/panel-android/actions/blockingActions.js @@ -68,6 +68,7 @@ function calculateDelta(oldState, newState) { return 0; } +// Modified version of _updateCliqzModuleWhitelist from app/panel/reducers/blocking.js export function anonymizeSiteTracker({ actionData, state }) { const updatedcliqzModuleData = JSON.parse(JSON.stringify(state.cliqzModuleData)); const { antiTracking, adBlock } = state.cliqzModuleData; diff --git a/app/panel-android/actions/settingsActions.js b/app/panel-android/actions/settingsActions.js index b92a84e5f..c850cc94e 100644 --- a/app/panel-android/actions/settingsActions.js +++ b/app/panel-android/actions/settingsActions.js @@ -125,7 +125,7 @@ export function selectItem({ actionData }) { } export function exportSettings({ state }) { - return sendMessageInPromise('gather_ghostery_export_data').then((result) => { + return sendMessageInPromise('getAndroidSettingsForExport').then((result) => { const { needsReload } = state; const settings_last_exported = Number((new Date()).getTime()); const exportResultText = `${t('settings_export_success')} ${moment(settings_last_exported).format('LLL')}`; diff --git a/src/background.js b/src/background.js index c7c1ec9ba..dcc74e8ff 100644 --- a/src/background.js +++ b/src/background.js @@ -747,9 +747,7 @@ function onMessageHandler(request, sender, callback) { } // HANDLE UNIVERSAL EVENTS HERE (NO ORIGIN LISTED ABOVE) - // The 'getPanelData' message is never sent by the panel, which uses ports only since 8.3.2 - // The message is still sent by panel-android and by the setup hub as of 8.4.0 - if (name === 'getPanelData') { + if (name === 'getPanelData') { // Used by panel-android and the intro hub if (!message.tabId) { utils.getActiveTab((activeTab) => { const data = panelData.get(message.view, activeTab); @@ -979,7 +977,7 @@ function onMessageHandler(request, sender, callback) { closeAndroidPanelTabs(); return false; } - if (name === 'gather_ghostery_export_data') { + if (name === 'getAndroidSettingsForExport') { const settings = account.buildUserSettings(); settings.site_blacklist = conf.site_blacklist; settings.site_whitelist = conf.site_whitelist; From 7c4e23aca1bf841352632a480c2eb55254de927c Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Sun, 9 Aug 2020 12:59:23 -0400 Subject: [PATCH 88/89] refactor getBrowserInfo check --- src/classes/Globals.js | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/classes/Globals.js b/src/classes/Globals.js index f0d85ba1b..9aed8fa14 100644 --- a/src/classes/Globals.js +++ b/src/classes/Globals.js @@ -153,21 +153,6 @@ class Globals { const version = parseInt(ua.browser.version.toString(), 10); // convert to string for Chrome const platform = ua.os.name.toLowerCase(); - // Check for Ghostery Android Browser - if (typeof chrome.runtime.getBrowserInfo === 'function') { - // TODO: This can create a race condition when BROWSER_INFO is evaluated at runtime. - // In background.js, IS_FIREFOX will always be true. - chrome.runtime.getBrowserInfo().then((info) => { - if (info.name === 'Ghostery') { - this.BROWSER_INFO.displayName = 'Ghostery Android Browser'; - this.BROWSER_INFO.name = 'ghostery_android'; - this.BROWSER_INFO.token = 'ga'; - this.BROWSER_INFO.os = 'android'; - this.BROWSER_INFO.version = info.version; - } - }); - } - // Set name and token properties. CMP uses `name` value. Metrics uses `token` if (this.IS_CLIQZ) { this.BROWSER_INFO.displayName = 'Cliqz'; @@ -185,7 +170,7 @@ class Globals { this.BROWSER_INFO.displayName = 'Chrome'; this.BROWSER_INFO.name = 'chrome'; this.BROWSER_INFO.token = 'ch'; - } else if (browser.includes('firefox') && !this.BROWSER_INFO.name.length) { + } else if (browser.includes('firefox')) { this.BROWSER_INFO.displayName = 'Firefox'; this.BROWSER_INFO.name = 'firefox'; this.BROWSER_INFO.token = 'ff'; @@ -208,6 +193,29 @@ class Globals { // Set version property this.BROWSER_INFO.version = version; + + // Check for the Ghostery Android browser + this._checkForGhosteryAndroid(); + } + + /** + * Check for Ghostery Android Browser and update BROWSER_INFO. + * Note: This is asynchronous and not available at runtime. + * @private + * @return boolean + */ + _checkForGhosteryAndroid() { + if (typeof chrome.runtime.getBrowserInfo === 'function') { + chrome.runtime.getBrowserInfo().then((info) => { + if (info.name === 'Ghostery') { + this.BROWSER_INFO.displayName = 'Ghostery Android Browser'; + this.BROWSER_INFO.name = 'ghostery_android'; + this.BROWSER_INFO.token = 'ga'; + this.BROWSER_INFO.os = 'android'; + this.BROWSER_INFO.version = info.version; + } + }); + } } } From d9fb792a4a9cc7bd628064ce783fb44885123eba Mon Sep 17 00:00:00 2001 From: Christopher Tino Date: Mon, 10 Aug 2020 12:17:42 -0400 Subject: [PATCH 89/89] ternary cleanup --- src/background.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/background.js b/src/background.js index dcc74e8ff..f17467dfd 100644 --- a/src/background.js +++ b/src/background.js @@ -1755,16 +1755,12 @@ function initializeGhosteryModules() { ]).then(() => { // run scheduledTasks on init scheduledTasks().then(() => { - // open the Ghostery Hub on install with justInstalled query parameter set to true - // we need to do this after running scheduledTasks for the first time + // Open the Ghostery Hub on install with justInstalled query parameter set to true. + // We need to do this after running scheduledTasks for the first time // because of an A/B test that determines which promo variant is shown in the Hub on install if (globals.JUST_INSTALLED) { - let route = (conf.hub_promo_variant === 'upgrade' || conf.hub_promo_variant === 'not_yet_set') ? '' : '#home'; - let showPremiumPromoModal = conf.hub_promo_variant === 'midnight'; - if (IS_ANDROID) { - route = '#home'; - showPremiumPromoModal = false; - } + const route = ((conf.hub_promo_variant === 'upgrade' || conf.hub_promo_variant === 'not_yet_set') && !IS_ANDROID) ? '' : '#home'; + const showPremiumPromoModal = (conf.hub_promo_variant === 'midnight' && !IS_ANDROID); chrome.tabs.create({ url: chrome.runtime.getURL(`./app/templates/hub.html?$justInstalled=true&pm=${showPremiumPromoModal}${route}`), active: true