diff --git a/browser/base/content/browser-loop.js b/browser/base/content/browser-loop.js deleted file mode 100644 index 40e48eccc4b0..000000000000 --- 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"); diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 61df291af60d..6174a0baf893 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -274,7 +274,6 @@ var gInitialPages = [ #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 @@ -1357,8 +1356,6 @@ var gBrowserInit = { gDataNotificationInfoBar.init(); #endif - LoopUI.init(); - gBrowserThumbnails.init(); // Add Devtools menuitems and listeners @@ -1540,7 +1537,6 @@ var gBrowserInit = { TabView.uninit(); SocialUI.uninit(); gBrowserThumbnails.uninit(); - LoopUI.uninit(); FullZoom.destroy(); Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history"); diff --git a/browser/extensions/loop/bootstrap.js b/browser/extensions/loop/bootstrap.js new file mode 100644 index 000000000000..90be7de3e68a --- /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"); + } +}