diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 1d9c011c2d25..406bb2be1dfb 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1700,9 +1700,9 @@ pref("loop.debug.websocket", false); pref("loop.debug.sdk", false); pref("loop.debug.twoWayMediaTelemetry", false); #ifdef DEBUG -pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: https://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:"); +pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src *; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:"); #else -pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: https://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:"); +pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src *; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:"); #endif pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto"); pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds"); diff --git a/browser/components/loop/MozLoopAPI.jsm b/browser/components/loop/MozLoopAPI.jsm index 67d538ae45b6..68a8f03b39be 100644 --- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -21,6 +21,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage", "resource:///modules/loop/LoopStorage.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose", "resource://gre/modules/MozSocialAPI.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata", + "resource://gre/modules/PageMetadata.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "UITour", @@ -844,6 +846,24 @@ function injectLoopAPI(targetWindow) { } }, + /** + * Gets the metadata related to the currently selected tab in + * the most recent window. + * + * @param {Function} A callback that is passed the metadata. + */ + getSelectedTabMetadata: { + value: function(callback) { + let win = Services.wm.getMostRecentWindow("navigator:browser"); + win.messageManager.addMessageListener("PageMetadata:PageDataResult", function onPageDataResult(msg) { + win.messageManager.removeMessageListener("PageMetadata:PageDataResult", onPageDataResult); + let pageData = msg.json; + callback(cloneValueInto(pageData, targetWindow)); + }); + win.gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData"); + } + }, + /** * Associates a session-id and a call-id with a window for debugging. * diff --git a/browser/components/loop/content/css/panel.css b/browser/components/loop/content/css/panel.css index 7c8c0dd35a58..c42c72a6adb7 100644 --- a/browser/components/loop/content/css/panel.css +++ b/browser/components/loop/content/css/panel.css @@ -170,25 +170,71 @@ body { /* Rooms */ .rooms { min-height: 100px; + padding: 0 1rem; } .rooms > h1 { font-weight: bold; color: #999; - padding: .5rem 1rem; -} - -.rooms > p { padding: .5rem 0; - margin: 0; } -.rooms > p > .btn { +.rooms > div > .context { + margin: .5rem 0 0; + background-color: #DEEFF7; + border-radius: 3px 3px 0 0; + padding: .5rem; +} + +.rooms > div > .context > .context-enabled { + margin-bottom: .5rem; + display: block; +} + +.rooms > div > .context > .context-enabled > input { + -moz-margin-start: 0; +} + +.rooms > div > .context > .context-preview { + float: right; + width: 100px; + max-height: 200px; + -moz-margin-start: 10px; + margin-bottom: 10px; +} + +body[dir=rtl] .rooms > div > .context > .context-preview { + float: left; +} + +.rooms > div > .context > .context-preview[src=""] { + display: none; +} + +.rooms > div > .context > .context-description { + display: block; + color: #707070; +} + +.rooms > div > .context > .context-url { + display: block; + color: #59A1D7; + clear: both; +} + +.rooms > div > .btn { display: block; font-size: 1rem; - margin: 0 auto; + margin: 0 auto .5rem; + width: 100%; padding: .5rem 1rem; + border-radius: 0 0 3px 3px; +} + +/* Remove when bug 1142671 is backed out. */ +.rooms > div > :not(.context) + .btn { border-radius: 3px; + margin-top: 0.5rem; } .room-list { @@ -197,6 +243,8 @@ body { overflow: auto; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; + margin-left: -1rem; + margin-right: -1rem; } .room-list:empty { diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js index 6005a20b7d35..22711093142a 100644 --- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -594,6 +594,7 @@ loop.panel = (function(_, mozL10n) { mixins: [Backbone.Events, sharedMixins.WindowCloseMixin], propTypes: { + mozLoop: React.PropTypes.object.isRequired, store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, userDisplayName: React.PropTypes.string.isRequired // for room creation @@ -666,7 +667,8 @@ loop.panel = (function(_, mozL10n) { ); }, this) ), - React.createElement("p", null, + React.createElement("div", null, + React.createElement(ContextInfo, {mozLoop: this.props.mozLoop}), React.createElement("button", {className: "btn btn-info new-room-button", onClick: this.handleCreateButtonClick, disabled: this._hasPendingOperation()}, @@ -678,6 +680,60 @@ loop.panel = (function(_, mozL10n) { } }); + /** + * Context info that is offered to be part of a Room. + */ + var ContextInfo = React.createClass({displayName: "ContextInfo", + propTypes: { + mozLoop: React.PropTypes.object.isRequired, + }, + + mixins: [sharedMixins.DocumentVisibilityMixin], + + getInitialState: function() { + return { + previewImage: "", + description: "", + url: "" + }; + }, + + onDocumentVisible: function() { + this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) { + var previewImage = metadata.previews.length ? metadata.previews[0] : ""; + var description = metadata.description || metadata.title; + var url = metadata.url; + this.setState({previewImage: previewImage, + description: description, + url: url}); + }.bind(this)); + }, + + onDocumentHidden: function() { + this.setState({previewImage: "", + description: "", + url: ""}); + }, + + render: function() { + if (!this.props.mozLoop.getLoopPref("contextInConverations.enabled") || + !this.state.url) { + return null; + } + return ( + React.createElement("div", {className: "context"}, + React.createElement("label", {className: "context-enabled"}, + React.createElement("input", {type: "checkbox"}), + mozL10n.get("context_offer_label") + ), + React.createElement("img", {className: "context-preview", src: this.state.previewImage}), + React.createElement("span", {className: "context-description"}, this.state.description), + React.createElement("span", {className: "context-url"}, this.state.url) + ) + ); + } + }); + /** * Panel view. */ @@ -819,7 +875,8 @@ loop.panel = (function(_, mozL10n) { React.createElement(Tab, {name: "rooms"}, React.createElement(RoomList, {dispatcher: this.props.dispatcher, store: this.props.roomStore, - userDisplayName: this._getUserDisplayName()}), + userDisplayName: this._getUserDisplayName(), + mozLoop: this.props.mozLoop}), React.createElement(ToSView, null) ), React.createElement(Tab, {name: "contacts"}, @@ -890,6 +947,7 @@ loop.panel = (function(_, mozL10n) { init: init, AuthLink: AuthLink, AvailabilityDropdown: AvailabilityDropdown, + ContextInfo: ContextInfo, GettingStartedView: GettingStartedView, PanelView: PanelView, RoomEntry: RoomEntry, diff --git a/browser/components/loop/content/js/panel.jsx b/browser/components/loop/content/js/panel.jsx index 171544bae78e..ca0c600177a4 100644 --- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -594,6 +594,7 @@ loop.panel = (function(_, mozL10n) { mixins: [Backbone.Events, sharedMixins.WindowCloseMixin], propTypes: { + mozLoop: React.PropTypes.object.isRequired, store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, userDisplayName: React.PropTypes.string.isRequired // for room creation @@ -666,13 +667,68 @@ loop.panel = (function(_, mozL10n) { />; }, this) } -

+

+ -

+
+ + ); + } + }); + + /** + * Context info that is offered to be part of a Room. + */ + var ContextInfo = React.createClass({ + propTypes: { + mozLoop: React.PropTypes.object.isRequired, + }, + + mixins: [sharedMixins.DocumentVisibilityMixin], + + getInitialState: function() { + return { + previewImage: "", + description: "", + url: "" + }; + }, + + onDocumentVisible: function() { + this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) { + var previewImage = metadata.previews.length ? metadata.previews[0] : ""; + var description = metadata.description || metadata.title; + var url = metadata.url; + this.setState({previewImage: previewImage, + description: description, + url: url}); + }.bind(this)); + }, + + onDocumentHidden: function() { + this.setState({previewImage: "", + description: "", + url: ""}); + }, + + render: function() { + if (!this.props.mozLoop.getLoopPref("contextInConverations.enabled") || + !this.state.url) { + return null; + } + return ( +
+ + + {this.state.description} + {this.state.url}
); } @@ -819,7 +875,8 @@ loop.panel = (function(_, mozL10n) { + userDisplayName={this._getUserDisplayName()} + mozLoop={this.props.mozLoop}/> @@ -890,6 +947,7 @@ loop.panel = (function(_, mozL10n) { init: init, AuthLink: AuthLink, AvailabilityDropdown: AvailabilityDropdown, + ContextInfo: ContextInfo, GettingStartedView: GettingStartedView, PanelView: PanelView, RoomEntry: RoomEntry, diff --git a/browser/components/loop/test/desktop-local/panel_test.js b/browser/components/loop/test/desktop-local/panel_test.js index c81daa313e2e..cd25edd8074c 100644 --- a/browser/components/loop/test/desktop-local/panel_test.js +++ b/browser/components/loop/test/desktop-local/panel_test.js @@ -651,7 +651,8 @@ describe("loop.panel", function() { React.createElement(loop.panel.RoomList, { store: roomStore, dispatcher: dispatcher, - userDisplayName: fakeEmail + userDisplayName: fakeEmail, + mozLoop: fakeMozLoop })); } @@ -708,6 +709,49 @@ describe("loop.panel", function() { var buttonNode = view.getDOMNode().querySelector("button[disabled]"); expect(buttonNode).to.not.equal(null); }); + + it("should show context information when a URL is available", + function() { + navigator.mozLoop.getLoopPref = function() { + return true; + } + + var view = TestUtils.renderIntoDocument( + React.createElement(loop.panel.ContextInfo, { + mozLoop: navigator.mozLoop + }) + ); + view.setState({ + previews: [""], + description: "fake description", + url: "https://www.example.com" + }); + + var contextEnabledCheckbox = view.getDOMNode().querySelector(".context-enabled"); + expect(contextEnabledCheckbox).to.not.equal(null); + }); + + it("should not show context information when a URL is unavailable", + function() { + navigator.mozLoop.getLoopPref = function() { + return true; + } + + var view = TestUtils.renderIntoDocument( + React.createElement(loop.panel.ContextInfo, { + mozLoop: navigator.mozLoop + }) + ); + view.setState({ + previews: [""], + description: "fake description", + url: "" + }); + + var contextInfo = view.getDOMNode(); + expect(contextInfo).to.equal(null); + }); + }); describe('loop.panel.ToSView', function() { diff --git a/browser/components/loop/ui/fake-mozLoop.js b/browser/components/loop/ui/fake-mozLoop.js index da13f90d5f72..2a52b24a16d9 100644 --- a/browser/components/loop/ui/fake-mozLoop.js +++ b/browser/components/loop/ui/fake-mozLoop.js @@ -115,6 +115,7 @@ navigator.mozLoop = { // Ensure we skip FTE completely. case "gettingStarted.seen": case "contacts.gravatars.promo": + case "contextInConverations.enabled": return true; case "contacts.gravatars.show": return false; @@ -127,6 +128,13 @@ navigator.mozLoop = { return "http://www.gravatar.com/avatar/" + (Math.ceil(Math.random() * 3) === 2 ? "0a996f0fe2727ef1668bdb11897e4459" : "foo") + ".jpg?default=blank&s=40"; }, + getSelectedTabMetadata: function(callback) { + callback({ + previews: ["chrome://branding/content/about-logo.png"], + description: "sample webpage description", + url: "https://www.example.com" + }); + }, contacts: { getAll: function(callback) { callback(null, [].concat(fakeContacts)); diff --git a/browser/components/loop/ui/ui-showcase.js b/browser/components/loop/ui/ui-showcase.js index 488a0d4a5e4a..5cc7f1b8a6a7 100644 --- a/browser/components/loop/ui/ui-showcase.js +++ b/browser/components/loop/ui/ui-showcase.js @@ -56,6 +56,28 @@ function noop(){} + // We save the visibility change listeners so that we can fake an event + // to the panel once we've loaded all the views. + var visibilityListeners = []; + var rootObject = window; + + rootObject.document.addEventListener = function(eventName, func) { + if (eventName === "visibilitychange") { + visibilityListeners.push(func); + } + window.addEventListener(eventName, func); + }; + + rootObject.document.removeEventListener = function(eventName, func) { + if (eventName === "visibilitychange") { + var index = visibilityListeners.indexOf(func); + visibilityListeners.splice(index, 1); + } + window.removeEventListener(eventName, func); + }; + + loop.shared.mixins.setRootObject(rootObject); + // Feedback API client configured to send data to the stage input server, // which is available at https://input.allizom.org var stageFeedbackApiClient = new loop.FeedbackAPIClient( @@ -757,6 +779,10 @@ window.addEventListener("DOMContentLoaded", function() { try { React.renderComponent(React.createElement(App, null), document.getElementById("main")); + + for (var listener of visibilityListeners) { + listener({target: {hidden: false}}); + } } catch(err) { console.error(err); uncaughtError = err; diff --git a/browser/components/loop/ui/ui-showcase.jsx b/browser/components/loop/ui/ui-showcase.jsx index 13754c5824d5..3f355d8e0843 100644 --- a/browser/components/loop/ui/ui-showcase.jsx +++ b/browser/components/loop/ui/ui-showcase.jsx @@ -56,6 +56,28 @@ function noop(){} + // We save the visibility change listeners so that we can fake an event + // to the panel once we've loaded all the views. + var visibilityListeners = []; + var rootObject = window; + + rootObject.document.addEventListener = function(eventName, func) { + if (eventName === "visibilitychange") { + visibilityListeners.push(func); + } + window.addEventListener(eventName, func); + }; + + rootObject.document.removeEventListener = function(eventName, func) { + if (eventName === "visibilitychange") { + var index = visibilityListeners.indexOf(func); + visibilityListeners.splice(index, 1); + } + window.removeEventListener(eventName, func); + }; + + loop.shared.mixins.setRootObject(rootObject); + // Feedback API client configured to send data to the stage input server, // which is available at https://input.allizom.org var stageFeedbackApiClient = new loop.FeedbackAPIClient( @@ -757,6 +779,10 @@ window.addEventListener("DOMContentLoaded", function() { try { React.renderComponent(, document.getElementById("main")); + + for (var listener of visibilityListeners) { + listener({target: {hidden: false}}); + } } catch(err) { console.error(err); uncaughtError = err; diff --git a/browser/locales/en-US/chrome/browser/loop/loop.properties b/browser/locales/en-US/chrome/browser/loop/loop.properties index e3e6201d5e2a..c26eb6a077c8 100644 --- a/browser/locales/en-US/chrome/browser/loop/loop.properties +++ b/browser/locales/en-US/chrome/browser/loop/loop.properties @@ -331,3 +331,6 @@ infobar_button_gotit_label=Got it! infobar_button_gotit_accesskey=G infobar_menuitem_dontshowagain_label=Don't show this again infobar_menuitem_dontshowagain_accesskey=D + +# Context in conversation strings +context_offer_label=Let's talk about this page