Bug 1033841: Ported Loop panel views to React. r=Standard8

This commit is contained in:
Nicolas Perriault
2014-07-04 17:08:55 +01:00
parent b0030d83b4
commit dffd8e9d71
15 changed files with 712 additions and 376 deletions

View File

@@ -2,6 +2,7 @@
* 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/. */
/* jshint esnext:true */
/* global loop:true, hawk, deriveHawkCredentials */
var loop = loop || {};

View File

@@ -2,6 +2,7 @@
* 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/. */
/* jshint esnext:true */
/* global loop:true */
var loop = loop || {};

View File

@@ -2,6 +2,7 @@
* 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/. */
/* jshint esnext:true */
/* global loop:true */
var loop = loop || {};

View File

@@ -1,8 +1,11 @@
/** @jsx React.DOM */
/* 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/. */
/* global loop:true */
/*jshint newcap:false*/
/*global loop:true, React */
var loop = loop || {};
loop.panel = (function(_, mozL10n) {
@@ -21,162 +24,188 @@ loop.panel = (function(_, mozL10n) {
/**
* Do not disturb panel subview.
*/
var DoNotDisturbView = sharedViews.BaseView.extend({
template: _.template([
'<label>',
' <input type="checkbox" <%- checked %>>',
' <span data-l10n-id="do_not_disturb"></span>',
'</label>',
].join('')),
events: {
"click input[type=checkbox]": "toggle"
var DoNotDisturb = React.createClass({displayName: 'DoNotDisturb',
getInitialState: function() {
return {doNotDisturb: navigator.mozLoop.doNotDisturb};
},
/**
* Toggles mozLoop activation status.
*/
toggle: function() {
handleCheckboxChange: function() {
// Note: side effect!
navigator.mozLoop.doNotDisturb = !navigator.mozLoop.doNotDisturb;
this.render();
this.setState({doNotDisturb: navigator.mozLoop.doNotDisturb});
},
render: function() {
this.$el.html(this.template({
checked: navigator.mozLoop.doNotDisturb ? "checked" : ""
}));
return this;
// XXX https://github.com/facebook/react/issues/310 for === htmlFor
return (
React.DOM.p( {className:"dnd"},
React.DOM.input( {type:"checkbox", checked:this.state.doNotDisturb,
id:"dnd-component", onChange:this.handleCheckboxChange} ),
React.DOM.label( {htmlFor:"dnd-component"}, __("do_not_disturb"))
)
);
}
});
var ToSView = sharedViews.BaseView.extend({
template: _.template([
'<p data-l10n-id="legal_text_and_links"',
' data-l10n-args=\'',
' {"terms_of_use_url": "https://accounts.firefox.com/legal/terms",',
' "privacy_notice_url": "www.mozilla.org/privacy/"',
' }\'></p>'
].join('')),
var ToSView = React.createClass({displayName: 'ToSView',
getInitialState: function() {
return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
},
render: function() {
if (navigator.mozLoop.getLoopCharPref('seenToS') === null) {
this.$el.html(this.template());
var tosHTML = __("legal_text_and_links", {
"terms_of_use_url": "https://accounts.firefox.com/legal/terms",
"privacy_notice_url": "www.mozilla.org/privacy/"
});
if (!this.state.seenToS) {
navigator.mozLoop.setLoopCharPref('seenToS', 'seen');
return React.DOM.p( {className:"tos",
dangerouslySetInnerHTML:{__html: tosHTML}});
} else {
return React.DOM.div(null );
}
return this;
}
});
var PanelLayout = React.createClass({displayName: 'PanelLayout',
propTypes: {
summary: React.PropTypes.string.isRequired
},
render: function() {
return (
React.DOM.div( {className:"share generate-url"},
React.DOM.div( {className:"description"},
React.DOM.p(null, this.props.summary)
),
React.DOM.div( {className:"action"},
this.props.children
)
)
);
}
});
var CallUrlResult = React.createClass({displayName: 'CallUrlResult',
propTypes: {
callUrl: React.PropTypes.string.isRequired,
retry: React.PropTypes.func.isRequired
},
handleButtonClick: function() {
this.props.retry();
},
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.
return (
PanelLayout( {summary:__("share_link_url")},
React.DOM.div( {className:"invite"},
React.DOM.input( {type:"url", value:this.props.callUrl, readOnly:"true"} ),
React.DOM.button( {onClick:this.handleButtonClick,
className:"btn btn-success"}, __("new_url"))
)
)
);
}
});
var CallUrlForm = React.createClass({displayName: 'CallUrlForm',
propTypes: {
client: React.PropTypes.object.isRequired,
notifier: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
pending: false,
disabled: true,
callUrl: false
};
},
retry: function() {
this.setState(this.getInitialState());
},
handleTextChange: function(event) {
this.setState({disabled: !event.currentTarget.value});
},
handleFormSubmit: function(event) {
event.preventDefault();
this.setState({pending: true});
this.props.client.requestCallUrl(
this.refs.caller.getDOMNode().value, this._onCallUrlReceived);
},
_onCallUrlReceived: function(err, callUrlData) {
var callUrl = false;
this.props.notifier.clear();
if (err) {
this.props.notifier.errorL10n("unable_retrieve_url");
} else {
callUrl = callUrlData.callUrl || callUrlData.call_url;
}
this.setState({pending: false, callUrl: callUrl});
},
render: function() {
// If we have a call url, render result
if (this.state.callUrl) {
return (
CallUrlResult( {callUrl:this.state.callUrl, retry:this.retry})
);
}
// If we don't display the form
var cx = React.addons.classSet;
return (
PanelLayout( {summary:__("get_link_to_share")},
React.DOM.form( {className:"invite", onSubmit:this.handleFormSubmit},
React.DOM.input( {type:"text", name:"caller", ref:"caller", required:"required",
className:cx({'pending': this.state.pending}),
onChange:this.handleTextChange,
placeholder:__("call_identifier_textinput_placeholder")} ),
React.DOM.button( {type:"submit", className:"get-url btn btn-success",
disabled:this.state.disabled},
__("get_a_call_url")
)
),
ToSView(null )
)
);
}
});
/**
* Panel view.
*/
var PanelView = sharedViews.BaseView.extend({
template: _.template([
'<div class="description">',
' <p data-l10n-id="get_link_to_share"></p>',
'</div>',
'<div class="action">',
' <form class="invite">',
' <input type="text" name="caller" data-l10n-id="caller" required>',
' <button type="submit" class="get-url btn btn-success"',
' data-l10n-id="get_a_call_url"></button>',
' </form>',
' <p class="tos"></p>',
' <p class="result hide">',
' <input id="call-url" type="url" readonly>',
' <a class="go-back btn btn-info" href="" data-l10n-id="new_url"></a>',
' </p>',
' <p class="dnd"></p>',
'</div>',
].join("")),
className: "share generate-url",
/**
* Do not disturb view.
* @type {DoNotDisturbView|undefined}
*/
dndView: undefined,
events: {
"keyup input[name=caller]": "changeButtonState",
"submit form.invite": "getCallUrl",
"click a.go-back": "goBack"
},
initialize: function(options) {
options = options || {};
if (!options.notifier) {
throw new Error("missing required notifier");
}
this.notifier = options.notifier;
this.client = new loop.Client();
},
getNickname: function() {
return this.$("input[name=caller]").val();
},
getCallUrl: function(event) {
this.notifier.clear();
event.preventDefault();
var callback = function(err, callUrlData) {
this.clearPending();
if (err) {
this.notifier.errorL10n("unable_retrieve_url");
this.render();
return;
}
this.onCallUrlReceived(callUrlData);
}.bind(this);
this.setPending();
this.client.requestCallUrl(this.getNickname(), callback);
},
goBack: function(event) {
event.preventDefault();
this.$(".action .result").hide();
this.$(".action .invite").show();
this.$(".description p").text(__("get_link_to_share"));
this.changeButtonState();
},
onCallUrlReceived: function(callUrlData) {
this.notifier.clear();
this.$(".action .invite").hide();
this.$(".action .invite input").val("");
this.$(".action .result input").val(callUrlData.callUrl);
this.$(".action .result").show();
this.$(".description p").text(__("share_link_url"));
},
setPending: function() {
this.$("[name=caller]").addClass("pending");
this.$(".get-url").addClass("disabled").attr("disabled", "disabled");
},
clearPending: function() {
this.$("[name=caller]").removeClass("pending");
this.changeButtonState();
},
changeButtonState: function() {
var enabled = !!this.$("input[name=caller]").val();
if (enabled) {
this.$(".get-url").removeClass("disabled")
.removeAttr("disabled", "disabled");
} else {
this.$(".get-url").addClass("disabled").attr("disabled", "disabled");
}
var PanelView = React.createClass({displayName: 'PanelView',
propTypes: {
notifier: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
},
render: function() {
this.$el.html(this.template());
// Do not Disturb sub view
this.dndView = new DoNotDisturbView({el: this.$(".dnd")}).render();
this.tosView = new ToSView({el: this.$(".tos")}).render();
return this;
return (
React.DOM.div(null,
CallUrlForm( {client:this.props.client,
notifier:this.props.notifier} ),
DoNotDisturb(null )
)
);
}
});
@@ -230,10 +259,12 @@ loop.panel = (function(_, mozL10n) {
* Resets this router to its initial state.
*/
reset: function() {
// purge pending notifications
this._notifier.clear();
// reset home view
this.loadView(new PanelView({notifier: this._notifier}));
var client = new loop.Client({
baseServerUrl: navigator.mozLoop.serverUrl
});
this.loadReactComponent(PanelView( {client:client,
notifier:this._notifier} ));
}
});
@@ -259,8 +290,9 @@ loop.panel = (function(_, mozL10n) {
return {
init: init,
DoNotDisturb: DoNotDisturb,
CallUrlForm: CallUrlForm,
PanelView: PanelView,
DoNotDisturbView: DoNotDisturbView,
PanelRouter: PanelRouter,
ToSView: ToSView
};

View File

@@ -0,0 +1,299 @@
/** @jsx React.DOM */
/* 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/. */
/*jshint newcap:false*/
/*global loop:true, React */
var loop = loop || {};
loop.panel = (function(_, mozL10n) {
"use strict";
var sharedViews = loop.shared.views,
// aliasing translation function as __ for concision
__ = mozL10n.get;
/**
* Panel router.
* @type {loop.desktopRouter.DesktopRouter}
*/
var router;
/**
* Do not disturb panel subview.
*/
var DoNotDisturb = React.createClass({
getInitialState: function() {
return {doNotDisturb: navigator.mozLoop.doNotDisturb};
},
handleCheckboxChange: function() {
// Note: side effect!
navigator.mozLoop.doNotDisturb = !navigator.mozLoop.doNotDisturb;
this.setState({doNotDisturb: navigator.mozLoop.doNotDisturb});
},
render: function() {
// XXX https://github.com/facebook/react/issues/310 for === htmlFor
return (
<p className="dnd">
<input type="checkbox" checked={this.state.doNotDisturb}
id="dnd-component" onChange={this.handleCheckboxChange} />
<label htmlFor="dnd-component">{__("do_not_disturb")}</label>
</p>
);
}
});
var ToSView = React.createClass({
getInitialState: function() {
return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
},
render: function() {
var tosHTML = __("legal_text_and_links", {
"terms_of_use_url": "https://accounts.firefox.com/legal/terms",
"privacy_notice_url": "www.mozilla.org/privacy/"
});
if (!this.state.seenToS) {
navigator.mozLoop.setLoopCharPref('seenToS', 'seen');
return <p className="tos"
dangerouslySetInnerHTML={{__html: tosHTML}}></p>;
} else {
return <div />;
}
}
});
var PanelLayout = React.createClass({
propTypes: {
summary: React.PropTypes.string.isRequired
},
render: function() {
return (
<div className="share generate-url">
<div className="description">
<p>{this.props.summary}</p>
</div>
<div className="action">
{this.props.children}
</div>
</div>
);
}
});
var CallUrlResult = React.createClass({
propTypes: {
callUrl: React.PropTypes.string.isRequired,
retry: React.PropTypes.func.isRequired
},
handleButtonClick: function() {
this.props.retry();
},
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.
return (
<PanelLayout summary={__("share_link_url")}>
<div className="invite">
<input type="url" value={this.props.callUrl} readOnly="true" />
<button onClick={this.handleButtonClick}
className="btn btn-success">{__("new_url")}</button>
</div>
</PanelLayout>
);
}
});
var CallUrlForm = React.createClass({
propTypes: {
client: React.PropTypes.object.isRequired,
notifier: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
pending: false,
disabled: true,
callUrl: false
};
},
retry: function() {
this.setState(this.getInitialState());
},
handleTextChange: function(event) {
this.setState({disabled: !event.currentTarget.value});
},
handleFormSubmit: function(event) {
event.preventDefault();
this.setState({pending: true});
this.props.client.requestCallUrl(
this.refs.caller.getDOMNode().value, this._onCallUrlReceived);
},
_onCallUrlReceived: function(err, callUrlData) {
var callUrl = false;
this.props.notifier.clear();
if (err) {
this.props.notifier.errorL10n("unable_retrieve_url");
} else {
callUrl = callUrlData.callUrl || callUrlData.call_url;
}
this.setState({pending: false, callUrl: callUrl});
},
render: function() {
// If we have a call url, render result
if (this.state.callUrl) {
return (
<CallUrlResult callUrl={this.state.callUrl} retry={this.retry}/>
);
}
// If we don't display the form
var cx = React.addons.classSet;
return (
<PanelLayout summary={__("get_link_to_share")}>
<form className="invite" onSubmit={this.handleFormSubmit}>
<input type="text" name="caller" ref="caller" required="required"
className={cx({'pending': this.state.pending})}
onChange={this.handleTextChange}
placeholder={__("call_identifier_textinput_placeholder")} />
<button type="submit" className="get-url btn btn-success"
disabled={this.state.disabled}>
{__("get_a_call_url")}
</button>
</form>
<ToSView />
</PanelLayout>
);
}
});
/**
* Panel view.
*/
var PanelView = React.createClass({
propTypes: {
notifier: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
},
render: function() {
return (
<div>
<CallUrlForm client={this.props.client}
notifier={this.props.notifier} />
<DoNotDisturb />
</div>
);
}
});
var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
/**
* DOM document object.
* @type {HTMLDocument}
*/
document: undefined,
routes: {
"": "home"
},
initialize: function(options) {
options = options || {};
if (!options.document) {
throw new Error("missing required document");
}
this.document = options.document;
this._registerVisibilityChangeEvent();
this.on("panel:open panel:closed", this.reset, this);
},
/**
* Register the DOM visibility API event for the whole document, and trigger
* appropriate events accordingly:
*
* - `panel:opened` when the panel is open
* - `panel:closed` when the panel is closed
*
* @link http://www.w3.org/TR/page-visibility/
*/
_registerVisibilityChangeEvent: function() {
this.document.addEventListener("visibilitychange", function(event) {
this.trigger(event.currentTarget.hidden ? "panel:closed"
: "panel:open");
}.bind(this));
},
/**
* Default entry point.
*/
home: function() {
this.reset();
},
/**
* Resets this router to its initial state.
*/
reset: function() {
this._notifier.clear();
var client = new loop.Client({
baseServerUrl: navigator.mozLoop.serverUrl
});
this.loadReactComponent(<PanelView client={client}
notifier={this._notifier} />);
}
});
/**
* Panel initialisation.
*/
function init() {
// Do the initial L10n setup, we do this before anything
// else to ensure the L10n environment is setup correctly.
mozL10n.initialize(navigator.mozLoop);
router = new PanelRouter({
document: document,
notifier: new sharedViews.NotificationListView({el: "#messages"})
});
Backbone.history.start();
// 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,
DoNotDisturb: DoNotDisturb,
CallUrlForm: CallUrlForm,
PanelView: PanelView,
PanelRouter: PanelRouter,
ToSView: ToSView
};
})(_, document.mozL10n);