Files
tubestation/browser/extensions/loop/bootstrap.js

1493 lines
54 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 pages 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() {}