Bug 1357641 - Part 1: Add onboarding tour notification, r=flod,mossop

This commit
- adds onboarding tour notification
- shows still not completed onboarding tour notifications in order
- opens target tour from tour notification for the target tour

MozReview-Commit-ID: AwLtwjoeARQ

--HG--
extra : rebase_source : 264531cf8aaf3f636faecf790a269d0166188f8a
This commit is contained in:
Fischer.json 2017-06-21 13:09:29 +08:00
Родитель 84434ba58f
Коммит 085aa9b471
5 изменённых файлов: 321 добавлений и 23 удалений

3
browser/extensions/onboarding/bootstrap.js поставляемый
Просмотреть файл

@ -11,7 +11,8 @@ Cu.import("resource://gre/modules/Preferences.jsm");
const PREF_WHITELIST = [
"browser.onboarding.enabled",
"browser.onboarding.hidden",
"browser.onboarding.notification.finished"
"browser.onboarding.notification.finished",
"browser.onboarding.notification.lastPrompted"
];
/**

Просмотреть файл

@ -37,7 +37,8 @@
display: none;
}
#onboarding-overlay-close-btn {
#onboarding-overlay-close-btn,
#onboarding-notification-close-btn {
position: absolute;
top: 15px;
offset-inline-end: 15px;
@ -50,7 +51,8 @@
padding: 12px;
}
#onboarding-overlay-close-btn:hover {
#onboarding-overlay-close-btn:hover,
#onboarding-notification-close-btn:hover {
background-color: rgba(204, 204, 204, 0.6);
}
@ -236,7 +238,8 @@
}
#onboarding-tour-search.onboarding-active,
#onboarding-tour-search:hover {
#onboarding-tour-search:hover,
#onboarding-notification-bar[data-target-tour-id=onboarding-tour-search] #onboarding-notification-tour-icon {
background-image: url("img/icons_search-colored.svg");
}
@ -245,7 +248,8 @@
}
#onboarding-tour-private-browsing.onboarding-active,
#onboarding-tour-private-browsing:hover {
#onboarding-tour-private-browsing:hover,
#onboarding-notification-bar[data-target-tour-id=onboarding-tour-private-browsing] #onboarding-notification-tour-icon {
background-image: url("img/icons_private-colored.svg");
}
@ -254,7 +258,8 @@
}
#onboarding-tour-addons.onboarding-active,
#onboarding-tour-addons:hover {
#onboarding-tour-addons:hover,
#onboarding-notification-bar[data-target-tour-id=onboarding-tour-addons] #onboarding-notification-tour-icon {
background-image: url("img/icons_addons-colored.svg");
}
@ -263,7 +268,8 @@
}
#onboarding-tour-customize.onboarding-active,
#onboarding-tour-customize:hover {
#onboarding-tour-customize:hover,
#onboarding-notification-bar[data-target-tour-id=onboarding-tour-customize] #onboarding-notification-tour-icon {
background-image: url("img/icons_customize-colored.svg");
}
@ -272,6 +278,105 @@
}
#onboarding-tour-default-browser.onboarding-active,
#onboarding-tour-default-browser:hover {
#onboarding-tour-default-browser:hover,
#onboarding-notification-bar[data-target-tour-id=onboarding-tour-default-browser] #onboarding-notification-tour-icon {
background-image: url("img/icons_default-colored.svg");
}
/* Tour Notifications */
#onboarding-notification-bar {
position: fixed;
z-index: 998; /* We want this always under #onboarding-overlay */
left: 0;
bottom: 0;
width: 100%;
height: 122px;
min-width: 1060px;
background: rgba(255, 255, 255, 0.97);
border-top: 2px solid #e9e9e9;
transition: transform 0.8s;
transform: translateY(122px);
}
#onboarding-notification-bar.onboarding-opened {
transform: translateY(0px);
}
#onboarding-notification-icon {
height: 36px;
background: url("img/overlay-icon.svg") no-repeat;
background-size: 36px;
background-position: 34px;
padding-inline-start: 190px;
position: absolute;
offset-block-start: 50%;
transform: translateY(-50%);
}
#onboarding-notification-icon::after {
--height: 22px;
content: attr(data-tooltip);
background: #5ce6e6;
position: absolute;
top: 0;
offset-inline-start: 68px;
color: #10404a;
font-size: 12px;
min-height: var(--height);
line-height: var(--height);
border-radius: calc(var(--height) / 2);
border: 1px solid #fff;
padding: 0 10px;
text-align: center;
}
#onboarding-notification-close-btn {
background-color: rgba(255, 255, 255, 0.97);
border: none;
position: absolute;
offset-block-start: 50%;
offset-inline-end: 34px;
transform: translateY(-50%);
}
#onboarding-notification-message-section {
height: 100%;
display: flex;
align-items: center;
position: absolute;
offset-block-start: 50%;
offset-inline-start: 50%;
transform: translate(-50%, -50%);
}
#onboarding-notification-body {
width: 420px;
margin: 0 15px;
color: #0c0c0d;;
display: inline-block;
}
#onboarding-notification-body * {
font-size: 13px
}
#onboarding-notification-tour-title {
margin: 0;
}
#onboarding-notification-tour-icon {
width: 64px;
height: 64px;
background-repeat: no-repeat;
}
#onboarding-notification-action-btn {
background: #0d96ff;
border: none;
border-radius: 3px;
padding: 10px 20px;
font-size: 14px;
color: #fff;
box-shadow: 0 1px 0 rgba(0,0,0,0.23);
}

Просмотреть файл

@ -27,6 +27,11 @@ const BRAND_SHORT_NAME = Services.strings
* id: "onboarding-tour-addons",
* // The string id of tour name which would be displayed on the navigation bar
* tourNameId: "onboarding.tour-addon",
* // The method returing strings used on tour notification
* getNotificationStrings(bundle):
* - title: // The string of tour notification title
* - message: // The string of tour notification message
* - button: // The string of tour notification action button title
* // Return a div appended with elements for this tours.
* // Each tour should contain the following 3 sections in the div:
* // .onboarding-tour-description, .onboarding-tour-content, .onboarding-tour-button.
@ -39,6 +44,13 @@ var onboardingTours = [
{
id: "onboarding-tour-private-browsing",
tourNameId: "onboarding.tour-private-browsing",
getNotificationStrings(bundle) {
return {
title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-private-browsing.title"),
message: bundle.GetStringFromName("onboarding.notification.onboarding-tour-private-browsing.message"),
button: bundle.GetStringFromName("onboarding.button.learnMore"),
};
},
getPage(win) {
let div = win.document.createElement("div");
div.innerHTML = `
@ -59,6 +71,13 @@ var onboardingTours = [
{
id: "onboarding-tour-addons",
tourNameId: "onboarding.tour-addons",
getNotificationStrings(bundle) {
return {
title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-addons.title"),
message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-addons.message", [BRAND_SHORT_NAME], 1),
button: bundle.GetStringFromName("onboarding.button.learnMore"),
};
},
getPage(win) {
let div = win.document.createElement("div");
div.innerHTML = `
@ -79,6 +98,13 @@ var onboardingTours = [
{
id: "onboarding-tour-customize",
tourNameId: "onboarding.tour-customize",
getNotificationStrings(bundle) {
return {
title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-customize.title"),
message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-customize.message", [BRAND_SHORT_NAME], 1),
button: bundle.GetStringFromName("onboarding.button.learnMore"),
};
},
getPage(win) {
let div = win.document.createElement("div");
div.innerHTML = `
@ -99,6 +125,13 @@ var onboardingTours = [
{
id: "onboarding-tour-search",
tourNameId: "onboarding.tour-search",
getNotificationStrings(bundle) {
return {
title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-search.title"),
message: bundle.GetStringFromName("onboarding.notification.onboarding-tour-search.message"),
button: bundle.GetStringFromName("onboarding.button.learnMore"),
};
},
getPage(win) {
let div = win.document.createElement("div");
div.innerHTML = `
@ -119,6 +152,13 @@ var onboardingTours = [
{
id: "onboarding-tour-default-browser",
tourNameId: "onboarding.tour-default-browser",
getNotificationStrings(bundle) {
return {
title: bundle.formatStringFromName("onboarding.notification.onboarding-tour-default-browser.title", [BRAND_SHORT_NAME], 1),
message: bundle.formatStringFromName("onboarding.notification.onboarding-tour-default-browser.message", [BRAND_SHORT_NAME], 1),
button: bundle.GetStringFromName("onboarding.button.learnMore"),
};
},
getPage(win) {
let div = win.document.createElement("div");
let defaultBrowserButtonId = win.matchMedia("(-moz-os-version: windows-win7)").matches ?
@ -147,7 +187,6 @@ var onboardingTours = [
class Onboarding {
constructor(contentWindow) {
this.init(contentWindow);
this._bundle = Services.strings.createBundle(BUNDLE_URI);
}
async init(contentWindow) {
@ -157,6 +196,8 @@ class Onboarding {
// We want to create and append elements after CSS is loaded so
// no flash of style changes and no additional reflow.
await this._loadCSS();
this._bundle = Services.strings.createBundle(BUNDLE_URI);
this._overlayIcon = this._renderOverlayIcon();
this._overlay = this._renderOverlay();
this._window.document.body.appendChild(this._overlayIcon);
@ -172,6 +213,25 @@ class Onboarding {
this._window.addEventListener("unload", () => this.destroy());
this._initPrefObserver();
this._initNotification();
}
_initNotification() {
let doc = this._window.document;
if (doc.hidden) {
// When the preloaded-browser feature is on,
// it would preload an hidden about:newtab in the background.
// We don't wnat to show notification in that hidden state.
let onVisible = () => {
if (!doc.hidden) {
doc.removeEventListener("visibilitychange", onVisible);
this.showNotification();
}
};
doc.addEventListener("visibilitychange", onVisible);
} else {
this.showNotification();
}
}
_initPrefObserver() {
@ -219,6 +279,16 @@ class Onboarding {
case "onboarding-overlay":
this.toggleOverlay();
break;
case "onboarding-notification-close-btn":
this.hideNotification();
break;
case "onboarding-notification-action-btn":
let tourId = this._notificationBar.dataset.targetTourId;
this.toggleOverlay();
this.gotoPage(tourId);
break;
}
if (evt.target.classList.contains("onboarding-tour-item")) {
this.gotoPage(evt.target.id);
@ -229,6 +299,9 @@ class Onboarding {
this._clearPrefObserver();
this._overlayIcon.remove();
this._overlay.remove();
if (this._notificationBar) {
this._notificationBar.remove();
}
}
toggleOverlay() {
@ -237,14 +310,13 @@ class Onboarding {
this._loadTours(onboardingTours);
}
this._overlay.classList.toggle("opened");
this.hideNotification();
this._overlay.classList.toggle("onboarding-opened");
let hiddenCheckbox = this._window.document.getElementById("onboarding-tour-hidden-checkbox");
if (hiddenCheckbox.checked) {
this.hide();
return;
}
this._overlay.classList.toggle("onboarding-opened");
}
gotoPage(tourId) {
@ -261,6 +333,108 @@ class Onboarding {
}
}
isTourCompleted(tourId) {
return Preferences.get(`browser.onboarding.tour.${tourId}.completed`, false);
}
showNotification() {
if (Preferences.get("browser.onboarding.notification.finished", false)) {
return;
}
// Pick out the next target tour to show
let targetTour = null;
// Take the last tour as the default last prompted
// so below would start from the 1st one if found no the last prompted from the pref.
let lastPromptedId = onboardingTours[onboardingTours.length - 1].id;
lastPromptedId = Preferences.get("browser.onboarding.notification.lastPrompted", lastPromptedId);
let lastTourIndex = onboardingTours.findIndex(tour => tour.id == lastPromptedId);
if (lastTourIndex < 0) {
// Couldn't find the tour.
// This could be because the pref was manually modified into unknown value
// or the tour version has been updated so have an new tours set.
// Take the last tour as the last prompted so would start from the 1st one below.
lastTourIndex = onboardingTours.length - 1;
}
// Form tours to notify into the order we want.
// For example, There are tour #0 ~ #5 and the #3 is the last prompted.
// This would form [#4, #5, #0, #1, #2, #3].
// So the 1st met incomplete tour in #4 ~ #2 would be the one to show.
// Or #3 would be the one to show if #4 ~ #2 are all completed.
let toursToNotify = [ ...onboardingTours.slice(lastTourIndex + 1), ...onboardingTours.slice(0, lastTourIndex + 1) ];
targetTour = toursToNotify.find(tour => !this.isTourCompleted(tour.id));
if (!targetTour) {
this.sendMessageToChrome("set-prefs", [{
name: "browser.onboarding.notification.finished",
value: true
}]);
return;
}
// Show the target tour notification
this._notificationBar = this._renderNotificationBar();
this._notificationBar.addEventListener("click", this);
this._window.document.body.appendChild(this._notificationBar);
this._notificationBar.dataset.targetTourId = targetTour.id;
let notificationStrings = targetTour.getNotificationStrings(this._bundle);
let actionBtn = this._notificationBar.querySelector("#onboarding-notification-action-btn");
actionBtn.textContent = notificationStrings.button;
let tourTitle = this._notificationBar.querySelector("#onboarding-notification-tour-title");
tourTitle.textContent = notificationStrings.title;
let tourMessage = this._notificationBar.querySelector("#onboarding-notification-tour-message");
tourMessage.textContent = notificationStrings.message;
this._notificationBar.addEventListener("transitionend", () => {
this._notificationBar.dataset.cssTransition = "end";
}, { once: true });
this._window.requestAnimationFrame(() => {
// Request the 2nd animation frame.
// This is to make sure the appending operation above and the css operation happen
// in the different layout tick so as to make sure the transition happens.
this._window.requestAnimationFrame(() => this._notificationBar.classList.add("onboarding-opened"));
});
this.sendMessageToChrome("set-prefs", [{
name: "browser.onboarding.notification.lastPrompted",
value: targetTour.id
}]);
}
hideNotification() {
if (this._notificationBar) {
this._notificationBar.classList.remove("onboarding-opened");
delete this._notificationBar.dataset.cssTransition;
}
}
_renderNotificationBar() {
let div = this._window.document.createElement("div");
div.id = "onboarding-notification-bar";
// Here we use `innerHTML` is for more friendly reading.
// The security should be fine because this is not from an external input.
div.innerHTML = `
<div id="onboarding-notification-icon"></div>
<section id="onboarding-notification-message-section">
<div id="onboarding-notification-tour-icon"></div>
<div id="onboarding-notification-body">
<h6 id="onboarding-notification-tour-title"></h6>
<span id="onboarding-notification-tour-message"></span>
</div>
<button id="onboarding-notification-action-btn"></button>
</section>
<button id="onboarding-notification-close-btn"></button>
`;
let toolTip = this._bundle.formatStringFromName("onboarding.notification-icon-tool-tip", [BRAND_SHORT_NAME], 1);
div.querySelector("#onboarding-notification-icon").setAttribute("data-tooltip", toolTip);
return div;
}
hide() {
this.sendMessageToChrome("set-prefs", [
{
@ -279,7 +453,6 @@ class Onboarding {
div.id = "onboarding-overlay";
// Here we use `innerHTML` is for more friendly reading.
// The security should be fine because this is not from an external input.
// We're not shipping yet so l10n strings is going to be closed for now.
div.innerHTML = `
<div id="onboarding-overlay-dialog">
<span id="onboarding-overlay-close-btn"></span>

Просмотреть файл

@ -1,11 +1,11 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# LOCALIZATION NOTE(onboarding.tour-title): This string will be used in the overlay title. %S is brandShortName
# LOCALIZATION NOTE(onboarding.overlay-title): This string will be used in the overlay title. %S is brandShortName
onboarding.overlay-title=Getting started with %S
onboarding.tour-search=One-Click Search
onboarding.tour-search.title=Find the needle or the haystack.
# LOCALIZATION NOTE (onboarding.tour-search.description): If Amazon is not part
# of the default searchplugins for your locale, you can replace it with another
# ecommerce website (if you're shipping one), but not with a general purpose
@ -13,33 +13,52 @@ onboarding.tour-search.title=Find the needle or the haystack.
# Wikipedia and drop Amazon from the text.
onboarding.tour-search.description=Having a default search engine doesnt mean its the only one you use. Pick a search engine or a site, like Amazon or Wikipedia, to search on the fly.
onboarding.tour-search.button=Open One-Click Search
onboarding.notification.onboarding-tour-search.title=Find it faster.
onboarding.notification.onboarding-tour-search.message=Access all of your favorite search engines with a click. Search the whole Web or just one website right from the search box.
onboarding.tour-private-browsing=Private Browsing
onboarding.tour-private-browsing.title=A little privacy goes a long way.
# LOCALIZATION NOTE(onboarding.tour-private-browsing.description): %S is brandShortName.
onboarding.tour-private-browsing.description=Browse the internet without saving your searches or the sites you visited. When your session ends, the cookies disappear from %S like they were never there.
onboarding.tour-private-browsing.button=Show Private Browsing in Menu
onboarding.hidden-checkbox-label=Hide the tour
onboarding.notification.onboarding-tour-private-browsing.title=Browse by yourself.
onboarding.notification.onboarding-tour-private-browsing.message=Theres no reason to share your online life with trackers every time you browse. Want to keep something to yourself? Use Private Browsing with Tracking Protection.
onboarding.tour-addons=Add-ons
onboarding.tour-addons.title=Add more functionality.
onboarding.notification.onboarding-tour-addons.title=Get more done.
# LOCALIZATION NOTE(onboarding.notification.onboarding-tour-addons.message): %S is brandShortName.
onboarding.notification.onboarding-tour-addons.message=Add-ons are small apps you can add to %S that do lots of things — from managing to-do lists, to downloading videos, to changing the look of your browser.
# LOCALIZATION NOTE(onboarding.tour-addons.description): This string will be used in the add-on tour description. %1$S is brandShortName
onboarding.tour-addons.description=Add-ons expand %1$Ss built-in features, so %1$S works the way you do. Compare prices, check the weather or express your personality with a custom theme.
onboarding.tour-addons.button=Show Add-ons in Menu
onboarding.tour-customize=Customize
onboarding.tour-customize.title=Do things your way.
# LOCALIZATION NOTE(onboarding.tour-customize.description): This string will be used in the customize tour description. %S is brandShortName
onboarding.tour-customize.description=Drag, drop, and reorder %Ss toolbar and menu to fit your needs. You can even select a compact theme to give websites more room.
onboarding.tour-customize.button=Show Customize in Menu
onboarding.notification.onboarding-tour-customize.title=Rearrange your toolbar.
# LOCALIZATION NOTE(onboarding.notification.onboarding-tour-customize.message): %S is brandShortName.
onboarding.notification.onboarding-tour-customize.message=Put the tools you use most right at your fingertips. Add more options to your toolbar. Or select a theme to make %S reflect your personality.
onboarding.tour-default-browser=Default Browser
onboarding.tour-default-browser.title=Were there for you.
# LOCALIZATION NOTE(onboarding.tour-default-browser.description): This string will be used in the default browser tour description. %1$S is brandShortName
onboarding.tour-default-browser.description=Love %1$S? Set it as your default browser. Then when you open a link from another application, %1$S has you covered.
# LOCALIZATION NOTE(onboarding.tour-default-browser.button): Label for a button to open the OS default browser settings where it's not possible to set the default browser directly. (OSX, Linux, Windows 8 and higher)
onboarding.tour-default-browser.button=Open Default Browser Settings
# LOCALIZATION NOTE(onboarding.tour-default-browser.win7.button): Label for a button to directly set the default browser (Windows 7). %S is brandShortName
onboarding.tour-default-browser.win7.button=Make %S Your Default Browser
# LOCALIZATION NOTE(onboarding.notification.onboarding-tour-default-browser.title): %S is brandShortName.
onboarding.notification.onboarding-tour-default-browser.title=Make %S your go-to browser.
# LOCALIZATION NOTE(onboarding.notification.onboarding-tour-default-browser.message): %1$S is brandShortName
onboarding.notification.onboarding-tour-default-browser.message=It doesnt take much to get the most from %1$S. Just set %1$S as your default browser and put control, customization, and protection on autopilot.
onboarding.hidden-checkbox-label=Hide the tour
#LOCALIZATION NOTE(onboarding.button.learnMore): this string is used as a button label, displayed near the message, and shared across all the onboarding notifications.
onboarding.button.learnMore=Learn More
# LOCALIZATION NOTE(onboarding.notification-icon-tool-tip): %S is brandShortName.
onboarding.notification-icon-tool-tip=New to %S?

Просмотреть файл

@ -31,7 +31,7 @@ function promiseOnboardingOverlayOpened(browser) {
return ContentTask.spawn(browser, {}, function() {
return new Promise(resolve => {
let overlay = content.document.querySelector("#onboarding-overlay");
if (overlay.classList.contains("opened")) {
if (overlay.classList.contains("onboarding-opened")) {
resolve(true);
return;
}