From bd670479a9e2ec49781b126273fa761f2ab728cd Mon Sep 17 00:00:00 2001 From: Dan Mosedale Date: Wed, 29 Oct 2014 14:10:28 -0700 Subject: [PATCH] Bug 1074674 - add button to copy room location to clipboard, r=NiKo --- browser/components/loop/content/js/panel.js | 28 +++++++- browser/components/loop/content/js/panel.jsx | 28 +++++++- .../loop/content/shared/css/panel.css | 43 ++++++++++++ .../shared/img/svg/checkmark-16x16.svg | 10 +++ .../content/shared/img/svg/copy-16x16.svg | 28 ++++++++ browser/components/loop/jar.mn | 2 + .../loop/test/desktop-local/panel_test.js | 66 +++++++++++++++++++ browser/components/loop/ui/fake-mozLoop.js | 1 + 8 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 browser/components/loop/content/shared/img/svg/checkmark-16x16.svg create mode 100644 browser/components/loop/content/shared/img/svg/copy-16x16.svg diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js index 77e7c9484414..d105c12b8ff3 100644 --- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -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 diff --git a/browser/components/loop/content/js/panel.jsx b/browser/components/loop/content/js/panel.jsx index a6b5a0a8fee4..a9bd028f6bd1 100644 --- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -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 ( -
+

- {room.roomName} + {room.roomName} +

@@ -762,6 +783,7 @@ loop.panel = (function(_, mozL10n) { AvailabilityDropdown: AvailabilityDropdown, CallUrlResult: CallUrlResult, PanelView: PanelView, + RoomEntry: RoomEntry, RoomList: RoomList, SettingsDropdown: SettingsDropdown, ToSView: ToSView diff --git a/browser/components/loop/content/shared/css/panel.css b/browser/components/loop/content/shared/css/panel.css index 9accb77a912e..bd7943ccfa61 100644 --- a/browser/components/loop/content/shared/css/panel.css +++ b/browser/components/loop/content/shared/css/panel.css @@ -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 { diff --git a/browser/components/loop/content/shared/img/svg/checkmark-16x16.svg b/browser/components/loop/content/shared/img/svg/checkmark-16x16.svg new file mode 100644 index 000000000000..2ac67b1da317 --- /dev/null +++ b/browser/components/loop/content/shared/img/svg/checkmark-16x16.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/browser/components/loop/content/shared/img/svg/copy-16x16.svg b/browser/components/loop/content/shared/img/svg/copy-16x16.svg new file mode 100644 index 000000000000..2b68a345bf5b --- /dev/null +++ b/browser/components/loop/content/shared/img/svg/copy-16x16.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + diff --git a/browser/components/loop/jar.mn b/browser/components/loop/jar.mn index d30e5da2b8d9..a7e89b973ab5 100644 --- a/browser/components/loop/jar.mn +++ b/browser/components/loop/jar.mn @@ -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) diff --git a/browser/components/loop/test/desktop-local/panel_test.js b/browser/components/loop/test/desktop-local/panel_test.js index c70504733f74..309ede42fb8b 100644 --- a/browser/components/loop/test/desktop-local/panel_test.js +++ b/browser/components/loop/test/desktop-local/panel_test.js @@ -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; diff --git a/browser/components/loop/ui/fake-mozLoop.js b/browser/components/loop/ui/fake-mozLoop.js index b212a00e84e7..2674ee8e512d 100644 --- a/browser/components/loop/ui/fake-mozLoop.js +++ b/browser/components/loop/ui/fake-mozLoop.js @@ -57,6 +57,7 @@ navigator.mozLoop = { } }, releaseCallData: function() {}, + copyString: function() {}, contacts: { getAll: function(callback) { callback(null, []);