Bug 1282899 - Land version 1.4.2 of the Loop system add-on in mozilla-central, rs=Standard8 for already reviewed code. a=sylvestre
authorMark Banner <standard8@mozilla.com>
Tue, 28 Jun 2016 20:47:33 +0100
changeset 339771 af55fb03c444b8a18e8b963781e50a1088075084
parent 339770 8113cb833a68bfa67466502a8b130f2153b6fde4
child 339772 9f2b365e53bc915de34f0b8afceab1096d7c5b00
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8, sylvestre
bugs1282899
milestone49.0a2
Bug 1282899 - Land version 1.4.2 of the Loop system add-on in mozilla-central, rs=Standard8 for already reviewed code. a=sylvestre
browser/extensions/loop/bootstrap.js
browser/extensions/loop/chrome/content/modules/MozLoopService.jsm
browser/extensions/loop/chrome/content/panels/js/roomViews.js
browser/extensions/loop/chrome/content/panels/test/roomViews_test.js
browser/extensions/loop/chrome/content/shared/js/activeRoomStore.js
browser/extensions/loop/chrome/content/shared/js/otSdkDriver.js
browser/extensions/loop/chrome/content/shared/js/utils.js
browser/extensions/loop/chrome/content/shared/test/activeRoomStore_test.js
browser/extensions/loop/chrome/content/shared/vendor/sdk.js
browser/extensions/loop/chrome/locale/en-US/loop.properties
browser/extensions/loop/chrome/locale/nn-NO/loop.properties
browser/extensions/loop/install.rdf.in
--- a/browser/extensions/loop/bootstrap.js
+++ b/browser/extensions/loop/bootstrap.js
@@ -872,26 +872,60 @@ var WindowListener = {
       /**
        * Indicates if tab sharing is paused.
        * Set by tab pause button, startBrowserSharing and stopBrowserSharing.
        * Defaults to false as link generator(owner) enters room we are sharing tabs.
        */
       _browserSharePaused: false, 
 
       /**
+       * Stores details about the last notification.
+       *
+       * @type {Object}
+       */
+      _lastNotification: {}, 
+
+      /**
+       * Used to determine if the browser sharing info bar is currently being
+       * shown or not.
+       */
+      _showingBrowserSharingInfoBar: function _showingBrowserSharingInfoBar() {
+        var browser = gBrowser.selectedBrowser;
+        var box = gBrowser.getNotificationBox(browser);
+        var notification = box.getNotificationWithValue(kBrowserSharingNotificationId);
+
+        return !!notification;}, 
+
+
+      /**
        * Shows an infobar notification at the top of the browser window that warns
        * the user that their browser tabs are being broadcasted through the current
        * conversation.
        * @param  {String} currentRoomToken Room we are currently joined.
        * @return {void}
        */
       _maybeShowBrowserSharingInfoBar: function _maybeShowBrowserSharingInfoBar(currentRoomToken) {var _this13 = this;
-        this._hideBrowserSharingInfoBar();
+        var participantsCount = this.LoopRooms.getNumParticipants(currentRoomToken);
+
+        if (this._showingBrowserSharingInfoBar()) {
+          // When we first open the room, there will be one or zero partipicants
+          // in the room. The notification box changes when there's more than one,
+          // so work that out here.
+          var notAlone = participantsCount > 1;
+          var previousNotAlone = this._lastNotification.participantsCount <= 1;
 
-        var participantsCount = this.LoopRooms.getNumParticipants(currentRoomToken);
+          // If we're not actually changing the notification bar, then don't
+          // re-display it. This avoids the bar sliding in twice.
+          if (notAlone !== previousNotAlone && 
+          this._browserSharePaused === this._lastNotification.paused) {
+            return;}
+
+
+          this._hideBrowserSharingInfoBar();}
+
 
         var initStrings = this._setInfoBarStrings(participantsCount > 1, this._browserSharePaused);
 
         var box = gBrowser.getNotificationBox();
         var bar = box.appendNotification(
         initStrings.message, // label
         kBrowserSharingNotificationId, // value
         // Icon defined in browser theme CSS.
@@ -930,17 +964,20 @@ var WindowListener = {
           type: "stop" }]);
 
 
 
         // Sets 'paused' class if needed.
         bar.classList.toggle("paused", !!this._browserSharePaused);
 
         // Keep showing the notification bar until the user explicitly closes it.
-        bar.persistence = -1;}, 
+        bar.persistence = -1;
+
+        this._lastNotification.participantsCount = participantsCount;
+        this._lastNotification.paused = this._browserSharePaused;}, 
 
 
       /**
        * Hides the infobar, permanantly if requested.
        *
        * @param   {Object}  browser Optional link to the browser we want to
        *                    remove the infobar from. If not present, defaults
        *                    to current browser instance.
--- a/browser/extensions/loop/chrome/content/modules/MozLoopService.jsm
+++ b/browser/extensions/loop/chrome/content/modules/MozLoopService.jsm
@@ -1100,29 +1100,29 @@ var MozLoopServiceInternal = {
         url: url, 
         remote: MozLoopService.getLoopPref("remote.autostart") }, 
       callback);
       if (!chatboxInstance) {
         resolve(null);
         // It's common for unit tests to overload Chat.open, so check if we actually
         // got a DOM node back.
       } else if (chatboxInstance.setAttribute) {
-          // Set properties that influence visual appearance of the chatbox right
-          // away to circumvent glitches.
-          chatboxInstance.setAttribute("customSize", "loopDefault");
-          chatboxInstance.parentNode.setAttribute("customSize", "loopDefault");
-          var buttons = "minimize,";
-          if (MozLoopService.getLoopPref("conversationPopOut.enabled")) {
-            buttons += "swap,";}
-
-          Chat.loadButtonSet(chatboxInstance, buttons + kChatboxHangupButton.id);
-          // Final fall-through in case a unit test overloaded Chat.open. Here we can
-          // immediately resolve the promise.
-        } else {
-            resolve(windowId);}});}, 
+        // Set properties that influence visual appearance of the chatbox right
+        // away to circumvent glitches.
+        chatboxInstance.setAttribute("customSize", "loopDefault");
+        chatboxInstance.parentNode.setAttribute("customSize", "loopDefault");
+        var buttons = "minimize,";
+        if (MozLoopService.getLoopPref("conversationPopOut.enabled")) {
+          buttons += "swap,";}
+
+        Chat.loadButtonSet(chatboxInstance, buttons + kChatboxHangupButton.id);
+        // Final fall-through in case a unit test overloaded Chat.open. Here we can
+        // immediately resolve the promise.
+      } else {
+        resolve(windowId);}});}, 
 
 
 
 
   /**
    * Fetch Firefox Accounts (FxA) OAuth parameters from the Loop Server.
    *
    * @return {Promise} resolved with the body of the hawk request for OAuth parameters.
--- a/browser/extensions/loop/chrome/content/panels/js/roomViews.js
+++ b/browser/extensions/loop/chrome/content/panels/js/roomViews.js
@@ -180,28 +180,19 @@ loop.roomViews = function (mozL10n) {
     componentWillUpdate: function componentWillUpdate(nextProps, nextState) {
       // The SDK needs to know about the configuration and the elements to use
       // for display. So the best way seems to pass the information here - ideally
       // the sdk wouldn't need to know this, but we can't change that.
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT && 
       nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
         this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({ 
           publisherConfig: this.getDefaultPublisherConfig({ 
-            publishVideo: !this.state.videoMuted }) }));}
-
-
+            publishVideo: !this.state.videoMuted }) }));}}, 
 
 
-      // Now that we're ready to share, automatically start sharing a tab only
-      // if we're not already connected to the room via the sdk, e.g. not in the
-      // case a remote participant just left.
-      if (nextState.roomState === ROOM_STATES.SESSION_CONNECTED && 
-      !(this.state.roomState === ROOM_STATES.SESSION_CONNECTED || 
-      this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS)) {
-        this.props.dispatcher.dispatch(new sharedActions.StartBrowserShare());}}, 
 
 
 
     /**
      * User clicked on the "Leave" button.
      */
     leaveRoom: function leaveRoom() {
       if (this.state.used) {
--- a/browser/extensions/loop/chrome/content/panels/test/roomViews_test.js
+++ b/browser/extensions/loop/chrome/content/panels/test/roomViews_test.js
@@ -365,52 +365,19 @@ describe("loop.roomViews", function () {
 
 
         activeRoomStore.setStoreState({ roomState: ROOM_STATES.MEDIA_WAIT });
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch, 
         new sharedActions.SetupStreamElements({ 
           publisherConfig: { 
-            fake: "config" } }));});
-
-
-
-
-      it("should dispatch a `StartBrowserShare` action when the SESSION_CONNECTED state is entered", function () {
-        activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
-        mountTestComponent();
-
-        activeRoomStore.setStoreState({ roomState: ROOM_STATES.SESSION_CONNECTED });
-
-        sinon.assert.calledOnce(dispatcher.dispatch);
-        sinon.assert.calledWithExactly(dispatcher.dispatch, 
-        new sharedActions.StartBrowserShare());});
+            fake: "config" } }));});});
 
 
-      it("should not dispatch a `StartBrowserShare` action when the previous state was HAS_PARTICIPANTS", function () {
-        activeRoomStore.setStoreState({ roomState: ROOM_STATES.HAS_PARTICIPANTS });
-        mountTestComponent();
-
-        activeRoomStore.setStoreState({ roomState: ROOM_STATES.SESSION_CONNECTED });
-
-        sinon.assert.notCalled(dispatcher.dispatch);});
-
-
-      it("should not dispatch a `StartBrowserShare` action when the previous state was SESSION_CONNECTED", function () {
-        activeRoomStore.setStoreState({ roomState: ROOM_STATES.SESSION_CONNECTED });
-        mountTestComponent();
-
-        activeRoomStore.setStoreState({ 
-          roomState: ROOM_STATES.SESSION_CONNECTED, 
-          // Additional change to force an update.
-          screenSharingState: "fake" });
-
-
-        sinon.assert.notCalled(dispatcher.dispatch);});});
 
 
 
     describe("#render", function () {
       it("should set document.title to store.serverData.roomName", function () {
         mountTestComponent();
 
         activeRoomStore.setStoreState({ roomName: "fakeName" });
--- a/browser/extensions/loop/chrome/content/shared/js/activeRoomStore.js
+++ b/browser/extensions/loop/chrome/content/shared/js/activeRoomStore.js
@@ -713,18 +713,26 @@ loop.store.ActiveRoomStore = function (m
         sessionId: actionData.sessionId, 
         roomState: ROOM_STATES.JOINED });
 
 
       this._setRefreshTimeout(actionData.expires);
 
       this._sdkDriver.connectSession(actionData);
 
-      loop.request("AddConversationContext", this._storeState.windowId, 
-      actionData.sessionId, "");}, 
+      this._browserSharingListener = this._handleSwitchBrowserShare.bind(this);
+
+      // Set up a listener for watching screen shares. This will get notified
+      // with the first windowId when it is added, so we may start off the sharing
+      // from within the listener.
+      loop.subscribe("BrowserSwitch", this._browserSharingListener);
+
+      loop.requestMulti(["AddConversationContext", this._storeState.windowId, 
+      actionData.sessionId, ""], 
+      ["AddBrowserSharingListener", this.getStoreState().windowId]);}, 
 
 
     /**
      * Handles recording when the sdk has connected to the servers.
      */
     connectedToSdkServers: function connectedToSdkServers() {
       this.setStoreState({ 
         roomState: ROOM_STATES.SESSION_CONNECTED });}, 
@@ -775,17 +783,24 @@ loop.store.ActiveRoomStore = function (m
           localSrcMediaElement: actionData.srcMediaElement });
 
         return;}
 
 
       this.setStoreState({ 
         remoteAudioEnabled: actionData.hasAudio, 
         remoteVideoEnabled: actionData.hasVideo, 
-        remoteSrcMediaElement: actionData.srcMediaElement });}, 
+        remoteSrcMediaElement: actionData.srcMediaElement });
+
+
+      // We start browser sharing here so that it starts *after* the audio/video
+      // has connected. This is to attempt to help performance when a room is
+      // initially joined.
+      if (this._isDesktop) {
+        this.startBrowserShare();}}, 
 
 
 
     /**
      * Handles a media stream being destroyed. This may be a local or a remote stream.
      *
      * @param {sharedActions.MediaStreamDestroyed} actionData
      */
@@ -861,29 +876,47 @@ loop.store.ActiveRoomStore = function (m
      * only be used for browser sharing.
      *
      * @param {Number} windowId  The new windowId to start sharing.
      */
     _handleSwitchBrowserShare: function _handleSwitchBrowserShare(windowId) {
       if (Array.isArray(windowId)) {
         windowId = windowId[0];}
 
-      if (!windowId) {
-        return;}
 
-      if (windowId.isError) {
+      // There was an error getting the windowId, so lets just reset things.
+      if (windowId && windowId.isError) {
         console.error("Error getting the windowId: " + windowId.message);
         this.dispatchAction(new sharedActions.ScreenSharingState({ 
           state: SCREEN_SHARE_STATES.INACTIVE }));
 
         return;}
 
 
+      // If there's no windowId, see if we've got one saved.
+      if (!windowId) {
+        // If we really don't have a window Id, then don't do anything.
+        if (!this._savedWindowId) {
+          return;}
+
+
+        windowId = this._savedWindowId;
+        delete this._savedWindowId;}
+
+
       var screenSharingState = this.getStoreState().screenSharingState;
 
+      // If we're inactive, or screen sharing is paused, just save the windowId
+      // for when we're ready.
+      if (screenSharingState === SCREEN_SHARE_STATES.INACTIVE || 
+      this._storeState.sharingPaused) {
+        this._savedWindowId = windowId;
+        return;}
+
+
       if (screenSharingState === SCREEN_SHARE_STATES.PENDING) {
         // Screen sharing is still pending, so assume that we need to kick it off.
         var options = { 
           videoSource: "browser", 
           constraints: { 
             browserWindow: windowId, 
             scrollWithPage: true } };
 
@@ -942,24 +975,20 @@ loop.store.ActiveRoomStore = function (m
       // For the unit test we already set the state here, instead of indirectly
       // via an action, because actions are queued thus depending on the
       // asynchronous nature of `loop.request`.
       this.setStoreState({ screenSharingState: SCREEN_SHARE_STATES.PENDING });
       this.dispatchAction(new sharedActions.ScreenSharingState({ 
         state: SCREEN_SHARE_STATES.PENDING }));
 
 
-      this._browserSharingListener = this._handleSwitchBrowserShare.bind(this);
-
-      // Set up a listener for watching screen shares. This will get notified
-      // with the first windowId when it is added, so we start off the sharing
-      // from within the listener.
-      loop.request("AddBrowserSharingListener", this.getStoreState().windowId).
-      then(this._browserSharingListener);
-      loop.subscribe("BrowserSwitch", this._browserSharingListener);}, 
+      // This attempts to start the actual browser sharing - we assume we've
+      // already got a windowId due to having requested it before connecting the
+      // media.
+      this._handleSwitchBrowserShare();}, 
 
 
     /**
      * Ends an active screenshare session.
      */
     endScreenShare: function endScreenShare() {
       if (this._browserSharingListener) {
         // Remove the browser sharing listener as we don't need it now.
@@ -980,16 +1009,26 @@ loop.store.ActiveRoomStore = function (m
      *
      * @param {sharedActions.ToggleBrowserSharing} actionData
      */
     toggleBrowserSharing: function toggleBrowserSharing(actionData) {
       this.setStoreState({ 
         sharingPaused: !actionData.enabled });
 
 
+      // If we've un-paused screen sharing, but we haven't started sharing, then
+      // we need to start that off.
+      if (actionData.enabled && 
+      this._storeState.screenSharingState === SCREEN_SHARE_STATES.PENDING) {
+        this._handleSwitchBrowserShare();} else 
+      {
+        // Otherwise just toggle the stream.
+        this._sdkDriver.toggleBrowserSharing(actionData.enabled);}
+
+
       // If unpausing, check the context as it might have changed.
       if (actionData.enabled) {
         this._checkTabContext();}}, 
 
 
 
     /**
      * Handles recording when a remote peer has connected to the servers.
--- a/browser/extensions/loop/chrome/content/shared/js/otSdkDriver.js
+++ b/browser/extensions/loop/chrome/content/shared/js/otSdkDriver.js
@@ -36,18 +36,17 @@ loop.OTSdkDriver = function () {
     this.connections = {};
 
     // Setup the metrics object to keep track of the number of connections we have
     // and the amount of streams.
     this._resetMetrics();
 
     this.dispatcher.register(this, [
     "setupStreamElements", 
-    "setMute", 
-    "toggleBrowserSharing"]);
+    "setMute"]);
 
 
     // Set loop.debug.sdk to true in the browser, or in standalone:
     // localStorage.setItem("debug.sdk", true);
     loop.shared.utils.getBoolPreference("debug.sdk", function (enabled) {
       // We don't bother with the else case - as we only create one instance of
       // OTSdkDriver per window, and hence, we leave the sdk set to its default
       // value.
@@ -232,21 +231,22 @@ loop.OTSdkDriver = function () {
       delete this._mockScreenSharePreviewEl;
       delete this._windowId;
       return true;}, 
 
 
     /**
      * Paused or resumes an active screenshare session as appropriate.
      *
-     * @param {sharedActions.ToggleBrowserSharing} actionData The data associated with the
-     *                                             action. See action.js.
+     * @param {Boolean} enabled  True if browser sharing should be enabled.
      */
-    toggleBrowserSharing: function toggleBrowserSharing(actionData) {
-      this.screenshare.publishVideo(actionData.enabled);}, 
+    toggleBrowserSharing: function toggleBrowserSharing(enabled) {
+      if (this.screenshare) {
+        this.screenshare.publishVideo(enabled);}}, 
+
 
 
     /**
      * Connects a session for the SDK, listening to the required events.
      *
      * sessionData items:
      * - sessionId: The OT session ID
      * - apiKey: The OT API key
@@ -532,20 +532,18 @@ loop.OTSdkDriver = function () {
      * received.
      *
      * @param {Stream} stream The SDK Stream:
      * https://tokbox.com/opentok/libraries/client/js/reference/Stream.html
      */
     _handleRemoteScreenShareCreated: function _handleRemoteScreenShareCreated(stream) {
       // Let the stores know first if the screen sharing is paused or not so
       // they can update the display properly
-      if (!stream[STREAM_PROPERTIES.HAS_VIDEO]) {
-        this.dispatcher.dispatch(new sharedActions.VideoScreenStreamChanged({ 
-          hasVideo: false }));}
-
+      this.dispatcher.dispatch(new sharedActions.VideoScreenStreamChanged({ 
+        hasVideo: stream[STREAM_PROPERTIES.HAS_VIDEO] }));
 
 
       // Let the stores know so they can update the display if needed.
       this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({ 
         receiving: true }));
 
 
       // There's no audio for screen shares so we don't need to worry about mute.
--- a/browser/extensions/loop/chrome/content/shared/js/utils.js
+++ b/browser/extensions/loop/chrome/content/shared/js/utils.js
@@ -356,27 +356,27 @@ if (inChrome) {
 
         callback(result.some(checkForInput));}).
       catch(function () {
         callback(false);});
 
       // MediaStreamTrack is the older version of the API, implemented originally
       // by Google Chrome.
     } else if ("MediaStreamTrack" in rootObject && 
-      "getSources" in rootObject.MediaStreamTrack) {
-        rootObject.MediaStreamTrack.getSources(function (result) {
-          function checkForInput(device) {
-            return device.kind === "audio" || device.kind === "video";}
+    "getSources" in rootObject.MediaStreamTrack) {
+      rootObject.MediaStreamTrack.getSources(function (result) {
+        function checkForInput(device) {
+          return device.kind === "audio" || device.kind === "video";}
 
 
-          callback(result.some(checkForInput));});} else 
+        callback(result.some(checkForInput));});} else 
 
-      {
-        // We don't know, so assume true.
-        callback(true);}}
+    {
+      // We don't know, so assume true.
+      callback(true);}}
 
 
 
   /**
    * Helper to allow getting some of the location data in a way that's compatible
    * with stubbing for unit tests.
    */
   function locationData() {
--- a/browser/extensions/loop/chrome/content/shared/test/activeRoomStore_test.js
+++ b/browser/extensions/loop/chrome/content/shared/test/activeRoomStore_test.js
@@ -46,16 +46,17 @@ describe("loop.store.ActiveRoomStore", f
 
     fakeSdkDriver = { 
       connectSession: sinon.stub(), 
       disconnectSession: sinon.stub(), 
       forceDisconnectAll: sinon.stub().callsArg(0), 
       retryPublishWithoutVideo: sinon.stub(), 
       startScreenShare: sinon.stub(), 
       switchAcquiredWindow: sinon.stub(), 
+      toggleBrowserSharing: sinon.stub(), 
       endScreenShare: sinon.stub().returns(true) };
 
 
     store = new loop.store.ActiveRoomStore(dispatcher, { 
       sdkDriver: fakeSdkDriver });
 
 
     sandbox.stub(document.mozL10n ? document.mozL10n : navigator.mozL10n, "get", function (x) {
@@ -190,17 +191,22 @@ describe("loop.store.ActiveRoomStore", f
 
       sinon.assert.calledOnce(clearTimeout);});
 
 
     it("should remove the sharing listener", function () {
       sandbox.stub(loop, "unsubscribe");
 
       // Setup the listener.
-      store.startBrowserShare(new sharedActions.StartBrowserShare());
+      store.joinedRoom(new sharedActions.JoinedRoom({ 
+        apiKey: "", 
+        sessionToken: "", 
+        sessionId: "", 
+        expires: 0 }));
+
 
       // Now simulate room failure.
       store.roomFailure(new sharedActions.RoomFailure({ 
         error: fakeError, 
         failedJoinRequest: false }));
 
 
       sinon.assert.calledOnce(loop.unsubscribe);
@@ -1201,17 +1207,22 @@ describe("loop.store.ActiveRoomStore", f
       sinon.assert.calledWithExactly(requestStubs["HangupNow"], 
       "fakeToken", "1627384950", "42");});
 
 
     it("should remove the sharing listener", function () {
       sandbox.stub(loop, "unsubscribe");
 
       // Setup the listener.
-      store.startBrowserShare(new sharedActions.StartBrowserShare());
+      store.joinedRoom(new sharedActions.JoinedRoom({ 
+        apiKey: "", 
+        sessionToken: "", 
+        sessionId: "", 
+        expires: 0 }));
+
 
       // Now simulate connection failure.
       store.connectionFailure(connectionFailureAction);
 
       sinon.assert.calledOnce(loop.unsubscribe);
       sinon.assert.calledWith(loop.unsubscribe, "BrowserSwitch");});
 
 
@@ -1332,17 +1343,35 @@ describe("loop.store.ActiveRoomStore", f
         hasAudio: true, 
         hasVideo: true, 
         isLocal: false, 
         srcMediaElement: fakeStreamElement }));
 
 
       expect(store.getStoreState().localVideoEnabled).eql(false);
       expect(store.getStoreState().remoteVideoEnabled).eql(true);
-      expect(store.getStoreState().remoteAudioEnabled).eql(true);});});
+      expect(store.getStoreState().remoteAudioEnabled).eql(true);});
+
+
+    it("should call startBrowserShare when is desktop", function () {
+      sandbox.stub(store, "startBrowserShare");
+      store._isDesktop = true;
+      store.setStoreState({ 
+        localVideoEnabled: false, 
+        remoteVideoEnabled: false });
+
+
+      store.mediaStreamCreated(new sharedActions.MediaStreamCreated({ 
+        hasAudio: true, 
+        hasVideo: true, 
+        isLocal: false, 
+        srcMediaElement: fakeStreamElement }));
+
+
+      sinon.assert.calledOnce(store.startBrowserShare);});});
 
 
 
   describe("#mediaStreamDestroyed", function () {
     var fakeStreamElement;
 
     beforeEach(function () {
       fakeStreamElement = { name: "fakeStreamElement" };
@@ -1489,42 +1518,32 @@ describe("loop.store.ActiveRoomStore", f
 
       expect(store.getStoreState().remoteVideoDimensions).eql({ 
         camera: { fake: 20 } });});});
 
 
 
 
   describe("#startBrowserShare", function () {
-    var getSelectedTabMetadataStub;
-
     beforeEach(function () {
-      getSelectedTabMetadataStub = sinon.stub();
-      LoopMochaUtils.stubLoopRequest({ 
-        GetSelectedTabMetadata: getSelectedTabMetadataStub.returns({ 
-          title: "fakeTitle", 
-          favicon: "fakeFavicon", 
-          url: "http://www.fakeurl.com" }) });
-
-
-
       store.setStoreState({ 
         roomState: ROOM_STATES.JOINED, 
         roomToken: "fakeToken", 
         sessionToken: "1627384950", 
         participants: [{ 
           displayName: "Owner", 
           owner: true }, 
         { 
           displayName: "Guest", 
           owner: false }] });
 
 
 
-      sandbox.stub(console, "error");});
+      sandbox.stub(console, "error");
+      sandbox.stub(store, "_handleSwitchBrowserShare");});
 
 
     afterEach(function () {
       store.endScreenShare();});
 
 
     it("should log an error if the state is not inactive", function () {
       store.setStoreState({ 
@@ -1551,167 +1570,274 @@ describe("loop.store.ActiveRoomStore", f
       store.startBrowserShare(new sharedActions.StartBrowserShare());
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWith(dispatcher.dispatch, 
       new sharedActions.ScreenSharingState({ 
         state: SCREEN_SHARE_STATES.PENDING }));});
 
 
 
-    it("should add a browser sharing listener for tab sharing", function () {
+    it("should call _handleSwitchBrowserShare", function () {
       store.startBrowserShare(new sharedActions.StartBrowserShare());
-      sinon.assert.calledOnce(requestStubs.AddBrowserSharingListener);});
-
-
-    it("should invoke the SDK driver with the correct options for tab sharing", function () {
-      store.startBrowserShare(new sharedActions.StartBrowserShare());
+
+      sinon.assert.calledOnce(store._handleSwitchBrowserShare);});});
+
+
+
+  describe("Screen share Events", function () {
+    it("should call _handleSwitchBrowserShare", function () {
+      sandbox.stub(store, "_handleSwitchBrowserShare");
+
+      store.joinedRoom(new sharedActions.JoinedRoom({ 
+        apiKey: "", 
+        sessionToken: "", 
+        sessionId: "", 
+        expires: 0 }));
+
+
+      LoopMochaUtils.publish("BrowserSwitch", 72);
+
+      sinon.assert.calledOnce(store._handleSwitchBrowserShare);});});
+
+
+
+  describe("#_handleSwitchBrowserShare", function () {
+    var getSelectedTabMetadataStub;
+
+    beforeEach(function () {
+      getSelectedTabMetadataStub = sinon.stub();
+      LoopMochaUtils.stubLoopRequest({ 
+        GetSelectedTabMetadata: getSelectedTabMetadataStub.returns({ 
+          title: "fakeTitle", 
+          favicon: "fakeFavicon", 
+          url: "http://www.fakeurl.com" }) });
+
+
+
+      store.setStoreState({ 
+        roomState: ROOM_STATES.JOINED, 
+        roomToken: "fakeToken", 
+        screenSharingState: SCREEN_SHARE_STATES.ACTIVE, 
+        sessionToken: "1627384950", 
+        participants: [{ 
+          displayName: "Owner", 
+          owner: true }, 
+        { 
+          displayName: "Guest", 
+          owner: false }] });
+
+
+
+      // Stub to prevent errors surfacing in the console.
+      sandbox.stub(console, "error");});
+
+
+    afterEach(function () {
+      store.endScreenShare();});
+
+
+    it("should log an error in the console", function () {
+      var err = new Error("foo");
+      err.isError = true;
+      store._handleSwitchBrowserShare(err);
+
+      sinon.assert.calledOnce(console.error);});
+
+
+    it("should end the screen sharing session when the listener receives an error", function () {
+      var err = new Error("foo");
+      err.isError = true;
+      store._handleSwitchBrowserShare(err);
+
+      // The dispatcher was already called once in beforeEach().
+      sinon.assert.calledOnce(dispatcher.dispatch);
+      sinon.assert.calledWith(dispatcher.dispatch, 
+      new sharedActions.ScreenSharingState({ 
+        state: SCREEN_SHARE_STATES.INACTIVE }));
+
+      sinon.assert.notCalled(fakeSdkDriver.switchAcquiredWindow);});
+
+
+    it("should save the windowId when the state is INACTIVE", function () {
+      store.setStoreState({ 
+        screenSharingState: SCREEN_SHARE_STATES.INACTIVE });
+
+
+      store._handleSwitchBrowserShare(72);
+
+      expect(store._savedWindowId, 72);});
+
+
+    it("should not do anything else when the state is INACTIVE", function () {
+      store.setStoreState({ 
+        screenSharingState: SCREEN_SHARE_STATES.INACTIVE });
+
+
+      store._handleSwitchBrowserShare(72);
+
+      sinon.assert.notCalled(dispatcher.dispatch);
+      sinon.assert.notCalled(fakeSdkDriver.startScreenShare);
+      sinon.assert.notCalled(fakeSdkDriver.switchAcquiredWindow);});
+
+
+    it("should save the windowId when sharing is paused", function () {
+      store.setStoreState({ 
+        screenSharingState: SCREEN_SHARE_STATES.ACTIVE, 
+        sharingPaused: true });
+
+
+      store._handleSwitchBrowserShare(72);
+
+      expect(store._savedWindowId, 72);});
+
+
+    it("should not do anything else when the state is paused", function () {
+      store.setStoreState({ 
+        screenSharingState: SCREEN_SHARE_STATES.ACTIVE, 
+        sharingPaused: true });
+
+
+      store._handleSwitchBrowserShare(72);
+
+      sinon.assert.notCalled(dispatcher.dispatch);
+      sinon.assert.notCalled(fakeSdkDriver.startScreenShare);
+      sinon.assert.notCalled(fakeSdkDriver.switchAcquiredWindow);});
+
+
+    it("should invoke the SDK driver with the correct options if the state is pending", function () {
+      store.setStoreState({ 
+        screenSharingState: SCREEN_SHARE_STATES.PENDING });
+
+
+      store._handleSwitchBrowserShare(42);
+
       sinon.assert.calledOnce(fakeSdkDriver.startScreenShare);
       sinon.assert.calledWith(fakeSdkDriver.startScreenShare, { 
         videoSource: "browser", 
         constraints: { 
           browserWindow: 42, 
           scrollWithPage: true } });});
 
 
 
 
+    it("should update the SDK driver when the state is active", function () {
+      store._handleSwitchBrowserShare(72);
+
+      sinon.assert.calledOnce(fakeSdkDriver.switchAcquiredWindow);
+      sinon.assert.calledWithExactly(fakeSdkDriver.switchAcquiredWindow, 72);});
+
+
+    it("should log an error if the state is unexpected", function () {
+      store.setStoreState({ 
+        screenSharingState: "invalid" });
+
+
+      store._handleSwitchBrowserShare(72);
+
+      sinon.assert.calledOnce(console.error);});
+
+
     it("should request the new metadata when the browser being shared change", function () {
-      store.startBrowserShare(new sharedActions.StartBrowserShare());
+      store._handleSwitchBrowserShare(42);
+
       clock.tick(500);
 
       sinon.assert.calledOnce(getSelectedTabMetadataStub);
-      sinon.assert.calledTwice(dispatcher.dispatch);
-      sinon.assert.calledWith(dispatcher.dispatch.getCall(1), 
+      sinon.assert.calledOnce(dispatcher.dispatch);
+      sinon.assert.calledWithExactly(dispatcher.dispatch, 
       new sharedActions.UpdateRoomContext({ 
         newRoomDescription: "fakeTitle", 
         newRoomThumbnail: "fakeFavicon", 
         newRoomURL: "http://www.fakeurl.com", 
         roomToken: store.getStoreState().roomToken }));});
 
 
 
     it("should process only one request", function () {
       store.startBrowserShare(new sharedActions.StartBrowserShare());
       // Simulates multiple requests.
-      LoopMochaUtils.publish("BrowserSwitch", 72);
-      LoopMochaUtils.publish("BrowserSwitch", 72);
+      store._handleSwitchBrowserShare(42);
+      store._handleSwitchBrowserShare(42);
 
       clock.tick(500);
-      sinon.assert.calledThrice(getSelectedTabMetadataStub);
-      sinon.assert.calledTwice(dispatcher.dispatch);
-      sinon.assert.calledWith(dispatcher.dispatch.getCall(1), 
+
+      sinon.assert.calledTwice(getSelectedTabMetadataStub);
+      sinon.assert.calledOnce(dispatcher.dispatch);
+      sinon.assert.calledWithExactly(dispatcher.dispatch, 
       new sharedActions.UpdateRoomContext({ 
         newRoomDescription: "fakeTitle", 
         newRoomThumbnail: "fakeFavicon", 
         newRoomURL: "http://www.fakeurl.com", 
         roomToken: store.getStoreState().roomToken }));});
 
 
 
     it("should not process a request without url", function () {
       getSelectedTabMetadataStub.returns({ 
         title: "fakeTitle", 
         favicon: "fakeFavicon" });
 
 
-      store.startBrowserShare(new sharedActions.StartBrowserShare());
+      store._handleSwitchBrowserShare(42);
+
       clock.tick(500);
 
       sinon.assert.calledOnce(getSelectedTabMetadataStub);
-      sinon.assert.calledOnce(dispatcher.dispatch);});
+      sinon.assert.notCalled(dispatcher.dispatch);});
 
 
     it("should not process a request if sharing is paused", function () {
       store.setStoreState({ 
         sharingPaused: true });
 
 
-      store.startBrowserShare(new sharedActions.StartBrowserShare());
+      store._handleSwitchBrowserShare(42);
       clock.tick(500);
 
       sinon.assert.notCalled(getSelectedTabMetadataStub);
-      sinon.assert.calledOnce(dispatcher.dispatch);});
+      sinon.assert.notCalled(dispatcher.dispatch);});
 
 
     it("should not process a request if no-one is in the room", function () {
       store.setStoreState({ 
         participants: [{ 
           displayName: "Owner", 
           owner: true }] });
 
 
 
-      store.startBrowserShare(new sharedActions.StartBrowserShare());
+      store._handleSwitchBrowserShare(42);
+
       clock.tick(500);
 
       sinon.assert.notCalled(getSelectedTabMetadataStub);
-      sinon.assert.calledOnce(dispatcher.dispatch);});});
-
-
-
-  describe("Screen share Events", function () {
-    beforeEach(function () {
-      store.startBrowserShare(new sharedActions.StartBrowserShare());
-
-      store.setStoreState({ 
-        screenSharingState: SCREEN_SHARE_STATES.ACTIVE });
-
-
-      // Stub to prevent errors surfacing in the console.
-      sandbox.stub(window.console, "error");});
-
-
-    afterEach(function () {
-      store.endScreenShare();});
-
-
-    it("should log an error in the console", function () {
-      var err = new Error("foo");
-      err.isError = true;
-      LoopMochaUtils.publish("BrowserSwitch", err);
-
-      sinon.assert.calledOnce(console.error);});
-
-
-    it("should update the SDK driver when a new window id is received", function () {
-      LoopMochaUtils.publish("BrowserSwitch", 72);
-
-      sinon.assert.calledOnce(fakeSdkDriver.switchAcquiredWindow);
-      sinon.assert.calledWithExactly(fakeSdkDriver.switchAcquiredWindow, 72);});
-
-
-    it("should end the screen sharing session when the listener receives an error", function () {
-      var err = new Error("foo");
-      err.isError = true;
-      LoopMochaUtils.publish("BrowserSwitch", err);
-
-      // The dispatcher was already called once in beforeEach().
-      sinon.assert.calledTwice(dispatcher.dispatch);
-      sinon.assert.calledWith(dispatcher.dispatch, 
-      new sharedActions.ScreenSharingState({ 
-        state: SCREEN_SHARE_STATES.INACTIVE }));
-
-      sinon.assert.notCalled(fakeSdkDriver.switchAcquiredWindow);});});
+      sinon.assert.notCalled(dispatcher.dispatch);});});
 
 
 
   describe("#endScreenShare", function () {
     it("should set the state to 'inactive'", function () {
       store.endScreenShare();
 
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWith(dispatcher.dispatch, 
       new sharedActions.ScreenSharingState({ 
         state: SCREEN_SHARE_STATES.INACTIVE }));});
 
 
 
     it("should remove the sharing listener", function () {
       // Setup the listener.
-      store.startBrowserShare(new sharedActions.StartBrowserShare());
+      store.joinedRoom(new sharedActions.JoinedRoom({ 
+        apiKey: "", 
+        sessionToken: "", 
+        sessionId: "", 
+        expires: 0 }));
+
 
       // Now stop the screen share.
       store.endScreenShare();
 
       sinon.assert.calledOnce(requestStubs.RemoveBrowserSharingListener);});});
 
 
 
@@ -1918,17 +2044,22 @@ describe("loop.store.ActiveRoomStore", f
       sinon.assert.calledWith(requestStubs["HangupNow"], "fakeToken", 
       "1627384950", "1234");});
 
 
     it("should remove the sharing listener", function () {
       sandbox.stub(loop, "unsubscribe");
 
       // Setup the listener.
-      store.startBrowserShare(new sharedActions.StartBrowserShare());
+      store.joinedRoom(new sharedActions.JoinedRoom({ 
+        apiKey: "", 
+        sessionToken: "", 
+        sessionId: "", 
+        expires: 0 }));
+
 
       // Now unload the window.
       store.windowUnload();
 
       sinon.assert.calledOnce(loop.unsubscribe);
       sinon.assert.calledWith(loop.unsubscribe, "BrowserSwitch");});
 
 
@@ -1987,17 +2118,22 @@ describe("loop.store.ActiveRoomStore", f
 
       sinon.assert.notCalled(requestStubs["HangupNow"]);});
 
 
     it("should remove the sharing listener", function () {
       sandbox.stub(loop, "unsubscribe");
 
       // Setup the listener.
-      store.startBrowserShare(new sharedActions.StartBrowserShare());
+      store.joinedRoom(new sharedActions.JoinedRoom({ 
+        apiKey: "", 
+        sessionToken: "", 
+        sessionId: "", 
+        expires: 0 }));
+
 
       // Now leave the room.
       store.leaveRoom();
 
       sinon.assert.calledOnce(loop.unsubscribe);
       sinon.assert.calledWith(loop.unsubscribe, "BrowserSwitch");});
 
 
--- a/browser/extensions/loop/chrome/content/shared/vendor/sdk.js
+++ b/browser/extensions/loop/chrome/content/shared/vendor/sdk.js
@@ -1,16 +1,16 @@
 /**
- * @license OpenTok.js v2.7.5 ccd6792 HEAD
+ * @license OpenTok.js v2.7.6 cff9122 HEAD
  *
  * Copyright (c) 2010-2015 TokBox, Inc.
  * Released under the MIT license
  * http://opensource.org/licenses/MIT
  *
- * Date: March 18 10:46:23 2016
+ * Date: June 24 07:05:09 2016
  */
 
 (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
 module.exports = Array;
 
 },{}],2:[function(require,module,exports){
 arguments[4][108][0].apply(exports,arguments)
 },{"dup":108}],3:[function(require,module,exports){
@@ -3811,22 +3811,22 @@ module.exports = reqAnimationFrame;
 'use strict';
 
 var Bluebird = require('bluebird');
 var makeEverythingAttachToOTHelpers = require('./makeEverythingAttachToOTHelpers');
 
 var util = exports;
 
 /**
- * @license  Common JS Helpers on OpenTok v2.7.5 ccd6792 HEAD
+ * @license  Common JS Helpers on OpenTok v2.7.6 cff9122 HEAD
  * http://www.tokbox.com/
  *
  * Copyright (c) 2016 TokBox, Inc.
  *
- * Date: March 18 10:46:49 2016
+ * Date: June 24 07:05:32 2016
  *
  */
 
 
 // OT Helper Methods
 //
 // helpers.js                           <- the root file
 // helpers/lib/{helper topic}.js        <- specialised helpers for specific tasks/topics
@@ -14164,67 +14164,95 @@ var substr = 'ab'.substr(-1) === 'b'
 ;
 
 }).call(this,require('_process'))
 
 },{"_process":79}],79:[function(require,module,exports){
 // shim for using process in browser
 
 var process = module.exports = {};
+
+// cached from whatever global is present so that test runners that stub it
+// don't break things.  But we need to wrap it in a try catch in case it is
+// wrapped in strict mode code which doesn't define any globals.  It's inside a
+// function because try/catches deoptimize in certain engines.
+
+var cachedSetTimeout;
+var cachedClearTimeout;
+
+(function () {
+  try {
+    cachedSetTimeout = setTimeout;
+  } catch (e) {
+    cachedSetTimeout = function () {
+      throw new Error('setTimeout is not defined');
+    }
+  }
+  try {
+    cachedClearTimeout = clearTimeout;
+  } catch (e) {
+    cachedClearTimeout = function () {
+      throw new Error('clearTimeout is not defined');
+    }
+  }
+} ())
 var queue = [];
 var draining = false;
 var currentQueue;
 var queueIndex = -1;
 
 function cleanUpNextTick() {
+    if (!draining || !currentQueue) {
+        return;
+    }
     draining = false;
     if (currentQueue.length) {
         queue = currentQueue.concat(queue);
     } else {
         queueIndex = -1;
     }
     if (queue.length) {
         drainQueue();
     }
 }
 
 function drainQueue() {
     if (draining) {
         return;
     }
-    var timeout = setTimeout(cleanUpNextTick);
+    var timeout = cachedSetTimeout(cleanUpNextTick);
     draining = true;
 
     var len = queue.length;
     while(len) {
         currentQueue = queue;
         queue = [];
         while (++queueIndex < len) {
             if (currentQueue) {
                 currentQueue[queueIndex].run();
             }
         }
         queueIndex = -1;
         len = queue.length;
     }
     currentQueue = null;
     draining = false;
-    clearTimeout(timeout);
+    cachedClearTimeout(timeout);
 }
 
 process.nextTick = function (fun) {
     var args = new Array(arguments.length - 1);
     if (arguments.length > 1) {
         for (var i = 1; i < arguments.length; i++) {
             args[i - 1] = arguments[i];
         }
     }
     queue.push(new Item(fun, args));
     if (queue.length === 1 && !draining) {
-        setTimeout(drainQueue, 0);
+        cachedSetTimeout(drainQueue, 0);
     }
 };
 
 // v8 likes predictible objects
 function Item(fun, array) {
     this.fun = fun;
     this.array = array;
 }
@@ -20249,19 +20277,19 @@ function ws(uri, protocols, opts) {
   }
   return instance;
 }
 
 if (WebSocket) ws.prototype = WebSocket.prototype;
 
 },{}],111:[function(require,module,exports){
 module.exports = {
-  "version": "v2.7.5",
-  "build": "ccd6792",
-  "buildTime": "March 18 10:46:49 2016",
+  "version": "v2.7.6",
+  "build": "cff9122",
+  "buildTime": "June 24 07:05:32 2016",
   "debug": "false",
   "websiteURL": "http://www.tokbox.com",
   "cdnURL": "http://static.opentok.com",
   "loggingURL": "http://hlg.tokbox.com/prod",
   "apiURL": "http://anvil.opentok.com",
   "messagingProtocol": "wss",
   "messagingPort": 443,
   "supportSSL": "true",
@@ -29949,16 +29977,17 @@ var PeerConnection = function(config) {
       // term, I want it to be noisy.
       throw new Error('PeerConnection broke!');
     }
 
     internalCreatePeerConnection(function() {
       subscribeProcessor(
         _peerConnection,
         config.numberOfSimulcastStreams,
+        config.offerConstraints,
 
         // Success: Relay Offer
         function(offer) {
           _offer = offer;
           relaySDP(RaptorConstants.Actions.OFFER, _offer, message.uri);
         },
 
         // Failure
@@ -30418,18 +30447,23 @@ module.exports = function PublisherPeerC
     _peerConnection.processMessage(type, message);
   };
 
   // Init
   this.init = function(iceServers, completion) {
     var pcConfig = {
       iceServers: iceServers,
       channels: channels,
-      numberOfSimulcastStreams: numberOfSimulcastStreams
-    };
+      numberOfSimulcastStreams: numberOfSimulcastStreams,
+      offerConstraints: {}
+    };
+
+    if (session.sessionInfo.reconnection) {
+      pcConfig.offerConstraints.iceRestart = true;
+    }
 
     setCertificates(pcConfig, function(err, pcConfigWithCerts) {
       if (err) {
         completion(err);
         return;
       }
 
       _peerConnection = PeerConnections.add(remoteConnection, streamId, pcConfigWithCerts);
@@ -31219,16 +31253,17 @@ var SDPHelpers = require('./sdp_helpers.
 // * set the new offer as the location description
 //
 // If there are no issues, the +success+ callback will be executed on completion.
 // Errors during any step will result in the +failure+ callback being executed.
 //
 module.exports = function subscribeProcessor(
   peerConnection,
   numberOfSimulcastStreams,
+  offerConstraints,
   success,
   failure
 ) {
   var generateErrorCallback, setLocalDescription;
 
   generateErrorCallback = function(message, prefix) {
     return function(errorReason) {
       logging.error(message);
@@ -31265,19 +31300,17 @@ module.exports = function subscribeProce
   peerConnection.createOffer(
     // Success
     setLocalDescription,
 
     // Failure
     generateErrorCallback('Error while creating Offer', 'CreateOffer'),
 
     // Constraints
-    {
-      iceRestart: true
-    }
+    offerConstraints
   );
 };
 
 },{"../logging.js":160,"./sdp_helpers.js":193}],196:[function(require,module,exports){
 'use strict';
 
 var OTHelpers = require('@opentok/ot-helpers');
 var parseIceServers = require('../messaging/raptor/parse_ice_servers.js');
@@ -31481,17 +31514,23 @@ module.exports = function SubscriberPeer
   };
 
   this.iceConnectionStateIsConnected = function() {
     return _peerConnection.iceConnectionStateIsConnected();
   };
 
   // Init
   this.init = function(completion) {
-    var pcConfig = {};
+    var pcConfig = {
+      offerConstraints: {}
+    };
+
+    if (session.sessionInfo.reconnection) {
+      pcConfig.offerConstraints.iceRestart = true;
+    }
 
     setCertificates(pcConfig, function(err, pcConfigWithCerts) {
       if (err) {
         completion(err);
         return;
       }
 
       _peerConnection = PeerConnections.add(
--- a/browser/extensions/loop/chrome/locale/en-US/loop.properties
+++ b/browser/extensions/loop/chrome/locale/en-US/loop.properties
@@ -2,17 +2,17 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # Panel Strings
 
 ## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
 ## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
 ## use "..." if \u2026 doesn't suit traditions in your locale.
-loopMenuItem_label=Start a conversation…
+loopMenuItem_label=Start a Conversation…
 loopMenuItem_accesskey=t
 
 ## LOCALIZATION_NOTE(sign_in_again_title_line_one, sign_in_again_title_line_two2):
 ## These are displayed together at the top of the panel when a user is needed to
 ## sign-in again. The emphesis is on the first line to get the user to sign-in again,
 ## and this is displayed in slightly larger font. Please arrange as necessary for
 ## your locale.
 ## {{clientShortname2}} will be replaced by the brand name for either string.
--- a/browser/extensions/loop/chrome/locale/nn-NO/loop.properties
+++ b/browser/extensions/loop/chrome/locale/nn-NO/loop.properties
@@ -28,17 +28,17 @@ panel_disconnect_button=Kopla frå
 
 ## LOCALIZATION_NOTE(first_time_experience_subheading2, first_time_experience_subheading_button_above): Message inviting the
 ## user to create his or her first conversation.
 first_time_experience_subheading2=Trykk på Hello-knappen for å surfa på nettet saman med ein ven.
 first_time_experience_subheading_button_above=Trykk på knappen ovanfor for å surfa på nettet saman med ein ven.
 
 ## LOCALIZATION_NOTE(first_time_experience_content, first_time_experience_content2): Message describing
 ## ways to use Hello project.
-first_time_experience_content=Bruk han til å planleggja, arbeida, og ha det moro i lag.
+first_time_experience_content=Bruk Hello til å planleggja, arbeida, og ha det moro i lag.
 first_time_experience_content2=Bruk han til å få gjort ting: planlegging, arbeid og for å ha det moro i lag.
 first_time_experience_button_label2=Sjå korleis det fungerer
 
 ## First Time Experience Slides
 fte_slide_1_title=Surf på nettsider med ein ven
 ## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
 ## will be replaced by the short name 2.
 fte_slide_1_copy=Om du planlegg ei reise eller leitar du etter ein present, {{clientShortname2}} hjelper deg med å ta raskare avgjerder i sanntid.
--- a/browser/extensions/loop/install.rdf.in
+++ b/browser/extensions/loop/install.rdf.in
@@ -4,17 +4,17 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 #filter substitution
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>loop@mozilla.org</em:id>
     <em:bootstrap>true</em:bootstrap>
-    <em:version>1.4.1</em:version>
+    <em:version>1.4.2</em:version>
     <em:type>2</em:type>
 
     <!-- Target Application this extension can install into,
          with minimum and maximum supported versions. -->
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>46.0a1</em:minVersion>