diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 0f19a06b147c..96d4bb51939b 100755 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -1498,9 +1498,6 @@ var gBrowserInit = { } } - // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008. - setTimeout(function() { SafeBrowsing.init(); }, 2000); - Services.obs.addObserver(gIdentityHandler, "perm-changed"); Services.obs.addObserver(gRemoteControl, "remote-active"); Services.obs.addObserver(gSessionHistoryObserver, "browser:purge-session-history"); @@ -4991,6 +4988,16 @@ var CombinedStopReload = { }); }, + /* This function is necessary to correctly vertically center the animation + within the toolbar, which uses -moz-pack-align:stretch; and thus a height + which is dependant on the font-size. */ + setAnimationImageHeightRelativeToToolbarButtonHeight() { + let dwu = window.getInterface(Ci.nsIDOMWindowUtils); + let toolbarItem = this.stopReloadContainer.closest(".customization-target > toolbaritem"); + let bounds = dwu.getBoundsWithoutFlushing(toolbarItem); + toolbarItem.style.setProperty("--toolbarbutton-height", bounds.height + "px"); + }, + switchToStop(aRequest, aWebProgress) { if (!this._initialized) return; @@ -5002,10 +5009,12 @@ var CombinedStopReload = { this.animate; this._cancelTransition(); - if (shouldAnimate) + if (shouldAnimate) { + this.setAnimationImageHeightRelativeToToolbarButtonHeight(); this.stopReloadContainer.setAttribute("animate", "true"); - else + } else { this.stopReloadContainer.removeAttribute("animate"); + } this.reload.setAttribute("displaystop", "true"); }, @@ -5019,10 +5028,12 @@ var CombinedStopReload = { !aWebProgress.isLoadingDocument && this.animate; - if (shouldAnimate) + if (shouldAnimate) { + this.setAnimationImageHeightRelativeToToolbarButtonHeight(); this.stopReloadContainer.setAttribute("animate", "true"); - else + } else { this.stopReloadContainer.removeAttribute("animate"); + } this.reload.removeAttribute("displaystop"); diff --git a/browser/base/content/test/newtab/browser_newtab_focus.js b/browser/base/content/test/newtab/browser_newtab_focus.js index f02260636812..afeb949745f8 100644 --- a/browser/base/content/test/newtab/browser_newtab_focus.js +++ b/browser/base/content/test/newtab/browser_newtab_focus.js @@ -8,7 +8,7 @@ add_task(async function() { await pushPrefs(["accessibility.tabfocus", 7]); // When the onboarding component is enabled, it would inject extra tour notification into - // the newtab page so there would be 2 more notification close button and action button + // the newtab page so there would be 3 more overlay button, notification close button and action button let onbardingEnabled = AppConstants.NIGHTLY_BUILD && Services.prefs.getBoolPref("browser.onboarding.enabled"); // Focus count in new tab page. @@ -26,7 +26,7 @@ add_task(async function() { } let tab = await addNewTabPageTab(); if (onbardingEnabled) { - FOCUS_COUNT += 2; + FOCUS_COUNT += 3; await promiseTourNotificationOpened(tab.linkedBrowser); } gURLBar.focus(); @@ -37,7 +37,7 @@ add_task(async function() { let expectedCount = 4; if (onbardingEnabled) { - expectedCount += 2; + expectedCount += 3; } countFocus(expectedCount); diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index e33378b2e4fa..970957b3ae9a 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -20,6 +20,9 @@ XPCOMUtils.defineLazyGetter(this, "WeaveService", () => XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService", "resource://gre/modules/ContextualIdentityService.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing", + "resource://gre/modules/SafeBrowsing.jsm"); + // lazy module getters /* global AboutHome:false, AboutNewTab:false, AddonManager:false, AppMenuNotifications:false, @@ -1185,6 +1188,10 @@ BrowserGlue.prototype = { ContextualIdentityService.load(); }); + Services.tm.idleDispatchToMainThread(() => { + SafeBrowsing.init(); + }, 5000); + this._sanitizer.onStartup(); E10SAccessibilityCheck.onWindowsRestored(); }, diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm index 2d0314783b7e..6ce15f877e60 100644 --- a/browser/components/sessionstore/SessionStore.jsm +++ b/browser/components/sessionstore/SessionStore.jsm @@ -3279,8 +3279,8 @@ var SessionStoreInternal = { } } - let restoreTabsLazily = this._prefBranch.getBoolPref("sessionstore.restore_tabs_lazily") && - this._prefBranch.getBoolPref("sessionstore.restore_on_demand"); + let restoreOnDemand = this._prefBranch.getBoolPref("sessionstore.restore_on_demand"); + let restoreTabsLazily = this._prefBranch.getBoolPref("sessionstore.restore_tabs_lazily") && restoreOnDemand; for (var t = 0; t < newTabCount; t++) { let tabData = winData.tabs[t]; @@ -3333,6 +3333,13 @@ var SessionStoreInternal = { tabbrowser.selectedTab = tab; tabbrowser.removeTab(leftoverTab); } + + // Prepare connection to the host when users hover mouse over this + // tab. If we're not restoring on demand, we'll prepare connection + // when we're restoring next tab. + if (!tabData.pinned && restoreOnDemand) { + this.speculativeConnectOnTabHover(tab, url); + } } tabs.push(tab); @@ -3417,6 +3424,42 @@ var SessionStoreInternal = { this._sendRestoreCompletedNotifications(); }, + /** + * Prepare connection to host beforehand. + * + * @param url + * URL of a host. + * @returns a flag indicates whether a connection has been made + */ + prepareConnectionToHost(url) { + if (!url.startsWith("about:")) { + let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect); + let uri = Services.io.newURI(url); + sc.speculativeConnect(uri, null, null); + return true; + } + return false; + }, + + /** + * Make a connection to a host when users hover mouse on a tab. + * + * @param tab + * A tab to set up a hover listener. + * @param url + * URL of a host. + */ + speculativeConnectOnTabHover(tab, url) { + tab.addEventListener("mouseover", () => { + let prepared = this.prepareConnectionToHost(url); + // This is used to test if a connection has been made beforehand. + if (gDebuggingEnabled) { + tab.__test_connection_prepared = prepared; + tab.__test_connection_url = url; + } + }, {once: true}); + }, + /** * Restore multiple windows using the provided state. * @param aWindow @@ -3683,6 +3726,19 @@ var SessionStoreInternal = { this.restoreTabContent(tab, options); } else if (!forceOnDemand) { TabRestoreQueue.add(tab); + // Check if a tab is in queue and will be restored + // after the currently loading tabs. If so, prepare + // a connection to host to speed up page loading. + if (TabRestoreQueue.willRestoreSoon(tab)) { + if (activeIndex in tabData.entries) { + let url = tabData.entries[activeIndex].url; + let prepared = this.prepareConnectionToHost(url); + if (gDebuggingEnabled) { + tab.__test_connection_prepared = prepared; + tab.__test_connection_url = url; + } + } + } this.restoreNextTab(); } } else { diff --git a/browser/components/sessionstore/test/browser.ini b/browser/components/sessionstore/test/browser.ini index 3e6f96393593..c6a11a084aeb 100644 --- a/browser/components/sessionstore/test/browser.ini +++ b/browser/components/sessionstore/test/browser.ini @@ -35,6 +35,7 @@ support-files = browser_scrollPositions_sample_frameset.html browser_scrollPositions_readerModeArticle.html browser_sessionStorage.html + browser_speculative_connect.html browser_248970_b_sample.html browser_339445_sample.html browser_423132_sample.html @@ -263,3 +264,5 @@ skip-if = !e10s # Tabs can't crash without e10s [browser_cookies.js] [browser_cookies_legacy.js] [browser_cookies_privacy.js] +[browser_speculative_connect.js] + diff --git a/browser/components/sessionstore/test/browser_speculative_connect.html b/browser/components/sessionstore/test/browser_speculative_connect.html new file mode 100644 index 000000000000..d8ce2f15384d --- /dev/null +++ b/browser/components/sessionstore/test/browser_speculative_connect.html @@ -0,0 +1,9 @@ + +
+ Dummy html page to test speculative connect +
+ + Hello Speculative Connect + + + diff --git a/browser/components/sessionstore/test/browser_speculative_connect.js b/browser/components/sessionstore/test/browser_speculative_connect.js new file mode 100644 index 000000000000..b261eda244f2 --- /dev/null +++ b/browser/components/sessionstore/test/browser_speculative_connect.js @@ -0,0 +1,96 @@ +const TEST_URLS = [ + "about:buildconfig", + "http://mochi.test:8888/browser/browser/components/sessionstore/test/browser_speculative_connect.html", + "" +]; + +/** + * This will open tabs in browser. This will also make the last tab + * inserted to be the selected tab. + */ +async function openTabs(win) { + for (let i = 0; i < TEST_URLS.length; ++i) { + await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URLS[i]); + } +} + +add_task(async function speculative_connect_restore_on_demand() { + Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true); + is(Services.prefs.getBoolPref("browser.sessionstore.restore_on_demand"), true, "We're restoring on demand"); + forgetClosedWindows(); + + // Open a new window and populate with tabs. + let win = await promiseNewWindowLoaded(); + await openTabs(win); + + // Close the window. + await BrowserTestUtils.closeWindow(win); + + // Reopen a window. + let newWin = undoCloseWindow(0); + // Make sure we wait until this window is restored. + await BrowserTestUtils.waitForEvent(newWin, "load"); + await BrowserTestUtils.waitForEvent(newWin.gBrowser.tabContainer, "SSTabRestored"); + + let tabs = newWin.gBrowser.tabs; + is(tabs.length, TEST_URLS.length + 1, "Restored right number of tabs"); + + let e = new MouseEvent("mouseover"); + + // First tab should be ignore, since it's the default blank tab when we open a new window. + + // Trigger a mouse enter on second tab. + tabs[1].dispatchEvent(e); + is(tabs[1].__test_connection_prepared, false, "Second tab doesn't have a connection prepared"); + is(tabs[1].__test_connection_url, TEST_URLS[0], "Second tab has correct url"); + + // Trigger a mouse enter on third tab. + tabs[2].dispatchEvent(e); + is(tabs[2].__test_connection_prepared, true, "Third tab has a connection prepared"); + is(tabs[2].__test_connection_url, TEST_URLS[1], "Third tab has correct url"); + + // Last tab is the previously selected tab. + tabs[3].dispatchEvent(e); + is(tabs[3].__test_connection_prepared, undefined, "Previous selected tab should not have a connection prepared"); + is(tabs[3].__test_connection_url, undefined, "Previous selected tab should not have a connection prepared"); + + await BrowserTestUtils.closeWindow(newWin); +}); + +add_task(async function speculative_connect_restore_automatically() { + Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false); + is(Services.prefs.getBoolPref("browser.sessionstore.restore_on_demand"), false, "We're restoring automatically"); + forgetClosedWindows(); + + // Open a new window and populate with tabs. + let win = await promiseNewWindowLoaded(); + await openTabs(win); + + // Close the window. + await BrowserTestUtils.closeWindow(win); + + // Reopen a window. + let newWin = undoCloseWindow(0); + // Make sure we wait until this window is restored. + await BrowserTestUtils.waitForEvent(newWin, "load"); + await BrowserTestUtils.waitForEvent(newWin.gBrowser.tabContainer, "SSTabRestored"); + + let tabs = newWin.gBrowser.tabs; + is(tabs.length, TEST_URLS.length + 1, "Restored right number of tabs"); + + // First tab is ignore, since it's the default tab open when we open new window + + // Second tab. + is(tabs[1].__test_connection_prepared, false, "Second tab doesn't have a connection prepared"); + is(tabs[1].__test_connection_url, TEST_URLS[0], "Second tab has correct host url"); + + // Third tab. + is(tabs[2].__test_connection_prepared, true, "Third tab has a connection prepared"); + is(tabs[2].__test_connection_url, TEST_URLS[1], "Third tab has correct host url"); + + // Last tab is the previously selected tab. + is(tabs[3].__test_connection_prepared, undefined, "Selected tab should not have a connection prepared"); + is(tabs[3].__test_connection_url, undefined, "Selected tab should not have a connection prepared"); + + await BrowserTestUtils.closeWindow(newWin); +}); diff --git a/browser/components/uitour/UITour.jsm b/browser/components/uitour/UITour.jsm index d55df0de5edf..e26b8b66e606 100644 --- a/browser/components/uitour/UITour.jsm +++ b/browser/components/uitour/UITour.jsm @@ -1005,7 +1005,8 @@ this.UITour = { * Called before opening or after closing a highlight or info panel to see if * we need to open or close the appMenu to see the annotation's anchor. */ - _setAppMenuStateForAnnotation(aWindow, aAnnotationType, aShouldOpenForHighlight, aCallback = null) { + _setAppMenuStateForAnnotation(aWindow, aAnnotationType, aShouldOpenForHighlight, aTarget = null, + aCallback = null) { log.debug("_setAppMenuStateForAnnotation:", aAnnotationType); log.debug("_setAppMenuStateForAnnotation: Menu is expected to be:", aShouldOpenForHighlight ? "open" : "closed"); @@ -1035,7 +1036,16 @@ this.UITour = { // Actually show or hide the menu if (this.appMenuOpenForAnnotation.size) { log.debug("_setAppMenuStateForAnnotation: Opening the menu"); - this.showMenu(aWindow, "appMenu", aCallback); + this.showMenu(aWindow, "appMenu", async () => { + // PanelMultiView's like the AppMenu might shuffle the DOM, which might result + // in our target being invalidated if it was anonymous content (since the XBL + // binding it belonged to got destroyed). We work around this by re-querying for + // the node and stuffing it into the old target structure. + log.debug("_setAppMenuStateForAnnotation: Refreshing target"); + let refreshedTarget = await this.getTarget(aWindow, aTarget.targetName); + aTarget.node = refreshedTarget.node; + aCallback(); + }); } else { log.debug("_setAppMenuStateForAnnotation: Closing the menu"); this.hideMenu(aWindow, "appMenu"); @@ -1152,6 +1162,7 @@ this.UITour = { this._setAppMenuStateForAnnotation(aChromeWindow, "highlight", this.targetIsInAppMenu(aTarget), + aTarget, showHighlightPanel.bind(this)); }, @@ -1281,9 +1292,17 @@ this.UITour = { return; } + // We need to bind the anchor argument to the showInfoPanel function call + // after _setAppMenuStateForAnnotation has finished, since + // _setAppMenuStateForAnnotation might have refreshed the anchor node. + let callShowInfoPanel = () => { + showInfoPanel.call(this, this._correctAnchor(aAnchor.node)); + }; + this._setAppMenuStateForAnnotation(aChromeWindow, "info", this.targetIsInAppMenu(aAnchor), - showInfoPanel.bind(this, this._correctAnchor(aAnchor.node))); + aAnchor, + callShowInfoPanel); }, isInfoOnTarget(aChromeWindow, aTargetName) { diff --git a/browser/components/uitour/test/browser_UITour2.js b/browser/components/uitour/test/browser_UITour2.js index 4915708430ba..eff0a8f6efa5 100644 --- a/browser/components/uitour/test/browser_UITour2.js +++ b/browser/components/uitour/test/browser_UITour2.js @@ -15,22 +15,26 @@ var tests = [ function test_info_customize_auto_open_close(done) { let popup = document.getElementById("UITourTooltip"); gContentAPI.showInfo("customize", "Customization", "Customize me please!"); - UITour.getTarget(window, "customize").then((customizeTarget) => { - waitForPopupAtAnchor(popup, customizeTarget.node, function checkPanelIsOpen() { - isnot(PanelUI.panel.state, "closed", "Panel should have opened before the popup anchored"); - ok(PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been set"); - // Move the info outside which should close the app menu. - gContentAPI.showInfo("appMenu", "Open Me", "You know you want to"); - UITour.getTarget(window, "appMenu").then((target) => { - waitForPopupAtAnchor(popup, target.node, function checkPanelIsClosed() { - isnot(PanelUI.panel.state, "open", - "Panel should have closed after the info moved elsewhere."); - ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up on close"); - done(); - }, "Info should move to the appMenu button"); - }); - }, "Info panel should be anchored to the customize button"); + let shownPromise = promisePanelShown(window); + shownPromise.then(() => { + UITour.getTarget(window, "customize").then((customizeTarget) => { + waitForPopupAtAnchor(popup, customizeTarget.node, function checkPanelIsOpen() { + isnot(PanelUI.panel.state, "closed", "Panel should have opened before the popup anchored"); + ok(PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been set"); + + // Move the info outside which should close the app menu. + gContentAPI.showInfo("appMenu", "Open Me", "You know you want to"); + UITour.getTarget(window, "appMenu").then((target) => { + waitForPopupAtAnchor(popup, target.node, function checkPanelIsClosed() { + isnot(PanelUI.panel.state, "open", + "Panel should have closed after the info moved elsewhere."); + ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up on close"); + done(); + }, "Info should move to the appMenu button"); + }); + }, "Info panel should be anchored to the customize button"); + }); }); }, function test_info_customize_manual_open_close(done) { diff --git a/browser/extensions/e10srollout/bootstrap.js b/browser/extensions/e10srollout/bootstrap.js index d150de77290f..f64f2daa0a5d 100644 --- a/browser/extensions/e10srollout/bootstrap.js +++ b/browser/extensions/e10srollout/bootstrap.js @@ -24,17 +24,19 @@ const TEST_THRESHOLD = { const MULTI_EXPERIMENT = { "beta": { buckets: { 1: .5, 4: 1, }, // 1 process: 50%, 4 processes: 50% - // See below for an explanation, this only allows webextensions. - get addonsDisableExperiment() { return getAddonsDisqualifyForMulti(); } }, + // When on the "beta" channel, getAddonsDisqualifyForMulti + // will return true if any addon installed is not a web extension. + // Therefore, this returns true if and only if all addons + // installed are web extensions or if no addons are installed + // at all. + addonsDisableExperiment(prefix) { return getAddonsDisqualifyForMulti(); } }, - "release": { buckets: { 1: .2, 4: 1 }, // 1 process: 20%, 4 processes: 80% + "release": { buckets: { 1: .99, 4: 1 }, // 1 process: 99%, 4 processes: 1% - // When on the "release" channel, getAddonsDisqualifyForMulti - // will return true if any addon installed is not a web extension. - // Therefore, this returns true if and only if all addons - // installed are web extensions or if no addons are installed - // at all. - get addonsDisableExperiment() { return getAddonsDisqualifyForMulti(); } } + // We don't want to allow users with any extension + // (webextension or otherwise in the experiment). prefix will + // be non-empty if there is any addon. + addonsDisableExperiment(prefix) { return !!prefix; } } }; const ADDON_ROLLOUT_POLICY = { @@ -183,7 +185,7 @@ function defineCohort() { // the default number of content processes (1 on beta) but still in the // test cohort. if (!(updateChannel in MULTI_EXPERIMENT) || - MULTI_EXPERIMENT[updateChannel].addonsDisableExperiment || + MULTI_EXPERIMENT[updateChannel].addonsDisableExperiment(cohortPrefix) || !eligibleForMulti || userOptedIn.multi || disqualified) { diff --git a/browser/extensions/e10srollout/install.rdf.in b/browser/extensions/e10srollout/install.rdf.in index 2cb35ec56e07..455a5b797977 100644 --- a/browser/extensions/e10srollout/install.rdf.in +++ b/browser/extensions/e10srollout/install.rdf.in @@ -10,7 +10,7 @@ e10srollout@mozilla.org - 1.80 + 1.85 2 true true diff --git a/browser/extensions/formautofill/FormAutofillContent.jsm b/browser/extensions/formautofill/FormAutofillContent.jsm index 448e79701582..0aa52b7bd810 100644 --- a/browser/extensions/formautofill/FormAutofillContent.jsm +++ b/browser/extensions/formautofill/FormAutofillContent.jsm @@ -105,21 +105,24 @@ AutofillProfileAutoCompleteSearch.prototype = { return; } - this._getAddresses({info, searchString}).then((addresses) => { + let collectionName = FormAutofillUtils.isAddressField(info.fieldName) ? + "addresses" : "creditCards"; + + this._getRecords({collectionName, info, searchString}).then((records) => { if (this.forceStop) { return; } // Sort addresses by timeLastUsed for showing the lastest used address at top. - addresses.sort((a, b) => b.timeLastUsed - a.timeLastUsed); + records.sort((a, b) => b.timeLastUsed - a.timeLastUsed); let handler = FormAutofillContent.getFormHandler(focusedInput); - let adaptedAddresses = handler.getAdaptedProfiles(addresses); + let adaptedRecords = handler.getAdaptedProfiles(records); let allFieldNames = FormAutofillContent.getAllFieldNames(focusedInput); let result = new ProfileAutoCompleteResult(searchString, info.fieldName, allFieldNames, - adaptedAddresses, + adaptedRecords, {}); listener.onSearchResult(this, result); @@ -136,27 +139,29 @@ AutofillProfileAutoCompleteSearch.prototype = { }, /** - * Get the address data from parent process for AutoComplete result. + * Get the records from parent process for AutoComplete result. * * @private * @param {Object} data * Parameters for querying the corresponding result. + * @param {string} data.collectionName + * The name used to specify which collection to retrieve records. * @param {string} data.searchString - * The typed string for filtering out the matched address. + * The typed string for filtering out the matched records. * @param {string} data.info * The input autocomplete property's information. * @returns {Promise} * Promise that resolves when addresses returned from parent process. */ - _getAddresses(data) { - this.log.debug("_getAddresses with data:", data); + _getRecords(data) { + this.log.debug("_getRecords with data:", data); return new Promise((resolve) => { - Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) { - Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult); + Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) { + Services.cpmm.removeMessageListener("FormAutofill:Records", getResult); resolve(result.data); }); - Services.cpmm.sendAsyncMessage("FormAutofill:GetAddresses", data); + Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", data); }); }, }; diff --git a/browser/extensions/formautofill/FormAutofillParent.jsm b/browser/extensions/formautofill/FormAutofillParent.jsm index 535dc74c6d88..d63b37cdb3e5 100644 --- a/browser/extensions/formautofill/FormAutofillParent.jsm +++ b/browser/extensions/formautofill/FormAutofillParent.jsm @@ -80,7 +80,7 @@ FormAutofillParent.prototype = { async init() { Services.obs.addObserver(this, "advanced-pane-loaded"); Services.ppmm.addMessageListener("FormAutofill:InitStorage", this); - Services.ppmm.addMessageListener("FormAutofill:GetAddresses", this); + Services.ppmm.addMessageListener("FormAutofill:GetRecords", this); Services.ppmm.addMessageListener("FormAutofill:SaveAddress", this); Services.ppmm.addMessageListener("FormAutofill:RemoveAddresses", this); Services.ppmm.addMessageListener("FormAutofill:OpenPreferences", this); @@ -181,8 +181,8 @@ FormAutofillParent.prototype = { this.profileStorage.initialize(); break; } - case "FormAutofill:GetAddresses": { - this._getAddresses(data, target); + case "FormAutofill:GetRecords": { + this._getRecords(data, target); break; } case "FormAutofill:SaveAddress": { @@ -217,7 +217,7 @@ FormAutofillParent.prototype = { this.profileStorage._saveImmediately(); Services.ppmm.removeMessageListener("FormAutofill:InitStorage", this); - Services.ppmm.removeMessageListener("FormAutofill:GetAddresses", this); + Services.ppmm.removeMessageListener("FormAutofill:GetRecords", this); Services.ppmm.removeMessageListener("FormAutofill:SaveAddress", this); Services.ppmm.removeMessageListener("FormAutofill:RemoveAddresses", this); Services.obs.removeObserver(this, "advanced-pane-loaded"); @@ -225,27 +225,32 @@ FormAutofillParent.prototype = { }, /** - * Get the address data from profile store and return addresses back to content + * Get the records from profile store and return results back to content * process. * * @private + * @param {string} data.collectionName + * The name used to specify which collection to retrieve records. * @param {string} data.searchString - * The typed string for filtering out the matched address. + * The typed string for filtering out the matched records. * @param {string} data.info * The input autocomplete property's information. * @param {nsIFrameMessageManager} target * Content's message manager. */ - _getAddresses({searchString, info}, target) { - let addresses = []; + _getRecords({collectionName, searchString, info}, target) { + let records; + let collection = this.profileStorage[collectionName]; - if (info && info.fieldName) { - addresses = this.profileStorage.addresses.getByFilter({searchString, info}); + if (!collection) { + records = []; + } else if (info && info.fieldName) { + records = collection.getByFilter({searchString, info}); } else { - addresses = this.profileStorage.addresses.getAll(); + records = collection.getAll(); } - target.sendAsyncMessage("FormAutofill:Addresses", addresses); + target.sendAsyncMessage("FormAutofill:Records", records); }, _updateSavedFieldNames() { @@ -256,12 +261,14 @@ FormAutofillParent.prototype = { Services.ppmm.initialProcessData.autofillSavedFieldNames.clear(); } - this.profileStorage.addresses.getAll().forEach((address) => { - Object.keys(address).forEach((fieldName) => { - if (!address[fieldName]) { - return; - } - Services.ppmm.initialProcessData.autofillSavedFieldNames.add(fieldName); + ["addresses", "creditCards"].forEach(c => { + this.profileStorage[c].getAll().forEach((record) => { + Object.keys(record).forEach((fieldName) => { + if (!record[fieldName]) { + return; + } + Services.ppmm.initialProcessData.autofillSavedFieldNames.add(fieldName); + }); }); }); @@ -299,9 +306,13 @@ FormAutofillParent.prototype = { } changedGUIDs.forEach(guid => this.profileStorage.addresses.notifyUsed(guid)); }); + // Address should be updated + Services.telemetry.scalarAdd("formautofill.addresses.fill_type_autofill_update", 1); return; } this.profileStorage.addresses.notifyUsed(address.guid); + // Address is merged successfully + Services.telemetry.scalarAdd("formautofill.addresses.fill_type_autofill", 1); } else { let changedGUIDs = this.profileStorage.addresses.mergeToStorage(address.record); if (!changedGUIDs.length) { @@ -320,6 +331,9 @@ FormAutofillParent.prototype = { target.ownerGlobal.openPreferences("panePrivacy", {origin: "autofillDoorhanger"}); }); + } else { + // We want to exclude the first time form filling. + Services.telemetry.scalarAdd("formautofill.addresses.fill_type_manual", 1); } } }, diff --git a/browser/extensions/formautofill/content/manageProfiles.js b/browser/extensions/formautofill/content/manageProfiles.js index 53f9dfa3634c..40bb65a9f2dd 100644 --- a/browser/extensions/formautofill/content/manageProfiles.js +++ b/browser/extensions/formautofill/content/manageProfiles.js @@ -63,7 +63,7 @@ ManageProfileDialog.prototype = { * @returns {promise} */ loadAddresses() { - return this.getAddresses().then(addresses => { + return this.getRecords({collectionName: "addresses"}).then(addresses => { log.debug("addresses:", addresses); // Sort by last modified time starting with most recent addresses.sort((a, b) => b.timeLastModified - a.timeLastModified); @@ -73,17 +73,27 @@ ManageProfileDialog.prototype = { }, /** - * Get addresses from storage. + * Get records from storage. * - * @returns {promise} + * @private + * @param {Object} data + * Parameters for querying the corresponding result. + * @param {string} data.collectionName + * The name used to specify which collection to retrieve records. + * @param {string} data.searchString + * The typed string for filtering out the matched records. + * @param {string} data.info + * The input autocomplete property's information. + * @returns {Promise} + * Promise that resolves when addresses returned from parent process. */ - getAddresses() { + getRecords(data) { return new Promise(resolve => { - Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) { - Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult); + Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) { + Services.cpmm.removeMessageListener("FormAutofill:Records", getResult); resolve(result.data); }); - Services.cpmm.sendAsyncMessage("FormAutofill:GetAddresses", {}); + Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", data); }); }, diff --git a/browser/extensions/formautofill/test/browser/browser_manageProfilesDialog.js b/browser/extensions/formautofill/test/browser/browser_manageProfilesDialog.js index 7f3265fb9961..3b79825b2c5f 100644 --- a/browser/extensions/formautofill/test/browser/browser_manageProfilesDialog.js +++ b/browser/extensions/formautofill/test/browser/browser_manageProfilesDialog.js @@ -9,10 +9,10 @@ const TEST_SELECTORS = { const DIALOG_SIZE = "width=600,height=400"; -function waitForAddresses() { +function waitForRecords() { return new Promise(resolve => { - Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) { - Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult); + Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) { + Services.cpmm.removeMessageListener("FormAutofill:Records", getResult); // Wait for the next tick for elements to get rendered. SimpleTest.executeSoon(resolve.bind(null, result.data)); }); @@ -54,7 +54,7 @@ add_task(async function test_removingSingleAndMultipleProfiles() { await saveAddress(TEST_ADDRESS_3); let win = window.openDialog(MANAGE_PROFILES_DIALOG_URL, null, DIALOG_SIZE); - await waitForAddresses(); + await waitForRecords(); let selAddresses = win.document.querySelector(TEST_SELECTORS.selAddresses); let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove); @@ -66,7 +66,7 @@ add_task(async function test_removingSingleAndMultipleProfiles() { is(btnRemove.disabled, false, "Remove button enabled"); is(btnEdit.disabled, false, "Edit button enabled"); EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win); - await waitForAddresses(); + await waitForRecords(); is(selAddresses.length, 2, "Two addresses left"); EventUtils.synthesizeMouseAtCenter(selAddresses.children[0], {}, win); @@ -75,7 +75,7 @@ add_task(async function test_removingSingleAndMultipleProfiles() { is(btnEdit.disabled, true, "Edit button disabled when multi-select"); EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win); - await waitForAddresses(); + await waitForRecords(); is(selAddresses.length, 0, "All addresses are removed"); win.close(); @@ -83,16 +83,16 @@ add_task(async function test_removingSingleAndMultipleProfiles() { add_task(async function test_profilesDialogWatchesStorageChanges() { let win = window.openDialog(MANAGE_PROFILES_DIALOG_URL, null, DIALOG_SIZE); - await waitForAddresses(); + await waitForRecords(); let selAddresses = win.document.querySelector(TEST_SELECTORS.selAddresses); await saveAddress(TEST_ADDRESS_1); - let addresses = await waitForAddresses(); + let addresses = await waitForRecords(); is(selAddresses.length, 1, "One address is shown"); await removeAddresses([addresses[0].guid]); - await waitForAddresses(); + await waitForRecords(); is(selAddresses.length, 0, "Address is removed"); win.close(); }); diff --git a/browser/extensions/formautofill/test/browser/head.js b/browser/extensions/formautofill/test/browser/head.js index 108f7dfc78a0..f850499a9ab5 100644 --- a/browser/extensions/formautofill/test/browser/head.js +++ b/browser/extensions/formautofill/test/browser/head.js @@ -64,16 +64,20 @@ async function openPopupOn(browser, selector) { await expectPopupOpen(browser); } -function getAddresses() { +function getRecords(data) { return new Promise(resolve => { - Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) { - Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult); + Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) { + Services.cpmm.removeMessageListener("FormAutofill:Records", getResult); resolve(result.data); }); - Services.cpmm.sendAsyncMessage("FormAutofill:GetAddresses", {}); + Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", data); }); } +function getAddresses() { + return getRecords({collectionName: "addresses"}); +} + function saveAddress(address) { Services.cpmm.sendAsyncMessage("FormAutofill:SaveAddress", {address}); return TestUtils.topicObserved("formautofill-storage-changed"); diff --git a/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js index 302c7a0c18f1..1093b0c5069c 100644 --- a/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js +++ b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js @@ -11,15 +11,15 @@ let {profileStorage} = Cu.import("resource://formautofill/ProfileStorage.jsm", { var ParentUtils = { cleanUpAddress() { - Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) { - Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult); + Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) { + Services.cpmm.removeMessageListener("FormAutofill:Records", getResult); let addresses = result.data; Services.cpmm.sendAsyncMessage("FormAutofill:RemoveAddresses", {guids: addresses.map(address => address.guid)}); }); - Services.cpmm.sendAsyncMessage("FormAutofill:GetAddresses", {searchString: ""}); + Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", {searchString: "", collectionName: "addresses"}); }, updateAddress(type, chromeMsg, msgData, contentMsg) { @@ -60,8 +60,8 @@ var ParentUtils = { }, checkAddresses({expectedAddresses}) { - Services.cpmm.addMessageListener("FormAutofill:Addresses", function getResult(result) { - Services.cpmm.removeMessageListener("FormAutofill:Addresses", getResult); + Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) { + Services.cpmm.removeMessageListener("FormAutofill:Records", getResult); let addresses = result.data; if (addresses.length !== expectedAddresses.length) { sendAsyncMessage("FormAutofillTest:areAddressesMatching", false); @@ -82,7 +82,7 @@ var ParentUtils = { sendAsyncMessage("FormAutofillTest:areAddressesMatching", true); }); - Services.cpmm.sendAsyncMessage("FormAutofill:GetAddresses", {searchString: ""}); + Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", {searchString: "", collectionName: "addresses"}); }, }; diff --git a/browser/extensions/formautofill/test/unit/test_getRecords.js b/browser/extensions/formautofill/test/unit/test_getRecords.js new file mode 100644 index 000000000000..c2912012dcd6 --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_getRecords.js @@ -0,0 +1,50 @@ +/* + * Test for make sure getRecords can retrieve right collection from storag. + */ + +"use strict"; + +Cu.import("resource://formautofill/FormAutofillParent.jsm"); +Cu.import("resource://formautofill/ProfileStorage.jsm"); + +add_task(async function test_getRecords() { + let formAutofillParent = new FormAutofillParent(); + + await formAutofillParent.init(); + await formAutofillParent.profileStorage.initialize(); + + let fakeResult = { + addresses: [{ + "given-name": "Timothy", + "additional-name": "John", + "family-name": "Berners-Lee", + "organization": "World Wide Web Consortium", + }], + creditCards: [{ + "cc-name": "John Doe", + "cc-number": "1234567812345678", + "cc-exp-month": 4, + "cc-exp-year": 2017, + }], + }; + + ["addresses", "creditCards", "nonExisting"].forEach(collectionName => { + let collection = profileStorage[collectionName]; + let expectedResult = fakeResult[collectionName] || []; + let target = { + sendAsyncMessage: function sendAsyncMessage(msg, payload) {}, + }; + let mock = sinon.mock(target); + mock.expects("sendAsyncMessage").once().withExactArgs("FormAutofill:Records", expectedResult); + + if (collection) { + sinon.stub(collection, "getAll"); + collection.getAll.returns(expectedResult); + } + formAutofillParent._getRecords({collectionName}, target); + mock.verify(); + if (collection) { + do_check_eq(collection.getAll.called, true); + } + }); +}); diff --git a/browser/extensions/formautofill/test/unit/xpcshell.ini b/browser/extensions/formautofill/test/unit/xpcshell.ini index 7a44489d054e..f8e82f0ab558 100644 --- a/browser/extensions/formautofill/test/unit/xpcshell.ini +++ b/browser/extensions/formautofill/test/unit/xpcshell.ini @@ -27,6 +27,7 @@ support-files = [test_getCategoriesFromFieldNames.js] [test_getFormInputDetails.js] [test_getInfo.js] +[test_getRecords.js] [test_isCJKName.js] [test_isFieldEligibleForAutofill.js] [test_markAsAutofillField.js] diff --git a/browser/extensions/onboarding/bootstrap.js b/browser/extensions/onboarding/bootstrap.js index fdcf26ff5304..cabf6b855848 100644 --- a/browser/extensions/onboarding/bootstrap.js +++ b/browser/extensions/onboarding/bootstrap.js @@ -13,10 +13,11 @@ XPCOMUtils.defineLazyModuleGetter(this, "Preferences", "resource://gre/modules/Preferences.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", - "resource://gre/modules/Timer.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", + "resource://gre/modules/FxAccounts.jsm"); -const BROWSER_READY_NOTIFICATION = "final-ui-startup"; +const BROWSER_READY_NOTIFICATION = "browser-delayed-startup-finished"; +const BROWSER_SESSION_STORE_NOTIFICATION = "sessionstore-windows-restored"; const PREF_WHITELIST = [ "browser.onboarding.enabled", "browser.onboarding.hidden", @@ -27,12 +28,14 @@ const PREF_WHITELIST = [ ]; [ - "onboarding-tour-private-browsing", "onboarding-tour-addons", "onboarding-tour-customize", "onboarding-tour-default-browser", "onboarding-tour-library", + "onboarding-tour-performance", + "onboarding-tour-private-browsing", "onboarding-tour-search", + "onboarding-tour-singlesearch", "onboarding-tour-sync", ].forEach(tourId => PREF_WHITELIST.push(`browser.onboarding.tour.${tourId}.completed`)); @@ -69,6 +72,54 @@ function initContentMessageListener() { }); } +let syncTourChecker = { + registered: false, + + observe() { + this.setComplete(); + }, + + init() { + if (Services.prefs.getBoolPref("browser.onboarding.tour.onboarding-tour-sync.completed", false)) { + return; + } + // Check if we've already logged in at startup. + fxAccounts.getSignedInUser().then(user => { + if (user) { + this.setComplete(); + return; + } + // Observe for login action if we haven't logged in yet. + this.register(); + }); + }, + + register() { + if (this.registered) { + return; + } + Services.obs.addObserver(this, "fxaccounts:onverified"); + this.registered = true; + }, + + setComplete() { + Services.prefs.setBoolPref("browser.onboarding.tour.onboarding-tour-sync.completed", true); + this.unregister(); + }, + + unregister() { + if (!this.registered) { + return; + } + Services.obs.removeObserver(this, "fxaccounts:onverified"); + this.registered = false; + }, + + uninit() { + this.unregister(); + }, +} + /** * onBrowserReady - Continues startup of the add-on after browser is ready. */ @@ -87,8 +138,15 @@ function observe(subject, topic, data) { switch (topic) { case BROWSER_READY_NOTIFICATION: Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION); - // Avoid running synchronously during this event that's used for timing - setTimeout(() => onBrowserReady()); + onBrowserReady(); + break; + case BROWSER_SESSION_STORE_NOTIFICATION: + Services.obs.removeObserver(observe, BROWSER_SESSION_STORE_NOTIFICATION); + // Postpone Firefox account checking until "before handling user events" + // phase to meet performance criteria. The reason we don't postpone the + // whole onBrowserReady here is because in that way we will miss onload + // events for onboarding.js. + Services.tm.idleDispatchToMainThread(() => syncTourChecker.init()); break; } } @@ -101,8 +159,10 @@ function startup(aData, aReason) { // Only start Onboarding when the browser UI is ready if (aReason === APP_STARTUP || aReason === ADDON_INSTALL) { Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION); + Services.obs.addObserver(observe, BROWSER_SESSION_STORE_NOTIFICATION); } else { onBrowserReady(); + syncTourChecker.init(); } } @@ -111,4 +171,5 @@ function shutdown(aData, aReason) { if (waitingForBrowserReady) { Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION); } + syncTourChecker.uninit(); } diff --git a/browser/extensions/onboarding/content/img/icons_performance-colored.svg b/browser/extensions/onboarding/content/img/icons_performance-colored.svg new file mode 100644 index 000000000000..68902a3ba2c2 --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_performance-colored.svg @@ -0,0 +1 @@ +Tip / Icon / Performance \ No newline at end of file diff --git a/browser/extensions/onboarding/content/img/icons_performance.svg b/browser/extensions/onboarding/content/img/icons_performance.svg new file mode 100644 index 000000000000..f4f4c6839d5d --- /dev/null +++ b/browser/extensions/onboarding/content/img/icons_performance.svg @@ -0,0 +1 @@ +Tip / Icon / Performance \ No newline at end of file diff --git a/browser/extensions/onboarding/content/onboarding-tour-agent.js b/browser/extensions/onboarding/content/onboarding-tour-agent.js index d396d81b846c..6046b3ef4242 100644 --- a/browser/extensions/onboarding/content/onboarding-tour-agent.js +++ b/browser/extensions/onboarding/content/onboarding-tour-agent.js @@ -38,6 +38,9 @@ document.getElementById("onboarding-overlay") case "onboarding-tour-search-button": Mozilla.UITour.openSearchPanel(() => {}); break; + case "onboarding-tour-singlesearch-button": + Mozilla.UITour.showMenu("urlbar"); + break; case "onboarding-tour-sync-button": let emailInput = document.getElementById("onboarding-tour-sync-email-input"); if (emailInput.checkValidity()) { diff --git a/browser/extensions/onboarding/content/onboarding.css b/browser/extensions/onboarding/content/onboarding.css index 13144654223a..89770cd67d3f 100644 --- a/browser/extensions/onboarding/content/onboarding.css +++ b/browser/extensions/onboarding/content/onboarding.css @@ -23,30 +23,35 @@ display: block; } -#onboarding-overlay-icon { - width: 36px; - height: 29px; +#onboarding-overlay-button { position: absolute; cursor: pointer; top: 30px; offset-inline-start: 30px; - background: url("img/overlay-icon.svg") no-repeat; + border: none; + /* Set to none so no grey contrast background in the high-contrast mode */ + background: none; +} + +#onboarding-overlay-button-icon { + width: 36px; } #onboarding-notification-icon::after, -#onboarding-overlay-icon::after { +#onboarding-overlay-button::after { background: #5ce6e6; position: absolute; font-size: 12px; border: 1px solid #fff; text-align: center; color: #10404a; + box-sizing: content-box; } -#onboarding-overlay-icon::after { +#onboarding-overlay-button::after { content: attr(aria-label); top: -6px; - offset-inline-start: 32px; + offset-inline-start: 39px; border-radius: 22px; padding: 5px 8px; min-width: 100px; @@ -57,29 +62,35 @@ display: none; } -#onboarding-overlay-close-btn, -#onboarding-notification-close-btn { - position: absolute; - top: 15px; - offset-inline-end: 15px; - cursor: pointer; - width: 16px; - height: 16px; - background-image: url(chrome://browser/skin/sidebar/close.svg); - background-position: center center; - background-repeat: no-repeat; - padding: 12px; +.onboarding-close-btn { + position: absolute; + top: 15px; + offset-inline-end: 15px; + cursor: pointer; + width: 16px; + height: 16px; + padding: 12px; + border: none; + background: var(--onboarding-overlay-dialog-background-color); + } + +.onboarding-close-btn::before { + content: url(chrome://browser/skin/sidebar/close.svg); + display: block; + margin-top: -8px; + margin-inline-start: -8px; } -#onboarding-overlay-close-btn:hover, +.onboarding-close-btn:hover, #onboarding-notification-close-btn:hover { background-color: rgba(204, 204, 204, 0.6); } #onboarding-overlay.onboarding-opened > #onboarding-overlay-dialog { + --onboarding-overlay-dialog-background-color: #f5f5f7; width: 960px; height: 510px; - background: #f5f5f7; + background: var(--onboarding-overlay-dialog-background-color); border: 1px solid rgba(9, 6, 13, 0.1); /* #09060D, 0.1 opacity */ border-radius: 3px; position: relative; @@ -135,10 +146,13 @@ } #onboarding-tour-list > li { + --padding-inline-start: 49px; + --padding-top: 14px; + --padding-bottom: 14px; list-style: none; - padding-inline-start: 49px; - padding-top: 14px; - padding-bottom: 14px; + padding-inline-start: var(--padding-inline-start); + padding-top: var(--padding-top); + padding-bottom: var(--padding-bottom); margin-inline-start: 16px; margin-bottom: 9px; background-repeat: no-repeat; @@ -161,12 +175,19 @@ } #onboarding-tour-list > li.onboarding-complete { - padding-inline-start: 29px; + --padding-inline-start: 29px; } #onboarding-tour-list > li.onboarding-active, #onboarding-tour-list > li:hover { color: #0A84FF; + /* With 1px transparent border, could see a border in the high-constrast mode */ + border: 1px solid transparent; + /* Substract 1px for the 1px transparent or a 1px shift would happen */ + padding-inline-start: calc(var(--padding-inline-start) - 1px); + padding-top: calc(var(--padding-top) - 1px); + padding-bottom: calc(var(--padding-bottom) - 1px); + background-color: #fff; } /* Default browser tour */ @@ -283,7 +304,8 @@ font-weight: 600; line-height: 21px; background: #0a84ff; - border: none; + /* With 1px transparent border, could see a border in the high-constrast mode */ + border: 1px solid transparent; border-radius: 0; color: #fff; float: inline-end; @@ -307,16 +329,20 @@ } /* Tour Icons */ -#onboarding-tour-search { +#onboarding-tour-search, +#onboarding-tour-singlesearch { background-image: url("img/icons_search.svg"); } #onboarding-tour-search.onboarding-active, -#onboarding-tour-search:hover { +#onboarding-tour-search:hover, +#onboarding-tour-singlesearch.onboarding-active, +#onboarding-tour-singlesearch:hover { background-image: url("img/icons_search-colored.svg"); } -#onboarding-notification-bar[data-target-tour-id=onboarding-tour-search] #onboarding-notification-tour-icon { +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-search] #onboarding-notification-tour-icon, +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-singlesearch] #onboarding-notification-tour-icon { background-image: url("img/icons_search-notification.svg"); } @@ -396,8 +422,24 @@ background-image: url("img/icons_search-colored.svg"); } +#onboarding-tour-performance { + background-image: url("img/icons_performance.svg"); +} + +#onboarding-tour-performance.onboarding-active, +#onboarding-tour-performance:hover { + background-image: url("img/icons_performance-colored.svg"); +} + +#onboarding-notification-bar[data-target-tour-id=onboarding-tour-performance] #onboarding-notification-tour-icon { + /* TODO: Placeholder icon. It should be replaced upon assets are available. + This is tracking in Bug 1382520. */ + background-image: url("img/icons_sync-notification.svg"); +} + /* Tour Notifications */ #onboarding-notification-bar { + --onboarding-notification-bar-background-color: rgba(255, 255, 255, 0.97); position: fixed; z-index: 20998; /* We want this always under #onboarding-overlay */ left: 0; @@ -405,7 +447,7 @@ width: 100%; height: 122px; min-width: 640px; - background: rgba(255, 255, 255, 0.97); + background: var(--onboarding-notification-bar-background-color); border-top: 2px solid #e9e9e9; transition: transform 0.8s; transform: translateY(122px); @@ -436,15 +478,14 @@ --vpadding: 3px; content: attr(data-tooltip); top: 0; - offset-inline-start: 68px; + offset-inline-start: 73px; line-height: calc(var(--height) - var(--vpadding) * 2); border-radius: calc(var(--height) / 2); padding: var(--vpadding) 10px; } #onboarding-notification-close-btn { - background-color: rgba(255, 255, 255, 0.97); - border: none; + background: var(--onboarding-notification-bar-background-color); position: absolute; offset-block-start: 50%; offset-inline-end: 34px; @@ -489,7 +530,8 @@ #onboarding-notification-action-btn { background: #0a84ff; - border: none; + /* With 1px transparent border, could see a border in the high-constrast mode */ + border: 1px solid transparent; border-radius: 0; padding: 10px 20px; font-size: 14px; diff --git a/browser/extensions/onboarding/content/onboarding.js b/browser/extensions/onboarding/content/onboarding.js index d07d8f326055..862d853c618b 100644 --- a/browser/extensions/onboarding/content/onboarding.js +++ b/browser/extensions/onboarding/content/onboarding.js @@ -250,6 +250,59 @@ var onboardingTourset = { return div; }, }, + "singlesearch": { + id: "onboarding-tour-singlesearch", + tourNameId: "onboarding.tour-singlesearch", + getNotificationStrings(bundle) { + return { + title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-singlesearch.title"), + message: bundle.GetStringFromName("onboarding.notification.onboarding-tour-singlesearch.message"), + button: bundle.GetStringFromName("onboarding.button.learnMore"), + }; + }, + getPage(win, bundle) { + let div = win.document.createElement("div"); + div.innerHTML = ` +
+

+

+
+
+ +
+ + `; + return div; + }, + }, + "performance": { + id: "onboarding-tour-performance", + tourNameId: "onboarding.tour-performance", + getNotificationStrings(bundle) { + return { + title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-performance.title"), + message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-performance.message", [BRAND_SHORT_NAME], 1), + button: bundle.GetStringFromName("onboarding.button.learnMore"), + }; + }, + getPage(win, bundle) { + let div = win.document.createElement("div"); + // TODO: The content image is a placeholder. It should be replaced upon assets are available. + // This is tracking in Bug 1382520. + div.innerHTML = ` +
+

+

+
+
+ +
+ `; + return div; + }, + }, }; /** @@ -264,8 +317,9 @@ class Onboarding { async init(contentWindow) { this._window = contentWindow; this._tours = []; + this._tourType = Services.prefs.getStringPref("browser.onboarding.tour-type", "update"); - let tourIds = this._getTourIDList(Services.prefs.getStringPref("browser.onboarding.tour-type", "update")); + let tourIds = this._getTourIDList(); tourIds.forEach(tourId => { if (onboardingTourset[tourId]) { this._tours.push(onboardingTourset[tourId]); @@ -311,7 +365,7 @@ class Onboarding { this._tourItems = []; this._tourPages = []; - this._overlayIcon = this._renderOverlayIcon(); + this._overlayIcon = this._renderOverlayButton(); this._overlayIcon.addEventListener("click", this); this._window.document.body.appendChild(this._overlayIcon); @@ -326,8 +380,8 @@ class Onboarding { this._window.requestIdleCallback(() => this._initNotification()); } - _getTourIDList(tourType) { - let tours = Services.prefs.getStringPref(`browser.onboarding.${tourType}tour`, ""); + _getTourIDList() { + let tours = Services.prefs.getStringPref(`browser.onboarding.${this._tourType}tour`, ""); return tours.split(",").filter(tourId => tourId !== "").map(tourId => tourId.trim()); } @@ -400,7 +454,7 @@ class Onboarding { } switch (evt.target.id) { - case "onboarding-overlay-icon": + case "onboarding-overlay-button": case "onboarding-overlay-close-btn": // If the clicking target is directly on the outer-most overlay, // that means clicking outside the tour content area. @@ -418,6 +472,12 @@ class Onboarding { this.gotoPage(tourId); this._removeTourFromNotificationQueue(tourId); break; + // These tours are tagged completed instantly upon showing. + case "onboarding-tour-default-browser": + case "onboarding-tour-sync": + case "onboarding-tour-performance": + this.setToursCompleted([ evt.target.id ]); + break; } let classList = evt.target.classList; if (classList.contains("onboarding-tour-item")) { @@ -679,9 +739,12 @@ class Onboarding { - + `; - let toolTip = this._bundle.formatStringFromName("onboarding.notification-icon-tool-tip", [BRAND_SHORT_NAME], 1); + let toolTip = this._bundle.formatStringFromName( + this._tourType === "new" ? "onboarding.notification-icon-tool-tip" : + "onboarding.notification-icon-tooltip-updated", + [BRAND_SHORT_NAME], 1); div.querySelector("#onboarding-notification-icon").setAttribute("data-tooltip", toolTip); return div; } @@ -707,7 +770,7 @@ class Onboarding { // The security should be fine because this is not from an external input. div.innerHTML = `
- +