tenfourfox/browser/extensions/loop/bootstrap.js
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

1125 lines
40 KiB
JavaScript
Raw Permalink 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 */
const { interfaces: Ci, utils: Cu, classes: Cc } = Components;
const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const kBrowserSharingNotificationId = "loop-sharing-notification";
const MIN_CURSOR_DELTA = 3;
const MIN_CURSOR_INTERVAL = 100;
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".
const PREF_LOG_LEVEL = "loop.debug.loglevel";
XPCOMUtils.defineLazyGetter(this, "log", () => {
let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
let 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(window) {
let document = window.document;
let gBrowser = window.gBrowser;
let xhrClass = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"];
let FileReader = window.FileReader;
let menuItem = null;
let isSlideshowOpen = false;
// 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() {
let 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() {
if (!this._constants) {
// GetAllConstants is synchronous even though it's using a callback.
this.LoopAPI.sendMessageToHandler({
name: "GetAllConstants"
}, result => {
this._constants = result;
});
}
return this._constants;
},
/**
* @return {Promise}
*/
promiseDocumentVisible(aDocument) {
if (!aDocument.hidden) {
return Promise.resolve(aDocument);
}
return new Promise((resolve) => {
aDocument.addEventListener("visibilitychange", function onVisibilityChanged() {
aDocument.removeEventListener("visibilitychange", onVisibilityChanged);
resolve(aDocument);
});
});
},
/**
* Toggle between opening or hiding the Loop panel.
*
* @param {DOMEvent} [event] Optional event that triggered the call to this
* function.
* @param {String} [tabId] Optional name of the tab to select after the panel
* has opened. Does nothing when the panel is hidden.
* @return {Promise}
*/
togglePanel: function(event, tabId = null) {
if (!this.panel) {
// We're on the hidden window! What fun!
let obs = win => {
Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
win.LoopUI.togglePanel(event, tabId);
};
Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
return window.OpenBrowserWindow();
}
if (this.panel.state == "open") {
return new Promise(resolve => {
this.panel.hidePopup();
resolve();
});
}
if (this.isSlideshowOpen) {
return Promise.resolve();
}
return this.openCallPanel(event, tabId).then(doc => {
let fm = Services.focus;
fm.moveFocus(doc.defaultView, null, fm.MOVEFOCUS_FIRST, fm.FLAG_NOSCROLL);
}).catch(err => {
Cu.reportError(err);
});
},
/**
* Opens the panel for Loop and sizes it appropriately.
*
* @param {event} event The event opening the panel, used to anchor
* the panel to the button which triggers it.
* @param {String} [tabId] Identifier of the tab to select when the panel is
* opened. Example: 'rooms', 'contacts', etc.
* @return {Promise}
*/
openCallPanel: function(event, tabId = null) {
return new Promise((resolve) => {
let callback = iframe => {
// Helper function to show a specific tab view in the panel.
function showTab() {
if (!tabId) {
resolve(LoopUI.promiseDocumentVisible(iframe.contentDocument));
return;
}
let win = iframe.contentWindow;
let ev = new win.CustomEvent("UIAction", Cu.cloneInto({
detail: {
action: "selectTab",
tab: tabId
}
}, win));
win.dispatchEvent(ev);
resolve(LoopUI.promiseDocumentVisible(iframe.contentDocument));
}
// If the panel has been opened and initialized before, we can skip waiting
// for the content to load - because it's already there.
if (("contentWindow" in iframe) && iframe.contentWindow.document.readyState == "complete") {
showTab();
return;
}
let documentDOMLoaded = () => {
iframe.removeEventListener("DOMContentLoaded", documentDOMLoaded, true);
// Handle window.close correctly on the panel.
this.hookWindowCloseForPanelClose(iframe.contentWindow);
iframe.contentWindow.addEventListener("loopPanelInitialized", function loopPanelInitialized() {
iframe.contentWindow.removeEventListener("loopPanelInitialized",
loopPanelInitialized);
showTab();
});
};
iframe.addEventListener("DOMContentLoaded", documentDOMLoaded, true);
let buckets = this.constants.LOOP_MAU_TYPE;
this.LoopAPI.sendMessageToHandler({
name: "TelemetryAddValue",
data: ["LOOP_MAU", buckets.OPEN_PANEL]
});
};
// Used to clear the temporary "login" state from the button.
Services.obs.notifyObservers(null, "loop-status-changed", null);
this.shouldResumeTour().then((resume) => {
if (resume) {
// Assume the conversation with the visitor wasn't open since we would
// have resumed the tour as soon as the visitor joined if it was (and
// the pref would have been set to false already.
this.MozLoopService.resumeTour("waiting");
resolve();
return;
}
this.LoopAPI.initialize();
let anchor = event ? event.target : this.toolbarButton.anchor;
this.PanelFrame.showPopup(
window,
anchor,
"loop", // Notification Panel Type
null, // Origin
"about:looppanel", // Source
null, // Size
callback);
});
});
},
/**
* Method to know whether actions to open the panel should instead resume the tour.
*
* We need the panel to be opened via UITour so that it gets @noautohide.
*
* @return {Promise} resolving with a {Boolean} of whether the tour should be resumed instead of
* opening the panel.
*/
shouldResumeTour: Task.async(function* () {
// Resume the FTU tour if this is the first time a room was joined by
// someone else since the tour.
if (!Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin")) {
return false;
}
if (!this.LoopRooms.participantsCount) {
// Nobody is in the rooms
return false;
}
let roomsWithNonOwners = yield this.roomsWithNonOwners();
if (!roomsWithNonOwners.length) {
// We were the only one in a room but we want to know about someone else joining.
return false;
}
return true;
}),
/**
* @return {Promise} resolved with an array of Rooms with participants (excluding owners)
*/
roomsWithNonOwners: function() {
return new Promise(resolve => {
this.LoopRooms.getAll((error, rooms) => {
let roomsWithNonOwners = [];
for (let room of rooms) {
if (!("participants" in room)) {
continue;
}
let numNonOwners = room.participants.filter(participant => !participant.owner).length;
if (!numNonOwners) {
continue;
}
roomsWithNonOwners.push(room);
}
resolve(roomsWithNonOwners);
});
});
},
/**
* Triggers the initialization of the loop service if necessary.
* Also adds appropraite observers for the UI.
*/
init: function() {
// 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(ex => {
if (!ex.message ||
(!ex.message.contains("not enabled") &&
!ex.message.contains("not needed"))) {
console.error(ex);
}
});
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;
}
// Cleanup when the window unloads.
window.addEventListener("unload", () => {
Services.obs.removeObserver(this, "loop-status-changed");
});
Services.obs.addObserver(this, "loop-status-changed", false);
this.updateToolbarState();
},
/**
* Adds a menu item to the browsers' Tools menu that open the Loop panel
* when selected.
*/
addMenuItem: function() {
let 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", () => this.togglePanel());
menu.insertBefore(menuItem, document.getElementById("sync-setup"));
},
/**
* Removes the menu item from the browsers' Tools menu.
*/
removeMenuItem: function() {
if (menuItem) {
menuItem.parentNode.removeChild(menuItem);
}
},
// Implements nsIObserver
observe: function(subject, topic, data) {
if (topic != "loop-status-changed") {
return;
}
this.updateToolbarState(data);
},
/**
* Updates the toolbar/menu-button state to reflect Loop status. 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(aReason = null) {
if (!this.toolbarButton.node) {
return;
}
let state = "";
let mozL10nId = "loop-call-button3";
let suffix = ".tooltiptext";
if (this.MozLoopService.errors.size) {
state = "error";
mozL10nId += "-error";
} else if (this.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(roomsWithNonOwners => {
if (roomsWithNonOwners.length > 0) {
mozL10nId += "-participantswaiting";
} else {
mozL10nId += "-active";
}
suffix += "2";
this.updateTooltiptext(mozL10nId + suffix);
this.toolbarButton.node.setAttribute("state", state);
});
return;
} else {
suffix += "2";
}
this.toolbarButton.node.setAttribute("state", state);
this.updateTooltiptext(mozL10nId + suffix);
},
/**
* Updates the tootltiptext to reflect Loop status. This should not be called
* from the hidden window.
*
* @param {string} [mozL10nId] l10n ID that refelct the current
* Loop status.
*/
updateTooltiptext: function(mozL10nId) {
this.toolbarButton.node.setAttribute("tooltiptext", mozL10nId);
var tooltiptext = CustomizableUI.getLocalizedProperty(this.toolbarButton, "tooltiptext");
this.toolbarButton.node.setAttribute("tooltiptext", tooltiptext);
},
/**
* Show a desktop notification when 'do not disturb' isn't enabled.
*
* @param {Object} options Set of options that may tweak the appearance and
* behavior of the notification.
* Option params:
* - {String} title Notification title message
* - {String} [message] Notification body text
* - {String} [icon] Notification icon
* - {String} [sound] Sound to play
* - {String} [selectTab] Tab to select when the panel
* opens
* - {Function} [onclick] Callback to invoke when
* the notification is clicked.
* Opens the panel by default.
*/
showNotification: function(options) {
if (this.MozLoopService.doNotDisturb) {
return;
}
if (!options.title) {
throw new Error("Missing title, can not display notification");
}
let notificationOptions = {
body: options.message || ""
};
if (options.icon) {
notificationOptions.icon = options.icon;
}
if (options.sound) {
// This will not do anything, until bug bug 1105222 is resolved.
notificationOptions.mozbehavior = {
soundFile: ""
};
this.playSound(options.sound);
}
let notification = new window.Notification(options.title, notificationOptions);
notification.addEventListener("click", () => {
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(() => {
if (typeof options.onclick == "function") {
options.onclick();
} else {
// Open the Loop panel as a default action.
this.openCallPanel(null, options.selectTab || null);
}
}, 0);
});
},
/**
* Play a sound in this window IF there's no sound playing yet.
*
* @param {String} name Name of the sound, like 'ringtone' or 'room-joined'
*/
playSound: function(name) {
if (this.ActiveSound || this.MozLoopService.doNotDisturb) {
return;
}
this.activeSound = new window.Audio();
this.activeSound.src = `chrome://loop/content/shared/sounds/${name}.ogg`;
this.activeSound.load();
this.activeSound.play();
this.activeSound.addEventListener("ended", () => this.activeSound = undefined, false);
},
/**
* Start listening to selected tab changes and notify any content page that's
* listening to 'BrowserSwitch' push messages.
*
* Push message parameters:
* - {Integer} windowId The new windowId for the browser.
*/
startBrowserSharing: function() {
if (!this._listeningToTabSelect) {
gBrowser.tabContainer.addEventListener("TabSelect", this);
this._listeningToTabSelect = true;
// Watch for title changes as opposed to location changes as more
// metadata about the page is available when this event fires.
gBrowser.addEventListener("DOMTitleChanged", this);
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);
}
this._maybeShowBrowserSharingInfoBar();
// Get the first window Id for the listener.
this.LoopAPI.broadcastPushMessage("BrowserSwitch",
gBrowser.selectedBrowser.outerWindowID);
},
/**
* Stop listening to selected tab changes.
*/
stopBrowserSharing: function() {
if (!this._listeningToTabSelect) {
return;
}
this._hideBrowserSharingInfoBar();
gBrowser.tabContainer.removeEventListener("TabSelect", this);
gBrowser.removeEventListener("DOMTitleChanged", this);
gBrowser.removeEventListener("mousemove", this);
this.removeRemoteCursor();
this._listeningToTabSelect = false;
this._browserSharePaused = false;
this._sendTelemetryEventsIfNeeded();
},
/**
* Sends telemetry events for pause/ resume buttons if needed.
*/
_sendTelemetryEventsIfNeeded: function() {
// The user can't click Resume button without clicking Pause button first.
if (!this._pauseButtonClicked) {
return;
}
let 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(cursorData) {
if (this._browserSharePaused || !this._listeningToTabSelect) {
return;
}
let browser = gBrowser.selectedBrowser;
let 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.
let 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";
},
/**
* Removes the remote cursor from the screen
*
* @param browser OPT browser where the cursor should be removed from.
*/
removeRemoteCursor: function() {
let 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(key) {
let str = this.MozLoopService.getStrings(key);
if (str) {
str = JSON.parse(str).textContent;
}
return str;
},
/**
* Shows an infobar notification at the top of the browser window that warns
* the user that their browser tabs are being broadcasted through the current
* conversation.
*/
_maybeShowBrowserSharingInfoBar: function() {
// Pre-load strings
let pausedStrings = {
label: this._getString("infobar_button_restart_label2"),
accesskey: this._getString("infobar_button_restart_accesskey"),
message: this._getString("infobar_screenshare_stop_sharing_message")
};
let unpausedStrings = {
label: this._getString("infobar_button_stop_label2"),
accesskey: this._getString("infobar_button_stop_accesskey"),
message: this._getString("infobar_screenshare_browser_message2")
};
let initStrings =
this._browserSharePaused ? pausedStrings : unpausedStrings;
this._hideBrowserSharingInfoBar();
let box = gBrowser.getNotificationBox();
let 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: (event, buttonInfo, buttonNode) => {
this._browserSharePaused = !this._browserSharePaused;
let stringObj = this._browserSharePaused ? pausedStrings : unpausedStrings;
bar.label = stringObj.message;
bar.classList.toggle("paused", this._browserSharePaused);
buttonNode.label = stringObj.label;
buttonNode.accessKey = stringObj.accesskey;
LoopUI.MozLoopService.toggleBrowserSharing(this._browserSharePaused);
if (this._browserSharePaused) {
this._pauseButtonClicked = true;
// if paused we stop sharing remote cursors
this.removeRemoteCursor();
} else {
this._resumeButtonClicked = true;
}
return true;
},
type: "pause"
},
{
label: this._getString("infobar_button_disconnect_label"),
accessKey: this._getString("infobar_button_disconnect_accesskey"),
isDefault: true,
callback: () => {
this.removeRemoteCursor();
this._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(browser) {
browser = browser || gBrowser.selectedBrowser;
let box = gBrowser.getNotificationBox(browser);
let notification = box.getNotificationWithValue(kBrowserSharingNotificationId);
let removed = false;
if (notification) {
box.removeNotification(notification);
removed = true;
}
return removed;
},
/**
* Broadcast 'BrowserSwitch' event.
*/
_notifyBrowserSwitch: function() {
// Get the first window Id for the listener.
this.LoopAPI.broadcastPushMessage("BrowserSwitch",
gBrowser.selectedBrowser.outerWindowID);
},
/**
* Handles events from gBrowser.
*/
handleEvent: function(event) {
switch (event.type) {
case "DOMTitleChanged":
// Get the new title of the shared tab
this._notifyBrowserSwitch();
break;
case "TabSelect":
let 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();
}
break;
case "mousemove":
this.handleMousemove(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(event) {
// Won't send events if not sharing (paused or not started).
if (this._browserSharePaused || !this._listeningToTabSelect) {
return;
}
// Only update every so often.
let now = Date.now();
if (now - this.lastCursorTime < MIN_CURSOR_INTERVAL) {
return;
}
this.lastCursorTime = now;
// Skip the update if cursor is out of bounds or didn't move much.
let browserBox = gBrowser.selectedBrowser.boxObject;
let deltaX = event.screenX - browserBox.screenX;
let deltaY = event.screenY - browserBox.screenY;
if (deltaX < 0 || deltaX > browserBox.width ||
deltaY < 0 || deltaY > browserBox.height ||
(Math.abs(deltaX - this.lastCursorX) < MIN_CURSOR_DELTA &&
Math.abs(deltaY - this.lastCursorY) < MIN_CURSOR_DELTA)) {
return;
}
this.lastCursorX = deltaX;
this.lastCursorY = deltaY;
this.LoopAPI.broadcastPushMessage("CursorPositionChange", {
ratioX: deltaX / browserBox.width,
ratioY: deltaY / browserBox.height
});
},
/**
* Fetch the favicon of the currently selected tab in the format of a data-uri.
*
* @param {Function} callback Function to be invoked with an error object as
* its first argument when an error occurred or
* a string as second argument when the favicon
* has been fetched.
*/
getFavicon: function(callback) {
let pageURI = gBrowser.selectedTab.linkedBrowser.currentURI.spec;
// If the tab pages url starts with http(s), fetch icon.
if (!/^https?:/.test(pageURI)) {
callback();
return;
}
this.PlacesUtils.promiseFaviconLinkUrl(pageURI).then(uri => {
// We XHR the favicon to get a File object, which we can pass to the FileReader
// object. The FileReader turns the File object into a data-uri.
let xhr = xhrClass.createInstance(Ci.nsIXMLHttpRequest);
xhr.open("get", uri.spec, true);
xhr.responseType = "blob";
xhr.overrideMimeType("image/x-icon");
xhr.onload = () => {
if (xhr.status != 200) {
callback(new Error("Invalid status code received for favicon XHR: " + xhr.status));
return;
}
let reader = new FileReader();
reader.onload = reader.onload = () => callback(null, reader.result);
reader.onerror = callback;
reader.readAsDataURL(xhr.response);
};
xhr.onerror = callback;
xhr.send();
}).catch(err => {
callback(err || new Error("No favicon found"));
});
}
};
XPCOMUtils.defineLazyModuleGetter(LoopUI, "hookWindowCloseForPanelClose", "resource://gre/modules/MozSocialAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(LoopUI, "LoopAPI", "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;
},
/**
* 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(window) {
if (window.LoopUI) {
// Disabled for 45, as 45 has its own menuitem.
// window.LoopUI.removeMenuItem();
// XXX Bug 1229352 - Add in tear-down of the panel.
}
},
// nsIWindowMediatorListener functions.
onOpenWindow: function(xulWindow) {
// A new window has opened.
let 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() {
},
onWindowTitleChange: function() {
}
};
/**
* 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(aDocument) {
// If we're not supposed to see the button, return zip.
if (!Services.prefs.getBoolPref("loop.enabled")) {
return null;
}
let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView);
let 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");
}
let 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: (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) {
case "boolean":
branch.setBoolPref(key, val);
break;
case "number":
branch.setIntPref(key, val);
break;
case "string":
branch.setCharPref(key, val);
break;
}
}
});
}
/**
* 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) {
// Hidden window didn't exist, so wait until startup is done.
let 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.
let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
let windows = wm.getEnumerator("navigator:browser");
while (windows.hasMoreElements()) {
let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
WindowListener.setupBrowserUI(domWindow);
}
// Wait for any new browser windows to open.
wm.addListener(WindowListener);
// Load our stylesheets.
let styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
.getService(Components.interfaces.nsIStyleSheetService);
let sheets = ["chrome://loop-shared/skin/loop.css"];
if (AppConstants.platform != "linux") {
sheets.push("chrome://loop/skin/platform.css");
}
for (let sheet of sheets) {
let styleSheetURI = Services.io.newURI(sheet, null, null);
styleSheetService.loadAndRegisterSheet(styleSheetURI,
styleSheetService.AUTHOR_SHEET);
}
}
/**
* 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");
let isLoopURL = ({ src }) => /^about:loopconversation#/.test(src);
[...Chat.chatboxes].filter(isLoopURL).forEach(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.
let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
let windows = wm.getEnumerator("navigator:browser");
while (windows.hasMoreElements()) {
let 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.
let styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
.getService(Components.interfaces.nsIStyleSheetService);
let sheets = ["chrome://loop/content/addon/css/loop.css",
"chrome://loop/skin/platform.css"];
for (let sheet of sheets) {
let styleSheetURI = Services.io.newURI(sheet, null, null);
if (styleSheetService.sheetRegistered(styleSheetURI,
styleSheetService.AUTHOR_SHEET)) {
styleSheetService.unregisterSheet(styleSheetURI,
styleSheetService.AUTHOR_SHEET);
}
}
// Unload modules.
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() {}