mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-01 17:23:59 +00:00
Bug 1000771: Add a button to copy Loop urls to the clipboard. r=mikedeboer
This commit is contained in:
parent
93c7dc5d0e
commit
eb4a9c2169
@ -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);
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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 */
|
||||
|
@ -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() {
|
||||
|
@ -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/"})
|
||||
)
|
||||
),
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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!
|
||||
|
Loading…
Reference in New Issue
Block a user