Bug 1074674 - add button to copy room location to clipboard, r=NiKo

This commit is contained in:
Dan Mosedale 2014-10-29 14:10:28 -07:00
parent ba7411aaf5
commit 30ec52feee
8 changed files with 200 additions and 6 deletions

View File

@ -471,8 +471,13 @@ loop.panel = (function(_, mozL10n) {
room: React.PropTypes.instanceOf(loop.store.Room).isRequired
},
getInitialState: function() {
return { urlCopied: false };
},
shouldComponentUpdate: function(nextProps, nextState) {
return nextProps.room.ctime > this.props.room.ctime;
return (nextProps.room.ctime > this.props.room.ctime) ||
(nextState.urlCopied !== this.state.urlCopied);
},
handleClickRoom: function(event) {
@ -480,6 +485,16 @@ loop.panel = (function(_, mozL10n) {
this.props.openRoom(this.props.room);
},
handleCopyButtonClick: function(event) {
event.preventDefault();
navigator.mozLoop.copyString(this.props.room.roomUrl);
this.setState({urlCopied: true});
},
handleMouseLeave: function(event) {
this.setState({urlCopied: false});
},
_isActive: function() {
// XXX bug 1074679 will implement this properly
return this.props.room.currSize > 0;
@ -491,12 +506,18 @@ loop.panel = (function(_, mozL10n) {
"room-entry": true,
"room-active": this._isActive()
});
var copyButtonClasses = React.addons.classSet({
'copy-link': true,
'checked': this.state.urlCopied
});
return (
React.DOM.div({className: roomClasses},
React.DOM.div({className: roomClasses, onMouseLeave: this.handleMouseLeave},
React.DOM.h2(null,
React.DOM.span({className: "room-notification"}),
room.roomName
room.roomName,
React.DOM.button({className: copyButtonClasses,
onClick: this.handleCopyButtonClick})
),
React.DOM.p(null,
React.DOM.a({ref: "room", href: "#", onClick: this.handleClickRoom},
@ -762,6 +783,7 @@ loop.panel = (function(_, mozL10n) {
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
PanelView: PanelView,
RoomEntry: RoomEntry,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView

View File

@ -471,8 +471,13 @@ loop.panel = (function(_, mozL10n) {
room: React.PropTypes.instanceOf(loop.store.Room).isRequired
},
getInitialState: function() {
return { urlCopied: false };
},
shouldComponentUpdate: function(nextProps, nextState) {
return nextProps.room.ctime > this.props.room.ctime;
return (nextProps.room.ctime > this.props.room.ctime) ||
(nextState.urlCopied !== this.state.urlCopied);
},
handleClickRoom: function(event) {
@ -480,6 +485,16 @@ loop.panel = (function(_, mozL10n) {
this.props.openRoom(this.props.room);
},
handleCopyButtonClick: function(event) {
event.preventDefault();
navigator.mozLoop.copyString(this.props.room.roomUrl);
this.setState({urlCopied: true});
},
handleMouseLeave: function(event) {
this.setState({urlCopied: false});
},
_isActive: function() {
// XXX bug 1074679 will implement this properly
return this.props.room.currSize > 0;
@ -491,12 +506,18 @@ loop.panel = (function(_, mozL10n) {
"room-entry": true,
"room-active": this._isActive()
});
var copyButtonClasses = React.addons.classSet({
'copy-link': true,
'checked': this.state.urlCopied
});
return (
<div className={roomClasses}>
<div className={roomClasses} onMouseLeave={this.handleMouseLeave}>
<h2>
<span className="room-notification" />
{room.roomName}
{room.roomName}
<button className={copyButtonClasses}
onClick={this.handleCopyButtonClick}/>
</h2>
<p>
<a ref="room" href="#" onClick={this.handleClickRoom}>
@ -762,6 +783,7 @@ loop.panel = (function(_, mozL10n) {
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
PanelView: PanelView,
RoomEntry: RoomEntry,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView

View File

@ -202,6 +202,49 @@ body {
text-decoration: underline;
}
.room-list > .room-entry > h2 > .copy-link {
display: inline-block;
width: 24px;
height: 24px;
border: none;
margin: .1em .5em; /* relative to _this_ line's font, not the document's */
background-color: transparent; /* override browser default for button tags */
}
@keyframes drop-and-fade-in {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 100; transform: translateY(0); }
}
.room-list > .room-entry:hover > h2 > .copy-link {
background: transparent url(../img/svg/copy-16x16.svg);
cursor: pointer;
animation: drop-and-fade-in 0.4s;
animation-fill-mode: forwards;
}
/* scale this up to 1.1x and then back to the original size */
@keyframes pulse {
0%, 100% { transform: scale(1.0); }
50% { transform: scale(1.1); }
}
.room-list > .room-entry > h2 > .copy-link.checked {
background: transparent url(../img/svg/checkmark-16x16.svg);
animation: pulse .250s;
animation-timing-function: ease-in-out;
}
.room-list > .room-entry > h2 {
display: inline-block;
}
/* keep the various room-entry row pieces aligned with each other */
.room-list > .room-entry > h2 > button,
.room-list > .room-entry > h2 > span {
vertical-align: middle;
}
/* Buttons */
.button-group {

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<circle fill-rule="evenodd" clip-rule="evenodd" fill="#0096DD" cx="8"
cy="8" r="8"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF"
d="M7.236,12L12,5.007L10.956,4L7.224,9.465l-2.14-2.326L4,8.146L7.236,12z"/>
</svg>

After

Width:  |  Height:  |  Size: 597 B

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<circle fill-rule="evenodd" clip-rule="evenodd" fill="#0096DD" cx="8" cy="8"
r="8"/>
<g>
<g>
<g>
<path fill-rule="evenodd" clip-rule="evenodd" fill="none"
stroke="#FFFFFF" stroke-width="0.75" stroke-miterlimit="10"
d="M10.815,6.286H7.556c-0.164,0-0.296,0.128-0.296,0.286v5.143C7.259,11.872,7.392,12,7.556,12h4.148
C11.867,12,12,11.872,12,11.714V7.429L10.815,6.286z
M8.741,6.275V5.143L7.556,4H7.528C6.509,4,4.593,4,4.593,4H4.296
C4.133,4,4,4.128,4,4.286v5.143c0,0.158,0.133,0.286,0.296,0.286H7.25V6.561c0-0.158,0.133-0.286,0.296-0.286H8.741z"/>
</g>
</g>
<g>
<polygon fill-rule="evenodd" clip-rule="evenodd"
fill="#FFFFFF" points="10.222,8 10.222,6.857 11.407,8"/>
</g>
<g>
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF"
points="6.963,5.714 6.963,4.571 8.148,5.714"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -48,6 +48,8 @@ browser.jar:
content/browser/loop/shared/img/svg/glyph-account-16x16.svg (content/shared/img/svg/glyph-account-16x16.svg)
content/browser/loop/shared/img/svg/glyph-signin-16x16.svg (content/shared/img/svg/glyph-signin-16x16.svg)
content/browser/loop/shared/img/svg/glyph-signout-16x16.svg (content/shared/img/svg/glyph-signout-16x16.svg)
content/browser/loop/shared/img/svg/copy-16x16.svg (content/shared/img/svg/copy-16x16.svg)
content/browser/loop/shared/img/svg/checkmark-16x16.svg (content/shared/img/svg/checkmark-16x16.svg)
content/browser/loop/shared/img/audio-call-avatar.svg (content/shared/img/audio-call-avatar.svg)
content/browser/loop/shared/img/beta-ribbon.svg (content/shared/img/beta-ribbon.svg)
content/browser/loop/shared/img/icons-10x10.svg (content/shared/img/icons-10x10.svg)

View File

@ -637,6 +637,72 @@ describe("loop.panel", function() {
});
});
describe("loop.panel.RoomEntry", function() {
var buttonNode, roomData, roomEntry, roomStore, dispatcher;
beforeEach(function() {
dispatcher = new loop.Dispatcher();
roomData = {
roomToken: "QzBbvGmIZWU",
roomUrl: "http://sample/QzBbvGmIZWU",
roomName: "Second Room Name",
maxSize: 2,
participants: [
{ displayName: "Alexis", account: "alexis@example.com",
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" },
{ displayName: "Adam",
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" }
],
ctime: 1405517418
};
roomStore = new loop.store.Room(roomData);
roomEntry = mountRoomEntry();
buttonNode = roomEntry.getDOMNode().querySelector("button.copy-link");
});
function mountRoomEntry() {
return TestUtils.renderIntoDocument(loop.panel.RoomEntry({
openRoom: sandbox.stub(),
room: roomStore
}));
}
it("should not display copy-link button by default", function() {
expect(buttonNode).to.not.equal(null);
});
it("should copy the URL when the click event fires", function() {
TestUtils.Simulate.click(buttonNode);
sinon.assert.calledOnce(navigator.mozLoop.copyString);
sinon.assert.calledWithExactly(navigator.mozLoop.copyString,
roomData.roomUrl);
});
it("should set state.urlCopied when the click event fires", function() {
TestUtils.Simulate.click(buttonNode);
expect(roomEntry.state.urlCopied).to.equal(true);
});
it("should switch to displaying a check icon when the URL has been copied",
function() {
TestUtils.Simulate.click(buttonNode);
expect(buttonNode.classList.contains("checked")).eql(true);
});
it("should not display a check icon after mouse leaves the entry",
function() {
var roomNode = roomEntry.getDOMNode();
TestUtils.Simulate.click(buttonNode);
TestUtils.SimulateNative.mouseOut(roomNode);
expect(buttonNode.classList.contains("checked")).eql(false);
});
});
describe("loop.panel.RoomList", function() {
var roomListStore, dispatcher;

View File

@ -57,6 +57,7 @@ navigator.mozLoop = {
}
},
releaseCallData: function() {},
copyString: function() {},
contacts: {
getAll: function(callback) {
callback(null, []);