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 @@
+
\ 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 @@
+
\ 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 = `
-
+