Bug 1223573 - Part 3. Move browser-loop.js to begin forming bootstrap.js. r=mikedeboer
☠☠ backed out by c95f8e8955b0 ☠ ☠
authorMark Banner <standard8@mozilla.com>
Fri, 27 Nov 2015 18:57:40 +0000
changeset 274459 1b4d6308002e3c2eacdfa3aae296b39dd328c096
parent 274458 a13b3bba5529eaa853befa21cae4c8162c3dd565
child 274460 d6754894897cbf3eafe0f15fb2f4389024a814dd
push id16429
push usermbanner@mozilla.com
push dateFri, 27 Nov 2015 18:58:34 +0000
treeherderfx-team@19876a153a00 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer
bugs1223573
milestone45.0a1
Bug 1223573 - Part 3. Move browser-loop.js to begin forming bootstrap.js. r=mikedeboer
browser/base/content/browser-loop.js
browser/base/content/browser.js
browser/extensions/loop/bootstrap.js
deleted file mode 100644
--- a/browser/base/content/browser-loop.js
+++ /dev/null
@@ -1,613 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// 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/.
-
-// the "exported" symbols
-var LoopUI;
-
-(function() {
-  const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-  const kBrowserSharingNotificationId = "loop-sharing-notification";
-  const kPrefBrowserSharingInfoBar = "browserSharing.showInfoBar";
-
-  LoopUI = {
-    /**
-     * @var {XULWidgetSingleWrapper} toolbarButton Getter for the Loop toolbarbutton
-     *                                             instance for this window.
-     */
-    get toolbarButton() {
-      delete this.toolbarButton;
-      return this.toolbarButton = CustomizableUI.getWidget("loop-button").forWindow(window);
-    },
-
-    /**
-     * @var {XULElement} panel Getter for the Loop panel element.
-     */
-    get panel() {
-      delete this.panel;
-      return this.panel = document.getElementById("loop-notification-panel");
-    },
-
-    /**
-     * @var {XULElement|null} browser Getter for the Loop panel browser element.
-     *                                Will be NULL if the panel hasn't loaded yet.
-     */
-    get browser() {
-      let browser = document.querySelector("#loop-notification-panel > #loop-panel-iframe");
-      if (browser) {
-        delete this.browser;
-        this.browser = browser;
-      }
-      return browser;
-    },
-
-    /**
-     * @var {String|null} selectedTab Getter for the name of the currently selected
-     *                                tab inside the Loop panel. Will be NULL if
-     *                                the panel hasn't loaded yet.
-     */
-    get selectedTab() {
-      if (!this.browser) {
-        return null;
-      }
-
-      let selectedTab = this.browser.contentDocument.querySelector(".tab-view > .selected");
-      return selectedTab && selectedTab.getAttribute("data-tab-name");
-    },
-
-    /**
-     * @return {Promise}
-     */
-    promiseDocumentVisible(aDocument) {
-      if (!aDocument.hidden) {
-        return Promise.resolve(aDocument);
-      }
-
-      return new Promise((resolve) => {
-        aDocument.addEventListener("visibilitychange", function onVisibilityChanged() {
-          aDocument.removeEventListener("visibilitychange", onVisibilityChanged);
-          resolve(aDocument);
-        });
-      });
-    },
-
-    /**
-     * Toggle between opening or hiding the Loop panel.
-     *
-     * @param {DOMEvent} [event] Optional event that triggered the call to this
-     *                           function.
-     * @param {String}   [tabId] Optional name of the tab to select after the panel
-     *                           has opened. Does nothing when the panel is hidden.
-     * @return {Promise}
-     */
-    togglePanel: function(event, tabId = null) {
-      if (!this.panel) {
-        // We're on the hidden window! What fun!
-        let obs = win => {
-          Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
-          win.LoopUI.togglePanel(event, tabId);
-        };
-        Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
-        return OpenBrowserWindow();
-      }
-      if (this.panel.state == "open") {
-        return new Promise(resolve => {
-          this.panel.hidePopup();
-          resolve();
-        });
-      }
-
-      return this.openCallPanel(event, tabId).then(doc => {
-        let fm = Services.focus;
-        fm.moveFocus(doc.defaultView, null, fm.MOVEFOCUS_FIRST, fm.FLAG_NOSCROLL);
-      }).catch(err => {
-        Cu.reportError(x);
-      });
-    },
-
-    /**
-     * Opens the panel for Loop and sizes it appropriately.
-     *
-     * @param {event}  event   The event opening the panel, used to anchor
-     *                         the panel to the button which triggers it.
-     * @param {String} [tabId] Identifier of the tab to select when the panel is
-     *                         opened. Example: 'rooms', 'contacts', etc.
-     * @return {Promise}
-     */
-    openCallPanel: function(event, tabId = null) {
-      return new Promise((resolve) => {
-        let callback = iframe => {
-          // Helper function to show a specific tab view in the panel.
-          function showTab() {
-            if (!tabId) {
-              resolve(LoopUI.promiseDocumentVisible(iframe.contentDocument));
-              return;
-            }
-
-            let win = iframe.contentWindow;
-            let ev = new win.CustomEvent("UIAction", Cu.cloneInto({
-              detail: {
-                action: "selectTab",
-                tab: tabId
-              }
-            }, win));
-            win.dispatchEvent(ev);
-            resolve(LoopUI.promiseDocumentVisible(iframe.contentDocument));
-          }
-
-          // If the panel has been opened and initialized before, we can skip waiting
-          // for the content to load - because it's already there.
-          if (("contentWindow" in iframe) && iframe.contentWindow.document.readyState == "complete") {
-            showTab();
-            return;
-          }
-
-          let documentDOMLoaded = () => {
-            iframe.removeEventListener("DOMContentLoaded", documentDOMLoaded, true);
-            // Handle window.close correctly on the panel.
-            this.hookWindowCloseForPanelClose(iframe.contentWindow);
-            iframe.contentWindow.addEventListener("loopPanelInitialized", function loopPanelInitialized() {
-              iframe.contentWindow.removeEventListener("loopPanelInitialized",
-                loopPanelInitialized);
-              showTab();
-            });
-          };
-          iframe.addEventListener("DOMContentLoaded", documentDOMLoaded, true);
-        };
-
-        // Used to clear the temporary "login" state from the button.
-        Services.obs.notifyObservers(null, "loop-status-changed", null);
-
-        this.shouldResumeTour().then((resume) => {
-          if (resume) {
-            // Assume the conversation with the visitor wasn't open since we would
-            // have resumed the tour as soon as the visitor joined if it was (and
-            // the pref would have been set to false already.
-            this.MozLoopService.resumeTour("waiting");
-            resolve();
-            return;
-          }
-
-          this.LoopAPI.initialize();
-
-          let anchor = event ? event.target : this.toolbarButton.anchor;
-          let setHeight = 410;
-          if (gBrowser.selectedBrowser.getAttribute("remote") === "true") {
-            setHeight = 262;
-          }
-          this.PanelFrame.showPopup(window, anchor,
-            "loop", null, "about:looppanel",
-            // Loop wants a fixed size for the panel. This also stops it dynamically resizing.
-            { width: 330, height: setHeight },
-            callback);
-        });
-      });
-    },
-
-    /**
-     * Method to know whether actions to open the panel should instead resume the tour.
-     *
-     * We need the panel to be opened via UITour so that it gets @noautohide.
-     *
-     * @return {Promise} resolving with a {Boolean} of whether the tour should be resumed instead of
-     *                   opening the panel.
-     */
-    shouldResumeTour: Task.async(function* () {
-      // Resume the FTU tour if this is the first time a room was joined by
-      // someone else since the tour.
-      if (!Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin")) {
-        return false;
-      }
-
-      if (!this.LoopRooms.participantsCount) {
-        // Nobody is in the rooms
-        return false;
-      }
-
-      let roomsWithNonOwners = yield this.roomsWithNonOwners();
-      if (!roomsWithNonOwners.length) {
-        // We were the only one in a room but we want to know about someone else joining.
-        return false;
-      }
-
-      return true;
-    }),
-
-    /**
-     * @return {Promise} resolved with an array of Rooms with participants (excluding owners)
-     */
-    roomsWithNonOwners: function() {
-      return new Promise(resolve => {
-        this.LoopRooms.getAll((error, rooms) => {
-          let roomsWithNonOwners = [];
-          for (let room of rooms) {
-            if (!("participants" in room)) {
-              continue;
-            }
-            let numNonOwners = room.participants.filter(participant => !participant.owner).length;
-            if (!numNonOwners) {
-              continue;
-            }
-            roomsWithNonOwners.push(room);
-          }
-          resolve(roomsWithNonOwners);
-        });
-      });
-    },
-
-    /**
-     * Triggers the initialization of the loop service.  Called by
-     * delayedStartup.
-     */
-    init: function() {
-      // Add observer notifications before the service is initialized
-      Services.obs.addObserver(this, "loop-status-changed", false);
-
-      // This is a promise for test purposes, but we don't want to be logging
-      // expected errors to the console, so we catch them here.
-      this.MozLoopService.initialize().catch(ex => {
-        if (!ex.message ||
-            (!ex.message.contains("not enabled") &&
-             !ex.message.contains("not needed"))) {
-          console.error(ex);
-        }
-      });
-      this.updateToolbarState();
-    },
-
-    uninit: function() {
-      Services.obs.removeObserver(this, "loop-status-changed");
-    },
-
-    // Implements nsIObserver
-    observe: function(subject, topic, data) {
-      if (topic != "loop-status-changed") {
-        return;
-      }
-      this.updateToolbarState(data);
-    },
-
-    /**
-     * Updates the toolbar/menu-button state to reflect Loop status.
-     *
-     * @param {string} [aReason] Some states are only shown if
-     *                           a related reason is provided.
-     *
-     *                 aReason="login": Used after a login is completed
-     *                   successfully. This is used so the state can be
-     *                   temporarily shown until the next state change.
-     */
-    updateToolbarState: function(aReason = null) {
-      if (!this.toolbarButton.node) {
-        return;
-      }
-      let state = "";
-      let mozL10nId = "loop-call-button3";
-      let suffix = ".tooltiptext";
-      if (this.MozLoopService.errors.size) {
-        state = "error";
-        mozL10nId += "-error";
-      } else if (this.MozLoopService.screenShareActive) {
-        state = "action";
-        mozL10nId += "-screensharing";
-      } else if (aReason == "login" && this.MozLoopService.userProfile) {
-        state = "active";
-        mozL10nId += "-active";
-        suffix += "2";
-      } else if (this.MozLoopService.doNotDisturb) {
-        state = "disabled";
-        mozL10nId += "-donotdisturb";
-      } else if (this.MozLoopService.roomsParticipantsCount > 0) {
-        state = "active";
-        this.roomsWithNonOwners().then(roomsWithNonOwners => {
-          if (roomsWithNonOwners.length > 0) {
-            mozL10nId += "-participantswaiting";
-          } else {
-            mozL10nId += "-active";
-          }
-
-          suffix += "2";
-          this.updateTooltiptext(mozL10nId + suffix);
-          this.toolbarButton.node.setAttribute("state", state);
-        });
-        return;
-      } else {
-        suffix += "2";
-      }
-
-      this.toolbarButton.node.setAttribute("state", state);
-      this.updateTooltiptext(mozL10nId + suffix);
-    },
-
-    /**
-     * Updates the tootltiptext to reflect Loop status.
-     *
-     * @param {string} [mozL10nId] l10n ID that refelct the current
-     *                           Loop status.
-     */
-    updateTooltiptext: function(mozL10nId) {
-      this.toolbarButton.node.setAttribute("tooltiptext", mozL10nId);
-      var tooltiptext = CustomizableUI.getLocalizedProperty(this.toolbarButton, "tooltiptext");
-      this.toolbarButton.node.setAttribute("tooltiptext", tooltiptext);
-    },
-
-    /**
-     * Show a desktop notification when 'do not disturb' isn't enabled.
-     *
-     * @param {Object} options Set of options that may tweak the appearance and
-     *                         behavior of the notification.
-     *                         Option params:
-     *                         - {String}   title       Notification title message
-     *                         - {String}   [message]   Notification body text
-     *                         - {String}   [icon]      Notification icon
-     *                         - {String}   [sound]     Sound to play
-     *                         - {String}   [selectTab] Tab to select when the panel
-     *                                                  opens
-     *                         - {Function} [onclick]   Callback to invoke when
-     *                                                  the notification is clicked.
-     *                                                  Opens the panel by default.
-     */
-    showNotification: function(options) {
-      if (this.MozLoopService.doNotDisturb) {
-        return;
-      }
-
-      if (!options.title) {
-        throw new Error("Missing title, can not display notification");
-      }
-
-      let notificationOptions = {
-        body: options.message || ""
-      };
-      if (options.icon) {
-        notificationOptions.icon = options.icon;
-      }
-      if (options.sound) {
-        // This will not do anything, until bug bug 1105222 is resolved.
-        notificationOptions.mozbehavior = {
-          soundFile: ""
-        };
-        this.playSound(options.sound);
-      }
-
-      let notification = new window.Notification(options.title, notificationOptions);
-      notification.addEventListener("click", e => {
-        if (window.closed) {
-          return;
-        }
-
-        try {
-          window.focus();
-        } catch (ex) {}
-
-        // We need a setTimeout here, otherwise the panel won't show after the
-        // window received focus.
-        window.setTimeout(() => {
-          if (typeof options.onclick == "function") {
-            options.onclick();
-          } else {
-            // Open the Loop panel as a default action.
-            this.openCallPanel(null, options.selectTab || null);
-          }
-        }, 0);
-      });
-    },
-
-    /**
-     * Play a sound in this window IF there's no sound playing yet.
-     *
-     * @param {String} name Name of the sound, like 'ringtone' or 'room-joined'
-     */
-    playSound: function(name) {
-      if (this.ActiveSound || this.MozLoopService.doNotDisturb) {
-        return;
-      }
-
-      this.activeSound = new window.Audio();
-      this.activeSound.src = `chrome://browser/content/loop/shared/sounds/${name}.ogg`;
-      this.activeSound.load();
-      this.activeSound.play();
-
-      this.activeSound.addEventListener("ended", () => this.activeSound = undefined, false);
-    },
-
-    /**
-     * Start listening to selected tab changes and notify any content page that's
-     * listening to 'BrowserSwitch' push messages.
-     *
-     * Push message parameters:
-     * - {Integer} windowId  The new windowId for the browser.
-     */
-    startBrowserSharing: function() {
-      if (!this._listeningToTabSelect) {
-        gBrowser.tabContainer.addEventListener("TabSelect", this);
-        this._listeningToTabSelect = true;
-      }
-
-      this._maybeShowBrowserSharingInfoBar();
-
-      // Get the first window Id for the listener.
-      this.LoopAPI.broadcastPushMessage("BrowserSwitch",
-        gBrowser.selectedBrowser.outerWindowID);
-    },
-
-    /**
-     * Stop listening to selected tab changes.
-     */
-    stopBrowserSharing: function() {
-      if (!this._listeningToTabSelect) {
-        return;
-      }
-
-      this._hideBrowserSharingInfoBar();
-      gBrowser.tabContainer.removeEventListener("TabSelect", this);
-      this._listeningToTabSelect = false;
-    },
-
-    /**
-     * Helper function to fetch a localized string via the MozLoopService API.
-     * It's currently inconveniently wrapped inside a string of stringified JSON.
-     *
-     * @param  {String} key The element id to get strings for.
-     * @return {String}
-     */
-    _getString: function(key) {
-      let str = this.MozLoopService.getStrings(key);
-      if (str) {
-        str = JSON.parse(str).textContent;
-      }
-      return str;
-    },
-
-    /**
-     * 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.
-     */
-    _maybeShowBrowserSharingInfoBar: function() {
-      this._hideBrowserSharingInfoBar();
-
-      // Don't show the infobar if it's been permanently disabled from the menu.
-      if (!this.MozLoopService.getLoopPref(kPrefBrowserSharingInfoBar)) {
-        return;
-      }
-
-      let box = gBrowser.getNotificationBox();
-      let paused = false;
-      let bar = box.appendNotification(
-        this._getString("infobar_screenshare_browser_message"),
-        kBrowserSharingNotificationId,
-        // Icon is defined in browser theme CSS.
-        null,
-        box.PRIORITY_WARNING_LOW,
-        [{
-          label: this._getString("infobar_button_pause_label"),
-          accessKey: this._getString("infobar_button_pause_accesskey"),
-          isDefault: false,
-          callback: (event, buttonInfo, buttonNode) => {
-            paused = !paused;
-            bar.label = paused ? this._getString("infobar_screenshare_paused_browser_message") :
-              this._getString("infobar_screenshare_browser_message");
-            bar.classList.toggle("paused", paused);
-            buttonNode.label = paused ? this._getString("infobar_button_resume_label") :
-              this._getString("infobar_button_pause_label");
-            buttonNode.accessKey = paused ? this._getString("infobar_button_resume_accesskey") :
-              this._getString("infobar_button_pause_accesskey");
-            return true;
-          }
-        },
-        {
-          label: this._getString("infobar_button_stop_label"),
-          accessKey: this._getString("infobar_button_stop_accesskey"),
-          isDefault: true,
-          callback: () => {
-            this._hideBrowserSharingInfoBar();
-            LoopUI.MozLoopService.hangupAllChatWindows();
-          }
-        }]
-      );
-
-      // Keep showing the notification bar until the user explicitly closes it.
-      bar.persistence = -1;
-    },
-
-    /**
-     * Hides the infobar, permanantly if requested.
-     *
-     * @param {Boolean} permanently Flag that determines if the infobar will never
-     *                              been shown again. Defaults to `false`.
-     * @return {Boolean} |true| if the infobar was hidden here.
-     */
-    _hideBrowserSharingInfoBar: function(permanently = false, browser) {
-      browser = browser || gBrowser.selectedBrowser;
-      let box = gBrowser.getNotificationBox(browser);
-      let notification = box.getNotificationWithValue(kBrowserSharingNotificationId);
-      let removed = false;
-      if (notification) {
-        box.removeNotification(notification);
-        removed = true;
-      }
-
-      if (permanently) {
-        this.MozLoopService.setLoopPref(kPrefBrowserSharingInfoBar, false);
-      }
-
-      return removed;
-    },
-
-    /**
-     * Handles events from gBrowser.
-     */
-    handleEvent: function(event) {
-      // We only should get "select" events.
-      if (event.type != "TabSelect") {
-        return;
-      }
-
-      let wasVisible = false;
-      // Hide the infobar from the previous tab.
-      if (event.detail.previousTab) {
-        wasVisible = this._hideBrowserSharingInfoBar(false,
-          event.detail.previousTab.linkedBrowser);
-      }
-
-      // We've changed the tab, so get the new window id.
-      this.LoopAPI.broadcastPushMessage("BrowserSwitch",
-        gBrowser.selectedBrowser.outerWindowID);
-
-      if (wasVisible) {
-        // If the infobar was visible before, we should show it again after the
-        // switch.
-        this._maybeShowBrowserSharingInfoBar();
-      }
-    },
-
-    /**
-     * Fetch the favicon of the currently selected tab in the format of a data-uri.
-     *
-     * @param  {Function} callback Function to be invoked with an error object as
-     *                             its first argument when an error occurred or
-     *                             a string as second argument when the favicon
-     *                             has been fetched.
-     */
-    getFavicon: function(callback) {
-      let pageURI = gBrowser.selectedTab.linkedBrowser.currentURI.spec;
-      // If the tab page’s url starts with http(s), fetch icon.
-      if (!/^https?:/.test(pageURI)) {
-        callback();
-        return;
-      }
-
-      this.PlacesUtils.promiseFaviconLinkUrl(pageURI).then(uri => {
-        // We XHR the favicon to get a File object, which we can pass to the FileReader
-        // object. The FileReader turns the File object into a data-uri.
-        let xhr = new XMLHttpRequest();
-        xhr.open("get", uri.spec, true);
-        xhr.responseType = "blob";
-        xhr.overrideMimeType("image/x-icon");
-        xhr.onload = () => {
-          if (xhr.status != 200) {
-            callback(new Error("Invalid status code received for favicon XHR: " + xhr.status));
-            return;
-          }
-
-          let reader = new FileReader();
-          reader.onload = reader.onload = () => callback(null, reader.result);
-          reader.onerror = callback;
-          reader.readAsDataURL(xhr.response);
-        };
-        xhr.onerror = callback;
-        xhr.send();
-      }).catch(err => {
-        callback(err || new Error("No favicon found"));
-      });
-    }
-  };
-})();
-
-XPCOMUtils.defineLazyModuleGetter(LoopUI, "hookWindowCloseForPanelClose", "resource://gre/modules/MozSocialAPI.jsm");
-XPCOMUtils.defineLazyModuleGetter(LoopUI, "LoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
-XPCOMUtils.defineLazyModuleGetter(LoopUI, "LoopRooms", "resource:///modules/loop/LoopRooms.jsm");
-XPCOMUtils.defineLazyModuleGetter(LoopUI, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
-XPCOMUtils.defineLazyModuleGetter(LoopUI, "PanelFrame", "resource:///modules/PanelFrame.jsm");
-XPCOMUtils.defineLazyModuleGetter(LoopUI, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -269,17 +269,16 @@ var gInitialPages = [
 #include browser-ctrlTab.js
 #include browser-customization.js
 #include browser-devedition.js
 #include browser-eme.js
 #include browser-feeds.js
 #include browser-fullScreen.js
 #include browser-fullZoom.js
 #include browser-gestureSupport.js
-#include browser-loop.js
 #include browser-places.js
 #include browser-plugins.js
 #include browser-safebrowsing.js
 #include browser-sidebar.js
 #include browser-social.js
 #include browser-syncui.js
 #include browser-tabview.js
 #include browser-thumbnails.js
@@ -1352,18 +1351,16 @@ var gBrowserInit = {
     // initialize the sync UI
     gSyncUI.init();
     gFxAccounts.init();
 
 #ifdef MOZ_DATA_REPORTING
     gDataNotificationInfoBar.init();
 #endif
 
-    LoopUI.init();
-
     gBrowserThumbnails.init();
 
     // Add Devtools menuitems and listeners
     gDevToolsBrowser.registerBrowserWindow(window);
 
     gMenuButtonBadgeManager.init();
 
     gMenuButtonUpdateBadge.init();
@@ -1535,17 +1532,16 @@ var gBrowserInit = {
       if (Win7Features)
         Win7Features.onCloseWindow();
 
       gPrefService.removeObserver(ctrlTab.prefName, ctrlTab);
       ctrlTab.uninit();
       TabView.uninit();
       SocialUI.uninit();
       gBrowserThumbnails.uninit();
-      LoopUI.uninit();
       FullZoom.destroy();
 
       Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-origin-blocked");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
new file mode 100644
--- /dev/null
+++ b/browser/extensions/loop/bootstrap.js
@@ -0,0 +1,619 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+var WindowListener = {
+
+  setupBrowserUI: function(window) {
+    const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+    const kBrowserSharingNotificationId = "loop-sharing-notification";
+    const kPrefBrowserSharingInfoBar = "browserSharing.showInfoBar";
+
+    let document = window.document;
+    let gBrowser = window.gBrowser;
+    let xhrClass = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"];
+    let FileReader = window.FileReader;
+
+    // the "exported" symbols
+    var LoopUI = {
+      /**
+       * @var {XULWidgetSingleWrapper} toolbarButton Getter for the Loop toolbarbutton
+       *                                             instance for this window.
+       */
+      get toolbarButton() {
+        delete this.toolbarButton;
+        return this.toolbarButton = CustomizableUI.getWidget("loop-button").forWindow(window);
+      },
+
+      /**
+       * @var {XULElement} panel Getter for the Loop panel element.
+       */
+      get panel() {
+        delete this.panel;
+        return this.panel = document.getElementById("loop-notification-panel");
+      },
+
+      /**
+       * @var {XULElement|null} browser Getter for the Loop panel browser element.
+       *                                Will be NULL if the panel hasn't loaded yet.
+       */
+      get browser() {
+        let browser = document.querySelector("#loop-notification-panel > #loop-panel-iframe");
+        if (browser) {
+          delete this.browser;
+          this.browser = browser;
+        }
+        return browser;
+      },
+
+      /**
+       * @var {String|null} selectedTab Getter for the name of the currently selected
+       *                                tab inside the Loop panel. Will be NULL if
+       *                                the panel hasn't loaded yet.
+       */
+      get selectedTab() {
+        if (!this.browser) {
+          return null;
+        }
+
+        let selectedTab = this.browser.contentDocument.querySelector(".tab-view > .selected");
+        return selectedTab && selectedTab.getAttribute("data-tab-name");
+      },
+
+      /**
+       * @return {Promise}
+       */
+      promiseDocumentVisible(aDocument) {
+        if (!aDocument.hidden) {
+          return Promise.resolve(aDocument);
+        }
+
+        return new Promise((resolve) => {
+          aDocument.addEventListener("visibilitychange", function onVisibilityChanged() {
+            aDocument.removeEventListener("visibilitychange", onVisibilityChanged);
+            resolve(aDocument);
+          });
+        });
+      },
+
+      /**
+       * Toggle between opening or hiding the Loop panel.
+       *
+       * @param {DOMEvent} [event] Optional event that triggered the call to this
+       *                           function.
+       * @param {String}   [tabId] Optional name of the tab to select after the panel
+       *                           has opened. Does nothing when the panel is hidden.
+       * @return {Promise}
+       */
+      togglePanel: function(event, tabId = null) {
+        if (!this.panel) {
+          // We're on the hidden window! What fun!
+          let obs = win => {
+            Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
+            win.LoopUI.togglePanel(event, tabId);
+          };
+          Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
+          return OpenBrowserWindow();
+        }
+        if (this.panel.state == "open") {
+          return new Promise(resolve => {
+            this.panel.hidePopup();
+            resolve();
+          });
+        }
+
+        return this.openCallPanel(event, tabId).then(doc => {
+          let fm = Services.focus;
+          fm.moveFocus(doc.defaultView, null, fm.MOVEFOCUS_FIRST, fm.FLAG_NOSCROLL);
+        }).catch(err => {
+          Cu.reportError(x);
+        });
+      },
+
+      /**
+       * Opens the panel for Loop and sizes it appropriately.
+       *
+       * @param {event}  event   The event opening the panel, used to anchor
+       *                         the panel to the button which triggers it.
+       * @param {String} [tabId] Identifier of the tab to select when the panel is
+       *                         opened. Example: 'rooms', 'contacts', etc.
+       * @return {Promise}
+       */
+      openCallPanel: function(event, tabId = null) {
+        return new Promise((resolve) => {
+          let callback = iframe => {
+            // Helper function to show a specific tab view in the panel.
+            function showTab() {
+              if (!tabId) {
+                resolve(LoopUI.promiseDocumentVisible(iframe.contentDocument));
+                return;
+              }
+
+              let win = iframe.contentWindow;
+              let ev = new win.CustomEvent("UIAction", Cu.cloneInto({
+                detail: {
+                  action: "selectTab",
+                  tab: tabId
+                }
+              }, win));
+              win.dispatchEvent(ev);
+              resolve(LoopUI.promiseDocumentVisible(iframe.contentDocument));
+            }
+
+            // If the panel has been opened and initialized before, we can skip waiting
+            // for the content to load - because it's already there.
+            if (("contentWindow" in iframe) && iframe.contentWindow.document.readyState == "complete") {
+              showTab();
+              return;
+            }
+
+            let documentDOMLoaded = () => {
+              iframe.removeEventListener("DOMContentLoaded", documentDOMLoaded, true);
+              // Handle window.close correctly on the panel.
+              this.hookWindowCloseForPanelClose(iframe.contentWindow);
+              iframe.contentWindow.addEventListener("loopPanelInitialized", function loopPanelInitialized() {
+                iframe.contentWindow.removeEventListener("loopPanelInitialized",
+                  loopPanelInitialized);
+                showTab();
+              });
+            };
+            iframe.addEventListener("DOMContentLoaded", documentDOMLoaded, true);
+          };
+
+          // Used to clear the temporary "login" state from the button.
+          Services.obs.notifyObservers(null, "loop-status-changed", null);
+
+          this.shouldResumeTour().then((resume) => {
+            if (resume) {
+              // Assume the conversation with the visitor wasn't open since we would
+              // have resumed the tour as soon as the visitor joined if it was (and
+              // the pref would have been set to false already.
+              this.MozLoopService.resumeTour("waiting");
+              resolve();
+              return;
+            }
+
+            this.LoopAPI.initialize();
+
+            let anchor = event ? event.target : this.toolbarButton.anchor;
+            let setHeight = 410;
+            if (gBrowser.selectedBrowser.getAttribute("remote") === "true") {
+              setHeight = 262;
+            }
+            this.PanelFrame.showPopup(window, anchor,
+              "loop", null, "about:looppanel",
+              // Loop wants a fixed size for the panel. This also stops it dynamically resizing.
+              { width: 330, height: setHeight },
+              callback);
+          });
+        });
+      },
+
+      /**
+       * Method to know whether actions to open the panel should instead resume the tour.
+       *
+       * We need the panel to be opened via UITour so that it gets @noautohide.
+       *
+       * @return {Promise} resolving with a {Boolean} of whether the tour should be resumed instead of
+       *                   opening the panel.
+       */
+      shouldResumeTour: Task.async(function* () {
+        // Resume the FTU tour if this is the first time a room was joined by
+        // someone else since the tour.
+        if (!Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin")) {
+          return false;
+        }
+
+        if (!this.LoopRooms.participantsCount) {
+          // Nobody is in the rooms
+          return false;
+        }
+
+        let roomsWithNonOwners = yield this.roomsWithNonOwners();
+        if (!roomsWithNonOwners.length) {
+          // We were the only one in a room but we want to know about someone else joining.
+          return false;
+        }
+
+        return true;
+      }),
+
+      /**
+       * @return {Promise} resolved with an array of Rooms with participants (excluding owners)
+       */
+      roomsWithNonOwners: function() {
+        return new Promise(resolve => {
+          this.LoopRooms.getAll((error, rooms) => {
+            let roomsWithNonOwners = [];
+            for (let room of rooms) {
+              if (!("participants" in room)) {
+                continue;
+              }
+              let numNonOwners = room.participants.filter(participant => !participant.owner).length;
+              if (!numNonOwners) {
+                continue;
+              }
+              roomsWithNonOwners.push(room);
+            }
+            resolve(roomsWithNonOwners);
+          });
+        });
+      },
+
+      /**
+       * Triggers the initialization of the loop service.  Called by
+       * delayedStartup.
+       */
+      init: function() {
+        // Add observer notifications before the service is initialized
+        Services.obs.addObserver(this, "loop-status-changed", false);
+
+        // This is a promise for test purposes, but we don't want to be logging
+        // expected errors to the console, so we catch them here.
+        this.MozLoopService.initialize().catch(ex => {
+          if (!ex.message ||
+              (!ex.message.contains("not enabled") &&
+               !ex.message.contains("not needed"))) {
+            console.error(ex);
+          }
+        });
+        this.updateToolbarState();
+      },
+
+      uninit: function() {
+        Services.obs.removeObserver(this, "loop-status-changed");
+      },
+
+      // Implements nsIObserver
+      observe: function(subject, topic, data) {
+        if (topic != "loop-status-changed") {
+          return;
+        }
+        this.updateToolbarState(data);
+      },
+
+      /**
+       * Updates the toolbar/menu-button state to reflect Loop status.
+       *
+       * @param {string} [aReason] Some states are only shown if
+       *                           a related reason is provided.
+       *
+       *                 aReason="login": Used after a login is completed
+       *                   successfully. This is used so the state can be
+       *                   temporarily shown until the next state change.
+       */
+      updateToolbarState: function(aReason = null) {
+        if (!this.toolbarButton.node) {
+          return;
+        }
+        let state = "";
+        let mozL10nId = "loop-call-button3";
+        let suffix = ".tooltiptext";
+        if (this.MozLoopService.errors.size) {
+          state = "error";
+          mozL10nId += "-error";
+        } else if (this.MozLoopService.screenShareActive) {
+          state = "action";
+          mozL10nId += "-screensharing";
+        } else if (aReason == "login" && this.MozLoopService.userProfile) {
+          state = "active";
+          mozL10nId += "-active";
+          suffix += "2";
+        } else if (this.MozLoopService.doNotDisturb) {
+          state = "disabled";
+          mozL10nId += "-donotdisturb";
+        } else if (this.MozLoopService.roomsParticipantsCount > 0) {
+          state = "active";
+          this.roomsWithNonOwners().then(roomsWithNonOwners => {
+            if (roomsWithNonOwners.length > 0) {
+              mozL10nId += "-participantswaiting";
+            } else {
+              mozL10nId += "-active";
+            }
+
+            suffix += "2";
+            this.updateTooltiptext(mozL10nId + suffix);
+            this.toolbarButton.node.setAttribute("state", state);
+          });
+          return;
+        } else {
+          suffix += "2";
+        }
+
+        this.toolbarButton.node.setAttribute("state", state);
+        this.updateTooltiptext(mozL10nId + suffix);
+      },
+
+      /**
+       * Updates the tootltiptext to reflect Loop status.
+       *
+       * @param {string} [mozL10nId] l10n ID that refelct the current
+       *                           Loop status.
+       */
+      updateTooltiptext: function(mozL10nId) {
+        this.toolbarButton.node.setAttribute("tooltiptext", mozL10nId);
+        var tooltiptext = CustomizableUI.getLocalizedProperty(this.toolbarButton, "tooltiptext");
+        this.toolbarButton.node.setAttribute("tooltiptext", tooltiptext);
+      },
+
+      /**
+       * Show a desktop notification when 'do not disturb' isn't enabled.
+       *
+       * @param {Object} options Set of options that may tweak the appearance and
+       *                         behavior of the notification.
+       *                         Option params:
+       *                         - {String}   title       Notification title message
+       *                         - {String}   [message]   Notification body text
+       *                         - {String}   [icon]      Notification icon
+       *                         - {String}   [sound]     Sound to play
+       *                         - {String}   [selectTab] Tab to select when the panel
+       *                                                  opens
+       *                         - {Function} [onclick]   Callback to invoke when
+       *                                                  the notification is clicked.
+       *                                                  Opens the panel by default.
+       */
+      showNotification: function(options) {
+        if (this.MozLoopService.doNotDisturb) {
+          return;
+        }
+
+        if (!options.title) {
+          throw new Error("Missing title, can not display notification");
+        }
+
+        let notificationOptions = {
+          body: options.message || ""
+        };
+        if (options.icon) {
+          notificationOptions.icon = options.icon;
+        }
+        if (options.sound) {
+          // This will not do anything, until bug bug 1105222 is resolved.
+          notificationOptions.mozbehavior = {
+            soundFile: ""
+          };
+          this.playSound(options.sound);
+        }
+
+        let notification = new window.Notification(options.title, notificationOptions);
+        notification.addEventListener("click", e => {
+          if (window.closed) {
+            return;
+          }
+
+          try {
+            window.focus();
+          } catch (ex) {}
+
+          // We need a setTimeout here, otherwise the panel won't show after the
+          // window received focus.
+          window.setTimeout(() => {
+            if (typeof options.onclick == "function") {
+              options.onclick();
+            } else {
+              // Open the Loop panel as a default action.
+              this.openCallPanel(null, options.selectTab || null);
+            }
+          }, 0);
+        });
+      },
+
+      /**
+       * Play a sound in this window IF there's no sound playing yet.
+       *
+       * @param {String} name Name of the sound, like 'ringtone' or 'room-joined'
+       */
+      playSound: function(name) {
+        if (this.ActiveSound || this.MozLoopService.doNotDisturb) {
+          return;
+        }
+
+        this.activeSound = new window.Audio();
+        this.activeSound.src = `chrome://browser/content/loop/shared/sounds/${name}.ogg`;
+        this.activeSound.load();
+        this.activeSound.play();
+
+        this.activeSound.addEventListener("ended", () => this.activeSound = undefined, false);
+      },
+
+      /**
+       * Start listening to selected tab changes and notify any content page that's
+       * listening to 'BrowserSwitch' push messages.
+       *
+       * Push message parameters:
+       * - {Integer} windowId  The new windowId for the browser.
+       */
+      startBrowserSharing: function() {
+        if (!this._listeningToTabSelect) {
+          gBrowser.tabContainer.addEventListener("TabSelect", this);
+          this._listeningToTabSelect = true;
+        }
+
+        this._maybeShowBrowserSharingInfoBar();
+
+        // Get the first window Id for the listener.
+        this.LoopAPI.broadcastPushMessage("BrowserSwitch",
+          gBrowser.selectedBrowser.outerWindowID);
+      },
+
+      /**
+       * Stop listening to selected tab changes.
+       */
+      stopBrowserSharing: function() {
+        if (!this._listeningToTabSelect) {
+          return;
+        }
+
+        this._hideBrowserSharingInfoBar();
+        gBrowser.tabContainer.removeEventListener("TabSelect", this);
+        this._listeningToTabSelect = false;
+      },
+
+      /**
+       * Helper function to fetch a localized string via the MozLoopService API.
+       * It's currently inconveniently wrapped inside a string of stringified JSON.
+       *
+       * @param  {String} key The element id to get strings for.
+       * @return {String}
+       */
+      _getString: function(key) {
+        let str = this.MozLoopService.getStrings(key);
+        if (str) {
+          str = JSON.parse(str).textContent;
+        }
+        return str;
+      },
+
+      /**
+       * 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.
+       */
+      _maybeShowBrowserSharingInfoBar: function() {
+        this._hideBrowserSharingInfoBar();
+
+        // Don't show the infobar if it's been permanently disabled from the menu.
+        if (!this.MozLoopService.getLoopPref(kPrefBrowserSharingInfoBar)) {
+          return;
+        }
+
+        let box = gBrowser.getNotificationBox();
+        let paused = false;
+        let bar = box.appendNotification(
+          this._getString("infobar_screenshare_browser_message"),
+          kBrowserSharingNotificationId,
+          // Icon is defined in browser theme CSS.
+          null,
+          box.PRIORITY_WARNING_LOW,
+          [{
+            label: this._getString("infobar_button_pause_label"),
+            accessKey: this._getString("infobar_button_pause_accesskey"),
+            isDefault: false,
+            callback: (event, buttonInfo, buttonNode) => {
+              paused = !paused;
+              bar.label = paused ? this._getString("infobar_screenshare_paused_browser_message") :
+                this._getString("infobar_screenshare_browser_message");
+              bar.classList.toggle("paused", paused);
+              buttonNode.label = paused ? this._getString("infobar_button_resume_label") :
+                this._getString("infobar_button_pause_label");
+              buttonNode.accessKey = paused ? this._getString("infobar_button_resume_accesskey") :
+                this._getString("infobar_button_pause_accesskey");
+              return true;
+            }
+          },
+          {
+            label: this._getString("infobar_button_stop_label"),
+            accessKey: this._getString("infobar_button_stop_accesskey"),
+            isDefault: true,
+            callback: () => {
+              this._hideBrowserSharingInfoBar();
+              LoopUI.MozLoopService.hangupAllChatWindows();
+            }
+          }]
+        );
+
+        // Keep showing the notification bar until the user explicitly closes it.
+        bar.persistence = -1;
+      },
+
+      /**
+       * Hides the infobar, permanantly if requested.
+       *
+       * @param {Boolean} permanently Flag that determines if the infobar will never
+       *                              been shown again. Defaults to `false`.
+       * @return {Boolean} |true| if the infobar was hidden here.
+       */
+      _hideBrowserSharingInfoBar: function(permanently = false, browser) {
+        browser = browser || gBrowser.selectedBrowser;
+        let box = gBrowser.getNotificationBox(browser);
+        let notification = box.getNotificationWithValue(kBrowserSharingNotificationId);
+        let removed = false;
+        if (notification) {
+          box.removeNotification(notification);
+          removed = true;
+        }
+
+        if (permanently) {
+          this.MozLoopService.setLoopPref(kPrefBrowserSharingInfoBar, false);
+        }
+
+        return removed;
+      },
+
+      /**
+       * Handles events from gBrowser.
+       */
+      handleEvent: function(event) {
+        // We only should get "select" events.
+        if (event.type != "TabSelect") {
+          return;
+        }
+
+        let wasVisible = false;
+        // Hide the infobar from the previous tab.
+        if (event.detail.previousTab) {
+          wasVisible = this._hideBrowserSharingInfoBar(false,
+            event.detail.previousTab.linkedBrowser);
+        }
+
+        // We've changed the tab, so get the new window id.
+        this.LoopAPI.broadcastPushMessage("BrowserSwitch",
+          gBrowser.selectedBrowser.outerWindowID);
+
+        if (wasVisible) {
+          // If the infobar was visible before, we should show it again after the
+          // switch.
+          this._maybeShowBrowserSharingInfoBar();
+        }
+      },
+
+      /**
+       * Fetch the favicon of the currently selected tab in the format of a data-uri.
+       *
+       * @param  {Function} callback Function to be invoked with an error object as
+       *                             its first argument when an error occurred or
+       *                             a string as second argument when the favicon
+       *                             has been fetched.
+       */
+      getFavicon: function(callback) {
+        let pageURI = gBrowser.selectedTab.linkedBrowser.currentURI.spec;
+        // If the tab page’s url starts with http(s), fetch icon.
+        if (!/^https?:/.test(pageURI)) {
+          callback();
+          return;
+        }
+
+        this.PlacesUtils.promiseFaviconLinkUrl(pageURI).then(uri => {
+          // We XHR the favicon to get a File object, which we can pass to the FileReader
+          // object. The FileReader turns the File object into a data-uri.
+          let xhr = new XMLHttpRequest();
+          xhr.open("get", uri.spec, true);
+          xhr.responseType = "blob";
+          xhr.overrideMimeType("image/x-icon");
+          xhr.onload = () => {
+            if (xhr.status != 200) {
+              callback(new Error("Invalid status code received for favicon XHR: " + xhr.status));
+              return;
+            }
+
+            let reader = new FileReader();
+            reader.onload = reader.onload = () => callback(null, reader.result);
+            reader.onerror = callback;
+            reader.readAsDataURL(xhr.response);
+          };
+          xhr.onerror = callback;
+          xhr.send();
+        }).catch(err => {
+          callback(err || new Error("No favicon found"));
+        });
+      }
+    };
+
+    XPCOMUtils.defineLazyModuleGetter(LoopUI, "hookWindowCloseForPanelClose", "resource://gre/modules/MozSocialAPI.jsm");
+    XPCOMUtils.defineLazyModuleGetter(LoopUI, "LoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
+    XPCOMUtils.defineLazyModuleGetter(LoopUI, "LoopRooms", "resource:///modules/loop/LoopRooms.jsm");
+    XPCOMUtils.defineLazyModuleGetter(LoopUI, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
+    XPCOMUtils.defineLazyModuleGetter(LoopUI, "PanelFrame", "resource:///modules/PanelFrame.jsm");
+    XPCOMUtils.defineLazyModuleGetter(LoopUI, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
+  }
+}