Bug 1142687 - Show context information for current page in the rooms view. r=standard8
☠☠ backed out by 6e463e53546b ☠ ☠
authorJared Wein <jwein@mozilla.com>
Tue, 24 Mar 2015 12:43:49 -0400
changeset 264261 b192e6e16c1bcd6add690997d89fb4c753281a69
parent 264260 15403c96b0d47275d3f4c3a0fd020ca7c909e385
child 264262 0994ae599f13a81f5159d620e09a142de0d6a4f0
push id4718
push userraliiev@mozilla.com
push dateMon, 11 May 2015 18:39:53 +0000
treeherdermozilla-beta@c20c4ef55f08 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersstandard8
bugs1142687
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1142687 - Show context information for current page in the rooms view. r=standard8
browser/app/profile/firefox.js
browser/components/loop/MozLoopAPI.jsm
browser/components/loop/content/css/panel.css
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/test/desktop-local/panel_test.js
browser/components/loop/ui/fake-mozLoop.js
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
browser/locales/en-US/chrome/browser/loop/loop.properties
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1695,19 +1695,19 @@ pref("loop.ping.timeout", 10000);
 pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
 pref("loop.feedback.product", "Loop");
 pref("loop.debug.loglevel", "Error");
 pref("loop.debug.dispatcher", false);
 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");
 pref("loop.fxa_oauth.tokendata", "");
 pref("loop.fxa_oauth.profile", "");
 pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc");
 pref("loop.contacts.gravatars.show", false);
 pref("loop.contacts.gravatars.promo", true);
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -16,16 +16,18 @@ Cu.import("resource:///modules/loop/Loop
 Cu.importGlobalProperties(["Blob"]);
 
 XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
                                         "resource:///modules/loop/LoopContacts.jsm");
 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",
                                         "resource:///modules/UITour.jsm");
 XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
   return Cc["@mozilla.org/xre/app-info;1"]
            .getService(Ci.nsIXULAppInfo)
            .QueryInterface(Ci.nsIXULRuntime);
@@ -840,16 +842,29 @@ function injectLoopAPI(targetWindow) {
         let md5Email = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
 
         // Compose the Gravatar URL.
         return "https://www.gravatar.com/avatar/" + md5Email + ".jpg?default=blank&s=" + size;
       }
     },
 
     /**
+     * Gets a metadata object that is related to the currently selected tab in
+     * the most recent window.
+     *
+     * @return {object} An object containing information scraped from the page.
+     */
+    getSelectedTabMetadata: {
+      value: function() {
+        let win = Services.wm.getMostRecentWindow("navigator:browser");
+        return win && cloneValueInto(PageMetadata.getData(win.gBrowser.selectedBrowser.contentDocument), targetWindow);
+      }
+    },
+
+    /**
      * Associates a session-id and a call-id with a window for debugging.
      *
      * @param  {string}  windowId  The window id.
      * @param  {string}  sessionId OT session id.
      * @param  {string}  callId    The callId on the server.
      */
     addConversationContext: {
       enumerable: true,
--- a/browser/components/loop/content/css/panel.css
+++ b/browser/components/loop/content/css/panel.css
@@ -165,43 +165,91 @@ body {
 .content-area input:not(.pristine):invalid {
   border-color: #d74345;
   box-shadow: 0 0 4px #c43c3e;
 }
 
 /* Rooms */
 .rooms {
   min-height: 100px;
+  padding: 0 1rem;
 }
 
 .rooms > h1 {
   font-weight: bold;
   color: #999;
-  padding: .5rem 1rem;
+  padding: .5rem 0;
+}
+
+.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;
 }
 
-.rooms > p {
-  padding: .5rem 0;
-  margin: 0;
+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 > p > .btn {
+.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 {
   max-height: 335px; /* XXX better computation needed */
   min-height: 7px;
   overflow: auto;
   border-top: 1px solid #ccc;
   border-bottom: 1px solid #ccc;
+  margin-left: -1rem;
+  margin-right: -1rem;
 }
 
 .room-list:empty {
   border-bottom-width: 0;
 }
 
 .room-list > .room-entry {
   padding: .5rem 1rem;
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -589,16 +589,17 @@ loop.panel = (function(_, mozL10n) {
 
   /**
    * Room list.
    */
   var RoomList = React.createClass({displayName: "RoomList",
     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
     },
 
     getInitialState: function() {
       return this.props.store.getStoreState();
     },
@@ -661,29 +662,83 @@ loop.panel = (function(_, mozL10n) {
             this.state.rooms.map(function(room, i) {
               return React.createElement(RoomEntry, {
                 key: room.roomToken, 
                 dispatcher: this.props.dispatcher, 
                 room: room}
               );
             }, 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()}, 
               mozL10n.get("rooms_new_room_button_label")
             )
           )
         )
       );
     }
   });
 
   /**
+   * 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() {
+      var metadata = this.props.mozLoop.getSelectedTabMetadata();
+      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});
+    },
+
+    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.
    */
   var PanelView = React.createClass({displayName: "PanelView",
     propTypes: {
       notifications: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       userProfile: React.PropTypes.object,
       // Used only for unit tests.
@@ -814,17 +869,18 @@ loop.panel = (function(_, mozL10n) {
         React.createElement("div", null, 
           React.createElement(NotificationListView, {notifications: this.props.notifications, 
                                 clearOnDocumentHidden: true}), 
           React.createElement(TabView, {ref: "tabView", selectedTab: this.props.selectedTab, 
             buttonsHidden: hideButtons, mozLoop: this.props.mozLoop}, 
             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"}, 
               React.createElement(ContactsList, {selectTab: this.selectTab, 
                             startForm: this.startForm, 
                             notifications: this.props.notifications})
             ), 
             React.createElement(Tab, {name: "contacts_add", hidden: true}, 
@@ -885,16 +941,17 @@ loop.panel = (function(_, mozL10n) {
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
   }
 
   return {
     init: init,
     AuthLink: AuthLink,
     AvailabilityDropdown: AvailabilityDropdown,
+    ContextInfo: ContextInfo,
     GettingStartedView: GettingStartedView,
     PanelView: PanelView,
     RoomEntry: RoomEntry,
     RoomList: RoomList,
     SettingsDropdown: SettingsDropdown,
     ToSView: ToSView,
     UserIdentity: UserIdentity,
   };
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -589,16 +589,17 @@ loop.panel = (function(_, mozL10n) {
 
   /**
    * Room list.
    */
   var RoomList = React.createClass({
     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
     },
 
     getInitialState: function() {
       return this.props.store.getStoreState();
     },
@@ -661,23 +662,77 @@ loop.panel = (function(_, mozL10n) {
             this.state.rooms.map(function(room, i) {
               return <RoomEntry
                 key={room.roomToken}
                 dispatcher={this.props.dispatcher}
                 room={room}
               />;
             }, this)
           }</div>
-          <p>
+          <div>
+            <ContextInfo mozLoop={this.props.mozLoop} />
             <button className="btn btn-info new-room-button"
                     onClick={this.handleCreateButtonClick}
                     disabled={this._hasPendingOperation()}>
               {mozL10n.get("rooms_new_room_button_label")}
             </button>
-          </p>
+          </div>
+        </div>
+      );
+    }
+  });
+
+  /**
+   * 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() {
+      var metadata = this.props.mozLoop.getSelectedTabMetadata();
+      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});
+    },
+
+    onDocumentHidden: function() {
+      this.setState({previewImage: "",
+                     description: "",
+                     url: ""});
+    },
+
+    render: function() {
+      if (!this.props.mozLoop.getLoopPref("contextInConverations.enabled") ||
+          !this.state.url) {
+        return null;
+      }
+      return (
+        <div className="context">
+          <label className="context-enabled">
+            <input type="checkbox"/>
+            {mozL10n.get("context_offer_label")}
+          </label>
+          <img className="context-preview" src={this.state.previewImage}/>
+          <span className="context-description">{this.state.description}</span>
+          <span className="context-url">{this.state.url}</span>
         </div>
       );
     }
   });
 
   /**
    * Panel view.
    */
@@ -814,17 +869,18 @@ loop.panel = (function(_, mozL10n) {
         <div>
           <NotificationListView notifications={this.props.notifications}
                                 clearOnDocumentHidden={true} />
           <TabView ref="tabView" selectedTab={this.props.selectedTab}
             buttonsHidden={hideButtons} mozLoop={this.props.mozLoop}>
             <Tab name="rooms">
               <RoomList dispatcher={this.props.dispatcher}
                         store={this.props.roomStore}
-                        userDisplayName={this._getUserDisplayName()}/>
+                        userDisplayName={this._getUserDisplayName()}
+                        mozLoop={this.props.mozLoop}/>
               <ToSView />
             </Tab>
             <Tab name="contacts">
               <ContactsList selectTab={this.selectTab}
                             startForm={this.startForm}
                             notifications={this.props.notifications} />
             </Tab>
             <Tab name="contacts_add" hidden={true}>
@@ -885,16 +941,17 @@ loop.panel = (function(_, mozL10n) {
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
   }
 
   return {
     init: init,
     AuthLink: AuthLink,
     AvailabilityDropdown: AvailabilityDropdown,
+    ContextInfo: ContextInfo,
     GettingStartedView: GettingStartedView,
     PanelView: PanelView,
     RoomEntry: RoomEntry,
     RoomList: RoomList,
     SettingsDropdown: SettingsDropdown,
     ToSView: ToSView,
     UserIdentity: UserIdentity,
   };
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -646,17 +646,18 @@ describe("loop.panel", function() {
       dispatch = sandbox.stub(dispatcher, "dispatch");
     });
 
     function createTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.panel.RoomList, {
           store: roomStore,
           dispatcher: dispatcher,
-          userDisplayName: fakeEmail
+          userDisplayName: fakeEmail,
+          mozLoop: fakeMozLoop
         }));
     }
 
     it("should dispatch a GetAllRooms action on mount", function() {
       createTestComponent();
 
       sinon.assert.calledOnce(dispatch);
       sinon.assert.calledWithExactly(dispatch, new sharedActions.GetAllRooms());
@@ -703,16 +704,59 @@ describe("loop.panel", function() {
       function() {
         roomStore.setStoreState({pendingInitialRetrieval: true});
 
         var view = createTestComponent();
 
         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() {
 
     it("should render when the value of loop.seenToS is not set", function() {
       navigator.mozLoop.getLoopPref = function(key) {
         return {
           "gettingStarted.seen": true,
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -110,28 +110,36 @@ var fakeContacts = [{
 navigator.mozLoop = {
   ensureRegistered: function() {},
   getAudioBlob: function(){},
   getLoopPref: function(pref) {
     switch(pref) {
       // Ensure we skip FTE completely.
       case "gettingStarted.seen":
       case "contacts.gravatars.promo":
+      case "contextInConverations.enabled":
         return true;
       case "contacts.gravatars.show":
         return false;
     }
   },
   setLoopPref: function(){},
   releaseCallData: function() {},
   copyString: function() {},
   getUserAvatar: function(emailAddress) {
     return "http://www.gravatar.com/avatar/" + (Math.ceil(Math.random() * 3) === 2 ?
       "0a996f0fe2727ef1668bdb11897e4459" : "foo") + ".jpg?default=blank&s=40";
   },
+  getSelectedTabMetadata: function() {
+    return {
+      previews: ["chrome://branding/content/about-logo.png"],
+      description: "sample webpage description",
+      url: "https://www.example.com"
+    };
+  },
   contacts: {
     getAll: function(callback) {
       callback(null, [].concat(fakeContacts));
     },
     on: function() {}
   },
   rooms: {
     getAll: function(version, callback) {
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -51,16 +51,38 @@
   }
 
   function returnFalse() {
     return false;
   }
 
   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(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
@@ -752,16 +774,20 @@
 
       };
     }
   }
 
   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;
     }
 
     _renderComponentsInIframes();
 
     // Put the title back, in case views changed it.
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -51,16 +51,38 @@
   }
 
   function returnFalse() {
     return false;
   }
 
   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(
     "https://input.allizom.org/api/v1/feedback", {
       product: "Loop"
     }
   );
 
@@ -752,16 +774,20 @@
 
       };
     }
   }
 
   window.addEventListener("DOMContentLoaded", function() {
     try {
       React.renderComponent(<App />, document.getElementById("main"));
+
+      for (var listener of visibilityListeners) {
+        listener({target: {hidden: false}});
+      }
     } catch(err) {
       console.error(err);
       uncaughtError = err;
     }
 
     _renderComponentsInIframes();
 
     // Put the title back, in case views changed it.
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -326,8 +326,11 @@ rooms_signout_alert=Open conversations w
 
 # Infobar strings
 
 infobar_screenshare_browser_message=Users in your conversation will now be able to see the contents of any tab you click on.
 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