mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1033841: Ported Loop panel views to React. r=Standard8
This commit is contained in:
parent
c3ab34545d
commit
671908ff16
@ -6,3 +6,16 @@ The standalone client is a set of web pages intended to be hosted on a standalon
|
||||
|
||||
The standalone client exists in standalone/ but shares items (from content/shared/) with the desktop implementation. See the README.md file in the standalone/ directory for how to run the server locally.
|
||||
|
||||
Working with JSX
|
||||
================
|
||||
|
||||
You need to install the JSX compiler in order to compile the .jsx files into regular .js ones.
|
||||
|
||||
The JSX compiler is installable using npm:
|
||||
|
||||
npm install -g react-tools
|
||||
|
||||
Once installed, run it with the --watch option, eg.:
|
||||
|
||||
jsx --watch --x jsx browser/components/loop/content/js/src \
|
||||
browser/components/loop/content/js
|
||||
|
@ -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 || {};
|
||||
|
@ -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 || {};
|
||||
|
@ -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 || {};
|
||||
|
@ -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
|
||||
};
|
||||
|
299
browser/components/loop/content/js/src/panel.jsx
Normal file
299
browser/components/loop/content/js/src/panel.jsx
Normal 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);
|
@ -15,6 +15,7 @@
|
||||
|
||||
<div id="main"></div>
|
||||
|
||||
<script type="text/javascript" src="loop/shared/libs/react-0.10.0.js"></script>
|
||||
<script type="text/javascript" src="loop/libs/l10n.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
|
||||
|
@ -53,8 +53,11 @@ a {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
.share .action p.dnd {
|
||||
margin-top: 1em;
|
||||
p.dnd {
|
||||
margin: 0 10px 10px 10px;
|
||||
/* The panel won't increase its height when using a bottom margin, while it
|
||||
works using a padding */
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.share .action input[type="text"],
|
||||
@ -65,6 +68,7 @@ a {
|
||||
font-size: .9em;
|
||||
width: 65%;
|
||||
padding: .5em;
|
||||
margin-right: .35em;
|
||||
}
|
||||
|
||||
.share .action input.pending {
|
||||
|
@ -18,7 +18,7 @@ loop.shared.router = (function(l10n) {
|
||||
var BaseRouter = Backbone.Router.extend({
|
||||
/**
|
||||
* Active view.
|
||||
* @type {loop.shared.views.BaseView}
|
||||
* @type {Object}
|
||||
*/
|
||||
_activeView: undefined,
|
||||
|
||||
@ -51,12 +51,38 @@ loop.shared.router = (function(l10n) {
|
||||
*
|
||||
* @param {loop.shared.views.BaseView} view View.
|
||||
*/
|
||||
loadView : function(view) {
|
||||
if (this._activeView) {
|
||||
this._activeView.remove();
|
||||
loadView: function(view) {
|
||||
this.clearActiveView();
|
||||
this._activeView = {type: "backbone", view: view.render().show()};
|
||||
this.updateView(this._activeView.view.$el);
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders a React component as current active view.
|
||||
*
|
||||
* @param {React} reactComponent React component.
|
||||
*/
|
||||
loadReactComponent: function(reactComponent) {
|
||||
this.clearActiveView();
|
||||
this._activeView = {
|
||||
type: "react",
|
||||
view: React.renderComponent(reactComponent,
|
||||
document.querySelector("#main"))
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears current active view.
|
||||
*/
|
||||
clearActiveView: function() {
|
||||
if (!this._activeView) {
|
||||
return;
|
||||
}
|
||||
if (this._activeView.type === "react") {
|
||||
React.unmountComponentAtNode(document.querySelector("#main"));
|
||||
} else {
|
||||
this._activeView.view.remove();
|
||||
}
|
||||
this._activeView = view.render().show();
|
||||
this.updateView(this._activeView.$el);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -139,8 +139,11 @@ describe("loop.conversation", function() {
|
||||
router.accept();
|
||||
|
||||
sinon.assert.calledOnce(conversation.initiate);
|
||||
sinon.assert.calledWithExactly(conversation.initiate, {
|
||||
baseServerUrl: "http://example.com",
|
||||
sinon.assert.calledWithMatch(conversation.initiate, {
|
||||
client: {
|
||||
mozLoop: navigator.mozLoop,
|
||||
settings: {}
|
||||
},
|
||||
outgoing: false
|
||||
});
|
||||
});
|
||||
|
@ -16,6 +16,7 @@
|
||||
<div id="fixtures"></div>
|
||||
<!-- libs -->
|
||||
<script src="../../content/libs/l10n.js"></script>
|
||||
<script src="../../content/shared/libs/react-0.10.0.js"></script>
|
||||
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
@ -35,8 +36,8 @@
|
||||
<script src="../../content/shared/js/router.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/js/client.js"></script>
|
||||
<script src="../../content/js/conversation.js"></script>
|
||||
<script src="../../content/js/desktopRouter.js"></script>
|
||||
<script src="../../content/js/conversation.js"></script>
|
||||
<script src="../../content/js/panel.js"></script>
|
||||
|
||||
<!-- Test scripts -->
|
||||
|
@ -5,12 +5,12 @@
|
||||
/*global loop, sinon */
|
||||
|
||||
var expect = chai.expect;
|
||||
var TestUtils = React.addons.TestUtils;
|
||||
|
||||
describe("loop.panel", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox, notifier, fakeXHR, requests = [], savedMozLoop,
|
||||
fakeSeenToSPref = 0;
|
||||
var sandbox, notifier, fakeXHR, requests = [];
|
||||
|
||||
function createTestRouter(fakeDocument) {
|
||||
return new loop.panel.PanelRouter({
|
||||
@ -42,18 +42,13 @@ describe("loop.panel", function() {
|
||||
return "http://example.com";
|
||||
},
|
||||
getStrings: function() {
|
||||
return "{}";
|
||||
return JSON.stringify({textContent: "fakeText"});
|
||||
},
|
||||
get locale() {
|
||||
return "en-US";
|
||||
},
|
||||
setLoopCharPref: sandbox.stub(),
|
||||
getLoopCharPref: function () {
|
||||
if (fakeSeenToSPref === 0) {
|
||||
return null;
|
||||
}
|
||||
return 'seen';
|
||||
}
|
||||
getLoopCharPref: sandbox.stub()
|
||||
};
|
||||
|
||||
document.mozL10n.initialize(navigator.mozLoop);
|
||||
@ -61,7 +56,6 @@ describe("loop.panel", function() {
|
||||
|
||||
afterEach(function() {
|
||||
delete navigator.mozLoop;
|
||||
$("#fixtures").empty();
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
@ -90,6 +84,7 @@ describe("loop.panel", function() {
|
||||
});
|
||||
|
||||
sandbox.stub(router, "loadView");
|
||||
sandbox.stub(router, "loadReactComponent");
|
||||
});
|
||||
|
||||
describe("#home", function() {
|
||||
@ -112,13 +107,21 @@ describe("loop.panel", function() {
|
||||
it("should load the home view", function() {
|
||||
router.reset();
|
||||
|
||||
sinon.assert.calledOnce(router.loadView);
|
||||
sinon.assert.calledWithExactly(router.loadView,
|
||||
sinon.match.instanceOf(loop.panel.PanelView));
|
||||
sinon.assert.calledOnce(router.loadReactComponent);
|
||||
sinon.assert.calledWithExactly(router.loadReactComponent,
|
||||
sinon.match(function(value) {
|
||||
return React.addons.TestUtils.isComponentOfType(
|
||||
value, loop.panel.PanelView);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(loop.panel.PanelRouter.prototype, "trigger");
|
||||
});
|
||||
|
||||
it("should listen to document visibility changes", function() {
|
||||
var fakeDocument = {
|
||||
hidden: true,
|
||||
@ -133,256 +136,204 @@ describe("loop.panel", function() {
|
||||
});
|
||||
|
||||
it("should trigger panel:open when the panel document is visible",
|
||||
function(done) {
|
||||
function() {
|
||||
var router = createTestRouter({
|
||||
hidden: false,
|
||||
addEventListener: function(name, cb) {
|
||||
setTimeout(function() {
|
||||
cb({currentTarget: {hidden: false}});
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
router.once("panel:open", function() {
|
||||
done();
|
||||
});
|
||||
sinon.assert.calledOnce(router.trigger);
|
||||
sinon.assert.calledWithExactly(router.trigger, "panel:open");
|
||||
});
|
||||
|
||||
it("should trigger panel:closed when the panel document is hidden",
|
||||
function(done) {
|
||||
function() {
|
||||
var router = createTestRouter({
|
||||
addEventListener: function(name, cb) {
|
||||
hidden: true,
|
||||
setTimeout(function() {
|
||||
addEventListener: function(name, cb) {
|
||||
cb({currentTarget: {hidden: true}});
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
router.once("panel:closed", function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
sinon.assert.calledOnce(router.trigger);
|
||||
sinon.assert.calledWithExactly(router.trigger, "panel:closed");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("loop.panel.DoNotDisturbView", function() {
|
||||
describe("loop.panel.DoNotDisturb", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
$("#fixtures").append('<div id="dnd-view"></div>');
|
||||
view = new loop.panel.DoNotDisturbView({el: $("#dnd-view")});
|
||||
view = TestUtils.renderIntoDocument(loop.panel.DoNotDisturb());
|
||||
});
|
||||
|
||||
describe("#toggle", function() {
|
||||
describe("Checkbox change event", function() {
|
||||
beforeEach(function() {
|
||||
navigator.mozLoop.doNotDisturb = false;
|
||||
|
||||
var checkbox = TestUtils.findRenderedDOMComponentWithTag(view, "input");
|
||||
TestUtils.Simulate.change(checkbox);
|
||||
});
|
||||
|
||||
it("should toggle the value of mozLoop.doNotDisturb", function() {
|
||||
view.toggle();
|
||||
|
||||
expect(navigator.mozLoop.doNotDisturb).eql(true);
|
||||
});
|
||||
|
||||
it("should update the DnD checkbox value", function() {
|
||||
view.toggle();
|
||||
|
||||
expect(view.$("input").is(":checked")).eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("render", function() {
|
||||
it("should check the dnd checkbox when dnd is enabled", function() {
|
||||
navigator.mozLoop.doNotDisturb = false;
|
||||
|
||||
view.render();
|
||||
|
||||
expect(view.$("input").is(":checked")).eql(false);
|
||||
});
|
||||
|
||||
it("should uncheck the dnd checkbox when dnd is disabled", function() {
|
||||
navigator.mozLoop.doNotDisturb = true;
|
||||
|
||||
view.render();
|
||||
|
||||
expect(view.$("input").is(":checked")).eql(true);
|
||||
expect(view.getDOMNode().querySelector("input").checked).eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("loop.panel.PanelView", function() {
|
||||
describe("loop.panel.CallUrlForm", function() {
|
||||
var fakeClient, callUrlData, view;
|
||||
|
||||
beforeEach(function() {
|
||||
$("#fixtures").append('<div id="messages"></div><div id="main"></div>');
|
||||
callUrlData = {
|
||||
call_url: "http://call.invalid/",
|
||||
expiresAt: 1000
|
||||
};
|
||||
|
||||
fakeClient = {
|
||||
requestCallUrl: function(_, cb) {
|
||||
cb(null, callUrlData);
|
||||
}
|
||||
};
|
||||
|
||||
view = TestUtils.renderIntoDocument(loop.panel.CallUrlForm({
|
||||
notifier: notifier,
|
||||
client: fakeClient
|
||||
}));
|
||||
});
|
||||
|
||||
describe("#getCallUrl", function() {
|
||||
describe("#render", function() {
|
||||
it("should render a ToSView", function() {
|
||||
TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Form submit event", function() {
|
||||
|
||||
function submitForm(callerValue) {
|
||||
// fill caller field
|
||||
TestUtils.Simulate.change(
|
||||
TestUtils.findRenderedDOMComponentWithTag(view, "input"), {
|
||||
target: {value: callerValue}
|
||||
});
|
||||
|
||||
// submit form
|
||||
TestUtils.Simulate.submit(
|
||||
TestUtils.findRenderedDOMComponentWithTag(view, "form"));
|
||||
}
|
||||
|
||||
it("should reset all pending notifications", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
||||
"requestCallUrl");
|
||||
var view = new loop.panel.PanelView({notifier: notifier}).render();
|
||||
submitForm("foo");
|
||||
|
||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
||||
|
||||
sinon.assert.calledOnce(view.notifier.clear, "clear");
|
||||
sinon.assert.calledOnce(notifier.clear, "clear");
|
||||
});
|
||||
|
||||
it("should request a call url to the server", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
||||
"requestCallUrl");
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
sandbox.stub(view, "getNickname").returns("foo");
|
||||
fakeClient.requestCallUrl = sandbox.stub();
|
||||
|
||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
||||
submitForm("foo");
|
||||
|
||||
sinon.assert.calledOnce(requestCallUrl);
|
||||
sinon.assert.calledWith(requestCallUrl, "foo");
|
||||
sinon.assert.calledOnce(fakeClient.requestCallUrl);
|
||||
sinon.assert.calledWith(fakeClient.requestCallUrl, "foo");
|
||||
});
|
||||
|
||||
it("should set the call url form in a pending state", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
||||
"requestCallUrl");
|
||||
sandbox.stub(loop.panel.PanelView.prototype, "setPending");
|
||||
// Cancel requestCallUrl effect to keep the state pending
|
||||
fakeClient.requestCallUrl = sandbox.stub();
|
||||
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
submitForm("foo");
|
||||
|
||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
||||
expect(view.state.pending).eql(true);
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(view.setPending);
|
||||
it("should update state with the call url received", function() {
|
||||
submitForm("foo");
|
||||
|
||||
expect(view.state.pending).eql(false);
|
||||
expect(view.state.callUrl).eql(callUrlData.call_url);
|
||||
});
|
||||
|
||||
it("should clear the pending state when a response is received",
|
||||
function() {
|
||||
sandbox.stub(loop.panel.PanelView.prototype,
|
||||
"clearPending");
|
||||
var requestCallUrl = sandbox.stub(
|
||||
loop.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
cb("fake error");
|
||||
});
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
submitForm("foo");
|
||||
|
||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
||||
|
||||
sinon.assert.calledOnce(view.clearPending);
|
||||
expect(view.state.pending).eql(false);
|
||||
});
|
||||
|
||||
it("should notify the user when the operation failed", function() {
|
||||
var requestCallUrl = sandbox.stub(
|
||||
loop.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
cb("fake error");
|
||||
});
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
it("should update CallUrlResult with the call url", function() {
|
||||
submitForm("foo");
|
||||
|
||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
||||
var urlField = view.getDOMNode().querySelector("input[type='url']");
|
||||
|
||||
sinon.assert.calledOnce(view.notifier.errorL10n);
|
||||
sinon.assert.calledWithExactly(view.notifier.errorL10n,
|
||||
"unable_retrieve_url");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#onCallUrlReceived", function() {
|
||||
var callUrlData;
|
||||
|
||||
beforeEach(function() {
|
||||
callUrlData = {
|
||||
callUrl: "http://call.me/",
|
||||
expiresAt: 1000
|
||||
};
|
||||
});
|
||||
|
||||
it("should update the text field with the call url", function() {
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
view.render();
|
||||
|
||||
view.onCallUrlReceived(callUrlData);
|
||||
|
||||
expect(view.$("#call-url").val()).eql("http://call.me/");
|
||||
expect(urlField.value).eql(callUrlData.call_url);
|
||||
});
|
||||
|
||||
it("should reset all pending notifications", function() {
|
||||
var view = new loop.panel.PanelView({notifier: notifier}).render();
|
||||
submitForm("foo");
|
||||
|
||||
view.onCallUrlReceived(callUrlData);
|
||||
|
||||
sinon.assert.calledOnce(view.notifier.clear);
|
||||
});
|
||||
sinon.assert.calledOnce(view.props.notifier.clear);
|
||||
});
|
||||
|
||||
describe("events", function() {
|
||||
describe("goBack", function() {
|
||||
it("should update the button state");
|
||||
it("should notify the user when the operation failed", function() {
|
||||
fakeClient.requestCallUrl = function(_, cb) {
|
||||
cb("fake error");
|
||||
};
|
||||
|
||||
submitForm("foo");
|
||||
|
||||
sinon.assert.calledOnce(notifier.errorL10n);
|
||||
sinon.assert.calledWithExactly(notifier.errorL10n,
|
||||
"unable_retrieve_url");
|
||||
});
|
||||
|
||||
describe("changeButtonState", function() {
|
||||
it("should do set the disabled state if there is no text");
|
||||
it("should do set the enabled state if there is text");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#render", function() {
|
||||
it("should render a DoNotDisturbView", function() {
|
||||
var renderDnD = sandbox.stub(loop.panel.DoNotDisturbView.prototype,
|
||||
"render");
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
|
||||
view.render();
|
||||
|
||||
sinon.assert.calledOnce(renderDnD);
|
||||
});
|
||||
|
||||
it("should render a ToSView", function() {
|
||||
var renderToS = sandbox.stub(loop.panel.ToSView.prototype, "render");
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
|
||||
view.render();
|
||||
|
||||
sinon.assert.calledOnce(renderToS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loop.panel.ToSView', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
$('#fixtures').append('<div id="#tos-view"></div>');
|
||||
|
||||
});
|
||||
|
||||
// XXX Until it's possible to easily test creation of text,
|
||||
// not doing so. As it stands, the magic in the L10nView
|
||||
// class makes stubbing BaseView.render impractical.
|
||||
|
||||
it("should set the value of the loop.seenToS preference to 'seen'",
|
||||
function() {
|
||||
var ToSView = new loop.panel.ToSView({el: $("#tos-view")});
|
||||
|
||||
ToSView.render();
|
||||
TestUtils.renderIntoDocument(loop.panel.ToSView());
|
||||
|
||||
sinon.assert.calledOnce(navigator.mozLoop.setLoopCharPref);
|
||||
sinon.assert.calledWithExactly(navigator.mozLoop.setLoopCharPref,
|
||||
'seenToS', 'seen');
|
||||
});
|
||||
|
||||
it("should render when the value of loop.seenToS is not set", function() {
|
||||
var renderToS = sandbox.spy(loop.panel.ToSView.prototype, "render");
|
||||
var ToSView = new loop.panel.ToSView({el: $('#tos-view')});
|
||||
|
||||
ToSView.render();
|
||||
|
||||
sinon.assert.calledOnce(renderToS);
|
||||
});
|
||||
|
||||
it("should not render when the value of loop.seenToS is set to 'seen'",
|
||||
it("should not set the value of loop.seenToS when it's already set",
|
||||
function() {
|
||||
var ToSView = new loop.panel.ToSView({el: $('#tos-view')});
|
||||
fakeSeenToSPref = 1;
|
||||
navigator.mozLoop.getLoopCharPref = function() {
|
||||
return "seen";
|
||||
};
|
||||
|
||||
ToSView.render();
|
||||
TestUtils.renderIntoDocument(loop.panel.ToSView());
|
||||
|
||||
sinon.assert.notCalled(navigator.mozLoop.setLoopCharPref);
|
||||
});
|
||||
|
||||
it("should render when the value of loop.seenToS is not set", function() {
|
||||
var view = TestUtils.renderIntoDocument(loop.panel.ToSView());
|
||||
|
||||
TestUtils.findRenderedDOMComponentWithClass(view, "tos");
|
||||
});
|
||||
|
||||
it("should not render when the value of loop.seenToS is set to 'seen'",
|
||||
function(done) {
|
||||
navigator.mozLoop.getLoopCharPref = function() {
|
||||
return "seen";
|
||||
};
|
||||
|
||||
try {
|
||||
TestUtils.findRenderedDOMComponentWithClass(view, "tos");
|
||||
} catch (err) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -69,7 +69,10 @@ describe("loop.shared.router", function() {
|
||||
it("should set the active view", function() {
|
||||
router.loadView(view);
|
||||
|
||||
expect(router._activeView).eql(view);
|
||||
expect(router._activeView).eql({
|
||||
type: "backbone",
|
||||
view: view
|
||||
});
|
||||
});
|
||||
|
||||
it("should load and render the passed view", function() {
|
||||
|
@ -10,7 +10,7 @@ do_not_disturb=Do not disturb
|
||||
|
||||
get_a_call_url=Get a call url
|
||||
new_url=New url
|
||||
caller.placeholder=Identify this call
|
||||
call_identifier_textinput_placeholder=Identify this call
|
||||
|
||||
unable_retrieve_url=Sorry, we were unable to retrieve a call url.
|
||||
|
||||
@ -32,6 +32,6 @@ network_disconnected=The network connection terminated abruptly.
|
||||
connection_error_see_console_notification=Call failed; see console for details.
|
||||
## LOCALIZATION NOTE (legal_text_and_links): In this item, don't translate the
|
||||
## part between {{..}}
|
||||
legal_text_and_links.innerHTML=By using this product you agree to the <a \
|
||||
href="{{terms_of_use_url}}">Terms of Use</a> and <a \
|
||||
legal_text_and_links=By using this product you agree to the <a \
|
||||
target="_blank" href="{{terms_of_use_url}}">Terms of Use</a> and <a \
|
||||
href="{{privacy_notice_url}}">Privacy Notice</a>
|
||||
|
Loading…
Reference in New Issue
Block a user