1493 lines
54 KiB
JavaScript
1493 lines
54 KiB
JavaScript
/* 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/. */
|
||
"use strict";
|
||
|
||
/* exported startup, shutdown, install, uninstall */var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {return typeof obj;} : function (obj) {return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj;};function _toConsumableArray(arr) {if (Array.isArray(arr)) {for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {arr2[i] = arr[i];}return arr2;} else {return Array.from(arr);}}var _Components =
|
||
|
||
Components;var Ci = _Components.interfaces;var Cu = _Components.utils;var Cc = _Components.classes;
|
||
|
||
var kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||
var kBrowserSharingNotificationId = "loop-sharing-notification";
|
||
|
||
var CURSOR_MIN_DELTA = 3;
|
||
var CURSOR_MIN_INTERVAL = 100;
|
||
var CURSOR_CLICK_DELAY = 1000;
|
||
// Due to bug 1051238 frame scripts are cached forever, so we can't update them
|
||
// as a restartless add-on. The Math.random() is the work around for this.
|
||
var FRAME_SCRIPT = "chrome://loop/content/modules/tabFrame.js?" + Math.random();
|
||
|
||
Cu.import("resource://gre/modules/Services.jsm");
|
||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||
|
||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
|
||
"resource:///modules/CustomizableUI.jsm");
|
||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||
"resource://gre/modules/Task.jsm");
|
||
|
||
// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
|
||
var PREF_LOG_LEVEL = "loop.debug.loglevel";
|
||
|
||
XPCOMUtils.defineLazyGetter(this, "log", function () {
|
||
var ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
|
||
var consoleOptions = {
|
||
maxLogLevelPref: PREF_LOG_LEVEL,
|
||
prefix: "Loop" };
|
||
|
||
return new ConsoleAPI(consoleOptions);});
|
||
|
||
|
||
/**
|
||
* This window listener gets loaded into each browser.xul window and is used
|
||
* to provide the required loop functions for the window.
|
||
*/
|
||
var WindowListener = {
|
||
// Records the add-on version once we know it.
|
||
addonVersion: "unknown",
|
||
|
||
/**
|
||
* Sets up the chrome integration within browser windows for Loop.
|
||
*
|
||
* @param {Object} window The window to inject the integration into.
|
||
*/
|
||
setupBrowserUI: function setupBrowserUI(window) {
|
||
var document = window.document;var
|
||
gBrowser = window.gBrowser;var gURLBar = window.gURLBar;
|
||
var xhrClass = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"];
|
||
var FileReader = window.FileReader;
|
||
var menuItem = null;
|
||
var isSlideshowOpen = false;
|
||
var titleChangedListener = null;
|
||
|
||
// the "exported" symbols
|
||
var LoopUI = {
|
||
/**
|
||
* @var {XULWidgetSingleWrapper} toolbarButton Getter for the Loop toolbarbutton
|
||
* instance for this window. This should
|
||
* not be used in the hidden 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() {
|
||
var browser = document.querySelector("#loop-notification-panel > #loop-panel-iframe");
|
||
if (browser) {
|
||
delete this.browser;
|
||
this.browser = browser;}
|
||
|
||
return browser;},
|
||
|
||
|
||
get isSlideshowOpen() {
|
||
return isSlideshowOpen;},
|
||
|
||
|
||
set isSlideshowOpen(aOpen) {
|
||
isSlideshowOpen = aOpen;
|
||
this.updateToolbarState();},
|
||
|
||
/**
|
||
* @return {Object} Getter for the Loop constants
|
||
*/
|
||
get constants() {var _this = this;
|
||
if (!this._constants) {
|
||
// GetAllConstants is synchronous even though it's using a callback.
|
||
this.LoopAPI.sendMessageToHandler({
|
||
name: "GetAllConstants" },
|
||
function (result) {
|
||
_this._constants = result;});}
|
||
|
||
|
||
|
||
return this._constants;},
|
||
|
||
|
||
get mm() {
|
||
return window.getGroupMessageManager("browsers");},
|
||
|
||
|
||
/**
|
||
* @return {Promise}
|
||
*/
|
||
promiseDocumentVisible: function promiseDocumentVisible(aDocument) {
|
||
if (!aDocument.hidden) {
|
||
return Promise.resolve(aDocument);}
|
||
|
||
|
||
return new Promise(function (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.
|
||
* @return {Promise}
|
||
*/
|
||
togglePanel: function togglePanel(event) {var _this2 = this;
|
||
if (!this.panel) {var _ret = function () {
|
||
// We're on the hidden window! What fun!
|
||
var obs = function obs(win) {
|
||
Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
|
||
win.LoopUI.togglePanel(event);};
|
||
|
||
Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
|
||
return { v: window.OpenBrowserWindow() };}();if ((typeof _ret === "undefined" ? "undefined" : _typeof(_ret)) === "object") return _ret.v;}
|
||
|
||
if (this.panel.state == "open") {
|
||
return new Promise(function (resolve) {
|
||
_this2.panel.hidePopup();
|
||
resolve();});}
|
||
|
||
|
||
|
||
if (this.isSlideshowOpen) {
|
||
return Promise.resolve();}
|
||
|
||
|
||
return this.openPanel(event).then(function (mm) {
|
||
if (mm) {
|
||
mm.sendAsyncMessage("Social:EnsureFocusElement");}}).
|
||
|
||
catch(function (err) {
|
||
Cu.reportError(err);});},
|
||
|
||
|
||
|
||
/**
|
||
* Called when a closing room has just been created, so we offer the
|
||
* user the chance to modify the name. For that we need to open the panel.
|
||
* Showing the proper layout is done on panel.jsx
|
||
*/
|
||
renameRoom: function renameRoom() {
|
||
this.openPanel();},
|
||
|
||
|
||
/**
|
||
* 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.
|
||
* @return {Promise}
|
||
*/
|
||
openPanel: function openPanel(event) {var _this3 = this;
|
||
if (PrivateBrowsingUtils.isWindowPrivate(window)) {
|
||
return Promise.reject();}
|
||
|
||
|
||
return new Promise(function (resolve) {
|
||
var callback = function callback(iframe) {
|
||
var mm = iframe.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
|
||
if (!("messageManager" in iframe)) {
|
||
iframe.messageManager = mm;}
|
||
|
||
|
||
if (!_this3._panelInitialized) {
|
||
_this3.hookWindowCloseForPanelClose(iframe);
|
||
_this3._panelInitialized = true;}
|
||
|
||
|
||
mm.sendAsyncMessage("Social:WaitForDocumentVisible");
|
||
mm.addMessageListener("Social:DocumentVisible", function onDocumentVisible() {
|
||
mm.removeMessageListener("Social:DocumentVisible", onDocumentVisible);
|
||
resolve(mm);});
|
||
|
||
|
||
var buckets = _this3.constants.LOOP_MAU_TYPE;
|
||
_this3.LoopAPI.sendMessageToHandler({
|
||
name: "TelemetryAddValue",
|
||
data: ["LOOP_ACTIVITY_COUNTER", buckets.OPEN_PANEL] });};
|
||
|
||
|
||
|
||
// Used to clear the temporary "login" state from the button.
|
||
Services.obs.notifyObservers(null, "loop-status-changed", null);
|
||
|
||
_this3.shouldResumeTour().then(function (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.
|
||
_this3.MozLoopService.resumeTour("waiting");
|
||
resolve(null);
|
||
return;}
|
||
|
||
|
||
_this3.LoopAPI.initialize();
|
||
|
||
var anchor = event ? event.target : _this3.toolbarButton.anchor;
|
||
_this3.PanelFrame.showPopup(
|
||
window,
|
||
anchor,
|
||
"loop", // Notification Panel Type
|
||
null, // Origin
|
||
"about:looppanel", // Source
|
||
null, // Size
|
||
callback);});});},
|
||
|
||
|
||
|
||
|
||
/**
|
||
* Wrapper for openPanel - to support Firefox 46 and 45.
|
||
*
|
||
* @param {event} event The event opening the panel, used to anchor
|
||
* the panel to the button which triggers it.
|
||
* @return {Promise}
|
||
*/
|
||
openCallPanel: function openCallPanel(event) {
|
||
return this.openPanel(event);},
|
||
|
||
|
||
/**
|
||
* 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;}
|
||
|
||
|
||
var 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 roomsWithNonOwners() {var _this4 = this;
|
||
return new Promise(function (resolve) {
|
||
_this4.LoopRooms.getAll(function (error, rooms) {
|
||
var roomsWithNonOwners = [];var _iteratorNormalCompletion = true;var _didIteratorError = false;var _iteratorError = undefined;try {
|
||
for (var _iterator = rooms[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {var room = _step.value;
|
||
if (!("participants" in room)) {
|
||
continue;}
|
||
|
||
var numNonOwners = room.participants.filter(function (participant) {return !participant.owner;}).length;
|
||
if (!numNonOwners) {
|
||
continue;}
|
||
|
||
roomsWithNonOwners.push(room);}} catch (err) {_didIteratorError = true;_iteratorError = err;} finally {try {if (!_iteratorNormalCompletion && _iterator.return) {_iterator.return();}} finally {if (_didIteratorError) {throw _iteratorError;}}}
|
||
|
||
resolve(roomsWithNonOwners);});});},
|
||
|
||
|
||
|
||
|
||
/**
|
||
* Triggers the initialization of the loop service if necessary.
|
||
* Also adds appropraite observers for the UI.
|
||
*/
|
||
init: function init() {var _this5 = this;
|
||
// 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(WindowListener.addonVersion).catch(function (ex) {
|
||
if (!ex.message ||
|
||
!ex.message.contains("not enabled") &&
|
||
!ex.message.contains("not needed")) {
|
||
console.error(ex);}});
|
||
|
||
|
||
|
||
// If we're in private browsing mode, then don't add the menu item,
|
||
// also don't add the listeners as we don't want to update the button.
|
||
if (PrivateBrowsingUtils.isWindowPrivate(window)) {
|
||
return;}
|
||
|
||
|
||
this.addMenuItem();
|
||
|
||
// Don't do the rest if this is for the hidden window - we don't
|
||
// have a toolbar there.
|
||
if (window == Services.appShell.hiddenDOMWindow) {
|
||
return;}
|
||
|
||
|
||
// Load the frame script into any tab, plus any that get created in the
|
||
// future.
|
||
this.mm.loadFrameScript(FRAME_SCRIPT, true);
|
||
|
||
// Cleanup when the window unloads.
|
||
window.addEventListener("unload", function () {
|
||
Services.obs.removeObserver(_this5, "loop-status-changed");});
|
||
|
||
|
||
Services.obs.addObserver(this, "loop-status-changed", false);
|
||
|
||
this.maybeAddCopyPanel();
|
||
this.updateToolbarState();},
|
||
|
||
|
||
/**
|
||
* Adds a menu item to the browsers' Tools menu that open the Loop panel
|
||
* when selected.
|
||
*/
|
||
addMenuItem: function addMenuItem() {var _this6 = this;
|
||
var menu = document.getElementById("menu_ToolsPopup");
|
||
if (!menu || menuItem) {
|
||
return;}
|
||
|
||
|
||
menuItem = document.createElementNS(kNSXUL, "menuitem");
|
||
menuItem.setAttribute("id", "menu_openLoop");
|
||
menuItem.setAttribute("label", this._getString("loopMenuItem_label"));
|
||
menuItem.setAttribute("accesskey", this._getString("loopMenuItem_accesskey"));
|
||
|
||
menuItem.addEventListener("command", function () {return _this6.togglePanel();});
|
||
|
||
menu.insertBefore(menuItem, document.getElementById("sync-setup"));},
|
||
|
||
|
||
/**
|
||
* Removes the menu item from the browsers' Tools menu.
|
||
*/
|
||
removeMenuItem: function removeMenuItem() {
|
||
if (menuItem) {
|
||
menuItem.parentNode.removeChild(menuItem);}},
|
||
|
||
|
||
|
||
/**
|
||
* Maybe add the copy panel if it's not throttled and passes other checks.
|
||
* @return {Promise} Resolved when decided and maybe panel-added.
|
||
*/
|
||
maybeAddCopyPanel: function maybeAddCopyPanel() {var _this7 = this;
|
||
// Don't bother adding the copy panel if we're in private browsing or
|
||
// the user wants to never see it again or we've shown it enough times.
|
||
if (PrivateBrowsingUtils.isWindowPrivate(window) ||
|
||
Services.prefs.getBoolPref("loop.copy.shown") ||
|
||
Services.prefs.getIntPref("loop.copy.showLimit") <= 0) {
|
||
return Promise.resolve();}
|
||
|
||
|
||
return Throttler.check("loop.copy").then(function () {return _this7.addCopyPanel();});},
|
||
|
||
|
||
/**
|
||
* Hook into the location bar copy command to open up the copy panel.
|
||
* @param {Function} onClickHandled Optional callback for finished clicks.
|
||
*/
|
||
addCopyPanel: function addCopyPanel(onClickHandled) {var _this8 = this, _arguments = arguments;
|
||
// Make a copy of the loop panel as a starting point for the copy panel.
|
||
var copy = this.panel.cloneNode(false);
|
||
copy.id = "loop-copy-notification-panel";
|
||
this.panel.parentNode.appendChild(copy);
|
||
|
||
// Record a telemetry copy panel action.
|
||
var addTelemetry = function addTelemetry(bucket) {
|
||
_this8.LoopAPI.sendMessageToHandler({
|
||
data: ["LOOP_COPY_PANEL_ACTIONS", _this8.constants.COPY_PANEL[bucket]],
|
||
name: "TelemetryAddValue" });};
|
||
|
||
|
||
|
||
// Handle events from the copy panel iframe content.
|
||
var onIframe = function onIframe(iframe) {
|
||
// Watch for events from the copy panel when loaded.
|
||
iframe.addEventListener("DOMContentLoaded", function onLoad() {
|
||
iframe.removeEventListener("DOMContentLoaded", onLoad);
|
||
|
||
// Size the panel to fit the rendered content adjusting for borders.
|
||
iframe.contentWindow.requestAnimationFrame(function () {
|
||
var height = iframe.contentDocument.documentElement.offsetHeight;
|
||
height += copy.boxObject.height - iframe.boxObject.height;
|
||
copy.style.height = height + "px";});
|
||
|
||
|
||
// Hide the copy panel then show the loop panel.
|
||
iframe.contentWindow.addEventListener("CopyPanelClick", function (event) {
|
||
iframe.parentNode.hidePopup();
|
||
|
||
// Show the Loop panel if the user wants it.
|
||
var _event$detail = event.detail;var accept = _event$detail.accept;var stop = _event$detail.stop;
|
||
if (accept) {
|
||
LoopUI.openPanel();}
|
||
|
||
|
||
// Stop showing the panel if the user says so.
|
||
if (stop) {
|
||
LoopUI.removeCopyPanel();
|
||
Services.prefs.setBoolPref("loop.copy.shown", true);}
|
||
|
||
|
||
// Generate the appropriate NO_AGAIN, NO_NEVER, YES_AGAIN,
|
||
// YES_NEVER probe based on the user's action.
|
||
var probe = (accept ? "YES" : "NO") + "_" + (stop ? "NEVER" : "AGAIN");
|
||
addTelemetry(probe);
|
||
|
||
// For testing, indicate that handling the click has finished.
|
||
try {
|
||
onClickHandled(event.detail);}
|
||
catch (ex) {
|
||
// Do nothing.
|
||
}});});};
|
||
|
||
|
||
|
||
|
||
// Override the default behavior of the copy command.
|
||
var controller = gURLBar._copyCutController;
|
||
controller._doCommand = controller.doCommand;
|
||
controller.doCommand = function () {
|
||
// Do the normal behavior first.
|
||
controller._doCommand.apply(controller, _arguments);
|
||
|
||
// Remove the panel if the user has seen it enough times.
|
||
var showLimit = Services.prefs.getIntPref("loop.copy.showLimit");
|
||
if (showLimit <= 0) {
|
||
LoopUI.removeCopyPanel();
|
||
return;}
|
||
|
||
|
||
// Don't bother prompting the user if already sharing.
|
||
if (_this8.MozLoopService.screenShareActive) {
|
||
return;}
|
||
|
||
|
||
// Update various counters.
|
||
Services.prefs.setIntPref("loop.copy.showLimit", showLimit - 1);
|
||
addTelemetry("SHOWN");
|
||
|
||
// Open up the copy panel at the loop button.
|
||
LoopUI.PanelFrame.showPopup(window, LoopUI.toolbarButton.anchor, "loop-copy",
|
||
null, "chrome://loop/content/panels/copy.html", null, onIframe);};},
|
||
|
||
|
||
|
||
/**
|
||
* Removes the copy panel copy hook and the panel.
|
||
*/
|
||
removeCopyPanel: function removeCopyPanel() {
|
||
var controller = gURLBar && gURLBar._copyCutController;
|
||
if (controller && controller._doCommand) {
|
||
controller.doCommand = controller._doCommand;
|
||
delete controller._doCommand;}
|
||
|
||
|
||
var copy = document.getElementById("loop-copy-notification-panel");
|
||
if (copy) {
|
||
copy.parentNode.removeChild(copy);}},
|
||
|
||
|
||
|
||
// Implements nsIObserver
|
||
observe: function observe(subject, topic, data) {
|
||
if (topic != "loop-status-changed") {
|
||
return;}
|
||
|
||
this.updateToolbarState(data);},
|
||
|
||
|
||
/**
|
||
* Updates the toolbar/menu-button state to reflect Loop status. This should
|
||
* not be called from the hidden window.
|
||
*
|
||
* @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 updateToolbarState() {var _this9 = this;var aReason = arguments.length <= 0 || arguments[0] === undefined ? null : arguments[0];
|
||
if (!this.toolbarButton.node) {
|
||
return;}
|
||
|
||
var state = "";
|
||
var mozL10nId = "loop-call-button3";
|
||
var suffix = ".tooltiptext";
|
||
if (this.MozLoopService.errors.size) {
|
||
state = "error";
|
||
mozL10nId += "-error";} else
|
||
if (this.isSlideshowOpen) {
|
||
state = "slideshow";} 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(function (roomsWithNonOwners) {
|
||
if (roomsWithNonOwners.length > 0) {
|
||
mozL10nId += "-participantswaiting";} else
|
||
{
|
||
mozL10nId += "-active";}
|
||
|
||
|
||
suffix += "2";
|
||
_this9.updateTooltiptext(mozL10nId + suffix);
|
||
_this9.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. This should not be called
|
||
* from the hidden window.
|
||
*
|
||
* @param {string} [mozL10nId] l10n ID that refelct the current
|
||
* Loop status.
|
||
*/
|
||
updateTooltiptext: function updateTooltiptext(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 showNotification(options) {var _this10 = this;
|
||
if (this.MozLoopService.doNotDisturb) {
|
||
return;}
|
||
|
||
|
||
if (!options.title) {
|
||
throw new Error("Missing title, can not display notification");}
|
||
|
||
|
||
var 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);}
|
||
|
||
|
||
var notification = new window.Notification(options.title, notificationOptions);
|
||
notification.addEventListener("click", function () {
|
||
if (window.closed) {
|
||
return;}
|
||
|
||
|
||
try {
|
||
window.focus();}
|
||
catch (ex) {}
|
||
// Do nothing.
|
||
|
||
|
||
// We need a setTimeout here, otherwise the panel won't show after the
|
||
// window received focus.
|
||
window.setTimeout(function () {
|
||
if (typeof options.onclick == "function") {
|
||
options.onclick();} else
|
||
{
|
||
// Open the Loop panel as a default action.
|
||
_this10.openPanel(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 playSound(name) {var _this11 = this;
|
||
if (this.ActiveSound || this.MozLoopService.doNotDisturb) {
|
||
return;}
|
||
|
||
|
||
this.activeSound = new window.Audio();
|
||
this.activeSound.src = "chrome://loop/content/shared/sounds/" + name + ".ogg";
|
||
this.activeSound.load();
|
||
this.activeSound.play();
|
||
|
||
this.activeSound.addEventListener("ended", function () {_this11.activeSound = undefined;}, false);},
|
||
|
||
|
||
/**
|
||
* Start listening to selected tab changes and notify any content page that's
|
||
* listening to 'BrowserSwitch' push messages. Also sets up a "joined"
|
||
* and "left" listener for LoopRooms so that we can toggle the infobar
|
||
* sharing messages when people come and go.
|
||
*
|
||
* @param {(String)} roomToken The current room that the link generator is connecting to.
|
||
*/
|
||
startBrowserSharing: function startBrowserSharing(roomToken) {var _this12 = this;
|
||
if (!this._listeningToTabSelect) {
|
||
gBrowser.tabContainer.addEventListener("TabSelect", this);
|
||
this._listeningToTabSelect = true;
|
||
|
||
titleChangedListener = this.handleDOMTitleChanged.bind(this);
|
||
|
||
this._roomsListener = this.handleRoomJoinedOrLeft.bind(this);
|
||
|
||
this.LoopRooms.on("joined", this._roomsListener);
|
||
this.LoopRooms.on("left", this._roomsListener);
|
||
|
||
// Watch for title changes as opposed to location changes as more
|
||
// metadata about the page is available when this event fires.
|
||
this.mm.addMessageListener("loop@mozilla.org:DOMTitleChanged",
|
||
titleChangedListener);
|
||
|
||
this._browserSharePaused = false;
|
||
|
||
// Add this event to the parent gBrowser to avoid adding and removing
|
||
// it for each individual tab's browsers.
|
||
gBrowser.addEventListener("mousemove", this);
|
||
gBrowser.addEventListener("click", this);}
|
||
|
||
|
||
this._currentRoomToken = roomToken;
|
||
this._maybeShowBrowserSharingInfoBar(roomToken);
|
||
|
||
// Get the first window Id for the listener.
|
||
var browser = gBrowser.selectedBrowser;
|
||
return new Promise(function (resolve) {
|
||
if (browser.outerWindowID) {
|
||
resolve(browser.outerWindowID);
|
||
return;}
|
||
|
||
|
||
browser.messageManager.addMessageListener("Browser:Init", function initListener() {
|
||
browser.messageManager.removeMessageListener("Browser:Init", initListener);
|
||
resolve(browser.outerWindowID);});}).
|
||
|
||
then(function (outerWindowID) {return (
|
||
_this12.LoopAPI.broadcastPushMessage("BrowserSwitch", outerWindowID));});},
|
||
|
||
|
||
/**
|
||
* Stop listening to selected tab changes.
|
||
*/
|
||
stopBrowserSharing: function stopBrowserSharing() {
|
||
if (!this._listeningToTabSelect) {
|
||
return;}
|
||
|
||
|
||
this._hideBrowserSharingInfoBar();
|
||
gBrowser.tabContainer.removeEventListener("TabSelect", this);
|
||
this.LoopRooms.off("joined", this._roomsListener);
|
||
this.LoopRooms.off("left", this._roomsListener);
|
||
|
||
if (titleChangedListener) {
|
||
this.mm.removeMessageListener("loop@mozilla.org:DOMTitleChanged",
|
||
titleChangedListener);
|
||
titleChangedListener = null;}
|
||
|
||
|
||
// Remove shared pointers related events
|
||
gBrowser.removeEventListener("mousemove", this);
|
||
gBrowser.removeEventListener("click", this);
|
||
this.removeRemoteCursor();
|
||
|
||
this._listeningToTabSelect = false;
|
||
this._browserSharePaused = false;
|
||
this._currentRoomToken = null;
|
||
|
||
this._sendTelemetryEventsIfNeeded();},
|
||
|
||
|
||
/**
|
||
* Sends telemetry events for pause/ resume buttons if needed.
|
||
*/
|
||
_sendTelemetryEventsIfNeeded: function _sendTelemetryEventsIfNeeded() {
|
||
// The user can't click Resume button without clicking Pause button first.
|
||
if (!this._pauseButtonClicked) {
|
||
return;}
|
||
|
||
|
||
var buckets = this.constants.SHARING_SCREEN;
|
||
this.LoopAPI.sendMessageToHandler({
|
||
name: "TelemetryAddValue",
|
||
data: [
|
||
"LOOP_INFOBAR_ACTION_BUTTONS",
|
||
buckets.PAUSED] });
|
||
|
||
|
||
|
||
if (this._resumeButtonClicked) {
|
||
this.LoopAPI.sendMessageToHandler({
|
||
name: "TelemetryAddValue",
|
||
data: [
|
||
"LOOP_INFOBAR_ACTION_BUTTONS",
|
||
buckets.RESUMED] });}
|
||
|
||
|
||
|
||
|
||
this._pauseButtonClicked = false;
|
||
this._resumeButtonClicked = false;},
|
||
|
||
|
||
/**
|
||
* If sharing is active, paints and positions the remote cursor
|
||
* over the screen
|
||
*
|
||
* @param cursorData Object with the correct position for the cursor
|
||
* {
|
||
* ratioX: position on the X axis (percentage value)
|
||
* ratioY: position on the Y axis (percentage value)
|
||
* }
|
||
*/
|
||
addRemoteCursor: function addRemoteCursor(cursorData) {
|
||
if (this._browserSharePaused || !this._listeningToTabSelect) {
|
||
return;}
|
||
|
||
|
||
var browser = gBrowser.selectedBrowser;
|
||
var cursor = document.getElementById("loop-remote-cursor");
|
||
if (!cursor) {
|
||
// Create a container to keep the pointer inside.
|
||
// This allows us to hide the overflow when out of bounds.
|
||
var cursorContainer = document.createElement("div");
|
||
cursorContainer.setAttribute("id", "loop-remote-cursor-container");
|
||
|
||
cursor = document.createElement("img");
|
||
cursor.setAttribute("id", "loop-remote-cursor");
|
||
cursorContainer.appendChild(cursor);
|
||
// Note that browser.parent is a xul:stack so container will use
|
||
// 100% of space if no other constrains added.
|
||
browser.parentNode.appendChild(cursorContainer);}
|
||
|
||
|
||
// Update the cursor's position with CSS.
|
||
cursor.style.left =
|
||
Math.abs(cursorData.ratioX * browser.boxObject.width) + "px";
|
||
cursor.style.top =
|
||
Math.abs(cursorData.ratioY * browser.boxObject.height) + "px";},
|
||
|
||
|
||
/**
|
||
* Adds the ripple effect animation to the cursor to show a click on the
|
||
* remote end of the conversation.
|
||
* Will only add it when:
|
||
* - A click is received (cursorData = true)
|
||
* - Sharing is active (this._listeningToTabSelect = true)
|
||
* - Remote cursor is being painted (cursor != undefined)
|
||
*
|
||
* @param clickData bool click event
|
||
*/
|
||
clickRemoteCursor: function clickRemoteCursor(clickData) {
|
||
if (!clickData || !this._listeningToTabSelect) {
|
||
return;}
|
||
|
||
|
||
var class_name = "clicked";
|
||
var cursor = document.getElementById("loop-remote-cursor");
|
||
if (!cursor) {
|
||
return;}
|
||
|
||
|
||
cursor.classList.add(class_name);
|
||
|
||
// after the proper time, we get rid of the animation
|
||
window.setTimeout(function () {
|
||
cursor.classList.remove(class_name);},
|
||
CURSOR_CLICK_DELAY);},
|
||
|
||
|
||
/**
|
||
* Removes the remote cursor from the screen
|
||
*/
|
||
removeRemoteCursor: function removeRemoteCursor() {
|
||
var cursor = document.getElementById("loop-remote-cursor");
|
||
|
||
if (cursor) {
|
||
cursor.parentNode.removeChild(cursor);}},
|
||
|
||
|
||
|
||
/**
|
||
* 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 _getString(key) {
|
||
var str = this.MozLoopService.getStrings(key);
|
||
if (str) {
|
||
str = JSON.parse(str).textContent;}
|
||
|
||
return str;},
|
||
|
||
|
||
/**
|
||
* Set correct strings for infobar notification based on if paused or empty.
|
||
*/
|
||
|
||
_setInfoBarStrings: function _setInfoBarStrings(nonOwnerParticipants, sharePaused) {
|
||
var message = void 0;
|
||
if (nonOwnerParticipants) {
|
||
// More than just the owner in the room.
|
||
message = this._getString(
|
||
sharePaused ? "infobar_screenshare_stop_sharing_message2" :
|
||
"infobar_screenshare_browser_message3");} else
|
||
|
||
{
|
||
// Just the owner in the room.
|
||
message = this._getString(
|
||
sharePaused ? "infobar_screenshare_stop_no_guest_message" :
|
||
"infobar_screenshare_no_guest_message");}
|
||
|
||
var label = this._getString(
|
||
sharePaused ? "infobar_button_restart_label2" : "infobar_button_stop_label2");
|
||
var accessKey = this._getString(
|
||
sharePaused ? "infobar_button_restart_accesskey" : "infobar_button_stop_accesskey");
|
||
|
||
return { message: message, label: label, accesskey: accessKey };},
|
||
|
||
|
||
/**
|
||
* 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,
|
||
|
||
/**
|
||
* 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);
|
||
|
||
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.
|
||
null, // image
|
||
box.PRIORITY_WARNING_LOW, // priority
|
||
[{ // buttons (Pause, Stop)
|
||
label: initStrings.label,
|
||
accessKey: initStrings.accesskey,
|
||
isDefault: false,
|
||
callback: function callback(event, buttonInfo, buttonNode) {
|
||
_this13._browserSharePaused = !_this13._browserSharePaused;
|
||
var guestPresent = _this13.LoopRooms.getNumParticipants(_this13._currentRoomToken) > 1;
|
||
var stringObj = _this13._setInfoBarStrings(guestPresent, _this13._browserSharePaused);
|
||
bar.label = stringObj.message;
|
||
bar.classList.toggle("paused", _this13._browserSharePaused);
|
||
buttonNode.label = stringObj.label;
|
||
buttonNode.accessKey = stringObj.accesskey;
|
||
LoopUI.MozLoopService.toggleBrowserSharing(_this13._browserSharePaused);
|
||
if (_this13._browserSharePaused) {
|
||
_this13._pauseButtonClicked = true;
|
||
// if paused we stop sharing remote cursors
|
||
_this13.removeRemoteCursor();} else
|
||
{
|
||
_this13._resumeButtonClicked = true;}
|
||
|
||
return true;},
|
||
|
||
type: "pause" },
|
||
|
||
{
|
||
label: this._getString("infobar_button_disconnect_label"),
|
||
accessKey: this._getString("infobar_button_disconnect_accesskey"),
|
||
isDefault: true,
|
||
callback: function callback() {
|
||
_this13.removeRemoteCursor();
|
||
_this13._hideBrowserSharingInfoBar();
|
||
LoopUI.MozLoopService.hangupAllChatWindows();},
|
||
|
||
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;},
|
||
|
||
|
||
/**
|
||
* 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.
|
||
* @return {Boolean} |true| if the infobar was hidden here.
|
||
*/
|
||
_hideBrowserSharingInfoBar: function _hideBrowserSharingInfoBar(browser) {
|
||
browser = browser || gBrowser.selectedBrowser;
|
||
var box = gBrowser.getNotificationBox(browser);
|
||
var notification = box.getNotificationWithValue(kBrowserSharingNotificationId);
|
||
var removed = false;
|
||
if (notification) {
|
||
box.removeNotification(notification);
|
||
removed = true;}
|
||
|
||
|
||
return removed;},
|
||
|
||
|
||
/**
|
||
* Broadcast 'BrowserSwitch' event.
|
||
*/
|
||
_notifyBrowserSwitch: function _notifyBrowserSwitch() {
|
||
// Get the first window Id for the listener.
|
||
this.LoopAPI.broadcastPushMessage("BrowserSwitch",
|
||
gBrowser.selectedBrowser.outerWindowID);},
|
||
|
||
|
||
/**
|
||
* Handles updating of the sharing infobar when the room participants
|
||
* change.
|
||
*/
|
||
handleRoomJoinedOrLeft: function handleRoomJoinedOrLeft() {
|
||
// Don't attempt to show it if we're not actively sharing.
|
||
if (!this._listeningToTabSelect) {
|
||
return;}
|
||
|
||
this._maybeShowBrowserSharingInfoBar(this._currentRoomToken);},
|
||
|
||
|
||
/**
|
||
* Handles events from the frame script.
|
||
*
|
||
* @param {Object} message The message received from the frame script.
|
||
*/
|
||
handleDOMTitleChanged: function handleDOMTitleChanged(message) {
|
||
if (!this._listeningToTabSelect || this._browserSharePaused) {
|
||
return;}
|
||
|
||
|
||
if (gBrowser.selectedBrowser == message.target) {
|
||
// Get the new title of the shared tab
|
||
this._notifyBrowserSwitch();}},
|
||
|
||
|
||
|
||
/**
|
||
* Handles events from gBrowser.
|
||
*/
|
||
handleEvent: function handleEvent(event) {
|
||
|
||
switch (event.type) {
|
||
case "TabSelect":{
|
||
var wasVisible = false;
|
||
// Hide the infobar from the previous tab.
|
||
if (event.detail.previousTab) {
|
||
wasVisible = this._hideBrowserSharingInfoBar(
|
||
event.detail.previousTab.linkedBrowser);
|
||
// And remove the cursor.
|
||
this.removeRemoteCursor();}
|
||
|
||
|
||
// We've changed the tab, so get the new window id.
|
||
this._notifyBrowserSwitch();
|
||
|
||
if (wasVisible) {
|
||
// If the infobar was visible before, we should show it again after the
|
||
// switch.
|
||
this._maybeShowBrowserSharingInfoBar(this._currentRoomToken);}
|
||
|
||
break;}
|
||
|
||
case "mousemove":
|
||
this.handleMousemove(event);
|
||
break;
|
||
case "click":
|
||
this.handleMouseClick(event);
|
||
break;}},
|
||
|
||
|
||
|
||
/**
|
||
* Handles mousemove events from gBrowser and send a broadcast message
|
||
* with all the data needed for sending link generator cursor position
|
||
* through the sdk.
|
||
*/
|
||
handleMousemove: function handleMousemove(event) {
|
||
// Won't send events if not sharing (paused or not started).
|
||
if (this._browserSharePaused || !this._listeningToTabSelect) {
|
||
return;}
|
||
|
||
|
||
// Only update every so often.
|
||
var now = Date.now();
|
||
if (now - this.lastCursorTime < CURSOR_MIN_INTERVAL) {
|
||
return;}
|
||
|
||
this.lastCursorTime = now;
|
||
|
||
// Skip the update if cursor is out of bounds or didn't move much.
|
||
var browserBox = gBrowser.selectedBrowser.boxObject;
|
||
var deltaX = event.screenX - browserBox.screenX;
|
||
var deltaY = event.screenY - browserBox.screenY;
|
||
if (deltaX < 0 || deltaX > browserBox.width ||
|
||
deltaY < 0 || deltaY > browserBox.height ||
|
||
Math.abs(deltaX - this.lastCursorX) < CURSOR_MIN_DELTA &&
|
||
Math.abs(deltaY - this.lastCursorY) < CURSOR_MIN_DELTA) {
|
||
return;}
|
||
|
||
this.lastCursorX = deltaX;
|
||
this.lastCursorY = deltaY;
|
||
|
||
this.LoopAPI.broadcastPushMessage("CursorPositionChange", {
|
||
ratioX: deltaX / browserBox.width,
|
||
ratioY: deltaY / browserBox.height });},
|
||
|
||
|
||
|
||
/**
|
||
* Handles mouse click events from gBrowser and send a broadcast message
|
||
* with all the data needed for sending link generator cursor click position
|
||
* through the sdk.
|
||
*/
|
||
handleMouseClick: function handleMouseClick() {
|
||
// We want to stop sending events if sharing is paused.
|
||
if (this._browserSharePaused) {
|
||
return;}
|
||
|
||
|
||
this.LoopAPI.broadcastPushMessage("CursorClick");},
|
||
|
||
|
||
/**
|
||
* 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 getFavicon(callback) {
|
||
var 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(function (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.
|
||
var xhr = xhrClass.createInstance(Ci.nsIXMLHttpRequest);
|
||
xhr.open("get", uri.spec, true);
|
||
xhr.responseType = "blob";
|
||
xhr.overrideMimeType("image/x-icon");
|
||
xhr.onload = function () {
|
||
if (xhr.status != 200) {
|
||
callback(new Error("Invalid status code received for favicon XHR: " + xhr.status));
|
||
return;}
|
||
|
||
|
||
var reader = new FileReader();
|
||
reader.onload = reader.onload = function () {return callback(null, reader.result);};
|
||
reader.onerror = callback;
|
||
reader.readAsDataURL(xhr.response);};
|
||
|
||
xhr.onerror = callback;
|
||
xhr.send();}).
|
||
catch(function (err) {
|
||
callback(err || new Error("No favicon found"));});} };
|
||
|
||
|
||
|
||
|
||
XPCOMUtils.defineLazyModuleGetter(LoopUI, "hookWindowCloseForPanelClose", "resource://gre/modules/MozSocialAPI.jsm");
|
||
XPCOMUtils.defineLazyModuleGetter(LoopUI, "LoopAPI", "chrome://loop/content/modules/MozLoopAPI.jsm");
|
||
XPCOMUtils.defineLazyModuleGetter(LoopUI, "LoopRooms", "chrome://loop/content/modules/LoopRooms.jsm");
|
||
XPCOMUtils.defineLazyModuleGetter(LoopUI, "MozLoopService", "chrome://loop/content/modules/MozLoopService.jsm");
|
||
XPCOMUtils.defineLazyModuleGetter(LoopUI, "PanelFrame", "resource:///modules/PanelFrame.jsm");
|
||
XPCOMUtils.defineLazyModuleGetter(LoopUI, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
|
||
|
||
LoopUI.init();
|
||
window.LoopUI = LoopUI;
|
||
|
||
// Export the Throttler to allow tests to overwrite parts of it.
|
||
window.LoopThrottler = Throttler;},
|
||
|
||
|
||
/**
|
||
* Take any steps to remove UI or anything from the browser window
|
||
* document.getElementById() etc. will work here.
|
||
*
|
||
* @param {Object} window The window to remove the integration from.
|
||
*/
|
||
tearDownBrowserUI: function tearDownBrowserUI(window) {
|
||
if (window.LoopUI) {
|
||
window.LoopUI.removeCopyPanel();
|
||
window.LoopUI.removeMenuItem();
|
||
|
||
// This stops the frame script being loaded to new tabs, but doesn't
|
||
// remove it from existing tabs (there's no way to do that).
|
||
window.LoopUI.mm.removeDelayedFrameScript(FRAME_SCRIPT);
|
||
|
||
// XXX Bug 1229352 - Add in tear-down of the panel.
|
||
}},
|
||
|
||
|
||
// nsIWindowMediatorListener functions.
|
||
onOpenWindow: function onOpenWindow(xulWindow) {
|
||
// A new window has opened.
|
||
var domWindow = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor).
|
||
getInterface(Ci.nsIDOMWindow);
|
||
|
||
// Wait for it to finish loading.
|
||
domWindow.addEventListener("load", function listener() {
|
||
domWindow.removeEventListener("load", listener, false);
|
||
|
||
// If this is a browser window then setup its UI.
|
||
if (domWindow.document.documentElement.getAttribute("windowtype") == "navigator:browser") {
|
||
WindowListener.setupBrowserUI(domWindow);}},
|
||
|
||
false);},
|
||
|
||
|
||
onCloseWindow: function onCloseWindow() {},
|
||
|
||
|
||
onWindowTitleChange: function onWindowTitleChange() {} };
|
||
|
||
|
||
|
||
/**
|
||
* Provide a way to throttle functionality using DNS to distribute 3 numbers for
|
||
* various distributions channels. DNS is used to scale distribution of the
|
||
* numbers as an A record pointing to a loopback address (127.*.*.*). Prefs are
|
||
* used to control behavior (what domain to check) and keep state (a ticket
|
||
* number to track if it needs to initialize, to wait for its turn, or is
|
||
* completed).
|
||
*/
|
||
var Throttler = {
|
||
// Each 8-bit block of the IP address allows for 0% rollout (value 0) to 100%
|
||
// rollout (value 255).
|
||
TICKET_LIMIT: 255,
|
||
|
||
// Allow the DNS service to be overwritten for testing.
|
||
_dns: Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService),
|
||
|
||
/**
|
||
* Check if a given feature should be throttled or not.
|
||
* @param {string} [prefPrefix] Start of the preference name for the feature.
|
||
* @return {Promise} Resolved on success, and rejected on throttled.
|
||
*/
|
||
check: function check(prefPrefix) {var _this14 = this;
|
||
return new Promise(function (resolve, reject) {
|
||
// Initialize the ticket (0-254) if it doesn't have a valid value yet.
|
||
var prefTicket = prefPrefix + ".ticket";
|
||
var ticket = Services.prefs.getIntPref(prefTicket);
|
||
if (ticket < 0) {
|
||
ticket = Math.floor(Math.random() * _this14.TICKET_LIMIT);
|
||
Services.prefs.setIntPref(prefTicket, ticket);}
|
||
|
||
// Short circuit if the special ticket value indicates we're good to go.
|
||
else if (ticket >= _this14.TICKET_LIMIT) {
|
||
resolve();
|
||
return;}
|
||
|
||
|
||
// Handle responses from the DNS resolution service request.
|
||
var onDNS = function onDNS(request, record) {
|
||
// Failed to get A-record, so skip for now.
|
||
if (record === null) {
|
||
reject();
|
||
return;}
|
||
|
||
|
||
// Ensure we have a special loopback value before checking other blocks.
|
||
var ipBlocks = record.getNextAddrAsString().split(".");
|
||
if (ipBlocks[0] !== "127") {
|
||
reject();
|
||
return;}
|
||
|
||
|
||
// Use a specific part of the A-record IP address depending on the
|
||
// channel. I.e., 127.[release/other].[beta].[aurora/nightly].
|
||
var index = 1;
|
||
switch (Services.prefs.getCharPref("app.update.channel")) {
|
||
case "beta":
|
||
index = 2;
|
||
break;
|
||
case "aurora":
|
||
case "nightly":
|
||
index = 3;
|
||
break;}
|
||
|
||
|
||
// Select the 1 out of 4 parts of the "."-separated IP address to check
|
||
// if the 8-bit threshold (0-255) exceeds the ticket (0-254).
|
||
if (ticket < ipBlocks[index]) {
|
||
// Remember that we're good to go to avoid future DNS checks.
|
||
Services.prefs.setIntPref(prefTicket, _this14.TICKET_LIMIT);
|
||
resolve();} else
|
||
|
||
{
|
||
reject();}};
|
||
|
||
|
||
|
||
// Look up the DNS A-record of a throttler hostname to decide to show.
|
||
_this14._dns.asyncResolve(Services.prefs.getCharPref(prefPrefix + ".throttler"),
|
||
_this14._dns.RESOLVE_DISABLE_IPV6, onDNS, Services.tm.mainThread);});} };
|
||
|
||
|
||
|
||
|
||
/**
|
||
* Creates the loop button on the toolbar. Due to loop being a system-addon
|
||
* CustomizableUI already has a placement location for the button, so that
|
||
* we can be on the toolbar.
|
||
*/
|
||
function createLoopButton() {
|
||
CustomizableUI.createWidget({
|
||
id: "loop-button",
|
||
type: "custom",
|
||
label: "loop-call-button3.label",
|
||
tooltiptext: "loop-call-button3.tooltiptext2",
|
||
privateBrowsingTooltiptext: "loop-call-button3-pb.tooltiptext",
|
||
defaultArea: CustomizableUI.AREA_NAVBAR,
|
||
removable: true,
|
||
onBuild: function onBuild(aDocument) {
|
||
// If we're not supposed to see the button, return zip.
|
||
if (!Services.prefs.getBoolPref("loop.enabled")) {
|
||
return null;}
|
||
|
||
|
||
var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView);
|
||
|
||
var node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
|
||
node.setAttribute("id", this.id);
|
||
node.classList.add("toolbarbutton-1");
|
||
node.classList.add("chromeclass-toolbar-additional");
|
||
node.classList.add("badged-button");
|
||
node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
|
||
if (isWindowPrivate) {
|
||
node.setAttribute("disabled", "true");}
|
||
|
||
var tooltiptext = isWindowPrivate ?
|
||
CustomizableUI.getLocalizedProperty(this, "privateBrowsingTooltiptext",
|
||
[CustomizableUI.getLocalizedProperty(this, "label")]) :
|
||
CustomizableUI.getLocalizedProperty(this, "tooltiptext");
|
||
node.setAttribute("tooltiptext", tooltiptext);
|
||
node.setAttribute("removable", "true");
|
||
node.addEventListener("command", function (event) {
|
||
aDocument.defaultView.LoopUI.togglePanel(event);});
|
||
|
||
|
||
return node;} });}
|
||
|
||
|
||
|
||
|
||
/**
|
||
* Loads the default preferences from the prefs file. This loads the preferences
|
||
* into the default branch, so they don't appear as user preferences.
|
||
*/
|
||
function loadDefaultPrefs() {
|
||
var branch = Services.prefs.getDefaultBranch("");
|
||
Services.scriptloader.loadSubScript("chrome://loop/content/preferences/prefs.js", {
|
||
pref: function pref(key, val) {
|
||
// If a previously set default pref exists don't overwrite it. This can
|
||
// happen for ESR or distribution.ini.
|
||
if (branch.getPrefType(key) != branch.PREF_INVALID) {
|
||
return;}
|
||
|
||
switch (typeof val === "undefined" ? "undefined" : _typeof(val)) {
|
||
case "boolean":
|
||
branch.setBoolPref(key, val);
|
||
break;
|
||
case "number":
|
||
branch.setIntPref(key, val);
|
||
break;
|
||
case "string":
|
||
branch.setCharPref(key, val);
|
||
break;}} });
|
||
|
||
|
||
|
||
|
||
if (Services.vc.compare(Services.appinfo.version, "47.0a1") < 0) {
|
||
branch.setBoolPref("loop.remote.autostart", false);}}
|
||
|
||
|
||
|
||
/**
|
||
* Called when the add-on is started, e.g. when installed or when Firefox starts.
|
||
*/
|
||
function startup(data) {
|
||
// Record the add-on version for when the UI is initialised.
|
||
WindowListener.addonVersion = data.version;
|
||
|
||
loadDefaultPrefs();
|
||
if (!Services.prefs.getBoolPref("loop.enabled")) {
|
||
return;}
|
||
|
||
|
||
createLoopButton();
|
||
|
||
// Attach to hidden window (for OS X).
|
||
if (AppConstants.platform == "macosx") {
|
||
try {
|
||
WindowListener.setupBrowserUI(Services.appShell.hiddenDOMWindow);}
|
||
catch (ex) {(function () {
|
||
// Hidden window didn't exist, so wait until startup is done.
|
||
var topic = "browser-delayed-startup-finished";
|
||
Services.obs.addObserver(function observer() {
|
||
Services.obs.removeObserver(observer, topic);
|
||
WindowListener.setupBrowserUI(Services.appShell.hiddenDOMWindow);},
|
||
topic, false);})();}}
|
||
|
||
|
||
|
||
// Attach to existing browser windows, for modifying UI.
|
||
var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
|
||
var windows = wm.getEnumerator("navigator:browser");
|
||
while (windows.hasMoreElements()) {
|
||
var domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
|
||
WindowListener.setupBrowserUI(domWindow);}
|
||
|
||
|
||
// Wait for any new browser windows to open.
|
||
wm.addListener(WindowListener);
|
||
|
||
// Load our stylesheets.
|
||
var styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"].
|
||
getService(Components.interfaces.nsIStyleSheetService);
|
||
var sheets = ["chrome://loop-shared/skin/loop.css"];
|
||
|
||
if (AppConstants.platform != "linux") {
|
||
sheets.push("chrome://loop/skin/platform.css");}var _iteratorNormalCompletion2 = true;var _didIteratorError2 = false;var _iteratorError2 = undefined;try {
|
||
|
||
|
||
for (var _iterator2 = sheets[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {var sheet = _step2.value;
|
||
var styleSheetURI = Services.io.newURI(sheet, null, null);
|
||
styleSheetService.loadAndRegisterSheet(styleSheetURI,
|
||
styleSheetService.AUTHOR_SHEET);}} catch (err) {_didIteratorError2 = true;_iteratorError2 = err;} finally {try {if (!_iteratorNormalCompletion2 && _iterator2.return) {_iterator2.return();}} finally {if (_didIteratorError2) {throw _iteratorError2;}}}}
|
||
|
||
|
||
|
||
/**
|
||
* Called when the add-on is shutting down, could be for re-installation
|
||
* or just uninstall.
|
||
*/
|
||
function shutdown(data, reason) {
|
||
// Close any open chat windows
|
||
Cu.import("resource:///modules/Chat.jsm");
|
||
var isLoopURL = function isLoopURL(_ref) {var src = _ref.src;return (/^about:loopconversation#/.test(src));};
|
||
[].concat(_toConsumableArray(Chat.chatboxes)).filter(isLoopURL).forEach(function (chatbox) {
|
||
chatbox.content.contentWindow.close();});
|
||
|
||
|
||
// Detach from hidden window (for OS X).
|
||
if (AppConstants.platform == "macosx") {
|
||
WindowListener.tearDownBrowserUI(Services.appShell.hiddenDOMWindow);}
|
||
|
||
|
||
// Detach from browser windows.
|
||
var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
|
||
var windows = wm.getEnumerator("navigator:browser");
|
||
while (windows.hasMoreElements()) {
|
||
var domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
|
||
WindowListener.tearDownBrowserUI(domWindow);}
|
||
|
||
|
||
// Stop waiting for browser windows to open.
|
||
wm.removeListener(WindowListener);
|
||
|
||
// If the app is shutting down, don't worry about cleaning up, just let
|
||
// it fade away...
|
||
if (reason == APP_SHUTDOWN) {
|
||
return;}
|
||
|
||
|
||
CustomizableUI.destroyWidget("loop-button");
|
||
|
||
// Unload stylesheets.
|
||
var styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"].
|
||
getService(Components.interfaces.nsIStyleSheetService);
|
||
var sheets = ["chrome://loop/content/addon/css/loop.css",
|
||
"chrome://loop/skin/platform.css"];var _iteratorNormalCompletion3 = true;var _didIteratorError3 = false;var _iteratorError3 = undefined;try {
|
||
for (var _iterator3 = sheets[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {var sheet = _step3.value;
|
||
var styleSheetURI = Services.io.newURI(sheet, null, null);
|
||
if (styleSheetService.sheetRegistered(styleSheetURI,
|
||
styleSheetService.AUTHOR_SHEET)) {
|
||
styleSheetService.unregisterSheet(styleSheetURI,
|
||
styleSheetService.AUTHOR_SHEET);}}
|
||
|
||
|
||
|
||
// Unload modules.
|
||
} catch (err) {_didIteratorError3 = true;_iteratorError3 = err;} finally {try {if (!_iteratorNormalCompletion3 && _iterator3.return) {_iterator3.return();}} finally {if (_didIteratorError3) {throw _iteratorError3;}}}Cu.unload("chrome://loop/content/modules/MozLoopAPI.jsm");
|
||
Cu.unload("chrome://loop/content/modules/LoopRooms.jsm");
|
||
Cu.unload("chrome://loop/content/modules/MozLoopService.jsm");}
|
||
|
||
|
||
function install() {}
|
||
|
||
function uninstall() {}
|