2014-06-19 04:56:02 +04:00
|
|
|
/* 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";
|
|
|
|
|
|
|
|
// A module for working with chat windows.
|
|
|
|
|
2015-09-30 16:35:22 +03:00
|
|
|
this.EXPORTED_SYMBOLS = ["Chat", "kDefaultButtonSet"];
|
2014-06-19 04:56:02 +04:00
|
|
|
|
|
|
|
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
|
|
|
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
|
|
|
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
|
|
|
|
2015-09-30 16:35:22 +03:00
|
|
|
const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
const kDefaultButtonSet = new Set(["minimize", "swap", "close"]);
|
|
|
|
const kHiddenDefaultButtons = new Set(["minimize", "close"]);
|
2015-10-07 15:03:21 +03:00
|
|
|
var gCustomButtons = new Map();
|
2015-09-30 16:35:22 +03:00
|
|
|
|
2014-06-19 04:56:02 +04:00
|
|
|
// A couple of internal helper function.
|
|
|
|
function isWindowChromeless(win) {
|
|
|
|
// XXX - stolen from browser-social.js, but there's no obvious place to
|
|
|
|
// put this so it can be shared.
|
|
|
|
|
|
|
|
// Is this a popup window that doesn't want chrome shown?
|
|
|
|
let docElem = win.document.documentElement;
|
|
|
|
// extrachrome is not restored during session restore, so we need
|
|
|
|
// to check for the toolbar as well.
|
2015-04-29 18:32:05 +03:00
|
|
|
let chromeless = docElem.getAttribute("chromehidden").includes("extrachrome") ||
|
|
|
|
docElem.getAttribute('chromehidden').includes("toolbar");
|
2014-06-19 04:56:02 +04:00
|
|
|
return chromeless;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isWindowGoodForChats(win) {
|
|
|
|
return !win.closed &&
|
|
|
|
!!win.document.getElementById("pinnedchats") &&
|
|
|
|
!isWindowChromeless(win) &&
|
|
|
|
!PrivateBrowsingUtils.isWindowPrivate(win);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getChromeWindow(contentWin) {
|
|
|
|
return contentWin.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
|
|
.QueryInterface(Ci.nsIDocShellTreeItem)
|
|
|
|
.rootTreeItem
|
|
|
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
|
|
.getInterface(Ci.nsIDOMWindow);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The exported Chat object
|
|
|
|
*/
|
|
|
|
|
2015-09-15 21:19:45 +03:00
|
|
|
var Chat = {
|
2014-12-23 10:35:28 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Iterator of <chatbox> elements from this module in all windows.
|
|
|
|
*/
|
|
|
|
get chatboxes() {
|
|
|
|
return function*() {
|
|
|
|
let winEnum = Services.wm.getEnumerator("navigator:browser");
|
|
|
|
while (winEnum.hasMoreElements()) {
|
|
|
|
let win = winEnum.getNext();
|
|
|
|
let chatbar = win.document.getElementById("pinnedchats");
|
|
|
|
if (!chatbar)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Make a new array instead of the live NodeList so this iterator can be
|
|
|
|
// used for closing/deleting.
|
2015-12-17 21:31:08 +03:00
|
|
|
let chatboxes = [...chatbar.children];
|
2014-12-23 10:35:28 +03:00
|
|
|
for (let chatbox of chatboxes) {
|
|
|
|
yield chatbox;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// include standalone chat windows
|
|
|
|
winEnum = Services.wm.getEnumerator("Social:Chat");
|
|
|
|
while (winEnum.hasMoreElements()) {
|
|
|
|
let win = winEnum.getNext();
|
|
|
|
if (win.closed)
|
|
|
|
continue;
|
|
|
|
yield win.document.getElementById("chatter");
|
|
|
|
}
|
|
|
|
}();
|
|
|
|
},
|
|
|
|
|
2014-06-19 04:56:02 +04:00
|
|
|
/**
|
|
|
|
* Open a new chatbox.
|
|
|
|
*
|
|
|
|
* @param contentWindow [optional]
|
|
|
|
* The content window that requested this chat. May be null.
|
2016-02-04 14:50:06 +03:00
|
|
|
* @param options
|
|
|
|
* Object that may contain the following properties:
|
|
|
|
* - origin
|
2014-06-19 04:56:02 +04:00
|
|
|
* The origin for the chat. This is primarily used as an identifier
|
|
|
|
* to help identify all chats from the same provider.
|
2016-02-04 14:50:06 +03:00
|
|
|
* - title
|
2014-06-19 04:56:02 +04:00
|
|
|
* The title to be used if a new chat window is created.
|
2016-02-04 14:50:06 +03:00
|
|
|
* - url
|
2014-06-19 04:56:02 +04:00
|
|
|
* The URL for the that. Should be under the origin. If an existing
|
|
|
|
* chatbox exists with the same URL, it will be reused and returned.
|
2016-02-04 14:50:06 +03:00
|
|
|
* - mode [optional]
|
2014-06-19 04:56:02 +04:00
|
|
|
* May be undefined or 'minimized'
|
2016-02-04 14:50:06 +03:00
|
|
|
* - focus [optional]
|
2014-06-19 04:56:02 +04:00
|
|
|
* Indicates if the chatbox should be focused. If undefined the chat
|
|
|
|
* will be focused if the window is currently handling user input (ie,
|
|
|
|
* if the chat is being opened as a direct result of user input)
|
2016-02-04 14:50:06 +03:00
|
|
|
* - remote [optional]
|
|
|
|
* Indicates if the chatbox browser should use the remote bindings
|
|
|
|
* to run in the content process when TRUE.
|
|
|
|
* @param callback
|
|
|
|
* Function to be invoked once the chat constructed. The chatbox binding
|
|
|
|
* is passed as the first argument.
|
|
|
|
*
|
2014-06-19 04:56:02 +04:00
|
|
|
* @return A chatbox binding. This binding has a number of promises which
|
|
|
|
* can be used to determine when the chatbox is being created and
|
|
|
|
* has loaded. Will return null if no chat can be created (Which
|
|
|
|
* should only happen in edge-cases)
|
|
|
|
*/
|
2016-02-04 14:50:06 +03:00
|
|
|
open: function(contentWindow, options, callback) {
|
2014-06-19 04:56:02 +04:00
|
|
|
let chromeWindow = this.findChromeWindowForChats(contentWindow);
|
|
|
|
if (!chromeWindow) {
|
|
|
|
Cu.reportError("Failed to open a chat window - no host window could be found.");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
let chatbar = chromeWindow.document.getElementById("pinnedchats");
|
|
|
|
chatbar.hidden = false;
|
2016-02-04 14:50:06 +03:00
|
|
|
if (options.remote) {
|
|
|
|
// Double check that current window can handle remote browser elements.
|
|
|
|
let browser = chromeWindow.gBrowser && chromeWindow.gBrowser.selectedBrowser;
|
|
|
|
if (!browser || browser.getAttribute("remote") != "true") {
|
|
|
|
options.remote = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let chatbox = chatbar.openChat(options, callback);
|
2014-06-19 04:56:02 +04:00
|
|
|
// getAttention is ignored if the target window is already foreground, so
|
|
|
|
// we can call it unconditionally.
|
|
|
|
chromeWindow.getAttention();
|
|
|
|
// If focus is undefined we want automatic focus handling, and only focus
|
|
|
|
// if a direct result of user action.
|
2016-02-04 14:50:06 +03:00
|
|
|
if (!("focus" in options)) {
|
2014-06-19 04:56:02 +04:00
|
|
|
let dwu = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
|
|
.getInterface(Ci.nsIDOMWindowUtils);
|
2016-02-04 14:50:06 +03:00
|
|
|
options.focus = dwu.isHandlingUserInput;
|
2014-06-19 04:56:02 +04:00
|
|
|
}
|
2016-02-04 14:50:06 +03:00
|
|
|
if (options.focus) {
|
2014-06-19 04:56:02 +04:00
|
|
|
chatbar.focus();
|
|
|
|
}
|
|
|
|
return chatbox;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Close all chats from the specified origin.
|
|
|
|
*
|
|
|
|
* @param origin
|
|
|
|
* The origin from which all chats should be closed.
|
|
|
|
*/
|
|
|
|
closeAll: function(origin) {
|
2014-12-23 10:35:28 +03:00
|
|
|
for (let chatbox of this.chatboxes) {
|
|
|
|
if (chatbox.content.getAttribute("origin") != origin) {
|
2014-06-19 04:56:02 +04:00
|
|
|
continue;
|
2014-12-23 10:35:28 +03:00
|
|
|
}
|
|
|
|
chatbox.close();
|
2014-06-19 04:56:02 +04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Focus the chatbar associated with a window
|
|
|
|
*
|
|
|
|
* @param window
|
|
|
|
*/
|
|
|
|
focus: function(win) {
|
|
|
|
let chatbar = win.document.getElementById("pinnedchats");
|
|
|
|
if (chatbar && !chatbar.hidden) {
|
|
|
|
chatbar.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
// This is exported as socialchat.xml needs to find a window when a chat
|
|
|
|
// is re-docked.
|
|
|
|
findChromeWindowForChats: function(preferredWindow) {
|
|
|
|
if (preferredWindow) {
|
|
|
|
preferredWindow = getChromeWindow(preferredWindow);
|
|
|
|
if (isWindowGoodForChats(preferredWindow)) {
|
|
|
|
return preferredWindow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// no good - we just use the "most recent" browser window which can host
|
|
|
|
// chats (we used to try and "group" all chats in the same browser window,
|
|
|
|
// but that didn't work out so well - see bug 835111
|
|
|
|
|
|
|
|
// Try first the most recent window as getMostRecentWindow works
|
|
|
|
// even on platforms where getZOrderDOMWindowEnumerator is broken
|
|
|
|
// (ie. Linux). This will handle most cases, but won't work if the
|
|
|
|
// foreground window is a popup.
|
|
|
|
let mostRecent = Services.wm.getMostRecentWindow("navigator:browser");
|
|
|
|
if (isWindowGoodForChats(mostRecent))
|
|
|
|
return mostRecent;
|
|
|
|
|
|
|
|
let topMost, enumerator;
|
|
|
|
// *sigh* - getZOrderDOMWindowEnumerator is broken except on Mac and
|
|
|
|
// Windows. We use BROKEN_WM_Z_ORDER as that is what some other code uses
|
|
|
|
// and a few bugs recommend searching mxr for this symbol to identify the
|
|
|
|
// workarounds - we want this code to be hit in such searches.
|
|
|
|
let os = Services.appinfo.OS;
|
|
|
|
const BROKEN_WM_Z_ORDER = os != "WINNT" && os != "Darwin";
|
|
|
|
if (BROKEN_WM_Z_ORDER) {
|
|
|
|
// this is oldest to newest and no way to change the order.
|
|
|
|
enumerator = Services.wm.getEnumerator("navigator:browser");
|
|
|
|
} else {
|
|
|
|
// here we explicitly ask for bottom-to-top so we can use the same logic
|
|
|
|
// where BROKEN_WM_Z_ORDER is true.
|
|
|
|
enumerator = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", false);
|
|
|
|
}
|
|
|
|
while (enumerator.hasMoreElements()) {
|
|
|
|
let win = enumerator.getNext();
|
|
|
|
if (!win.closed && isWindowGoodForChats(win))
|
|
|
|
topMost = win;
|
|
|
|
}
|
|
|
|
return topMost;
|
|
|
|
},
|
2015-09-30 16:35:22 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a button to the collection of custom buttons that can be added to the
|
|
|
|
* titlebar of a chatbox.
|
|
|
|
* For the button to be visible, `Chat#loadButtonSet` has to be called with
|
|
|
|
* the new buttons' ID in the buttonSet argument.
|
|
|
|
*
|
|
|
|
* @param {Object} button Button object that may contain the following fields:
|
|
|
|
* - {String} id Button identifier.
|
|
|
|
* - {Function} [onBuild] Function that returns a valid DOM node to
|
|
|
|
* represent the button.
|
|
|
|
* - {Function} [onCommand] Callback function that is invoked when the DOM
|
|
|
|
* node is clicked.
|
|
|
|
*/
|
|
|
|
registerButton: function(button) {
|
|
|
|
if (gCustomButtons.has(button.id))
|
|
|
|
return;
|
|
|
|
gCustomButtons.set(button.id, button);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load a set of predefined buttons in a chatbox' titlebar.
|
|
|
|
*
|
|
|
|
* @param {XULDOMNode} chatbox Chatbox XUL element.
|
|
|
|
* @param {Set|String} buttonSet Set of buttons to show in the titlebar. This
|
|
|
|
* may be a comma-separated string or a predefined
|
|
|
|
* set object.
|
|
|
|
*/
|
|
|
|
loadButtonSet: function(chatbox, buttonSet = kDefaultButtonSet) {
|
|
|
|
if (!buttonSet)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// When the buttonSet is coming from an XML attribute, it will be a string.
|
|
|
|
if (typeof buttonSet == "string") {
|
2015-12-17 21:31:08 +03:00
|
|
|
buttonSet = buttonSet.split(",").map(button => button.trim());
|
2015-09-30 16:35:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure to keep the current set around.
|
|
|
|
chatbox.setAttribute("buttonSet", [...buttonSet].join(","));
|
|
|
|
|
|
|
|
let isUndocked = !chatbox.chatbar;
|
|
|
|
let document = chatbox.ownerDocument;
|
|
|
|
let titlebarNode = document.getAnonymousElementByAttribute(chatbox, "class",
|
|
|
|
"chat-titlebar");
|
|
|
|
let buttonsSeen = new Set();
|
|
|
|
|
|
|
|
for (let buttonId of buttonSet) {
|
|
|
|
buttonId = buttonId.trim();
|
|
|
|
buttonsSeen.add(buttonId);
|
|
|
|
let nodes, node;
|
|
|
|
if (kDefaultButtonSet.has(buttonId)) {
|
|
|
|
node = document.getAnonymousElementByAttribute(chatbox, "anonid", buttonId);
|
|
|
|
if (!node)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
node.hidden = isUndocked && kHiddenDefaultButtons.has(buttonId) ? true : false;
|
|
|
|
} else if (gCustomButtons.has(buttonId)) {
|
|
|
|
let button = gCustomButtons.get(buttonId);
|
|
|
|
let buttonClass = "chat-" + buttonId;
|
|
|
|
// Custom buttons are not defined in the chatbox binding, thus not
|
|
|
|
// anonymous elements.
|
|
|
|
nodes = titlebarNode.getElementsByClassName(buttonClass);
|
|
|
|
node = nodes && nodes.length ? nodes[0] : null;
|
|
|
|
if (!node) {
|
|
|
|
// Allow custom buttons to build their own button node.
|
|
|
|
if (button.onBuild) {
|
|
|
|
node = button.onBuild(chatbox);
|
|
|
|
} else {
|
|
|
|
// We can also build a normal toolbarbutton to insert.
|
|
|
|
node = document.createElementNS(kNSXUL, "toolbarbutton");
|
|
|
|
node.classList.add(buttonClass);
|
|
|
|
node.classList.add("chat-toolbarbutton");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (button.onCommand) {
|
|
|
|
node.addEventListener("command", e => {
|
|
|
|
button.onCommand(e, chatbox);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
titlebarNode.appendChild(node);
|
|
|
|
}
|
|
|
|
|
|
|
|
// When the chat is undocked and the button wants to be visible then, it
|
|
|
|
// will be.
|
|
|
|
node.hidden = isUndocked && !button.visibleWhenUndocked;
|
|
|
|
} else {
|
|
|
|
Cu.reportError("Chatbox button '" + buttonId + "' could not be found!\n");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hide any button that is part of the default set, but not of the current set.
|
|
|
|
for (let button of kDefaultButtonSet) {
|
|
|
|
if (!buttonsSeen.has(button))
|
|
|
|
document.getAnonymousElementByAttribute(chatbox, "anonid", button).hidden = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|