Bug 1000771: Add a button to copy Loop urls to the clipboard. r=mikedeboer

This commit is contained in:
Nicolas Perriault 2014-08-07 15:45:08 +01:00
parent 93c7dc5d0e
commit eb4a9c2169
8 changed files with 146 additions and 50 deletions

View File

@ -11,8 +11,10 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
"resource://gre/modules/MozSocialAPI.jsm");
"resource://gre/modules/MozSocialAPI.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper");
this.EXPORTED_SYMBOLS = ["injectLoopAPI"];
/**
@ -225,6 +227,19 @@ function injectLoopAPI(targetWindow) {
});
}
},
/**
* Copies passed string onto the system clipboard.
*
* @param {String} str The string to copy
*/
copyString: {
enumerable: true,
writable: true,
value: function(str) {
clipboardHelper.copyString(str);
}
}
};
let contentObj = Cu.createObjectIn(targetWindow);

View File

@ -152,11 +152,17 @@ loop.panel = (function(_, mozL10n) {
});
var CallUrlResult = React.createClass({displayName: 'CallUrlResult',
propTypes: {
callUrl: React.PropTypes.string,
notifier: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
pending: false,
callUrl: ''
copied: false,
callUrl: this.props.callUrl || ""
};
},
@ -184,7 +190,7 @@ loop.panel = (function(_, mozL10n) {
if (err) {
this.props.notifier.errorL10n("unable_retrieve_url");
this.setState({pending: false});
this.setState(this.getInitialState());
} else {
try {
var callUrl = new window.URL(callUrlData.callUrl);
@ -194,43 +200,57 @@ loop.panel = (function(_, mozL10n) {
callUrl.pathname.split('/').pop();
navigator.mozLoop.setLoopCharPref('loopToken', token);
this.setState({pending: false, callUrl: callUrl.href});
this.setState({pending: false, copied: false, callUrl: callUrl.href});
} catch(e) {
console.log(e);
this.props.notifier.errorL10n("unable_retrieve_url");
this.setState({pending: false});
this.setState(this.getInitialState());
}
}
},
_generateMailto: function() {
_generateMailTo: function() {
return encodeURI([
"mailto:?subject=" + __("share_email_subject") + "&",
"body=" + __("share_email_body", {callUrl: this.state.callUrl})
].join(""));
},
handleEmailButtonClick: function(event) {
// Note: side effect
document.location = event.target.dataset.mailto;
},
handleCopyButtonClick: function(event) {
// XXX the mozLoop object should be passed as a prop, to ease testing and
// using a fake implementation in UI components showcase.
navigator.mozLoop.copyString(this.state.callUrl);
this.setState({copied: true});
},
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;
var inputCSSClass = {
"pending": this.state.pending,
"callUrl": !this.state.pending
};
return (
PanelLayout({summary: __("share_link_header_text")},
React.DOM.div({className: "invite"},
React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true",
className: cx(inputCSSClass)}),
React.DOM.a({className: cx({btn: true, hide: !this.state.callUrl}),
href: this._generateMailto()},
React.DOM.span(null,
__("share_button")
)
)
className: cx({pending: this.state.pending})}),
React.DOM.p({className: "button-group url-actions"},
React.DOM.button({className: "btn btn-email", disabled: !this.state.callUrl,
onClick: this.handleEmailButtonClick,
'data-mailto': this._generateMailTo()},
__("share_button")
),
React.DOM.button({className: "btn btn-copy", disabled: !this.state.callUrl,
onClick: this.handleCopyButtonClick},
this.state.copied ? __("copied_url_button") :
__("copy_url_button")
)
)
)
)
);
@ -243,14 +263,17 @@ loop.panel = (function(_, mozL10n) {
var PanelView = React.createClass({displayName: 'PanelView',
propTypes: {
notifier: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
client: React.PropTypes.object.isRequired,
// Mostly used for UI components showcase and unit tests
callUrl: React.PropTypes.string
},
render: function() {
return (
React.DOM.div(null,
CallUrlResult({client: this.props.client,
notifier: this.props.notifier}),
notifier: this.props.notifier,
callUrl: this.props.callUrl}),
ToSView(null),
AvailabilityDropdown(null)
)

View File

@ -152,11 +152,17 @@ loop.panel = (function(_, mozL10n) {
});
var CallUrlResult = React.createClass({
propTypes: {
callUrl: React.PropTypes.string,
notifier: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
pending: false,
callUrl: ''
copied: false,
callUrl: this.props.callUrl || ""
};
},
@ -184,7 +190,7 @@ loop.panel = (function(_, mozL10n) {
if (err) {
this.props.notifier.errorL10n("unable_retrieve_url");
this.setState({pending: false});
this.setState(this.getInitialState());
} else {
try {
var callUrl = new window.URL(callUrlData.callUrl);
@ -194,43 +200,57 @@ loop.panel = (function(_, mozL10n) {
callUrl.pathname.split('/').pop();
navigator.mozLoop.setLoopCharPref('loopToken', token);
this.setState({pending: false, callUrl: callUrl.href});
this.setState({pending: false, copied: false, callUrl: callUrl.href});
} catch(e) {
console.log(e);
this.props.notifier.errorL10n("unable_retrieve_url");
this.setState({pending: false});
this.setState(this.getInitialState());
}
}
},
_generateMailto: function() {
_generateMailTo: function() {
return encodeURI([
"mailto:?subject=" + __("share_email_subject") + "&",
"body=" + __("share_email_body", {callUrl: this.state.callUrl})
].join(""));
},
handleEmailButtonClick: function(event) {
// Note: side effect
document.location = event.target.dataset.mailto;
},
handleCopyButtonClick: function(event) {
// XXX the mozLoop object should be passed as a prop, to ease testing and
// using a fake implementation in UI components showcase.
navigator.mozLoop.copyString(this.state.callUrl);
this.setState({copied: true});
},
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;
var inputCSSClass = {
"pending": this.state.pending,
"callUrl": !this.state.pending
};
return (
<PanelLayout summary={__("share_link_header_text")}>
<div className="invite">
<input type="url" value={this.state.callUrl} readOnly="true"
className={cx(inputCSSClass)} />
<a className={cx({btn: true, hide: !this.state.callUrl})}
href={this._generateMailto()}>
<span>
{__("share_button")}
</span>
</a>
className={cx({pending: this.state.pending})} />
<p className="button-group url-actions">
<button className="btn btn-email" disabled={!this.state.callUrl}
onClick={this.handleEmailButtonClick}
data-mailto={this._generateMailTo()}>
{__("share_button")}
</button>
<button className="btn btn-copy" disabled={!this.state.callUrl}
onClick={this.handleCopyButtonClick}>
{this.state.copied ? __("copied_url_button") :
__("copy_url_button")}
</button>
</p>
</div>
</PanelLayout>
);
@ -243,14 +263,17 @@ loop.panel = (function(_, mozL10n) {
var PanelView = React.createClass({
propTypes: {
notifier: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
client: React.PropTypes.object.isRequired,
// Mostly used for UI components showcase and unit tests
callUrl: React.PropTypes.string
},
render: function() {
return (
<div>
<CallUrlResult client={this.props.client}
notifier={this.props.notifier} />
notifier={this.props.notifier}
callUrl={this.props.callUrl} />
<ToSView />
<AvailabilityDropdown />
</div>

View File

@ -86,15 +86,17 @@
margin-top: 10px;
}
.share .action .btn:hover {
.share > .action .btn:hover {
background-color: #008ACB;
border: 1px solid #008ACB;
}
.share .action .btn span {
margin-top: 2px;
font-size: 12px;
display: inline-block;
.share > .action > .invite > .url-actions {
margin: 0 0 5px;
}
.share > .action > .invite > .url-actions > .btn:first-child {
-moz-margin-end: 1em;
}
/* Specific cases */

View File

@ -48,7 +48,8 @@ describe("loop.panel", function() {
return "en-US";
},
setLoopCharPref: sandbox.stub(),
getLoopCharPref: sandbox.stub().returns("unseen")
getLoopCharPref: sandbox.stub().returns("unseen"),
copyString: sandbox.stub()
};
document.mozL10n.initialize(navigator.mozLoop);
@ -314,9 +315,28 @@ describe("loop.panel", function() {
}));
view.setState({pending: false, callUrl: "http://example.com"});
TestUtils.findRenderedDOMComponentWithTag(view, "a");
var shareButton = view.getDOMNode().querySelector("a.btn");
expect(shareButton.href).to.equal(encodeURI(mailto));
TestUtils.findRenderedDOMComponentWithClass(view, "btn-email");
expect(view.getDOMNode().querySelector(".btn-email").dataset.mailto)
.to.equal(encodeURI(mailto));
});
it("should feature a copy button capable of copying the call url when clicked", function() {
fakeClient.requestCallUrl = sandbox.stub();
var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
notifier: notifier,
client: fakeClient
}));
view.setState({
pending: false,
copied: false,
callUrl: "http://example.com"
});
TestUtils.Simulate.click(view.getDOMNode().querySelector(".btn-copy"));
sinon.assert.calledOnce(navigator.mozLoop.copyString);
sinon.assert.calledWithExactly(navigator.mozLoop.copyString,
view.state.callUrl);
});
it("should notify the user when the operation failed", function() {

View File

@ -93,8 +93,14 @@
return (
ShowCase(null,
Section({name: "PanelView"},
Example({summary: "332px wide", dashed: "true", style: {width: "332px"}},
React.DOM.p({className: "note"},
React.DOM.strong(null, "Note:"), " 332px wide."
),
Example({summary: "Pending call url retrieval", dashed: "true", style: {width: "332px"}},
PanelView(null)
),
Example({summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}},
PanelView({callUrl: "http://invalid.example.url/"})
)
),

View File

@ -93,9 +93,15 @@
return (
<ShowCase>
<Section name="PanelView">
<Example summary="332px wide" dashed="true" style={{width: "332px"}}>
<p className="note">
<strong>Note:</strong> 332px wide.
</p>
<Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}>
<PanelView />
</Example>
<Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}>
<PanelView callUrl="http://invalid.example.url/" />
</Example>
</Section>
<Section name="IncomingCallView">

View File

@ -62,4 +62,5 @@ feedback_window_will_close_in=This window will close in {{countdown}} seconds
share_email_subject=Loop invitation to chat
share_email_body=Please click that link to call me back:\r\n\r\n{{callUrl}}
share_button=Email
copy_url_button=Copy
copied_url_button=Copied!