author | Jared Wein <jwein@mozilla.com> |
Tue, 18 Nov 2014 12:39:29 -0500 | |
changeset 216372 | e15cb9b338278fb232d64f6d688ed59e90e80eec |
parent 216371 | 121a5cc63382816393d515993817925c41c1f2f7 |
child 216373 | c040e198d145b60627b3aaded10e477f03a31869 |
push id | 52026 |
push user | kwierso@gmail.com |
push date | Wed, 19 Nov 2014 02:37:17 +0000 |
treeherder | mozilla-inbound@d197d16c0caa [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | mikedeboer |
bugs | 1083466 |
milestone | 36.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1639,16 +1639,18 @@ pref("shumway.disabled", true); // The maximum amount of decoded image data we'll willingly keep around (we // might keep around more than this, but we'll try to get down to this value). // (This is intentionally on the high side; see bug 746055.) pref("image.mem.max_decoded_image_kb", 256000); pref("loop.enabled", true); pref("loop.server", "https://loop.services.mozilla.com"); pref("loop.seenToS", "unseen"); +pref("loop.gettingStarted.seen", false); +pref("loop.gettingStarted.url", "https://bugzilla.mozilla.org/show_bug.cgi?id=1099462"); pref("loop.learnMoreUrl", "https://www.firefox.com/hello/"); pref("loop.legal.ToS_url", "https://hello.firefox.com/legal/terms/"); pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/"); pref("loop.do_not_disturb", false); pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/ringtone.ogg"); pref("loop.retry_delay.start", 60000); pref("loop.retry_delay.limit", 300000); pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
--- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -447,16 +447,33 @@ function injectLoopAPI(targetWindow) { enumerable: true, writable: true, value: function(prefName) { return MozLoopService.getLoopCharPref(prefName); } }, /** + * Set any boolean preference under "loop." + * + * @param {String} prefName The name of the pref without the preceding "loop." + * @param {bool} value The value to set. + * + * Any errors thrown by the Mozilla pref API are logged to the console + * and cause false to be returned. + */ + setLoopBoolPref: { + enumerable: true, + writable: true, + value: function(prefName, value) { + MozLoopService.setLoopBoolPref(prefName, value); + } + }, + + /** * Return any preference under "loop." that's coercible to a boolean * preference. * * @param {String} prefName The name of the pref without the preceding * "loop." * * Any errors thrown by the Mozilla pref API are logged to the console * and cause null to be returned. This includes the case of the preference @@ -592,16 +609,27 @@ function injectLoopAPI(targetWindow) { enumerable: true, writable: true, value: function() { return MozLoopService.openFxASettings(); }, }, /** + * Opens the Getting Started tour in the browser. + */ + openGettingStartedTour: { + enumerable: true, + writable: true, + value: function() { + return MozLoopService.openGettingStartedTour(); + }, + }, + + /** * Copies passed string onto the system clipboard. * * @param {String} str The string to copy */ copyString: { enumerable: true, writable: true, value: function(str) {
--- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -1255,16 +1255,33 @@ this.MozLoopService = { } catch (ex) { log.error("getLoopCharPref had trouble getting " + prefName + "; exception: " + ex); return null; } }, /** + * Set any boolean preference under "loop.". + * + * @param {String} prefName The name of the pref without the preceding "loop." + * @param {boolean} value The value to set. + * + * Any errors thrown by the Mozilla pref API are logged to the console. + */ + setLoopBoolPref: function(prefName, value) { + try { + Services.prefs.setBoolPref("loop." + prefName, value); + } catch (ex) { + log.error("setLoopCharPref had trouble setting " + prefName + + "; exception: " + ex); + } + }, + + /** * Return any preference under "loop." that's coercible to a character * preference. * * @param {String} prefName The name of the pref without the preceding * "loop." * * Any errors thrown by the Mozilla pref API are logged to the console * and cause null to be returned. This includes the case of the preference @@ -1383,16 +1400,29 @@ this.MozLoopService = { let win = Services.wm.getMostRecentWindow("navigator:browser"); win.switchToTabHavingURI(url.toString(), true); } catch (ex) { log.error("Error opening FxA settings", ex); } }), /** + * Opens the Getting Started tour in the browser. + */ + openGettingStartedTour: Task.async(function() { + try { + let url = Services.prefs.getCharPref("loop.gettingStarted.url"); + let win = Services.wm.getMostRecentWindow("navigator:browser"); + win.switchToTabHavingURI(url, true); + } catch (ex) { + log.error("Error opening Getting Started tour", ex); + } + }), + + /** * Performs a hawk based request to the loop server. * * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. * One of the LOOP_SESSION_TYPE members. * @param {String} path The path to make the request to. * @param {String} method The request method, e.g. 'POST', 'GET'. * @param {Object} payloadObj An object which is converted to JSON and * transmitted with the request.
--- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -160,16 +160,44 @@ loop.panel = (function(_, mozL10n) { React.DOM.span(null, __("display_name_dnd_status")) ) ) ) ); } }); + var GettingStartedView = React.createClass({displayName: 'GettingStartedView', + componentDidMount: function() { + navigator.mozLoop.setLoopBoolPref("gettingStarted.seen", true); + }, + + handleButtonClick: function() { + navigator.mozLoop.openGettingStartedTour(); + }, + + render: function() { + if (navigator.mozLoop.getLoopBoolPref("gettingStarted.seen")) { + return null; + } + return ( + React.DOM.div({id: "fte-getstarted"}, + React.DOM.header({id: "fte-title"}, + mozL10n.get("first_time_experience_title", { + "clientShortname": mozL10n.get("clientShortname2") + }) + ), + Button({htmlId: "fte-button", + onClick: this.handleButtonClick, + caption: mozL10n.get("first_time_experience_button_label")}) + ) + ); + } + }); + var ToSView = React.createClass({displayName: 'ToSView', getInitialState: function() { return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')}; }, render: function() { if (this.state.seenToS == "unseen") { var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url'); @@ -402,17 +430,17 @@ loop.panel = (function(_, mozL10n) { render: function() { // XXX setting elem value from a state (in the callUrl input) // makes it immutable ie read only but that is fine in our case. // readOnly attr will suppress a warning regarding this issue // from the react lib. var cx = React.addons.classSet; return ( React.DOM.div({className: "generate-url"}, - React.DOM.header(null, __("share_link_header_text")), + React.DOM.header({id: "share-link-header"}, mozL10n.get("share_link_header_text")), React.DOM.div({className: "generate-url-stack"}, React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true", onCopy: this.handleLinkExfiltration, className: cx({"generate-url-input": true, pending: this.state.pending, // Used in functional testing, signals that // call url was received from loop server callUrl: !this.state.pending})}), @@ -704,27 +732,29 @@ loop.panel = (function(_, mozL10n) { * The rooms feature is hidden by default for now. Once it gets mainstream, * this method can be simplified. */ _renderRoomsOrCallTab: function() { if (!this._roomsEnabled()) { return ( Tab({name: "call"}, React.DOM.div({className: "content-area"}, + GettingStartedView(null), CallUrlResult({client: this.props.client, notifications: this.props.notifications, callUrl: this.props.callUrl}), ToSView(null) ) ) ); } return ( Tab({name: "rooms"}, + GettingStartedView(null), RoomList({dispatcher: this.props.dispatcher, store: this.props.roomStore, userDisplayName: this._getUserDisplayName()}), ToSView(null) ) ); }, @@ -825,21 +855,22 @@ loop.panel = (function(_, mozL10n) { // Notify the window that we've finished initalization and initial layout var evtObject = document.createEvent('Event'); evtObject.initEvent('loopPanelInitialized', true, false); window.dispatchEvent(evtObject); } return { init: init, - UserIdentity: UserIdentity, AuthLink: AuthLink, AvailabilityDropdown: AvailabilityDropdown, CallUrlResult: CallUrlResult, + GettingStartedView: GettingStartedView, PanelView: PanelView, RoomEntry: RoomEntry, RoomList: RoomList, SettingsDropdown: SettingsDropdown, - ToSView: ToSView + ToSView: ToSView, + UserIdentity: UserIdentity, }; })(_, document.mozL10n); document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -160,16 +160,44 @@ loop.panel = (function(_, mozL10n) { <span>{__("display_name_dnd_status")}</span> </li> </ul> </div> ); } }); + var GettingStartedView = React.createClass({ + componentDidMount: function() { + navigator.mozLoop.setLoopBoolPref("gettingStarted.seen", true); + }, + + handleButtonClick: function() { + navigator.mozLoop.openGettingStartedTour(); + }, + + render: function() { + if (navigator.mozLoop.getLoopBoolPref("gettingStarted.seen")) { + return null; + } + return ( + <div id="fte-getstarted"> + <header id="fte-title"> + {mozL10n.get("first_time_experience_title", { + "clientShortname": mozL10n.get("clientShortname2") + })} + </header> + <Button htmlId="fte-button" + onClick={this.handleButtonClick} + caption={mozL10n.get("first_time_experience_button_label")} /> + </div> + ); + } + }); + var ToSView = React.createClass({ getInitialState: function() { return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')}; }, render: function() { if (this.state.seenToS == "unseen") { var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url'); @@ -402,17 +430,17 @@ loop.panel = (function(_, mozL10n) { render: function() { // XXX setting elem value from a state (in the callUrl input) // makes it immutable ie read only but that is fine in our case. // readOnly attr will suppress a warning regarding this issue // from the react lib. var cx = React.addons.classSet; return ( <div className="generate-url"> - <header>{__("share_link_header_text")}</header> + <header id="share-link-header">{mozL10n.get("share_link_header_text")}</header> <div className="generate-url-stack"> <input type="url" value={this.state.callUrl} readOnly="true" onCopy={this.handleLinkExfiltration} className={cx({"generate-url-input": true, pending: this.state.pending, // Used in functional testing, signals that // call url was received from loop server callUrl: !this.state.pending})} /> @@ -704,27 +732,29 @@ loop.panel = (function(_, mozL10n) { * The rooms feature is hidden by default for now. Once it gets mainstream, * this method can be simplified. */ _renderRoomsOrCallTab: function() { if (!this._roomsEnabled()) { return ( <Tab name="call"> <div className="content-area"> + <GettingStartedView /> <CallUrlResult client={this.props.client} notifications={this.props.notifications} callUrl={this.props.callUrl} /> <ToSView /> </div> </Tab> ); } return ( <Tab name="rooms"> + <GettingStartedView /> <RoomList dispatcher={this.props.dispatcher} store={this.props.roomStore} userDisplayName={this._getUserDisplayName()}/> <ToSView /> </Tab> ); }, @@ -825,21 +855,22 @@ loop.panel = (function(_, mozL10n) { // Notify the window that we've finished initalization and initial layout var evtObject = document.createEvent('Event'); evtObject.initEvent('loopPanelInitialized', true, false); window.dispatchEvent(evtObject); } return { init: init, - UserIdentity: UserIdentity, AuthLink: AuthLink, AvailabilityDropdown: AvailabilityDropdown, CallUrlResult: CallUrlResult, + GettingStartedView: GettingStartedView, PanelView: PanelView, RoomEntry: RoomEntry, RoomList: RoomList, SettingsDropdown: SettingsDropdown, - ToSView: ToSView + ToSView: ToSView, + UserIdentity: UserIdentity, }; })(_, document.mozL10n); document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/shared/css/panel.css +++ b/browser/components/loop/content/shared/css/panel.css @@ -97,20 +97,46 @@ body { /* Content area and input fields */ .content-area { padding: 14px; } .content-area header { font-weight: 700; +} + +#fte-getstarted { + padding-top: 1em; + padding-bottom: 1em; + border-bottom: 1px solid #ccc; + margin-bottom: 1em; +} + +#fte-title { + text-align: center; + margin-bottom: .5em; +} + +#fte-button { + width: 50%; + display: block; + margin-left: auto; + margin-right: auto; + font-size: 1rem; + padding: .5rem 1rem; + height: auto; /* Needed to override .button's height:26px; */ +} + +/* Need to remove when these rules when the Beta tag is removed */ +#share-link-header { -moz-padding-start: 20px; } - -.tab-view + .tab .content-area header { +#fte-getstarted + .generate-url > #share-link-header, +.tab-view + .tab .content-area > .generate-url > #share-link-header { /* The header shouldn't be indented if the tabs are present. */ -moz-padding-start: 0; } .content-area label { display: block; width: 100%; margin-top: 10px; @@ -429,16 +455,17 @@ body[dir=rtl] .dropdown-menu-item { right: 4px; } body[dir=rtl] .generate-url-spinner { left: 4px; right: auto; } +#fte-button, .generate-url .button { background-color: #0096dd; border-color: #0096dd; color: #fff; } .generate-url .button:hover { background-color: #008acb;
--- a/browser/components/loop/content/shared/js/views.js +++ b/browser/components/loop/content/shared/js/views.js @@ -713,34 +713,37 @@ loop.shared.views = (function(_, OT, l10 }); var Button = React.createClass({displayName: 'Button', propTypes: { caption: React.PropTypes.string.isRequired, onClick: React.PropTypes.func.isRequired, disabled: React.PropTypes.bool, additionalClass: React.PropTypes.string, + htmlId: React.PropTypes.string, }, getDefaultProps: function() { return { disabled: false, additionalClass: "", + htmlId: "", }; }, render: function() { var cx = React.addons.classSet; var classObject = { button: true, disabled: this.props.disabled }; if (this.props.additionalClass) { classObject[this.props.additionalClass] = true; } return ( React.DOM.button({onClick: this.props.onClick, disabled: this.props.disabled, + id: this.props.htmlId, className: cx(classObject)}, React.DOM.span({className: "button-caption"}, this.props.caption), this.props.children ) ) } });
--- a/browser/components/loop/content/shared/js/views.jsx +++ b/browser/components/loop/content/shared/js/views.jsx @@ -713,34 +713,37 @@ loop.shared.views = (function(_, OT, l10 }); var Button = React.createClass({ propTypes: { caption: React.PropTypes.string.isRequired, onClick: React.PropTypes.func.isRequired, disabled: React.PropTypes.bool, additionalClass: React.PropTypes.string, + htmlId: React.PropTypes.string, }, getDefaultProps: function() { return { disabled: false, additionalClass: "", + htmlId: "", }; }, render: function() { var cx = React.addons.classSet; var classObject = { button: true, disabled: this.props.disabled }; if (this.props.additionalClass) { classObject[this.props.additionalClass] = true; } return ( <button onClick={this.props.onClick} disabled={this.props.disabled} + id={this.props.htmlId} className={cx(classObject)}> <span className="button-caption">{this.props.caption}</span> {this.props.children} </button> ) } });
--- a/browser/components/loop/test/desktop-local/panel_test.js +++ b/browser/components/loop/test/desktop-local/panel_test.js @@ -29,19 +29,20 @@ describe("loop.panel", function() { doNotDisturb: true, fxAEnabled: true, getStrings: function() { return JSON.stringify({textContent: "fakeText"}); }, get locale() { return "en-US"; }, - getLoopBoolPref: sandbox.stub(), setLoopCharPref: sandbox.stub(), getLoopCharPref: sandbox.stub().returns("unseen"), + getLoopBoolPref: sandbox.stub(), + setLoopBoolPref: sandbox.stub(), getPluralForm: function() { return "fakeText"; }, copyString: sandbox.stub(), noteCallUrlExpiry: sinon.spy(), composeEmail: sinon.spy(), telemetryAdd: sinon.spy(), contacts: { @@ -356,16 +357,47 @@ describe("loop.panel", function() { }); describe("#render", function() { it("should render a ToSView", function() { var view = createTestPanelView(); TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView); }); + + it("should not render a ToSView when the view has been 'seen'", function() { + navigator.mozLoop.getLoopCharPref = function() { + return "seen"; + }; + var view = createTestPanelView(); + + try { + TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView); + sinon.assert.fail("Should not find the ToSView if it has been 'seen'"); + } catch (ex) {} + }); + + it("should render a GettingStarted view", function() { + var view = createTestPanelView(); + + TestUtils.findRenderedComponentWithType(view, loop.panel.GettingStartedView); + }); + + it("should not render a GettingStartedView when the view has been seen", function() { + navigator.mozLoop.getLoopBoolPref = function() { + return true; + }; + var view = createTestPanelView(); + + try { + TestUtils.findRenderedComponentWithType(view, loop.panel.GettingStartedView); + sinon.assert.fail("Should not find the GettingStartedView if it has been seen"); + } catch (ex) {} + }); + }); }); describe("loop.panel.CallUrlResult", function() { var fakeClient, callUrlData, view; beforeEach(function() { callUrlData = {