summaryrefslogtreecommitdiffstats
path: root/comm/suite/chatzilla/xul
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/suite/chatzilla/xul
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/suite/chatzilla/xul')
-rw-r--r--comm/suite/chatzilla/xul/content/about/about.js112
-rw-r--r--comm/suite/chatzilla/xul/content/about/about.xul57
-rw-r--r--comm/suite/chatzilla/xul/content/browserOverlay.xul24
-rw-r--r--comm/suite/chatzilla/xul/content/channels.js875
-rw-r--r--comm/suite/chatzilla/xul/content/channels.xul108
-rw-r--r--comm/suite/chatzilla/xul/content/chatzilla.xul141
-rw-r--r--comm/suite/chatzilla/xul/content/chatzillaOverlay.js11
-rw-r--r--comm/suite/chatzilla/xul/content/chatzillaOverlay.xul42
-rw-r--r--comm/suite/chatzilla/xul/content/commands.js4760
-rw-r--r--comm/suite/chatzilla/xul/content/config-add.js55
-rw-r--r--comm/suite/chatzilla/xul/content/config-add.xul55
-rw-r--r--comm/suite/chatzilla/xul/content/config.css33
-rw-r--r--comm/suite/chatzilla/xul/content/config.js1775
-rw-r--r--comm/suite/chatzilla/xul/content/config.xul82
-rw-r--r--comm/suite/chatzilla/xul/content/dynamic.css7
-rw-r--r--comm/suite/chatzilla/xul/content/handlers.js3960
-rw-r--r--comm/suite/chatzilla/xul/content/install-plugin/install-plugin.js97
-rw-r--r--comm/suite/chatzilla/xul/content/install-plugin/install-plugin.xul43
-rw-r--r--comm/suite/chatzilla/xul/content/menus.js513
-rw-r--r--comm/suite/chatzilla/xul/content/menus.xul97
-rw-r--r--comm/suite/chatzilla/xul/content/messages.js104
-rw-r--r--comm/suite/chatzilla/xul/content/mungers.js904
-rw-r--r--comm/suite/chatzilla/xul/content/networks-edit.css15
-rw-r--r--comm/suite/chatzilla/xul/content/networks-edit.js390
-rw-r--r--comm/suite/chatzilla/xul/content/networks-edit.xul155
-rw-r--r--comm/suite/chatzilla/xul/content/networks-server.js94
-rw-r--r--comm/suite/chatzilla/xul/content/networks-server.xul84
-rw-r--r--comm/suite/chatzilla/xul/content/networks.js228
-rw-r--r--comm/suite/chatzilla/xul/content/output-base.css528
-rw-r--r--comm/suite/chatzilla/xul/content/output-window.html209
-rw-r--r--comm/suite/chatzilla/xul/content/output-window.js588
-rw-r--r--comm/suite/chatzilla/xul/content/popups.xul124
-rw-r--r--comm/suite/chatzilla/xul/content/pref-irc-toolkit.xul24
-rw-r--r--comm/suite/chatzilla/xul/content/prefs.js1213
-rw-r--r--comm/suite/chatzilla/xul/content/prefsOverlay.xul31
-rw-r--r--comm/suite/chatzilla/xul/content/scripts.xul55
-rw-r--r--comm/suite/chatzilla/xul/content/static.js5639
-rw-r--r--comm/suite/chatzilla/xul/lib/munger.js245
-rw-r--r--comm/suite/chatzilla/xul/lib/tree-utils.js1716
-rw-r--r--comm/suite/chatzilla/xul/skin/about.css44
-rw-r--r--comm/suite/chatzilla/xul/skin/browserOverlay.css7
-rw-r--r--comm/suite/chatzilla/xul/skin/channels.css24
-rw-r--r--comm/suite/chatzilla/xul/skin/chatzilla.css305
-rw-r--r--comm/suite/chatzilla/xul/skin/chatzillaOverlay.css7
-rw-r--r--comm/suite/chatzilla/xul/skin/images/admin-graphic.pngbin0 -> 566 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/admin-symbol.pngbin0 -> 461 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/arrow-down.pngbin0 -> 192 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/chatzilla-16.pngbin0 -> 261 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/drop-indicator-bottom.pngbin0 -> 416 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/founder-graphic.pngbin0 -> 582 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/founder-symbol.pngbin0 -> 325 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/halfop-graphic.pngbin0 -> 588 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/halfop-symbol.pngbin0 -> 481 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/input-send.pngbin0 -> 290 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/logging-off.pngbin0 -> 289 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/logging-on.pngbin0 -> 308 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/logo.pngbin0 -> 1499 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/multiline-contract.pngbin0 -> 255 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/multiline-expand.pngbin0 -> 255 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/no-graphic.pngbin0 -> 606 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/no-symbol.pngbin0 -> 154 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/op-graphic.pngbin0 -> 599 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/op-symbol.pngbin0 -> 600 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/source_png/spbubble-off.pngbin0 -> 26256 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/source_png/spbubble-on.pngbin0 -> 31259 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/source_svg/logging.svg61
-rw-r--r--comm/suite/chatzilla/xul/skin/images/source_svg/userlist_icons.svg636
-rw-r--r--comm/suite/chatzilla/xul/skin/images/spbubble-off.pngbin0 -> 710 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/spbubble-on.pngbin0 -> 663 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/voice-graphic.pngbin0 -> 378 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/images/voice-symbol.pngbin0 -> 325 bytes
-rw-r--r--comm/suite/chatzilla/xul/skin/install-plugin.css9
-rw-r--r--comm/suite/chatzilla/xul/skin/networks-edit.css7
-rw-r--r--comm/suite/chatzilla/xul/skin/output-dark.css226
-rw-r--r--comm/suite/chatzilla/xul/skin/output-default.css67
-rw-r--r--comm/suite/chatzilla/xul/skin/output-light.css217
-rw-r--r--comm/suite/chatzilla/xul/skin/output-loud.css202
-rw-r--r--comm/suite/chatzilla/xul/skin/output-marble.css148
78 files changed, 27153 insertions, 0 deletions
diff --git a/comm/suite/chatzilla/xul/content/about/about.js b/comm/suite/chatzilla/xul/content/about/about.js
new file mode 100644
index 0000000000..b68c64aa8f
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/about/about.js
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var ownerClient = null;
+
+// To be able to load static.js, we need a few things defined first:
+function CIRCNetwork() {}
+function CIRCServer() {}
+function CIRCChannel() {}
+function CIRCUser() {}
+function CIRCChanUser() {}
+function CIRCDCCUser() {}
+function CIRCDCCChat() {}
+function CIRCDCCFile() {}
+function CIRCDCCFileTransfer() {}
+function CIRCSTS() {}
+
+// Our friend from messages.js:
+function getMsg(msgName, params, deflt)
+{
+ return client.messageManager.getMsg(msgName, params, deflt);
+}
+
+function onLoad()
+{
+ const propsPath = "chrome://chatzilla/locale/chatzilla.properties";
+
+ // Find our owner, if we have one.
+ ownerClient = window.arguments ? window.arguments[0].client : null;
+ if (ownerClient)
+ ownerClient.aboutDialog = window;
+
+ client.entities = new Object();
+ client.messageManager = new MessageManager(client.entities);
+ client.messageManager.loadBrands();
+ client.defaultBundle = client.messageManager.addBundle(propsPath);
+
+ var version = getVersionInfo();
+ client.userAgent = getMsg(MSG_VERSION_REPLY, [version.cz, version.ua]);
+
+ var verLabel = document.getElementById("version");
+ var verString = verLabel.getAttribute("format").replace("%S", version.cz);
+ verLabel.setAttribute("value", verString);
+ verLabel.setAttribute("condition", __cz_condition);
+
+ var localizers = document.getElementById("localizers");
+ var localizerNames = getMsg("locale.authors", null, "");
+ if (localizerNames && (localizerNames.substr(0, 11) != "XXX REPLACE"))
+ {
+ localizerNames = localizerNames.split(/\s*;\s*/);
+
+ for (var i = 0; i < localizerNames.length; i++) {
+ var loc = document.createElement("label");
+ loc.setAttribute("value", localizerNames[i]);
+ localizers.appendChild(loc);
+ }
+ }
+ else
+ {
+ var localizersHeader = document.getElementById("localizers-header");
+ localizersHeader.style.display = "none";
+ localizers.style.display = "none";
+ }
+
+ if (window.opener)
+ {
+ // Force the window to be the right size now, not later.
+ window.sizeToContent();
+
+ // Position it centered over, but never up or left of parent.
+ var opener = window.opener;
+ var sx = Math.max((opener.outerWidth - window.outerWidth ) / 2, 0);
+ var sy = Math.max((opener.outerHeight - window.outerHeight) / 2, 0);
+ window.moveTo(opener.screenX + sx, opener.screenY + sy);
+ }
+
+ /* Find and focus the dialog's default button (OK), otherwise the focus
+ * lands on the first focusable content - the homepage link. Links in XUL
+ * look horrible when focused.
+ */
+ var binding = document.documentElement;
+ var defaultButton = binding.getButton(binding.defaultButton);
+ if (defaultButton)
+ setTimeout(function() { defaultButton.focus() }, 0);
+}
+
+function onUnload()
+{
+ if (ownerClient)
+ delete ownerClient.aboutDialog;
+}
+
+function copyVersion()
+{
+ const cbID = Components.interfaces.nsIClipboard.kGlobalClipboard;
+ var cb = getService("@mozilla.org/widget/clipboard;1", "nsIClipboard");
+ var tr = newObject("@mozilla.org/widget/transferable;1", "nsITransferable");
+ var str = newObject("@mozilla.org/supports-string;1", "nsISupportsString");
+
+ str.data = client.userAgent;
+ tr.setTransferData("text/unicode", str, str.data.length * 2);
+ cb.setData(tr, null, cbID);
+}
+
+function openHomepage()
+{
+ if (ownerClient)
+ ownerClient.dispatch("goto-url", {url: MSG_SOURCE_REPLY});
+ else
+ window.opener.open(MSG_SOURCE_REPLY, "_blank");
+}
diff --git a/comm/suite/chatzilla/xul/content/about/about.xul b/comm/suite/chatzilla/xul/content/about/about.xul
new file mode 100644
index 0000000000..54dc7b1bd4
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/about/about.xul
@@ -0,0 +1,57 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/about.dtd">
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://chatzilla/skin/about.css" type="text/css"?>
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ windowtype="irc:chatzilla:about"
+ buttons="accept"
+ onload="onLoad()"
+ onunload="onUnload()"
+ title="&window.title;">
+
+ <script src="chrome://chatzilla/content/lib/js/utils.js"/>
+ <script src="chrome://chatzilla/content/about/about.js"/>
+ <script src="chrome://chatzilla/content/lib/js/message-manager.js"/>
+ <script src="chrome://chatzilla/content/static.js"/>
+
+ <vbox class="box-padded" flex="1">
+ <hbox>
+ <image id="logo"/>
+ <vbox flex="1">
+ <hbox>
+ <label class="header large-text" id="name" value="&chatzilla.label;"/>
+ <spacer flex="1"/>
+ <label class="header" id="version" value="&version.unknown.label;" format="&version.known.label;"/>
+ </hbox>
+ <hbox>
+ <label class="text-link" onclick="openHomepage()" value="&homepage.label;"/>
+ <spacer flex="1"/>
+ <label class="text-link" onclick="copyVersion()" value="&copyversion.label;"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ <description id="description">&description.label;</description>
+ <label class="contributors-label header" value="&section.core.label;"/>
+ <vbox class="contributors">
+ <label>Robert Ginda</label>
+ <label>Gijs Kruitbosch</label>
+ <label>James Ross</label>
+ <label>Samuel Sieb</label>
+ </vbox>
+ <label class="contributors-label header" id="localizers-header" value="&section.locale.label;"/>
+ <vbox class="contributors" id="localizers">
+ <!-- These are inserted from onLoad(), as read from locale file. -->
+ </vbox>
+ <label class="contributors-label header" value="&section.contrib.label;"/>
+ <vbox class="contributors">
+ <label>Lim Chee Aun (graphics)</label>
+ </vbox>
+ </vbox>
+ <separator id="groove" class="groove"/>
+</dialog>
diff --git a/comm/suite/chatzilla/xul/content/browserOverlay.xul b/comm/suite/chatzilla/xul/content/browserOverlay.xul
new file mode 100644
index 0000000000..a6e31a3158
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/browserOverlay.xul
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/browserOverlay.dtd" >
+
+<!-- This is the overlay that adds a "Chatzilla" button to the toolbar palette. -->
+
+<?xml-stylesheet href="chrome://chatzilla/skin/browserOverlay.css"
+ type="text/css"?>
+
+<overlay id="ChatzillaBrowserToolbar"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script src="chrome://chatzilla/content/chatzillaOverlay.js"/>
+
+<toolbarpalette id="BrowserToolbarPalette">
+ <toolbarbutton id="chatzilla-open" oncommand="toIRC()"
+ class="toolbarbutton-1 chromeclass-toolbar-additional"
+ label="&czButton.label;" tooltiptext="&czButton.label;"/>
+</toolbarpalette>
+</overlay>
diff --git a/comm/suite/chatzilla/xul/content/channels.js b/comm/suite/chatzilla/xul/content/channels.js
new file mode 100644
index 0000000000..912e62c14a
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/channels.js
@@ -0,0 +1,875 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var client;
+var network;
+var channels = new Array();
+var tree = { view: null, newItem: null, share: new Object() };
+var xul = new Object();
+
+
+// Create list of operations. These are handled by common code.
+const OPS = new Array();
+OPS.push({ key: "noop", ignore: true });
+OPS.push({ key: "list", canStop: false });
+OPS.push({ key: "load", canStop: true });
+OPS.push({ key: "filter", canStop: true });
+
+
+// Define constants for each operation.
+// JavaScript won't let you delete things declared with "var", workaround:
+// NOTE: This order MUST be the same as those above!
+window.s = 0;
+const OP_LIST = ++s; // A /list operation on the server.
+const OP_LOAD = ++s; // Loading the saved file.
+const OP_FILTER = ++s; // Filtering the loaded list.
+
+
+// Define constants for the valid states of each operation.
+// All states before STATE_START must be idle (stopped) states.
+// All states from STATE_START onwards must be busy (running) states.
+s = 0;
+const STATE_IDLE = ++s; // Not doing this operation.
+const STATE_ERROR = ++s; // Error occurred: don't try do to any more.
+const STATE_START = ++s; // Starting an operation.
+const STATE_RUN = ++s; // Running...
+const STATE_STOP = ++s; // Clean-up/ending operation.
+delete window.s;
+
+
+// Store all the operation data here.
+var data = {
+ list: { state: STATE_IDLE },
+ load: { state: STATE_IDLE },
+ filter: { state: STATE_IDLE }
+};
+
+
+// This should keep things responsive enough, for the user to click buttons and
+// edit the filter text and options, without giving up too much time to letting
+// Gecko catch up.
+const PROCESS_TIME_MAX = 200;
+const PROCESS_DELAY = 50;
+
+const colIDToSortKey = { chanColName: "name",
+ chanColUsers: "users",
+ chanColTopic: "topic" };
+const sortKeyToColID = { name: "chanColName",
+ users: "chanColUsers",
+ topic: "chanColTopic" };
+
+function onLoad()
+{
+ function ondblclick(event) { tree.view.onRouteDblClick(event); };
+ function onkeypress(event) { tree.view.onRouteKeyPress(event); };
+ function onfocus(event) { tree.view.onRouteFocus(event); };
+ function onblur(event) { tree.view.onRouteBlur(event); };
+
+ function doJoin()
+ {
+ if (joinChannel())
+ window.close();
+ };
+
+ client = window.arguments[0].client;
+ client.joinDialog = window;
+
+ window.dd = client.mainWindow.dd;
+ window.ASSERT = client.mainWindow.ASSERT;
+ window.toUnicode = client.mainWindow.toUnicode;
+ window.getMsg = client.mainWindow.getMsg;
+ window.MSG_CHANNEL_OPENED = client.mainWindow.MSG_CHANNEL_OPENED;
+ window.MSG_FMT_JSEXCEPTION = client.mainWindow.MSG_FMT_JSEXCEPTION;
+ window.MT_INFO = client.mainWindow.MT_INFO;
+
+ // Import "MSG_CD_*"...
+ for (var m in client.mainWindow)
+ {
+ if (m.substr(0, 7) == "MSG_CD_")
+ window[m] = client.mainWindow[m];
+ }
+
+ // Cache all the XUL DOM elements.
+ var elements = ["network", "networks", "channel", "includeTopic",
+ "lastUpdated", "join", "minUsers", "maxUsers", "refresh",
+ "bottomPanel", "channels", "loadContainer", "loadLabel",
+ "loadBarDeck", "loadBar"];
+ for (var i = 0; i < elements.length; i++)
+ xul[elements[i]] = document.getElementById(elements[i]);
+
+ // Set the <dialog>'s class so we can do platform-specific CSS.
+ var dialog = document.getElementById("chatzilla-window");
+ dialog.className = "platform-" + client.platform;
+
+ // Set up the channel tree view.
+ tree.view = new XULTreeView(tree.share);
+ tree.view.onRowCommand = doJoin;
+ tree.view.cycleHeader = changeSort;
+ xul.channels.treeBoxObject.view = tree.view;
+
+ // If the new "search" binding is not working (i.e. doesn't exist)...
+ if (!("searchButton" in xul.channel))
+ {
+ // ...restore the text boxes to their former selves.
+ xul.channel.setAttribute("timeout", "500");
+ xul.channel.setAttribute("type", "timed");
+ xul.minUsers.setAttribute("timeout", "500");
+ xul.minUsers.setAttribute("type", "timed");
+ xul.maxUsers.setAttribute("timeout", "500");
+ xul.maxUsers.setAttribute("type", "timed");
+ }
+
+ // Sort by user count, descending.
+ changeSort("chanColUsers");
+
+ xul.channels.addEventListener("dblclick", ondblclick, false);
+ xul.channels.addEventListener("keypress", onkeypress, false);
+ xul.channels.addEventListener("focus", onfocus, false);
+ xul.channels.addEventListener("blur", onblur, false);
+
+ tree.newItem = new ChannelEntry("", "", MSG_CD_CREATE);
+ tree.newItem.first = true;
+ tree.view.childData.appendChild(tree.newItem);
+
+ var opener = window.arguments[0].opener;
+ if (opener)
+ {
+ // Force the window to be the right size now, not later.
+ window.sizeToContent();
+
+ // Position it centered over, but never up or left of parent.
+ var sx = Math.max((opener.outerWidth - window.outerWidth ) / 2, 0);
+ var sy = Math.max((opener.outerHeight - window.outerHeight) / 2, 0);
+ window.moveTo(opener.screenX + sx, opener.screenY + sy);
+ }
+
+ setNetwork(window.arguments[0].network);
+ setTimeout(updateOperations, PROCESS_DELAY);
+ if (network)
+ xul.channel.focus();
+ else
+ xul.network.focus();
+}
+
+function onUnload()
+{
+ delete client.joinDialog;
+}
+
+function onKeyPress(event)
+{
+ if (event.keyCode == event.DOM_VK_RETURN)
+ {
+ if (joinChannel())
+ window.close();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ else if (event.keyCode == event.DOM_VK_UP)
+ {
+ if (tree.view.selectedIndex > 0)
+ {
+ tree.view.selectedIndex = tree.view.selectedIndex - 1;
+ ensureRowIsVisible();
+ }
+ event.preventDefault();
+ }
+ else if (event.keyCode == event.DOM_VK_DOWN)
+ {
+ if (tree.view.selectedIndex < tree.view.rowCount - 1)
+ {
+ tree.view.selectedIndex = tree.view.selectedIndex + 1;
+ ensureRowIsVisible();
+ }
+ event.preventDefault();
+ }
+}
+
+function onShowingNetworks()
+{
+ while (xul.networks.lastChild)
+ xul.networks.removeChild(xul.networks.lastChild);
+
+ /* Show any network meeting at least 1 requirement:
+ * - Non-temporary (i.e. real network).
+ * - Currently connected.
+ * - Has visible tab in main window.
+ */
+ var networks = new Array();
+ for (var n in client.networks)
+ {
+ if (!client.networks[n].temporary
+ || client.networks[n].isConnected()
+ || client.mainWindow.getTabForObject(client.networks[n]))
+ {
+ networks.push(client.networks[n].unicodeName);
+ }
+ }
+ networks.sort();
+ for (var i = 0; i < networks.length; i++)
+ {
+ var menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("label", networks[i]);
+ xul.networks.appendChild(menuitem);
+ }
+}
+
+function onSelectionChange()
+{
+ update();
+}
+
+function onFilter()
+{
+ update();
+ if (network)
+ startOperation(OP_FILTER);
+}
+
+function setNetwork(newNetwork, noUpdate)
+{
+ xul.network.value = newNetwork ? newNetwork.unicodeName : "";
+ update();
+}
+
+function update()
+{
+ let newNetwork = client.getNetwork(xul.network.value);
+ if (network != newNetwork)
+ {
+ network = newNetwork;
+ if (network)
+ startOperation(OP_LOAD);
+ }
+
+ if (network)
+ {
+ var index = tree.view.selectedIndex;
+ var rows = tree.view.childData;
+ var row = index == -1 ? null : rows.locateChildByVisualRow(index);
+ var listFile = getListFile();
+ var listMod = 0;
+ if (listFile.exists() && (listFile.fileSize > 0))
+ listMod = listFile.lastModifiedTime;
+
+ xul.join.disabled = network.isConnected() && (!row || !row.name);
+ xul.lastUpdated.value = listMod ? getMsg(MSG_CD_UPDATED, [strftime(MSG_CD_UPDATED_FORMAT, new Date(listMod))]) : MSG_CD_UPDATED_NEVER;
+ xul.refresh.disabled = !network.isConnected() ||
+ (getOperationState(OP_LIST) == STATE_START) ||
+ (getOperationState(OP_LIST) == STATE_RUN);
+ xul.bottomPanel.selectedIndex = 1;
+ }
+ else
+ {
+ xul.join.disabled = !xul.network.value;
+ xul.lastUpdated.value = "";
+ xul.refresh.disabled = true;
+ xul.bottomPanel.selectedIndex = 0;
+ }
+}
+
+function joinChannel()
+{
+ update();
+ if (xul.join.disabled)
+ return false;
+
+ /* Calculate the row index AS IF the 'create' row is visible. We're going
+ * to use this so that the index chosen by the user is always consistent,
+ * whatever the visibility of the 'create' row - an index of 0 is ALWAYS
+ * the 'create' row, and >= 1 is ALWAYS the searched rows.
+ */
+ var index = tree.view.selectedIndex;
+ var row = tree.view.childData.locateChildByVisualRow(index);
+ var realIndex = index + (tree.newItem.isHidden ? 1 : 0);
+
+ client.dispatch("attach", { ircUrl: xul.network.value + "/" + row.name });
+
+ return true;
+}
+
+function focusSearch()
+{
+ xul.channel.focus();
+}
+
+function refreshList()
+{
+ startOperation(OP_LIST);
+}
+
+function updateProgress(label, pro)
+{
+ if (label)
+ {
+ xul.loadLabel.value = label;
+ }
+ else
+ {
+ var msg = getMsg(MSG_CD_SHOWING,
+ [(tree.view.rowCount - (tree.newItem.isHidden ? 0 : 1)),
+ channels.length]);
+ xul.loadLabel.value = msg;
+ }
+
+ xul.loadBarDeck.selectedIndex = (typeof pro == "undefined") ? 1 : 0;
+
+ if ((typeof pro == "undefined") || (pro == -1))
+ {
+ xul.loadBar.mode = "undetermined";
+ }
+ else
+ {
+ xul.loadBar.mode = "determined";
+ xul.loadBar.value = pro;
+ }
+}
+
+function changeSort(col)
+{
+ if (typeof col == "object")
+ col = col.id;
+
+ col = colIDToSortKey[col];
+ // Users default to descending, others ascending.
+ var dir = (col == "users" ? -1 : 1);
+
+ if (col == tree.share.sortColumn)
+ dir = -tree.share.sortDirection;
+
+ var colID = sortKeyToColID[tree.share.sortColumn];
+ var colNode = document.getElementById(colID);
+ if (colNode)
+ {
+ colNode.removeAttribute("sortActive");
+ colNode.removeAttribute("sortDirection");
+ }
+
+ tree.view.childData.setSortColumn(col, dir);
+
+ colID = sortKeyToColID[tree.share.sortColumn];
+ colNode = document.getElementById(colID);
+ if (colNode)
+ {
+ colNode.setAttribute("sortActive", "true");
+ var sortDir = (dir > 0 ? "ascending" : "descending");
+ colNode.setAttribute("sortDirection", sortDir);
+ }
+}
+
+
+// ***** BEGIN OPERATIONS CODE *****
+
+
+/* Return the static data about an operation (e.g. whether it can be
+ * stopped, etc.). The data returned is always the same for a given op code.
+ */
+function getOperation(op)
+{
+ ASSERT(op in OPS, "Invalid op-code: " + op);
+ return OPS[op];
+}
+
+/* Returns the live data about an operation (e.g. current state). Accepts
+ * either the op ID or the static data (as returned from getOperation(op)).
+ */
+function getOperationData(op)
+{
+ if (typeof op == "object")
+ return data[op.key];
+ return data[getOperation(op).key];
+}
+
+// Returns the current state of an operation; accepts same as getOperationData.
+function getOperationState(op)
+{
+ return getOperationData(op).state;
+}
+
+function startOperation(op)
+{
+ var ops = getOperation(op);
+ if (ops.ignore)
+ return;
+
+ var dbg = "startOperation(" + ops.key + ")";
+ var opData = getOperationData(ops);
+
+ // STATE_ERROR operations must not do anything. Assert and bail.
+ if (!ASSERT(opData.state != STATE_ERROR, dbg + " in STATE_ERROR"))
+ return;
+
+ // Check we can stop a non-idle operation.
+ if (!ASSERT((opData.state == STATE_IDLE) || ops.canStop,
+ dbg + " not in STATE_IDLE and can't stop"))
+ {
+ return;
+ }
+
+ // Stop the current operation.
+ if (opData.state != STATE_IDLE)
+ stopOperation(op);
+
+ // Begin!
+ var opData = getOperationData(op);
+ opData.state = STATE_START;
+ processOperation(op);
+ ASSERT(opData.state == STATE_RUN, dbg + " didn't enter STATE_RUN");
+}
+
+function updateOperations()
+{
+ for (var i = 1; i < OPS.length; i++)
+ {
+ var state = getOperationState(i);
+ if ((state == STATE_RUN) || (state == STATE_STOP))
+ processOperation(i);
+ }
+
+ setTimeout(updateOperations, PROCESS_DELAY);
+}
+
+function processOperation(op)
+{
+ var ops = getOperation(op);
+ if (ops.ignore)
+ return;
+
+ var dbg = "processOperation(" + ops.key + ")";
+ var opData = getOperationData(ops);
+
+ var fn = "processOp";
+ fn += ops.key[0].toUpperCase() + ops.key.substr(1);
+ if (opData.state == STATE_START)
+ fn += "Start";
+ else if (opData.state == STATE_RUN)
+ fn += "Run";
+ else if (opData.state == STATE_STOP)
+ fn += "Stop";
+ // assert and return if we're in a different state:
+ else if (!ASSERT(false, dbg + " invalid state: " + opData.state))
+ return;
+
+ try
+ {
+ var newState = window[fn](opData);
+ if (typeof newState != "undefined")
+ opData.state = newState;
+ }
+ catch(ex)
+ {
+ /* If an error has occurred, we display it (updateProgress) and then
+ * halt our operations to prevent further damage.
+ */
+ dd("Exception in channels.js: " + dbg + ": " + fn + ": " + formatException(ex));
+ updateProgress(formatException(ex));
+ opData.state = STATE_ERROR;
+ }
+}
+
+function stopOperation(op)
+{
+ var ops = getOperation(op);
+ if (ops.ignore)
+ return;
+
+ var dbg = "stopOperation(" + ops.key + ")";
+ var opData = getOperationData(ops);
+
+ // STATE_ERROR operations must not do anything. Assert and bail.
+ if (!ASSERT(opData.state != STATE_ERROR, dbg + " in STATE_ERROR"))
+ return;
+
+ // Nothing to do for STATE_IDLE. We shouldn't really be here, so assert.
+ if (!ASSERT(opData.state != STATE_IDLE, dbg + " in STATE_IDLE"))
+ return;
+
+ // Force the end and process synchronously.
+ opData.state = STATE_STOP;
+ processOperation(op);
+ ASSERT(opData.state == STATE_IDLE, dbg + " didn't enter STATE_IDLE");
+}
+
+// ***** END OPERATIONS CODE *****
+
+
+// ***** BEGIN OPERATION HANDLERS *****
+
+function processOpListStart(opData)
+{
+ ASSERT(network, "No network");
+ ASSERT(network.isConnected(), "Network is disconnected");
+
+ // Updates the refresh button.
+ update();
+
+ // Show a general message until we get some data.
+ updateProgress(MSG_CD_FETCHING, -1);
+
+ // Get the file we're going to save to, and start the /list.
+ var file = getListFile();
+ network.list("", file.path);
+
+ return STATE_RUN;
+}
+
+function processOpListRun(opData)
+{
+ // Update the progress and end if /list done for "list only" state.
+ updateProgress(getMsg(MSG_CD_FETCHED, network._list.count), -1);
+
+ // Stop if the network's /list has finished.
+ return (network._list.done ? STATE_STOP : STATE_RUN);
+}
+
+function processOpListStop(opData)
+{
+ // Updates the refresh button.
+ update();
+
+ // Check that /list finished okay if we're just doing a list.
+ if ("error" in network._list)
+ {
+ updateProgress(MSG_CD_ERROR_LIST);
+ }
+ else
+ {
+ updateProgress();
+ if (getOperationState(OP_LOAD) == STATE_IDLE)
+ startOperation(OP_LOAD);
+ }
+
+ return STATE_IDLE;
+}
+
+function processOpLoadStart(opData)
+{
+ ASSERT(network, "No network");
+
+ // Nuke contents.
+ tree.view.selectedIndex = -1;
+ if (tree.view.childData.childData.length > 1)
+ tree.view.childData.removeChildrenAtIndex(1, tree.view.childData.childData.length - 1);
+
+ var file = getListFile();
+ if (!file.exists())
+ {
+ // We tried to do a load, but the file does not exist. Start a list to
+ // fill up the file.
+ startOperation(OP_LIST);
+
+ // File still doesn't exist, just give up.
+ if (!file.exists())
+ return STATE_IDLE;
+ }
+
+ // Nuke more stuff.
+ channels = new Array();
+
+ // And... here we go.
+ opData.loadFile = new LocalFile(file, "<");
+ opData.loadPendingData = "";
+ opData.loadChunk = 10000;
+ opData.loadedSoFar = 0;
+
+ return STATE_RUN;
+}
+
+function processOpLoadRun(opData)
+{
+ // All states before STATE_START are "not running" states.
+ var opListRunning = (getOperationState(OP_LIST) >= STATE_START);
+
+ var end = Number(new Date()) + PROCESS_TIME_MAX;
+ while (Number(new Date()) < end)
+ {
+ var nlIndex = opData.loadPendingData.indexOf("\n");
+ if (nlIndex == -1)
+ {
+ opData.loadedSoFar += opData.loadChunk;
+ var newChunk = opData.loadFile.read(opData.loadChunk);
+ if (newChunk)
+ opData.loadPendingData += newChunk;
+ nlIndex = opData.loadPendingData.indexOf("\n");
+ if (nlIndex == -1)
+ break;
+ }
+
+ var line = opData.loadPendingData.substr(0, nlIndex);
+ opData.loadPendingData = opData.loadPendingData.substr(nlIndex + 1);
+
+ line = toUnicode(line, "UTF-8");
+ var ary = line.match(/^([^ ]+) ([^ ]+) (.*)$/);
+ if (ary)
+ {
+ var chan = new ChannelEntry(ary[1], ary[2], ary[3]);
+ channels.push(chan);
+ }
+ }
+
+ var dataLeft = opData.loadFile.inputStream.available();
+
+ // We shouldn't update the display when listing as well, as we're not
+ // going to show anything useful (always 100% or near to it, and
+ // replaces the 'fetching' message).
+ if (!opListRunning)
+ {
+ var pro = opData.loadedSoFar / (opData.loadedSoFar + dataLeft);
+ pro = Math.round(100 * pro);
+ updateProgress(getMsg(MSG_CD_LOADED, channels.length), pro);
+ }
+
+ // Done if there is no more data, and we're not *expecting* any more.
+ if ((dataLeft == 0) && !opListRunning)
+ return STATE_STOP;
+
+ return STATE_RUN;
+}
+
+function processOpLoadStop(opData)
+{
+ if (channels.length > 0)
+ tree.view.childData.appendChildren(channels);
+ opData.loadFile.close();
+ delete opData.loadFile;
+ delete opData.loadPendingData;
+ delete opData.loadChunk;
+ delete opData.loadedSoFar;
+ delete opData.loadNeverComplete;
+ updateProgress();
+
+ startOperation(OP_FILTER);
+
+ return STATE_IDLE;
+}
+
+function processOpFilterStart(opData)
+{
+ // Catch filtering with the same options on the same channels:
+ var newOptions = {network: xul.network.value.toLowerCase(),
+ text: xul.channel.value.toLowerCase(),
+ min: xul.minUsers.value * 1,
+ max: xul.maxUsers.value * 1,
+ listLen: channels.length,
+ searchTopics: xul.includeTopic.checked};
+
+ if (("filterOptions" in window) &&
+ equalsObject(window.filterOptions, newOptions))
+ {
+ return STATE_IDLE;
+ }
+
+ window.filterOptions = newOptions;
+
+ opData.text = newOptions.text;
+ opData.searchTopics = newOptions.searchTopics;
+ opData.minUsers = newOptions.min;
+ opData.maxUsers = newOptions.max;
+ opData.exactMatch = null;
+ opData.currentIndex = 0;
+ opData.channelText = opData.text;
+
+ // Log the filter, indicating which features the user is using.
+ var filters = new Array();
+ if (opData.channelText)
+ filters.push("name");
+ if (opData.searchTopics)
+ filters.push("topics");
+ if (opData.minUsers)
+ filters.push("min-users");
+ if (opData.maxUsers)
+ filters.push("max-users");
+
+ if (opData.channelText &&
+ (arrayIndexOf(["#", "&", "+", "!"], opData.channelText[0]) == -1))
+ {
+ opData.channelText = "#" + opData.channelText;
+ }
+ else
+ {
+ // Log that user has specified an explicit prefix.
+ filters.push("prefix");
+ }
+
+ // Update special "create channel" row, and select it.
+ tree.newItem.name = opData.channelText;
+ tree.newItem.unHide();
+
+ // Scroll to the top and select the "create channel" row.
+ tree.view.selectedIndex = 0;
+ xul.channels.treeBoxObject.invalidateRow(0);
+ xul.channels.treeBoxObject.scrollToRow(0);
+ ensureRowIsVisible();
+
+ updateProgress(getMsg(MSG_CD_FILTERING, [0, channels.length]), 0);
+
+ return STATE_RUN;
+}
+
+function processOpFilterRun(opData)
+{
+ var end = Number(new Date()) + PROCESS_TIME_MAX;
+ var more = false;
+
+ // Save selection because freeze/thaw screws it up.
+ // Note that we only save the item if it isn't the "create channel" row.
+ var index = tree.view.selectedIndex;
+ var item = null;
+ if (index > 0)
+ item = tree.view.childData.locateChildByVisualRow(index);
+
+ tree.view.freeze();
+ for (var i = opData.currentIndex; i < channels.length; i++)
+ {
+ var c = channels[i];
+
+ var match = (c.nameLC.indexOf(opData.text) != -1) ||
+ (opData.searchTopics &&
+ (c.topicLC.indexOf(opData.text) != -1));
+
+ if (opData.minUsers && (c.users < opData.minUsers))
+ match = false;
+ if (opData.maxUsers && (c.users > opData.maxUsers))
+ match = false;
+
+ if (match)
+ c.unHide();
+ else
+ c.hide();
+
+ if (match && (c.nameLC == opData.channelText))
+ opData.exactMatch = c;
+
+ opData.currentIndex = i;
+ if ((new Date()) > end)
+ {
+ more = true;
+ break;
+ }
+ }
+ tree.view.thaw();
+
+ // No item selected by user, so use our exact match instead.
+ if (!item && opData.exactMatch)
+ item = opData.exactMatch;
+
+ // Restore selected item.
+ if (item)
+ tree.view.selectedIndex = item.calculateVisualRow();
+ else
+ tree.view.selectedIndex = 0;
+
+ ensureRowIsVisible();
+
+ updateProgress(getMsg(MSG_CD_FILTERING,
+ [opData.currentIndex, channels.length]),
+ 100 * opData.currentIndex / channels.length);
+
+ return (more ? STATE_RUN : STATE_STOP);
+}
+
+function processOpFilterStop(opData)
+{
+ if (opData.exactMatch)
+ {
+ tree.newItem.hide();
+ }
+ // If nothing is selected, select the "create channel" row.
+ else if (tree.view.selectedIndex < 0)
+ {
+ tree.view.selectedIndex = 0;
+ }
+
+ ensureRowIsVisible();
+
+ delete opData.text;
+ delete opData.searchTopics;
+ delete opData.minUsers;
+ delete opData.maxUsers;
+ delete opData.exactMatch;
+ delete opData.currentIndex;
+ delete opData.channelText;
+ updateProgress();
+
+ return STATE_IDLE;
+}
+
+
+// ***** END OPERATION HANDLERS *****
+
+
+function ensureRowIsVisible()
+{
+ if (tree.view.selectedIndex >= 0)
+ xul.channels.treeBoxObject.ensureRowIsVisible(tree.view.selectedIndex);
+ else
+ xul.channels.treeBoxObject.ensureRowIsVisible(0);
+}
+
+function getListFile(temp)
+{
+ ASSERT(network, "No network");
+ var file = new LocalFile(network.prefs["logFileName"]);
+ if (temp)
+ file.localFile.leafName = "list.temp";
+ else
+ file.localFile.leafName = "list.txt";
+ return file.localFile;
+}
+
+
+// Tree ChannelEntry objects //
+function ChannelEntry(name, users, topic)
+{
+ this.setColumnPropertyName("chanColName", "name");
+ this.setColumnPropertyName("chanColUsers", "users");
+ this.setColumnPropertyName("chanColTopic", "topic");
+
+ // Nuke color codes and bold etc.
+ topic = topic.replace(/[\x1F\x02\x0F\x16]/g, "");
+ topic = topic.replace(/\x03\d{1,2}(?:,\d{1,2})?/g, "");
+
+ this.name = name;
+ this.users = users;
+ this.topic = topic;
+
+ this.nameLC = this.name.toLowerCase();
+ this.topicLC = this.topic.toLowerCase();
+}
+
+ChannelEntry.prototype = new XULTreeViewRecord(tree.share);
+
+ChannelEntry.prototype.sortCompare =
+function chanentry_sortcmp(a, b)
+{
+ var sc = a._share.sortColumn;
+ var sd = a._share.sortDirection;
+
+ // Make sure the special 'first' row is always first.
+ if ("first" in a)
+ return -1;
+ if ("first" in b)
+ return 1;
+
+ if (sc == "users")
+ {
+ // Force a numeric comparison.
+ a = 1 * a[sc];
+ b = 1 * b[sc];
+ }
+ else
+ {
+ // Case-insensitive, please.
+ a = a[sc].toLowerCase();
+ b = b[sc].toLowerCase();
+ }
+
+ if (a < b)
+ return -1 * sd;
+
+ if (a > b)
+ return 1 * sd;
+
+ return 0;
+}
diff --git a/comm/suite/chatzilla/xul/content/channels.xul b/comm/suite/chatzilla/xul/content/channels.xul
new file mode 100644
index 0000000000..7770a7a0d9
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/channels.xul
@@ -0,0 +1,108 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/channels.dtd">
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://chatzilla/skin/channels.css" type="text/css"?>
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="irc:chatzilla:channels"
+ id="chatzilla-window"
+ buttons="cancel"
+ onload="onLoad()"
+ onunload="onUnload()"
+ ondialogaccept="return joinChannel()"
+ title="&window.title;">
+
+ <script src="chrome://chatzilla/content/lib/js/utils.js"/>
+ <script src="chrome://chatzilla/content/lib/js/file-utils.js"/>
+ <script src="chrome://chatzilla/content/lib/xul/tree-utils.js"/>
+ <script src="channels.js"/>
+
+ <vbox flex="1">
+ <hbox id="topPanel">
+ <grid flex="1">
+ <columns>
+ <column/><column flex="1"/><column id="rightPanel"/>
+ </columns>
+ <rows>
+ <row align="center">
+ <label value="&network.label;" accesskey="&network.accesskey;"
+ control="network"/>
+ <menulist id="network" editable="true" tabindex="1"
+ oncommand="onFilter()" onblur="onFilter()"
+ onkeypress="onKeyPress(event)" onkeyup="update()">
+ <menupopup id="networks" onpopupshowing="onShowingNetworks()">
+ </menupopup>
+ </menulist>
+ <button id="join" disabled="true" default="true" tabindex="4"
+ label="&join.label;" accesskey="&join.accesskey;"
+ oncommand="if (joinChannel()) window.close()"/>
+ </row>
+ <row align="center">
+ <label value="&channel.label;" accesskey="&channel.accesskey;"
+ control="channel"/>
+ <textbox id="channel" type="search" tabindex="2"
+ oncommand="onFilter()" onkeypress="onKeyPress(event)"/>
+ <hbox align="center">
+ <label value="&minusers.label;" accesskey="&minusers.accesskey;"
+ control="minUsers"/>
+ <textbox id="minUsers" type="search" flex="1" tabindex="5"
+ oncommand="onFilter()"/>
+ </hbox>
+ </row>
+ <row align="center">
+ <spacer/>
+ <checkbox id="includeTopic" checked="true" tabindex="3"
+ label="&topics.label;" accesskey="&topics.accesskey;"
+ oncommand="onFilter(); focusSearch()"/>
+ <hbox align="center">
+ <label value="&maxusers.label;" accesskey="&maxusers.accesskey;"
+ control="maxUsers"/>
+ <textbox id="maxUsers" type="search" flex="1" tabindex="6"
+ oncommand="onFilter()"/>
+ </hbox>
+ </row>
+ <row align="center">
+ <spacer/>
+ <label id="lastUpdated"/>
+ <button id="refresh"
+ tabindex="7"
+ label="&refreshNow.label;"
+ accesskey="&refreshNow.accesskey;"
+ oncommand="refreshList(); focusSearch();"/>
+ </row>
+ </rows>
+ </grid>
+ </hbox>
+ <deck id="bottomPanel" flex="1" selectedindex="0">
+ <hbox pack="center" align="center">
+ <label value="&network.hint.label;"/>
+ </hbox>
+ <vbox>
+ <tree id="channels" flex="1" hidecolumnpicker="true" seltype="single" tabindex="8"
+ onselect="onSelectionChange()">
+ <treecols>
+ <treecol label="&col.name;" width="100" id="chanColName"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.users;" width="50" id="chanColUsers"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="&col.topic;" flex="1" id="chanColTopic"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+ <hbox id="loadContainer">
+ <label id="loadLabel" flex="1" crop="right"/>
+ <deck id="loadBarDeck">
+ <progressmeter id="loadBar" mode="undetermined" />
+ <box/>
+ </deck>
+ </hbox>
+ </vbox>
+ </deck>
+ </vbox>
+</dialog>
diff --git a/comm/suite/chatzilla/xul/content/chatzilla.xul b/comm/suite/chatzilla/xul/content/chatzilla.xul
new file mode 100644
index 0000000000..a543c1e6ae
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/chatzilla.xul
@@ -0,0 +1,141 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE window SYSTEM "chrome://chatzilla/locale/chatzilla.dtd">
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://chatzilla/skin/chatzilla.css" type="text/css"?>
+
+<?xul-overlay href="chrome://chatzilla/content/scripts.xul"?>
+<?xul-overlay href="chrome://chatzilla/content/popups.xul"?>
+<?xul-overlay href="chrome://chatzilla/content/menus.xul"?>
+
+<window id="chatzilla-window"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:aaa="http://www.w3.org/2005/07/aaa"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ orient="vertical" onload="onLoad();" onunload="onUnload();"
+ onclose="return onClose();" onmouseover="onMouseOver(event);"
+ persist="width height screenX screenY sizemode" windowtype="irc:chatzilla">
+
+ <script src="chrome://global/content/contentAreaUtils.js"/>
+ <script src="chrome://communicator/content/findUtils.js"/>
+
+ <html:link rel="icon" href="chrome://chatzilla/skin/images/logo.png" style="display:none"/>
+
+ <overlaytarget id="scripts-overlay-target"/>
+ <overlaytarget id="popup-overlay-target"/>
+ <overlaytarget id="menu-overlay-target"/>
+ <stringbundleset id="chatzilla-stringbundle">
+ <stringbundle id="findBundle" src="chrome://global/locale/finddialog.properties"/>
+ </stringbundleset>
+
+ <vbox id="upper-box" flex="1">
+ <hbox id="tabpanels-contents-box" flex="1">
+ <vbox id="user-list-box" width="125" persist="collapsed width">
+
+ <tree id="user-list" flex="1" hidecolumnpicker="true"
+ ondblclick="onUserDoubleClick(event);"
+ context="context:userlist" aaa:live="off" aria-live="off"
+ aaa:relevant="additions removals text"
+ aria-relevant="additions removals text">
+ <treecols>
+ <treecol id="usercol" hideheader="true" flex="1"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ </vbox> <!-- user-list-box -->
+
+ <splitter id="main-splitter" collapse="before" persist="collapsed left">
+ <grippy/>
+ </splitter>
+
+ <vbox flex="1" id="browser-box">
+ <deck id="output-deck" flex="1"/>
+ </vbox>
+
+ </hbox> <!-- tabpanels-contents-box -->
+
+ <vbox id="tabstrip-box" flex="0" crop="right">
+ <hbox id="view-tabs" persist="collapsed" flex="1"
+ ondragover="tabsDNDObserver.onDragOver(event);"
+ ondragexit="tabsDNDObserver.onDragExit(event);"
+ ondrop="tabsDNDObserver.onDrop(event);">
+ <tabs id="views-tbar-inner" flex="1"
+ onselect="onTabSelect(event)" setfocus="false">
+ <tab hidden="true"/> <!-- dummy tab to keep the freaking xbl from
+ causing an exception -->
+ </tabs>
+ <spacer id="views-tbar-spacer"/>
+ </hbox>
+ <hbox id="tabs-drop-indicator-bar" collapsed="true">
+ <hbox id="tabs-drop-indicator" mousethrough="always"/>
+ </hbox>
+ </vbox>
+
+ </vbox> <!-- upper-box -->
+
+ <splitter id="input-splitter" orient="vertical" collapse="after"
+ collapsed="true"/>
+
+ <hbox id="input-widgets" align="center">
+ <button id="server-nick" type="menu" label="" tooltiptext="&server-nick.tooltip;"/>
+ <hbox id="multiline-box" flex="1" collapsed="true">
+ <box id="multiline-hug-box" flex="1">
+ <textbox id="multiline-input" multiline="true" flex="1" height="100px"
+ class="multiline-input-widget" onfocus="onInputFocus();"
+ tabindex="-1"/>
+ </box>
+ <vbox>
+ <toolbarbutton id="button-input" flex="1"
+ oncommand="onMultilineSend(event);"
+ tooltiptext="&multiline-send.tooltip;" />
+ <toolbarbutton id="button-multiline-contract"
+ oncommand="dispatch('pref multiline false');"
+ tooltiptext="&multiline-contract.tooltip;" />
+ </vbox>
+ </hbox>
+ <hbox id="singleline-box" flex="1" collapsed="true">
+ <box id="singleline-hug-box" flex="1">
+ <textbox id="input" class="input-widget" flex="1"
+ onfocus="onInputFocus();" tabindex="-1"/>
+ </box>
+ <toolbarbutton id="button-multiline-expand"
+ oncommand="dispatch('pref multiline true');"
+ tooltiptext="&multiline-expand.tooltip;"/>
+ </hbox>
+ </hbox>
+
+ <statusbar id="status-bar"
+ class="chromeclass-status"
+ persist="collapsed">
+ <statusbarpanel id="component-bar"/>
+ <statusbarpanel id="status-text" label="" flex="1" crop="right"/>
+ <statusbarpanel id="status-progress-panel" class="statusbarpanel-progress">
+ <progressmeter id="status-progress-bar"
+ class="progressmeter-statusbar"
+ mode="undetermined"
+ value="0"/>
+ </statusbarpanel>
+ <statusbarpanel id="security-button"
+ class="statusbarpanel-iconic-text"
+ dir="reverse"
+ label=""
+ oncommand="displayCertificateInfo();"/>
+ <statusbarpanel id="alert-status"
+ class="statusbarpanel-iconic"
+ oncommand="updateAlertIcon(true);"/>
+ <statusbarpanel id="logging-status"
+ class="statusbarpanel-iconic"
+ oncommand="onLoggingIcon();"/>
+ <statusbarpanel id="offline-status"
+ class="statusbarpanel-iconic"
+ oncommand="client.offlineObserver.toggleOffline();"/>
+ </statusbar>
+
+</window>
diff --git a/comm/suite/chatzilla/xul/content/chatzillaOverlay.js b/comm/suite/chatzilla/xul/content/chatzillaOverlay.js
new file mode 100644
index 0000000000..7841b40f20
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/chatzillaOverlay.js
@@ -0,0 +1,11 @@
+/* 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/. */
+
+function toIRC()
+{
+
+ toOpenWindowByType("irc:chatzilla", "chrome://chatzilla/content/chatzilla.xul");
+
+}
+
diff --git a/comm/suite/chatzilla/xul/content/chatzillaOverlay.xul b/comm/suite/chatzilla/xul/content/chatzillaOverlay.xul
new file mode 100644
index 0000000000..755e4fd813
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/chatzillaOverlay.xul
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/chatzillaOverlay.dtd" >
+
+<!-- This is the overlay that addes "Chatzilla" to the (global) task menu. -->
+
+<?xml-stylesheet href="chrome://chatzilla/skin/chatzillaOverlay.css" type="text/css"?>
+
+<overlay id="ChatzillaTaskMenuID"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script type="application/x-javascript" src="chrome://chatzilla/content/chatzillaOverlay.js"/>
+
+<keyset id="tasksKeys">
+ <key id="key_irc" key="&ircCmd.commandkey;" command="Tasks:IRC" modifiers="accel"/>
+</keyset>
+
+<commandset id="tasksCommands">
+ <command id="Tasks:IRC" oncommand="toIRC();"/>
+</commandset>
+
+<menupopup id="windowPopup">
+ <menuitem
+ label="&ircCmd.label;"
+ accesskey="&ircCmd.accesskey;"
+ key="key_irc"
+ command="Tasks:IRC"
+ id="tasksMenuIRC"
+ class="menuitem-iconic"
+ insertafter="tasksMenuAddressBook,tasksMenuEditor,IMMenuItem,tasksMenuNavigator"/>
+</menupopup>
+
+<statusbarpanel id="component-bar">
+ <toolbarbutton class="taskbutton" id="mini-irc" oncommand="toIRC()"
+ insertafter="mini-addr,mini-comp,mini-aim,mini-nav" tooltiptext="&ircCmd.label;"/>
+</statusbarpanel>
+
+</overlay>
diff --git a/comm/suite/chatzilla/xul/content/commands.js b/comm/suite/chatzilla/xul/content/commands.js
new file mode 100644
index 0000000000..a99ca52c3e
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/commands.js
@@ -0,0 +1,4760 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+const CMD_CONSOLE = 0x01;
+const CMD_NEED_NET = 0x02;
+const CMD_NEED_SRV = 0x04;
+const CMD_NEED_CHAN = 0x08;
+const CMD_NEED_USER = 0x10;
+
+function initCommands()
+{
+ // Keep this in sync with the command.js section in chatzilla.properties.
+ var cmdary =
+ [/* "real" commands */
+ ["about", cmdAbout, CMD_CONSOLE],
+ ["alias", cmdAlias, CMD_CONSOLE,
+ "[<alias-name> [<command-list>]]"],
+ ["attach", cmdAttach, CMD_CONSOLE,
+ "<irc-url>"],
+ ["away", cmdAway, CMD_CONSOLE,
+ "[<reason>]"],
+ ["back", cmdAway, CMD_CONSOLE],
+ ["ban", cmdBanOrExcept, CMD_NEED_CHAN | CMD_CONSOLE,
+ "[<nickname>]"],
+ ["cancel", cmdCancel, CMD_CONSOLE],
+ ["charset", cmdCharset, CMD_CONSOLE,
+ "[<new-charset>]"],
+ ["channel-motif", cmdMotif, CMD_NEED_CHAN | CMD_CONSOLE,
+ "[<motif> [<channel>]]"],
+ ["channel-pref", cmdPref, CMD_NEED_CHAN | CMD_CONSOLE,
+ "[<pref-name> [<pref-value>]]"],
+ ["cmd-undo", "cmd-docommand cmd_undo", 0],
+ ["cmd-redo", "cmd-docommand cmd_redo", 0],
+ ["cmd-cut", "cmd-docommand cmd_cut", 0],
+ ["cmd-copy", "cmd-docommand cmd_copy", 0],
+ ["cmd-paste", "cmd-docommand cmd_paste", 0],
+ ["cmd-delete", "cmd-docommand cmd_delete", 0],
+ ["cmd-selectall", "cmd-docommand cmd_selectAll", 0],
+ ["cmd-copy-link-url", "cmd-docommand cmd_copyLink", 0,
+ "<url>"],
+ ["cmd-mozilla-prefs", "cmd-docommand cmd_mozillaPrefs", 0],
+ ["cmd-prefs", "cmd-docommand cmd_chatzillaPrefs", 0],
+ ["cmd-chatzilla-prefs", "cmd-docommand cmd_chatzillaPrefs", 0],
+ ["cmd-chatzilla-opts", "cmd-docommand cmd_chatzillaPrefs", 0],
+ ["cmd-docommand", cmdDoCommand, 0,
+ "<cmd-name>"],
+ ["create-tab-for-view", cmdCreateTabForView, 0,
+ "<view>"],
+ ["custom-away", cmdAway, 0],
+ ["op", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE,
+ "<nickname> [<...>]"],
+ ["dcc-accept", cmdDCCAccept, CMD_CONSOLE,
+ "[<nickname> [<type> [<file>]]]"],
+ ["dcc-accept-list", cmdDCCAutoAcceptList, CMD_NEED_NET | CMD_CONSOLE],
+ ["dcc-accept-list-add", cmdDCCAutoAcceptAdd,
+ CMD_NEED_NET | CMD_CONSOLE,
+ "<nickname>"],
+ ["dcc-accept-list-remove", cmdDCCAutoAcceptDel,
+ CMD_NEED_NET | CMD_CONSOLE,
+ "<nickname>"],
+ ["dcc-chat", cmdDCCChat, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<nickname>]"],
+ ["dcc-close", cmdDCCClose, CMD_CONSOLE,
+ "[<nickname> [<type> [<file>]]]"],
+ ["dcc-decline", cmdDCCDecline, CMD_CONSOLE,
+ "[<nickname>]"],
+ ["dcc-list", cmdDCCList, CMD_CONSOLE,
+ "[<type>]"],
+ ["dcc-send", cmdDCCSend, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<nickname> [<file>]]"],
+ ["dcc-show-file", cmdDCCShowFile, CMD_CONSOLE,
+ "<file>"],
+ ["delayed", cmdDelayed, CMD_CONSOLE,
+ "<delay> <rest>"],
+ ["deop", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE,
+ "<nickname> [<...>]"],
+ ["describe", cmdDescribe, CMD_NEED_SRV | CMD_CONSOLE,
+ "<target> <action>"],
+ ["hop", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE,
+ "<nickname> [<...>]"],
+ ["dehop", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE,
+ "<nickname> [<...>]"],
+ ["voice", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE,
+ "<nickname> [<...>]"],
+ ["devoice", cmdChanUserMode, CMD_NEED_CHAN | CMD_CONSOLE,
+ "<nickname> [<...>]"],
+ ["clear-view", cmdClearView, CMD_CONSOLE,
+ "[<view>]"],
+ ["client", cmdClient, CMD_CONSOLE],
+ ["commands", cmdCommands, CMD_CONSOLE,
+ "[<pattern>]"],
+ ["ctcp", cmdCTCP, CMD_NEED_SRV | CMD_CONSOLE,
+ "<target> <code> [<params>]"],
+ ["default-charset", cmdCharset, CMD_CONSOLE,
+ "[<new-charset>]"],
+ ["delete-view", cmdDeleteView, CMD_CONSOLE,
+ "[<view>]"],
+ ["desc", cmdDesc, CMD_CONSOLE,
+ "[<description>]"],
+ ["disable-plugin", cmdDisablePlugin, CMD_CONSOLE],
+ ["disconnect", cmdDisconnect, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<reason>]"],
+ ["disconnect-all", cmdDisconnectAll, CMD_CONSOLE,
+ "[<reason>]"],
+ ["echo", cmdEcho, CMD_CONSOLE,
+ "<message>"],
+ ["edit-networks", cmdEditNetworks, CMD_CONSOLE],
+ ["enable-plugin", cmdEnablePlugin, CMD_CONSOLE,
+ "<plugin>"],
+ ["eval", cmdEval, CMD_CONSOLE,
+ "<expression>"],
+ ["evalsilent", cmdEval, CMD_CONSOLE,
+ "<expression>"],
+ ["except", cmdBanOrExcept, CMD_NEED_CHAN | CMD_CONSOLE,
+ "[<nickname>]"],
+ ["find", cmdFind, 0,
+ "[<rest>]"],
+ ["find-again", cmdFindAgain, 0],
+ ["focus-input", cmdFocusInput, 0],
+ ["font-family", cmdFont, CMD_CONSOLE,
+ "[<font>]"],
+ ["font-family-other", cmdFont, 0],
+ ["font-size", cmdFont, CMD_CONSOLE,
+ "[<font-size>]"],
+ ["font-size-other", cmdFont, 0],
+ ["goto-startup", cmdGotoStartup, CMD_CONSOLE],
+ ["goto-url", cmdGotoURL, 0,
+ "<url> [<anchor>]"],
+ ["goto-url-newwin", cmdGotoURL, 0,
+ "<url> [<anchor>]"],
+ ["goto-url-newtab", cmdGotoURL, 0,
+ "<url> [<anchor>]"],
+ ["help", cmdHelp, CMD_CONSOLE,
+ "[<pattern>]"],
+ ["hide-view", cmdHideView, CMD_CONSOLE,
+ "[<view>]"],
+ ["identify", cmdIdentify, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<password>]"],
+ ["idle-away", cmdAway, 0],
+ ["idle-back", cmdAway, 0],
+ ["ignore", cmdIgnore, CMD_NEED_NET | CMD_CONSOLE,
+ "[<mask>]"],
+ ["input-text-direction", cmdInputTextDirection, 0,
+ "<dir>"],
+ ["install-plugin", cmdInstallPlugin, CMD_CONSOLE,
+ "[<url> [<name>]]"],
+ ["invite", cmdInvite, CMD_NEED_SRV | CMD_CONSOLE,
+ "<nickname> [<channel-name>]"],
+ ["join", cmdJoin, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<channel-name> [<key>]]"],
+ ["join-charset", cmdJoin, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<channel-name> <charset> [<key>]]"],
+ ["jump-to-anchor", cmdJumpToAnchor, CMD_NEED_NET,
+ "<anchor> [<channel-name>]"],
+ ["kick", cmdKick, CMD_NEED_CHAN | CMD_CONSOLE,
+ "<nickname> [<reason>]"],
+ ["kick-ban", cmdKick, CMD_NEED_CHAN | CMD_CONSOLE,
+ "<nickname> [<reason>]"],
+ ["knock", cmdKnock, CMD_NEED_SRV | CMD_CONSOLE,
+ "<channel-name> [<reason>]"],
+ ["leave", cmdLeave, CMD_NEED_NET | CMD_CONSOLE,
+ "[<channel-name>] [<reason>]"],
+ ["links", cmdSimpleCommand, CMD_NEED_SRV | CMD_CONSOLE],
+ ["list", cmdList, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<channel-name>]"],
+ ["list-plugins", cmdListPlugins, CMD_CONSOLE,
+ "[<plugin>]"],
+ ["load", cmdLoad, CMD_CONSOLE,
+ "<url>"],
+ ["log", cmdLog, CMD_CONSOLE,
+ "[<state>]"],
+ ["map", cmdSimpleCommand, CMD_NEED_SRV | CMD_CONSOLE],
+ ["marker", cmdMarker, CMD_CONSOLE],
+ ["marker-clear", cmdMarker, CMD_CONSOLE],
+ ["marker-set", cmdMarker, CMD_CONSOLE],
+ ["match-users", cmdMatchUsers, CMD_NEED_CHAN | CMD_CONSOLE,
+ "<mask>"],
+ ["me", cmdMe, CMD_CONSOLE,
+ "<action>"],
+ ["motd", cmdSimpleCommand, CMD_NEED_SRV | CMD_CONSOLE],
+ ["mode", cmdMode, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<target>] [<modestr> [<param> [<...>]]]"],
+ ["motif", cmdMotif, CMD_CONSOLE,
+ "[<motif>]"],
+ ["msg", cmdMsg, CMD_NEED_SRV | CMD_CONSOLE,
+ "<nickname> <message>"],
+ ["name", cmdName, CMD_CONSOLE,
+ "[<username>]"],
+ ["names", cmdNames, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<channel-name>]"],
+ ["network", cmdNetwork, CMD_CONSOLE,
+ "<network-name>"],
+ ["network-motif", cmdMotif, CMD_NEED_NET | CMD_CONSOLE,
+ "[<motif> [<network>]]"],
+ ["network-pref", cmdPref, CMD_NEED_NET | CMD_CONSOLE,
+ "[<pref-name> [<pref-value>]]"],
+ ["networks", cmdNetworks, CMD_CONSOLE],
+ ["nick", cmdNick, CMD_CONSOLE,
+ "[<nickname>]"],
+ ["notice", cmdNotice, CMD_NEED_SRV | CMD_CONSOLE,
+ "<nickname> <message>"],
+ ["notify", cmdNotify, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<nickname> [<...>]]"],
+ ["open-at-startup", cmdOpenAtStartup, CMD_CONSOLE,
+ "[<toggle>]"],
+ ["oper", cmdOper, CMD_NEED_SRV | CMD_CONSOLE,
+ "<opername> [<password>]"],
+ ["ping", cmdPing, CMD_NEED_SRV | CMD_CONSOLE,
+ "<nickname>"],
+ ["plugin-pref", cmdPref, CMD_CONSOLE,
+ "<plugin> [<pref-name> [<pref-value>]]"],
+ ["pref", cmdPref, CMD_CONSOLE,
+ "[<pref-name> [<pref-value>]]"],
+ ["print", cmdPrint, CMD_CONSOLE],
+ ["query", cmdQuery, CMD_NEED_SRV | CMD_CONSOLE,
+ "<nickname> [<message>]"],
+ ["quit", cmdQuit, CMD_CONSOLE,
+ "[<reason>]"],
+ ["quote", cmdQuote, CMD_NEED_NET | CMD_CONSOLE,
+ "<irc-command>"],
+ ["rename", cmdRename, CMD_CONSOLE,
+ "[<label>]"],
+ ["reload-plugin", cmdReload, CMD_CONSOLE,
+ "<plugin>"],
+ ["rlist", cmdRlist, CMD_NEED_SRV | CMD_CONSOLE,
+ "<regexp>"],
+ ["reconnect", cmdReconnect, CMD_NEED_NET | CMD_CONSOLE,
+ "[<reason>]"],
+ ["reconnect-all", cmdReconnectAll, CMD_CONSOLE,
+ "[<reason>]"],
+ ["rejoin", cmdRejoin,
+ CMD_NEED_SRV | CMD_NEED_CHAN | CMD_CONSOLE,
+ "[<reason>]"],
+ ["reload-ui", cmdReloadUI, 0],
+ ["save", cmdSave, CMD_CONSOLE,
+ "[<filename> [<savetype>]]"],
+ ["say", cmdSay, CMD_CONSOLE,
+ "<message>"],
+ ["server", cmdServer, CMD_CONSOLE,
+ "<hostname> [<port> [<password>]]"],
+ ["set-current-view", cmdSetCurrentView, 0,
+ "<view>"],
+ ["stats", cmdSimpleCommand, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<params>]"],
+ ["squery", cmdSquery, CMD_NEED_SRV | CMD_CONSOLE,
+ "<service> [<commands>]"],
+ ["sslserver", cmdServer, CMD_CONSOLE,
+ "<hostname> [<port> [<password>]]"],
+ ["ssl-exception", cmdSSLException, 0,
+ "[<hostname> <port> [<connect>]]"],
+ ["stalk", cmdStalk, CMD_CONSOLE,
+ "[<text>]"],
+ ["supports", cmdSupports, CMD_NEED_SRV | CMD_CONSOLE],
+ ["sync-font", cmdSync, 0],
+ ["sync-header", cmdSync, 0],
+ ["sync-log", cmdSync, 0],
+ ["sync-motif", cmdSync, 0],
+ ["sync-timestamp", cmdSync, 0],
+ ["sync-window", cmdSync, 0],
+ ["testdisplay", cmdTestDisplay, CMD_CONSOLE],
+ ["text-direction", cmdTextDirection, 0,
+ "<dir>"],
+ ["time", cmdTime, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<nickname>]"],
+ ["timestamps", cmdTimestamps, CMD_CONSOLE,
+ "[<toggle>]"],
+ ["toggle-ui", cmdToggleUI, CMD_CONSOLE,
+ "<thing>"],
+ ["toggle-pref", cmdTogglePref, 0,
+ "<pref-name>"],
+ ["toggle-group", cmdToggleGroup, 0,
+ "<group-id>"],
+ ["topic", cmdTopic, CMD_NEED_CHAN | CMD_CONSOLE,
+ "[<new-topic>]"],
+ ["unalias", cmdAlias, CMD_CONSOLE,
+ "<alias-name>"],
+ ["unignore", cmdIgnore, CMD_NEED_NET | CMD_CONSOLE,
+ "<mask>"],
+ ["unban", cmdBanOrExcept, CMD_NEED_CHAN | CMD_CONSOLE,
+ "<nickname>"],
+ ["unexcept", cmdBanOrExcept, CMD_NEED_CHAN | CMD_CONSOLE],
+ ["uninstall-plugin", cmdUninstallPlugin, CMD_CONSOLE,
+ "<plugin>"],
+ ["unstalk", cmdUnstalk, CMD_CONSOLE,
+ "<text>"],
+ ["urls", cmdURLs, CMD_CONSOLE,
+ "[<number>]"],
+ ["user", cmdUser, CMD_CONSOLE,
+ "[<username> <description>]"],
+ ["userhost", cmdUserhost, CMD_NEED_SRV | CMD_CONSOLE,
+ "<nickname> [<...>]"],
+ ["userip", cmdUserip, CMD_NEED_SRV | CMD_CONSOLE,
+ "<nickname> [<...>]"],
+ ["usermode", cmdUsermode, CMD_CONSOLE,
+ "[<new-mode>]"],
+ ["user-motif", cmdMotif, CMD_NEED_USER | CMD_CONSOLE,
+ "[<motif> [<user>]]"],
+ ["user-pref", cmdPref, CMD_NEED_USER | CMD_CONSOLE,
+ "[<pref-name> [<pref-value>]]"],
+ ["version", cmdVersion, CMD_NEED_SRV | CMD_CONSOLE,
+ "[<nickname>]"],
+ ["websearch", cmdWebSearch, CMD_CONSOLE,
+ "<selected-text>"],
+ ["who", cmdWho, CMD_NEED_SRV | CMD_CONSOLE,
+ "<rest>"],
+ ["whois", cmdWhoIs, CMD_NEED_SRV | CMD_CONSOLE,
+ "<nickname> [<...>]"],
+ ["whowas", cmdWhoWas, CMD_NEED_SRV | CMD_CONSOLE,
+ "<nickname> [<limit>]"],
+ ["wii", cmdWhoIsIdle, CMD_NEED_SRV | CMD_CONSOLE,
+ "<nickname> [<...>]"],
+
+ /* aliases */
+ ["exit", "quit", CMD_CONSOLE,
+ "[<reason>]"],
+ ["j", "join", CMD_CONSOLE,
+ "[<channel-name> [<key>]]"],
+ ["pass", "quote PASS", CMD_CONSOLE,
+ "<password>"],
+ ["part", "leave", CMD_CONSOLE],
+ ["raw", "quote", CMD_CONSOLE],
+ // Shortcuts to useful URLs:
+ ["faq", "goto-url-newtab faq", 0],
+ ["homepage", "goto-url-newtab homepage", 0],
+ // Used to display a nickname in the menu only.
+ ["label-user", "echo", 0,
+ "<unspecified>"],
+ ["label-user-multi", "echo", 0,
+ "<unspecified>"],
+ // These are all the font family/size menu commands...
+ ["font-family-default", "font-family default", 0],
+ ["font-family-serif", "font-family serif", 0],
+ ["font-family-sans-serif", "font-family sans-serif", 0],
+ ["font-family-monospace", "font-family monospace", 0],
+ ["font-size-default", "font-size default", 0],
+ ["font-size-small", "font-size small", 0],
+ ["font-size-medium", "font-size medium", 0],
+ ["font-size-large", "font-size large", 0],
+ ["font-size-bigger", "font-size bigger", 0],
+ // This next command is not visible; it maps to Ctrl-=, which is what
+ // you get when the user tries to do Ctrl-+ (previous command's key).
+ ["font-size-bigger2", "font-size bigger", 0],
+ ["font-size-smaller", "font-size smaller", 0],
+ ["toggle-oas", "open-at-startup toggle", 0],
+ ["toggle-ccm", "toggle-pref collapseMsgs", 0],
+ ["toggle-copy", "toggle-pref copyMessages", 0],
+ ["toggle-usort", "toggle-pref sortUsersByMode", 0],
+ ["toggle-umode", "toggle-pref showModeSymbols", 0],
+ ["toggle-timestamps","timestamps toggle", 0],
+ ["motif-dark", "motif dark", 0],
+ ["motif-light", "motif light", 0],
+ ["sync-output", "evalsilent syncOutputFrame(this)", 0],
+ ["userlist", "toggle-ui userlist", CMD_CONSOLE],
+ ["tabstrip", "toggle-ui tabstrip", CMD_CONSOLE],
+ ["statusbar", "toggle-ui status", CMD_CONSOLE],
+ ["header", "toggle-ui header", CMD_CONSOLE],
+
+ // text-direction aliases
+ ["rtl", "text-direction rtl", CMD_CONSOLE],
+ ["ltr", "text-direction ltr", CMD_CONSOLE],
+ ["toggle-text-dir", "text-direction toggle", 0],
+ ["irtl", "input-text-direction rtl", CMD_CONSOLE],
+ ["iltr", "input-text-direction ltr", CMD_CONSOLE],
+ // Services aliases
+ ["cs", "quote cs", 0],
+ ["ms", "quote ms", 0],
+ ["ns", "quote ns", 0]
+ ];
+
+ // set the stringbundle associated with these commands.
+ cmdary.stringBundle = client.defaultBundle;
+
+ client.commandManager = new CommandManager(client.defaultBundle);
+ client.commandManager.defaultFlags = CMD_CONSOLE;
+ client.commandManager.isCommandSatisfied = isCommandSatisfied;
+ client.commandManager.defineCommands(cmdary);
+
+ var restList = ["reason", "action", "text", "message", "params", "font",
+ "expression", "ircCommand", "prefValue", "newTopic", "file",
+ "password", "commandList", "commands", "description",
+ "selectedText"];
+ var stateList = ["connect"];
+
+ client.commandManager.argTypes.__aliasTypes__(restList, "rest");
+ client.commandManager.argTypes.__aliasTypes__(stateList, "state");
+ client.commandManager.argTypes["plugin"] = parsePlugin;
+}
+
+function isCommandSatisfied(e, command)
+{
+ if (typeof command == "undefined")
+ command = e.command;
+ else if (typeof command == "string")
+ command = this.commands[command];
+
+ if (command.flags & CMD_NEED_USER)
+ {
+ if (!("user" in e) || !e.user)
+ {
+ e.parseError = getMsg(MSG_ERR_NEED_USER, command.name);
+ return false;
+ }
+ }
+
+ if (command.flags & CMD_NEED_CHAN)
+ {
+ if (!("channel" in e) || !e.channel)
+ {
+ e.parseError = getMsg(MSG_ERR_NEED_CHANNEL, command.name);
+ return false;
+ }
+ }
+
+ if (command.flags & CMD_NEED_SRV)
+ {
+ if (!("server" in e) || !e.server)
+ {
+ e.parseError = getMsg(MSG_ERR_NEED_SERVER, command.name);
+ return false;
+ }
+
+ if (e.network.state != NET_ONLINE)
+ {
+ e.parseError = MSG_ERR_NOT_CONNECTED;
+ return false;
+ }
+ }
+
+ if (command.flags & (CMD_NEED_NET | CMD_NEED_SRV | CMD_NEED_CHAN))
+ {
+ if (!("network" in e) || !e.network)
+ {
+ e.parseError = getMsg(MSG_ERR_NEED_NETWORK, command.name);
+ return false;
+ }
+ }
+
+ return CommandManager.prototype.isCommandSatisfied(e, command);
+}
+
+CIRCChannel.prototype.dispatch =
+CIRCNetwork.prototype.dispatch =
+CIRCUser.prototype.dispatch =
+CIRCDCCChat.prototype.dispatch =
+CIRCDCCFileTransfer.prototype.dispatch =
+client.dispatch =
+function this_dispatch(text, e, isInteractive, flags)
+{
+ e = getObjectDetails(this, e);
+ return dispatch(text, e, isInteractive, flags);
+}
+
+function dispatch(text, e, isInteractive, flags)
+{
+ if (typeof isInteractive == "undefined")
+ isInteractive = false;
+
+ if (!e)
+ e = new Object();
+
+ if (!("sourceObject" in e))
+ e.__proto__ = getObjectDetails(client.currentObject);
+
+ if (!("isInteractive" in e))
+ e.isInteractive = isInteractive;
+
+ if (!("inputData" in e))
+ e.inputData = "";
+
+ /* split command from arguments */
+ var ary = text.match(/(\S+) ?(.*)/);
+ if (!ary)
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_COMMAND, ""));
+ return null;
+ }
+
+ e.commandText = ary[1];
+ if (ary[2])
+ e.inputData = stringTrim(ary[2]);
+
+ /* list matching commands */
+ ary = client.commandManager.list(e.commandText, flags, true);
+ var rv = null;
+ var i;
+
+ switch (ary.length)
+ {
+ case 0:
+ /* no match, try again */
+ if (e.server && e.server.isConnected &&
+ client.prefs["guessCommands"])
+ {
+ /* Want to keep the source details. */
+ var e2 = getObjectDetails(e.sourceObject);
+ e2.inputData = e.commandText + " " + e.inputData;
+ return dispatch("quote", e2);
+ }
+
+ display(getMsg(MSG_ERR_UNKNOWN_COMMAND, e.commandText), MT_ERROR);
+ break;
+
+ case 1:
+ /* one match, good for you */
+ var cm = client.commandManager;
+
+ if (cm.currentDispatchDepth >= cm.maxDispatchDepth)
+ {
+ /* We've reatched the max dispatch depth, so we need to unwind
+ * the entire stack of commands.
+ */
+ cm.dispatchUnwinding = true;
+ }
+ // Don't start any new commands while unwinding.
+ if (cm.dispatchUnwinding)
+ break;
+
+ cm.currentDispatchDepth++;
+
+ var ex;
+ try
+ {
+ rv = dispatchCommand(ary[0], e, flags);
+ }
+ catch (ex)
+ {
+ display(getMsg(MSG_ERR_INTERNAL_DISPATCH, ary[0].name),
+ MT_ERROR);
+ display(formatException(ex), MT_ERROR);
+ if (typeof ex == "object" && "stack" in ex)
+ dd(formatException(ex) + "\n" + ex.stack);
+ else
+ dd(formatException(ex), MT_ERROR);
+ }
+
+ cm.currentDispatchDepth--;
+ if (cm.dispatchUnwinding && (cm.currentDispatchDepth == 0))
+ {
+ /* Last level to unwind, and this is where we display the
+ * message. We need to leave it until here because displaying
+ * a message invokes a couple of commands itself, and we need
+ * to not be right on the dispatch limit for that.
+ */
+ cm.dispatchUnwinding = false;
+ display(getMsg(MSG_ERR_MAX_DISPATCH_DEPTH, ary[0].name),
+ MT_ERROR);
+ }
+ break;
+
+ default:
+ /* more than one match, show the list */
+ var str = "";
+ for (i in ary)
+ str += (str) ? ", " + ary[i].name : ary[i].name;
+ display(getMsg(MSG_ERR_AMBIGCOMMAND,
+ [e.commandText, ary.length, str]), MT_ERROR);
+ }
+
+ return rv;
+}
+
+function dispatchCommand (command, e, flags)
+{
+ function displayUsageError (e, details)
+ {
+ if (!("isInteractive" in e) || !e.isInteractive)
+ {
+ var caller = Components.stack.caller.caller;
+ if (caller.name == "dispatch")
+ caller = caller.caller;
+ var error = new Error (details);
+ error.fileName = caller.filename;
+ error.lineNumber = caller.lineNumber;
+ error.name = caller.name;
+ display (formatException(error), MT_ERROR);
+ }
+ else
+ {
+ display (details, MT_ERROR);
+ }
+
+ //display (getMsg(MSG_FMT_USAGE, [e.command.name, e.command.usage]),
+ // MT_USAGE);
+ return null;
+ };
+
+ function callHooks (command, isBefore)
+ {
+ var names, hooks;
+
+ if (isBefore)
+ hooks = command.beforeHooks;
+ else
+ hooks = command.afterHooks;
+
+ for (var h in hooks)
+ {
+ if ("dbgDispatch" in client && client.dbgDispatch)
+ {
+ dd ("calling " + (isBefore ? "before" : "after") +
+ " hook " + h);
+ }
+ try
+ {
+ hooks[h](e);
+ }
+ catch (ex)
+ {
+ if (e.command.name != "hook-session-display")
+ {
+ display(getMsg(MSG_ERR_INTERNAL_HOOK, h), MT_ERROR);
+ display(formatException(ex), MT_ERROR);
+ }
+ else
+ {
+ dd(getMsg(MSG_ERR_INTERNAL_HOOK, h));
+ }
+
+ dd("Caught exception calling " +
+ (isBefore ? "before" : "after") + " hook " + h);
+ dd(formatException(ex));
+ if (typeof ex == "object" && "stack" in ex)
+ dd(ex.stack);
+ else
+ dd(getStackTrace());
+ }
+ }
+ };
+
+ e.command = command;
+
+ if (!e.command.enabled)
+ {
+ /* disabled command */
+ display (getMsg(MSG_ERR_DISABLED, e.command.name),
+ MT_ERROR);
+ return null;
+ }
+
+ function parseAlias(aliasLine, e) {
+ /* Only 1 of these will be presented to the user. Math.max is used to
+ supply the 'worst' error */
+ const ALIAS_ERR_REQ_PRMS = 1;
+ const ALIAS_ERR_REQ_SRV = 2;
+ const ALIAS_ERR_REQ_RECIP = 3;
+
+ /* double slashes because of the string to regexp conversion, which
+ turns these into single slashes */
+ const SIMPLE_REPLACE = "\\$\\((\\d+)\\)";
+ const CUMUL_REPLACE = "\\$\\((\\d+)\\+\\)";
+ const RANGE_REPLACE = "\\$\\((\\d+)\\-(\\d+)\\)";
+ const NICK_REPLACE = "\\$\\((nick)\\)";
+ const RECIP_REPLACE = "\\$\\((recip)\\)";
+ const ALL_REPLACE = "\\$\\((all)\\)";
+ if (!aliasLine.match(/\$/))
+ {
+ if (e.inputData)
+ display(getMsg(MSG_EXTRA_PARAMS, e.inputData), MT_WARN);
+ return aliasLine;
+ }
+
+ function replaceAll(match, single, cumulative, start, end, nick, recip, all)
+ {
+ if (single)
+ {
+ // Simple 1-parameter replace
+ if (arrayHasElementAt(parameters, single - 1))
+ {
+ paramsUsed = Math.max(paramsUsed, single);
+ return parameters[single-1];
+ }
+ maxParamsAsked = Math.max(maxParamsAsked, single);
+ errorMsg = Math.max(ALIAS_ERR_REQ_PRMS, errorMsg);
+ return match;
+ }
+ if (cumulative)
+ {
+ // Cumulative Replace: parameters cumulative and up
+ if (arrayHasElementAt(parameters, cumulative - 1))
+ {
+ paramsUsed = parameters.length;
+ // there are never leftover parameters for $(somenumber+)
+ return parameters.slice(cumulative - 1).join(" ");
+ }
+ maxParamsAsked = Math.max(maxParamsAsked, cumulative);
+ errorMsg = Math.max(ALIAS_ERR_REQ_PRMS, errorMsg);
+ return match;
+ }
+ if (start && end)
+ {
+ // Ranged replace: parameters start through end
+ //'decrement to correct 0-based index.
+ if (start > end)
+ {
+ var iTemp = end;
+ end = start;
+ start = iTemp;
+ // We obviously have a very stupid user, but we're nice
+ }
+ start--;
+ if (arrayHasElementAt(parameters, start) &&
+ arrayHasElementAt(parameters, end - 1))
+ {
+ paramsUsed = Math.max(paramsUsed,end);
+ return parameters.slice(start, end).join(" ");
+ }
+ maxParamsAsked = Math.max(maxParamsAsked, end);
+ errorMsg = Math.max(ALIAS_ERR_REQ_PRMS, errorMsg);
+ return match;
+ }
+ if (nick)
+ {
+ // Replace with own nickname
+ if (e.network && e.server && e.network.state == NET_ONLINE)
+ return e.server.me.unicodeName;
+
+ errorMsg = Math.max(ALIAS_ERR_REQ_SRV, errorMsg);
+ return null;
+ }
+ if (recip)
+ {
+ // Replace with current recipient
+ if (e.channel)
+ return e.channel.unicodeName;
+
+ if (e.user)
+ return e.user.unicodeName;
+
+ errorMsg = ALIAS_ERR_REQ_RECIP;
+ return null;
+ }
+ // Replace with all parameters
+ paramsUsed = parameters.length;
+ return parameters.join(" ");
+ };
+
+ // If the replace function has a problem, this is an error constant:
+ var errorMsg = 0;
+
+ var paramsUsed = 0;
+ var maxParamsAsked = 0;
+
+ /* set parameters array and escaping \ and ; in parameters so the
+ * parameters don't get split up by the command list split later on */
+ e.inputData = e.inputData.replace(/([\\;])/g, "\\$1");
+ var parameters = e.inputData.match(/\S+/g);
+ if (!parameters)
+ parameters = [];
+
+ // replace in the command line.
+ var expr = [SIMPLE_REPLACE, CUMUL_REPLACE, RANGE_REPLACE, NICK_REPLACE,
+ RECIP_REPLACE, ALL_REPLACE].join("|");
+ aliasLine = aliasLine.replace(new RegExp(expr, "gi"), replaceAll);
+
+ if (errorMsg)
+ {
+ switch (errorMsg)
+ {
+ case ALIAS_ERR_REQ_PRMS:
+ display(getMsg(MSG_ERR_REQUIRED_NR_PARAM,
+ [maxParamsAsked - parameters.length,
+ maxParamsAsked]), MT_ERROR);
+ break;
+ case ALIAS_ERR_REQ_SRV:
+ display(getMsg(MSG_ERR_NEED_SERVER, e.command.name),
+ MT_ERROR);
+ break;
+ case ALIAS_ERR_REQ_RECIP:
+ display(getMsg(MSG_ERR_NEED_RECIP, e.command.name),
+ MT_ERROR);
+ break;
+ }
+ return null;
+ }
+
+ // return the revised command line.
+ if (paramsUsed < parameters.length)
+ {
+ var pmstring = parameters.slice(paramsUsed,
+ parameters.length).join(" ");
+ display(getMsg(MSG_EXTRA_PARAMS, pmstring), MT_WARN);
+ }
+ return aliasLine;
+ };
+
+ function callBeforeHooks()
+ {
+ if ("beforeHooks" in client.commandManager)
+ callHooks(client.commandManager, true);
+ if ("beforeHooks" in e.command)
+ callHooks(e.command, true);
+ };
+
+ function callAfterHooks()
+ {
+ if ("afterHooks" in e.command)
+ callHooks(e.command, false);
+ if ("afterHooks" in client.commandManager)
+ callHooks(client.commandManager, false);
+ };
+
+ var h, i;
+
+ if (typeof e.command.func == "function")
+ {
+ /* dispatch a real function */
+
+ client.commandManager.parseArguments(e);
+ if ("parseError" in e)
+ return displayUsageError(e, e.parseError);
+
+ if (("dbgDispatch" in client) && client.dbgDispatch)
+ {
+ var str = "";
+ for (i = 0; i < e.command.argNames.length; ++i)
+ {
+ var name = e.command.argNames[i];
+ if (name in e)
+ str += " " + name + ": " + e[name];
+ else if (name != ":")
+ str += " ?" + name;
+ }
+ dd(">>> " + e.command.name + str + " <<<");
+ }
+
+ callBeforeHooks();
+ try
+ {
+ e.returnValue = e.command.func(e);
+ }
+ finally
+ {
+ callAfterHooks();
+ /* set client.lastEvent *after* dispatching, so the dispatched
+ * function actually get's a chance to see the last event. */
+ if (("dbgDispatch" in client) && client.dbgDispatch)
+ client.lastEvent = e;
+ }
+ }
+ else if (typeof e.command.func == "string")
+ {
+ /* dispatch an alias (semicolon delimited list of subcommands) */
+
+ var commandList;
+ //Don't make use of e.inputData if we have multiple commands in 1 alias
+ if (e.command.func.match(/\$\(.*\)|(?:^|[^\\])(?:\\\\)*;/))
+ commandList = parseAlias(e.command.func, e);
+ else
+ commandList = e.command.func + " " + e.inputData;
+
+ if (commandList == null)
+ return null;
+ commandList = commandList.split(";");
+
+ i = 0;
+ while (i < commandList.length) {
+ if (commandList[i].match(/(?:^|[^\\])(?:\\\\)*$/) ||
+ (i == commandList.length - 1))
+ {
+ commandList[i] = commandList[i].replace(/\\(.)/g, "$1");
+ i++;
+ }
+ else
+ {
+ commandList[i] = commandList[i] + ";" + commandList[i + 1];
+ commandList.splice(i + 1, 1);
+ }
+ }
+
+ callBeforeHooks();
+ try
+ {
+ for (i = 0; i < commandList.length; ++i)
+ {
+ var newEvent = Clone(e);
+ delete newEvent.command;
+ commandList[i] = stringTrim(commandList[i]);
+ dispatch(commandList[i], newEvent, flags);
+ }
+ }
+ finally
+ {
+ callAfterHooks();
+ }
+ }
+ else
+ {
+ display(getMsg(MSG_ERR_NOTIMPLEMENTED, e.command.name), MT_ERROR);
+ return null;
+ }
+
+ return ("returnValue" in e) ? e.returnValue : null;
+}
+
+/* parse function for <plugin> parameters */
+function parsePlugin(e, name)
+{
+ var ary = e.unparsedData.match(/(?:(\S+))(?:\s+(.*))?$/);
+ if (!ary)
+ return false;
+
+ var plugin;
+
+ if (ary[1])
+ {
+ plugin = getPluginById(ary[1]);
+ if (!plugin)
+ return false;
+
+ }
+
+ e.unparsedData = ary[2] || "";
+ e[name] = plugin;
+ return true;
+}
+
+function getToggle (toggle, currentState)
+{
+ if (toggle == "toggle")
+ toggle = !currentState;
+
+ return toggle;
+}
+
+/******************************************************************************
+ * command definitions from here on down.
+ */
+
+function cmdDisablePlugin(e)
+{
+ disablePlugin(e.plugin, false);
+}
+
+function cmdEnablePlugin(e)
+{
+ if (e.plugin.enabled)
+ {
+ display(getMsg(MSG_IS_ENABLED, e.plugin.id));
+ return;
+ }
+
+ if (e.plugin.API > 0)
+ {
+ if (!e.plugin.enable())
+ {
+ display(getMsg(MSG_CANT_ENABLE, e.plugin.id));
+ e.plugin.prefs["enabled"] = false;
+ return;
+ }
+ e.plugin.prefs["enabled"] = true;
+ }
+ else if (!("enablePlugin" in e.plugin.scope))
+ {
+ display(getMsg(MSG_CANT_ENABLE, e.plugin.id));
+ return;
+ }
+ else
+ {
+ e.plugin.scope.enablePlugin();
+ }
+
+ display(getMsg(MSG_PLUGIN_ENABLED, e.plugin.id));
+ e.plugin.enabled = true;
+}
+
+function cmdBanOrExcept(e)
+{
+ var modestr;
+ switch (e.command.name)
+ {
+ case "ban":
+ modestr = "+bbbb";
+ break;
+
+ case "unban":
+ modestr = "-bbbb";
+ break;
+
+ case "except":
+ modestr = "+eeee";
+ break;
+
+ case "unexcept":
+ modestr = "-eeee";
+ break;
+
+ default:
+ ASSERT(0, "Dispatch from unknown name " + e.command.name);
+ return;
+ }
+
+ /* If we're unbanning, or banning in odd cases, we may actually be talking
+ * about a user who is not in the channel, so we need to check the server
+ * for information as well.
+ */
+ if (!e.user && e.nickname)
+ e.user = e.channel.getUser(e.nickname);
+ if (!e.user && e.nickname)
+ e.user = e.server.getUser(e.nickname);
+
+ var masks = new Array();
+
+ if (e.userList)
+ {
+ for (var i = 0; i < e.userList.length; i++)
+ masks.push(fromUnicode(e.userList[i].getBanMask(), e.server));
+ }
+ else if (e.user)
+ {
+ // We have a real user object, so get their proper 'ban mask'.
+ masks = [fromUnicode(e.user.getBanMask(), e.server)];
+ }
+ else if (e.nickname)
+ {
+ /* If we have either ! or @ in the nickname assume the user has given
+ * us a complete mask and pass it directly, otherwise assume it is
+ * only the nickname and use * for username/host.
+ */
+ masks = [fromUnicode(e.nickname, e.server)];
+ if (!/[!@]/.test(e.nickname))
+ masks[0] = masks[0] + "!*@*";
+ }
+ else
+ {
+ // Nothing specified, so we want to list the bans/excepts.
+ masks = [""];
+ }
+
+ // Collapses into groups we can do individually.
+ masks = combineNicks(masks);
+
+ for (var i = 0; i < masks.length; i++)
+ {
+ e.server.sendData("MODE " + e.channel.encodedName + " " +
+ modestr.substr(0, masks[i].count + 1) +
+ " " + masks[i] + "\n");
+ }
+}
+
+function cmdCancel(e)
+{
+ if (e.network && e.network.isRunningList())
+ {
+ // We're running a /list, terminate the output so we return to sanity.
+ display(MSG_CANCELLING_LIST);
+ return e.network.abortList();
+ }
+
+ if (e.network && ((e.network.state == NET_CONNECTING) ||
+ (e.network.state == NET_WAITING)))
+ {
+ // We're trying to connect to a network, and want to cancel. Do so:
+ if (e.deleteWhenDone)
+ e.network.deleteWhenDone = true;
+
+ display(getMsg(MSG_CANCELLING, e.network.unicodeName));
+ return e.network.cancel();
+ }
+
+ // If we're transferring a file, abort it.
+ var source = e.sourceObject;
+ if ((source.TYPE == "IRCDCCFileTransfer") && source.isActive())
+ return source.abort();
+
+ display(MSG_NOTHING_TO_CANCEL, MT_ERROR);
+}
+
+function cmdChanUserMode(e)
+{
+ var modestr;
+ switch (e.command.name)
+ {
+ case "op":
+ modestr = "+oooo";
+ break;
+
+ case "deop":
+ modestr = "-oooo";
+ break;
+
+ case "hop":
+ modestr = "+hhhh";
+ break;
+
+ case "dehop":
+ modestr = "-hhhh";
+ break;
+
+ case "voice":
+ modestr = "+vvvv";
+ break;
+
+ case "devoice":
+ modestr = "-vvvv";
+ break;
+
+ default:
+ ASSERT(0, "Dispatch from unknown name " + e.command.name);
+ return;
+ }
+
+ var nicks;
+ var user;
+ var nickList = new Array();
+ // Prefer pre-canonicalised list, then a * passed to the command directly,
+ // then a normal list, then finally a singular item (canon. or otherwise).
+ if (e.canonNickList)
+ {
+ nicks = combineNicks(e.canonNickList);
+ }
+ else if (e.nickname && (e.nickname == "*"))
+ {
+ var me = e.server.me;
+ var mode = modestr.substr(1, 1);
+ var adding = modestr[0] == "+";
+ for (userKey in e.channel.users)
+ {
+ var user = e.channel.users[userKey];
+ /* Never change our own mode and avoid trying to change someone
+ * else in a no-op manner (e.g. voicing an already voiced user).
+ */
+ if ((user.encodedName != me.encodedName) &&
+ (arrayContains(user.modes, mode) ^ adding))
+ {
+ nickList.push(user.encodedName);
+ }
+ }
+ nicks = combineNicks(nickList);
+ }
+ else if (e.nicknameList)
+ {
+ for (var i = 0; i < e.nicknameList.length; i++)
+ {
+ user = e.channel.getUser(e.nicknameList[i]);
+ if (!user)
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_USER, e.nicknameList[i]), MT_ERROR);
+ return;
+ }
+ nickList.push(user.encodedName);
+ }
+ nicks = combineNicks(nickList);
+ }
+ else if (e.nickname)
+ {
+ user = e.channel.getUser(e.nickname);
+ if (!user)
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_USER, e.nickname), MT_ERROR);
+ return;
+ }
+ var str = new String(user.encodedName);
+ str.count = 1;
+ nicks = [str];
+ }
+ else
+ {
+ // Panic?
+ dd("Help! Channel user mode command with no users...?");
+ }
+
+ for (var i = 0; i < nicks.length; ++i)
+ {
+ e.server.sendData("MODE " + e.channel.encodedName + " " +
+ modestr.substr(0, nicks[i].count + 1) +
+ " " + nicks[i] + "\n");
+ }
+}
+
+function cmdCharset(e)
+{
+ var pm;
+
+ if (e.command.name == "default-charset")
+ {
+ pm = client.prefManager;
+ msg = MSG_CURRENT_CHARSET;
+ }
+ else
+ {
+ pm = e.sourceObject.prefManager;
+ msg = MSG_CURRENT_CHARSET_VIEW;
+ }
+
+ if (e.newCharset)
+ {
+ if (e.newCharset == "-")
+ {
+ pm.clearPref("charset");
+ }
+ else
+ {
+ if(!checkCharset(e.newCharset))
+ {
+ display(getMsg(MSG_ERR_INVALID_CHARSET, e.newCharset),
+ MT_ERROR);
+ return;
+ }
+ pm.prefs["charset"] = e.newCharset;
+ }
+ }
+
+ display(getMsg(msg, pm.prefs["charset"]));
+
+ // If we're on a channel, get the topic again so it can be re-decoded.
+ if (e.newCharset && e.server && e.channel)
+ e.server.sendData("TOPIC " + e.channel.encodedName + "\n");
+}
+
+function cmdCreateTabForView(e)
+{
+ return getTabForObject(e.view, true);
+}
+
+function cmdDelayed(e)
+{
+ function _dispatch()
+ {
+ // Clear inputData so that commands without arguments work properly
+ e.inputData = "";
+ dispatch(e.rest, e, e.isInteractive);
+ }
+ setTimeout(_dispatch, e.delay * 1000);
+}
+
+function cmdSync(e)
+{
+ var fun;
+
+ switch (e.command.name)
+ {
+ case "sync-font":
+ fun = function ()
+ {
+ if (view.prefs["displayHeader"])
+ view.setHeaderState(false);
+ view.changeCSS(view.getFontCSS("data"), "cz-fonts");
+ if (view.prefs["displayHeader"])
+ view.setHeaderState(true);
+ };
+ break;
+
+ case "sync-header":
+ fun = function ()
+ {
+ view.setHeaderState(view.prefs["displayHeader"]);
+ };
+ break;
+
+ case "sync-motif":
+ fun = function ()
+ {
+ view.changeCSS(view.prefs["motif.current"]);
+ updateAppMotif(view.prefs["motif.current"]);
+ // Refresh the motif settings.
+ view.updateMotifSettings();
+ };
+ break;
+
+ case "sync-timestamp":
+ fun = function ()
+ {
+ updateTimestamps(view);
+ };
+ break;
+
+ case "sync-window":
+ fun = function ()
+ {
+ if (window && window.location &&
+ window.location.href != view.prefs["outputWindowURL"])
+ {
+ syncOutputFrame(view);
+ }
+ };
+ break;
+
+ case "sync-log":
+ fun = function ()
+ {
+ if (view.prefs["log"] ^ Boolean(view.logFile))
+ {
+ if (view.prefs["log"])
+ client.openLogFile(view, true);
+ else
+ client.closeLogFile(view, true);
+ updateLoggingIcon();
+ }
+ };
+ break;
+ }
+
+ var view = e.sourceObject;
+ var window;
+ if (("frame" in view) && view.frame)
+ window = getContentWindow(view.frame);
+
+ try
+ {
+ fun();
+ }
+ catch(ex)
+ {
+ dd("Exception in " + e.command.name + " for " + e.sourceObject.unicodeName + ": " + ex);
+ }
+}
+
+function cmdSimpleCommand(e)
+{
+ e.server.sendData(e.command.name + " " + e.inputData + "\n");
+}
+
+function cmdSquery(e)
+{
+ var data;
+
+ if (e.commands)
+ data = "SQUERY " + e.service + " :" + e.commands + "\n";
+ else
+ data = "SQUERY " + e.service + "\n";
+
+ e.server.sendData(data);
+}
+
+function cmdHelp(e)
+{
+ if (!e.pattern)
+ {
+ if ("hello" in e)
+ display(MSG_HELP_INTRO, "HELLO");
+ else
+ display(MSG_HELP_INTRO);
+ return;
+ }
+
+ var ary = client.commandManager.list(e.pattern, CMD_CONSOLE, true);
+
+ if (ary.length == 0)
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_COMMAND, e.pattern), MT_ERROR);
+ return;
+ }
+
+ for (var i in ary)
+ {
+ display(getMsg(MSG_FMT_USAGE, [ary[i].name, ary[i].helpUsage]),
+ MT_USAGE);
+ display(ary[i].help, MT_HELP);
+ }
+
+ return;
+}
+
+function cmdTestDisplay(e)
+{
+ startMsgGroup("testdisplay", MSG_COLLAPSE_TEST);
+ display(MSG_TEST_HELLO, MT_HELLO);
+ display(MSG_TEST_INFO, MT_INFO);
+ display(MSG_TEST_ERROR, MT_ERROR);
+ display(MSG_TEST_HELP, MT_HELP);
+ display(MSG_TEST_USAGE, MT_USAGE);
+ display(MSG_TEST_STATUS, MT_STATUS);
+
+ if (e.server && e.server.me)
+ {
+ var me = e.server.me;
+ var sampleUser = {TYPE: "IRCUser",
+ encodedName: "ircmonkey", collectionKey: ":ircmonkey",
+ unicodeName: "IRCMonkey", viewName: "IRCMonkey",
+ host: "", name: "IRCMonkey"};
+ var sampleChannel = {TYPE: "IRCChannel",
+ encodedName: "#mojo", collectionKey: ":#mojo",
+ unicodeName: "#Mojo", viewName: "#Mojo",
+ name: "#Mojo"};
+
+ function test (from, to)
+ {
+ var fromText = (from != me) ? from.TYPE + " ``" + from.name + "''" :
+ MSG_YOU;
+ var toText = (to != me) ? to.TYPE + " ``" + to.name + "''" :
+ MSG_YOU;
+
+ display (getMsg(MSG_TEST_PRIVMSG, [fromText, toText]),
+ "PRIVMSG", from, to);
+ display (getMsg(MSG_TEST_ACTION, [fromText, toText]),
+ "ACTION", from, to);
+ display (getMsg(MSG_TEST_NOTICE, [fromText, toText]),
+ "NOTICE", from, to);
+ }
+
+ test (sampleUser, me); /* from user to me */
+ test (me, sampleUser); /* me to user */
+
+ display(MSG_TEST_URL, "PRIVMSG", sampleUser, me);
+ display(MSG_TEST_STYLES, "PRIVMSG", sampleUser, me);
+ display(MSG_TEST_EMOTICON, "PRIVMSG", sampleUser, me);
+ display(MSG_TEST_RHEET, "PRIVMSG", sampleUser, me);
+ display(unescape(MSG_TEST_CTLCHR), "PRIVMSG", sampleUser, me);
+ display(unescape(MSG_TEST_COLOR), "PRIVMSG", sampleUser, me);
+ display(MSG_TEST_QUOTE, "PRIVMSG", sampleUser, me);
+
+ if (e.channel)
+ {
+ test (sampleUser, sampleChannel); /* user to channel */
+ test (me, sampleChannel); /* me to channel */
+ display(MSG_TEST_TOPIC, "TOPIC", sampleUser, sampleChannel);
+ display(MSG_TEST_JOIN, "JOIN", sampleUser, sampleChannel);
+ display(MSG_TEST_PART, "PART", sampleUser, sampleChannel);
+ display(MSG_TEST_KICK, "KICK", sampleUser, sampleChannel);
+ display(MSG_TEST_QUIT, "QUIT", sampleUser, sampleChannel);
+ display(getMsg(MSG_TEST_STALK, me.unicodeName),
+ "PRIVMSG", sampleUser, sampleChannel);
+ display(MSG_TEST_STYLES, "PRIVMSG", me, sampleChannel);
+ }
+ }
+ endMsgGroup();
+}
+
+function cmdNetwork(e)
+{
+ let network = client.getNetwork(e.networkName);
+
+ if (!network)
+ {
+ display (getMsg(MSG_ERR_UNKNOWN_NETWORK, e.networkName), MT_ERROR);
+ return;
+ }
+
+ dispatch("create-tab-for-view", { view: network });
+ dispatch("set-current-view", { view: network });
+}
+
+function cmdNetworks(e)
+{
+ var wrapper = newInlineText(MSG_NETWORKS_HEADA);
+
+ var netnames = keys(client.networks).sort();
+
+ for (let i = 0; i < netnames.length; i++)
+ {
+ let net = client.networks[netnames[i]];
+ let hasSecure = networkHasSecure(net.serverList);
+
+ var linkData = {
+ "data": net.unicodeName,
+ "href": (hasSecure ? "ircs://" : "irc://") + net.canonicalName
+ };
+ wrapper.appendChild(newInlineText(linkData, "chatzilla-link", "a"));
+
+ if (i < netnames.length - 1)
+ wrapper.appendChild(document.createTextNode(", "));
+ }
+
+ // Display an "Edit" link.
+ var spanb = document.createElementNS(XHTML_NS, "html:span");
+
+ client.munger.getRule(".inline-buttons").enabled = true;
+ var msg = getMsg(MSG_NETWORKS_HEADB2, "edit-networks");
+ client.munger.munge(msg, spanb, getObjectDetails(client.currentObject));
+ client.munger.getRule(".inline-buttons").enabled = false;
+
+ wrapper.appendChild(spanb);
+ display(wrapper, MT_INFO);
+}
+
+function cmdEditNetworks(e)
+{
+ toOpenWindowByType("irc:chatzilla:networks",
+ "chrome://chatzilla/content/networks-edit.xul",
+ "chrome,resizable,dialog", client);
+}
+
+function cmdServer(e)
+{
+ let scheme = (e.command.name == "sslserver") ? "ircs" : "irc";
+
+ var ary = e.hostname.match(/^(.*):(\d+)$/);
+ if (ary)
+ {
+ // Foolish user obviously hasn't read the instructions, but we're nice.
+ e.password = e.port;
+ e.port = ary[2];
+ e.hostname = ary[1];
+ }
+
+ gotoIRCURL({scheme: scheme, host: e.hostname, port: e.port,
+ pass: e.password, isserver: true});
+}
+
+function cmdSSLException(e)
+{
+ var opts = "chrome,centerscreen,modal";
+ var location = e.hostname ? e.hostname + ':' + e.port : undefined;
+ var args = {location: location, prefetchCert: true};
+
+ window.openDialog("chrome://pippki/content/exceptionDialog.xul",
+ "", opts, args);
+
+ if (!args.exceptionAdded)
+ return;
+
+ if (e.connect)
+ {
+ // When we come via the inline button, we just want to reconnect
+ if (e.source == "mouse")
+ dispatch("reconnect");
+ else
+ dispatch("sslserver " + e.hostname + " " + e.port);
+ }
+}
+
+
+function cmdQuit(e)
+{
+ // if we're not connected to anything, just close the window
+ if (!("getConnectionCount" in client) || (client.getConnectionCount() == 0))
+ {
+ client.userClose = true;
+ window.close();
+ return;
+ }
+
+ // Otherwise, try to close gracefully:
+ client.wantToQuit(e.reason, true);
+}
+
+function cmdDisconnect(e)
+{
+ if ((typeof e.reason != "string") || !e.reason)
+ e.reason = e.network.prefs["defaultQuitMsg"];
+ if (!e.reason)
+ e.reason = client.userAgent;
+
+ e.network.quit(e.reason);
+}
+
+function cmdDisconnectAll(e)
+{
+ var netReason;
+ if (confirmEx(MSG_CONFIRM_DISCONNECT_ALL, ["!yes", "!no"]) != 0)
+ return;
+
+ var conNetworks = client.getConnectedNetworks();
+ if (conNetworks.length <= 0)
+ {
+ display(MSG_NO_CONNECTED_NETS, MT_ERROR);
+ return;
+ }
+
+ for (var i = 0; i < conNetworks.length; i++)
+ {
+ netReason = e.reason;
+ if ((typeof netReason != "string") || !netReason)
+ netReason = conNetworks[i].prefs["defaultQuitMsg"];
+ netReason = (netReason ? netReason : client.userAgent);
+ conNetworks[i].quit(netReason);
+ }
+}
+
+function cmdDeleteView(e)
+{
+ if (!e.view)
+ e.view = e.sourceObject;
+
+ if (("lockView" in e.view) && e.view.lockView)
+ {
+ setTabState(e.view, "attention");
+ return;
+ }
+
+ if (e.view.TYPE == "IRCChannel" && e.view.joined)
+ {
+ e.view.dispatch("part", { deleteWhenDone: true });
+ return;
+ }
+
+ if (e.view.TYPE.substr(0, 6) == "IRCDCC")
+ {
+ if (e.view.isActive())
+ e.view.abort();
+ // abort() calls disconnect() if it is appropriate.
+ // Fall through: we don't delete on disconnect.
+ }
+
+ if (e.view.TYPE == "IRCNetwork" && (e.view.state == NET_CONNECTING ||
+ e.view.state == NET_WAITING))
+ {
+ e.view.dispatch("cancel", { deleteWhenDone: true });
+ return;
+ }
+
+ if (client.viewsArray.length < 2)
+ {
+ display(MSG_ERR_LAST_VIEW, MT_ERROR);
+ return;
+ }
+
+ var tb = getTabForObject(e.view);
+ if (tb)
+ {
+ var i = deleteTab (tb);
+ if (i != -1)
+ {
+ if (e.view.logFile)
+ {
+ e.view.logFile.close();
+ e.view.logFile = null;
+ }
+ delete e.view.messageCount;
+ delete e.view.messages;
+ deleteFrame(e.view);
+
+ var oldView = client.currentObject;
+ if (client.currentObject == e.view)
+ {
+ if (i >= client.viewsArray.length)
+ i = client.viewsArray.length - 1;
+ oldView = client.viewsArray[i].source
+ }
+ client.currentObject = null;
+ oldView.dispatch("set-current-view", { view: oldView });
+ }
+ }
+}
+
+function cmdHideView(e)
+{
+ if (!e.view)
+ e.view = e.sourceObject;
+
+ if (client.viewsArray.length < 2)
+ {
+ display(MSG_ERR_LAST_VIEW_HIDE, MT_ERROR);
+ return;
+ }
+
+ if ("messages" in e.view)
+ {
+ // Detach messages from output window content.
+ if (e.view.messages.parentNode)
+ e.view.messages.parentNode.removeChild(e.view.messages);
+
+ /* XXX Bug 335998: Adopt the messages into our own internal document
+ * so that when the real one the messages were in gets incorrectly
+ * GC-collected (see bug) the nodes still have an ownerDocument.
+ */
+ client.adoptNode(e.view.messages, client.hiddenDocument);
+ }
+
+ var tb = getTabForObject(e.view);
+
+ if (tb)
+ {
+ var i = deleteTab (tb);
+ if (i != -1)
+ {
+ deleteFrame(e.view);
+
+ var oldView = client.currentObject;
+ if (client.currentObject == e.view)
+ {
+ if (i >= client.viewsArray.length)
+ i = client.viewsArray.length - 1;
+ oldView = client.viewsArray[i].source
+ }
+ client.currentObject = null;
+ oldView.dispatch("set-current-view", { view: oldView });
+ }
+ }
+}
+
+function cmdClearView(e)
+{
+ if (!e.view)
+ e.view = e.sourceObject;
+
+ e.view.messages = null;
+ e.view.messageCount = 0;
+
+ e.view.displayHere(MSG_MESSAGES_CLEARED);
+
+ syncOutputFrame(e.view);
+}
+
+function cmdDesc(e)
+{
+ if (e.network != null) // somewhere on a network
+ {
+ dispatch("network-pref", {prefValue: e.description, prefName: "desc",
+ network: e.network,
+ isInteractive: e.isInteractive});
+ }
+ else // no network, change the general pref
+ {
+ dispatch("pref", {prefName: "desc", prefValue: e.description,
+ isInteractive: e.isInteractive});
+ }
+}
+
+function cmdName(e)
+{
+ if (e.network != null) // somewhere on a network
+ {
+ dispatch("network-pref", {prefName: "username", prefValue: e.username,
+ network: e.network,
+ isInteractive: e.isInteractive});
+ }
+ else // no network, change the general pref
+ {
+ dispatch("pref", {prefName: "username", prefValue: e.username,
+ isInteractive: e.isInteractive});
+ }
+}
+
+function cmdNames(e)
+{
+ if (e.hasOwnProperty("channelName"))
+ {
+ e.channel = new CIRCChannel(e.server, e.channelName);
+ }
+ else
+ {
+ if (!e.channel)
+ {
+ display(getMsg(MSG_ERR_REQUIRED_PARAM, "channel-name"), MT_ERROR);
+ return;
+ }
+ }
+
+ e.channel.pendingNamesReply = true;
+ e.server.sendData("NAMES " + e.channel.encodedName + "\n");
+}
+
+function cmdReconnect(e)
+{
+ if (e.network.isConnected())
+ {
+ // Set reconnect flag
+ e.network.reconnect = true;
+ if (typeof e.reason != "string")
+ e.reason = MSG_RECONNECTING;
+ // Now we disconnect.
+ e.network.quit(e.reason);
+ }
+ else
+ {
+ e.network.connect(e.network.requireSecurity);
+ }
+}
+
+function cmdReconnectAll(e)
+{
+ var reconnected = false;
+ for (var net in client.networks)
+ {
+ if (client.networks[net].isConnected() ||
+ ("messages" in client.networks[net]))
+ {
+ client.networks[net].dispatch("reconnect", { reason: e.reason });
+ reconnected = true;
+ }
+ }
+ if (!reconnected)
+ display(MSG_NO_RECONNECTABLE_NETS, MT_ERROR);
+}
+
+function cmdRejoin(e)
+{
+ if (e.channel.joined)
+ {
+ if (!e.reason)
+ e.reason = "";
+ e.channel.dispatch("part", { reason: e.reason, deleteWhenDone: false });
+ }
+
+ e.channel.join(e.channel.mode.key);
+}
+
+function cmdRename(e)
+{
+ var tab = getTabForObject(e.sourceObject);
+ if (!tab)
+ {
+ feedback(e, getMsg(MSG_ERR_INTERNAL_DISPATCH, "rename"));
+ return;
+ }
+ var label = e.label || prompt(MSG_TAB_NAME_PROMPT, tab.label);
+ if (!label)
+ {
+ return;
+ }
+ e.sourceObject.prefs["tabLabel"] = label;
+}
+
+
+function cmdTogglePref (e)
+{
+ var state = !client.prefs[e.prefName];
+ client.prefs[e.prefName] = state;
+ feedback(e, getMsg (MSG_FMT_PREF,
+ [e.prefName, state ? MSG_VAL_ON : MSG_VAL_OFF]));
+}
+
+function cmdToggleGroup(e)
+{
+ var document = getContentDocument(e.sourceObject.frame);
+ var msgs = document.querySelectorAll("[msg-groups*=\"" + e.groupId + "\"]");
+ if (!msgs.length)
+ return;
+
+ var isHidden = (msgs[0].style.display == "none");
+ for (i = 0; i < msgs.length; i++)
+ {
+ if (isHidden)
+ msgs[i].style.display = "";
+ else
+ msgs[i].style.display = "none";
+ }
+
+ var els = msgs[0].previousSibling.querySelectorAll(".chatzilla-link");
+ var button = els[els.length - 1];
+ if (button.text == MSG_COLLAPSE_HIDE)
+ {
+ button.text = MSG_COLLAPSE_SHOW;
+ button.title = MSG_COLLAPSE_SHOWTITLE;
+ }
+ else
+ {
+ button.text = MSG_COLLAPSE_HIDE;
+ button.title = MSG_COLLAPSE_HIDETITLE;
+ }
+}
+
+function cmdToggleUI(e)
+{
+ var ids = new Array();
+
+ switch (e.thing)
+ {
+ case "tabstrip":
+ ids = ["view-tabs"];
+ break;
+
+ case "userlist":
+ ids = ["main-splitter", "user-list-box"];
+ break;
+
+ case "header":
+ client.currentObject.prefs["displayHeader"] =
+ !client.currentObject.prefs["displayHeader"];
+ return;
+
+ case "status":
+ ids = ["status-bar"];
+ break;
+
+ default:
+ ASSERT (0,"Unknown element ``" + e.thing +
+ "'' passed to onToggleVisibility.");
+ return;
+ }
+
+ var newState;
+ var elem = document.getElementById(ids[0]);
+ var sourceObject = e.sourceObject;
+ var newState = !elem.collapsed;
+
+ for (var i in ids)
+ {
+ elem = document.getElementById(ids[i]);
+ elem.collapsed = newState;
+ }
+
+ updateTitle();
+ dispatch("focus-input");
+}
+
+function cmdCommands(e)
+{
+ display(MSG_COMMANDS_HEADER);
+
+ var matchResult = client.commandManager.listNames(e.pattern, CMD_CONSOLE);
+ matchResult = matchResult.join(", ");
+
+ if (e.pattern)
+ display(getMsg(MSG_MATCHING_COMMANDS, [e.pattern, matchResult]));
+ else
+ display(getMsg(MSG_ALL_COMMANDS, matchResult));
+}
+
+function cmdAttach(e)
+{
+ if (e.ircUrl.search(/ircs?:\/\//i) != 0)
+ e.ircUrl = "irc://" + e.ircUrl;
+
+ var parsedURL = parseIRCURL(e.ircUrl);
+ if (!parsedURL)
+ {
+ display(getMsg(MSG_ERR_BAD_IRCURL, e.ircUrl), MT_ERROR);
+ return;
+ }
+
+ gotoIRCURL(e.ircUrl);
+}
+
+function cmdMatchUsers(e)
+{
+ var matches = e.channel.findUsers(e.mask);
+ var uc = matches.unchecked;
+ var msgNotChecked = "";
+
+ // Get a pretty list of nicknames:
+ var nicknames = [];
+ for (var i = 0; i < matches.users.length; i++)
+ nicknames.push(matches.users[i].unicodeName);
+
+ var nicknameStr = arraySpeak(nicknames);
+
+ // Were we unable to check one or more of the users?
+ if (uc != 0)
+ msgNotChecked = getMsg(MSG_MATCH_UNCHECKED, uc);
+
+ if (matches.users.length == 0)
+ display(getMsg(MSG_NO_MATCHING_NICKS, msgNotChecked));
+ else
+ display(getMsg(MSG_MATCHING_NICKS, [nicknameStr, msgNotChecked]));
+}
+
+function cmdMe(e)
+{
+ if (!("act" in e.sourceObject))
+ {
+ display(getMsg(MSG_ERR_IMPROPER_VIEW, "me"), MT_ERROR);
+ return;
+ }
+ _sendMsgTo(e.action, "ACTION", e.sourceObject);
+}
+
+function cmdDescribe(e)
+{
+ var target = e.server.addTarget(e.target);
+ _sendMsgTo(e.action, "ACTION", target, e.sourceObject);
+}
+
+function cmdMode(e)
+{
+ var chan;
+
+ // Make sure the user can leave the channel name out from a channel view.
+ if ((!e.target || /^[\+\-].+/.test(e.target)) &&
+ !(chan && e.server.getChannel(chan)))
+ {
+ if (e.channel)
+ {
+ chan = e.channel.canonicalName;
+ if (e.param && e.modestr)
+ {
+ e.paramList.unshift(e.modestr);
+ }
+ else if (e.modestr)
+ {
+ e.paramList = [e.modestr];
+ e.param = e.modestr;
+ }
+ e.modestr = e.target;
+ }
+ else
+ {
+ display(getMsg(MSG_ERR_REQUIRED_PARAM, "target"), MT_ERROR);
+ return;
+ }
+ }
+ else
+ {
+ chan = fromUnicode(e.target, e.server);
+ }
+
+ // Check whether our mode string makes sense
+ if (!e.modestr)
+ {
+ e.modestr = "";
+ if (!e.channel && arrayContains(e.server.channelTypes, chan[0]))
+ e.channel = new CIRCChannel(e.server, null, chan);
+ if (e.channel)
+ e.channel.pendingModeReply = true;
+ }
+ else if (!(/^([+-][a-z]+)+$/i).test(e.modestr))
+ {
+ display(getMsg(MSG_ERR_INVALID_MODE, e.modestr), MT_ERROR);
+ return;
+ }
+
+ var params = (e.param) ? " " + e.paramList.join(" ") : "";
+ e.server.sendData("MODE " + chan + " " + fromUnicode(e.modestr, e.server) +
+ params + "\n");
+}
+
+function cmdMotif(e)
+{
+ var pm;
+ var msg;
+
+ if (e.command.name == "channel-motif")
+ {
+ pm = e.channel.prefManager;
+ msg = MSG_CURRENT_CSS_CHAN;
+ }
+ else if (e.command.name == "network-motif")
+ {
+ pm = e.network.prefManager;
+ msg = MSG_CURRENT_CSS_NET;
+ }
+ else if (e.command.name == "user-motif")
+ {
+ pm = e.user.prefManager;
+ msg = MSG_CURRENT_CSS_USER;
+ }
+ else
+ {
+ pm = client.prefManager;
+ msg = MSG_CURRENT_CSS;
+ }
+
+ if (e.motif)
+ {
+ if (e.motif == "-")
+ {
+ // delete local motif in favor of default
+ pm.clearPref("motif.current");
+ e.motif = pm.prefs["motif.current"];
+ }
+ else if (e.motif.search(/^(file|https?|ftp):/i) != -1)
+ {
+ // specific css file
+ pm.prefs["motif.current"] = e.motif;
+ }
+ else
+ {
+ // motif alias
+ var prefName = "motif." + e.motif;
+ if (client.prefManager.isKnownPref(prefName))
+ {
+ e.motif = client.prefManager.prefs[prefName];
+ }
+ else
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_MOTIF, e.motif), MT_ERROR);
+ return;
+ }
+
+ pm.prefs["motif.current"] = e.motif;
+ }
+
+ }
+
+ display (getMsg(msg, pm.prefs["motif.current"]));
+}
+
+function cmdList(e)
+{
+ if (!e.channelName)
+ {
+ e.channelName = "";
+ var c = e.server.channelCount;
+ if ((c > client.SAFE_LIST_COUNT) && !("listWarned" in e.network))
+ {
+ client.munger.getRule(".inline-buttons").enabled = true;
+ display(getMsg(MSG_LIST_CHANCOUNT, [c, "list"]), MT_WARN);
+ client.munger.getRule(".inline-buttons").enabled = false;
+ e.network.listWarned = true;
+ return;
+ }
+ }
+
+ e.network.list(e.channelName);
+}
+
+function cmdListPlugins(e)
+{
+ function listPlugin(plugin, i)
+ {
+ var enabled;
+ if ((plugin.API > 0) || ("disablePlugin" in plugin.scope))
+ enabled = plugin.enabled;
+ else
+ enabled = MSG_ALWAYS;
+
+ display(getMsg(MSG_FMT_PLUGIN1, [i, plugin.url]));
+ display(getMsg(MSG_FMT_PLUGIN2,
+ [plugin.id, plugin.version, enabled, plugin.status]));
+ display(getMsg(MSG_FMT_PLUGIN3, plugin.description));
+ }
+
+ if (e.plugin)
+ {
+ listPlugin(e.plugin, 0);
+ return;
+ }
+
+ var i = 0;
+ for (var k in client.plugins)
+ listPlugin(client.plugins[k], i++);
+
+ if (i == 0)
+ display(MSG_NO_PLUGINS);
+}
+
+function cmdRlist(e)
+{
+ try
+ {
+ var re = new RegExp(e.regexp, "i");
+ }
+ catch (ex)
+ {
+ display(MSG_ERR_INVALID_REGEX, MT_ERROR);
+ return;
+ }
+
+ var c = e.server.channelCount;
+ if ((c > client.SAFE_LIST_COUNT) && !("listWarned" in e.network))
+ {
+ client.munger.getRule(".inline-buttons").enabled = true;
+ display(getMsg(MSG_LIST_CHANCOUNT, [c, "rlist " + e.regexp]), MT_WARN);
+ client.munger.getRule(".inline-buttons").enabled = false;
+ e.network.listWarned = true;
+ return;
+ }
+ e.network.list(re);
+}
+
+function cmdReloadUI(e)
+{
+ if (!("getConnectionCount" in client) ||
+ client.getConnectionCount() == 0)
+ {
+ window.location.href = window.location.href;
+ }
+}
+
+function cmdQuery(e)
+{
+ // We'd rather *not* trigger the user.start event this time.
+ blockEventSounds("user", "start");
+ var user = openQueryTab(e.server, e.nickname);
+ dispatch("set-current-view", { view: user });
+
+ if (e.message)
+ _sendMsgTo(e.message, "PRIVMSG", user);
+
+ return user;
+}
+
+function cmdSay(e)
+{
+ if (!("say" in e.sourceObject))
+ {
+ display(getMsg(MSG_ERR_IMPROPER_VIEW, "say"), MT_ERROR);
+ return;
+ }
+
+ _sendMsgTo(e.message, "PRIVMSG", e.sourceObject)
+}
+
+function cmdMsg(e)
+{
+ var target = e.server.addTarget(e.nickname);
+ _sendMsgTo(e.message, "PRIVMSG", target, e.sourceObject);
+}
+
+function _sendMsgTo(message, msgType, target, displayObj)
+{
+ if (!displayObj)
+ displayObj = target;
+
+
+ var msg = filterOutput(message, msgType, target);
+
+ var o = getObjectDetails(target);
+ var lines = o.server ? o.server.splitLinesForSending(msg, true) : [msg];
+
+ for (var i = 0; i < lines.length; i++)
+ {
+ msg = lines[i];
+ if (!(o.server && o.server.caps["echo-message"]))
+ {
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ displayObj.display(msg, msgType, "ME!", target);
+ client.munger.getRule(".mailto").enabled = false;
+ }
+ if (msgType == "PRIVMSG")
+ target.say(msg);
+ else if (msgType == "NOTICE")
+ target.notice(msg);
+ else if (msgType == "ACTION")
+ target.act(msg);
+ }
+}
+
+function cmdNick(e)
+{
+ if (!e.nickname)
+ {
+ var curNick;
+ if (e.server && e.server.isConnected)
+ curNick = e.server.me.unicodeName;
+ else if (e.network)
+ curNick = e.network.prefs["nickname"];
+ else
+ curNick = client.prefs["nickname"];
+
+ e.nickname = prompt(MSG_NICK_PROMPT, curNick);
+ if (e.nickname == null)
+ return;
+ e.nickname = e.nickname.replace(/ /g, "_");
+ }
+
+ if (e.server && e.server.isConnected)
+ e.server.changeNick(e.nickname);
+
+ if (e.network)
+ {
+ /* We want to save in all non-online cases, including NET_CONNECTING,
+ * as we will only get a NICK reply if we are completely connected.
+ */
+ if (e.network.state == NET_ONLINE)
+ {
+ e.network.pendingNickChange = e.nickname;
+ }
+ else
+ {
+ e.network.prefs["nickname"] = e.nickname;
+ e.network.preferredNick = e.nickname;
+ }
+ }
+ else
+ {
+ client.prefs["nickname"] = e.nickname;
+ updateTitle(client);
+ }
+}
+
+function cmdNotice(e)
+{
+ var target = e.server.addTarget(e.nickname);
+ _sendMsgTo(e.message, "NOTICE", target, e.sourceObject);
+}
+
+function cmdQuote(e)
+{
+ /* Check we are connected, or at least pretending to be connected, so this
+ * can actually send something. The only thing that's allowed to send
+ * before the 001 is PASS, so if the command is not that and the net is not
+ * online, we stop too.
+ */
+ if ((e.network.state != NET_ONLINE) &&
+ (!e.server.isConnected || !e.ircCommand.match(/^\s*PASS/i)))
+ {
+ feedback(e, MSG_ERR_NOT_CONNECTED);
+ return;
+ }
+ e.server.sendData(fromUnicode(e.ircCommand) + "\n", e.sourceObject);
+}
+
+function cmdEval(e)
+{
+ var sourceObject = e.sourceObject;
+
+ try
+ {
+ sourceObject.doEval = function (__s) { return eval(__s); }
+ if (e.command.name == "eval")
+ sourceObject.display(e.expression, MT_EVALIN);
+ var rv = String(sourceObject.doEval (e.expression));
+ if (e.command.name == "eval")
+ sourceObject.display(rv, MT_EVALOUT);
+
+ }
+ catch (ex)
+ {
+ sourceObject.display(String(ex), MT_ERROR);
+ }
+}
+
+function cmdFocusInput(e)
+{
+ const WWATCHER_CTRID = "@mozilla.org/embedcomp/window-watcher;1";
+ const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher;
+
+ var watcher =
+ Components.classes[WWATCHER_CTRID].getService(nsIWindowWatcher);
+ if (watcher.activeWindow == window)
+ client.input.focus();
+ else
+ document.commandDispatcher.focusedElement = client.input;
+}
+
+function cmdGotoStartup(e)
+{
+ openStartupURLs();
+}
+
+function cmdGotoURL(e)
+{
+ if (/^ircs?:/.test(e.url))
+ {
+ gotoIRCURL(e.url);
+ return;
+ }
+
+ if (/^x-irc-dcc-(chat|file):[0-9a-fA-F]+$/.test(e.url))
+ {
+ var view = client.dcc.findByID(e.url.substr(15));
+ if (view)
+ dispatch("set-current-view", {view: view});
+ return;
+ }
+
+ if (/^x-cz-command:/.test(e.url))
+ {
+ var ary = e.url.match(/^x-cz-command:(.*)$/i);
+ e.sourceObject.dispatch(decodeURI(ary[1]),
+ {isInteractive: true, source: e.source});
+ return;
+ }
+
+ try
+ {
+ var uri = Services.io.newURI(e.url, "UTF-8");
+ }
+ catch (ex)
+ {
+ // Given "goto-url faq bar", expand to "http://.../faq/#bar"
+ var localeURLKey = "msg.localeurl." + e.url;
+ var hash = (("anchor" in e) && e.anchor) ? "#" + e.anchor : "";
+ if (localeURLKey != getMsg(localeURLKey))
+ dispatch(e.command.name + " " + getMsg(localeURLKey) + hash);
+ else
+ display(getMsg(MSG_ERR_INVALID_URL, e.url), MT_ERROR);
+
+ dispatch("focus-input");
+ return;
+ }
+
+ var browserWin = getWindowByType("navigator:browser");
+ var location = browserWin ? browserWin.gBrowser.currentURI.spec : null;
+ var action = e.command.name;
+ let where = "current";
+
+ // We don't want to replace ChatZilla running in a tab.
+ if ((action == "goto-url-newwin") ||
+ ((action == "goto-url") && location &&
+ location.startsWith("chrome://chatzilla/content/")))
+ {
+ where = "window";
+ }
+
+ if (action == "goto-url-newtab")
+ {
+ where = e.shiftKey ? "tabshifted" : "tab";
+ }
+
+ try
+ {
+ let loadInBackground =
+ Services.prefs.getBoolPref("browser.tabs.loadDivertedInBackground");
+ openLinkIn(e.url, where, { inBackground: loadInBackground });
+ }
+ catch (ex)
+ {
+ dd(formatException(ex));
+ }
+ dispatch("focus-input");
+}
+
+function cmdCTCP(e)
+{
+ var obj = e.server.addTarget(e.target);
+ obj.ctcp(e.code, e.params);
+}
+
+function cmdJoin(e)
+{
+ /* This check makes sure we only check if the *user* entered anything, and
+ * ignore any contextual information, like the channel the command was
+ * run on.
+ */
+ if ((!e.hasOwnProperty("channelName") || !e.channelName) &&
+ !e.channelToJoin)
+ {
+ if (client.joinDialog)
+ {
+ client.joinDialog.setNetwork(e.network);
+ client.joinDialog.focus();
+ return;
+ }
+
+ window.openDialog("chrome://chatzilla/content/channels.xul", "",
+ "resizable=yes",
+ { client: client, network: e.network || null,
+ opener: window });
+ return null;
+ }
+
+ var chan;
+ if (!e.channelToJoin)
+ {
+ if (!("charset" in e))
+ {
+ e.charset = null;
+ }
+ else if (e.charset && !checkCharset(e.charset))
+ {
+ display (getMsg(MSG_ERR_INVALID_CHARSET, e.charset), MT_ERROR);
+ return null;
+ }
+
+ if (e.channelName.search(",") != -1)
+ {
+ // We can join multiple channels! Woo!
+ var chans = e.channelName.split(",");
+ var keys = [];
+ if (e.key)
+ keys = e.key.split(",");
+ for (var c in chans)
+ {
+ chan = dispatch("join", { network: e.network,
+ server: e.server,
+ charset: e.charset,
+ channelName: chans[c],
+ key: keys.shift() });
+ }
+ return chan;
+ }
+
+ if ((arrayIndexOf(["#", "&", "+", "!"], e.channelName[0]) == -1) &&
+ (arrayIndexOf(e.server.channelTypes, e.channelName[0]) == -1))
+ {
+ e.channelName = e.server.channelTypes[0] + e.channelName;
+ }
+
+ var charset = e.charset ? e.charset : e.network.prefs["charset"];
+ chan = e.server.addChannel(e.channelName, charset);
+ if (e.charset)
+ chan.prefs["charset"] = e.charset;
+ }
+ else
+ {
+ chan = e.channelToJoin;
+ }
+
+ e.key = client.tryToGetLogin(chan.getURL(), "chan", "*", e.key, false, "");
+ chan.join(e.key);
+
+ /* !-channels are "safe" channels, and get a server-generated prefix. For
+ * this reason, we shouldn't do anything client-side until the server
+ * replies (since the reply will have the appropriate prefix). */
+ if (chan.unicodeName[0] != "!")
+ {
+ dispatch("create-tab-for-view", { view: chan });
+ dispatch("set-current-view", { view: chan });
+ }
+
+ return chan;
+}
+
+function cmdLeave(e)
+{
+ function leaveChannel(channelName)
+ {
+ var channelToLeave;
+ // This function will return true if we should continue processing
+ // channel names. If we discover that we were passed an invalid channel
+ // name, but have a channel on the event, we'll just leave that channel
+ // with the full message (including what we thought was a channel name)
+ // and return false in order to not process the rest of what we thought
+ // was a channel name. If there's a genuine error, e.g. because the user
+ // specified a non-existing channel and isn't in a channel either, we
+ // will also return a falsy value
+ var shouldContinue = true;
+ if (arrayIndexOf(e.server.channelTypes, channelName[0]) == -1)
+ {
+ // No valid prefix character. Check they really meant a channel...
+ var valid = false;
+ for (var i = 0; i < e.server.channelTypes.length; i++)
+ {
+ // Hmm, not ideal...
+ var chan = e.server.getChannel(e.server.channelTypes[i] +
+ channelName);
+ if (chan)
+ {
+ // Yes! They just missed that single character.
+ channelToLeave = chan;
+ valid = true;
+ break;
+ }
+ }
+
+ // We can only let them get away here if we've got a channel.
+ if (!valid)
+ {
+ if (e.channel)
+ {
+ /* Their channel name was invalid, but we have a channel
+ * view, so we'll assume they did "/leave part msg".
+ * NB: we use e.channelName here to get the full channel
+ * name before we (may have) split it.
+ */
+ e.reason = e.channelName + (e.reason ? " " + e.reason : "");
+ channelToLeave = e.channel;
+ shouldContinue = false;
+ }
+ else
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_CHANNEL, channelName),
+ MT_ERROR);
+ return;
+ }
+ }
+ }
+ else
+ {
+ // Valid prefix, so get real channel (if it exists...).
+ channelToLeave = e.server.getChannel(channelName);
+ if (!channelToLeave)
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_CHANNEL, channelName),
+ MT_ERROR);
+ return;
+ }
+ }
+
+ if (!("deleteWhenDone" in e))
+ e.deleteWhenDone = client.prefs["deleteOnPart"];
+
+ /* If it's not active, we're not actually in it, even though the view is
+ * still here.
+ */
+ if (channelToLeave.active)
+ {
+ channelToLeave.deleteWhenDone = e.deleteWhenDone;
+
+ if (!e.reason)
+ e.reason = "";
+
+ e.server.sendData("PART " + channelToLeave.encodedName + " :" +
+ fromUnicode(e.reason, channelToLeave) + "\n");
+ }
+ else
+ {
+ /* We can leave the channel when not active
+ * this will close the view and prevent rejoin after a reconnect
+ */
+ if (channelToLeave.joined)
+ channelToLeave.joined = false;
+
+ if (e.deleteWhenDone)
+ channelToLeave.dispatch("delete-view");
+ }
+
+ return shouldContinue;
+ };
+
+ if (!e.server)
+ {
+ display(getMsg(MSG_ERR_IMPROPER_VIEW, e.command.name), MT_ERROR);
+ return;
+ }
+
+ if (!e.hasOwnProperty("channelName") && e.channel)
+ e.channelName = e.channel.unicodeName;
+
+ if (e.hasOwnProperty("channelName"))
+ {
+ if (!e.channelName)
+ {
+ // No channel specified and command not sent from a channel view
+ display(getMsg(MSG_ERR_NEED_CHANNEL, e.command.name), MT_ERROR);
+ return;
+ }
+
+
+ var channels = e.channelName.split(",");
+ for (var i = 0; i < channels.length; i++)
+ {
+ // Skip empty channel names:
+ if (!channels[i])
+ continue;
+
+ // If we didn't successfully leave, stop processing the
+ // rest of the channels:
+ if (!leaveChannel(channels[i]))
+ break;
+ }
+ }
+}
+
+function cmdMarker(e)
+{
+ if (!client.initialized)
+ return;
+
+ var view = e.sourceObject;
+ if (!("setActivityMarker" in e.sourceObject))
+ return;
+
+ var marker = e.sourceObject.getActivityMarker();
+ if ((e.command.name == "marker") && (marker == null))
+ {
+ // Marker is not currently set but user wants to scroll to it,
+ // so we just call set like normal.
+ e.command.name = "marker-set";
+ }
+
+ switch(e.command.name)
+ {
+ case "marker": /* Scroll to the marker. */
+ e.sourceObject.scrollToElement("marker", "center");
+ break;
+ case "marker-set": /* Set (or reset) the marker. */
+ e.sourceObject.setActivityMarker(true);
+ e.sourceObject.scrollToElement("marker", "center");
+ break;
+ case "marker-clear": /* Clear the marker. */
+ e.sourceObject.setActivityMarker(false);
+ break;
+ default:
+ view.display(MSG_ERR_UNKNOWN_COMMAND, e.command.name);
+ }
+}
+
+function cmdReload(e)
+{
+ dispatch("load " + e.plugin.url);
+}
+
+function cmdLoad(e)
+{
+ if (!e.scope)
+ e.scope = new Object();
+
+ if (!("plugin" in e.scope))
+ {
+ e.scope.plugin = { url: e.url, id: MSG_UNKNOWN, version: -1,
+ description: "", status: MSG_LOADING, enabled: false,
+ PluginAPI: 1, cwd: e.url.match(/^(.*?)[^\/]+$/)[1]};
+
+ }
+
+ var plugin = e.scope.plugin;
+ plugin.scope = e.scope;
+
+ try
+ {
+ var rvStr;
+ var rv = rvStr = client.load(e.url, e.scope);
+ let oldPlugin = getPluginByURL(e.url);
+ if (oldPlugin && !disablePlugin(oldPlugin, true))
+ {
+ display(getMsg(MSG_ERR_SCRIPTLOAD, e.url));
+ return null;
+ }
+
+ if ("init" in plugin)
+ {
+ // Sanity check plugin's methods and properties:
+ var okay = false;
+ if (!("id" in plugin) || (plugin.id == MSG_UNKNOWN))
+ display(getMsg(MSG_ERR_PLUGINAPI_NOID, e.url));
+ else if (!(plugin.id.match(/^[A-Za-z0-9-_]+$/)))
+ display(getMsg(MSG_ERR_PLUGINAPI_FAULTYID, e.url));
+ else if (!("enable" in plugin))
+ display(getMsg(MSG_ERR_PLUGINAPI_NOENABLE, e.url));
+ else if (!("disable" in plugin))
+ display(getMsg(MSG_ERR_PLUGINAPI_NODISABLE, e.url));
+ else
+ okay = true;
+
+ if (!okay)
+ {
+ display (getMsg(MSG_ERR_SCRIPTLOAD, e.url));
+ return null;
+ }
+
+ plugin.API = 1;
+ plugin.prefary = [["enabled", true, "hidden"]];
+ rv = rvStr = plugin.init(e.scope);
+
+ var branch = "extensions.irc.plugins." + plugin.id + ".";
+ var prefManager = new PrefManager(branch, client.defaultBundle);
+ prefManager.addPrefs(plugin.prefary);
+ plugin.prefManager = prefManager;
+ plugin.prefs = prefManager.prefs;
+ if ("onPrefChanged" in plugin)
+ prefManager.addObserver(plugin);
+ client.prefManager.addObserver(prefManager);
+ client.prefManagers.push(prefManager);
+ }
+ else
+ {
+ plugin.API = 0;
+ if ("initPlugin" in e.scope)
+ rv = rvStr = e.scope.initPlugin(e.scope);
+ plugin.enabled = true;
+ }
+ plugin.status = "loaded";
+
+ if (typeof rv == "function")
+ rvStr = "function";
+
+ if (!plugin.id)
+ plugin.id = 'plugin' + randomString(8);
+
+ client.plugins[plugin.id] = plugin;
+
+ feedback(e, getMsg(MSG_SUBSCRIPT_LOADED, [e.url, rvStr]), MT_INFO);
+
+ if ((plugin.API > 0) && plugin.prefs["enabled"])
+ dispatch("enable-plugin " + plugin.id);
+ return {rv: rv};
+ }
+ catch (ex)
+ {
+ display (getMsg(MSG_ERR_SCRIPTLOAD, e.url));
+ display (formatException(ex), MT_ERROR);
+ }
+
+ return null;
+}
+
+function cmdWho(e)
+{
+ e.network.pendingWhoReply = true;
+ e.server.LIGHTWEIGHT_WHO = false;
+ e.server.who(e.rest);
+}
+
+function cmdWhoIs(e)
+{
+ if (!isinstance(e.network.whoisList, Object))
+ e.network.whoisList = {};
+
+ for (var i = 0; i < e.nicknameList.length; i++)
+ {
+ if ((i < e.nicknameList.length - 1) &&
+ (e.server.toLowerCase(e.nicknameList[i]) ==
+ e.server.toLowerCase(e.nicknameList[i + 1])))
+ {
+ e.server.whois(e.nicknameList[i] + " " + e.nicknameList[i]);
+ i++;
+ }
+ else
+ {
+ e.server.whois(e.nicknameList[i]);
+ }
+ e.network.whoisList[e.server.toLowerCase(e.nicknameList[i])] = null;
+ }
+}
+
+function cmdWhoIsIdle(e)
+{
+ for (var i = 0; i < e.nicknameList.length; i++)
+ e.server.whois(e.nicknameList[i] + " " + e.nicknameList[i]);
+}
+
+function cmdWhoWas(e)
+{
+ e.server.whowas(e.nickname, e.limit);
+}
+
+function cmdTopic(e)
+{
+ if (!e.newTopic)
+ e.server.sendData("TOPIC " + e.channel.encodedName + "\n");
+ else
+ e.channel.setTopic(e.newTopic);
+}
+
+function cmdAbout(e)
+{
+ if (e.source)
+ {
+ if ("aboutDialog" in client)
+ return client.aboutDialog.focus();
+
+ window.openDialog("chrome://chatzilla/content/about/about.xul", "",
+ "chrome,dialog", { client: client });
+ }
+ else
+ {
+ var ver = CIRCServer.prototype.VERSION_RPLY;
+ client.munger.getRule(".inline-buttons").enabled = true;
+ display(getMsg(MSG_ABOUT_VERSION, [ver, "about"]));
+ display(MSG_ABOUT_HOMEPAGE);
+ client.munger.getRule(".inline-buttons").enabled = false;
+ }
+}
+
+function cmdAlias(e)
+{
+ var aliasDefs = client.prefs["aliases"];
+ function getAlias(commandName)
+ {
+ for (var i = 0; i < aliasDefs.length; ++i)
+ {
+ var ary = aliasDefs[i].match(/^(.*?)\s*=\s*(.*)$/);
+ if (ary[1] == commandName)
+ return [i, ary[2]];
+ }
+
+ return null;
+ };
+
+ var ary;
+
+ if ((e.commandList == "-") || (e.command.name == "unalias"))
+ {
+ /* remove alias */
+ ary = getAlias(e.aliasName);
+ if (!ary)
+ {
+ display(getMsg(MSG_NOT_AN_ALIAS, e.aliasName), MT_ERROR);
+ return;
+ }
+
+ // Command Manager is updated when the preference changes.
+ arrayRemoveAt(aliasDefs, ary[0]);
+ aliasDefs.update();
+
+ feedback(e, getMsg(MSG_ALIAS_REMOVED, e.aliasName));
+ }
+ else if (e.aliasName && e.commandList)
+ {
+ /* add/change alias */
+ ary = getAlias(e.aliasName);
+ if (ary)
+ aliasDefs[ary[0]] = e.aliasName + " = " + e.commandList;
+ else
+ aliasDefs.push(e.aliasName + " = " + e.commandList);
+
+ // Command Manager is updated when the preference changes.
+ aliasDefs.update();
+
+ feedback(e, getMsg(MSG_ALIAS_CREATED, [e.aliasName, e.commandList]));
+ }
+ else if (e.aliasName)
+ {
+ /* display alias */
+ ary = getAlias(e.aliasName);
+ if (!ary)
+ display(getMsg(MSG_NOT_AN_ALIAS, e.aliasName), MT_ERROR);
+ else
+ display(getMsg(MSG_FMT_ALIAS, [e.aliasName, ary[1]]));
+ }
+ else
+ {
+ /* list aliases */
+ if (aliasDefs.length == 0)
+ {
+ display(MSG_NO_ALIASES);
+ }
+ else
+ {
+ for (var i = 0; i < aliasDefs.length; ++i)
+ {
+ ary = aliasDefs[i].match(/^(.*?)\s*=\s*(.*)$/);
+ if (ary)
+ display(getMsg(MSG_FMT_ALIAS, [ary[1], ary[2]]));
+ else
+ display(getMsg(MSG_ERR_BADALIAS, aliasDefs[i]));
+ }
+ }
+ }
+}
+
+function cmdAway(e)
+{
+ function sendToAllNetworks(command, reason)
+ {
+ for (var n in client.networks)
+ {
+ var net = client.networks[n];
+ if (net.primServ && (net.state == NET_ONLINE))
+ {
+ // If we can override the network's away state, or they are
+ // already idly-away, or they're not away to begin with:
+ if (overrideAway || net.isIdleAway || !net.prefs["away"])
+ {
+ net.dispatch(command, {reason: reason });
+ net.isIdleAway = (e.command.name == "idle-away");
+ }
+ }
+ }
+ };
+
+ // Idle away shouldn't override away state set by the user.
+ var overrideAway = (e.command.name.indexOf("idle") != 0);
+
+ if ((e.command.name == "away") || (e.command.name == "custom-away") ||
+ (e.command.name == "idle-away"))
+ {
+ /* going away */
+ if (e.command.name == "custom-away")
+ {
+ e.reason = prompt(MSG_AWAY_PROMPT);
+ // prompt() returns null for cancelling, a string otherwise (even if empty).
+ if (e.reason == null)
+ return;
+ }
+ // No parameter, or user entered nothing in the prompt.
+ if (!e.reason)
+ e.reason = MSG_AWAY_DEFAULT;
+
+ // Update away list (remove from current location).
+ for (var i = 0; i < client.awayMsgs.length; i++)
+ {
+ if (client.awayMsgs[i].message == e.reason)
+ {
+ client.awayMsgs.splice(i, 1);
+ break;
+ }
+ }
+ // Always put new item at start.
+ var newMsg = { message: e.reason };
+ client.awayMsgs.unshift(newMsg);
+ // Make sure we've not exceeded the limit set.
+ if (client.awayMsgs.length > client.awayMsgCount)
+ client.awayMsgs.splice(client.awayMsgCount);
+ // And now, to save the list!
+ try
+ {
+ var awayFile = new nsLocalFile(client.prefs["profilePath"]);
+ awayFile.append("awayMsgs.txt");
+ var awayLoader = new TextSerializer(awayFile);
+ if (awayLoader.open(">"))
+ {
+ awayLoader.serialize(client.awayMsgs);
+ awayLoader.close();
+ }
+ }
+ catch(ex)
+ {
+ display(getMsg(MSG_ERR_AWAY_SAVE, formatException(ex)), MT_ERROR);
+ }
+
+ // Actually do away stuff, is this on a specific network?
+ if (e.server)
+ {
+ var normalNick = e.network.prefs["nickname"];
+ var awayNick = e.network.prefs["awayNick"];
+ if (e.network.state == NET_ONLINE)
+ {
+ // Postulate that if normal nick and away nick are the same,
+ // user doesn't want to change nicks:
+ if (awayNick && (normalNick != awayNick))
+ e.server.changeNick(awayNick);
+ e.server.sendData("AWAY :" + fromUnicode(e.reason, e.network) +
+ "\n");
+ }
+ if (awayNick && (normalNick != awayNick))
+ e.network.preferredNick = awayNick;
+ e.network.prefs["away"] = e.reason;
+ }
+ else
+ {
+ // Client view, do command for all networks.
+ sendToAllNetworks("away", e.reason);
+ client.prefs["away"] = e.reason;
+
+ // Don't tell people how to get back if they're idle:
+ var idleMsgParams = [e.reason, client.prefs["awayIdleTime"]];
+ if (e.command.name == "idle-away")
+ var msg = getMsg(MSG_IDLE_AWAY_ON, idleMsgParams);
+ else
+ msg = getMsg(MSG_AWAY_ON, e.reason);
+
+ // Display on the *client* tab, or on the current tab iff
+ // there's nowhere else they'll hear about it:
+ if (("frame" in client) && client.frame)
+ client.display(msg);
+ else if (!client.getConnectedNetworks())
+ display(msg);
+ }
+ }
+ else
+ {
+ /* returning */
+ if (e.server)
+ {
+ if (e.network.state == NET_ONLINE)
+ {
+ var curNick = e.server.me.unicodeName;
+ var awayNick = e.network.prefs["awayNick"];
+ if (awayNick && (curNick == awayNick))
+ e.server.changeNick(e.network.prefs["nickname"]);
+ e.server.sendData("AWAY\n");
+ }
+ // Go back to old nick, even if not connected:
+ if (awayNick && (curNick == awayNick))
+ e.network.preferredNick = e.network.prefs["nickname"];
+ e.network.prefs["away"] = "";
+ }
+ else
+ {
+ client.prefs["away"] = "";
+ // Client view, do command for all networks.
+ sendToAllNetworks("back");
+ if (("frame" in client) && client.frame)
+ client.display(MSG_AWAY_OFF);
+ else if (!client.getConnectedNetworks())
+ display(MSG_AWAY_OFF);
+ }
+ }
+}
+
+function cmdOpenAtStartup(e)
+{
+ var origURL = e.sourceObject.getURL();
+ var url = makeCanonicalIRCURL(origURL);
+ var list = client.prefs["initialURLs"];
+ ensureCachedCanonicalURLs(list);
+ var index = arrayIndexOf(list.canonicalURLs, url);
+
+ if (e.toggle == null)
+ {
+ if (index == -1)
+ display(getMsg(MSG_STARTUP_NOTFOUND, url));
+ else
+ display(getMsg(MSG_STARTUP_EXISTS, url));
+ return;
+ }
+
+ e.toggle = getToggle(e.toggle, (index != -1));
+
+ if (e.toggle)
+ {
+ // yes, please open at startup
+ if (index == -1)
+ {
+ list.push(origURL);
+ list.update();
+ display(getMsg(MSG_STARTUP_ADDED, url));
+ }
+ else
+ {
+ display(getMsg(MSG_STARTUP_EXISTS, url));
+ }
+ }
+ else
+ {
+ // no, please don't open at startup
+ if (index != -1)
+ {
+ arrayRemoveAt(list, index);
+ list.update();
+ display(getMsg(MSG_STARTUP_REMOVED, url));
+ }
+ else
+ {
+ display(getMsg(MSG_STARTUP_NOTFOUND, url));
+ }
+ }
+}
+
+function cmdOper(e)
+{
+ e.password = client.tryToGetLogin(e.server.getURL(), "oper", e.opername,
+ e.password, true, MSG_NEED_OPER_PASSWORD);
+
+ if (!e.password)
+ return;
+
+ e.server.sendData("OPER " + fromUnicode(e.opername, e.server) + " " +
+ fromUnicode(e.password, e.server) + "\n");
+}
+
+function cmdPing (e)
+{
+ e.network.dispatch("ctcp", { target: e.nickname, code: "PING" });
+}
+
+function cmdPref (e)
+{
+ var msg;
+ var pm;
+
+ if (e.command.name == "network-pref")
+ {
+ pm = e.network.prefManager;
+ msg = MSG_FMT_NETPREF;
+ }
+ else if (e.command.name == "channel-pref")
+ {
+ pm = e.channel.prefManager;
+ msg = MSG_FMT_CHANPREF;
+ }
+ else if (e.command.name == "plugin-pref")
+ {
+ pm = e.plugin.prefManager;
+ msg = MSG_FMT_PLUGINPREF;
+ }
+ else if (e.command.name == "user-pref")
+ {
+ pm = e.user.prefManager;
+ msg = MSG_FMT_USERPREF;
+ }
+ else
+ {
+ pm = client.prefManager;
+ msg = MSG_FMT_PREF;
+ }
+
+ var ary = pm.listPrefs(e.prefName);
+ if (ary.length == 0)
+ {
+ display (getMsg(MSG_ERR_UNKNOWN_PREF, [e.prefName]),
+ MT_ERROR);
+ return false;
+ }
+
+ if (e.prefValue == "-")
+ e.deletePref = true;
+
+ if (e.deletePref)
+ {
+ if (!(e.prefName in pm.prefRecords))
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_PREF, [e.prefName]), MT_ERROR);
+ return false;
+ }
+
+ try
+ {
+ pm.clearPref(e.prefName);
+ }
+ catch (ex)
+ {
+ // ignore exception generated by clear of nonexistant pref
+ if (!("result" in ex) ||
+ ex.result != Components.results.NS_ERROR_UNEXPECTED)
+ {
+ throw ex;
+ }
+ }
+
+ var prefValue = pm.prefs[e.prefName];
+ feedback (e, getMsg(msg, [e.prefName, pm.prefs[e.prefName]]));
+ return true;
+ }
+
+ if (e.prefValue)
+ {
+ if (!(e.prefName in pm.prefRecords))
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_PREF, [e.prefName]), MT_ERROR);
+ return false;
+ }
+
+ var r = pm.prefRecords[e.prefName];
+ var def, type;
+
+ if (typeof r.defaultValue == "function")
+ def = r.defaultValue(e.prefName);
+ else
+ def = r.defaultValue;
+
+ type = typeof def;
+
+ switch (type)
+ {
+ case "number":
+ e.prefValue = Number(e.prefValue);
+ break;
+ case "boolean":
+ e.prefValue = (e.prefValue.toLowerCase() == "true");
+ break;
+ case "string":
+ break;
+ default:
+ if (isinstance(e.prefValue, Array))
+ e.prefValue = e.prefValue.join("; ");
+ if (isinstance(def, Array))
+ e.prefValue = pm.stringToArray(e.prefValue);
+ break;
+ }
+
+ pm.prefs[e.prefName] = e.prefValue;
+ if (isinstance(e.prefValue, Array))
+ e.prefValue = e.prefValue.join("; ");
+ feedback (e, getMsg(msg, [e.prefName, e.prefValue]));
+ }
+ else
+ {
+ for (var i = 0; i < ary.length; ++i)
+ {
+ var value;
+ if (isinstance(pm.prefs[ary[i]], Array))
+ value = pm.prefs[ary[i]].join("; ");
+ else
+ value = pm.prefs[ary[i]];
+
+ feedback(e, getMsg(msg, [ary[i], value]));
+ }
+ }
+
+ return true;
+}
+
+function cmdPrint(e)
+{
+ if (("frame" in e.sourceObject) && e.sourceObject.frame &&
+ getContentWindow(e.sourceObject.frame))
+ {
+ getContentWindow(e.sourceObject.frame).print();
+ }
+ else
+ {
+ display(MSG_ERR_UNABLE_TO_PRINT);
+ }
+}
+
+function cmdVersion(e)
+{
+ if (e.nickname)
+ e.network.dispatch("ctcp", { target: e.nickname, code: "VERSION"});
+ else
+ e.server.sendData(fromUnicode("VERSION") + "\n", e.sourceObject);
+}
+
+function cmdEcho(e)
+{
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ display(e.message);
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+function cmdInvite(e)
+{
+ var channel;
+
+ if (e.channelName)
+ {
+ channel = e.server.getChannel(e.channelName);
+ if (!channel)
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_CHANNEL, e.channelName), MT_ERROR);
+ return;
+ }
+ }
+ else if (e.channel)
+ {
+ channel = e.channel;
+ }
+ else
+ {
+ display(getMsg(MSG_ERR_NO_CHANNEL, e.command.name), MT_ERROR);
+ return;
+ }
+
+ channel.invite(e.nickname);
+}
+
+function cmdKick(e)
+{
+ if (e.userList)
+ {
+ if (e.command.name == "kick-ban")
+ {
+ e.sourceObject.dispatch("ban", { userList: e.userList,
+ canonNickList: e.canonNickList,
+ user: e.user,
+ nickname: e.user.encodedName });
+ }
+
+ /* Note that we always do /kick below; the /ban is covered above.
+ * Also note that we are required to pass the nickname, to satisfy
+ * the dispatching of the command (which is defined with a required
+ * <nickname> parameter). It's not required for /ban, above, but it
+ * seems prudent to include it anyway.
+ */
+ for (var i = 0; i < e.userList.length; i++)
+ {
+ var e2 = { user: e.userList[i],
+ nickname: e.userList[i].encodedName };
+ e.sourceObject.dispatch("kick", e2);
+ }
+ return;
+ }
+
+ if (!e.user)
+ e.user = e.channel.getUser(e.nickname);
+
+ if (!e.user)
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_USER, e.nickname), MT_ERROR);
+ return;
+ }
+
+ if (e.command.name == "kick-ban")
+ e.sourceObject.dispatch("ban", { nickname: e.user.encodedName });
+
+ e.user.kick(e.reason);
+}
+
+function cmdKnock(e)
+{
+ var rest = (e.reason ? " :" + fromUnicode(e.reason, e.server) : "") + "\n";
+ e.server.sendData("KNOCK " + fromUnicode(e.channelName, e.server) + rest);
+}
+
+function cmdClient(e)
+{
+ if (!("messages" in client))
+ {
+ client.display(MSG_WELCOME, "HELLO");
+ dispatch("set-current-view", { view: client });
+ dispatch("help", { hello: true });
+ dispatch("networks");
+ }
+ else
+ {
+ dispatch("set-current-view", { view: client });
+ }
+}
+
+function cmdNotify(e)
+{
+ var net = e.network;
+ var supports_monitor = net.primServ.supports["monitor"];
+
+ if (!e.nickname)
+ {
+ if (net.prefs["notifyList"].length > 0)
+ {
+ if (supports_monitor)
+ {
+ // Just get the status of the monitor list from the server.
+ net.primServ.sendData("MONITOR S\n");
+ }
+ else
+ {
+ /* delete the lists and force a ISON check, this will
+ * print the current online/offline status when the server
+ * responds */
+ delete net.onList;
+ delete net.offList;
+ onNotifyTimeout();
+ }
+ }
+ else
+ {
+ display(MSG_NO_NOTIFY_LIST);
+ }
+ }
+ else
+ {
+ var adds = new Array();
+ var subs = new Array();
+
+ for (var i in e.nicknameList)
+ {
+ var nickname = e.server.toLowerCase(e.nicknameList[i]);
+ var list = net.prefs["notifyList"];
+ list = e.server.toLowerCase(list.join(";")).split(";");
+ var idx = arrayIndexOf (list, nickname);
+ if (idx == -1)
+ {
+ net.prefs["notifyList"].push (nickname);
+ adds.push(nickname);
+ }
+ else
+ {
+ arrayRemoveAt (net.prefs["notifyList"], idx);
+ subs.push(nickname);
+ }
+ }
+ net.prefs["notifyList"].update();
+
+ var msgname;
+
+ if (adds.length > 0)
+ {
+ if (supports_monitor)
+ net.primServ.sendMonitorList(adds, true);
+
+ msgname = (adds.length == 1) ? MSG_NOTIFY_ADDONE :
+ MSG_NOTIFY_ADDSOME;
+ display(getMsg(msgname, arraySpeak(adds)));
+ }
+
+ if (subs.length > 0)
+ {
+ if (supports_monitor)
+ net.primServ.sendMonitorList(subs, false);
+
+ msgname = (subs.length == 1) ? MSG_NOTIFY_DELONE :
+ MSG_NOTIFY_DELSOME;
+ display(getMsg(msgname, arraySpeak(subs)));
+ }
+
+ delete net.onList;
+ delete net.offList;
+ if (!supports_monitor)
+ onNotifyTimeout();
+ }
+}
+
+function cmdStalk(e)
+{
+ var list = client.prefs["stalkWords"];
+
+ if (!e.text)
+ {
+ if (list.length == 0)
+ display(MSG_NO_STALK_LIST);
+ else
+ {
+ function alphabetize(a, b)
+ {
+ var A = a.toLowerCase();
+ var B = b.toLowerCase();
+ if (A < B) return -1;
+ if (B < A) return 1;
+ return 0;
+ }
+
+ list.sort(alphabetize);
+ display(getMsg(MSG_STALK_LIST, list.join(", ")));
+ }
+ return;
+ }
+
+ var notStalkingWord = true;
+ var loweredText = e.text.toLowerCase();
+
+ for (var i = 0; i < list.length; ++i)
+ if (list[i].toLowerCase() == loweredText)
+ notStalkingWord = false;
+
+ if (notStalkingWord)
+ {
+ list.push(e.text);
+ list.update();
+ display(getMsg(MSG_STALK_ADD, e.text));
+ }
+ else
+ {
+ display(getMsg(MSG_STALKING_ALREADY, e.text));
+ }
+}
+
+function cmdUnstalk(e)
+{
+ e.text = e.text.toLowerCase();
+ var list = client.prefs["stalkWords"];
+
+ for (var i = 0; i < list.length; ++i)
+ {
+ if (list[i].toLowerCase() == e.text)
+ {
+ list.splice(i, 1);
+ list.update();
+ display(getMsg(MSG_STALK_DEL, e.text));
+ return;
+ }
+ }
+
+ display(getMsg(MSG_ERR_UNKNOWN_STALK, e.text), MT_ERROR);
+}
+
+function cmdUser(e)
+{
+ dispatch("name", {username: e.username, network: e.network,
+ isInteractive: e.isInteractive});
+ dispatch("desc", {description: e.description, network: e.network,
+ isInteractive: e.isInteractive});
+}
+
+function cmdUserhost(e)
+{
+ var nickList = combineNicks(e.nicknameList, 5);
+ for (var i = 0; i < nickList.length; i++)
+ {
+ e.server.userhost(nickList[i]);
+ }
+}
+
+function cmdUserip(e)
+{
+ // Check if the server supports this
+ if (!e.server.servCmds.userip)
+ {
+ display(getMsg(MSG_ERR_UNSUPPORTED_COMMAND, "USERIP"), MT_ERROR);
+ return;
+ }
+ var nickList = combineNicks(e.nicknameList, 5);
+ for (var i = 0; i < nickList.length; i++)
+ e.server.userip(nickList[i]);
+}
+
+function cmdUsermode(e)
+{
+ if (e.newMode)
+ {
+ if (e.sourceObject.network)
+ e.sourceObject.network.prefs["usermode"] = e.newMode;
+ else
+ client.prefs["usermode"] = e.newMode;
+ }
+ else
+ {
+ if (e.server && e.server.isConnected)
+ {
+ e.server.sendData("mode " + e.server.me.encodedName + "\n");
+ }
+ else
+ {
+ var prefs;
+
+ if (e.network)
+ prefs = e.network.prefs;
+ else
+ prefs = client.prefs;
+
+ display(getMsg(MSG_USER_MODE,
+ [prefs["nickname"], prefs["usermode"]]),
+ MT_MODE);
+ }
+ }
+}
+
+function cmdLog(e)
+{
+ var view = e.sourceObject;
+
+ if (e.state != null)
+ {
+ e.state = getToggle(e.state, view.prefs["log"])
+ view.prefs["log"] = e.state;
+ }
+ else
+ {
+ if (view.prefs["log"])
+ display(getMsg(MSG_LOGGING_ON, getLogPath(view)));
+ else
+ display(MSG_LOGGING_OFF);
+ }
+}
+
+function cmdSave(e)
+{
+ var OutputProgressListener =
+ {
+ onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ // Use this to access onStateChange flags
+ var requestSpec;
+ try
+ {
+ var channel = aRequest.QueryInterface(nsIChannel);
+ requestSpec = channel.URI.spec;
+ }
+ catch (ex) { }
+
+ // Detect end of file saving of any file:
+ if (aStateFlags & nsIWebProgressListener.STATE_STOP)
+ {
+ if (aStatus == kErrorBindingAborted)
+ aStatus = 0;
+
+ // We abort saving for all errors except if image src file is
+ // not found
+ var abortSaving = (aStatus != 0 && aStatus != kFileNotFound);
+ if (abortSaving)
+ {
+ // Cancel saving
+ wbp.cancelSave();
+ display(getMsg(MSG_SAVE_ERR_FAILED, aMessage), MT_ERROR);
+ return;
+ }
+
+ if (aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK
+ && wbp.currentState == nsIWBP.PERSIST_STATE_FINISHED)
+ {
+ // Let the user know:
+ pm = [e.sourceObject.viewName, getURLSpecFromFile(file)];
+ display(getMsg(MSG_SAVE_SUCCESSFUL, pm), MT_INFO);
+ }
+ /* Check if we've finished. WebBrowserPersist screws up when we
+ * don't save additional files. Cope when saving html only or
+ * text.
+ */
+ else if (!requestSpec && saveType > 0)
+ {
+ if (wbp)
+ wbp.progressListener = null;
+ pm = [e.sourceObject.viewName, getURLSpecFromFile(file)];
+ display(getMsg(MSG_SAVE_SUCCESSFUL, pm), MT_INFO);
+ }
+ }
+ },
+
+ onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress,
+ aMaxSelfProgress, aCurTotalProgress,
+ aMaxTotalProgress) {},
+ onLocationChange: function(aWebProgress, aRequest, aLocation) {},
+ onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) {},
+ onSecurityChange: function(aWebProgress, aRequest, state) {},
+
+ QueryInterface: function(aIID)
+ {
+ if (aIID.equals(Components.interfaces.nsIWebProgressListener)
+ || aIID.equals(Components.interfaces.nsISupports)
+ || aIID.equals(Components.interfaces.nsISupportsWeakReference))
+ {
+ return this;
+ }
+
+ throw Components.results.NS_NOINTERFACE;
+ }
+ };
+
+ const kFileNotFound = 2152857618;
+ const kErrorBindingAborted = 2152398850;
+
+ const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
+ const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
+ const nsIChannel = Components.interfaces.nsIChannel;
+
+ var wbp = newObject("@mozilla.org/embedding/browser/nsWebBrowserPersist;1",
+ nsIWBP);
+ wbp.progressListener = OutputProgressListener;
+
+ var file, saveType, saveFolder, docToBeSaved, title;
+ var flags, fileType, charLimit;
+ var dialogTitle, rv, pm;
+
+ // We want proper descriptions and no "All Files" option.
+ const TYPELIST = [[MSG_SAVE_COMPLETEVIEW,"*.htm;*.html"],
+ [MSG_SAVE_HTMLONLYVIEW,"*.htm;*.html"],
+ [MSG_SAVE_PLAINTEXTVIEW,"*.txt"], "$noAll"];
+ // constants and variables for the wbp.saveDocument call
+ var saveTypes =
+ {
+ complete: 0,
+ htmlonly: 1,
+ text: 2
+ };
+
+ if (!e.filename)
+ {
+ dialogTitle = getMsg(MSG_SAVE_DIALOGTITLE, e.sourceObject.viewName);
+ rv = pickSaveAs(dialogTitle, TYPELIST, e.sourceObject.viewName +
+ ".html");
+ if (!rv.ok)
+ return;
+ saveType = rv.picker.filterIndex;
+ file = rv.file;
+ e.filename = rv.file.path;
+ }
+ else
+ {
+ try
+ {
+ // Try to use this as a path
+ file = nsLocalFile(e.filename);
+ }
+ catch (ex)
+ {
+ // try to use it as a URL
+ try
+ {
+ file = getFileFromURLSpec(e.filename);
+ }
+ catch(ex)
+ {
+ // What is the user thinking? It's not rocket science...
+ display(getMsg(MSG_SAVE_ERR_INVALID_PATH, e.filename),
+ MT_ERROR);
+ return;
+ }
+ }
+
+ // Get extension and determine savetype
+ if (!e.savetype)
+ {
+ var extension = file.path.substr(file.path.lastIndexOf("."));
+ if (extension == ".txt")
+ {
+ saveType = saveTypes["text"];
+ }
+ else if (extension.match(/\.x?html?$/))
+ {
+ saveType = saveTypes["complete"];
+ }
+ else
+ {
+ // No saveType and no decent extension --> out!
+ var errMsg;
+ if (extension.indexOf(".") < 0)
+ errMsg = MSG_SAVE_ERR_NO_EXT;
+ else
+ errMsg = getMsg(MSG_SAVE_ERR_INVALID_EXT, extension);
+ display(errMsg, MT_ERROR);
+ return;
+ }
+ }
+ else
+ {
+ if (!(e.savetype in saveTypes))
+ {
+ // no valid saveType
+ display(getMsg(MSG_SAVE_ERR_INVALID_SAVETYPE, e.savetype),
+ MT_ERROR);
+ return;
+ }
+ saveType = saveTypes[e.savetype];
+ }
+
+ var askforreplace = (e.isInteractive && file.exists());
+ if (askforreplace && !confirm(getMsg(MSG_SAVE_FILEEXISTS, e.filename)))
+ return;
+ }
+
+ // We don't want to convert anything, leave everything as is and replace
+ // old files, as the user has been prompted about that already.
+ wbp.persistFlags |= nsIWBP.PERSIST_FLAGS_NO_CONVERSION
+ | nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES
+ | nsIWBP.PERSIST_FLAGS_NO_BASE_TAG_MODIFICATIONS
+ | nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES
+ | nsIWBP.PERSIST_FLAGS_DONT_FIXUP_LINKS
+ | nsIWBP.PERSIST_FLAGS_DONT_CHANGE_FILENAMES;
+
+ // Set the document from the current view, and set a usable title
+ docToBeSaved = getContentDocument(e.sourceObject.frame);
+ var headElement = docToBeSaved.getElementsByTagName("HEAD")[0];
+ var titleElements = docToBeSaved.getElementsByTagName("title");
+ // Remove an existing title, there shouldn't be more than one.
+ if (titleElements.length > 0)
+ titleElements[0].parentNode.removeChild(titleElements[0]);
+ title = docToBeSaved.createElement("title");
+ title.appendChild(docToBeSaved.createTextNode(document.title +
+ " (" + new Date() + ")"));
+ headElement.appendChild(title);
+ // Set standard flags, file type, saveFolder and character limit
+ flags = nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES;
+ fileType = "text/html";
+ saveFolder = null;
+ charLimit = 0;
+
+ // Do saveType specific stuff
+ switch (saveType)
+ {
+ case saveTypes["complete"]:
+ // Get the directory into which to save associated files.
+ saveFolder = file.clone();
+ var baseName = saveFolder.leafName;
+ baseName = baseName.substring(0, baseName.lastIndexOf("."));
+ saveFolder.leafName = getMsg(MSG_SAVE_FILES_FOLDER, baseName);
+ break;
+ // html only does not need any additional configuration
+ case saveTypes["text"]:
+ // set flags for Plain Text
+ flags = nsIWBP.ENCODE_FLAGS_FORMATTED;
+ flags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS;
+ flags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT;
+ // set the file type and set character limit to 80
+ fileType = "text/plain";
+ charLimit = 80;
+ break;
+ }
+
+ try
+ {
+ wbp.saveDocument(docToBeSaved, file, saveFolder, fileType, flags,
+ charLimit);
+ }
+ catch (ex)
+ {
+ pm = [e.sourceObject.viewName, e.filename, ex.message];
+ display(getMsg(MSG_SAVE_ERR_FAILED, pm), MT_ERROR);
+ }
+ // Error handling and finishing message is done by the listener
+}
+
+function cmdSupports(e)
+{
+ var server = e.server;
+ var data = server.supports;
+
+ if ("channelTypes" in server)
+ display(getMsg(MSG_SUPPORTS_CHANTYPES,
+ server.channelTypes.join(", ")));
+ if ("channelModes" in server)
+ {
+ display(getMsg(MSG_SUPPORTS_CHANMODESA,
+ server.channelModes.a.join(", ")));
+ display(getMsg(MSG_SUPPORTS_CHANMODESB,
+ server.channelModes.b.join(", ")));
+ display(getMsg(MSG_SUPPORTS_CHANMODESC,
+ server.channelModes.c.join(", ")));
+ display(getMsg(MSG_SUPPORTS_CHANMODESD,
+ server.channelModes.d.join(", ")));
+ }
+
+ if ("userModes" in server)
+ {
+ var list = new Array();
+ for (var m in server.userModes)
+ {
+ list.push(getMsg(MSG_SUPPORTS_USERMODE, [
+ server.userModes[m].mode,
+ server.userModes[m].symbol
+ ]));
+ }
+ display(getMsg(MSG_SUPPORTS_USERMODES, list.join(", ")));
+ }
+
+ var listB1 = new Array();
+ var listB2 = new Array();
+ var listN = new Array();
+ for (var k in data)
+ {
+ if (typeof data[k] == "boolean")
+ {
+ if (data[k])
+ listB1.push(k);
+ else
+ listB2.push(k);
+ }
+ else
+ {
+ listN.push(getMsg(MSG_SUPPORTS_MISCOPTION, [ k, data[k] ] ));
+ }
+ }
+ listB1.sort();
+ listB2.sort();
+ listN.sort();
+ display(getMsg(MSG_SUPPORTS_FLAGSON, listB1.join(", ")));
+ display(getMsg(MSG_SUPPORTS_FLAGSOFF, listB2.join(", ")));
+ display(getMsg(MSG_SUPPORTS_MISCOPTIONS, listN.join(", ")));
+
+ var listCaps = new Array();
+ var listCapsEnabled = new Array();
+ for (var cap in server.caps)
+ {
+ listCaps.push(cap);
+ if (server.caps[cap])
+ listCapsEnabled.push(cap);
+ }
+ if (listCaps.length > 0)
+ {
+ listCaps.sort();
+ listCapsEnabled.sort();
+ display(getMsg(MSG_SUPPORTS_CAPS, listCaps.join(", ")));
+ display(getMsg(MSG_SUPPORTS_CAPSON, listCapsEnabled.join(", ")));
+ }
+}
+
+function cmdDoCommand(e)
+{
+ if (e.cmdName == "cmd_mozillaPrefs")
+ {
+ // Open SeaMonkey preferences.
+ goPreferences("navigator_pane");
+ }
+ else if (e.cmdName == "cmd_chatzillaPrefs")
+ {
+ var prefWin = getWindowByType("irc:chatzilla:config");
+ if (!prefWin)
+ {
+ window.openDialog('chrome://chatzilla/content/config.xul', '',
+ 'chrome,resizable,dialog=no', window);
+ }
+ else
+ {
+ prefWin.focus();
+ }
+ }
+ else if (e.cmdName == "cmd_selectAll")
+ {
+ var userList = document.getElementById("user-list");
+ var elemFocused = document.commandDispatcher.focusedElement;
+
+ if (userList.view && (elemFocused == userList))
+ userList.view.selection.selectAll();
+ else
+ doCommand("cmd_selectAll");
+ }
+ else
+ {
+ doCommand(e.cmdName);
+ }
+}
+
+function cmdTime(e)
+{
+ if (e.nickname)
+ e.network.dispatch("ctcp", { target: e.nickname, code: "TIME"});
+ else
+ e.server.sendData(fromUnicode("TIME") + "\n", e.sourceObject);
+}
+
+function cmdTimestamps(e)
+{
+ var view = e.sourceObject;
+
+ if (e.toggle != null)
+ {
+ e.toggle = getToggle(e.toggle, view.prefs["timestamps"])
+ view.prefs["timestamps"] = e.toggle;
+ }
+ else
+ {
+ display(getMsg(MSG_FMT_PREF, ["timestamps",
+ view.prefs["timestamps"]]));
+ }
+}
+
+function cmdSetCurrentView(e)
+{
+ if ("lockView" in e.view)
+ delete e.view.lockView;
+
+ setCurrentObject(e.view);
+}
+
+function cmdJumpToAnchor(e)
+{
+ if (e.hasOwnProperty("channelName"))
+ {
+ e.channel = new CIRCChannel(e.server, e.channelName);
+ }
+ else if (!e.channel)
+ {
+ display(getMsg(MSG_ERR_REQUIRED_PARAM, "channel-name"), MT_ERROR);
+ return;
+ }
+ if (!e.channel.frame)
+ {
+ display(getMsg(MSG_JUMPTO_ERR_NOCHAN, e.channel.unicodeName), MT_ERROR);
+ return;
+ }
+
+ var document = getContentDocument(e.channel.frame);
+ var row = document.getElementById(e.anchor);
+
+ if (!row)
+ {
+ display(getMsg(MSG_JUMPTO_ERR_NOANCHOR), MT_ERROR);
+ return;
+ }
+
+ dispatch("set-current-view", {view: e.channel});
+ e.channel.scrollToElement(row, "center");
+}
+
+function cmdIdentify(e)
+{
+ e.password = client.tryToGetLogin(e.server.parent.getURL(), "nick",
+ e.server.me.name, e.password, true,
+ MSG_NEED_IDENTIFY_PASSWORD);
+ if (!e.password)
+ return;
+
+ e.server.sendData("NS IDENTIFY " + fromUnicode(e.password, e.server) +
+ "\n");
+}
+
+function cmdIgnore(e)
+{
+ if (("mask" in e) && e.mask)
+ {
+ e.mask = e.server.toLowerCase(e.mask);
+
+ if (e.command.name == "ignore")
+ {
+ if (e.network.ignore(e.mask))
+ display(getMsg(MSG_IGNORE_ADD, e.mask));
+ else
+ display(getMsg(MSG_IGNORE_ADDERR, e.mask));
+ }
+ else
+ {
+ if (e.network.unignore(e.mask))
+ display(getMsg(MSG_IGNORE_DEL, e.mask));
+ else
+ display(getMsg(MSG_IGNORE_DELERR, e.mask));
+ }
+ // Update pref:
+ var ignoreList = keys(e.network.ignoreList);
+ e.network.prefs["ignoreList"] = ignoreList;
+ e.network.prefs["ignoreList"].update();
+ }
+ else
+ {
+ var list = new Array();
+ for (var m in e.network.ignoreList)
+ list.push(m);
+ if (list.length == 0)
+ display(MSG_IGNORE_LIST_1);
+ else
+ display(getMsg(MSG_IGNORE_LIST_2, arraySpeak(list)));
+ }
+}
+
+function cmdFont(e)
+{
+ var view = client;
+ var pref, val, pVal;
+
+ if (e.command.name == "font-family")
+ {
+ pref = "font.family";
+ val = e.font;
+
+ // Save new value, then display pref value.
+ if (val)
+ view.prefs[pref] = val;
+
+ display(getMsg(MSG_FONTS_FAMILY_FMT, view.prefs[pref]));
+ }
+ else if (e.command.name == "font-size")
+ {
+ pref = "font.size";
+ val = e.fontSize;
+
+ // Ok, we've got an input.
+ if (val)
+ {
+ // Get the current value, use user's default if needed.
+ pVal = view.prefs[pref];
+ if (pVal == 0)
+ pVal = getDefaultFontSize();
+
+ // Handle user's input...
+ switch(val) {
+ case "default":
+ val = 0;
+ break;
+
+ case "small":
+ val = getDefaultFontSize() - 2;
+ break;
+
+ case "medium":
+ val = getDefaultFontSize();
+ break;
+
+ case "large":
+ val = getDefaultFontSize() + 2;
+ break;
+
+ case "smaller":
+ val = pVal - 2;
+ break;
+
+ case "bigger":
+ val = pVal + 2;
+ break;
+
+ default:
+ if (isNaN(val))
+ val = 0;
+ else
+ val = Number(val);
+ }
+ // Save the new value.
+ view.prefs[pref] = val;
+ }
+
+ // Show the user what the pref is set to.
+ if (view.prefs[pref] == 0)
+ display(MSG_FONTS_SIZE_DEFAULT);
+ else
+ display(getMsg(MSG_FONTS_SIZE_FMT, view.prefs[pref]));
+ }
+ else if (e.command.name == "font-family-other")
+ {
+ val = prompt(MSG_FONTS_FAMILY_PICK, view.prefs["font.family"]);
+ if (!val)
+ return;
+
+ dispatch("font-family", { font: val });
+ }
+ else if (e.command.name == "font-size-other")
+ {
+ pVal = view.prefs["font.size"];
+ if (pVal == 0)
+ pVal = getDefaultFontSize();
+
+ val = prompt(MSG_FONTS_SIZE_PICK, pVal);
+ if (!val)
+ return;
+
+ dispatch("font-size", { fontSize: val });
+ }
+}
+
+function cmdDCCChat(e)
+{
+ if (!client.prefs["dcc.enabled"])
+ return display(MSG_DCC_NOT_ENABLED);
+
+ if (!e.nickname && !e.user)
+ return display(MSG_DCC_ERR_NOUSER);
+
+ var user;
+ if (e.nickname)
+ user = e.server.addUser(e.nickname);
+ else
+ user = e.server.addUser(e.user.unicodeName);
+
+ var u = client.dcc.addUser(user);
+ var c = client.dcc.addChat(u, client.dcc.getNextPort());
+ c.request();
+
+ client.munger.getRule(".inline-buttons").enabled = true;
+ var cmd = getMsg(MSG_DCC_COMMAND_CANCEL, "dcc-close " + c.id);
+ display(getMsg(MSG_DCCCHAT_SENT_REQUEST, c._getParams().concat(cmd)),
+ "DCC-CHAT");
+ client.munger.getRule(".inline-buttons").enabled = false;
+
+ return true;
+}
+
+function cmdDCCClose(e)
+{
+ if (!client.prefs["dcc.enabled"])
+ return display(MSG_DCC_NOT_ENABLED);
+
+ // If there is no nickname specified, use current view.
+ if (!e.nickname)
+ {
+ // Both DCC chat and file transfers can be aborted like this.
+ if (e.sourceObject.TYPE.substr(0, 6) == "IRCDCC")
+ {
+ if (e.sourceObject.isActive())
+ return e.sourceObject.abort();
+ return true;
+ }
+ // ...if there is one.
+ return display(MSG_DCC_ERR_NOTDCC);
+ }
+
+ var o = client.dcc.findByID(e.nickname);
+ if (o)
+ // Direct ID --> object request.
+ return o.abort();
+
+ if (e.type)
+ e.type = [e.type.toLowerCase()];
+ else
+ e.type = ["chat", "file"];
+
+ // Go ask the DCC code for some matching requets.
+ var list = client.dcc.getMatches
+ (e.nickname, e.file, e.type, [DCC_DIR_GETTING, DCC_DIR_SENDING],
+ [DCC_STATE_REQUESTED, DCC_STATE_ACCEPTED, DCC_STATE_CONNECTED]);
+
+ // Disconnect if only one match.
+ if (list.length == 1)
+ return list[0].abort();
+
+ // Oops, couldn't figure the user's requets out, so give them some help.
+ display(getMsg(MSG_DCC_ACCEPTED_MATCHES, [list.length]));
+ display(MSG_DCC_MATCHES_HELP);
+ return true;
+}
+
+function cmdDCCSend(e)
+{
+ if (!client.prefs["dcc.enabled"])
+ return display(MSG_DCC_NOT_ENABLED);
+
+ const DIRSVC_CID = "@mozilla.org/file/directory_service;1";
+ const nsIProperties = Components.interfaces.nsIProperties;
+
+ if (!e.nickname && !e.user)
+ return display(MSG_DCC_ERR_NOUSER);
+
+ // Accept the request passed in...
+ var file;
+ if (!e.file)
+ {
+ var pickerRv = pickOpen(MSG_DCCFILE_SEND);
+ if (!pickerRv.ok)
+ return false;
+ file = pickerRv.file;
+ }
+ else
+ {
+ // Wrap in try/catch because nsIFile creation throws a freaking
+ // error if it doesn't get a FULL path.
+ try
+ {
+ file = nsLocalFile(e.file);
+ }
+ catch(ex)
+ {
+ // Ok, try user's home directory.
+ var fl = Components.classes[DIRSVC_CID].getService(nsIProperties);
+ file = fl.get("Home", Components.interfaces.nsIFile);
+
+ // Another freaking try/catch wrapper.
+ try
+ {
+ // NOTE: This is so pathetic it can't cope with any path
+ // separators in it, so don't even THINK about lobing a
+ // relative path at it.
+ file.append(e.file);
+
+ // Wow. We survived.
+ }
+ catch (ex)
+ {
+ return display(MSG_DCCFILE_ERR_NOTFOUND);
+ }
+ }
+ }
+ if (!file.exists())
+ return display(MSG_DCCFILE_ERR_NOTFOUND);
+ if (!file.isFile())
+ return display(MSG_DCCFILE_ERR_NOTAFILE);
+ if (!file.isReadable())
+ return display(MSG_DCCFILE_ERR_NOTREADABLE);
+
+ var user;
+ if (e.nickname)
+ user = e.server.addUser(e.nickname);
+ else
+ user = e.server.addUser(e.user.unicodeName);
+
+ var u = client.dcc.addUser(user);
+ var c = client.dcc.addFileTransfer(u, client.dcc.getNextPort());
+ c.request(file);
+
+ client.munger.getRule(".inline-buttons").enabled = true;
+ var cmd = getMsg(MSG_DCC_COMMAND_CANCEL, "dcc-close " + c.id);
+ display(getMsg(MSG_DCCFILE_SENT_REQUEST, [c.user.unicodeName, c.localIP,
+ c.port, c.filename,
+ getSISize(c.size), cmd]),
+ "DCC-FILE");
+ client.munger.getRule(".inline-buttons").enabled = false;
+
+ return true;
+}
+
+function cmdDCCList(e) {
+ if (!client.prefs["dcc.enabled"])
+ return display(MSG_DCC_NOT_ENABLED);
+
+ var counts = { pending: 0, connected: 0, failed: 0 };
+ var k;
+
+ // Get all the DCC sessions.
+ var list = client.dcc.getMatches();
+
+ for (k = 0; k < list.length; k++) {
+ var c = list[k];
+ var type = c.TYPE.substr(6, c.TYPE.length - 6);
+
+ var dir = MSG_UNKNOWN;
+ var tf = MSG_UNKNOWN;
+ if (c.state.dir == DCC_DIR_SENDING)
+ {
+ dir = MSG_DCCLIST_DIR_OUT;
+ tf = MSG_DCCLIST_TO;
+ }
+ else if (c.state.dir == DCC_DIR_GETTING)
+ {
+ dir = MSG_DCCLIST_DIR_IN;
+ tf = MSG_DCCLIST_FROM;
+ }
+
+ var state = MSG_UNKNOWN;
+ var cmds = "";
+ switch (c.state.state)
+ {
+ case DCC_STATE_REQUESTED:
+ state = MSG_DCC_STATE_REQUEST;
+ if (c.state.dir == DCC_DIR_GETTING)
+ {
+ cmds = getMsg(MSG_DCC_COMMAND_ACCEPT, "dcc-accept " + c.id) + " " +
+ getMsg(MSG_DCC_COMMAND_DECLINE, "dcc-decline " + c.id);
+ }
+ else
+ {
+ cmds = getMsg(MSG_DCC_COMMAND_CANCEL, "dcc-close " + c.id);
+ }
+ counts.pending++;
+ break;
+ case DCC_STATE_ACCEPTED:
+ state = MSG_DCC_STATE_ACCEPT;
+ counts.connected++;
+ break;
+ case DCC_STATE_DECLINED:
+ state = MSG_DCC_STATE_DECLINE;
+ break;
+ case DCC_STATE_CONNECTED:
+ state = MSG_DCC_STATE_CONNECT;
+ cmds = getMsg(MSG_DCC_COMMAND_CLOSE, "dcc-close " + c.id);
+ if (c.TYPE == "IRCDCCFileTransfer")
+ {
+ state = getMsg(MSG_DCC_STATE_CONNECTPRO,
+ [c.progress,
+ getSISize(c.position), getSISize(c.size),
+ getSISpeed(c.speed)]);
+ }
+ counts.connected++;
+ break;
+ case DCC_STATE_DONE:
+ state = MSG_DCC_STATE_DISCONNECT;
+ break;
+ case DCC_STATE_ABORTED:
+ state = MSG_DCC_STATE_ABORT;
+ counts.failed++;
+ break;
+ case DCC_STATE_FAILED:
+ state = MSG_DCC_STATE_FAIL;
+ counts.failed++;
+ break;
+ }
+ client.munger.getRule(".inline-buttons").enabled = true;
+ display(getMsg(MSG_DCCLIST_LINE, [k + 1, state, dir, type, tf,
+ c.unicodeName, c.remoteIP, c.port,
+ cmds]));
+ client.munger.getRule(".inline-buttons").enabled = false;
+ }
+ display(getMsg(MSG_DCCLIST_SUMMARY, [counts.pending, counts.connected,
+ counts.failed]));
+ return true;
+}
+
+function cmdDCCAutoAcceptList(e)
+{
+ if (!client.prefs["dcc.enabled"])
+ return display(MSG_DCC_NOT_ENABLED);
+
+ var list = e.network.prefs["dcc.autoAccept.list"];
+
+ if (list.length == 0)
+ display(MSG_DCCACCEPT_DISABLED);
+ else
+ display(getMsg(MSG_DCCACCEPT_LIST, arraySpeak(list)));
+
+ return true;
+}
+
+function cmdDCCAutoAcceptAdd(e)
+{
+ if (!client.prefs["dcc.enabled"])
+ return display(MSG_DCC_NOT_ENABLED);
+
+ var list = e.network.prefs["dcc.autoAccept.list"];
+
+ if (!e.user && e.server)
+ e.user = e.server.getUser(e.nickname);
+
+ var mask = e.user ? "*!" + e.user.name + "@" + e.user.host : e.nickname;
+ var index = arrayIndexOf(list, mask);
+ if (index == -1)
+ {
+ list.push(mask);
+ list.update();
+ display(getMsg(MSG_DCCACCEPT_ADD, mask));
+ }
+ else
+ {
+ display(getMsg(MSG_DCCACCEPT_ADDERR,
+ e.user ? e.user.unicodeName : e.nickname));
+ }
+ return true;
+}
+
+function cmdDCCAutoAcceptDel(e)
+{
+ if (!client.prefs["dcc.enabled"])
+ return display(MSG_DCC_NOT_ENABLED);
+
+ var list = e.network.prefs["dcc.autoAccept.list"];
+
+ if (!e.user && e.server)
+ e.user = e.server.getUser(e.nickname);
+
+ var maskObj, newList = new Array();
+ for (var m = 0; m < list.length; ++m)
+ {
+ maskObj = getHostmaskParts(list[m]);
+ if (e.nickname == list[m] ||
+ (e.user && hostmaskMatches(e.user, maskObj, e.server)))
+ {
+ display(getMsg(MSG_DCCACCEPT_DEL, list[m]));
+ }
+ else
+ {
+ newList.push(list[m]);
+ }
+ }
+
+ if (list.length > newList.length)
+ e.network.prefs["dcc.autoAccept.list"] = newList;
+ else
+ display(getMsg(MSG_DCCACCEPT_DELERR,
+ e.user ? e.user.unicodeName : e.nickname));
+
+ return true;
+}
+
+function cmdDCCAccept(e)
+{
+ if (!client.prefs["dcc.enabled"])
+ return display(MSG_DCC_NOT_ENABLED);
+
+ function accept(c)
+ {
+ if (c.TYPE == "IRCDCCChat")
+ {
+ if (!c.accept())
+ return false;
+
+ display(getMsg(MSG_DCCCHAT_ACCEPTED, c._getParams()), "DCC-CHAT");
+ return true;
+ }
+
+ // Accept the request passed in...
+ var filename = c.filename;
+ var ext = "*";
+ var m = filename.match(/...\.([a-z]+)$/i);
+ if (m)
+ ext = "*." + m[1];
+
+ var pickerRv = pickSaveAs(getMsg(MSG_DCCFILE_SAVE_TO, filename),
+ ["$all", ext], filename);
+ if (!pickerRv.ok)
+ return false;
+
+ if (!c.accept(pickerRv.file))
+ return false;
+
+ display(getMsg(MSG_DCCFILE_ACCEPTED, c._getParams()), "DCC-FILE");
+ return true;
+ };
+
+ // If there is no nickname specified, use the "last" item.
+ // This is the last DCC request that arrvied.
+ if (!e.nickname && client.dcc.last)
+ {
+ if ((new Date() - client.dcc.lastTime) >= 10000)
+ return accept(client.dcc.last);
+ return display(MSG_DCC_ERR_ACCEPT_TIME);
+ }
+
+ var o = client.dcc.findByID(e.nickname);
+ if (o)
+ // Direct ID --> object request.
+ return accept(o);
+
+ if (e.type)
+ e.type = [e.type.toLowerCase()];
+ else
+ e.type = ["chat", "file"];
+
+ // Go ask the DCC code for some matching requets.
+ var list = client.dcc.getMatches(e.nickname, e.file, e.type,
+ [DCC_DIR_GETTING], [DCC_STATE_REQUESTED]);
+ // Accept if only one match.
+ if (list.length == 1)
+ return accept(list[0]);
+
+ // Oops, couldn't figure the user's request out, so give them some help.
+ display(getMsg(MSG_DCC_PENDING_MATCHES, [list.length]));
+ display(MSG_DCC_MATCHES_HELP);
+ return true;
+}
+
+function cmdDCCDecline(e)
+{
+ if (!client.prefs["dcc.enabled"])
+ return display(MSG_DCC_NOT_ENABLED);
+
+ function decline(c)
+ {
+ // Decline the request passed in...
+ c.decline();
+ if (c.TYPE == "IRCDCCChat")
+ display(getMsg(MSG_DCCCHAT_DECLINED, c._getParams()), "DCC-CHAT");
+ else
+ display(getMsg(MSG_DCCFILE_DECLINED, c._getParams()), "DCC-FILE");
+ };
+
+ // If there is no nickname specified, use the "last" item.
+ // This is the last DCC request that arrvied.
+ if (!e.nickname && client.dcc.last)
+ return decline(client.dcc.last);
+
+ var o = client.dcc.findByID(e.nickname);
+ if (o)
+ // Direct ID --> object request.
+ return decline(o);
+
+ if (!e.type)
+ e.type = ["chat", "file"];
+
+ // Go ask the DCC code for some matching requets.
+ var list = client.dcc.getMatches(e.nickname, e.file, e.type,
+ [DCC_DIR_GETTING], [DCC_STATE_REQUESTED]);
+ // Decline if only one match.
+ if (list.length == 1)
+ return decline(list[0]);
+
+ // Oops, couldn't figure the user's requets out, so give them some help.
+ display(getMsg(MSG_DCC_PENDING_MATCHES, [list.length]));
+ display(MSG_DCC_MATCHES_HELP);
+ return true;
+}
+
+function cmdDCCShowFile(e)
+{
+ var f = getFileFromURLSpec(e.file);
+ if (f)
+ f = nsLocalFile(f.path);
+ if (f && f.parent && f.parent.exists())
+ {
+ try
+ {
+ f.reveal();
+ }
+ catch (ex)
+ {
+ dd(formatException(ex));
+ }
+ }
+}
+
+function cmdTextDirection(e)
+{
+ var direction;
+ var sourceObject = getContentDocument(e.sourceObject.frame).body;
+
+ switch (e.dir)
+ {
+ case "toggle":
+ if (sourceObject.getAttribute("dir") == "rtl")
+ direction = 'ltr';
+ else
+ direction = 'rtl';
+ break;
+ case "rtl":
+ direction = 'rtl';
+ break;
+ default:
+ // that is "case "ltr":",
+ // but even if !e.dir OR e.dir is an invalid value -> set to
+ // default direction
+ direction = 'ltr';
+ }
+ client.input.setAttribute("dir", direction);
+ sourceObject.setAttribute("dir", direction);
+
+ return true;
+}
+
+function cmdInputTextDirection(e)
+{
+ var direction;
+
+ switch (e.dir)
+ {
+ case "rtl":
+ client.input.setAttribute("dir", "rtl");
+ break
+ default:
+ // that is "case "ltr":", but even if !e.dir OR e.dir is an
+ //invalid value -> set to default direction
+ client.input.setAttribute("dir", "ltr");
+ }
+
+ return true;
+}
+
+function cmdInstallPlugin(e)
+{
+ var ipURL = "chrome://chatzilla/content/install-plugin/install-plugin.xul";
+ var ctx = {};
+ var pluginDownloader =
+ {
+ onStartRequest: function _onStartRequest(request, context)
+ {
+ var tempName = "plugin-install.temp";
+ if (urlMatches)
+ tempName += urlMatches[2];
+
+ ctx.outFile = getTempFile(client.prefs["profilePath"], tempName);
+ ctx.outFileH = fopen(ctx.outFile, ">");
+ },
+ onDataAvailable: function _onDataAvailable(request, context, stream,
+ offset, count)
+ {
+ if (!ctx.inputStream)
+ ctx.inputStream = toSInputStream(stream, true);
+
+ ctx.outFileH.write(ctx.inputStream.readBytes(count));
+ },
+ onStopRequest: function _onStopRequest(request, context, statusCode)
+ {
+ ctx.outFileH.close();
+
+ if (statusCode == 0)
+ {
+ client.installPlugin(e.name, ctx.outFile);
+ }
+ else
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_ERR_DOWNLOAD, statusCode),
+ MT_ERROR);
+ }
+
+ try
+ {
+ ctx.outFile.remove(false);
+ }
+ catch (ex)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_ERR_REMOVE_TEMP, ex),
+ MT_ERROR);
+ }
+ }
+ };
+
+ if (!e.url)
+ {
+ if ("installPluginDialog" in client)
+ return client.installPluginDialog.focus();
+
+ window.openDialog(ipURL, "", "chrome,dialog", client);
+ return;
+ }
+
+ var urlMatches = e.url.match(/([^\/]+?)((\..{0,3}){0,2})$/);
+ if (!e.name)
+ {
+ if (urlMatches)
+ {
+ e.name = urlMatches[1];
+ }
+ else
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_NO_NAME, MT_ERROR);
+ return;
+ }
+ }
+
+ // Do real install here.
+ switch (e.url.match(/^[^:]+/)[0])
+ {
+ case "file":
+ client.installPlugin(e.name, e.url);
+ break;
+
+ case "http":
+ case "https":
+ try
+ {
+ var channel = Services.io.newChannel(
+ e.url, "UTF-8", null, null,
+ Services.scriptSecurityManager.getSystemPrincipal(), null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+ display(getMsg(MSG_INSTALL_PLUGIN_DOWNLOADING, e.url),
+ MT_INFO);
+ channel.asyncOpen(pluginDownloader, { e: e });
+ }
+ catch (ex)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_ERR_DOWNLOAD, ex), MT_ERROR);
+ return;
+ }
+ break;
+
+ default:
+ display(MSG_INSTALL_PLUGIN_ERR_PROTOCOL, MT_ERROR);
+ }
+}
+
+function cmdUninstallPlugin(e)
+{
+ if (e.plugin)
+ {
+ client.uninstallPlugin(e.plugin);
+ }
+}
+
+function cmdFind(e)
+{
+ if (!e.rest)
+ {
+ findInPage(getFindData(e));
+ return;
+ }
+
+ // Used from the inputbox, set the search string and find the first
+ // occurrence using find-again.
+ const FINDSVC_ID = "@mozilla.org/find/find_service;1";
+ var findService = getService(FINDSVC_ID, "nsIFindService");
+ // Make sure it searches the entire document, but don't lose the old setting
+ var oldWrap = findService.wrapFind;
+ findService.wrapFind = true;
+ findService.searchString = e.rest;
+ findAgainInPage(getFindData(e));
+ // Restore wrap setting:
+ findService.wrapFind = oldWrap;
+}
+
+function cmdFindAgain(e)
+{
+ if (canFindAgainInPage())
+ findAgainInPage(getFindData(e));
+}
+
+function cmdURLs(e)
+{
+ var urls = client.urlLogger.read().reverse();
+
+ if (urls.length == 0)
+ {
+ display(MSG_URLS_NONE);
+ }
+ else
+ {
+ /* Temporarily remove the URL logger to avoid changing the list when
+ * displaying it.
+ */
+ var logger = client.urlLogger;
+ delete client.urlLogger;
+
+ var num = e.number || client.prefs["urls.display"];
+ if (num > urls.length)
+ num = urls.length;
+ display(getMsg(MSG_URLS_HEADER, num));
+
+ for (var i = 0; i < num; i++)
+ display(getMsg(MSG_URLS_ITEM, [i + 1, urls[i]]));
+
+ client.urlLogger = logger;
+ }
+}
+
+function cmdWebSearch(e)
+{
+ let submission = Services.search.currentEngine
+ .getSubmission(e.selectedText);
+ let newTabPref = Services.prefs.getBoolPref("browser.search.opentabforcontextsearch");
+ dispatch(newTabPref ? "goto-url-newtab" : "goto-url-newwin",
+ {url: submission.uri.asciiSpec,
+ shiftKey: e.shiftKey});
+}
diff --git a/comm/suite/chatzilla/xul/content/config-add.js b/comm/suite/chatzilla/xul/content/config-add.js
new file mode 100644
index 0000000000..81034e2071
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/config-add.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var rv, rad, box1, box2;
+
+function changeType()
+{
+ box2.disabled = (rad.value == "net");
+}
+
+function onOK()
+{
+ rv.ok = true;
+
+ rv.type = rad.value;
+ rv.net = box1.value;
+ rv.chan = box2.value;
+
+ return true;
+}
+
+function onCancel()
+{
+ rv.ok = false;
+
+ return true;
+}
+
+function onLoad()
+{
+ rad = document.getElementById("prefType");
+ box1 = document.getElementById("prefName1");
+ box2 = document.getElementById("prefName2");
+
+ rv = window.arguments[0];
+
+ if (!("type" in rv))
+ rv.type = "";
+ if (!("net" in rv))
+ rv.net = "";
+ if (!("chan" in rv))
+ rv.chan = "";
+ rv.ok = false;
+
+ if (rv.type == "net")
+ rad.selectedIndex = 0;
+ if (rv.type == "chan")
+ rad.selectedIndex = 1;
+ if (rv.type == "user")
+ rad.selectedIndex = 2;
+
+ box1.value = rv.net || "";
+ box2.value = rv.chan || "";
+}
diff --git a/comm/suite/chatzilla/xul/content/config-add.xul b/comm/suite/chatzilla/xul/content/config-add.xul
new file mode 100644
index 0000000000..84834ca5a7
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/config-add.xul
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/config.dtd">
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="config.css" type="text/css"?>
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="irc:chatzilla:config:add"
+ buttons="accept,cancel"
+ ondialogaccept="onOK();"
+ ondialogcancel="onCancel();"
+ onload="onLoad();"
+ title="&config.add.title;">
+
+ <script src="config-add.js"/>
+
+ <vbox>
+ <hbox align="center">
+ <label value="&config.type.label;" accesskey="&config.type.accesskey;"
+ tooltiptext="&config.type.hint;" control="prefType"/>
+ <!-- Beware the hacks, number 264: add |value| attribute to make .value
+ work when the dialog loads (|selected| selects an item, but doesn't
+ set .value). -->
+ <radiogroup orient="horizontal" id="prefType" value="chan"
+ onselect="changeType();">
+ <radio value="net" label="&network;"/>
+ <radio value="chan" label="&channel;" selected="true"/>
+ <radio value="user" label="&user;"/>
+ </radiogroup>
+ </hbox>
+ <separator class="groove"/>
+ <grid>
+ <columns><column/><column flex="1"/></columns>
+ <rows>
+ <row align="center">
+ <label value="&config.network.label;" control="prefName1"
+ tooltiptext="&config.network.hint;"
+ accesskey="&config.network.accesskey;"/>
+ <textbox id="prefName1"/>
+ </row>
+ <row align="center">
+ <label value="&config.target.label;" control="prefName2"
+ tooltiptext="&config.target.hint;"
+ accesskey="&config.target.accesskey;"/>
+ <textbox id="prefName2"/>
+ </row>
+ </rows>
+ </grid>
+ </vbox>
+</dialog>
diff --git a/comm/suite/chatzilla/xul/content/config.css b/comm/suite/chatzilla/xul/content/config.css
new file mode 100644
index 0000000000..dfbd77fa5b
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/config.css
@@ -0,0 +1,33 @@
+/* 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/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+/* OSX uses lotsa padding on tabs. Need to allow window to expand. */
+
+/* Instead of setting on dialog, we're going to set it on the two main
+ * components of the window, so that large changes in the size of bits of the
+ * dialog don't upset things.
+ */
+
+/* Set min-width on the left-side tree. */
+#pref-objects {
+ min-width: 25ex;
+}
+
+/* Set min-width and min-height on tabs container. */
+#pref-object-deck {
+ min-width: 65ex;
+ min-height: 32em;
+ width: 65ex;
+ height: 32em;
+}
+
+scroller {
+ overflow: auto;
+}
+
+listbox {
+ min-height: 7em;
+}
diff --git a/comm/suite/chatzilla/xul/content/config.js b/comm/suite/chatzilla/xul/content/config.js
new file mode 100644
index 0000000000..12eefa9da3
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/config.js
@@ -0,0 +1,1775 @@
+/* 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/. */
+
+const MEDIATOR_CONTRACTID = "@mozilla.org/appshell/window-mediator;1";
+
+const nsIWindowMediator = Components.interfaces.nsIWindowMediator;
+
+const CONFIG_WINDOWTYPE = "irc:chatzilla:config";
+
+/* Now we create and set up some required items from other Chatzilla JS files
+ * that we really have no reason to load, but the ones we do need won't work
+ * without these...
+ */
+var ASSERT = function(cond, msg) { if (!cond) { alert(msg); } return cond; }
+var client;
+
+function CIRCNetwork() {}
+function CIRCServer() {}
+function CIRCChannel() {}
+function CIRCChanUser() {}
+function CIRCUser() {}
+function CIRCDCC() {}
+function CIRCDCCUser() {}
+function CIRCDCCChat() {}
+function CIRCDCCFileTransfer() {}
+function CIRCSTS() {}
+
+function getObjectDetails(obj)
+{
+ var rv = new Object();
+ rv.sourceObject = obj;
+ rv.TYPE = obj.TYPE;
+ rv.parent = ("parent" in obj) ? obj.parent : null;
+ rv.user = null;
+ rv.channel = null;
+ rv.server = null;
+ rv.network = null;
+
+ switch (obj.TYPE)
+ {
+ case "PrefNetwork":
+ rv.network = obj;
+ if ("primServ" in rv.network)
+ rv.server = rv.network.primServ;
+ else
+ rv.server = null;
+ break;
+
+ case "PrefChannel":
+ rv.channel = obj;
+ rv.server = rv.channel.parent;
+ rv.network = rv.server.parent;
+ break;
+
+ case "PrefUser":
+ rv.user = obj;
+ rv.server = rv.user.parent;
+ rv.network = rv.server.parent;
+ break;
+ }
+
+ return rv;
+}
+
+/* Global object for the prefs. The 'root' of all the objects to do with the
+ * prefs.
+ */
+function PrefGlobal()
+{
+ this.networks = new Object();
+ this.commandManager = new Object();
+ this.commandManager.defineCommand = function() {};
+ this.commandManager.removeCommand = function() {};
+ this.entities = new Object();
+ this.hostCompat = new Object();
+}
+PrefGlobal.prototype.TYPE = "PrefGlobal";
+
+/* Represents a single network in the hierarchy.
+ *
+ * |force| - If true, sets a pref on this object. This makes sure the object
+ * is "known" next time we load up (since we look for any prefs).
+ *
+ * |show| - If true, the object still exists even if the magic pref is not set.
+ * Thus, allows an object to exist without any prefs set.
+ */
+function PrefNetwork(parent, name, force, show)
+{
+ if (":" + name in parent.networks)
+ return parent.networks[":" + name];
+
+ this.parent = parent;
+ this.unicodeName = name;
+ this.viewName = name;
+ this.canonicalName = name;
+ this.collectionKey = ":" + name;
+ this.encodedName = name;
+ this.prettyName = getMsg(MSG_PREFS_FMT_DISPLAY_NETWORK, this.unicodeName);
+ this.servers = new Object();
+ this.primServ = new PrefServer(this, "dummy server");
+ this.channels = this.primServ.channels;
+ this.users = this.primServ.users;
+ this.prefManager = getNetworkPrefManager(this);
+ this.prefs = this.prefManager.prefs;
+ this.prefManager.onPrefChanged = function(){};
+
+ if (force)
+ this.prefs["hasPrefs"] = true;
+
+ if (this.prefs["hasPrefs"] || show)
+ this.parent.networks[this.collectionKey] = this;
+
+ return this;
+};
+PrefNetwork.prototype.TYPE = "PrefNetwork";
+
+/* Cleans up the mess. */
+PrefNetwork.prototype.clear =
+function pnet_clear()
+{
+ this.prefs["hasPrefs"] = false;
+ delete this.parent.networks[this.collectionKey];
+}
+
+/* A middle-management object.
+ *
+ * Exists only to satisfy the IRC library pref functions that expect this
+ * particular hierarchy.
+ */
+function PrefServer(parent, name)
+{
+ this.parent = parent;
+ this.unicodeName = name;
+ this.viewName = name;
+ this.canonicalName = name;
+ this.collectionKey = ":" + name;
+ this.encodedName = name;
+ this.prettyName = this.unicodeName; // Not used, thus not localised.
+ this.channels = new Object();
+ this.users = new Object();
+ this.parent.servers[this.collectionKey] = this;
+ return this;
+};
+PrefServer.prototype.TYPE = "PrefServer";
+
+/* Represents a single channel in the hierarchy.
+ *
+ * |force| and |show| the same as PrefNetwork.
+ */
+function PrefChannel(parent, name, force, show)
+{
+ if (":" + name in parent.channels)
+ return parent.channels[":" + name];
+
+ this.parent = parent;
+ this.unicodeName = name;
+ this.viewName = name;
+ this.canonicalName = name;
+ this.collectionKey = ":" + name;
+ this.encodedName = name;
+ this.prettyName = getMsg(MSG_PREFS_FMT_DISPLAY_CHANNEL,
+ [this.parent.parent.unicodeName, this.unicodeName]);
+ this.prefManager = getChannelPrefManager(this);
+ this.prefs = this.prefManager.prefs;
+ this.prefManager.onPrefChanged = function(){};
+
+ if (force)
+ this.prefs["hasPrefs"] = true;
+
+ if (this.prefs["hasPrefs"] || show)
+ this.parent.channels[this.collectionKey] = this;
+
+ return this;
+};
+PrefChannel.prototype.TYPE = "PrefChannel";
+
+/* Cleans up the mess. */
+PrefChannel.prototype.clear =
+function pchan_clear()
+{
+ this.prefs["hasPrefs"] = false;
+ delete this.parent.channels[this.collectionKey];
+}
+
+/* Represents a single user in the hierarchy.
+ *
+ * |force| and |show| the same as PrefNetwork.
+ */
+function PrefUser(parent, name, force, show)
+{
+ if (":" + name in parent.users)
+ return parent.users[":" + name];
+
+ this.parent = parent;
+ this.unicodeName = name;
+ this.viewName = name;
+ this.canonicalName = name;
+ this.collectionKey = ":" + name;
+ this.encodedName = name;
+ this.prettyName = getMsg(MSG_PREFS_FMT_DISPLAY_USER,
+ [this.parent.parent.unicodeName, this.unicodeName]);
+ this.prefManager = getUserPrefManager(this);
+ this.prefs = this.prefManager.prefs;
+ this.prefManager.onPrefChanged = function(){};
+
+ if (force)
+ this.prefs["hasPrefs"] = true;
+
+ if (this.prefs["hasPrefs"] || show)
+ this.parent.users[this.collectionKey] = this;
+
+ return this;
+};
+PrefUser.prototype.TYPE = "PrefUser";
+
+/* Cleans up the mess. */
+PrefUser.prototype.clear =
+function puser_clear()
+{
+ this.prefs["hasPrefs"] = false;
+ delete this.parent.users[this.collectionKey];
+}
+
+// Stores a list of |PrefObject|s.
+function PrefObjectList()
+{
+ this.objects = new Array();
+
+ return this;
+}
+
+// Add an object, and init it's private data.
+PrefObjectList.prototype.addObject =
+function polist_addObject(pObject)
+{
+ this.objects.push(pObject);
+ return pObject.privateData = new ObjectPrivateData(pObject, this.objects.length - 1);
+}
+
+/* Removes an object, without changing the index. */
+PrefObjectList.prototype.deleteObject =
+function polist_addObject(index)
+{
+ this.objects[index].privateData.clear();
+ this.objects[index].clear();
+ this.objects[index] = { privateData: null };
+}
+
+// Get a specific object.
+PrefObjectList.prototype.getObject =
+function polist_getObject(index)
+{
+ return this.objects[index].privateData;
+}
+
+// Gets the private data for an object.
+PrefObjectList.getPrivateData =
+function polist_getPrivateData(object)
+{
+ return object.privateData;
+}
+
+// Stores the pref object's private data.
+function ObjectPrivateData(parent, index)
+{
+ this.parent = parent; // Real pref object.
+ this.prefs = new Object();
+ this.groups = new Object();
+
+ this.arrayIndex = index;
+ this.deckIndex = -1;
+ this.dataLoaded = false;
+
+ var treeObj = document.getElementById("pref-tree-object");
+ this.tree = document.getElementById("pref-tree");
+ this.treeContainer = document.createElement("treeitem");
+ this.treeNode = document.createElement("treerow");
+ this.treeCell = document.createElement("treecell");
+
+ this.treeContainer.setAttribute("prefobjectindex", this.arrayIndex);
+ this.treeCell.setAttribute("label", this.parent.unicodeName);
+
+ switch (this.parent.TYPE)
+ {
+ case "PrefChannel":
+ case "PrefUser":
+ var p = this.parent.parent.parent; // Network object.
+ var pData = PrefObjectList.getPrivateData(p);
+
+ if (!("treeChildren" in pData) || !pData.treeChildren)
+ {
+ pData.treeChildren = document.createElement("treechildren");
+ pData.treeContainer.appendChild(pData.treeChildren);
+ treeObj.view.toggleOpenState(treeObj.view.rowCount - 1);
+ }
+ pData.treeContainer.setAttribute("container", "true");
+ pData.treeChildren.appendChild(this.treeContainer);
+ break;
+
+ default:
+ this.tree.appendChild(this.treeContainer);
+ break;
+ }
+
+ this.treeContainer.appendChild(this.treeNode);
+ this.treeNode.appendChild(this.treeCell);
+
+ return this;
+}
+
+// Creates all the XUL elements needed to show this pref object.
+ObjectPrivateData.prototype.loadXUL =
+function opdata_loadXUL(tabOrder)
+{
+ var t = this;
+
+ /* Function that sorts the preferences by their label, else they look
+ * fairly random in order.
+ *
+ * Sort keys: not grouped, sub-group name, boolean, pref label.
+ */
+ function sortByLabel(a, b) {
+ if (t.prefs[a].subGroup || t.prefs[b].subGroup)
+ {
+ // Non-grouped go first.
+ if (!t.prefs[a].subGroup)
+ return -1;
+ if (!t.prefs[b].subGroup)
+ return 1;
+
+ // Sub-group names.
+ if (t.prefs[a].subGroup < t.prefs[b].subGroup)
+ return -1;
+ if (t.prefs[a].subGroup > t.prefs[b].subGroup)
+ return 1;
+ }
+
+ // Booleans go first.
+ if ((t.prefs[a].type == "boolean") && (t.prefs[b].type != "boolean"))
+ return -1;
+ if ((t.prefs[a].type != "boolean") && (t.prefs[b].type == "boolean"))
+ return 1;
+
+ // ...then label.
+ if (t.prefs[a].label < t.prefs[b].label)
+ return -1;
+ if (t.prefs[a].label > t.prefs[b].label)
+ return 1;
+ return 0;
+ };
+
+ if (this.deckIndex >= 0)
+ return;
+
+ this.deck = document.getElementById("pref-object-deck");
+ this.tabbox = document.createElement("tabbox");
+ this.tabs = document.createElement("tabs");
+ this.tabPanels = document.createElement("tabpanels");
+
+ this.tabbox.setAttribute("flex", 1);
+ this.tabPanels.setAttribute("flex", 1);
+
+ this.tabbox.appendChild(this.tabs);
+ this.tabbox.appendChild(this.tabPanels);
+ this.deck.appendChild(this.tabbox);
+
+ this.deckIndex = this.deck.childNodes.length - 1;
+
+ this.loadData();
+
+ var prefList = keys(this.prefs);
+ prefList.sort(sortByLabel);
+
+ for (var i = 0; i < tabOrder.length; i++)
+ {
+ var pto = tabOrder[i];
+ var needTab = pto.fixed;
+ if (!needTab)
+ {
+ // Not a "always visible" tab, check we need it.
+ for (var j = 0; j < prefList.length; j++)
+ {
+ if (this.prefs[prefList[j]].mainGroup == pto.name)
+ {
+ needTab = true;
+ break;
+ }
+ }
+ }
+ if (needTab)
+ this.addGroup(pto.name);
+ }
+
+ for (i = 0; i < prefList.length; i++)
+ this.prefs[prefList[i]].loadXUL();
+
+ if (this.tabs.childNodes.length > 0)
+ this.tabbox.selectedIndex = 0;
+}
+
+// Loads all the prefs.
+ObjectPrivateData.prototype.loadData =
+function opdata_loadData()
+{
+ if (this.dataLoaded)
+ return;
+
+ this.dataLoaded = true;
+
+ // Now get the list of pref names, and add them...
+ var prefList = this.parent.prefManager.prefNames;
+
+ for (var i in prefList)
+ this.addPref(prefList[i]);
+}
+
+// Clears up all the XUL objects and data.
+ObjectPrivateData.prototype.clear =
+function opdata_clear()
+{
+ //dd("Removing prefs for " + this.parent.displayName + " {");
+ if (!this.dataLoaded)
+ this.loadData();
+ for (var i in this.prefs)
+ this.prefs[i].clear();
+ //dd("}");
+
+ if (this.deckIndex >= 0)
+ {
+ this.deck.removeChild(this.tabbox);
+ this.treeContainer.removeAttribute("container");
+ this.treeContainer.parentNode.removeChild(this.treeContainer);
+ }
+}
+
+// Resets all the prefs to their original values.
+ObjectPrivateData.prototype.reset =
+function opdata_reset()
+{
+ for (var i in this.prefs)
+ if (this.prefs[i].type != "hidden")
+ this.prefs[i].reset();
+}
+
+// Adds a pref to the internal data structures.
+ObjectPrivateData.prototype.addPref =
+function opdata_addPref(name)
+{
+ return this.prefs[name] = new PrefData(this, name);
+}
+
+// Adds a group to a pref object's data.
+ObjectPrivateData.prototype.addGroup =
+function opdata_addPref(name)
+{
+ // Special group for prefs we don't want shown (nothing sinister here).
+ if (name == "hidden")
+ return null;
+
+ if (!(name in this.groups))
+ this.groups[name] = new PrefMainGroup(this, name);
+
+ return this.groups[name];
+}
+
+// Represents a single pref on a single object within the pref window.
+function PrefData(parent, name)
+{
+ // We want to keep all this "worked out" info, so make a hash of all
+ // the prefs on the pwData [Pref Window Data] property of the object.
+
+ // First, lets find out what kind of pref we've got:
+ this.parent = parent; // Private data for pref object.
+ this.name = name;
+ this.manager = this.parent.parent.prefManager; // PrefManager.
+ this.record = this.manager.prefRecords[name]; // PrefRecord.
+ this.def = this.record.defaultValue; // Default value.
+ this.type = typeof this.def; // Pref type.
+ this.val = this.manager.prefs[name]; // Current value.
+ this.startVal = this.val; // Start value.
+ this.label = this.record.label; // Display name.
+ this.help = this.record.help; // Help text.
+ this.group = this.record.group; // Group identifier.
+ this.labelFor = "none"; // Auto-grouped label.
+
+ // Handle defered prefs (call defer function, and use resulting
+ // value/type instead).
+ if (this.type == "function")
+ this.def = this.def(this.name);
+ this.type = typeof this.def;
+
+ // And those arrays... this just makes our life easier later by having
+ // a particular name for array prefs.
+ if (isinstance(this.def, Array))
+ this.type = "array";
+
+ if (this.group == "hidden")
+ this.type = "hidden";
+
+ // Convert "a.b" into sub-properties...
+ var m = this.group.match(/^([^.]*)(\.(.*))?$/)
+ ASSERT(m, "Failed group match!");
+ this.mainGroup = m[1];
+ this.subGroup = m[3];
+
+ return this;
+}
+
+/* Creates all the XUL elements to display this one pref. */
+PrefData.prototype.loadXUL =
+function pdata_loadXUL()
+{
+ if (this.type == "hidden")
+ return;
+
+ // Create the base box for the pref.
+ this.box = document.createElement("box");
+ this.box.orient = "horizontal";
+ this.box.setAttribute("align", "center");
+
+ switch (this.type)
+ {
+ case "string":
+ label = document.createElement("label");
+ label.setAttribute("value", this.label);
+ label.width = 100;
+ label.flex = 1;
+ this.box.appendChild(label);
+
+ this.edit = document.createElement("textbox");
+ // We choose the size based on the length of the default.
+ if (this.def.length < 8)
+ this.edit.setAttribute("size", "10");
+ else if (this.def.length < 20)
+ this.edit.setAttribute("size", "25");
+ else
+ this.edit.flex = 1;
+
+ var editCont = document.createElement("hbox");
+ editCont.flex = 1000;
+ editCont.appendChild(this.edit);
+ this.box.appendChild(editCont);
+
+ // But if it's a file/URL...
+ if (this.def.match(/^([a-z]+:\/|[a-z]:\\)/i))
+ {
+ // ...we make it as big as possible.
+ this.edit.removeAttribute("size");
+ this.edit.flex = 1;
+
+ if (!this.name.match(/path$/i) &&
+ (this.def.match(/^(file|chrome):\//i) ||
+ this.name.match(/filename$/i)))
+ {
+ // So long as the pref name doesn't end in "path", and
+ // it's chrome:, file: or a local file, we add the button.
+ var ext = "";
+ var m = this.def.match(/\.([a-z0-9]+)$/);
+ if (m)
+ ext = "*." + m[1];
+
+ // We're cheating again here, if it ends "filename" it's
+ // a local file path.
+ var type = (this.name.match(/filename$/i) ? "file" : "fileurl");
+ type = (this.name.match(/folder$/i) ? "folder" : type);
+ appendButton(this.box, "onPrefBrowse",
+ { label: MSG_PREFS_BROWSE, spec: ext,
+ kind: type });
+ }
+ }
+ break;
+
+ case "number":
+ label = document.createElement("label");
+ label.setAttribute("value", this.label);
+ label.width = 100;
+ label.flex = 1;
+ this.box.appendChild(label);
+
+ this.edit = document.createElement("textbox");
+ this.edit.setAttribute("size", "5");
+ this.edit.setAttribute("type", "number");
+ this.edit.setAttribute("min", "-1");
+
+ editCont = document.createElement("hbox");
+ editCont.flex = 1000;
+ editCont.appendChild(this.edit);
+ this.box.appendChild(editCont);
+ break;
+
+ case "boolean":
+ this.edit = document.createElement("checkbox");
+ this.edit.setAttribute("label", this.label);
+ this.box.appendChild(this.edit);
+ break;
+
+ case "array":
+ this.box.removeAttribute("align");
+
+ var oBox = document.createElement("box");
+ oBox.orient = "vertical";
+ oBox.flex = 1;
+ this.box.appendChild(oBox);
+
+ if (this.help)
+ {
+ label = document.createElement("label");
+ label.appendChild(document.createTextNode(this.help));
+ oBox.appendChild(label);
+ }
+
+ this.edit = document.createElement("listbox");
+ this.edit.flex = 1;
+ this.edit.setAttribute("style", "height: 1em;");
+ this.edit.setAttribute("kind", "url");
+ if (this.def.length > 0 && this.def[0].match(/^file:\//))
+ this.edit.setAttribute("kind", "fileurl");
+ this.edit.setAttribute("onselect", "gPrefWindow.onPrefListSelect(this);");
+ this.edit.setAttribute("ondblclick", "gPrefWindow.onPrefListEdit(this);");
+ oBox.appendChild(this.edit);
+
+ var box = document.createElement("box");
+ box.orient = "vertical";
+ this.box.appendChild(box);
+
+ // NOTE: This order is important - getRelatedItem needs to be
+ // kept in sync with this order. Perhaps a better way is needed...
+ appendButton(box, "onPrefListUp", { label: MSG_PREFS_MOVE_UP,
+ "class": "up" });
+ appendButton(box, "onPrefListDown", { label: MSG_PREFS_MOVE_DOWN,
+ "class": "down" });
+ appendSeparator(box);
+ appendButton(box, "onPrefListAdd", { label: MSG_PREFS_ADD });
+ appendButton(box, "onPrefListEdit", { label: MSG_PREFS_EDIT });
+ appendButton(box, "onPrefListDelete", { label: MSG_PREFS_DELETE });
+ break;
+
+ default:
+ // This is really more of an error case, since we really should
+ // know about all the valid pref types.
+ var label = document.createElement("label");
+ label.setAttribute("value", "[not editable] " + this.type);
+ this.box.appendChild(label);
+ }
+
+ this.loadData();
+
+ if (this.edit)
+ {
+ this.edit.setAttribute("prefobjectindex", this.parent.arrayIndex);
+ this.edit.setAttribute("prefname", this.name);
+ // Associate textbox with label for accessibility.
+ if (label)
+ {
+ this.edit.id = this.manager.branchName + this.name;
+ label.setAttribute("control", this.edit.id);
+ }
+ }
+
+ if (!ASSERT("groups" in this.parent, "Must have called " +
+ "[ObjectPrivateData].loadXUL before trying to display prefs."))
+ return;
+
+ this.parent.addGroup(this.mainGroup);
+ if (this.subGroup)
+ this.parent.groups[this.mainGroup].addGroup(this.subGroup);
+
+ if (!this.subGroup)
+ this.parent.groups[this.mainGroup].box.appendChild(this.box);
+ else
+ this.parent.groups[this.mainGroup].groups[this.subGroup].box.appendChild(this.box);
+
+ // Setup tooltip stuff...
+ if (this.help && (this.type != "array"))
+ {
+ this.box.setAttribute("tooltiptitle", this.label);
+ this.box.setAttribute("tooltipcontent", this.help);
+ this.box.setAttribute("onmouseover", "gPrefWindow.onPrefMouseOver(this);");
+ this.box.setAttribute("onmousemove", "gPrefWindow.onPrefMouseMove(this);");
+ this.box.setAttribute("onmouseout", "gPrefWindow.onPrefMouseOut(this);");
+ }
+}
+
+/* Loads the pref's data into the edit component. */
+PrefData.prototype.loadData =
+function pdata_loadData()
+{
+ /* Note about .value and .setAttribute as used here:
+ *
+ * XBL doesn't kick in until CSS is calculated on a node, so the code makes
+ * a compromise and uses these two methods as appropriate. Initally this
+ * is called is before the node has been placed in the document DOM tree,
+ * and thus hasn't been "magiced" by XBL and so .value is meaningless to
+ * it. After initally being set as an attribute, it's added to the DOM,
+ * XBL kicks in, and after that .value is the only way to change the value.
+ */
+ switch (this.type)
+ {
+ case "string":
+ if (this.edit.hasAttribute("value"))
+ this.edit.value = this.val;
+ else
+ this.edit.setAttribute("value", this.val);
+ break;
+
+ case "number":
+ if (this.edit.hasAttribute("value"))
+ this.edit.value = this.val;
+ else
+ this.edit.setAttribute("value", this.val);
+ break;
+
+ case "boolean":
+ if (this.edit.hasAttribute("checked"))
+ this.edit.checked = this.val;
+ else
+ this.edit.setAttribute("checked", this.val);
+ break;
+
+ case "array":
+ // Remove old entires.
+ while (this.edit.firstChild)
+ this.edit.removeChild(this.edit.firstChild);
+
+ // Add new ones.
+ for (var i = 0; i < this.val.length; i++)
+ {
+ var item = document.createElement("listitem");
+ item.value = this.val[i];
+ item.crop = "center";
+ item.setAttribute("label", this.val[i]);
+ this.edit.appendChild(item);
+ }
+
+ // Make sure buttons are up-to-date.
+ gPrefWindow.onPrefListSelect(this.edit);
+ break;
+
+ default:
+ }
+}
+
+/* Cleans up the mess. */
+PrefData.prototype.clear =
+function pdata_clear()
+{
+ //dd("Clearing pref " + this.name);
+ if (("box" in this) && this.box)
+ {
+ this.box.parentNode.removeChild(this.box);
+ delete this.box;
+ }
+ try {
+ this.manager.clearPref(this.name);
+ } catch(ex) {}
+}
+
+/* Resets the pref to it's default. */
+PrefData.prototype.reset =
+function pdata_reset()
+{
+ //try {
+ // this.manager.clearPref(this.name);
+ //} catch(ex) {}
+ this.val = this.def;
+ this.loadData();
+}
+
+/* Saves the pref... or would do. */
+PrefData.prototype.save =
+function pdata_save()
+{
+ //FIXME//
+}
+
+// Represents a "main group", i.e. a tab for a single pref object.
+function PrefMainGroup(parent, name)
+{
+ // Init this group's object.
+ this.parent = parent; // Private data for pref object.
+ this.name = name;
+ this.groups = new Object();
+ this.label = getMsg("pref.group." + this.name + ".label", null, this.name);
+ this.tab = document.createElement("tab");
+ this.tabPanel = document.createElement("tabpanel");
+ this.box = this.sb = document.createElement("scroller");
+
+ this.tab.setAttribute("label", this.label);
+ this.tabPanel.setAttribute("orient", "vertical");
+ this.sb.setAttribute("orient", "vertical");
+ this.sb.setAttribute("flex", 1);
+
+ this.parent.tabs.appendChild(this.tab);
+ this.parent.tabPanels.appendChild(this.tabPanel);
+ this.tabPanel.appendChild(this.sb);
+
+ return this;
+}
+// Adds a sub group to this main group.
+PrefMainGroup.prototype.addGroup =
+function pmgroup_addGroup(name)
+{
+ // If the sub group doesn't exist, make it exist.
+ if (!(name in this.groups))
+ this.groups[name] = new PrefSubGroup(this, name);
+
+ return this.groups[name];
+}
+
+// Represents a "sub group", i.e. a groupbox on a tab, for a single main group.
+function PrefSubGroup(parent, name)
+{
+ this.parent = parent; // Main group.
+ this.name = name;
+ this.fullGroup = this.parent.name + "." + this.name;
+ this.label = getMsg("pref.group." + this.fullGroup + ".label", null, this.name);
+ this.help = getMsg("pref.group." + this.fullGroup + ".help", null, "");
+ this.gb = document.createElement("groupbox");
+ this.cap = document.createElement("caption");
+ this.box = document.createElement("box");
+
+ this.cap.setAttribute("label", this.label);
+ this.gb.appendChild(this.cap);
+ this.box.orient = "vertical";
+
+ // If there's some help text, we place it as the first thing inside
+ // the <groupbox>, as an explanation for the entire subGroup.
+ if (this.help)
+ {
+ this.helpLabel = document.createElement("label");
+ this.helpLabel.appendChild(document.createTextNode(this.help));
+ this.gb.appendChild(this.helpLabel);
+ }
+ this.gb.appendChild(this.box);
+ this.parent.box.appendChild(this.gb);
+
+ return this;
+}
+
+// Actual pref window itself.
+function PrefWindow()
+{
+ // Not loaded until the pref list and objects have been created in |onLoad|.
+ this.loaded = false;
+
+ /* PREF TAB ORDER: Determins the order, and fixed tabs, found on the UI.
+ *
+ * It is an array of mainGroup names, and a flag indicating if the tab
+ * should be created even when there's no prefs for it.
+ *
+ * This is for consistency, although none of the ChatZilla built-in pref
+ * objects leave fixed tabs empty currently.
+ */
+ this.prefTabOrder = [{ fixed: true, name: "general"},
+ { fixed: true, name: "appearance"},
+ { fixed: false, name: "lists"},
+ { fixed: false, name: "dcc"},
+ { fixed: false, name: "startup"},
+ { fixed: false, name: "global"},
+ { fixed: false, name: "munger"}
+ ];
+
+ /* PREF OBJECTS: Stores all the objects we've using that have prefs.
+ *
+ * Each object gets a "privateData" object, which is then used by the pref
+ * window code for storing all of it's data on the object.
+ */
+ this.prefObjects = null;
+
+ /* TOOLTIPS: Special tooltips for preference items.
+ *
+ * Timer: return value from |setTimeout| whenever used. There is only
+ * ever one timer going for the tooltips.
+ * Showing: stores visibility of the tooltip.
+ * ShowDelay: ms delay which them mouse must be still to show tooltips.
+ * HideDelay: ms delay before the tooltips hide themselves.
+ */
+ this.tooltipTimer = 0;
+ this.tooltipShowing = false;
+ this.tooltipShowDelay = 1000;
+ this.tooltipHideDelay = 20000;
+ this.tooltipBug418798 = false;
+}
+PrefWindow.prototype.TYPE = "PrefWindow";
+
+/* Updates the tooltip state, either showing or hiding it. */
+PrefWindow.prototype.setTooltipState =
+function pwin_setTooltipState(visible)
+{
+ // Shortcut: if we're already in the right state, don't bother.
+ if (this.tooltipShowing == visible)
+ return;
+
+ var tt = document.getElementById("czPrefTip");
+
+ // If we're trying to show it, and we have a reference object
+ // (this.tooltipObject), we are ok to go.
+ if (visible && this.tooltipObject)
+ {
+ /* Get the boxObject for the reference object, as we're going to
+ * place to tooltip explicitly based on this.
+ */
+ var tipobjBox = this.tooltipObject.boxObject;
+
+ // Adjust the width to that of the reference box.
+ tt.sizeTo(tipobjBox.width, tt.boxObject.height);
+ /* show tooltip using the reference object, and it's screen
+ * position. NOTE: Most of these parameters are supposed to affect
+ * things, and they do seem to matter, but don't work anything like
+ * the documentation. Be careful changing them.
+ */
+ tt.showPopup(this.tooltipObject, -1, -1, "tooltip", "bottomleft", "topleft");
+
+ // Set the timer to hide the tooltip some time later...
+ // (note the fun inline function)
+ this.tooltipTimer = setTimeout(setTooltipState, this.tooltipHideDelay,
+ this, false);
+ this.tooltipShowing = true;
+ }
+ else
+ {
+ /* We're here because either we are meant to be hiding the tooltip,
+ * or we lacked the information needed to show one. So hide it.
+ */
+ tt.hidePopup();
+ this.tooltipShowing = false;
+ }
+}
+
+/** Window event handlers **/
+
+/* Initalises, and loads all the data/utilities and prefs. */
+PrefWindow.prototype.onLoad =
+function pwin_onLoad()
+{
+ // Get ourselves a base object for the object hierarchy.
+ client = new PrefGlobal();
+
+ // Kick off the localisation load.
+ initMessages();
+
+ // Use localised name.
+ client.viewName = MSG_PREFS_GLOBAL;
+ client.unicodeName = client.viewName;
+ client.prettyName = client.viewName;
+
+ // Use the window mediator service to prevent mutliple instances.
+ var windowMediator = Components.classes[MEDIATOR_CONTRACTID];
+ var windowManager = windowMediator.getService(nsIWindowMediator);
+ var enumerator = windowManager.getEnumerator(CONFIG_WINDOWTYPE);
+
+ // We only want one open at a time because don't (currently) cope with
+ // pref-change notifications. In fact, it's not easy to cope with.
+ // Save it for some time later. :)
+
+ enumerator.getNext();
+ if (enumerator.hasMoreElements())
+ {
+ alert(MSG_PREFS_ALREADYOPEN);
+ window.close();
+ return;
+ }
+
+ // Make sure we know what host we're on.
+ initApplicationCompatibility();
+
+ // Kick off the core pref initalisation code.
+ initPrefs();
+
+ // Turn off all notifications, or it'll get confused when any pref
+ // does change.
+ client.prefManager.onPrefChanged = function(){};
+
+ // The list of objects we're tacking the prefs of.
+ this.prefObjects = new PrefObjectList();
+
+ /* Oh, this is such an odd way to do this. But hey, it works. :)
+ * What we do is ask the pref branch for the client object to give us
+ * a list of all the preferences under it. This just gets us all the
+ * Chatzilla prefs. Then, we filter them so that we only deal with
+ * ones for networks, channels and users. This means, even if only
+ * one pref is set for a channel, we get it's network and channel
+ * object created here.
+ */
+ var prefRE = new RegExp("^networks.([^.]+)" +
+ "(?:\\.(users|channels)?\\.([^.]+))?\\.");
+ var rv = new Object();
+ var netList = client.prefManager.prefBranch.getChildList("networks.", rv);
+ for (var i in netList)
+ {
+ var m = netList[i].match(prefRE);
+ if (!m)
+ continue;
+
+ var netName = unMungeNetworkName(m[1]);
+ /* We're forcing the network into existance (3rd param) if the
+ * pref is actually set, as opposed to just being known about (the
+ * pref branch will list all *known* prefs, not just those set).
+ *
+ * NOTE: |force| will, if |true|, set a property on the object so it
+ * will still exist next time we're here. If |false|, the
+ * the object will only come into existance if this magical
+ * [hidden] pref is set.
+ */
+ var force = client.prefManager.prefBranch.prefHasUserValue(netList[i]);
+
+ // Don't bother with the new if it's already there (time saving).
+ if (!(":" + netName in client.networks))
+ new PrefNetwork(client, netName, force);
+
+ if ((2 in m) && (3 in m) && (":" + netName in client.networks))
+ {
+ let net = client.networks[":" + netName];
+
+ // Create a channel object if appropriate.
+ if (m[2] == "channels")
+ new PrefChannel(net.primServ, unMungeNetworkName(m[3]), force);
+
+ // Create a user object if appropriate.
+ if (m[2] == "users")
+ new PrefUser(net.primServ, unMungeNetworkName(m[3]), force);
+ }
+ }
+
+ // Store out object that represents the current view.
+ var currentView = null;
+
+ // Enumerate source window's tab list...
+ if (("arguments" in window) && (0 in window.arguments) && ("client" in window.arguments[0]))
+ {
+ /* Make sure we survive this, external data could be bad. :) */
+ try
+ {
+ var czWin = window.arguments[0];
+ var s;
+ var n, c, u;
+ this.ownerClient = czWin.client;
+ this.ownerClient.configWindow = window;
+
+ /* Go nick the source window's view list. We can then show items in
+ * the tree for the currently connected/shown networks, channels
+ * and users even if they don't have any known prefs yet.
+ *
+ * NOTE: the |false, true| params tell the objects to not force
+ * any prefs into existance, but still show in the tree.
+ */
+ for (i = 0; i < czWin.client.viewsArray.length; i++)
+ {
+ var view = czWin.client.viewsArray[i].source;
+
+ // Network view...
+ if (view.TYPE == "IRCNetwork")
+ {
+ n = new PrefNetwork(client, view.unicodeName, false, true);
+ if (view == czWin.client.currentObject)
+ currentView = n;
+ }
+
+ if (view.TYPE.match(/^IRC(Channel|User)$/))
+ {
+ n = new PrefNetwork(client, view.parent.parent.unicodeName,
+ false, true);
+ s = n.primServ;
+ }
+
+ // Channel view...
+ if (view.TYPE == "IRCChannel")
+ {
+ c = new PrefChannel(s, view.unicodeName, false, true);
+ if (view == czWin.client.currentObject)
+ currentView = c;
+ }
+
+ // User view...
+ if (view.TYPE == "IRCUser")
+ {
+ u = new PrefUser(s, view.unicodeName, false, true);
+ if (view == czWin.client.currentObject)
+ currentView = u;
+ }
+ }
+ }
+ catch(ex)
+ {}
+ }
+
+ // Add the client object...
+ this.prefObjects.addObject(client);
+ // ...and everyone else.
+ var i, j;
+ /* We sort the keys (property names, i.e. network names). This means the UI
+ * will show them in lexographical order, which is good.
+ */
+ var sortedNets = keys(client.networks).sort();
+ for (i = 0; i < sortedNets.length; i++) {
+ net = client.networks[sortedNets[i]];
+ this.prefObjects.addObject(net);
+
+ var sortedChans = keys(net.channels).sort();
+ for (j = 0; j < sortedChans.length; j++)
+ this.prefObjects.addObject(net.channels[sortedChans[j]]);
+
+ var sortedUsers = keys(net.users).sort();
+ for (j = 0; j < sortedUsers.length; j++)
+ this.prefObjects.addObject(net.users[sortedUsers[j]]);
+ }
+
+ // Select the first item in the list.
+ var prefTree = document.getElementById("pref-tree-object");
+ if ("selection" in prefTree.treeBoxObject)
+ prefTree.treeBoxObject.selection.select(0);
+ else
+ prefTree.view.selection.select(0);
+
+ // Find and select the current view.
+ for (i = 0; i < this.prefObjects.objects.length; i++)
+ {
+ if (this.prefObjects.objects[i] == currentView)
+ {
+ if ("selection" in prefTree.treeBoxObject)
+ prefTree.treeBoxObject.selection.select(i);
+ else
+ prefTree.view.selection.select(i);
+ }
+ }
+
+ this.onSelectObject();
+
+ // We're done, without error, so it's safe to show the stuff.
+ document.getElementById("loadDeck").selectedIndex = 1;
+ // This allows [OK] to actually save, without this it'll just close.
+ this.loaded = true;
+
+ // Force the window to be the right size now, not later.
+ window.sizeToContent();
+ // XXX: If we're on mac, make it wider because the default theme's
+ // tabs are huge:
+ if (client.platform == "Mac")
+ window.resizeBy(140, 0);
+
+ // Center window.
+ if (("arguments" in window) && (0 in window.arguments))
+ {
+ var ow = window.arguments[0];
+
+ window.moveTo(ow.screenX + Math.max((ow.outerWidth - window.outerWidth ) / 2, 0),
+ ow.screenY + Math.max((ow.outerHeight - window.outerHeight) / 2, 0));
+ }
+}
+
+/* Closing the window. Clean up. */
+PrefWindow.prototype.onClose =
+function pwin_onClose()
+{
+ if (this.ownerClient)
+ delete this.ownerClient.configWindow;
+ if (this.loaded)
+ destroyPrefs();
+}
+
+/* OK button. */
+PrefWindow.prototype.onOK =
+function pwin_onOK()
+{
+ if (this.onApply())
+ window.close();
+ return true;
+}
+
+/* Apply button. */
+PrefWindow.prototype.onApply =
+function pwin_onApply()
+{
+ // If the load failed, better not to try to save.
+ if (!this.loaded)
+ return false;
+
+ try {
+ // Get an array of all the (XUL) items we have to save.
+ var list = getPrefTags();
+
+ //if (!confirm("There are " + list.length + " pref tags to save. OK?")) return false;
+
+ for (var i = 0; i < list.length; i++)
+ {
+ // Save this one pref...
+ var index = list[i].getAttribute("prefobjectindex");
+ var name = list[i].getAttribute("prefname");
+ // Get the private data for the object out, since everything is
+ // based on this.
+ var po = this.prefObjects.getObject(index);
+ var pref = po.prefs[name];
+
+ var value;
+ // We just need to force the value from the DOMString form to
+ // the right form, so we don't get anything silly happening.
+ switch (pref.type)
+ {
+ case "string":
+ value = list[i].value;
+ break;
+ case "number":
+ value = 1 * list[i].value;
+ break;
+ case "boolean":
+ value = list[i].checked;
+ break;
+ case "array":
+ var l = new Array();
+ for (var j = 0; j < list[i].childNodes.length; j++)
+ l.push(list[i].childNodes[j].value);
+ value = l;
+ break;
+ default:
+ throw "Unknown pref type: " + pref.type + "!";
+ }
+ /* Fun stuff. We don't want to save if the user hasn't changed
+ * it. This is so that if the default is defered, and changed,
+ * but this hasn't, we keep the defered default. Which is a
+ * Good Thing. :)
+ */
+ if (((pref.type != "array") && (value != pref.startVal)) ||
+ ((pref.type == "array") &&
+ (value.join(";") != pref.startVal.join(";"))))
+ {
+ po.parent.prefs[pref.name] = value;
+ }
+ // Update our saved value, so the above check works the 2nd
+ // time the user clicks Apply.
+ pref.val = value;
+ pref.startVal = pref.val;
+ }
+
+ return true;
+ }
+ catch (e)
+ {
+ alert(getMsg(MSG_PREFS_ERR_SAVE, e));
+ return false;
+ }
+}
+
+/* Cancel button. */
+PrefWindow.prototype.onCancel =
+function pwin_onCancel()
+{
+ window.close();
+ return true;
+}
+
+/** Tooltips' event handlers **/
+
+PrefWindow.prototype.onPrefMouseOver =
+function pwin_onPrefMouseOver(object)
+{
+ this.tooltipObject = object;
+ this.tooltipTitle = object.getAttribute("tooltiptitle");
+ this.tooltipText = object.getAttribute("tooltipcontent");
+ // Reset the timer now we're over a new pref.
+ clearTimeout(this.tooltipTimer);
+ this.tooltipTimer = setTimeout(setTooltipState, this.tooltipShowDelay,
+ this, true);
+}
+
+PrefWindow.prototype.onPrefMouseMove =
+function pwin_onPrefMouseMove(object)
+{
+ // If the tooltip isn't showing, we need to reset the timer.
+ if (!this.tooltipShowing)
+ {
+ clearTimeout(this.tooltipTimer);
+ this.tooltipTimer = setTimeout(setTooltipState, this.tooltipShowDelay,
+ this, true);
+ }
+}
+
+PrefWindow.prototype.onPrefMouseOut =
+function pwin_onPrefMouseOut(object)
+{
+ // Left the pref! Hide tooltip, and clear timer.
+ this.setTooltipState(false);
+ clearTimeout(this.tooltipTimer);
+}
+
+PrefWindow.prototype.onTooltipPopupShowing =
+function pwin_onTooltipPopupShowing(popup)
+{
+ if (!this.tooltipText)
+ return false;
+
+ var fChild = popup.firstChild;
+ var diff = popup.boxObject.height - fChild.boxObject.height;
+
+ // Setup the labels...
+ var ttt = document.getElementById("czPrefTipTitle");
+ ttt.firstChild.nodeValue = this.tooltipTitle;
+ var ttl = document.getElementById("czPrefTipLabel");
+ ttl.firstChild.nodeValue = this.tooltipText;
+
+ /* In Gecko 1.9, the popup has done no layout at this point, unlike in
+ * earlier versions. As a result, the box object of all the elements
+ * within it are 0x0. It also means the height of the labels isn't
+ * updated. To deal with this, we avoid calling sizeTo with the box
+ * object (as it's 0) and instead just force the popup height to 0 -
+ * otherwise it will only ever get bigger each time, never smaller.
+ */
+ if (popup.boxObject.width == 0)
+ this.tooltipBug418798 = true;
+
+ if (this.tooltipBug418798)
+ popup.height = 0;
+ else
+ popup.sizeTo(popup.boxObject.width, fChild.boxObject.height + diff);
+
+ return true;
+}
+
+/** Components' event handlers **/
+
+// Selected an item in the tree.
+PrefWindow.prototype.onSelectObject =
+function pwin_onSelectObject()
+{
+ var prefTree = document.getElementById("pref-tree-object");
+ var rv = new Object();
+ if ("selection" in prefTree.treeBoxObject)
+ prefTree.treeBoxObject.selection.getRangeAt(0, rv, {});
+ else
+ prefTree.view.selection.getRangeAt(0, rv, {});
+ var prefTreeIndex = rv.value;
+
+ var delButton = document.getElementById("object-delete");
+ if (prefTreeIndex > 0)
+ delButton.removeAttribute("disabled");
+ else
+ delButton.setAttribute("disabled", "true");
+
+ var prefItem = prefTree.contentView.getItemAtIndex(prefTreeIndex);
+
+ var pObjectIndex = prefItem.getAttribute("prefobjectindex");
+ var pObject = this.prefObjects.getObject(pObjectIndex);
+
+ if (!ASSERT(pObject, "Object not found for index! " +
+ prefItem.getAttribute("prefobjectindex")))
+ return;
+
+ pObject.loadXUL(this.prefTabOrder);
+
+ var header = document.getElementById("pref-header");
+ header.setAttribute("title", getMsg(MSG_PREFS_FMT_HEADER,
+ pObject.parent.prettyName));
+
+ var deck = document.getElementById("pref-object-deck");
+ deck.selectedIndex = pObject.deckIndex;
+
+ this.currentObject = pObject;
+}
+
+// Browse button for file prefs.
+PrefWindow.prototype.onPrefBrowse =
+function pwin_onPrefBrowse(button)
+{
+ var spec = "$all";
+ if (button.hasAttribute("spec"))
+ spec = button.getAttribute("spec") + " " + spec;
+
+ var type = button.getAttribute("kind");
+ var edit = button.previousSibling.lastChild;
+
+ var rv;
+ if (type == "folder")
+ {
+ try
+ {
+ // if the user set the pref to an invalid folder, this will throw:
+ var current = getFileFromURLSpec(edit.value);
+ }
+ catch (ex)
+ {
+ // Just setting it to null will work:
+ current = null;
+ }
+ rv = pickGetFolder(MSG_PREFS_BROWSE_TITLE, current);
+ }
+ else
+ {
+ rv = pickOpen(MSG_PREFS_BROWSE_TITLE, spec);
+ }
+
+ if (!rv.ok)
+ return;
+
+ edit.value = (type == "file") ? rv.file.path : rv.picker.fileURL.spec;
+},
+
+// Selection changed on listbox.
+PrefWindow.prototype.onPrefListSelect =
+function pwin_onPrefListSelect(object)
+{
+ var list = getRelatedItem(object, "list");
+ var buttons = new Object();
+ buttons.up = getRelatedItem(object, "button-up");
+ buttons.down = getRelatedItem(object, "button-down");
+ buttons.add = getRelatedItem(object, "button-add");
+ buttons.edit = getRelatedItem(object, "button-edit");
+ buttons.del = getRelatedItem(object, "button-delete");
+
+ if (("selectedItems" in list) && list.selectedItems &&
+ list.selectedItems.length)
+ {
+ buttons.edit.removeAttribute("disabled");
+ buttons.del.removeAttribute("disabled");
+ }
+ else
+ {
+ buttons.edit.setAttribute("disabled", "true");
+ buttons.del.setAttribute("disabled", "true");
+ }
+
+ if (!("selectedItems" in list) || !list.selectedItems ||
+ list.selectedItems.length == 0 || list.selectedIndex == 0)
+ {
+ buttons.up.setAttribute("disabled", "true");
+ }
+ else
+ {
+ buttons.up.removeAttribute("disabled");
+ }
+
+ if (!("selectedItems" in list) || !list.selectedItems ||
+ list.selectedItems.length == 0 ||
+ list.selectedIndex == list.childNodes.length - 1)
+ {
+ buttons.down.setAttribute("disabled", "true");
+ }
+ else
+ {
+ buttons.down.removeAttribute("disabled");
+ }
+}
+
+// Up button for lists.
+PrefWindow.prototype.onPrefListUp =
+function pwin_onPrefListUp(object)
+{
+ var list = getRelatedItem(object, "list");
+
+ var selected = list.selectedItems[0];
+ var before = selected.previousSibling;
+ if (before)
+ {
+ before.parentNode.insertBefore(selected, before);
+ list.selectItem(selected);
+ list.ensureElementIsVisible(selected);
+ }
+}
+
+// Down button for lists.
+PrefWindow.prototype.onPrefListDown =
+function pwin_onPrefListDown(object)
+{
+ var list = getRelatedItem(object, "list");
+
+ var selected = list.selectedItems[0];
+ if (selected.nextSibling)
+ {
+ if (selected.nextSibling.nextSibling)
+ list.insertBefore(selected, selected.nextSibling.nextSibling);
+ else
+ list.appendChild(selected);
+
+ list.selectItem(selected);
+ }
+}
+
+// Add button for lists.
+PrefWindow.prototype.onPrefListAdd =
+function pwin_onPrefListAdd(object)
+{
+ var list = getRelatedItem(object, "list");
+ var newItem;
+
+ switch (list.getAttribute("kind"))
+ {
+ case "url":
+ var item = prompt(MSG_PREFS_LIST_ADD);
+ if (item)
+ {
+ newItem = document.createElement("listitem");
+ newItem.setAttribute("label", item);
+ newItem.value = item;
+ list.appendChild(newItem);
+ this.onPrefListSelect(object);
+ }
+ break;
+ case "file":
+ case "fileurl":
+ var spec = "$all";
+
+ var rv = pickOpen(MSG_PREFS_BROWSE_TITLE, spec);
+ if (rv.ok)
+ {
+ var data = { file: rv.file.path, fileurl: rv.picker.fileURL.spec };
+ var kind = list.getAttribute("kind");
+
+ newItem = document.createElement("listitem");
+ newItem.setAttribute("label", data[kind]);
+ newItem.value = data[kind];
+ list.appendChild(newItem);
+ this.onPrefListSelect(object);
+ }
+
+ break;
+ }
+}
+
+// Edit button for lists.
+PrefWindow.prototype.onPrefListEdit =
+function pwin_onPrefListEdit(object)
+{
+ var list = getRelatedItem(object, "list");
+
+ switch (list.getAttribute("kind"))
+ {
+ case "url":
+ case "file":
+ case "fileurl":
+ // We're letting the user edit file types here, since it saves us
+ // a lot of work, and we can't let them pick a file OR a directory,
+ // so they pick a file and can edit it off to use a directory.
+ var listItem = list.selectedItems[0];
+ var newValue = prompt(MSG_PREFS_LIST_EDIT, listItem.value);
+ if (newValue)
+ {
+ listItem.setAttribute("label", newValue);
+ listItem.value = newValue;
+ }
+ break;
+ }
+}
+
+// Delete button for lists.
+PrefWindow.prototype.onPrefListDelete =
+function pwin_onPrefListDelete(object)
+{
+ var list = getRelatedItem(object, "list");
+
+ var listItem = list.selectedItems[0];
+ if (confirm(getMsg(MSG_PREFS_LIST_DELETE, listItem.value)))
+ list.removeChild(listItem);
+}
+
+/* Add... button. */
+PrefWindow.prototype.onAddObject =
+function pwin_onAddObject()
+{
+ var rv = new Object();
+
+ /* Try to nobble the current selection and pre-fill as needed. */
+ switch (this.currentObject.parent.TYPE)
+ {
+ case "PrefNetwork":
+ rv.type = "net";
+ rv.net = this.currentObject.parent.unicodeName;
+ break;
+ case "PrefChannel":
+ rv.type = "chan";
+ rv.net = this.currentObject.parent.parent.parent.unicodeName;
+ rv.chan = this.currentObject.parent.unicodeName;
+ break;
+ case "PrefUser":
+ rv.type = "user";
+ rv.net = this.currentObject.parent.parent.parent.unicodeName;
+ rv.chan = this.currentObject.parent.unicodeName;
+ break;
+ }
+
+ // Show add dialog, passing the data object along.
+ window.openDialog("config-add.xul", "cz-config-add", "chrome,dialog,modal", rv);
+
+ if (!rv.ok)
+ return;
+
+ /* Ok, so what type did they want again?
+ *
+ * NOTE: The param |true| in the object creation calls is for |force|. It
+ * causes the hidden pref to be set for the objects so they are shown
+ * every time this window opens, until the user deletes them.
+ */
+ switch (rv.type)
+ {
+ case "net":
+ this.prefObjects.addObject(new PrefNetwork(client, rv.net, true));
+ break;
+ case "chan":
+ if (!(":" + rv.net in client.networks))
+ this.prefObjects.addObject(new PrefNetwork(client, rv.net, true));
+ this.prefObjects.addObject(new PrefChannel(client.networks[":" + rv.net].primServ, rv.chan, true));
+ break;
+ case "user":
+ if (!(":" + rv.net in client.networks))
+ this.prefObjects.addObject(new PrefNetwork(client, rv.net, true));
+ this.prefObjects.addObject(new PrefUser(client.networks[":" + rv.net].primServ, rv.chan, true));
+ break;
+ default:
+ // Oops. Not good, if we got here.
+ alert("Unknown pref type: " + rv.type);
+ }
+}
+
+/* Delete button. */
+PrefWindow.prototype.onDeleteObject =
+function pwin_onDeleteObject()
+{
+ // Save current node before we re-select.
+ var sel = this.currentObject;
+
+ // Check they want to go ahead.
+ if (!confirm(getMsg(MSG_PREFS_OBJECT_DELETE, sel.parent.unicodeName)))
+ return;
+
+ // Select a new item BEFORE removing the current item, so the <tree>
+ // doesn't freak out on us.
+ var prefTree = document.getElementById("pref-tree-object");
+ if ("selection" in prefTree.treeBoxObject)
+ prefTree.treeBoxObject.selection.select(0);
+ else
+ prefTree.view.selection.select(0);
+
+ // If it's a network, nuke all the channels and users too.
+ if (sel.parent.TYPE == "PrefNetwork")
+ {
+ var chans = sel.parent.channels;
+ for (k in chans)
+ PrefObjectList.getPrivateData(chans[k]).clear();
+
+ var users = sel.parent.users;
+ for (k in users)
+ PrefObjectList.getPrivateData(users[k]).clear();
+ }
+ sel.clear();
+
+ this.onSelectObject();
+}
+
+/* Reset button. */
+PrefWindow.prototype.onResetObject =
+function pwin_onResetObject()
+{
+ // Save current node before we re-select.
+ var sel = this.currentObject;
+
+ // Check they want to go ahead.
+ if (!confirm(getMsg(MSG_PREFS_OBJECT_RESET, sel.parent.unicodeName)))
+ return;
+
+ // Reset the prefs.
+ sel.reset();
+}
+
+// End of PrefWindow. //
+
+/*** Base functions... ***/
+
+/* Gets a "related" items, such as the buttons associated with a list. */
+function getRelatedItem(object, thing)
+{
+ switch (object.nodeName)
+ {
+ case "listbox":
+ switch (thing) {
+ case "list":
+ return object;
+ case "button-up":
+ return object.parentNode.nextSibling.childNodes[0];
+ case "button-down":
+ return object.parentNode.nextSibling.childNodes[1];
+ case "button-add":
+ return object.parentNode.nextSibling.childNodes[3];
+ case "button-edit":
+ return object.parentNode.nextSibling.childNodes[4];
+ case "button-delete":
+ return object.parentNode.nextSibling.childNodes[5];
+ }
+ break;
+ case "button":
+ var n = object.parentNode.previousSibling.lastChild;
+ if (n)
+ return getRelatedItem(n, thing);
+ break;
+ }
+ return null;
+}
+
+// Wrap this call so we have the right |this|.
+function setTooltipState(w, s)
+{
+ w.setTooltipState(s);
+}
+
+// Reverses the Pref Manager's munging of network names.
+function unMungeNetworkName(name)
+{
+ name = ecmaUnescape(name);
+ return name.replace(/_/g, ":").replace(/-/g, ".");
+}
+
+// Adds a button to a container, setting up the command in a simple way.
+function appendButton(cont, oncommand, attr)
+{
+ var btn = document.createElement("button");
+ if (attr)
+ for (var a in attr)
+ btn.setAttribute(a, attr[a]);
+ if (oncommand)
+ btn.setAttribute("oncommand", "gPrefWindow." + oncommand + "(this);");
+ else
+ btn.setAttribute("disabled", "true");
+ cont.appendChild(btn);
+}
+
+// Like appendButton, but just drops in a separator.
+function appendSeparator(cont, attr)
+{
+ var spacer = document.createElement("separator");
+ if (attr)
+ for (var a in attr)
+ spacer.setAttribute(a, attr[a]);
+ cont.appendChild(spacer);
+}
+
+/* This simply collects together all the <textbox>, <checkbox> and <listbox>
+ * elements that have the attribute "prefname". Thus, we generate a list of
+ * all elements that are for prefs.
+ */
+function getPrefTags()
+{
+ var rv = new Array();
+ var i, list;
+
+ list = document.getElementsByTagName("textbox");
+ for (i = 0; i < list.length; i++)
+ {
+ if (list[i].hasAttribute("prefname"))
+ rv.push(list[i]);
+ }
+ list = document.getElementsByTagName("checkbox");
+ for (i = 0; i < list.length; i++)
+ {
+ if (list[i].hasAttribute("prefname"))
+ rv.push(list[i]);
+ }
+ list = document.getElementsByTagName("listbox");
+ for (i = 0; i < list.length; i++)
+ {
+ if (list[i].hasAttribute("prefname"))
+ rv.push(list[i]);
+ }
+
+ return rv;
+}
+
+// Sets up the "extra1" button (Apply).
+function setupButtons()
+{
+ // Hacky-hacky-hack. Looks like the new toolkit does provide a solution,
+ // but we need to support SeaMonkey too. :)
+
+ var dialog = document.documentElement;
+ dialog.getButton("extra1").label = dialog.getAttribute("extra1Label");
+}
+
+// And finally, we want one of these.
+var gPrefWindow = new PrefWindow();
diff --git a/comm/suite/chatzilla/xul/content/config.xul b/comm/suite/chatzilla/xul/content/config.xul
new file mode 100644
index 0000000000..47cacbfd3d
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/config.xul
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/config.dtd">
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="config.css" type="text/css"?>
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="irc:chatzilla:config"
+ id="chatzilla-window"
+ buttons="accept,cancel,extra1"
+ extra1Label="&dialog.apply;"
+ ondialogaccept="gPrefWindow.onOK();"
+ ondialogcancel="gPrefWindow.onCancel();"
+ ondialogextra1="gPrefWindow.onApply();"
+ onload="setupButtons(); gPrefWindow.onLoad();"
+ onunload="gPrefWindow.onClose();"
+ title="&window.title;">
+
+ <script src="chrome://chatzilla/content/lib/js/utils.js"/>
+ <script src="chrome://chatzilla/content/lib/js/file-utils.js"/>
+ <script src="chrome://chatzilla/content/lib/js/pref-manager.js"/>
+ <script src="chrome://chatzilla/content/lib/js/message-manager.js"/>
+ <script src="chrome://chatzilla/content/prefs.js"/>
+ <script src="chrome://chatzilla/content/messages.js"/>
+ <script src="config.js"/>
+ <script src="chrome://chatzilla/content/static.js"/>
+
+ <tooltip id="czPrefTip" orient="vertical"
+ onpopupshowing="return gPrefWindow.onTooltipPopupShowing(this);">
+ <vbox>
+ <label id="czPrefTipTitle" class="header">.</label>
+ <label id="czPrefTipLabel">.</label>
+ </vbox>
+ <spacer flex="1"/>
+ </tooltip>
+
+ <deck id="loadDeck" flex="1">
+ <vbox flex="1" align="center" pack="center">
+ <label class="loading" value="&loading.label;"/>
+ </vbox>
+ <hbox flex="1">
+ <vbox id="pref-objects">
+ <tree id="pref-tree-object" flex="1" seltype="single"
+ hidecolumnpicker="true" onselect="gPrefWindow.onSelectObject();">
+ <treecols>
+ <treecol id="pref-col-name" primary="true" flex="1"
+ hideheader="true"/>
+ </treecols>
+ <treechildren id="pref-tree"/>
+ </tree>
+ <hbox>
+ <!--
+ <button label="&object.add.label;" tooltiptext="&object.add.hint;"
+ accesskey="&object.add.accesskey;" flex="1"
+ oncommand="gPrefWindow.onAddObject();"/>
+ -->
+ <button label="&object.del.label;" tooltiptext="&object.del.hint;"
+ accesskey="&object.del.accesskey;" flex="1"
+ oncommand="gPrefWindow.onDeleteObject();" id="object-delete"/>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <dialogheader id="pref-header" title=""/>
+ <deck flex="1" id="pref-object-deck"/>
+ <hbox align="center">
+ <button label="&object.reset.label;" tooltiptext="&object.reset.hint;"
+ accesskey="&object.reset.accesskey;"
+ oncommand="gPrefWindow.onResetObject();"/>
+ <spacer flex="1"/>
+ <html:a onclick="" target="_blank" href="&homepage.url;"
+ style="display: block; color: blue; text-decoration:
+ underline;">&homepage.label;</html:a>
+ </hbox>
+ </vbox>
+ </hbox>
+ </deck>
+</dialog>
diff --git a/comm/suite/chatzilla/xul/content/dynamic.css b/comm/suite/chatzilla/xul/content/dynamic.css
new file mode 100644
index 0000000000..d585c07ab2
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/dynamic.css
@@ -0,0 +1,7 @@
+/* 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/. */
+
+/* empty css file. rules are appended to this dynamically */
+
+
diff --git a/comm/suite/chatzilla/xul/content/handlers.js b/comm/suite/chatzilla/xul/content/handlers.js
new file mode 100644
index 0000000000..74e0f1c856
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/handlers.js
@@ -0,0 +1,3960 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+window.onresize =
+function onresize()
+{
+ for (var i = 0; i < client.deck.childNodes.length; i++)
+ scrollDown(client.deck.childNodes[i], true);
+}
+
+function onInputFocus()
+{
+}
+
+function showErrorDlg(message)
+{
+ const XUL_MIME = "application/vnd.mozilla.xul+xml";
+ const XUL_KEY = "http://www.mozilla.org/keymaster/" +
+ "gatekeeper/there.is.only.xul";
+
+ const TITLE = "ChatZilla run-time error";
+ const HEADER = "There was a run-time error with ChatZilla. " +
+ "Please report the following information:";
+
+ const OL_JS = "document.getElementById('tb').value = '%S';";
+ const TB_STYLE = ' multiline="true" readonly="true"' +
+ ' style="width: 60ex; height: 20em;"';
+
+ const ERROR_DLG = '<?xml version="1.0"?>' +
+ '<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>' +
+ '<dialog xmlns="' + XUL_KEY + '" buttons="accept" ' +
+ 'title="' + TITLE + '" onload="' + OL_JS + '">' +
+ '<label>' + HEADER + '</label><textbox' + TB_STYLE + ' id="tb"/>' +
+ '</dialog>';
+
+ var content = message.replace(/\n/g, "\\n");
+ content = content.replace(/'/g, "\\'").replace(/"/g, "&quot;");
+ content = content.replace(/</g, "&lt;").replace(/>/g, "&gt;");
+ content = ERROR_DLG.replace("%S", content);
+ content = encodeURIComponent(content);
+ content = "data:" + XUL_MIME + "," + content;
+
+ setTimeout(function() {
+ window.openDialog(content, "_blank", "chrome,modal");
+ }, 100);
+}
+
+function onLoad()
+{
+ dd ("Initializing ChatZilla {");
+ try
+ {
+ init();
+ }
+ catch (ex)
+ {
+ dd("caught exception while initializing:\n" + dumpObjectTree(ex));
+ var exception = formatException(ex) + (ex.stack && "\n" + ex.stack);
+ showErrorDlg(exception + "\n" + dumpObjectTree(ex));
+ }
+
+ dd("}");
+ mainStep();
+}
+
+function initHandlers()
+{
+ var node;
+ node = document.getElementById("input");
+ node.addEventListener("keypress", onInputKeyPress, false);
+ node = document.getElementById("multiline-input");
+ node.addEventListener("keypress", onMultilineInputKeyPress, false);
+ node.active = false;
+
+ window.onkeypress = onWindowKeyPress;
+
+ window.isFocused = false;
+ window.addEventListener("focus", onWindowFocus, true);
+ window.addEventListener("blur", onWindowBlue, true);
+
+ client.inputPopup = null;
+
+ // Should fail silently pre-moz1.4
+ doCommandWithParams("cmd_clipboardDragDropHook",
+ {addhook: CopyPasteHandler});
+}
+
+function onClose()
+{
+ // Assume close needs authorization from user.
+ var close = false;
+
+ // Close has already been authorized.
+ if ("userClose" in client && client.userClose)
+ close = true;
+
+ // Not connected, no need for authorization.
+ if (!("getConnectionCount" in client) || (client.getConnectionCount() == 0))
+ close = true;
+
+ if (!close)
+ {
+ // Authorization needed from user.
+ client.wantToQuit();
+ return false;
+ }
+
+ return true;
+}
+
+function onUnload()
+{
+ dd("Shutting down ChatZilla.");
+
+ /* Disable every loaded & enabled plugin to give them all a chance to
+ * clean up anything beyond the ChatZilla window (e.g. libraries). All
+ * errors are disregarded as there's nothing we can do at this point.
+ * Wipe the plugin list afterwards for safety.
+ */
+ for (var k in client.plugins)
+ {
+ if ((client.plugins[k].API > 0) && client.plugins[k].enabled)
+ {
+ try
+ {
+ client.plugins[k].disable();
+ }
+ catch(ex) {}
+ }
+ }
+ client.plugins = new Object();
+
+ // Close all dialogs.
+ if ("joinDialog" in client)
+ client.joinDialog.close();
+ if ("configWindow" in client)
+ client.configWindow.close();
+ if ("installPluginDialog" in client)
+ client.installPluginDialog.close();
+ if ("aboutDialog" in client)
+ client.aboutDialog.close();
+
+ // We don't trust anybody.
+ client.hiddenDocument = null;
+ uninitOfflineIcon();
+ uninitIdleAutoAway(client.prefs["awayIdleTime"]);
+ destroy();
+}
+
+function onNotImplemented()
+{
+ alert (getMsg("onNotImplementedMsg"));
+}
+
+/* tab click */
+function onTabClick(e, id)
+{
+ if (e.which != 2)
+ return;
+
+ var tbi = document.getElementById(id);
+ var view = client.viewsArray[tbi.getAttribute("viewKey")];
+
+ if (e.which == 2)
+ {
+ dispatch("hide", { view: view.source, source: "mouse" });
+ return;
+ }
+}
+
+function onTabSelect(e)
+{
+ var tabs = e.target;
+
+ /* Hackaround, bug 314230. XBL likes focusing a tab before onload has fired.
+ * That means the tab we're trying to select here will be the hidden one,
+ * which doesn't have a viewKey. We catch that case.
+ */
+ if (!tabs.selectedItem.hasAttribute("viewKey"))
+ return;
+
+ var key = tabs.selectedItem.getAttribute("viewKey");
+ var view = client.viewsArray[key];
+ dispatch("set-current-view", {view:view.source});
+}
+
+function onMessageViewClick(e)
+{
+ if ((e.which != 1) && (e.which != 2))
+ return true;
+
+ var cx = getMessagesContext(null, e.target);
+ cx.source = "mouse";
+ cx.shiftKey = e.shiftKey;
+ var command = getEventCommand(e);
+ if (!client.commandManager.isCommandSatisfied(cx, command))
+ return false;
+
+ dispatch(command, cx);
+ dispatch("focus-input");
+ e.preventDefault();
+ return true;
+}
+
+function onMessageViewMouseDown(e)
+{
+ if ((typeof startScrolling != "function") ||
+ ((e.which != 1) && (e.which != 2)))
+ {
+ return true;
+ }
+
+ var cx = getMessagesContext(null, e.target);
+ var command = getEventCommand(e);
+ if (!client.commandManager.isCommandSatisfied(cx, command))
+ startScrolling(e);
+ return true;
+}
+
+function onMessageViewContextMenu(e)
+{
+ var elem = e.target;
+ var menu = document.getElementById("context:messages");
+ while (elem)
+ {
+ if (elem.localName && elem.localName.toLowerCase() == "input")
+ {
+ menu = document.getElementById("context:edit");
+ break;
+ }
+ elem = elem.parentNode;
+ }
+ document.popupNode = e.target;
+ if ("openPopupAtScreen" in menu)
+ menu.openPopupAtScreen(e.screenX, e.screenY, true);
+ else
+ menu.showPopup(null, e.screenX + 2, e.screenY + 2, "context", "", "");
+ e.stopPropagation();
+ e.preventDefault();
+}
+
+function getEventCommand(e)
+{
+ let where = Services.prefs.getIntPref("browser.link.open_newwindow");
+ if ((where != 3) && ((e.which == 2) || e.ctrlKey) &&
+ Services.prefs.getBoolPref("browser.tabs.opentabfor.middleclick", true))
+ where = 3;
+
+ if (where == 2)
+ return "goto-url-newwin";
+ if (where == 3)
+ return "goto-url-newtab";
+ return "goto-url";
+}
+
+function onMouseOver (e)
+{
+ var i = 0;
+ var target = e.target;
+ var status = "";
+ while (!status && target && i < 5)
+ {
+ if ("getAttribute" in target)
+ {
+ status = target.getAttribute("href");
+ if (!status)
+ status = target.getAttribute("status-text");
+ }
+ ++i;
+ target = target.parentNode;
+ }
+
+ // Setting client.status to "" will revert it to the default automatically.
+ client.status = status;
+}
+
+function onMultilineInputKeyPress (e)
+{
+ if ((e.ctrlKey || e.metaKey) && e.keyCode == 13)
+ {
+ /* meta-enter, execute buffer */
+ onMultilineSend(e);
+ }
+ else
+ {
+ if ((e.ctrlKey || e.metaKey) && e.keyCode == 40)
+ {
+ /* ctrl/meta-down, switch to single line mode */
+ dispatch ("pref multiline false");
+ }
+ }
+}
+
+function onMultilineSend(e)
+{
+ var multiline = document.getElementById("multiline-input");
+ e.line = multiline.value;
+ if (e.line.search(/\S/) == -1)
+ return;
+ onInputCompleteLine (e);
+ multiline.value = "";
+ if (("multiLineForPaste" in client) && client.multiLineForPaste)
+ client.prefs["multiline"] = false;
+}
+
+function onTooltip(event)
+{
+ const XLinkNS = "http://www.w3.org/1999/xlink";
+
+ var tipNode = event.originalTarget;
+ var titleText = null;
+ var XLinkTitleText = null;
+
+ var element = document.tooltipNode;
+ while (element && (element != document.documentElement))
+ {
+ if (element.nodeType == Node.ELEMENT_NODE)
+ {
+ var text;
+ if (element.hasAttribute("title"))
+ text = element.getAttribute("title");
+ else if (element.hasAttributeNS(XLinkNS, "title"))
+ text = element.getAttributeNS(XLinkNS, "title");
+
+ if (text)
+ {
+ tipNode.setAttribute("label", text);
+ return true;
+ }
+ }
+
+ element = element.parentNode;
+ }
+
+ return false;
+}
+
+function onInputKeyPress (e)
+{
+ if (client.prefs["outgoing.colorCodes"])
+ setTimeout(onInputKeyPressCallback, 100, e.target);
+
+ switch (e.keyCode)
+ {
+ case 9: /* tab */
+ if (!e.ctrlKey && !e.metaKey)
+ {
+ onTabCompleteRequest(e);
+ e.preventDefault();
+ }
+ return;
+
+ case 77: /* Hackaround for carbon on mac sending us this instead of 13
+ * for ctrl+enter. 77 = "M", and ctrl+M was originally used
+ * to send a CR in a terminal. */
+ // Fallthrough if ctrl was pressed, otherwise break out to default:
+ if (!e.ctrlKey)
+ break;
+
+ case 13: /* CR */
+ e.line = e.target.value;
+ e.target.value = "";
+ if (e.line.search(/\S/) == -1)
+ return;
+ if (e.ctrlKey)
+ e.line = client.COMMAND_CHAR + "say " + e.line;
+ onInputCompleteLine (e);
+ return;
+
+ case 37: /* left */
+ if (e.altKey && e.metaKey)
+ cycleView(-1);
+ return;
+
+ case 38: /* up */
+ if (e.ctrlKey || e.metaKey)
+ {
+ /* ctrl/meta-up, switch to multi line mode */
+ dispatch ("pref multiline true");
+ }
+ else
+ {
+ if (client.lastHistoryReferenced == -2)
+ {
+ client.lastHistoryReferenced = -1;
+ e.target.value = client.incompleteLine;
+ }
+ else if (client.lastHistoryReferenced <
+ client.inputHistory.length - 1)
+ {
+ e.target.value =
+ client.inputHistory[++client.lastHistoryReferenced];
+ }
+ }
+ e.preventDefault();
+ return;
+
+ case 39: /* right */
+ if (e.altKey && e.metaKey)
+ cycleView(+1);
+ return;
+
+ case 40: /* down */
+ if (client.lastHistoryReferenced > 0)
+ e.target.value =
+ client.inputHistory[--client.lastHistoryReferenced];
+ else if (client.lastHistoryReferenced == -1)
+ {
+ e.target.value = "";
+ client.lastHistoryReferenced = -2;
+ }
+ else
+ {
+ client.lastHistoryReferenced = -1;
+ e.target.value = client.incompleteLine;
+ }
+ e.preventDefault();
+ return;
+ }
+ client.lastHistoryReferenced = -1;
+ client.incompleteLine = e.target.value;
+}
+
+function onTabCompleteRequest (e)
+{
+ var elem = document.commandDispatcher.focusedElement;
+ var singleInput = document.getElementById("input");
+ if (document.getBindingParent(elem) != singleInput)
+ return;
+
+ var selStart = singleInput.selectionStart;
+ var selEnd = singleInput.selectionEnd;
+ var line = singleInput.value;
+
+ if (!line)
+ {
+ if ("defaultCompletion" in client.currentObject)
+ singleInput.value = client.currentObject.defaultCompletion;
+ // If there was nothing to complete, help the user:
+ if (!singleInput.value)
+ display(MSG_LEAVE_INPUTBOX, MT_INFO);
+ return;
+ }
+
+ if (selStart != selEnd)
+ {
+ /* text is highlighted, just move caret to end and exit */
+ singleInput.selectionStart = singleInput.selectionEnd = line.length;
+ return;
+ }
+
+ var wordStart = line.substr(0, selStart).search(/\s\S*$/);
+ if (wordStart == -1)
+ wordStart = 0;
+ else
+ ++wordStart;
+
+ var wordEnd = line.substr(selStart).search(/\s/);
+ if (wordEnd == -1)
+ wordEnd = line.length;
+ else
+ wordEnd += selStart;
+
+ // Double tab on nothing, inform user how to get out of the input box
+ if (wordEnd == wordStart)
+ {
+ display(MSG_LEAVE_INPUTBOX, MT_INFO);
+ return;
+ }
+
+ if ("performTabMatch" in client.currentObject)
+ {
+ var word = line.substring(wordStart, wordEnd);
+ var wordLower = word.toLowerCase();
+ var d = getObjectDetails(client.currentObject);
+ if (d.server)
+ wordLower = d.server.toLowerCase(word);
+
+ var co = client.currentObject;
+
+ // We need some special knowledge of how to lower-case strings.
+ var lcFn;
+ if ("getLCFunction" in co)
+ lcFn = co.getLCFunction();
+
+ var matches = co.performTabMatch(line, wordStart, wordEnd, wordLower,
+ selStart, lcFn);
+ /* if we get null back, we're supposed to fail silently */
+ if (!matches)
+ return;
+
+ var doubleTab = false;
+ var date = new Date();
+ if ((date - client.lastTabUp) <= client.DOUBLETAB_TIME)
+ doubleTab = true;
+ else
+ client.lastTabUp = date;
+
+ if (doubleTab)
+ {
+ /* if the user hit tab twice quickly, */
+ if (matches.length > 0)
+ {
+ /* then list possible completions, */
+ display(getMsg(MSG_FMT_MATCHLIST,
+ [matches.length, word,
+ matches.sort().join(", ")]));
+ }
+ else
+ {
+ /* or display an error if there are none. */
+ display(getMsg(MSG_ERR_NO_MATCH, word), MT_ERROR);
+ }
+ }
+ else if (matches.length >= 1)
+ {
+ var match;
+ if (matches.length == 1)
+ match = matches[0];
+ else
+ match = getCommonPfx(matches, lcFn);
+ singleInput.value = line.substr(0, wordStart) + match +
+ line.substr(wordEnd);
+ if (wordEnd < line.length)
+ {
+ /* if the word we completed was in the middle if the line
+ * then move the cursor to the end of the completed word. */
+ var newpos = wordStart + match.length;
+ if (matches.length == 1)
+ {
+ /* word was fully completed, move one additional space */
+ ++newpos;
+ }
+ singleInput.selectionEnd = e.target.selectionStart = newpos;
+ }
+ }
+ }
+
+}
+
+function onWindowKeyPress(e)
+{
+ var code = Number(e.keyCode);
+ var w;
+ var newOfs;
+ var userList = document.getElementById("user-list");
+ var elemFocused = document.commandDispatcher.focusedElement;
+
+ const isMac = client.platform == "Mac";
+ const isLinux = client.platform == "Linux";
+ const isWindows = client.platform == "Windows";
+ const isOS2 = client.platform == "OS/2";
+ const isUnknown = !(isMac || isLinux || isWindows || isOS2);
+
+ switch (code)
+ {
+ case 9: /* Tab */
+ // Control-Tab => next tab (all platforms)
+ // Control-Shift-Tab => previous tab (all platforms)
+ if (e.ctrlKey && !e.altKey && !e.metaKey)
+ {
+ cycleView(e.shiftKey ? -1: 1);
+ e.preventDefault();
+ }
+ break;
+
+ case 33: /* Page Up */
+ case 34: /* Page Down */
+ // Control-Page Up => previous tab (all platforms)
+ // Control-Page Down => next tab (all platforms)
+ if ((e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) ||
+ (e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey))
+ {
+ cycleView(2 * code - 67);
+ e.preventDefault();
+ break;
+ }
+
+ if (!e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey &&
+ (elemFocused != userList))
+ {
+ w = client.currentFrame;
+ newOfs = w.pageYOffset + (w.innerHeight * 0.75) *
+ (2 * code - 67);
+ if (newOfs > 0)
+ w.scrollTo(w.pageXOffset, newOfs);
+ else
+ w.scrollTo(w.pageXOffset, 0);
+ e.preventDefault();
+ }
+ break;
+
+ case 37: /* Left Arrow */
+ case 39: /* Right Arrow */
+ // Command-Alt-Left Arrow => previous tab (Mac only)
+ // Command-Alt-Right Arrow => next tab (Mac only)
+ if (isMac && e.metaKey && e.altKey && !e.ctrlKey && !e.shiftKey)
+ {
+ cycleView(code - 38);
+ e.preventDefault();
+ }
+ break;
+
+ case 219: /* [ */
+ case 221: /* ] */
+ // Command-Shift-[ => previous tab (Mac only)
+ // Command-Shift-] => next tab (Mac only)
+ if (isMac && e.metaKey && e.shiftKey && !e.altKey && !e.ctrlKey)
+ {
+ cycleView(code - 220);
+ e.preventDefault();
+ }
+ break;
+
+ case 117: /* F6 */
+ // F6 => focus next (all platforms)
+ // Shift-F6 => focus previous (all platforms)
+ if (!e.altKey && !e.ctrlKey && !e.metaKey)
+ {
+ advanceKeyboardFocus(e.shiftKey ? -1 : 1);
+ e.preventDefault();
+ }
+ break;
+ }
+
+ // Code is zero if we have a typeable character triggering the event.
+ if (code != 0)
+ return;
+
+ // OS X only: Command-{ and Command-}
+ // Newer geckos seem to only provide these keys in charCode, not keyCode
+ if (isMac && e.metaKey && e.shiftKey && !e.altKey && !e.ctrlKey)
+ {
+ if (e.charCode == 123 || e.charCode == 125)
+ {
+ cycleView(e.charCode - 124);
+ e.preventDefault();
+ return;
+ }
+ }
+
+ // Numeric shortcuts
+
+ // The following code is copied from:
+ // /mozilla/browser/base/content/browser.js
+ // Revision: 1.748
+ // Lines: 1397-1421
+
+ // \d in a RegExp will find any Unicode character with the "decimal digit"
+ // property (Nd)
+ var regExp = /\d/;
+ if (!regExp.test(String.fromCharCode(e.charCode)))
+ return;
+
+ // Some Unicode decimal digits are in the range U+xxx0 - U+xxx9 and some are
+ // in the range U+xxx6 - U+xxxF. Find the digit 1 corresponding to our
+ // character.
+ var digit1 = (e.charCode & 0xFFF0) | 1;
+ if (!regExp.test(String.fromCharCode(digit1)))
+ digit1 += 6;
+
+ var idx = e.charCode - digit1;
+
+ if ((0 <= idx) && (idx <= 8))
+ {
+ var modifier = (e.altKey ? 0x1 : 0) |
+ (e.ctrlKey ? 0x2 : 0) |
+ (e.shiftKey ? 0x4 : 0) |
+ (e.metaKey ? 0x8 : 0);
+
+ var modifierMask;
+ if (client.prefs["tabGotoKeyModifiers"])
+ modifierMask = client.prefs["tabGotoKeyModifiers"];
+ else
+ modifierMask = 0x1; // alt
+
+ if ((modifier & modifierMask) == modifierMask)
+ {
+ // Pressing 1-8 takes you to that tab, while pressing 9 takes you
+ // to the last tab always.
+ if (idx == 8)
+ idx = client.viewsArray.length - 1;
+
+ if ((idx in client.viewsArray) && client.viewsArray[idx].source)
+ {
+ var newView = client.viewsArray[idx].source;
+ dispatch("set-current-view", { view: newView });
+ }
+ e.preventDefault();
+ return;
+ }
+ }
+}
+
+function onWindowFocus(e)
+{
+ window.isFocused = true;
+}
+
+function onWindowBlue(e)
+{
+ window.isFocused = false;
+
+ // If we're tracking last read lines, set a mark on the current view
+ // when losing focus.
+ if (client.currentObject && client.currentObject.prefs["autoMarker"])
+ client.currentObject.dispatch("marker-set");
+}
+
+function onInputCompleteLine(e)
+{
+ if (!client.inputHistory.length || client.inputHistory[0] != e.line)
+ {
+ client.inputHistory.unshift(e.line);
+ if (client.inputHistoryLogger)
+ client.inputHistoryLogger.append(e.line);
+ }
+
+ if (client.inputHistory.length > client.MAX_HISTORY)
+ client.inputHistory.pop();
+
+ client.lastHistoryReferenced = -1;
+ client.incompleteLine = "";
+
+ if (e.line[0] == client.COMMAND_CHAR)
+ {
+ if (client.prefs["outgoing.colorCodes"])
+ e.line = replaceColorCodes(e.line);
+ dispatch(e.line.substr(1), null, true);
+ }
+ else /* plain text */
+ {
+ /* color codes */
+ if (client.prefs["outgoing.colorCodes"])
+ e.line = replaceColorCodes(e.line);
+ client.sayToCurrentTarget(e.line, true);
+ }
+}
+
+function onNotifyTimeout()
+{
+ for (var n in client.networks)
+ {
+ var net = client.networks[n];
+ if (net.isConnected()) {
+ if ((net.prefs["notifyList"].length > 0) &&
+ (!net.primServ.supports["monitor"])) {
+ let isonList = net.prefs["notifyList"];
+ net.primServ.sendData ("ISON " + isonList.join(" ") + "\n");
+ } else {
+ /* Just send a ping to see if we're alive. */
+ net.primServ.sendData ("PING :ALIVECHECK\n");
+ }
+ }
+ }
+}
+
+function onWhoTimeout()
+{
+ function checkWho()
+ {
+ var checkNext = (net.lastWhoCheckChannel == null);
+ for (var c in net.primServ.channels)
+ {
+ var chan = net.primServ.channels[c];
+
+ if (checkNext && chan.active &&
+ chan.getUsersLength() < client.prefs["autoAwayCap"])
+ {
+ net.primServ.LIGHTWEIGHT_WHO = true;
+ net.primServ.who(chan.unicodeName);
+ net.lastWhoCheckChannel = chan;
+ net.lastWhoCheckTime = Number(new Date());
+ return;
+ }
+
+ if (chan == net.lastWhoCheckChannel)
+ checkNext = true;
+ }
+ if (net.lastWhoCheckChannel)
+ {
+ net.lastWhoCheckChannel = null;
+ checkWho();
+ }
+ };
+
+ for (var n in client.networks)
+ {
+ var net = client.networks[n];
+ var period = net.prefs["autoAwayPeriod"];
+ // The time since the last check, with a 5s error margin to
+ // stop us from not checking because the timer fired a tad early:
+ var waited = Number(new Date()) - net.lastWhoCheckTime + 5000;
+ if (net.isConnected() && (period != 0) && (period * 60000 < waited) &&
+ !net.primServ.caps["away-notify"])
+ checkWho();
+ }
+}
+
+function onInputKeyPressCallback (el)
+{
+ function doPopup(popup)
+ {
+ if (client.inputPopup && client.inputPopup != popup)
+ client.inputPopup.hidePopup();
+
+ client.inputPopup = popup;
+ if (popup)
+ {
+ if (el.nodeName == "textbox")
+ {
+ popup.showPopup(el, -1, -1, "tooltip", "topleft", "bottomleft");
+ }
+ else
+ {
+ var box = el.ownerDocument.getBoxObjectFor(el);
+ var pos = { x: client.mainWindow.screenX + box.screenX + 5,
+ y: client.mainWindow.screenY + box.screenY + box.height + 25 };
+ popup.moveTo(pos.x, pos.y);
+ popup.showPopup(el, 0, 0, "tooltip");
+ }
+ }
+ }
+
+ var text = " " + el.value.substr(0, el.selectionStart);
+ if (el.selectionStart != el.selectionEnd)
+ text = "";
+
+ if (text.match(/[^%]%C[0-9]{0,2},?[0-9]{0,2}$/))
+ doPopup(document.getElementById("colorTooltip"));
+ else if (text.match(/[^%]%$/))
+ doPopup(document.getElementById("percentTooltip"));
+ else
+ doPopup(null);
+}
+
+function onUserDoubleClick(event)
+{
+ if ((event.button != 0) ||
+ event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
+ {
+ return;
+ }
+ var userList = document.getElementById("user-list");
+ if (!userList.view || !userList.view.selection)
+ return;
+ var currentIndex = userList.view.selection.currentIndex;
+ if (currentIndex < 0)
+ return;
+ var nickname = getNicknameForUserlistRow(currentIndex);
+ dispatch("query", {nickname: nickname, source: "mouse"});
+}
+
+client.onFindEnd =
+CIRCNetwork.prototype.onFindEnd =
+CIRCChannel.prototype.onFindEnd =
+CIRCUser.prototype.onFindEnd =
+CIRCDCCChat.prototype.onFindEnd =
+CIRCDCCFileTransfer.prototype.onFindEnd =
+function this_onfindend(e)
+{
+ this.scrollToElement("selection", "inview");
+}
+
+CIRCChannel.prototype._updateConferenceMode =
+function my_updateconfmode()
+{
+ const minDiff = client.CONFERENCE_LOW_PASS;
+
+ var enabled = this.prefs["conference.enabled"];
+ var userLimit = this.prefs["conference.limit"];
+ var userCount = this.getUsersLength();
+
+ if (userLimit == 0)
+ {
+ // userLimit == 0 --> always off.
+ if (enabled)
+ this.prefs["conference.enabled"] = false;
+ }
+ else if (userLimit == 1)
+ {
+ // userLimit == 1 --> always on.
+ if (!enabled)
+ this.prefs["conference.enabled"] = true;
+ }
+ else if (enabled && (userCount < userLimit - minDiff))
+ {
+ this.prefs["conference.enabled"] = false;
+ }
+ else if (!enabled && (userCount > userLimit + minDiff))
+ {
+ this.prefs["conference.enabled"] = true;
+ }
+}
+
+CIRCServer.prototype.CTCPHelpClientinfo =
+function serv_ccinfohelp()
+{
+ return MSG_CTCPHELP_CLIENTINFO;
+}
+
+CIRCServer.prototype.CTCPHelpAction =
+function serv_ccinfohelp()
+{
+ return MSG_CTCPHELP_ACTION;
+}
+
+CIRCServer.prototype.CTCPHelpTime =
+function serv_ccinfohelp()
+{
+ return MSG_CTCPHELP_TIME;
+}
+
+CIRCServer.prototype.CTCPHelpVersion =
+function serv_ccinfohelp()
+{
+ return MSG_CTCPHELP_VERSION;
+}
+
+CIRCServer.prototype.CTCPHelpSource =
+function serv_csrchelp()
+{
+ return MSG_CTCPHELP_SOURCE;
+}
+
+CIRCServer.prototype.CTCPHelpOs =
+function serv_oshelp()
+{
+ return MSG_CTCPHELP_OS;
+}
+
+CIRCServer.prototype.CTCPHelpHost =
+function serv_hosthelp()
+{
+ return MSG_CTCPHELP_HOST;
+}
+
+CIRCServer.prototype.CTCPHelpPing =
+function serv_ccinfohelp()
+{
+ return MSG_CTCPHELP_PING;
+}
+
+CIRCServer.prototype.CTCPHelpDcc =
+function serv_ccinfohelp()
+{
+ return MSG_CTCPHELP_DCC;
+}
+
+/**
+ * Calculates delay before the next automatic connection attempt.
+ *
+ * If the number of connection attempts is limited, use fixed interval
+ * MIN_RECONNECT_MS. For unlimited attempts (-1), use exponential backoff: the
+ * interval between connection attempts to the network (not individual
+ * servers) is doubled after each attempt, up to MAX_RECONNECT_MS.
+ */
+CIRCNetwork.prototype.getReconnectDelayMs =
+function my_getReconnectDelayMs()
+{
+ var nServers = this.serverList.length;
+
+ if ((-1 != this.MAX_CONNECT_ATTEMPTS) ||
+ (0 != this.connectCandidate % nServers))
+ {
+ return this.MIN_RECONNECT_MS;
+ }
+
+ var networkRound = Math.ceil(this.connectCandidate / nServers);
+
+ var rv = this.MIN_RECONNECT_MS * Math.pow(2, networkRound - 1);
+
+ // clamp rv between MIN/MAX_RECONNECT_MS
+ rv = Math.min(Math.max(rv, this.MIN_RECONNECT_MS), this.MAX_RECONNECT_MS);
+
+ return rv;
+}
+
+CIRCNetwork.prototype.onInit =
+function net_oninit ()
+{
+ this.logFile = null;
+ this.lastServer = null;
+}
+
+CIRCNetwork.prototype.onInfo =
+function my_netinfo (e)
+{
+ this.display(e.msg, "INFO", undefined, undefined, e.tags);
+}
+
+CIRCNetwork.prototype.onUnknown =
+function my_unknown (e)
+{
+ if ("pendingWhoisLines" in e.server)
+ {
+ /* whois lines always have the nick in param 2 */
+ e.user = new CIRCUser(e.server, null, e.params[2]);
+
+ e.destMethod = "onUnknownWhois";
+ e.destObject = this;
+ return;
+ }
+
+ e.params.shift(); /* remove the code */
+ e.params.shift(); /* and the dest. nick (always me) */
+
+ // Handle random IRC numerics automatically.
+ var msg = getMsg("msg.irc." + e.code, null, "");
+ if (msg)
+ {
+ if (arrayIndexOf(e.server.channelTypes, e.params[0][0]) != -1)
+ {
+ // Message about a channel (e.g. join failed).
+ e.channel = new CIRCChannel(e.server, null, e.params[0]);
+ }
+
+ var targetDisplayObj = this;
+ if (e.channel && ("messages" in e.channel))
+ targetDisplayObj = e.channel;
+
+ // Check for /knock support for the +i message.
+ if (((e.code == 471) || (e.code == 473) || (e.code == 475)) &&
+ ("knock" in e.server.servCmds))
+ {
+ var args = [msg, e.channel.unicodeName,
+ "knock " + e.channel.unicodeName];
+ msg = getMsg("msg.irc." + e.code + ".knock", args, "");
+ client.munger.getRule(".inline-buttons").enabled = true;
+ targetDisplayObj.display(msg, undefined, undefined, undefined,
+ e.tags);
+ client.munger.getRule(".inline-buttons").enabled = false;
+ }
+ else
+ {
+ targetDisplayObj.display(msg, undefined, undefined, undefined,
+ e.tags);
+ }
+
+ if (e.channel)
+ {
+ if (e.channel.busy)
+ {
+ e.channel.busy = false;
+ updateProgress();
+ }
+ }
+ else
+ {
+ // Network type error?
+ if (this.busy)
+ {
+ this.busy = false;
+ updateProgress();
+ }
+ }
+ return;
+ }
+
+ /* if it looks like some kind of "end of foo" code, and we don't
+ * already have a mapping for it, make one up */
+ var length = e.params.length;
+ if (!(e.code in client.responseCodeMap) &&
+ (e.params[length - 1].search (/^end of/i) != -1))
+ {
+ client.responseCodeMap[e.code] = "---";
+ }
+
+ this.display(toUnicode(e.params.join(" "), this), e.code.toUpperCase(),
+ undefined, undefined, e.tags);
+}
+
+CIRCNetwork.prototype.lastWhoCheckChannel = null;
+CIRCNetwork.prototype.lastWhoCheckTime = 0;
+CIRCNetwork.prototype.on001 = /* Welcome! */
+CIRCNetwork.prototype.on002 = /* your host is */
+CIRCNetwork.prototype.on003 = /* server born-on date */
+CIRCNetwork.prototype.on004 = /* server id */
+CIRCNetwork.prototype.on005 = /* server features */
+CIRCNetwork.prototype.on250 = /* highest connection count */
+CIRCNetwork.prototype.on251 = /* users */
+CIRCNetwork.prototype.on252 = /* opers online (in params[2]) */
+CIRCNetwork.prototype.on254 = /* channels found (in params[2]) */
+CIRCNetwork.prototype.on255 = /* link info */
+CIRCNetwork.prototype.on265 = /* local user details */
+CIRCNetwork.prototype.on266 = /* global user details */
+CIRCNetwork.prototype.on375 = /* start of MOTD */
+CIRCNetwork.prototype.on372 = /* MOTD line */
+CIRCNetwork.prototype.on376 = /* end of MOTD */
+CIRCNetwork.prototype.on422 = /* no MOTD */
+CIRCNetwork.prototype.on670 = /* STARTTLS Success */
+CIRCNetwork.prototype.on691 = /* STARTTLS Failure */
+CIRCNetwork.prototype.on902 = /* SASL Nick locked */
+CIRCNetwork.prototype.on903 = /* SASL Auth success */
+CIRCNetwork.prototype.on904 = /* SASL Auth failed */
+CIRCNetwork.prototype.on905 = /* SASL Command too long */
+CIRCNetwork.prototype.on906 = /* SASL Aborted */
+CIRCNetwork.prototype.on907 = /* SASL Already authenticated */
+CIRCNetwork.prototype.on908 = /* SASL Mechanisms */
+function my_showtonet (e)
+{
+ var p = (3 in e.params) ? e.params[2] + " " : "";
+ var str = "";
+
+ switch (e.code)
+ {
+ case "004":
+ case "005":
+ str = e.params.slice(3).join(" ");
+ break;
+
+ case "001":
+ // Code moved to lower down to speed this bit up. :)
+ var c, u;
+ // If we've switched servers, *first* we must rehome our objects.
+ if (this.lastServer && (this.lastServer != this.primServ))
+ {
+ for (c in this.lastServer.channels)
+ this.lastServer.channels[c].rehome(this.primServ);
+ for (u in this.lastServer.users)
+ this.lastServer.users[u].rehome(this.primServ);
+
+ // This makes sure we have the *right* me object.
+ this.primServ.me.rehome(this.primServ);
+ }
+
+ // Update the list of ignored users from the prefs:
+ var ignoreAry = this.prefs["ignoreList"];
+ for (var j = 0; j < ignoreAry.length; ++j)
+ this.ignoreList[ignoreAry[j]] = getHostmaskParts(ignoreAry[j]);
+
+ // Update everything.
+ // Welcome to history.
+ addURLToHistory(this.getURL());
+ updateTitle(this);
+ this.updateHeader();
+ client.updateHeader();
+ updateSecurityIcon();
+ updateStalkExpression(this);
+
+ client.ident.removeNetwork(this);
+
+ // Figure out what nick we *really* want:
+ if (this.prefs["away"] && this.prefs["awayNick"])
+ this.preferredNick = this.prefs["awayNick"];
+ else
+ this.preferredNick = this.prefs["nickname"];
+
+ // Pretend this never happened.
+ delete this.pendingNickChange;
+
+ str = e.decodeParam(2);
+
+ break;
+
+ case "251": /* users */
+ this.doAutoPerform();
+
+ // Set our initial monitor list
+ if ((this.primServ.supports["monitor"]) &&
+ (this.prefs["notifyList"].length > 0))
+ {
+ this.primServ.sendMonitorList(this.prefs["notifyList"], true);
+ }
+
+ this.isIdleAway = client.isIdleAway;
+ if (this.prefs["away"])
+ this.dispatch("away", { reason: this.prefs["away"] });
+
+ if (this.lastServer)
+ {
+ // Re-join channels from previous connection.
+ for (c in this.primServ.channels)
+ {
+ var chan = this.primServ.channels[c];
+ if (chan.joined)
+ chan.join(chan.mode.key);
+ }
+ }
+ this.lastServer = this.primServ;
+
+ if ("pendingURLs" in this)
+ {
+ var target = this.pendingURLs.pop();
+ while (target)
+ {
+ gotoIRCURL(target.url, target.e);
+ target = this.pendingURLs.pop();
+ }
+ delete this.pendingURLs;
+ }
+
+ // Do this after the JOINs, so they are quicker.
+ // This is not time-critical code.
+ if (client.prefs["dcc.enabled"] && this.prefs["dcc.useServerIP"])
+ {
+ var delayFn = function(t) {
+ // This is the quickest way to get out host/IP.
+ t.pendingUserhostReply = true;
+ t.primServ.sendData("USERHOST " +
+ t.primServ.me.encodedName + "\n");
+ };
+ setTimeout(delayFn, 1000 * Math.random(), this);
+ }
+
+ // Had some collision during connect.
+ if (this.primServ.me.unicodeName != this.preferredNick)
+ {
+ this.reclaimLeft = this.RECLAIM_TIMEOUT;
+ this.reclaimName();
+ }
+
+ if ("onLogin" in this)
+ {
+ ev = new CEvent("network", "login", this, "onLogin");
+ client.eventPump.addEvent(ev);
+ }
+
+ str = e.decodeParam(e.params.length - 1);
+ break;
+
+ case "376": /* end of MOTD */
+ case "422": /* no MOTD */
+ this.busy = false;
+ updateProgress();
+
+ /* Some servers (wrongly) dont send 251, so try
+ auto-perform after the MOTD as well */
+ this.doAutoPerform();
+ /* no break */
+
+ case "372":
+ case "375":
+ case "376":
+ if (this.IGNORE_MOTD)
+ return;
+ /* no break */
+
+ default:
+ str = e.decodeParam(e.params.length - 1);
+ break;
+ }
+
+ this.displayHere(p + str, e.code.toUpperCase(), undefined, undefined,
+ e.tags);
+}
+
+CIRCNetwork.prototype.onUnknownCTCPReply =
+function my_ctcprunk (e)
+{
+ this.display(getMsg(MSG_FMT_CTCPREPLY,
+ [toUnicode(e.CTCPCode, this),
+ toUnicode(e.CTCPData, this), e.user.unicodeName]),
+ "CTCP_REPLY", e.user, e.server.me, e.tags);
+}
+
+CIRCNetwork.prototype.onNotice =
+function my_notice(e)
+{
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ this.display(e.decodeParam(2), "NOTICE", this, e.server.me, e.tags);
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+CIRCNetwork.prototype.onPrivmsg =
+function my_privmsg(e)
+{
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ this.display(e.decodeParam(2), "PRIVMSG", this, e.server.me, e.tags);
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+/* userhost reply */
+CIRCNetwork.prototype.on302 =
+function my_302(e)
+{
+ if (client.prefs["dcc.enabled"] && this.prefs["dcc.useServerIP"] &&
+ ("pendingUserhostReply" in this))
+ {
+ var me = new RegExp("^" + this.primServ.me.encodedName + "\\*?=", "i");
+ if (e.params[2].match(me))
+ client.dcc.addHost(this.primServ.me.host, true);
+
+ delete this.pendingUserhostReply;
+ return true;
+ }
+
+ e.destMethod = "onUnknown";
+ e.destObject = this;
+
+ return true;
+}
+
+CIRCNetwork.prototype.on303 = /* ISON (aka notify) reply */
+function my_303 (e)
+{
+ function lower(text)
+ {
+ return e.server.toLowerCase(text);
+ };
+
+ var onList = new Array();
+ // split() gives an array of one item ("") when splitting "", which we
+ // don't want, so only do the split if there's something to split.
+ if (e.params[2])
+ onList = stringTrim(e.server.toLowerCase(e.params[2])).split(/\s+/);
+ var offList = new Array();
+ var newArrivals = new Array();
+ var newDepartures = new Array();
+ var o = getObjectDetails(client.currentObject);
+ var displayTab;
+ var i;
+
+ if ("network" in o && o.network == this && client.currentObject != this)
+ displayTab = client.currentObject;
+
+ for (i = 0; i < this.prefs["notifyList"].length; i++)
+ {
+ if (!arrayContains(onList, lower(this.prefs["notifyList"][i])))
+ /* user is not on */
+ offList.push(lower(this.prefs["notifyList"][i]));
+ }
+
+ if ("onList" in this)
+ {
+ for (i in onList)
+ if (!arrayContains(this.onList, onList[i]))
+ /* we didn't know this person was on */
+ newArrivals.push(onList[i]);
+ }
+ else
+ this.onList = newArrivals = onList;
+
+ if ("offList" in this)
+ {
+ for (i in offList)
+ if (!arrayContains(this.offList, offList[i]))
+ /* we didn't know this person was off */
+ newDepartures.push(offList[i]);
+ }
+ else
+ this.offList = newDepartures = offList;
+
+ if (newArrivals.length > 0)
+ {
+ this.displayHere (arraySpeak (newArrivals, "is", "are") +
+ " online.", "NOTIFY-ON", undefined, undefined,
+ e.tags);
+ if (displayTab)
+ displayTab.displayHere (arraySpeak (newArrivals, "is", "are") +
+ " online.", "NOTIFY-ON", undefined,
+ undefined, e.tags);
+ }
+
+ if (newDepartures.length > 0)
+ {
+ this.displayHere (arraySpeak (newDepartures, "is", "are") +
+ " offline.", "NOTIFY-OFF", undefined, undefined,
+ e.tags);
+ if (displayTab)
+ displayTab.displayHere (arraySpeak (newDepartures, "is", "are") +
+ " offline.", "NOTIFY-OFF", undefined,
+ undefined, e.tags);
+ }
+
+ this.onList = onList;
+ this.offList = offList;
+
+}
+
+CIRCNetwork.prototype.on730 = /* RPL_MONONLINE */
+CIRCNetwork.prototype.on731 = /* RPL_MONOFFLINE */
+function my_monnotice(e)
+{
+ var userList = e.params[2].split(",");
+ var nickList = [];
+ var o = getObjectDetails(client.currentObject);
+ var displayTab;
+ var i;
+ var msg;
+
+ if ("network" in o && o.network == this && client.currentObject != this)
+ displayTab = client.currentObject;
+
+ for (i = 0; i < userList.length; i++)
+ {
+ var nick = e.server.toLowerCase(userList[i].split("!")[0]);
+
+ // Make sure this nick is in the notify list.
+ if (this.prefs["notifyList"].indexOf(nick) < 0)
+ {
+ this.prefs["notifyList"].push(nick);
+ this.prefs["notifyList"].update();
+ }
+ nickList.push(nick);
+ }
+
+ if (e.code == "730") // RPL_MONONLINE
+ msg = arraySpeak (nickList, "is", "are") + " online.";
+ else // RPL_MONOFFLINE
+ msg = arraySpeak (nickList, "is", "are") + " offline.";
+ this.displayHere(msg, e.code, undefined, undefined, e.tags);
+ if (displayTab)
+ displayTab.displayHere(msg, e.code, undefined, undefined, e.tags);
+}
+
+CIRCNetwork.prototype.on732 = /* RPL_MONLIST */
+function my_732(e)
+{
+ if (!this.pendingNotifyList)
+ this.pendingNotifyList = [];
+ var nickList = e.server.toLowerCase(e.params[2]).split(",")
+ this.pendingNotifyList = this.pendingNotifyList.concat(nickList);
+}
+
+CIRCNetwork.prototype.on733 = /* RPL_ENDOFMONLIST */
+function my_733(e)
+{
+ if (this.pendingNotifyList)
+ {
+ this.prefs["notifyList"] = this.pendingNotifyList;
+ this.prefs["notifyList"].update();
+ this.display(getMsg(MSG_NOTIFY_LIST, arraySpeak(this.pendingNotifyList)));
+ delete this.pendingNotifyList;
+ if (e.params[2])
+ this.display(e.params[2], e.code, undefined, undefined, e.tags);
+ }
+ else
+ {
+ this.prefs["notifyList"] = [];
+ this.prefs["notifyList"].update();
+ display(MSG_NO_NOTIFY_LIST, e.code, undefined, undefined, e.tags);
+ }
+}
+
+CIRCNetwork.prototype.on734 = /* ERR_MONLISTFULL */
+function my_734(e)
+{
+ var nickList = e.server.toLowerCase(e.params[3]).split(",")
+ var i;
+ var msgname;
+
+ for (i = 0; i < nickList.length; i++)
+ {
+ var j = this.prefs["notifyList"].indexOf(nickList[i]);
+ if (j >= 0)
+ arrayRemoveAt(this.prefs["notifyList"], j);
+ }
+ this.prefs["notifyList"].update();
+
+ if (e.params[4])
+ this.display(e.params[4], e.code, undefined, undefined, e.tags)
+ else
+ this.display(MSG_NOTIFY_FULL);
+
+ msgname = (nickList.length == 1) ? MSG_NOTIFY_DELONE :
+ MSG_NOTIFY_DELSOME;
+ this.display(getMsg(msgname, arraySpeak(nickList)));
+}
+
+/* away off reply */
+CIRCNetwork.prototype.on305 =
+function my_305(e)
+{
+ this.display(MSG_AWAY_OFF, e.code, undefined, undefined, e.tags);
+
+ return true;
+}
+
+/* away on reply */
+CIRCNetwork.prototype.on306 =
+function my_306(e)
+{
+ var idleMsgParams = [this.prefs["away"], client.prefs["awayIdleTime"]];
+ if (!this.isIdleAway)
+ this.display(getMsg(MSG_AWAY_ON, this.prefs["away"]), e.code,
+ undefined, undefined, e.tags);
+ else
+ this.display(getMsg(MSG_IDLE_AWAY_ON, idleMsgParams), e.code,
+ undefined, undefined, e.tags);
+
+ return true;
+}
+
+
+CIRCNetwork.prototype.on263 = /* 'try again' */
+function my_263 (e)
+{
+ /* Urgh, this one's a pain. We need to abort whatever we tried, and start
+ * it again if appropriate.
+ *
+ * Known causes of this message:
+ * - LIST, with or without a parameter.
+ */
+
+ if (("_list" in this) && !this._list.done && (this._list.count == 0))
+ {
+ // We attempted a LIST, and we think it failed. :)
+ this._list.done = true;
+ this._list.error = e.decodeParam(2);
+ // Return early for this one if we're saving it.
+ if ("file" in this._list)
+ return true;
+ }
+
+ e.destMethod = "onUnknown";
+ e.destObject = this;
+ return true;
+}
+
+CIRCNetwork.prototype.isRunningList =
+function my_running_list()
+{
+ /* The list is considered "running" when a cancel is effective. This means
+ * that even if _list.done is true (finished recieving data), we will still
+ * be "running" whilst we have undisplayed items.
+ */
+ return (("_list" in this) &&
+ (!this._list.done || (this._list.length > this._list.displayed)) &&
+ !this._list.cancelled);
+}
+
+CIRCNetwork.prototype.list =
+function my_list(word, file)
+{
+ const NORMAL_FILE_TYPE = Components.interfaces.nsIFile.NORMAL_FILE_TYPE;
+
+ if (("_list" in this) && !this._list.done)
+ return false;
+
+ this._list = new Array();
+ this._list.string = word;
+ this._list.regexp = null;
+ this._list.done = false;
+ this._list.count = 0;
+ if (file)
+ {
+ var lfile = new LocalFile(file);
+ if (!lfile.localFile.exists())
+ {
+ // futils.umask may be 0022. Result is 0644.
+ lfile.localFile.create(NORMAL_FILE_TYPE, 0o666 & ~futils.umask);
+ }
+ this._list.file = new LocalFile(lfile.localFile, ">");
+ }
+
+ if (isinstance(word, RegExp))
+ {
+ this._list.regexp = word;
+ this._list.string = "";
+ word = "";
+ }
+
+ if (word)
+ this.primServ.sendData("LIST " + fromUnicode(word, this) + "\n");
+ else
+ this.primServ.sendData("LIST\n");
+
+ return true;
+}
+
+CIRCNetwork.prototype.listInit =
+function my_list_init ()
+{
+ function checkEndList (network)
+ {
+ if (network._list.count == network._list.lastLength)
+ {
+ network.on323();
+ }
+ else
+ {
+ network._list.lastLength = network._list.count;
+ network._list.endTimeout =
+ setTimeout(checkEndList, 5000, network);
+ }
+ }
+
+ function outputList (network)
+ {
+ const CHUNK_SIZE = 5;
+ var list = network._list;
+ if (list.cancelled)
+ {
+ if (list.done)
+ {
+ /* The server is no longer throwing stuff at us, so now
+ * we can safely kill the list.
+ */
+ network.display(getMsg(MSG_LIST_END,
+ [list.displayed, list.count]));
+ delete network._list;
+ }
+ else
+ {
+ /* We cancelled the list, but we're still getting data.
+ * Handle that data, but don't display, and do it more
+ * slowly, so we cause less lag.
+ */
+ setTimeout(outputList, 1000, network);
+ }
+ return;
+ }
+ if (list.length > list.displayed)
+ {
+ var start = list.displayed;
+ var end = list.length;
+ if (end - start > CHUNK_SIZE)
+ end = start + CHUNK_SIZE;
+ for (var i = start; i < end; ++i)
+ network.displayHere(getMsg(MSG_FMT_CHANLIST, list[i]), "322",
+ undefined, undefined, list[i][3]);
+ list.displayed = end;
+ }
+ if (list.done && (list.displayed == list.length))
+ {
+ if (list.event323)
+ {
+ var length = list.event323.params.length;
+ network.displayHere(list.event323.params[length - 1], "323",
+ undefined, undefined, list.event323.tags);
+ }
+ network.displayHere(getMsg(MSG_LIST_END,
+ [list.displayed, list.count]));
+ }
+ else
+ {
+ setTimeout(outputList, 250, network);
+ }
+ }
+
+ if (!("_list" in this))
+ {
+ this._list = new Array();
+ this._list.string = MSG_UNKNOWN;
+ this._list.regexp = null;
+ this._list.done = false;
+ this._list.count = 0;
+ }
+
+ if (!("file" in this._list))
+ {
+ this._list.displayed = 0;
+ if (client.currentObject != this)
+ display (getMsg(MSG_LIST_REROUTED, this.unicodeName));
+ setTimeout(outputList, 250, this);
+ }
+ this._list.lastLength = 0;
+ this._list.endTimeout = setTimeout(checkEndList, 5000, this);
+}
+
+CIRCNetwork.prototype.abortList =
+function my_abortList()
+{
+ this._list.cancelled = true;
+}
+
+CIRCNetwork.prototype.on321 = /* LIST reply header */
+function my_321 (e)
+{
+ this.listInit();
+
+ if (!("file" in this._list))
+ this.displayHere (e.params[2] + " " + e.params[3], "321");
+}
+
+CIRCNetwork.prototype.on323 = /* end of LIST reply */
+function my_323 (e)
+{
+ if (this._list.endTimeout)
+ {
+ clearTimeout(this._list.endTimeout);
+ delete this._list.endTimeout;
+ }
+ if (("file" in this._list))
+ this._list.file.close();
+
+ this._list.done = true;
+ this._list.event323 = e;
+}
+
+CIRCNetwork.prototype.on322 = /* LIST reply */
+function my_listrply (e)
+{
+ if (!("_list" in this) || !("lastLength" in this._list))
+ this.listInit();
+
+ ++this._list.count;
+
+ /* If the list has been cancelled, don't bother adding all this info
+ * anymore. Do increase the count (above), otherwise we never truly notice
+ * the list being finished.
+ */
+ if (this._list.cancelled)
+ return;
+
+ var chanName = e.decodeParam(2);
+ var topic = e.decodeParam(4);
+ if (!this._list.regexp || chanName.match(this._list.regexp) ||
+ topic.match(this._list.regexp))
+ {
+ if (!("file" in this._list))
+ {
+ this._list.push([chanName, e.params[3], topic, e.tags]);
+ }
+ else
+ {
+ this._list.file.write(fromUnicode(chanName, "UTF-8") + " " +
+ e.params[3] + " " +
+ fromUnicode(topic, "UTF-8") + "\n");
+ }
+ }
+}
+
+CIRCNetwork.prototype.on401 = /* ERR_NOSUCHNICK */
+CIRCNetwork.prototype.on402 = /* ERR_NOSUCHSERVER */
+CIRCNetwork.prototype.on403 = /* ERR_NOSUCHCHANNEL */
+function my_401(e)
+{
+ var server, channel, user;
+
+ /* Note that servers generally only send 401 and 402, sharing the former
+ * between nicknames and channels, but we're ready for anything.
+ */
+ if (e.code == 402)
+ server = e.decodeParam(2);
+ else if (arrayIndexOf(e.server.channelTypes, e.params[2][0]) != -1)
+ channel = new CIRCChannel(e.server, null, e.params[2]);
+ else
+ user = new CIRCUser(e.server, null, e.params[2]);
+
+ if (user && this.whoisList && (user.collectionKey in this.whoisList))
+ {
+ // If this is from a /whois, send a /whowas and don't display anything.
+ this.primServ.whowas(user.unicodeName, 1);
+ this.whoisList[user.collectionKey] = false;
+ return;
+ }
+
+ if (user)
+ user.display(getMsg(MSG_IRC_401, [user.unicodeName]), e.code,
+ undefined, undefined, e.tags);
+ else if (server)
+ this.display(getMsg(MSG_IRC_402, [server]), e.code,
+ undefined, undefined, e.tags);
+ else if (channel)
+ channel.display(getMsg(MSG_IRC_403, [channel.unicodeName]), e.code,
+ undefined, undefined, e.tags);
+ else
+ dd("on401: unreachable code.");
+}
+
+/* 464; "invalid or missing password", occurs as a reply to both OPER and
+ * sometimes initially during user registration. */
+CIRCNetwork.prototype.on464 =
+function my_464(e)
+{
+ if (this.state == NET_CONNECTING)
+ {
+ // If we are in the process of connecting we are needing a login
+ // password, subtly different from after user registration.
+ this.display(MSG_IRC_464_LOGIN, e.code, undefined, undefined, e.tags);
+ }
+ else
+ {
+ e.destMethod = "onUnknown";
+ e.destObject = this;
+ }
+}
+
+/* end of WHO */
+CIRCNetwork.prototype.on315 =
+function my_315 (e)
+{
+ var matches;
+ if ("whoMatches" in this)
+ matches = this.whoMatches;
+ else
+ matches = 0;
+
+ if ("pendingWhoReply" in this)
+ this.display(getMsg(MSG_WHO_END, [e.params[2], matches]), e.code,
+ undefined, undefined, e.tags);
+
+ if ("whoUpdates" in this)
+ {
+ var userlist = document.getElementById("user-list");
+ for (var c in this.whoUpdates)
+ {
+ for (var i = 0; i < this.whoUpdates[c].length; i++)
+ {
+ var index = this.whoUpdates[c][i].chanListEntry.childIndex;
+ userlist.treeBoxObject.invalidateRow(index);
+ }
+ this.primServ.channels[c].updateUsers(this.whoUpdates[c]);
+ }
+ delete this.whoUpdates;
+ }
+
+ delete this.pendingWhoReply;
+ delete this.whoMatches;
+}
+
+CIRCNetwork.prototype.on352 =
+function my_352 (e)
+{
+ //0-352 1-sender 2-channel 3-ident 4-host
+ //5-server 6-nick 7-H/G 8-hops and realname
+ if ("pendingWhoReply" in this)
+ {
+ var status;
+ if (e.user.isAway)
+ status = MSG_GONE;
+ else
+ status = MSG_HERE;
+
+ this.display(getMsg(MSG_WHO_MATCH,
+ [e.params[6], e.params[3], e.params[4],
+ e.user.desc, status, e.decodeParam(2),
+ e.params[5], e.user.hops]), e.code, e.user,
+ undefined, e.tags);
+ }
+
+ updateTitle(e.user);
+ if ("whoMatches" in this)
+ ++this.whoMatches;
+ else
+ this.whoMatches = 1;
+
+ if (!("whoUpdates" in this))
+ this.whoUpdates = new Object();
+
+ if (e.userHasChanges)
+ {
+ for (var c in e.server.channels)
+ {
+ var chan = e.server.channels[c];
+ if (chan.active && (e.user.collectionKey in chan.users))
+ {
+ if (!(c in this.whoUpdates))
+ this.whoUpdates[c] = new Array();
+ this.whoUpdates[c].push(chan.users[e.user.collectionKey]);
+ }
+ }
+ }
+}
+
+CIRCNetwork.prototype.on354 =
+function my_354(e)
+{
+ //0-352 1-sender 2-type 3-channel 4-ident 5-host
+ //6-server 7-nick 8-H/G 9-hops 10-account 11-realname
+ if ("pendingWhoReply" in this)
+ {
+ var status;
+ if (e.user.isAway)
+ status = MSG_GONE;
+ else
+ status = MSG_HERE;
+
+ this.display(getMsg(MSG_WHO_MATCH,
+ [e.params[7], e.params[4], e.params[5],
+ e.user.desc, status, e.decodeParam(3),
+ e.params[6], e.user.hops]), e.code, e.user,
+ undefined, e.tags);
+ }
+
+ updateTitle(e.user);
+ if ("whoMatches" in this)
+ ++this.whoMatches;
+ else
+ this.whoMatches = 1;
+
+ if (!("whoUpdates" in this))
+ this.whoUpdates = new Object();
+
+ if (e.userHasChanges)
+ {
+ for (var c in e.server.channels)
+ {
+ var chan = e.server.channels[c];
+ if (chan.active && (e.user.collectionKey in chan.users))
+ {
+ if (!(c in this.whoUpdates))
+ this.whoUpdates[c] = new Array();
+ this.whoUpdates[c].push(chan.users[e.user.collectionKey]);
+ }
+ }
+ }
+}
+
+CIRCNetwork.prototype.on301 = /* user away message */
+function my_301(e)
+{
+ if (e.user.awayMessage != e.user.lastShownAwayMessage)
+ {
+ var params = [e.user.unicodeName, e.user.awayMessage];
+ e.user.display(getMsg(MSG_WHOIS_AWAY, params), e.code,
+ undefined, undefined, e.tags);
+ e.user.lastShownAwayMessage = e.user.awayMessage;
+ }
+}
+
+CIRCNetwork.prototype.on311 = /* whois name */
+CIRCNetwork.prototype.on319 = /* whois channels */
+CIRCNetwork.prototype.on312 = /* whois server */
+CIRCNetwork.prototype.on317 = /* whois idle time */
+CIRCNetwork.prototype.on318 = /* whois end of whois*/
+CIRCNetwork.prototype.on330 = /* ircu's 330 numeric ("X is logged in as Y") */
+CIRCNetwork.prototype.onUnknownWhois = /* misc whois line */
+function my_whoisreply (e)
+{
+ var text = "egads!";
+ var nick = e.params[2];
+ var lowerNick = this.primServ.toLowerCase(nick);
+ var user;
+
+ if (this.whoisList && (e.code != 318) && (lowerNick in this.whoisList))
+ this.whoisList[lowerNick] = true;
+
+ if (e.user)
+ {
+ user = e.user;
+ nick = user.unicodeName;
+ }
+
+ switch (Number(e.code))
+ {
+ case 311:
+ // Clear saved away message so it appears and can be reset.
+ if (e.user)
+ e.user.lastShownAwayMessage = "";
+
+ text = getMsg(MSG_WHOIS_NAME,
+ [nick, e.params[3], e.params[4],
+ e.decodeParam(6)]);
+ break;
+
+ case 319:
+ var ary = stringTrim(e.decodeParam(3)).split(" ");
+ text = getMsg(MSG_WHOIS_CHANNELS, [nick, arraySpeak(ary)]);
+ break;
+
+ case 312:
+ text = getMsg(MSG_WHOIS_SERVER,
+ [nick, e.params[3], e.params[4]]);
+ break;
+
+ case 317:
+ text = getMsg(MSG_WHOIS_IDLE,
+ [nick, formatDateOffset(Number(e.params[3])),
+ new Date(Number(e.params[4]) * 1000)]);
+ break;
+
+ case 318:
+ // If the user isn't here, then we sent a whowas in on401.
+ // Don't display the "end of whois" message.
+ if (this.whoisList && (lowerNick in this.whoisList) &&
+ !this.whoisList[lowerNick])
+ {
+ delete this.whoisList[lowerNick];
+ return;
+ }
+ if (this.whoisList)
+ delete this.whoisList[lowerNick];
+
+ text = getMsg(MSG_WHOIS_END, nick);
+ if (user)
+ user.updateHeader();
+ break;
+
+ case 330:
+ text = getMsg(MSG_FMT_LOGGED_ON, [e.decodeParam(2), e.params[3]]);
+ break;
+
+ default:
+ text = toUnicode(e.params.splice(2, e.params.length).join(" "),
+ this);
+ }
+
+ if (e.user)
+ e.user.display(text, e.code, undefined, undefined, e.tags);
+ else
+ this.display(text, e.code, undefined, undefined, e.tags);
+}
+
+CIRCNetwork.prototype.on341 = /* invite reply */
+function my_341 (e)
+{
+ this.display (getMsg(MSG_YOU_INVITE, [e.decodeParam(2), e.decodeParam(3)]),
+ "341", undefined, undefined, e.tags);
+}
+
+CIRCNetwork.prototype.onInvite = /* invite message */
+function my_invite (e)
+{
+ var invitee = e.params[1];
+ if (invitee == e.server.me.unicodeName)
+ {
+ client.munger.getRule(".inline-buttons").enabled = true;
+ this.display(getMsg(MSG_INVITE_YOU, [e.user.unicodeName, e.user.name,
+ e.user.host,
+ e.channel.unicodeName,
+ e.channel.unicodeName,
+ e.channel.getURL()]),
+ "INVITE", undefined, undefined, e.tags);
+ client.munger.getRule(".inline-buttons").enabled = false;
+
+ if ("messages" in e.channel)
+ e.channel.join();
+ }
+ else
+ {
+ this.display(getMsg(MSG_INVITE_SOMEONE, [e.user.unicodeName,
+ invitee,
+ e.channel.unicodeName]),
+ "INVITE", undefined, undefined, e.tags);
+ }
+}
+
+CIRCNetwork.prototype.on433 = /* nickname in use */
+function my_433 (e)
+{
+ var nick = toUnicode(e.params[2], this);
+
+ if ("pendingReclaimCheck" in this)
+ {
+ delete this.pendingReclaimCheck;
+ return;
+ }
+
+ if (this.state == NET_CONNECTING)
+ {
+ // Force a number, thanks.
+ var nickIndex = 1 * arrayIndexOf(this.prefs["nicknameList"], nick);
+ var newnick = null;
+
+ dd("433: failed with " + nick + " (" + nickIndex + ")");
+
+ var tryList = true;
+
+ if ((("_firstNick" in this) && (this._firstNick == -1)) ||
+ (this.prefs["nicknameList"].length == 0) ||
+ ((nickIndex != -1) && (this.prefs["nicknameList"].length < 2)))
+ {
+ tryList = false;
+ }
+
+ if (tryList)
+ {
+ nickIndex = (nickIndex + 1) % this.prefs["nicknameList"].length;
+
+ if (("_firstNick" in this) && (this._firstNick == nickIndex))
+ {
+ // We're back where we started. Give up with this method.
+ this._firstNick = -1;
+ tryList = false;
+ }
+ }
+
+ if (tryList)
+ {
+ newnick = this.prefs["nicknameList"][nickIndex];
+ dd(" trying " + newnick + " (" + nickIndex + ")");
+
+ // Save first index we've tried.
+ if (!("_firstNick" in this))
+ this._firstNick = nickIndex;
+ }
+ else if (this.NICK_RETRIES > 0)
+ {
+ newnick = this.INITIAL_NICK + "_";
+ this.NICK_RETRIES--;
+ dd(" trying " + newnick);
+ }
+
+ if (newnick)
+ {
+ this.INITIAL_NICK = newnick;
+ this.display(getMsg(MSG_RETRY_NICK, [nick, newnick]), "433",
+ undefined, undefined, e.tags);
+ this.primServ.changeNick(newnick);
+ }
+ else
+ {
+ this.display(getMsg(MSG_NICK_IN_USE, nick), "433",
+ undefined, undefined, e.tags);
+ }
+ }
+ else
+ {
+ this.display(getMsg(MSG_NICK_IN_USE, nick), "433",
+ undefined, undefined, e.tags);
+ }
+}
+
+CIRCNetwork.prototype.onStartConnect =
+function my_sconnect (e)
+{
+ this.busy = true;
+ updateProgress();
+ if ("_firstNick" in this)
+ delete this._firstNick;
+
+ client.munger.getRule(".inline-buttons").enabled = true;
+ this.display(getMsg(MSG_CONNECTION_ATTEMPT,
+ [this.getURL(), e.server.getURL(), this.unicodeName,
+ "cancel"]), "INFO");
+ client.munger.getRule(".inline-buttons").enabled = false;
+
+ if (this.prefs["identd.enabled"])
+ {
+ try
+ {
+ client.ident.addNetwork(this, e.server);
+ }
+ catch (ex)
+ {
+ display(getMsg(MSG_IDENT_ERROR, formatException(ex)), MT_ERROR);
+ }
+ }
+
+ this.NICK_RETRIES = this.prefs["nicknameList"].length + 3;
+
+ // When connection begins, autoperform has not been sent
+ this.autoPerformSent = false;
+}
+
+CIRCNetwork.prototype.onError =
+function my_neterror (e)
+{
+ var msg;
+ var type = MT_ERROR;
+
+ if (typeof e.errorCode != "undefined")
+ {
+ switch (e.errorCode)
+ {
+ case JSIRC_ERR_NO_SOCKET:
+ msg = MSG_ERR_NO_SOCKET;
+ break;
+
+ case JSIRC_ERR_EXHAUSTED:
+ // error already displayed in onDisconnect
+ break;
+
+ case JSIRC_ERR_OFFLINE:
+ msg = MSG_ERR_OFFLINE;
+ break;
+
+ case JSIRC_ERR_NO_SECURE:
+ msg = getMsg(MSG_ERR_NO_SECURE, this.unicodeName);
+ break;
+
+ case JSIRC_ERR_CANCELLED:
+ msg = MSG_ERR_CANCELLED;
+ type = MT_INFO;
+ break;
+
+ case JSIRC_ERR_PAC_LOADING:
+ msg = MSG_WARN_PAC_LOADING;
+ type = MT_WARN;
+ break;
+ }
+ }
+ else
+ {
+ msg = e.params[e.params.length - 1];
+ }
+
+ dispatch("sync-header");
+ updateTitle();
+
+ if (this.state == NET_OFFLINE)
+ {
+ this.busy = false;
+ updateProgress();
+ }
+
+ client.ident.removeNetwork(this);
+
+ if (msg)
+ this.display(msg, type);
+
+ if (e.errorCode == JSIRC_ERR_PAC_LOADING)
+ return;
+
+ if (this.deleteWhenDone)
+ this.dispatch("delete-view");
+
+ delete this.deleteWhenDone;
+}
+
+
+CIRCNetwork.prototype.onDisconnect =
+function my_netdisconnect (e)
+{
+ var msg, msgNetwork;
+ var msgType = MT_ERROR;
+ var retrying = true;
+
+ if (typeof e.disconnectStatus != "undefined")
+ {
+ switch (e.disconnectStatus)
+ {
+ case 0:
+ msg = getMsg(MSG_CONNECTION_CLOSED,
+ [this.getURL(), e.server.getURL()]);
+ break;
+
+ case NS_ERROR_CONNECTION_REFUSED:
+ msg = getMsg(MSG_CONNECTION_REFUSED,
+ [this.getURL(), e.server.getURL()]);
+ break;
+
+ case NS_ERROR_NET_TIMEOUT:
+ msg = getMsg(MSG_CONNECTION_TIMEOUT,
+ [this.getURL(), e.server.getURL()]);
+ break;
+
+ case NS_ERROR_NET_RESET:
+ msg = getMsg(MSG_CONNECTION_RESET,
+ [this.getURL(), e.server.getURL()]);
+ break;
+
+ case NS_ERROR_NET_INTERRUPT:
+ msg = getMsg(MSG_CONNECTION_INTERRUPT,
+ [this.getURL(), e.server.getURL()]);
+ break;
+
+ case NS_ERROR_UNKNOWN_HOST:
+ msg = getMsg(MSG_UNKNOWN_HOST,
+ [e.server.hostname, this.getURL(),
+ e.server.getURL()]);
+ break;
+
+ case NS_ERROR_UNKNOWN_PROXY_HOST:
+ msg = getMsg(MSG_UNKNOWN_PROXY_HOST,
+ [this.getURL(), e.server.getURL()]);
+ break;
+
+ case NS_ERROR_PROXY_CONNECTION_REFUSED:
+ msg = MSG_PROXY_CONNECTION_REFUSED;
+ break;
+
+ case NS_ERROR_OFFLINE:
+ msg = MSG_ERR_OFFLINE;
+ retrying = false;
+ break;
+
+ case NS_ERROR_ABORT:
+ if (Services.io.offline)
+ {
+ msg = getMsg(MSG_CONNECTION_ABORT_OFFLINE,
+ [this.getURL(), e.server.getURL()]);
+ }
+ else
+ {
+ msg = getMsg(MSG_CONNECTION_ABORT_UNKNOWN,
+ [this.getURL(), e.server.getURL(),
+ formatException(e.exception)]);
+ }
+ retrying = false;
+ break;
+
+ default:
+ var errClass = getNSSErrorClass(e.disconnectStatus);
+ // Check here if it's a cert error.
+ // The exception adding dialog will explain the reasons.
+ if (errClass == ERROR_CLASS_BAD_CERT)
+ {
+ var cmd = "ssl-exception";
+ cmd += " " + e.server.hostname + " " + e.server.port;
+ cmd += " true";
+ msg = getMsg(MSG_INVALID_CERT, [this.getURL(), cmd]);
+ retrying = false;
+ break;
+ }
+
+ // If it's a protocol error, we can still display a useful message.
+ var statusMsg = e.disconnectStatus;
+ if (errClass == ERROR_CLASS_SSL_PROTOCOL)
+ {
+ var nssErrSvc = getService("@mozilla.org/nss_errors_service;1",
+ "nsINSSErrorsService");
+ var errMsg = nssErrSvc.getErrorMessage(e.disconnectStatus);
+ errMsg = errMsg.replace(/\.$/, "");
+ statusMsg += " (" + errMsg + ")";
+ }
+
+ msg = getMsg(MSG_CLOSE_STATUS,
+ [this.getURL(), e.server.getURL(),
+ statusMsg]);
+ break;
+ }
+ }
+ else
+ {
+ msg = getMsg(MSG_CONNECTION_CLOSED,
+ [this.getURL(), e.server.getURL()]);
+ }
+
+ // e.quitting signals the disconnect was intended: don't use "ERROR".
+ if (e.quitting)
+ {
+ msgType = "DISCONNECT";
+ msg = getMsg(MSG_CONNECTION_QUIT,
+ [this.getURL(), e.server.getURL(), this.unicodeName,
+ "reconnect"]);
+ msgNetwork = msg;
+ }
+ // We won't reconnect if the error was really bad, or if the user doesn't
+ // want us to do so.
+ else if (!retrying || !this.stayingPower)
+ {
+ msgNetwork = msg;
+ }
+ else
+ {
+ var delayStr = formatDateOffset(this.getReconnectDelayMs() / 1000);
+ if (this.MAX_CONNECT_ATTEMPTS == -1)
+ {
+ msgNetwork = getMsg(MSG_RECONNECTING_IN,
+ [msg, delayStr, this.unicodeName, "cancel"]);
+ }
+ else if (this.connectAttempt < this.MAX_CONNECT_ATTEMPTS)
+ {
+ var left = this.MAX_CONNECT_ATTEMPTS - this.connectAttempt;
+ if (left == 1)
+ {
+ msgNetwork = getMsg(MSG_RECONNECTING_IN_LEFT1,
+ [msg, delayStr, this.unicodeName,
+ "cancel"]);
+ }
+ else
+ {
+ msgNetwork = getMsg(MSG_RECONNECTING_IN_LEFT,
+ [msg, left, delayStr, this.unicodeName,
+ "cancel"]);
+ }
+ }
+ else
+ {
+ msgNetwork = getMsg(MSG_CONNECTION_EXHAUSTED, msg);
+ }
+ }
+
+ /* If we were connected ok, put an error on all tabs. If we were only
+ * /trying/ to connect, and failed, just put it on the network tab.
+ */
+ client.munger.getRule(".inline-buttons").enabled = true;
+ if (this.state == NET_ONLINE)
+ {
+ for (var v in client.viewsArray)
+ {
+ var obj = client.viewsArray[v].source;
+ if (obj == this)
+ {
+ obj.displayHere(msgNetwork, msgType);
+ }
+ else if (obj != client)
+ {
+ var details = getObjectDetails(obj);
+ if ("server" in details && details.server == e.server)
+ obj.displayHere(msg, msgType);
+ }
+ }
+ }
+ else
+ {
+ this.busy = false;
+ updateProgress();
+
+ // Don't do anything if we're cancelling.
+ if (this.state != NET_CANCELLING)
+ {
+ this.displayHere(msgNetwork, msgType);
+ }
+ }
+ client.munger.getRule(".inline-buttons").enabled = false;
+
+ for (var c in this.primServ.channels)
+ {
+ var channel = this.primServ.channels[c];
+ channel._clearUserList();
+ }
+
+ dispatch("sync-header");
+ updateTitle();
+ updateProgress();
+ updateSecurityIcon();
+
+ client.ident.removeNetwork(this);
+
+ if ("userClose" in client && client.userClose &&
+ client.getConnectionCount() == 0)
+ window.close();
+
+ // Renew the STS policy.
+ if (e.server.isSecure && ("sts" in e.server.caps) && client.sts.ENABLED)
+ {
+ var policy = client.sts.parseParameters(e.server.capvals["sts"]);
+ client.sts.setPolicy(e.server.hostname, e.server.port, policy.duration);
+ }
+
+ if (("reconnect" in this) && this.reconnect)
+ {
+ if ("stsUpgradePort" in this)
+ {
+ e.server.port = this.stsUpgradePort;
+ e.server.isSecure = true;
+ delete this.stsUpgradePort;
+ }
+ this.connect(this.requireSecurity);
+ delete this.reconnect;
+ }
+}
+
+CIRCNetwork.prototype.onCTCPReplyPing =
+function my_replyping (e)
+{
+ // see bug 326523
+ if (stringTrim(e.CTCPData).length != 13)
+ {
+ this.display(getMsg(MSG_PING_REPLY_INVALID, e.user.unicodeName),
+ "INFO", e.user, "ME!", e.tags);
+ return;
+ }
+
+ var delay = formatDateOffset((new Date() - new Date(Number(e.CTCPData))) /
+ 1000);
+ this.display(getMsg(MSG_PING_REPLY, [e.user.unicodeName, delay]), "INFO",
+ e.user, "ME!", e.tags);
+}
+
+CIRCNetwork.prototype.on221 =
+CIRCNetwork.prototype.onUserMode =
+function my_umode (e)
+{
+ if ("user" in e && e.user)
+ {
+ e.user.updateHeader();
+ this.display(getMsg(MSG_USER_MODE, [e.user.unicodeName, e.params[2]]),
+ MT_MODE, undefined, undefined, e.tags);
+ }
+ else
+ {
+ this.display(getMsg(MSG_USER_MODE, [e.params[1], e.params[2]]),
+ MT_MODE, undefined, undefined, e.tags);
+ }
+}
+
+CIRCNetwork.prototype.onNick =
+function my_cnick (e)
+{
+ if (!ASSERT(userIsMe(e.user), "network nick event for third party"))
+ return;
+
+ if (("pendingNickChange" in this) &&
+ (this.pendingNickChange == e.user.unicodeName))
+ {
+ this.prefs["nickname"] = e.user.unicodeName;
+ this.preferredNick = e.user.unicodeName;
+ delete this.pendingNickChange;
+ }
+
+ if (getTabForObject(this))
+ {
+ this.displayHere(getMsg(MSG_NEWNICK_YOU, e.user.unicodeName),
+ "NICK", "ME!", e.user, e.tags);
+ }
+
+ this.updateHeader();
+ updateStalkExpression(this);
+}
+
+CIRCNetwork.prototype.onPing =
+function my_netping (e)
+{
+ this.updateHeader(this);
+}
+
+CIRCNetwork.prototype.onPong =
+function my_netpong (e)
+{
+ this.updateHeader(this);
+}
+
+CIRCNetwork.prototype.onWallops =
+function my_netwallops(e)
+{
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ if (e.user)
+ this.display(e.msg, "WALLOPS/WALLOPS", e.user, this, e.tags);
+ else
+ this.display(e.msg, "WALLOPS/WALLOPS", undefined, this, e.tags);
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+/* unknown command reply */
+CIRCNetwork.prototype.on421 =
+function my_421(e)
+{
+ this.display(getMsg(MSG_IRC_421, e.decodeParam(2)), MT_ERROR, undefined,
+ undefined, e.tags);
+ return true;
+}
+
+/* cap reply */
+CIRCNetwork.prototype.onCap =
+function my_cap(e)
+{
+ if (e.params[2] == "LS")
+ {
+ // Handle the STS upgrade policy if we have one.
+ if (e.server.pendingCapNegotiation && e.stsUpgradePort)
+ {
+ this.display(getMsg(MSG_STS_UPGRADE, e.stsUpgradePort));
+ this.reconnect = true;
+ this.stsUpgradePort = e.stsUpgradePort;
+ this.quit(MSG_RECONNECTING);
+ return true;
+ }
+
+ // Don't show the raw message until we've registered.
+ if (this.state == NET_ONLINE)
+ {
+
+ var listCaps = new Array();
+ for (var cap in e.server.caps)
+ {
+ var value = e.server.capvals[cap];
+ if (value)
+ cap += "=" + value;
+ listCaps.push(cap);
+ }
+ if (listCaps.length > 0)
+ {
+ listCaps.sort();
+ this.display(getMsg(MSG_SUPPORTS_CAPS, listCaps.join(", ")));
+ }
+ }
+
+ // Update the STS duration policy.
+ if (e.server.isSecure && ("sts" in e.server.caps) && client.sts.ENABLED)
+ {
+ var policy = client.sts.parseParameters(e.server.capvals["sts"]);
+ client.sts.setPolicy(e.server.hostname, e.server.port, policy.duration);
+ }
+ }
+ else if (e.params[2] == "LIST")
+ {
+ var listCapsEnabled = new Array();
+ for (var cap in e.server.caps)
+ {
+ if (e.server.caps[cap])
+ {
+ listCapsEnabled.push(cap);
+ }
+ }
+ if (listCapsEnabled.length > 0)
+ {
+ listCapsEnabled.sort();
+ this.display(getMsg(MSG_SUPPORTS_CAPSON,
+ listCapsEnabled.join(", ")));
+ }
+ }
+ else if (e.params[2] == "ACK")
+ {
+ if (e.capsOn.length)
+ this.display(getMsg(MSG_CAPS_ON, e.capsOn.join(", ")));
+ if (e.capsOff.length)
+ this.display(getMsg(MSG_CAPS_OFF, e.capsOff.join(", ")));
+ }
+ else if (e.params[2] == "NAK")
+ {
+ this.display(getMsg(MSG_CAPS_ERROR, e.caps.join(", ")));
+ }
+ else if (e.params[2] == "NEW")
+ {
+ // Handle a new STS policy
+ if (client.sts.ENABLED && (arrayContains(e.newcaps, "sts")))
+ {
+ var policy = client.sts.parseParameters(e.server.capvals["sts"]);
+ if (!e.server.isSecure && policy.port)
+ {
+ // Inform the user of the new upgrade policy and
+ // offer an option to reconnect.
+ client.munger.getRule(".inline-buttons").enabled = true;
+ this.display(getMsg(MSG_STS_UPGRADE_NEW, [this.unicodeName, "reconnect"]));
+ client.munger.getRule(".inline-buttons").enabled = false;
+ }
+ else if (e.server.isSecure && policy.duration)
+ {
+ // Renew the policy's duration.
+ client.sts.setPolicy(e.server.hostname, e.server.port, policy.duration);
+ }
+ }
+ }
+ return true;
+}
+
+// Notify the user of received CTCP requests.
+CIRCNetwork.prototype.onReceiveCTCP =
+function my_ccrecv(e)
+{
+ // Do nothing if we receive these.
+ if ((e.type == "ctcp-action") ||
+ (e.type == "ctcp-dcc") ||
+ (e.type == "unk-ctcp"))
+ return true;
+
+ this.display(getMsg(MSG_FMT_CTCPRECV,
+ [toUnicode(e.CTCPCode, this),
+ toUnicode(e.CTCPData, this), e.user.unicodeName]),
+ "CTCP_REQUEST", e.user, e.server.me, e.tags);
+
+ return true;
+}
+
+/* SASL authentication start */
+CIRCNetwork.prototype.onSASLStart =
+function my_saslstart(e)
+{
+ if (!e.mechs || e.mechs.indexOf("plain") !== -1)
+ e.server.sendData("AUTHENTICATE PLAIN\n");
+}
+
+/* SASL authentication response */
+CIRCNetwork.prototype.onAuthenticate =
+function my_auth(e)
+{
+ if (e.params[1] !== "+")
+ return;
+
+ var username = e.server.me.encodedName;
+ var password = client.tryToGetLogin(e.server.parent.getURL(), "sasl",
+ e.server.me.name, null, true,
+ getMsg(MSG_SASL_PASSWORD, username));
+ if (!password)
+ {
+ // Abort authentication.
+ e.server.sendAuthAbort();
+ return;
+ }
+
+ var auth = username + '\0' + username + '\0' + password;
+ e.server.sendAuthResponse(auth);
+}
+
+CIRCNetwork.prototype.onNetsplitBatch =
+function my_netsplit_batch(e)
+{
+ for (var c in this.primServ.channels)
+ {
+ if (e.starting)
+ {
+ this.startMsgGroup(e.reftag, getMsg(MSG_BATCH_NETSPLIT_START,
+ [e.params[3],
+ e.params[4]]),
+ e.batchtype);
+ }
+ else
+ {
+ this.display(MSG_BATCH_NETSPLIT_END, e.batchtype);
+ this.endMsgGroup();
+ }
+ }
+}
+
+CIRCNetwork.prototype.onNetjoinBatch =
+function my_netjoin_batch(e)
+{
+ for (var c in this.primServ.channels)
+ {
+ if (e.starting)
+ {
+ this.startMsgGroup(e.reftag, getMsg(MSG_BATCH_NETJOIN_START,
+ [e.params[3],
+ e.params[4]]),
+ e.batchtype);
+ }
+ else
+ {
+ this.display(MSG_BATCH_NETJOIN_END, e.batchtype);
+ this.endMsgGroup();
+ }
+ }
+}
+
+CIRCChannel.prototype.onChathistoryBatch =
+function my_chathistory_batch(e)
+{
+ if (e.starting)
+ {
+ this.startMsgGroup(e.reftag, getMsg(MSG_BATCH_CHATHISTORY_START,
+ [e.params[3]]),
+ e.batchtype);
+ }
+ else
+ {
+ this.display(MSG_BATCH_CHATHISTORY_END, e.batchtype);
+ this.endMsgGroup();
+ }
+}
+
+CIRCNetwork.prototype.onUnknownBatch =
+CIRCChannel.prototype.onUnknownBatch =
+CIRCUser.prototype.onUnknownBatch =
+function my_unknown_batch(e)
+{
+ if (e.starting)
+ {
+ this.startMsgGroup(e.reftag, getMsg(MSG_BATCH_UNKNOWN,
+ [e.batchtype,
+ e.params.slice(3)]),
+ "BATCH");
+ }
+ else
+ {
+ this.display(MSG_BATCH_UNKNOWN_END, e.batchtype);
+ this.endMsgGroup();
+ }
+}
+
+/* user away status */
+CIRCNetwork.prototype.onAway =
+function my_away(e)
+{
+ var userlist = document.getElementById("user-list");
+ for (var c in e.server.channels)
+ {
+ var chan = e.server.channels[c];
+ if (chan.active && (e.user.collectionKey in chan.users))
+ {
+ let index = chan.users[e.user.collectionKey].chanListEntry.childIndex;
+ userlist.treeBoxObject.invalidateRow(index);
+ e.server.channels[c].updateUsers([e.user.collectionKey]);
+ }
+ }
+}
+
+/* user host changed */
+CIRCNetwork.prototype.onChghost =
+function my_chghost(e)
+{
+ e.user.updateHeader();
+}
+
+CIRCNetwork.prototype.reclaimName =
+function my_reclaimname()
+{
+ var network = this;
+
+ function callback() {
+ network.reclaimName();
+ };
+
+ if ("pendingReclaimCheck" in this)
+ delete this.pendingReclaimCheck;
+
+ // Function to attempt to get back the nickname the user wants.
+ if ((this.state != NET_ONLINE) || !this.primServ)
+ return false;
+
+ if (this.primServ.me.unicodeName == this.preferredNick)
+ return false;
+
+ this.reclaimLeft -= this.RECLAIM_WAIT;
+
+ if (this.reclaimLeft <= 0)
+ return false;
+
+ this.pendingReclaimCheck = true;
+ this.INITIAL_NICK = this.preferredNick;
+ this.primServ.changeNick(this.preferredNick);
+
+ setTimeout(callback, this.RECLAIM_WAIT);
+
+ return true;
+}
+
+CIRCNetwork.prototype.doAutoPerform =
+function net_autoperform()
+{
+ if (("autoPerformSent" in this) && (this.autoPerformSent == false))
+ {
+ var cmdary = client.prefs["autoperform.network"].concat(this.prefs["autoperform"]);
+ for (var i = 0; i < cmdary.length; ++i)
+ {
+ if (cmdary[i][0] == "/")
+ this.dispatch(cmdary[i].substr(1));
+ else
+ this.dispatch(cmdary[i]);
+ }
+ this.autoPerformSent = true;
+ }
+}
+
+
+/* We want to override the base implementations. */
+CIRCChannel.prototype._join = CIRCChannel.prototype.join;
+CIRCChannel.prototype._part = CIRCChannel.prototype.part;
+
+CIRCChannel.prototype.join =
+function chan_join(key)
+{
+ var joinFailedFn = function _joinFailedFn(t)
+ {
+ delete t.joinTimer;
+ t.busy = false;
+ updateProgress();
+ }
+ if (!this.joined)
+ {
+ this.joinTimer = setTimeout(joinFailedFn, 30000, this);
+ this.busy = true;
+ updateProgress();
+ }
+ this._join(key);
+}
+
+CIRCChannel.prototype.part =
+function chan_part(reason)
+{
+ var partFailedFn = function _partFailedFn(t)
+ {
+ delete t.partTimer;
+ t.busy = false;
+ updateProgress();
+ }
+ this.partTimer = setTimeout(partFailedFn, 30000, this);
+ this.busy = true;
+ updateProgress();
+ this._part(reason);
+}
+
+client.setActivityMarker =
+CIRCNetwork.prototype.setActivityMarker =
+CIRCChannel.prototype.setActivityMarker =
+CIRCUser.prototype.setActivityMarker =
+CIRCDCCChat.prototype.setActivityMarker =
+CIRCDCCFileTransfer.prototype.setActivityMarker =
+function view_setactivitymarker(state)
+{
+ if (!client.initialized)
+ return;
+
+ // Always clear the activity marker first.
+ var markedRow = this.getActivityMarker();
+ if (markedRow)
+ {
+ markedRow.classList.remove("chatzilla-line-marker");
+ }
+
+ if (state)
+ {
+ // Mark the last row.
+ var target = this.messages.firstChild.lastChild;
+ if (!target)
+ return;
+ target.classList.add("chatzilla-line-marker");
+ }
+}
+
+client.getActivityMarker =
+CIRCNetwork.prototype.getActivityMarker =
+CIRCChannel.prototype.getActivityMarker =
+CIRCUser.prototype.getActivityMarker =
+CIRCDCCChat.prototype.getActivityMarker =
+CIRCDCCFileTransfer.prototype.getActivityMarker =
+function view_getactivitymarker()
+{
+ return this.messages.querySelector(".chatzilla-line-marker");
+}
+CIRCChannel.prototype.onInit =
+function chan_oninit ()
+{
+ this.logFile = null;
+ this.pendingNamesReply = false;
+ this.importantMessages = 0;
+}
+
+CIRCChannel.prototype.onPrivmsg =
+function my_cprivmsg (e)
+{
+ var msg = e.decodeParam(2);
+ var msgtype = "PRIVMSG";
+ if ("msgPrefix" in e)
+ msgtype += "/" + e.msgPrefix.symbol;
+
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ this.display(msg, msgtype, e.user, this, e.tags);
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+/* end of names */
+CIRCChannel.prototype.on366 =
+function my_366 (e)
+{
+ // First clear up old users:
+ var removals = new Array();
+ while (this.userList.childData.childData.length > 0)
+ {
+ var userToRemove = this.userList.childData.childData[0]._userObj;
+ this.removeFromList(userToRemove);
+ removals.push(userToRemove);
+ }
+ this.removeUsers(removals);
+
+ var entries = new Array(), updates = new Array();
+ for (var u in this.users)
+ {
+ entries.push(new UserEntry(this.users[u], this.userListShare));
+ updates.push(this.users[u]);
+ }
+ this.addUsers(updates);
+
+ this.userList.childData.appendChildren(entries);
+
+ if (this.pendingNamesReply)
+ {
+ this.parent.parent.display (e.channel.unicodeName + ": " +
+ e.params[3], "366", undefined, undefined,
+ e.tags);
+ }
+ this.pendingNamesReply = false;
+
+ // Update conference mode now we have a complete user list.
+ this._updateConferenceMode();
+}
+
+CIRCChannel.prototype.onTopic = /* user changed topic */
+CIRCChannel.prototype.on332 = /* TOPIC reply */
+function my_topic (e)
+{
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ if (e.code == "TOPIC")
+ this.display (getMsg(MSG_TOPIC_CHANGED, [this.topicBy, this.topic]),
+ "TOPIC", undefined, undefined, e.tags);
+
+ if (e.code == "332")
+ {
+ if (this.topic)
+ {
+ this.display (getMsg(MSG_TOPIC,
+ [this.unicodeName, this.topic]),
+ "TOPIC", undefined, undefined, e.tags);
+ }
+ else
+ {
+ this.display(getMsg(MSG_NO_TOPIC, this.unicodeName), "TOPIC",
+ undefined, undefined, e.tags);
+ }
+ }
+
+ this.updateHeader();
+ updateTitle(this);
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+CIRCChannel.prototype.on333 = /* Topic setter information */
+function my_topicinfo (e)
+{
+ this.display (getMsg(MSG_TOPIC_DATE, [this.unicodeName, this.topicBy,
+ this.topicDate]), "TOPIC",
+ undefined, undefined, e.tags);
+}
+
+CIRCChannel.prototype.on353 = /* names reply */
+function my_topic (e)
+{
+ if (this.pendingNamesReply)
+ {
+ this.parent.parent.display (e.channel.unicodeName + ": " +
+ e.params[4], "NAMES", undefined, undefined,
+ e.tags);
+ }
+}
+
+CIRCChannel.prototype.on367 = /* channel ban stuff */
+function my_bans(e)
+{
+ if ("pendingBanList" in this)
+ return;
+
+ var msg = getMsg(MSG_BANLIST_ITEM,
+ [e.user.unicodeName, e.ban, this.unicodeName, e.banTime]);
+ if (this.iAmHalfOp() || this.iAmOp())
+ msg += " " + getMsg(MSG_BANLIST_BUTTON, "mode -b " + e.ban);
+
+ client.munger.getRule(".inline-buttons").enabled = true;
+ this.display(msg, "BAN", undefined, undefined, e.tags);
+ client.munger.getRule(".inline-buttons").enabled = false;
+}
+
+CIRCChannel.prototype.on368 =
+function my_endofbans(e)
+{
+ if ("pendingBanList" in this)
+ return;
+
+ this.display(getMsg(MSG_BANLIST_END, this.unicodeName), "BAN", undefined,
+ undefined, e.tags);
+}
+
+CIRCChannel.prototype.on348 = /* channel except stuff */
+function my_excepts(e)
+{
+ if ("pendingExceptList" in this)
+ return;
+
+ var msg = getMsg(MSG_EXCEPTLIST_ITEM, [e.user.unicodeName, e.except,
+ this.unicodeName, e.exceptTime]);
+ if (this.iAmHalfOp() || this.iAmOp())
+ msg += " " + getMsg(MSG_EXCEPTLIST_BUTTON, "mode -e " + e.except);
+
+ client.munger.getRule(".inline-buttons").enabled = true;
+ this.display(msg, "EXCEPT", undefined, undefined, e.tags);
+ client.munger.getRule(".inline-buttons").enabled = false;
+}
+
+CIRCChannel.prototype.on349 =
+function my_endofexcepts(e)
+{
+ if ("pendingExceptList" in this)
+ return;
+
+ this.display(getMsg(MSG_EXCEPTLIST_END, this.unicodeName), "EXCEPT",
+ undefined, undefined, e.tags);
+}
+
+CIRCChannel.prototype.on482 =
+function my_needops(e)
+{
+ if ("pendingExceptList" in this)
+ return;
+
+ this.display(getMsg(MSG_CHANNEL_NEEDOPS, this.unicodeName), MT_ERROR,
+ undefined, undefined, e.tags);
+}
+
+CIRCChannel.prototype.onNotice =
+function my_notice (e)
+{
+ var msgtype = "NOTICE";
+ if ("msgPrefix" in e)
+ msgtype += "/" + e.msgPrefix.symbol;
+
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ this.display(e.decodeParam(2), msgtype, e.user, this, e.tags);
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+CIRCChannel.prototype.onCTCPAction =
+function my_caction (e)
+{
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ this.display(e.CTCPData, "ACTION", e.user, this, e.tags);
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+CIRCChannel.prototype.onUnknownCTCP =
+function my_unkctcp (e)
+{
+ this.display (getMsg(MSG_UNKNOWN_CTCP, [e.CTCPCode, e.CTCPData,
+ e.user.unicodeName]),
+ "BAD-CTCP", e.user, this, e.tags);
+}
+
+CIRCChannel.prototype.onJoin =
+function my_cjoin (e)
+{
+ dispatch("create-tab-for-view", { view: e.channel });
+
+ if (userIsMe(e.user))
+ {
+ var params = [e.user.unicodeName, e.channel.unicodeName];
+ this.display(getMsg(MSG_YOU_JOINED, params), "JOIN",
+ e.server.me, this, e.tags);
+ /* Tell the user that conference mode is on, lest they forget (if it
+ * subsequently turns itself off, they'll get a message anyway).
+ */
+ if (this.prefs["conference.enabled"])
+ this.display(MSG_CONF_MODE_STAYON);
+ addURLToHistory(this.getURL());
+
+ if ("joinTimer" in this)
+ {
+ clearTimeout(this.joinTimer);
+ delete this.joinTimer;
+ this.busy = false;
+ updateProgress();
+ }
+
+ /* !-channels are "safe" channels, and get a server-generated prefix.
+ * For this reason, creating the channel is delayed until this point.
+ */
+ if (e.channel.unicodeName[0] == "!")
+ dispatch("set-current-view", { view: e.channel });
+
+ this.doAutoPerform();
+ }
+ else
+ {
+ if (!this.prefs["conference.enabled"])
+ {
+ this.display(getMsg(MSG_SOMEONE_JOINED,
+ [e.user.unicodeName, e.user.name, e.user.host,
+ e.channel.unicodeName]),
+ "JOIN", e.user, this, e.tags);
+ }
+
+ /* Only do this for non-me joins so us joining doesn't reset it (when
+ * we join the usercount is always 1). Also, do this after displaying
+ * the join message so we don't get cryptic effects such as a user
+ * joining causes *only* a "Conference mode enabled" message.
+ */
+ this._updateConferenceMode();
+ }
+
+ /* We don't want to add ourself here, since the names reply we'll be
+ * getting right after the join will include us as well! (FIXME)
+ */
+ if (!userIsMe(e.user))
+ {
+ this.addUsers([e.user]);
+ var entry = new UserEntry(e.user, this.userListShare);
+ this.userList.childData.appendChild(entry);
+ this.userList.childData.reSort();
+ }
+ this.updateHeader();
+}
+
+CIRCChannel.prototype.onPart =
+function my_cpart(e)
+{
+ this.removeUsers([e.user]);
+ this.updateHeader();
+
+ if (userIsMe(e.user))
+ {
+ var msg = e.reason ? MSG_YOU_LEFT_REASON : MSG_YOU_LEFT;
+ var params = [e.user.unicodeName, e.channel.unicodeName, e.reason];
+ this.display(getMsg(msg, params), "PART", e.user, this, e.tags);
+ this._clearUserList();
+
+ if ("partTimer" in this)
+ {
+ clearTimeout(this.partTimer);
+ delete this.partTimer;
+ this.busy = false;
+ updateProgress();
+ }
+
+ if (this.deleteWhenDone)
+ this.dispatch("delete-view");
+
+ delete this.deleteWhenDone;
+ }
+ else
+ {
+ /* We're ok to update this before the message, because the only thing
+ * that can happen is *disabling* of conference mode.
+ */
+ this._updateConferenceMode();
+
+ if (!this.prefs["conference.enabled"])
+ {
+ var msg = e.reason ? MSG_SOMEONE_LEFT_REASON : MSG_SOMEONE_LEFT;
+ var params = [e.user.unicodeName, e.channel.unicodeName, e.reason];
+ this.display(getMsg(msg, params), "PART", e.user, this, e.tags);
+ }
+
+ this.removeFromList(e.user);
+ }
+}
+
+CIRCChannel.prototype.onKick =
+function my_ckick (e)
+{
+ if (userIsMe (e.lamer))
+ {
+ if (e.user)
+ {
+ this.display (getMsg(MSG_YOURE_GONE,
+ [e.lamer.unicodeName, e.channel.unicodeName,
+ e.user.unicodeName, e.reason]),
+ "KICK", e.user, this, e.tags);
+ }
+ else
+ {
+ this.display (getMsg(MSG_YOURE_GONE,
+ [e.lamer.unicodeName, e.channel.unicodeName,
+ MSG_SERVER, e.reason]),
+ "KICK", (void 0), this, e.tags);
+ }
+
+ this._clearUserList();
+ /* Try 1 re-join attempt if allowed. */
+ if (this.prefs["autoRejoin"])
+ this.join(this.mode.key);
+ }
+ else
+ {
+ var enforcerProper, enforcerNick;
+ if (e.user && userIsMe(e.user))
+ {
+ enforcerProper = "YOU";
+ enforcerNick = "ME!";
+ }
+ else if (e.user)
+ {
+ enforcerProper = e.user.unicodeName;
+ enforcerNick = e.user.encodedName;
+ }
+ else
+ {
+ enforcerProper = MSG_SERVER;
+ enforcerNick = MSG_SERVER;
+ }
+
+ this.display(getMsg(MSG_SOMEONE_GONE,
+ [e.lamer.unicodeName, e.channel.unicodeName,
+ enforcerProper, e.reason]),
+ "KICK", e.user, this, e.tags);
+
+ this.removeFromList(e.lamer);
+ }
+
+ this.removeUsers([e.lamer]);
+ this.updateHeader();
+}
+
+CIRCChannel.prototype.removeFromList =
+function my_removeFromList(user)
+{
+ // Remove the user from the list and 'disconnect' the user from their entry:
+ var idx = user.chanListEntry.childIndex;
+ this.userList.childData.removeChildAtIndex(idx);
+
+ delete user.chanListEntry._userObj;
+ delete user.chanListEntry;
+}
+
+CIRCChannel.prototype.onChanMode =
+function my_cmode (e)
+{
+ if (e.code == "MODE")
+ {
+ var msg = e.decodeParam(1);
+ for (var i = 2; i < e.params.length; i++)
+ msg += " " + e.decodeParam(i);
+
+ var source = e.user ? e.user.unicodeName : e.source;
+ this.display(getMsg(MSG_MODE_CHANGED, [msg, source]),
+ "MODE", (e.user || null), this, e.tags);
+ }
+ else if ("pendingModeReply" in this)
+ {
+ var msg = e.decodeParam(3);
+ for (var i = 4; i < e.params.length; i++)
+ msg += " " + e.decodeParam(i);
+
+ var view = ("messages" in this && this.messages) ? this : e.network;
+ view.display(getMsg(MSG_MODE_ALL, [this.unicodeName, msg]), "MODE",
+ undefined, undefined, e.tags);
+ delete this.pendingModeReply;
+ }
+ var updates = new Array();
+ for (var u in e.usersAffected)
+ updates.push(e.usersAffected[u]);
+ this.updateUsers(updates);
+
+ this.updateHeader();
+ updateTitle(this);
+ if (client.currentObject == this)
+ updateUserList();
+}
+
+CIRCChannel.prototype.onNick =
+function my_cnick (e)
+{
+ if (userIsMe (e.user))
+ {
+ if (getTabForObject(this))
+ {
+ this.displayHere(getMsg(MSG_NEWNICK_YOU, e.user.unicodeName),
+ "NICK", "ME!", e.user, e.tags);
+ }
+ this.parent.parent.updateHeader();
+ }
+ else if (!this.prefs["conference.enabled"])
+ {
+ this.display(getMsg(MSG_NEWNICK_NOTYOU, [e.oldNick,
+ e.user.unicodeName]),
+ "NICK", e.user, this, e.tags);
+ }
+
+ this.updateUsers([e.user]);
+ if (client.currentObject == this)
+ updateUserList();
+}
+
+CIRCChannel.prototype.onQuit =
+function my_cquit (e)
+{
+ if (userIsMe(e.user))
+ {
+ /* I dont think this can happen */
+ var pms = [e.user.unicodeName, e.server.parent.unicodeName, e.reason];
+ this.display(getMsg(MSG_YOU_QUIT, pms),"QUIT", e.user, this, e.tags);
+ this._clearUserList();
+ }
+ else
+ {
+ // See onPart for why this is ok before the message.
+ this._updateConferenceMode();
+
+ if (!this.prefs["conference.enabled"])
+ {
+ this.display(getMsg(MSG_SOMEONE_QUIT,
+ [e.user.unicodeName,
+ e.server.parent.unicodeName, e.reason]),
+ "QUIT", e.user, this, e.tags);
+ }
+ }
+
+ this.removeUsers([e.user]);
+ this.removeFromList(e.user);
+
+ this.updateHeader();
+}
+
+CIRCChannel.prototype.doAutoPerform =
+function my_cautoperform()
+{
+ var cmdary = client.prefs["autoperform.channel"].concat(this.prefs["autoperform"]);
+ for (var i = 0; i < cmdary.length; ++i)
+ {
+ if (cmdary[i][0] == "/")
+ this.dispatch(cmdary[i].substr(1));
+ else
+ this.dispatch(cmdary[i]);
+ }
+}
+
+CIRCChannel.prototype._clearUserList =
+function _my_clearuserlist()
+{
+ if (this.userList && this.userList.childData &&
+ this.userList.childData.childData)
+ {
+ this.userList.freeze();
+ var len = this.userList.childData.childData.length;
+ while (len > 0)
+ {
+ var entry = this.userList.childData.childData[--len];
+ this.userList.childData.removeChildAtIndex(len);
+ delete entry._userObj.chanListEntry;
+ delete entry._userObj;
+ }
+ this.userList.thaw();
+ }
+}
+
+CIRCUser.prototype.onInit =
+function user_oninit ()
+{
+ this.logFile = null;
+ this.lastShownAwayMessage = "";
+}
+
+CIRCUser.prototype.onPrivmsg =
+function my_cprivmsg(e)
+{
+ var sourceObj = e.user;
+ var destObj = e.server.me;
+ var displayObj = this;
+
+ if (!("messages" in this))
+ {
+ var limit = client.prefs["newTabLimit"];
+ if (limit == 0 || client.viewsArray.length < limit)
+ {
+ if (e.user != e.server.me)
+ {
+ openQueryTab(e.server, e.user.unicodeName);
+ }
+ else
+ {
+ // This is a self-message, i.e. we received a message that
+ // looks like it came from us. Display it accordingly.
+ sourceObj = e.server.me;
+ destObj = openQueryTab(e.server, e.params[1]);
+ displayObj = destObj;
+ }
+ }
+ }
+
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ displayObj.display(e.decodeParam(2), "PRIVMSG", sourceObj, destObj, e.tags);
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+CIRCUser.prototype.onNick =
+function my_unick (e)
+{
+ if (userIsMe(e.user))
+ {
+ this.parent.parent.updateHeader();
+ updateTitle();
+ }
+ else if ("messages" in this && this.messages)
+ {
+ this.display(getMsg(MSG_NEWNICK_NOTYOU, [e.oldNick, e.user.unicodeName]),
+ "NICK", e.user, this, e.tags);
+ }
+
+ this.updateHeader();
+ var tab = getTabForObject(this);
+ if (tab)
+ tab.setAttribute("label", this.unicodeName);
+}
+
+CIRCUser.prototype.onNotice =
+function my_notice (e)
+{
+ var msg = e.decodeParam(2);
+ var displayMailto = client.prefs["munger.mailto"];
+
+ var ary = msg.match(/^\[([^ ]+)\]\s+/);
+ if (ary)
+ {
+ var channel = e.server.getChannel(ary[1]);
+ if (channel)
+ {
+ client.munger.getRule(".mailto").enabled = displayMailto;
+ channel.display(msg, "NOTICE", this, e.server.me, e.tags);
+ client.munger.getRule(".mailto").enabled = false;
+ return;
+ }
+ }
+
+ var sourceObj = this;
+ var destObj = e.server.me;
+ var displayObj = this;
+
+ if (e.user == e.server.me)
+ {
+ // This is a self-message, i.e. we received a message that
+ // looks like it came from us. Display it accordingly.
+ var sourceObj = e.server.me;
+ var destObj = e.server.addTarget(e.params[1]);
+ var displayObj = e.server.parent;
+ }
+
+ client.munger.getRule(".mailto").enabled = displayMailto;
+ displayObj.display(msg, "NOTICE", sourceObj, destObj, e.tags);
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+CIRCUser.prototype.onCTCPAction =
+function my_uaction(e)
+{
+ if (!("messages" in this))
+ {
+ var limit = client.prefs["newTabLimit"];
+ if (limit == 0 || client.viewsArray.length < limit)
+ openQueryTab(e.server, e.user.unicodeName);
+ }
+
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ this.display(e.CTCPData, "ACTION", this, e.server.me, e.tags);
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+CIRCUser.prototype.onUnknownCTCP =
+function my_unkctcp (e)
+{
+ this.parent.parent.display (getMsg(MSG_UNKNOWN_CTCP,
+ [e.CTCPCode, e.CTCPData,
+ e.user.unicodeName]),
+ "BAD-CTCP", this, e.server.me, e.tags);
+}
+
+function onDCCAutoAcceptTimeout(o, folder)
+{
+ // user may have already accepted or declined
+ if (o.state.state != DCC_STATE_REQUESTED)
+ return;
+
+ if (o.TYPE == "IRCDCCChat")
+ {
+ o.accept();
+ display(getMsg(MSG_DCCCHAT_ACCEPTED, o._getParams()), "DCC-CHAT");
+ }
+ else
+ {
+ var dest, leaf, tries = 0;
+ while (true)
+ {
+ leaf = escapeFileName(o.filename);
+ if (++tries > 1)
+ {
+ // A file with the same name as the offered file already exists
+ // in the user's download folder. Add [x] before the extension.
+ // The extension is the last dot to the end of the string,
+ // unless it is one of the special-cased compression extensions,
+ // in which case the second to last dot is used. The second
+ // extension can only contain letters, to avoid mistakes like
+ // "patch-version1[2].0.gz". If no file extension is present,
+ // the [x] is just appended to the filename.
+ leaf = leaf.replace(/(\.[a-z]*\.(gz|bz2|z)|\.[^\.]*|)$/i,
+ "[" + tries + "]$&");
+ }
+
+ dest = getFileFromURLSpec(folder);
+ dest.append(leaf);
+ if (!dest.exists())
+ break;
+ }
+ o.accept(dest);
+ display(getMsg(MSG_DCCFILE_ACCEPTED, o._getParams()), "DCC-FILE");
+ }
+}
+
+CIRCUser.prototype.onDCCChat =
+function my_dccchat(e)
+{
+ if (!client.prefs["dcc.enabled"])
+ return;
+
+ var u = client.dcc.addUser(e.user, e.host);
+ var c = client.dcc.addChat(u, e.port);
+
+ var str = MSG_DCCCHAT_GOT_REQUEST;
+ var cmds = getMsg(MSG_DCC_COMMAND_ACCEPT, "dcc-accept " + c.id) + " " +
+ getMsg(MSG_DCC_COMMAND_DECLINE, "dcc-decline " + c.id);
+
+ var allowList = this.parent.parent.prefs["dcc.autoAccept.list"];
+ for (var m = 0; m < allowList.length; ++m)
+ {
+ if (hostmaskMatches(e.user, getHostmaskParts(allowList[m])))
+ {
+ var acceptDelay = client.prefs["dcc.autoAccept.delay"];
+ if (acceptDelay == 0)
+ {
+ str = MSG_DCCCHAT_ACCEPTING_NOW;
+ }
+ else
+ {
+ str = MSG_DCCCHAT_ACCEPTING;
+ cmds = [(acceptDelay / 1000), cmds];
+ }
+ setTimeout(onDCCAutoAcceptTimeout, acceptDelay, c);
+ break;
+ }
+ }
+
+ client.munger.getRule(".inline-buttons").enabled = true;
+ this.parent.parent.display(getMsg(str, c._getParams().concat(cmds)),
+ "DCC-CHAT", undefined, undefined, e.tags);
+ client.munger.getRule(".inline-buttons").enabled = false;
+
+ // Pass the event over to the DCC Chat object.
+ e.set = "dcc-chat";
+ e.destObject = c;
+ e.destMethod = "onGotRequest";
+}
+
+CIRCUser.prototype.onDCCSend =
+function my_dccsend(e)
+{
+ if (!client.prefs["dcc.enabled"])
+ return;
+
+ var u = client.dcc.addUser(e.user, e.host);
+ var f = client.dcc.addFileTransfer(u, e.port, e.file, e.size);
+
+ var str = MSG_DCCFILE_GOT_REQUEST;
+ var cmds = getMsg(MSG_DCC_COMMAND_ACCEPT, "dcc-accept " + f.id) + " " +
+ getMsg(MSG_DCC_COMMAND_DECLINE, "dcc-decline " + f.id);
+
+ var allowList = this.parent.parent.prefs["dcc.autoAccept.list"];
+ for (var m = 0; m < allowList.length; ++m)
+ {
+ if (hostmaskMatches(e.user, getHostmaskParts(allowList[m]),
+ this.parent))
+ {
+ var acceptDelay = client.prefs["dcc.autoAccept.delay"];
+ if (acceptDelay == 0)
+ {
+ str = MSG_DCCFILE_ACCEPTING_NOW;
+ }
+ else
+ {
+ str = MSG_DCCFILE_ACCEPTING;
+ cmds = [(acceptDelay / 1000), cmds];
+ }
+ setTimeout(onDCCAutoAcceptTimeout, acceptDelay,
+ f, this.parent.parent.prefs["dcc.downloadsFolder"]);
+ break;
+ }
+ }
+
+ client.munger.getRule(".inline-buttons").enabled = true;
+ this.parent.parent.display(getMsg(str,[e.user.unicodeName,
+ e.host, e.port, e.file,
+ getSISize(e.size)].concat(cmds)),
+ "DCC-FILE", undefined, undefined, e.tags);
+ client.munger.getRule(".inline-buttons").enabled = false;
+
+ // Pass the event over to the DCC File object.
+ e.set = "dcc-file";
+ e.destObject = f;
+ e.destMethod = "onGotRequest";
+}
+
+CIRCUser.prototype.onDCCReject =
+function my_dccreject(e)
+{
+ if (!client.prefs["dcc.enabled"])
+ return;
+
+ //FIXME: Uh... cope. //
+
+ // Pass the event over to the DCC Chat object.
+ //e.set = "dcc-file";
+ //e.destObject = f;
+ //e.destMethod = "onGotReject";
+}
+
+CIRCUser.prototype.doAutoPerform =
+function my_autoperform()
+{
+ var cmdary = client.prefs["autoperform.user"].concat(this.prefs["autoperform"]);
+ for (var i = 0; i < cmdary.length; ++i)
+ {
+ if (cmdary[i][0] == "/")
+ this.dispatch(cmdary[i].substr(1));
+ else
+ this.dispatch(cmdary[i]);
+ }
+}
+
+CIRCDCCChat.prototype.onInit =
+function my_dccinit(e)
+{
+}
+
+CIRCDCCChat.prototype._getParams =
+function my_dccgetparams()
+{
+ return [this.unicodeName, this.remoteIP, this.port];
+}
+
+CIRCDCCChat.prototype.onPrivmsg =
+function my_dccprivmsg(e)
+{
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ this.displayHere(toUnicode(e.line, this), "PRIVMSG", e.user, "ME!");
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+CIRCDCCChat.prototype.onCTCPAction =
+function my_uaction(e)
+{
+ client.munger.getRule(".mailto").enabled = client.prefs["munger.mailto"];
+ this.displayHere(e.CTCPData, "ACTION", e.user, "ME!");
+ client.munger.getRule(".mailto").enabled = false;
+}
+
+CIRCDCCChat.prototype.onUnknownCTCP =
+function my_unkctcp(e)
+{
+ this.displayHere(getMsg(MSG_UNKNOWN_CTCP, [e.CTCPCode, e.CTCPData,
+ e.user.unicodeName]),
+ "BAD-CTCP", e.user, "ME!");
+}
+
+CIRCDCCChat.prototype.onConnect =
+function my_dccconnect(e)
+{
+ playEventSounds("dccchat", "connect");
+ this.displayHere(getMsg(MSG_DCCCHAT_OPENED, this._getParams()), "DCC-CHAT");
+}
+
+CIRCDCCChat.prototype.onAbort =
+function my_dccabort(e)
+{
+ this.display(getMsg(MSG_DCCCHAT_ABORTED, this._getParams()), "DCC-CHAT");
+}
+
+CIRCDCCChat.prototype.onFail =
+function my_dccfail(e)
+{
+ this.display(getMsg(MSG_DCCCHAT_FAILED, this._getParams()), "DCC-CHAT");
+}
+
+CIRCDCCChat.prototype.onDisconnect =
+function my_dccdisconnect(e)
+{
+ playEventSounds("dccchat", "disconnect");
+ this.display(getMsg(MSG_DCCCHAT_CLOSED, this._getParams()), "DCC-CHAT");
+}
+
+
+CIRCDCCFileTransfer.prototype.onInit =
+function my_dccfileinit(e)
+{
+ this.busy = false;
+ updateProgress();
+}
+
+CIRCDCCFileTransfer.prototype._getParams =
+function my_dccfilegetparams()
+{
+ var dir = MSG_UNKNOWN;
+
+ if (this.state.dir == DCC_DIR_GETTING)
+ dir = MSG_DCCLIST_FROM;
+
+ if (this.state.dir == DCC_DIR_SENDING)
+ dir = MSG_DCCLIST_TO;
+
+ return [this.filename, dir, this.unicodeName,
+ this.remoteIP, this.port];
+}
+
+CIRCDCCFileTransfer.prototype.onConnect =
+function my_dccfileconnect(e)
+{
+ this.displayHere(getMsg(MSG_DCCFILE_OPENED, this._getParams()), "DCC-FILE");
+ this.busy = true;
+ this.speed = 0;
+ updateProgress();
+ this._lastUpdate = new Date();
+ this._lastPosition = 0;
+ this._lastSpeedTime = new Date();
+}
+
+CIRCDCCFileTransfer.prototype.onProgress =
+function my_dccfileprogress(e)
+{
+ var now = new Date();
+ var pcent = this.progress;
+
+ var tab = getTabForObject(this);
+
+ // If we've moved 100KiB or waited 10s, update the progress bar.
+ if ((this.position > this._lastPosition + 102400) ||
+ (now - this._lastUpdate > 10000))
+ {
+ updateProgress();
+ updateTitle();
+
+ if (tab)
+ tab.setAttribute("label", this.viewName + " (" + pcent + "%)");
+
+ var change = (this.position - this._lastPosition);
+ var speed = change / ((now - this._lastSpeedTime) / 1000); // B/s
+ this._lastSpeedTime = now;
+
+ /* Use an average of the last speed, and this speed, so we get a little
+ * smoothing to it.
+ */
+ this.speed = (this.speed + speed) / 2;
+ this.updateHeader();
+ this._lastPosition = this.position;
+ }
+
+ // If it's also been 10s or more since we last displayed a msg...
+ if (now - this._lastUpdate > 10000)
+ {
+ this._lastUpdate = now;
+
+ var args = [pcent, getSISize(this.position), getSISize(this.size),
+ getSISpeed(this.speed)];
+
+ // We supress this message if the view is hidden.
+ if (tab)
+ this.displayHere(getMsg(MSG_DCCFILE_PROGRESS, args), "DCC-FILE");
+ }
+}
+
+CIRCDCCFileTransfer.prototype.onAbort =
+function my_dccfileabort(e)
+{
+ this.busy = false;
+ updateProgress();
+ updateTitle();
+ this.display(getMsg(MSG_DCCFILE_ABORTED, this._getParams()), "DCC-FILE");
+}
+
+CIRCDCCFileTransfer.prototype.onFail =
+function my_dccfilefail(e)
+{
+ this.busy = false;
+ updateProgress();
+ updateTitle();
+ this.display(getMsg(MSG_DCCFILE_FAILED, this._getParams()), "DCC-FILE");
+}
+
+CIRCDCCFileTransfer.prototype.onDisconnect =
+function my_dccfiledisconnect(e)
+{
+ this.busy = false;
+ updateProgress();
+ this.updateHeader();
+ updateTitle();
+
+ var msg, tab = getTabForObject(this);
+ if (tab)
+ tab.setAttribute("label", this.viewName + " (DONE)");
+
+ if (this.state.dir == DCC_DIR_GETTING)
+ {
+ var localURL = getURLSpecFromFile(this.localPath);
+ var cmd = "dcc-show-file " + localURL;
+ var msgId = (client.platform == "Mac") ? MSG_DCCFILE_CLOSED_SAVED_MAC :
+ MSG_DCCFILE_CLOSED_SAVED;
+ msg = getMsg(msgId, this._getParams().concat(localURL, cmd));
+ }
+ else
+ {
+ msg = getMsg(MSG_DCCFILE_CLOSED_SENT, this._getParams());
+ }
+ client.munger.getRule(".inline-buttons").enabled = true;
+ this.display(msg, "DCC-FILE");
+ client.munger.getRule(".inline-buttons").enabled = false;
+}
+
+var CopyPasteHandler = new Object();
+
+CopyPasteHandler.allowDrop =
+CopyPasteHandler.allowStartDrag =
+CopyPasteHandler.onCopyOrDrag =
+function phand_bogus()
+{
+ return true;
+}
+
+CopyPasteHandler.onPasteOrDrop =
+function phand_onpaste(e, data)
+{
+ // XXXbug 329487: The effect of onPasteOrDrop's return value is actually the
+ // exact opposite of the definition in the IDL.
+
+ // Don't mess with the multiline box at all.
+ if (client.prefs["multiline"])
+ return true;
+
+ var str = new Object();
+ var strlen = new Object();
+ data.getTransferData("text/unicode", str, strlen);
+ str.value.QueryInterface(Components.interfaces.nsISupportsString);
+ str.value.data = str.value.data.replace(/(^\s*[\r\n]+|[\r\n]+\s*$)/g, "");
+
+ // XXX part of what follows is a very ugly hack to make links (with a title)
+ // not open the multiline box. We 'should' be able to ask the transferable
+ // what flavours it supports, but testing showed that by the time we can ask
+ // for that info, it's forgotten about everything apart from text/unicode.
+ var lines = str.value.data.split("\n");
+ var m = lines[0].match(client.linkRE);
+
+ if ((str.value.data.indexOf("\n") == -1) ||
+ (m && (m[0] == lines[0]) && (lines.length == 2)))
+ {
+ // If, after stripping leading/trailing empty lines, the string is a
+ // single line, or it's a link with a title, put it back in
+ // the transferable and return.
+ data.setTransferData("text/unicode", str.value,
+ str.value.data.length * 2);
+ return true;
+ }
+
+ // If it's a drop, move the text cursor to the mouse position.
+ if (e && ("rangeOffset" in e))
+ client.input.setSelectionRange(e.rangeOffset, e.rangeOffset);
+
+ str = client.input.value.substr(0, client.input.selectionStart) +
+ str.value.data + client.input.value.substr(client.input.selectionEnd);
+ client.prefs["multiline"] = true;
+ // We want to auto-collapse after send, so the user is not thrown off by the
+ // "strange" input box if they didn't specifically ask for it:
+ client.multiLineForPaste = true;
+ client.input.value = str;
+ return false;
+}
+
+CopyPasteHandler.QueryInterface =
+function phand_qi(iid)
+{
+ if (iid.equals(Components.interfaces.nsISupports) ||
+ iid.equals(Components.interfaces.nsIClipboardDragDropHooks))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+function UserEntry(userObj, channelListShare)
+{
+ var self = this;
+ function getUName()
+ {
+ return userObj.unicodeName;
+ };
+ function getSortFn()
+ {
+ if (client.prefs["sortUsersByMode"])
+ return ule_sortByMode;
+ return ule_sortByName;
+ };
+
+ // This object is used to represent a user in the userlist. To work with our
+ // JS tree view, it needs a bunch of stuff that is set through the
+ // constructor and the prototype (see also a couple of lines down). Here we
+ // call the original constructor to do some work for us:
+ XULTreeViewRecord.call(this, channelListShare);
+
+ // This magic function means the unicodeName is used for display:
+ this.setColumnPropertyName("usercol", getUName);
+
+ // We need this for sorting by mode (op, hop, voice, etc.)
+ this._userObj = userObj;
+
+ // When the user leaves, we need to have the entry so we can remove it:
+ userObj.chanListEntry = this;
+
+ // Gross hack: we set up the sort function by getter so we don't have to go
+ // back (array sort -> xpc -> our pref lib -> xpc -> pref interfaces) for
+ // every bloody compare. Now it will be a function that doesn't need prefs
+ // after being retrieved, which is much much faster.
+ this.__defineGetter__("sortCompare", getSortFn);
+}
+
+// See explanation in the constructor.
+UserEntry.prototype = XULTreeViewRecord.prototype;
+
+function ule_sortByName(a, b)
+{
+ if (a._userObj.unicodeName == b._userObj.unicodeName)
+ return 0;
+ var aName = a._userObj.unicodeName.toLowerCase();
+ var bName = b._userObj.unicodeName.toLowerCase();
+ return (aName < bName ? -1 : 1);
+}
+
+function ule_sortByMode(a, b)
+{
+ if (a._userObj.sortName == b._userObj.sortName)
+ return 0;
+ var aName = a._userObj.sortName.toLowerCase();
+ var bName = b._userObj.sortName.toLowerCase();
+ return (aName < bName ? -1 : 1);
+}
diff --git a/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.js b/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.js
new file mode 100644
index 0000000000..3116d9a2f3
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.js
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var client;
+var plugin;
+
+function onLoad()
+{
+ client = window.arguments[0];
+ client.installPluginDialog = window;
+ window.getMsg = client.messageManager.getMsg;
+ window.MSG_ALERT = client.mainWindow.MSG_ALERT;
+
+ hookEvent("chk-name-auto", "command", changeAutoName);
+ hookEvent("txt-source", "input", sourceChange);
+ hookEvent("btn-browse", "command", browseForSource);
+
+ // Center on CZ:
+ var ow = client.mainWindow;
+ window.sizeToContent();
+ window.moveTo(ow.screenX + Math.max((ow.outerWidth - window.outerWidth ) / 2, 0),
+ ow.screenY + Math.max((ow.outerHeight - window.outerHeight) / 2, 0));
+}
+
+function changeAutoName(event)
+{
+ var useAutoName = document.getElementById("chk-name-auto");
+ var pluginName = document.getElementById("txt-name");
+ if (useAutoName.checked)
+ {
+ pluginName.setAttribute("disabled", "true");
+ sourceChange(null);
+ }
+ else
+ {
+ pluginName.removeAttribute("disabled");
+ }
+}
+
+function sourceChange(event)
+{
+ var useAutoName = document.getElementById("chk-name-auto");
+ var pluginName = document.getElementById("txt-name");
+ var sourceLoc = document.getElementById("txt-source");
+
+ if (useAutoName.checked)
+ {
+ var ary = sourceLoc.value.match(/([^\/]+?)(\..{0,3}){0,2}$/);
+ pluginName.value = (ary ? ary[1] : sourceLoc.value);
+ }
+}
+
+function browseForSource(event)
+{
+ var rv = pickOpen(client.mainWindow.MSG_INSTALL_PLUGIN_SELECT_SOURCE,
+ "*.js;*.zip;*.jar");
+
+ if (("file" in rv) && rv.file)
+ {
+ rv.path = rv.file.path;
+ rv.spec = rv.picker.fileURL.spec;
+ }
+
+ if (rv.reason == 0)
+ {
+ var sourceLoc = document.getElementById("txt-source");
+ sourceLoc.value = rv.spec;
+ sourceChange(null);
+ }
+}
+
+function doOK()
+{
+ var pluginName = document.getElementById("txt-name");
+ var pluginSource = document.getElementById("txt-source");
+ if (!pluginName.value)
+ {
+ alert(client.mainWindow.MSG_INSTALL_PLUGIN_ERR_SPEC_NAME);
+ return false;
+ }
+
+ client.dispatch("install-plugin", {name: pluginName.value,
+ url: pluginSource.value});
+ delete client.installPluginDialog;
+}
+
+function doCancel()
+{
+ delete client.installPluginDialog;
+}
+
+function hookEvent(id, event, handler)
+{
+ var item = document.getElementById(id);
+ item.addEventListener(event, handler, false);
+}
diff --git a/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.xul b/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.xul
new file mode 100644
index 0000000000..90de4f6105
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/install-plugin/install-plugin.xul
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+
+<!DOCTYPE window SYSTEM "chrome://chatzilla/locale/install-plugin.dtd">
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://chatzilla/skin/install-plugin.css" type="text/css"?>
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml" onload="onLoad();"
+ title="&windowtitle;"
+ ondialogaccept="return doOK()"
+ ondialogcancel="doCancel()">
+ <script src="install-plugin.js"/>
+ <script src="../lib/js/file-utils.js"/>
+ <script src="../lib/js/utils.js"/>
+ <grid>
+ <columns><column/><column flex="1"/></columns>
+ <rows>
+ <row align="center">
+ <label id="lbl-source" accesskey="&source.accesskey;"
+ control="txt-source" value="&source.label;"/>
+ <textbox id="txt-source"/>
+ <button id="btn-browse" label="&browse.label;"
+ accesskey="&browse.accesskey;"/>
+ </row>
+ <row align="center">
+ <label id="lbl-name" control="txt-name"
+ value="&name.label;" accesskey="&name.accesskey;"/>
+ <textbox id="txt-name" disabled="true"/>
+ </row>
+ <row align="center">
+ <spacer/>
+ <checkbox id="chk-name-auto" checked="true"
+ label="&name.autopick.label;"
+ accesskey="&name.autopick.accesskey;"/>
+ </row>
+ </rows>
+ </grid>
+</dialog>
diff --git a/comm/suite/chatzilla/xul/content/menus.js b/comm/suite/chatzilla/xul/content/menus.js
new file mode 100644
index 0000000000..1fecac268d
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/menus.js
@@ -0,0 +1,513 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+function initMenus()
+{
+ function isMotif(name)
+ {
+ return "client.prefs['motif.current'] == " +
+ "client.prefs['motif." + name + "']";
+ };
+
+ function isFontFamily(name)
+ {
+ return "cx.sourceObject.prefs['font.family'] == '" + name + "'";
+ };
+
+ function isFontFamilyCustom()
+ {
+ return "!cx.sourceObject.prefs['font.family']." +
+ "match(/^(default|(sans-)?serif|monospace)$/)";
+ };
+
+ function isFontSize(size)
+ {
+ return "cx.fontSize == cx.fontSizeDefault + " + size;
+ };
+
+ function isFontSizeCustom()
+ {
+ // It's "custom" if it's set (non-zero/not default), not the default
+ // size (medium) and not +/-2 (small/large).
+ return "'fontSize' in cx && cx.fontSize != 0 && " +
+ "cx.fontSizeDefault != cx.fontSize && " +
+ "Math.abs((cx.fontSizeDefault - cx.fontSize) / 2) != 1";
+ };
+
+ function onMenuCommand(event, window)
+ {
+ var commandName = event.originalTarget.getAttribute("commandname");
+ var params = new Object();
+ if ("cx" in client.menuManager && client.menuManager.cx)
+ params = client.menuManager.cx;
+ params.sourceWindow = window;
+ params.source = "menu";
+ params.shiftKey = event.shiftKey;
+
+ dispatch(commandName, params, true);
+
+ delete client.menuManager.cx;
+ };
+
+ client.onMenuCommand = onMenuCommand;
+ client.menuSpecs = new Object();
+ var menuManager = new MenuManager(client.commandManager,
+ client.menuSpecs,
+ getCommandContext,
+ "client.onMenuCommand(event, window);");
+ client.menuManager = menuManager;
+
+ client.menuSpecs["maintoolbar"] = {
+ items:
+ [
+ ["disconnect"],
+ ["quit"],
+ ["part"]
+ ]
+ };
+
+ // OS values
+ var Win = "(client.platform == 'Windows')";
+ var NotWin = "(client.platform != 'Windows')";
+ var Linux = "(client.platform == 'Linux')";
+ var NotLinux = "(client.platform != 'Linux')";
+ var Mac = "(client.platform == 'Mac')";
+ var NotMac = "(client.platform != 'Mac')";
+
+ // IRC specific values
+ var ViewClient = "(cx.TYPE == 'IRCClient')";
+ var ViewNetwork = "(cx.TYPE == 'IRCNetwork')";
+ var ViewChannel = "(cx.TYPE == 'IRCChannel')";
+ var ViewUser = "(cx.TYPE == 'IRCUser')";
+ var ViewDCC = "(cx.TYPE.substr(0, 6) == 'IRCDCC')";
+
+ // IRC specific combinations
+ var ChannelActive = "(" + ViewChannel + " and cx.channel.active)";
+ var ChannelInactive = "(" + ViewChannel + " and !cx.channel.active)";
+ var DCCActive = "(" + ViewDCC + " and cx.sourceObject.isActive())";
+ var NetConnected = "(cx.network and cx.network.isConnected())";
+ var NetDisconnected = "(cx.network and !cx.network.isConnected())";
+
+ client.menuSpecs["mainmenu:chatzilla"] = {
+ label: MSG_MNU_CHATZILLA,
+ accesskey: getAccessKeyForMenu('MSG_MNU_CHATZILLA'),
+ getContext: getDefaultContext,
+ items:
+ [
+ ["cmd-prefs"],
+ ["install-plugin"],
+ ["goto-startup"],
+ ["-"],
+ ["print"],
+ ["save"],
+ ["-", {visibleif: NotMac}],
+ ["exit", {visibleif: Win}],
+ ["quit", {visibleif: NotMac + " and " + NotWin}]
+ ]
+ };
+
+ client.menuSpecs["mainmenu:irc"] = {
+ label: MSG_MNU_IRC,
+ accesskey: getAccessKeyForMenu('MSG_MNU_IRC'),
+ getContext: getDefaultContext,
+ items:
+ [
+ ["join"],
+ ["-"],
+ ["edit-networks"],
+ ["-"],
+ [">popup:views"],
+ [">popup:nickname"],
+ ["-"],
+ ["clear-view"],
+ ["hide-view", {enabledif: "client.viewsArray.length > 1"}],
+ ["toggle-oas",
+ {type: "checkbox",
+ checkedif: "isStartupURL(cx.sourceObject.getURL())"}],
+ ["-"],
+ ["leave", {visibleif: ChannelActive}],
+ ["rejoin", {visibleif: ChannelInactive}],
+ ["dcc-close", {visibleif: DCCActive}],
+ ["delete-view", {visibleif: "!" + ChannelActive + " and !" + DCCActive}],
+ ["disconnect", {visibleif: NetConnected}],
+ ["reconnect", {visibleif: NetDisconnected}],
+ ["-"],
+ ["toggle-text-dir"]
+ ]
+ };
+
+ client.menuSpecs["popup:views"] = {
+ label: MSG_MNU_VIEWS,
+ accesskey: getAccessKeyForMenu('MSG_MNU_VIEWS'),
+ getContext: getViewsContext,
+ items:
+ [
+ ["goto-url", {type: "radio",
+ checkedif: "cx.url == cx.sourceObject.getURL()",
+ repeatfor: "cx.views",
+ repeatgroup: "item.group",
+ repeatmap: "cx.url = item.url; cx.label = item.label"}]
+ ]
+ };
+
+ client.menuSpecs["mainmenu:edit"] = {
+ label: MSG_MNU_EDIT,
+ accesskey: getAccessKeyForMenu('MSG_MNU_EDIT'),
+ getContext: getDefaultContext,
+ items:
+ [
+ ["cmd-undo", {enabledif: "getCommandEnabled('cmd_undo')"}],
+ ["cmd-redo", {enabledif: "getCommandEnabled('cmd_redo')"}],
+ ["-"],
+ ["cmd-cut", {enabledif: "getCommandEnabled('cmd_cut')"}],
+ ["cmd-copy", {enabledif: "getCommandEnabled('cmd_copy')"}],
+ ["cmd-paste", {enabledif: "getCommandEnabled('cmd_paste')"}],
+ ["cmd-delete", {enabledif: "getCommandEnabled('cmd_delete')"}],
+ ["-"],
+ ["cmd-selectall", {enabledif: "getCommandEnabled('cmd_selectAll')"}],
+ ["-"],
+ ["find"],
+ ["find-again", {enabledif: "canFindAgainInPage()"}],
+ ["-"],
+ ["cmd-mozilla-prefs"]
+ ]
+ };
+
+ client.menuSpecs["popup:motifs"] = {
+ label: MSG_MNU_MOTIFS,
+ accesskey: getAccessKeyForMenu('MSG_MNU_MOTIFS'),
+ items:
+ [
+ ["motif-dark",
+ {type: "checkbox",
+ checkedif: isMotif("dark")}],
+ ["motif-light",
+ {type: "checkbox",
+ checkedif: isMotif("light")}],
+ ]
+ };
+
+ client.menuSpecs["mainmenu:view"] = {
+ label: MSG_MNU_VIEW,
+ accesskey: getAccessKeyForMenu('MSG_MNU_VIEW'),
+ getContext: getDefaultContext,
+ items:
+ [
+ ["tabstrip",
+ {type: "checkbox",
+ checkedif: "isVisible('view-tabs')"}],
+ ["header",
+ {type: "checkbox",
+ checkedif: "cx.sourceObject.prefs['displayHeader']"}],
+ ["userlist",
+ {type: "checkbox",
+ checkedif: "isVisible('user-list-box')"}],
+ ["statusbar",
+ {type: "checkbox",
+ checkedif: "isVisible('status-bar')"}],
+ ["-"],
+ [">popup:motifs"],
+ [">popup:fonts"],
+ ["-"],
+ ["toggle-ccm",
+ {type: "checkbox",
+ checkedif: "client.prefs['collapseMsgs']"}],
+ ["toggle-copy",
+ {type: "checkbox",
+ checkedif: "client.prefs['copyMessages']"}],
+ ["toggle-timestamps",
+ {type: "checkbox",
+ checkedif: "cx.sourceObject.prefs['timestamps']"}]
+ ]
+ };
+
+ /* Mac expects a help menu with this ID, and there is nothing we can do
+ * about it. */
+ client.menuSpecs["mainmenu:help"] = {
+ label: MSG_MNU_HELP,
+ accesskey: getAccessKeyForMenu('MSG_MNU_HELP'),
+ domID: "menu_Help",
+ items:
+ [
+ ["-"],
+ ["homepage"],
+ ["faq"],
+ ["-"],
+ ["about", {id: "aboutName"}]
+ ]
+ };
+
+ client.menuSpecs["popup:fonts"] = {
+ label: MSG_MNU_FONTS,
+ accesskey: getAccessKeyForMenu('MSG_MNU_FONTS'),
+ getContext: getFontContext,
+ items:
+ [
+ ["font-size-bigger", {}],
+ ["font-size-smaller", {}],
+ ["-"],
+ ["font-size-default",
+ {type: "checkbox", checkedif: "!cx.fontSize"}],
+ ["font-size-small",
+ {type: "checkbox", checkedif: isFontSize(-2)}],
+ ["font-size-medium",
+ {type: "checkbox", checkedif: isFontSize(0)}],
+ ["font-size-large",
+ {type: "checkbox", checkedif: isFontSize(+2)}],
+ ["font-size-other",
+ {type: "checkbox", checkedif: isFontSizeCustom()}],
+ ["-"],
+ ["font-family-default",
+ {type: "checkbox", checkedif: isFontFamily("default")}],
+ ["font-family-serif",
+ {type: "checkbox", checkedif: isFontFamily("serif")}],
+ ["font-family-sans-serif",
+ {type: "checkbox", checkedif: isFontFamily("sans-serif")}],
+ ["font-family-monospace",
+ {type: "checkbox", checkedif: isFontFamily("monospace")}],
+ ["font-family-other",
+ {type: "checkbox", checkedif: isFontFamilyCustom()}]
+ ]
+ };
+
+ // Me is op.
+ var isop = "(cx.channel.iAmOp()) && ";
+ // Me is op or half-op.
+ var isopish = "(cx.channel.iAmOp() || cx.channel.iAmHalfOp()) && ";
+ // Server has half-ops.
+ var shop = "(cx.server.supports.prefix.indexOf('h') > 0) && ";
+ // User is Me or Me is op.
+ var isoporme = "((cx.user == cx.server.me) || cx.channel.iAmOp()) && ";
+
+ client.menuSpecs["popup:opcommands"] = {
+ label: MSG_MNU_OPCOMMANDS,
+ accesskey: getAccessKeyForMenu('MSG_MNU_OPCOMMANDS'),
+ items:
+ [
+ ["op", {visibleif: isop + "!cx.user.isOp"}],
+ ["deop", {visibleif: isop + "cx.user.isOp"}],
+ ["hop", {visibleif: isop + "!cx.user.isHalfOp"}],
+ ["dehop", {visibleif: isoporme + "cx.user.isHalfOp"}],
+ ["voice", {visibleif: isopish + "!cx.user.isVoice"}],
+ ["devoice", {visibleif: isopish + "cx.user.isVoice"}],
+ ["-"],
+ ["ban", {enabledif: "(" + isop + "1) || (" + isopish + "!cx.user.isOp)"}],
+ ["unban", {enabledif: "(" + isop + "1) || (" + isopish + "!cx.user.isOp)"}],
+ ["kick", {enabledif: "(" + isop + "1) || (" + isopish + "!cx.user.isOp)"}],
+ ["kick-ban", {enabledif: "(" + isop + "1) || (" + isopish + "!cx.user.isOp)"}]
+ ]
+ };
+
+
+ client.menuSpecs["popup:usercommands"] = {
+ label: MSG_MNU_USERCOMMANDS,
+ accesskey: getAccessKeyForMenu('MSG_MNU_USERCOMMANDS'),
+ items:
+ [
+ ["query", {visibleif: "cx.channel && cx.user"}],
+ ["whois", {visibleif: "cx.user"}],
+ ["whowas", {visibleif: "cx.nickname && !cx.user"}],
+ ["ping", {visibleif: "cx.user"}],
+ ["time", {visibleif: "cx.user"}],
+ ["version", {visibleif: "cx.user"}],
+ ["-", {visibleif: "cx.user"}],
+ ["dcc-chat", {visibleif: "cx.user"}],
+ ["dcc-send", {visibleif: "cx.user"}],
+ ]
+ };
+
+
+ client.menuSpecs["context:userlist"] = {
+ getContext: getUserlistContext,
+ items:
+ [
+ ["toggle-usort", {type: "checkbox",
+ checkedif: "client.prefs['sortUsersByMode']"}],
+ ["toggle-umode", {type: "checkbox",
+ checkedif: "client.prefs['showModeSymbols']"}],
+ ["-", {visibleif: "cx.nickname"}],
+ ["label-user", {visibleif: "cx.nickname && (cx.userCount == 1)",
+ header: true}],
+ ["label-user-multi", {visibleif: "cx.nickname && (cx.userCount != 1)",
+ header: true}],
+ [">popup:opcommands", {visibleif: "cx.nickname",
+ enabledif: isopish + "true"}],
+ [">popup:usercommands", {visibleif: "cx.nickname",
+ enabledif: "cx.userCount == 1"}],
+ ]
+ };
+
+ var urlenabled = "has('url')";
+ var urlexternal = "has('url') && cx.url.search(/^ircs?:/i) == -1";
+ var textselected = "getCommandEnabled('cmd_copy')";
+
+ client.menuSpecs["context:messages"] = {
+ getContext: getMessagesContext,
+ items:
+ [
+ ["goto-url", {visibleif: urlenabled}],
+ ["goto-url-newwin", {visibleif: urlexternal}],
+ ["goto-url-newtab", {visibleif: urlexternal}],
+ ["cmd-copy-link-url", {visibleif: urlenabled}],
+ ["cmd-copy", {visibleif: "!" + urlenabled, enabledif: textselected }],
+ ["cmd-selectall", {visibleif: "!" + urlenabled }],
+ ["websearch", {visibleif: textselected}],
+ ["-", {visibleif: "cx.nickname"}],
+ ["label-user", {visibleif: "cx.nickname", header: true}],
+ [">popup:opcommands", {visibleif: "cx.channel && cx.nickname",
+ enabledif: isopish + "cx.user"}],
+ [">popup:usercommands", {visibleif: "cx.nickname"}],
+ ["-"],
+ ["clear-view"],
+ ["hide-view", {enabledif: "client.viewsArray.length > 1"}],
+ ["toggle-oas",
+ {type: "checkbox",
+ checkedif: "isStartupURL(cx.sourceObject.getURL())"}],
+ ["-"],
+ ["leave", {visibleif: ChannelActive}],
+ ["rejoin", {visibleif: ChannelInactive}],
+ ["dcc-close", {visibleif: DCCActive}],
+ ["delete-view", {visibleif: "!" + ChannelActive + " and !" + DCCActive}],
+ ["disconnect", {visibleif: NetConnected}],
+ ["reconnect", {visibleif: NetDisconnected}],
+ ["-"],
+ ["toggle-text-dir"]
+ ]
+ };
+
+ client.menuSpecs["context:tab"] = {
+ getContext: getTabContext,
+ items:
+ [
+ ["clear-view"],
+ ["hide-view", {enabledif: "client.viewsArray.length > 1"}],
+ ["toggle-oas",
+ {type: "checkbox",
+ checkedif: "isStartupURL(cx.sourceObject.getURL())"}],
+ ["-"],
+ ["leave", {visibleif: ChannelActive}],
+ ["rejoin", {visibleif: ChannelInactive}],
+ ["dcc-close", {visibleif: DCCActive}],
+ ["delete-view", {visibleif: "!" + ChannelActive + " and !" + DCCActive}],
+ ["disconnect", {visibleif: NetConnected}],
+ ["reconnect", {visibleif: NetDisconnected}],
+ ["-"],
+ ["rename"],
+ ["-"],
+ ["toggle-text-dir"]
+ ]
+ };
+
+ client.menuSpecs["context:edit"] = {
+ getContext: getDefaultContext,
+ items:
+ [
+ ["cmd-undo", {enabledif: "getCommandEnabled('cmd_undo')"}],
+ ["-"],
+ ["cmd-cut", {enabledif: "getCommandEnabled('cmd_cut')"}],
+ ["cmd-copy", {enabledif: "getCommandEnabled('cmd_copy')"}],
+ ["cmd-paste", {enabledif: "getCommandEnabled('cmd_paste')"}],
+ ["cmd-delete", {enabledif: "getCommandEnabled('cmd_delete')"}],
+ ["-"],
+ ["cmd-selectall", {enabledif: "getCommandEnabled('cmd_selectAll')"}]
+ ]
+ }
+
+ // Gross hacks to figure out if we're away:
+ var netAway = "cx.network.prefs['away']";
+ var cliAway = "client.prefs['away']";
+ var awayCheckNet = "(cx.network and (" + netAway + " == item.message))";
+ var awayCheckCli = "(!cx.network and (" + cliAway + " == item.message))";
+ var awayChecked = awayCheckNet + " or " + awayCheckCli;
+ var areBack = "(cx.network and !" + netAway + ") or " +
+ "(!cx.network and !" + cliAway + ")";
+
+ client.menuSpecs["mainmenu:nickname"] = {
+ label: client.prefs["nickname"],
+ domID: "server-nick",
+ getContext: getDefaultContext,
+ items:
+ [
+ ["nick"],
+ ["-"],
+ ["back", {type: "checkbox", checkedif: areBack}],
+ ["away", {type: "checkbox",
+ checkedif: awayChecked,
+ repeatfor: "client.awayMsgs",
+ repeatmap: "cx.reason = item.message" }],
+ ["-"],
+ ["custom-away"]
+ ]
+ };
+
+ client.menuSpecs["popup:nickname"] = {
+ label: MSG_STATUS,
+ accesskey: getAccessKeyForMenu('MSG_STATUS'),
+ getContext: getDefaultContext,
+ items: client.menuSpecs["mainmenu:nickname"].items
+ };
+
+}
+
+function createMenus()
+{
+ client.menuManager.createMenus(document, "mainmenu");
+ client.menuManager.createContextMenus(document);
+}
+
+function getCommandContext (id, event)
+{
+ var cx = { originalEvent: event };
+
+ if (id in client.menuSpecs)
+ {
+ if ("getContext" in client.menuSpecs[id])
+ cx = client.menuSpecs[id].getContext(cx);
+ else if ("cx" in client.menuManager)
+ {
+ //dd ("using existing context");
+ cx = client.menuManager.cx;
+ }
+ else
+ {
+ //no context.
+ }
+ }
+ else
+ {
+ dd ("getCommandContext: unknown menu id " + id);
+ }
+
+ if (typeof cx == "object")
+ {
+ if (!("menuManager" in cx))
+ cx.menuManager = client.menuManager;
+ if (!("contextSource" in cx))
+ cx.contextSource = id;
+ if ("dbgContexts" in client && client.dbgContexts)
+ dd ("context '" + id + "'\n" + dumpObjectTree(cx));
+ }
+
+ return cx;
+}
+
+/**
+ * Gets an accesskey for the menu with label string ID labelString.
+ * At first, we attempt to extract it from the label string, otherwise
+ * we fall back to using a separate string.
+ *
+ * @param labelString the id for the locale string corresponding to the label
+ * @return the accesskey for the menu.
+ */
+function getAccessKeyForMenu(labelString)
+{
+ var rv = getAccessKey(window[labelString]);
+ if (!rv)
+ rv = window[labelString + "_ACCESSKEY"] || "";
+ return rv;
+}
+
+
diff --git a/comm/suite/chatzilla/xul/content/menus.xul b/comm/suite/chatzilla/xul/content/menus.xul
new file mode 100644
index 0000000000..3cf2e34d13
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/menus.xul
@@ -0,0 +1,97 @@
+<?xml version="1.0"?>
+
+<!--
+ -
+ - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/chatzilla.dtd" >
+
+<overlay id="chatzilla-menu-overlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <overlaytarget id="menu-overlay-target">
+
+ <!-- parents for the command manager-managed objects -->
+ <keyset id="dynamic-keys"/>
+ <popupset id="dynamic-popups"/>
+
+ <!-- tooltip thingy -->
+ <tooltip id="html-tooltip-node" onpopupshowing="return onTooltip(event);"/>
+ <tooltip id="xul-tooltip-node" onpopupshowing="return onTooltip(event);"/>
+
+ <!-- Commands -->
+ <commandset id="chatzilla-commands">
+
+ <!-- Edit commands -->
+ <commandset id="selectEditMenuItems"/>
+ <commandset id="globalEditMenuItems"/>
+ <commandset id="undoEditMenuItems"/>
+ <commandset id="clipboardEditMenuItems"/>
+ <command id="cmd_undo"/>
+ <command id="cmd_redo"/>
+ <command id="cmd_cut"/>
+ <command id="cmd_copy"/>
+ <command id="cmd_paste"/>
+ <command id="cmd_delete"/>
+ <command id="cmd_selectAll"/>
+
+ <!-- Tasks commands, from overlay -->
+ <commandset id="tasksCommands"/>
+ </commandset>
+
+ <!-- Keys -->
+
+ <keyset id="chatzillaKeys">
+ <key id="key:reloadui" modifiers="accel alt" key="R"
+ oncommand="if (typeof cmdReloadUI =='function') cmdReloadUI(); else window.location.href = window.location.href;"/>
+
+ <!-- Edit keys -->
+ <key id="key_undo"/>
+ <key id="key_redo"/>
+ <key id="key_cut"/>
+ <key id="key_copy"/>
+ <key id="key_paste"/>
+ <key id="key_delete"/>
+ <key id="key_selectAll"/>
+
+ <!-- Tasks keys, from overlay -->
+ <keyset id="tasksKeys"/>
+ </keyset>
+
+ <!-- Main menu bar -->
+ <toolbox flex="1" id="main-toolbox">
+ <menubar id="mainmenu" persist="collapsed"
+ grippytooltiptext="&Menubar.tooltip;">
+
+ <!-- ChatZilla menu placeholder, see menus.js -->
+ <menu id="mainmenu:chatzilla"><menupopup/></menu>
+
+ <!-- IRC menu placeholder, see menus.js -->
+ <menu id="mainmenu:irc"><menupopup/></menu>
+
+ <!-- Edit menu placeholder, see menus.js -->
+ <menu id="mainmenu:edit"><menupopup/></menu>
+
+ <!-- View menu placeholder, see menus.js -->
+ <menu id="mainmenu:view"><menupopup/></menu>
+
+ <!-- Tasks menu -->
+ <menu id="tasksMenu"/>
+
+ <!-- Window menu -->
+ <menu id="windowMenu"/>
+
+ <!-- Help menu -->
+ <!-- Mac expects a help menu with this ID, and there is nothing we can
+ do about it. -->
+ <menu id="menu_Help"/>
+ </menubar>
+
+ </toolbox>
+
+ </overlaytarget>
+
+</overlay>
+
diff --git a/comm/suite/chatzilla/xul/content/messages.js b/comm/suite/chatzilla/xul/content/messages.js
new file mode 100644
index 0000000000..b0eb1822ee
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/messages.js
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+function initMessages()
+{
+ var path = "chrome://chatzilla/locale/chatzilla.properties";
+
+ client.messageManager = new MessageManager(client.entities);
+ client.messageManager.enableHankakuToZenkaku = true;
+ client.messageManager.loadBrands();
+ client.defaultBundle = client.messageManager.addBundle(path);
+
+ client.viewName = client.unicodeName = MSG_CLIENT_NAME;
+ client.responseCodeMap =
+ {
+ "HELLO": MSG_RSP_HELLO,
+ "HELP" : MSG_RSP_HELP,
+ "USAGE": MSG_RSP_USAGE,
+ "ERROR": MSG_RSP_ERROR,
+ "WARNING": MSG_RSP_WARN,
+ "INFO": MSG_RSP_INFO,
+ "EVAL-IN": MSG_RSP_EVIN,
+ "EVAL-OUT": MSG_RSP_EVOUT,
+ "DISCONNECT": MSG_RSP_DISCONNECT,
+ "JOIN": "-->|",
+ "PART": "<--|",
+ "QUIT": "|<--",
+ "NICK": "=-=",
+ "TOPIC": "=-=",
+ "KICK": "=-=",
+ "MODE": "=-=",
+ "END_STATUS": "---",
+ "DCC-CHAT": "[DCC]",
+ "DCC-FILE": "[DCC]",
+ "315": "---", /* end of WHO */
+ "318": "---", /* end of WHOIS */
+ "366": "---", /* end of NAMES */
+ "376": "---" /* end of MOTD */
+ };
+}
+
+function checkCharset(charset)
+{
+ return client.messageManager.checkCharset(charset);
+}
+
+function toUnicode (msg, charsetOrView)
+{
+ if (!msg)
+ return msg;
+
+ var charset;
+ if (typeof charsetOrView == "object")
+ charset = charsetOrView.prefs["charset"];
+ else if (typeof charsetOrView == "string")
+ charset = charsetOrView;
+ else
+ charset = client.currentObject.prefs["charset"];
+
+ return client.messageManager.toUnicode(msg, charset);
+}
+
+function fromUnicode (msg, charsetOrView)
+{
+ if (!msg)
+ return msg;
+
+ var charset;
+ if (typeof charsetOrView == "object")
+ charset = charsetOrView.prefs["charset"];
+ else if (typeof charsetOrView == "string")
+ charset = charsetOrView;
+ else
+ charset = client.currentObject.prefs["charset"];
+
+ return client.messageManager.fromUnicode(msg, charset);
+}
+
+function getMsg(msgName, params, deflt)
+{
+ return client.messageManager.getMsg(msgName, params, deflt);
+}
+
+function getMsgFrom(bundle, msgName, params, deflt)
+{
+ return client.messageManager.getMsgFrom(bundle, msgName, params, deflt);
+}
+
+/* message types, don't localize */
+const MT_ATTENTION = "ATTENTION";
+const MT_ERROR = "ERROR";
+const MT_HELLO = "HELLO";
+const MT_HELP = "HELP";
+const MT_MODE = "MODE";
+const MT_WARN = "WARNING";
+const MT_INFO = "INFO";
+const MT_USAGE = "USAGE";
+const MT_STATUS = "STATUS";
+const MT_EVALIN = "EVAL-IN";
+const MT_EVALOUT = "EVAL-OUT";
+
diff --git a/comm/suite/chatzilla/xul/content/mungers.js b/comm/suite/chatzilla/xul/content/mungers.js
new file mode 100644
index 0000000000..dce8e9980f
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/mungers.js
@@ -0,0 +1,904 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+/* This file contains the munger functions and rules used by ChatZilla.
+ * It's generally a bad idea to call munger functions inside ChatZilla for
+ * anything but munging (chat) output.
+ */
+
+function initMunger()
+{
+ /* linkRE: the general URL linkifier regular expression:
+ *
+ * - start with whitespace, non-word, or begining-of-line
+ * - then match:
+ * - EITHER scheme (word + hyphen), colon, then lots of non-whitespace
+ * - OR "www" followed by at least 2 sets of:
+ * - "." plus some non-whitespace, non-"." characters
+ * - must end match with a word-break
+ * - include a "/" or "=" beyond break if present
+ * - end with whitespace, non-word, or end-of-line
+ */
+ client.linkRE =
+ /(?:\W|^)((?:(\w[\w-]+):[^\s]+|www(\.[^.\s]+){2,})\b[\/=\)]?)(?=\s|\W|$)/;
+
+ // Colours: \x03, with optional foreground and background colours
+ client.colorRE = /(\x03((\d{1,2})(,\d{1,2}|)|))/;
+
+ client.whitespaceRE = new RegExp("(\\S{" + client.MAX_WORD_DISPLAY + ",})");
+
+ const LOW_PRIORITY = 5;
+ const NORMAL_PRIORITY = 10;
+ const HIGH_PRIORITY = 15;
+ const HIGHER_PRIORITY = 20;
+
+ var munger = client.munger = new CMunger(insertText);
+ // Special internal munger!
+ munger.addRule(".inline-buttons", /(\[\[.*?\]\])/, insertInlineButton,
+ HIGH_PRIORITY, LOW_PRIORITY, false);
+ munger.addRule("quote", /(``|'')/, insertQuote,
+ NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule("bold", /(?:[\s(\[]|^)(\*[^*()]*\*)(?:[\s\]).,;!\?]|$)/,
+ "chatzilla-bold", NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule("underline", /(?:[\s(\[]|^)(\_[^_()]*\_)(?:[\s\]).,;!\?]|$)/,
+ "chatzilla-underline", NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule("italic", /(?:\s|^)(\/[^\/()]*\/)(?:[\s.,]|$)/,
+ "chatzilla-italic", NORMAL_PRIORITY, NORMAL_PRIORITY);
+ /* allow () chars inside |code()| blocks */
+ munger.addRule("teletype", /(?:\s|^)(\|[^|]*\|)(?:[\s.,]|$)/,
+ "chatzilla-teletype", NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule(".mirc-colors", client.colorRE, mircChangeColor,
+ NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule(".mirc-bold", /(\x02)/, mircToggleBold,
+ NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule(".mirc-underline", /(\x1f)/, mircToggleUnder,
+ NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule(".mirc-color-reset", /(\x0f)/, mircResetColor,
+ NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule(".mirc-reverse", /(\x16)/, mircReverseColor,
+ NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule(".ansi-escape-sgr", /(\x1b\[([\d;]*)m)/,
+ ansiEscapeSGR, NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule("ctrl-char", /([\x01-\x1f])/, showCtrlChar,
+ NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule("link", client.linkRE, insertLink, NORMAL_PRIORITY, HIGH_PRIORITY);
+
+ // This has a higher starting priority so as to get it to match before the
+ // normal link, which won't know about mailto and then fail.
+ munger.addRule(".mailto",
+ /(?:\W|^)((mailto:)?[^:;\\<>\[\]()\'\"\s\u201d]+@[^.<>\[\]()\'\"\s\u201d]+\.[^<>\[\]()\'\"\s\u201d]+)/i,
+ insertMailToLink, NORMAL_PRIORITY, HIGHER_PRIORITY, false);
+
+ addBugzillaLinkMungerRule(client.prefs["bugKeyword"], NORMAL_PRIORITY, NORMAL_PRIORITY);
+
+ munger.addRule("channel-link",
+ /(?:[^\w#]|^)[@%+]?(#[^<>,\[\](){}\"\s\u201d]*[^:,.<>\[\](){}\'\"\s\u201d])/i,
+ insertChannelLink, NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule("talkback-link", /(?:\W|^)(TB\d{8,}[A-Z]?)(?:\W|$)/,
+ insertTalkbackLink, NORMAL_PRIORITY, NORMAL_PRIORITY);
+
+ munger.addRule("face",
+ /((^|\s)(?:[>O]?[B8=:;(xX%][~']?[-^v"]?(?:[)|(PpSs0oO#\?\*\[\]\/\\]|D+)|>[-^v]?\)|[oO9][._][oO9])(\s|$))/,
+ insertSmiley, NORMAL_PRIORITY, NORMAL_PRIORITY);
+ munger.addRule("rheet", /(?:\W|^)(rhee+t\!*)(?:\s|$)/i, insertRheet, 10, 10);
+ munger.addRule("word-hyphenator", client.whitespaceRE,
+ insertHyphenatedWord, LOW_PRIORITY, NORMAL_PRIORITY);
+
+ client.enableColors = client.prefs["munger.colorCodes"];
+ var branch = client.prefManager.prefBranch;
+ for (var entry in munger.entries)
+ {
+ if (!isinstance(munger.entries[entry], Object))
+ continue;
+
+ for (var rule in munger.entries[entry])
+ {
+ if (rule[0] == ".")
+ continue;
+
+ try
+ {
+ munger.entries[entry][rule].enabled =
+ branch.getBoolPref("munger." + rule);
+ }
+ catch (ex)
+ {
+ // nada
+ }
+ }
+ }
+}
+
+function addBugzillaLinkMungerRule(keywords, priority, startPriority)
+{
+ client.munger.addRule("bugzilla-link",
+ new RegExp("(?:\\W|^)(("+keywords+")\\s+(?:#?\\d+|#[^\\s,]{1,20})(?:\\s+comment\\s+#?\\d+)?)","i"),
+ insertBugzillaLink, priority, startPriority);
+
+}
+
+function insertLink(matchText, containerTag, data, mungerEntry)
+{
+ var href;
+ var linkText;
+
+ var trailing;
+ ary = matchText.match(/([.,?\)]+)$/);
+ if (ary)
+ {
+ linkText = RegExp.leftContext;
+ trailing = ary[1];
+
+ // We special-case links that end with (something), often found on wikis
+ // if "trailing" starts with ) and there's an unclosed ( in the
+ // "linkText"; then we put the final ) back in
+ if ((trailing.indexOf(")") == 0) && (linkText.match(/\([^\)]*$/)))
+ {
+
+ linkText += ")";
+ trailing = trailing.substr(1);
+ }
+ }
+ else
+ {
+ linkText = matchText;
+ }
+
+ var ary = linkText.match(/^(\w[\w-]+):/);
+ if (ary)
+ {
+ if (!client.checkURLScheme(ary[1]))
+ {
+ mungerEntry.enabled = false;
+ client.munger.munge(matchText, containerTag, data);
+ mungerEntry.enabled = true;
+ return;
+ }
+
+ href = linkText;
+ }
+ else
+ {
+ href = "http://" + linkText;
+ }
+
+ /* This gives callers to the munger control over URLs being logged; the
+ * channel topic munger uses this, as well as the "is important" checker.
+ * If either of |dontLogURLs| or |noStateChange| is present and true, we
+ * don't log.
+ */
+ if ((!("dontLogURLs" in data) || !data.dontLogURLs) &&
+ (!("noStateChange" in data) || !data.noStateChange) &&
+ client.urlLogger)
+ {
+ client.urlLogger.append(href);
+ }
+
+ var anchor = document.createElementNS(XHTML_NS, "html:a");
+ var mircRE = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g;
+ anchor.setAttribute("href", href.replace(mircRE, ""));
+
+ // Carry over formatting.
+ var otherFormatting = calcClass(data);
+ if (otherFormatting)
+ anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
+ else
+ anchor.setAttribute("class", "chatzilla-link");
+
+ anchor.setAttribute("target", "_content");
+ mungerEntry.enabled = false;
+ data.inLink = true;
+ client.munger.munge(linkText, anchor, data);
+ mungerEntry.enabled = true;
+ delete data.inLink;
+ containerTag.appendChild(anchor);
+ if (trailing)
+ insertText(trailing, containerTag, data);
+
+}
+
+function insertMailToLink(matchText, containerTag, eventData, mungerEntry)
+{
+ if (("inLink" in eventData) && eventData.inLink)
+ {
+ mungerEntry.enabled = false;
+ client.munger.munge(matchText, containerTag, eventData);
+ mungerEntry.enabled = true;
+ return;
+ }
+
+ var href;
+
+ if (matchText.toLowerCase().indexOf("mailto:") != 0)
+ href = "mailto:" + matchText;
+ else
+ href = matchText;
+
+ var anchor = document.createElementNS(XHTML_NS, "html:a");
+ var mircRE = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g;
+ anchor.setAttribute("href", href.replace(mircRE, ""));
+
+ // Carry over formatting.
+ var otherFormatting = calcClass(eventData);
+ if (otherFormatting)
+ anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
+ else
+ anchor.setAttribute("class", "chatzilla-link");
+
+ //anchor.setAttribute ("target", "_content");
+ mungerEntry.enabled = false;
+ eventData.inLink = true;
+ client.munger.munge(matchText, anchor, eventData);
+ mungerEntry.enabled = true;
+ delete eventData.inLink;
+ containerTag.appendChild(anchor);
+
+}
+
+function insertChannelLink(matchText, containerTag, eventData, mungerEntry)
+{
+ if (("inLink" in eventData) && eventData.inLink)
+ {
+ mungerEntry.enabled = false;
+ client.munger.munge(matchText, containerTag, eventData);
+ mungerEntry.enabled = true;
+ return;
+ }
+
+ var bogusChannels =
+ /^#(include|error|define|if|ifdef|else|elsif|endif)$/i;
+
+ if (!("network" in eventData) || !eventData.network ||
+ matchText.search(bogusChannels) != -1)
+ {
+ containerTag.appendChild(document.createTextNode(matchText));
+ return;
+ }
+
+ var linkText = removeColorCodes(matchText);
+ var encodedLinkText = fromUnicode(linkText, eventData.sourceObject);
+ var anchor = document.createElementNS(XHTML_NS, "html:a");
+ anchor.setAttribute("href", eventData.network.getURL(encodedLinkText));
+
+ // Carry over formatting.
+ var otherFormatting = calcClass(eventData);
+ if (otherFormatting)
+ anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
+ else
+ anchor.setAttribute("class", "chatzilla-link");
+
+ mungerEntry.enabled = false;
+ eventData.inLink = true;
+ client.munger.munge(matchText, anchor, eventData);
+ mungerEntry.enabled = true;
+ delete eventData.inLink;
+ containerTag.appendChild(anchor);
+}
+
+function insertTalkbackLink(matchText, containerTag, eventData, mungerEntry)
+{
+ if (("inLink" in eventData) && eventData.inLink)
+ {
+ mungerEntry.enabled = false;
+ client.munger.munge(matchText, containerTag, eventData);
+ mungerEntry.enabled = true;
+ return;
+ }
+
+ var anchor = document.createElementNS(XHTML_NS, "html:a");
+
+ anchor.setAttribute("href", "http://talkback-public.mozilla.org/" +
+ "search/start.jsp?search=2&type=iid&id=" + matchText);
+
+ // Carry over formatting.
+ var otherFormatting = calcClass(eventData);
+ if (otherFormatting)
+ anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
+ else
+ anchor.setAttribute("class", "chatzilla-link");
+
+ mungerEntry.enabled = false;
+ client.munger.munge(matchText, anchor, eventData);
+ mungerEntry.enabled = true;
+ containerTag.appendChild(anchor);
+}
+
+function insertBugzillaLink (matchText, containerTag, eventData, mungerEntry)
+{
+ if (("inLink" in eventData) && eventData.inLink)
+ {
+ mungerEntry.enabled = false;
+ client.munger.munge(matchText, containerTag, eventData);
+ mungerEntry.enabled = true;
+ return;
+ }
+
+ var prefs = client.prefs;
+ if (eventData.channel)
+ prefs = eventData.channel.prefs;
+ else if (eventData.network)
+ prefs = eventData.network.prefs;
+
+ var bugURL = prefs["bugURL"];
+ var bugURLcomment = prefs["bugURL.comment"];
+
+ if (bugURL.length > 0)
+ {
+ var idOrAlias = matchText.match(new RegExp("(?:"+client.prefs["bugKeyword"]+")\\s+#?(\\d+|[^\\s,]{1,20})","i"))[1];
+ bugURL = bugURL.replace("%s", idOrAlias);
+
+ var commentNum = matchText.match(/comment\s+#?(\d+)/i);
+ if (commentNum)
+ {
+ /* If the comment is a complete URL, use only that, replacing %1$s
+ * and %2$s with the bug number and comment number, respectively.
+ * Otherwise, append the comment preference to the main one,
+ * replacing just %s in each.
+ */
+ if (bugURLcomment.match(/^\w+:/))
+ {
+ bugURL = bugURLcomment;
+ bugURL = bugURL.replace("%1$s", idOrAlias);
+ bugURL = bugURL.replace("%2$s", commentNum[1]);
+ }
+ else
+ {
+ bugURL += bugURLcomment.replace("%s", commentNum[1]);
+ }
+ }
+
+ var anchor = document.createElementNS(XHTML_NS, "html:a");
+ anchor.setAttribute("href", bugURL);
+ // Carry over formatting.
+ var otherFormatting = calcClass(eventData);
+ if (otherFormatting)
+ anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
+ else
+ anchor.setAttribute("class", "chatzilla-link");
+
+ anchor.setAttribute("target", "_content");
+ mungerEntry.enabled = false;
+ eventData.inLink = true;
+ client.munger.munge(matchText, anchor, eventData);
+ mungerEntry.enabled = true;
+ delete eventData.inLink;
+ containerTag.appendChild(anchor);
+ }
+ else
+ {
+ mungerEntry.enabled = false;
+ client.munger.munge(matchText, containerTag, eventData);
+ mungerEntry.enabled = true;
+ }
+}
+
+function insertRheet(matchText, containerTag, eventData, mungerEntry)
+{
+ if (("inLink" in eventData) && eventData.inLink)
+ {
+ mungerEntry.enabled = false;
+ client.munger.munge(matchText, containerTag, eventData);
+ mungerEntry.enabled = true;
+ return;
+ }
+
+ var anchor = document.createElementNS(XHTML_NS, "html:a");
+ anchor.setAttribute("href",
+ "http://ftp.mozilla.org/pub/mozilla.org/mozilla/libraries/bonus-tracks/rheet.wav");
+ anchor.setAttribute("class", "chatzilla-rheet chatzilla-link");
+ //anchor.setAttribute ("target", "_content");
+ insertText(matchText, anchor, eventData);
+ containerTag.appendChild(anchor);
+}
+
+function insertQuote (matchText, containerTag)
+{
+ if (matchText == "``")
+ containerTag.appendChild(document.createTextNode("\u201c"));
+ else
+ containerTag.appendChild(document.createTextNode("\u201d"));
+ containerTag.appendChild(document.createElementNS(XHTML_NS, "html:wbr"));
+}
+
+function insertSmiley(emoticon, containerTag, eventData, mungerEntry)
+{
+ let smilies = {
+ "face-alien": "\uD83D\uDC7D",
+ "face-lol": "\uD83D\uDE02",
+ "face-laugh": "\uD83D\uDE04",
+ "face-sweat_smile": "\uD83D\uDE05",
+ "face-innocent": "\uD83D\uDE07",
+ "face-evil": "\uD83D\uDE08",
+ "face-wink": "\uD83D\uDE09",
+ "face-smile": "\uD83D\uDE0A",
+ "face-cool": "\uD83D\uDE0E",
+ "face-neutral": "\uD83D\uDE10",
+ "face-thinking": "\uD83D\uDE14",
+ "face-confused": "\uD83D\uDE15",
+ "face-kissing": "\uD83D\uDE17",
+ "face-tongue": "\uD83D\uDE1B",
+ "face-worried": "\uD83D\uDE1F",
+ "face-angry": "\uD83D\uDE20",
+ "face-cry": "\uD83D\uDE22",
+ "face-surprised": "\uD83D\uDE2D",
+ "face-eek": "\uD83D\uDE31",
+ "face-red": "\uD83D\uDE33",
+ "face-dizzy": "\uD83D\uDE35",
+ "face-sad": "\uD83D\uDE41",
+ "face-rolleyes": "\uD83D\uDE44",
+ "face-zipped": "\uD83E\uDD10",
+ "face-rofl": "\uD83E\uDD23",
+ "face-woozy": "\uD83E\uDD74",
+ };
+
+ let type;
+
+ if (emoticon.search(/\>[-^v]?\)/) != -1)
+ type = "face-alien";
+ else if (emoticon.search(/\>[=:;][-^v]?[(|]|[Xx][-^v]?[(\[]/) != -1)
+ type = "face-angry";
+ else if (emoticon.search(/[=:;][-^v]?[Ss]/) != -1)
+ type = "face-confused";
+ else if (emoticon.search(/[B8][-^v]?[)\]]/) != -1)
+ type = "face-cool";
+ else if (emoticon.search(/[=:;][~'][-^v]?\(/) != -1)
+ type = "face-cry";
+ else if (emoticon.search(/o[._]O|O[._]o/) != -1)
+ type = "face-dizzy";
+ else if (emoticon.search(/o[._]o|O[._]O/) != -1)
+ type = "face-eek";
+ else if (emoticon.search(/\>[=:;][-^v]?D/) != -1)
+ type = "face-evil";
+ else if (emoticon.search(/O[=:][-^v]?[)]/) != -1)
+ type = "face-innocent";
+ else if (emoticon.search(/[=:;][-^v]?[*]/) != -1)
+ type = "face-kissing";
+ else if (emoticon.search(/[=:;][-^v]?DD/) != -1)
+ type = "face-lol";
+ else if (emoticon.search(/[=:;][-^v]?D/) != -1)
+ type = "face-laugh";
+ else if (emoticon.search(/\([-^v]?D|[xX][-^v]?D/) != -1)
+ type = "face-rofl";
+ else if (emoticon.search(/[=:;][-^v]?\|/) != -1)
+ type = "face-neutral";
+ else if (emoticon.search(/[=:;][-^v]?\?/) != -1)
+ type = "face-thinking";
+ else if (emoticon.search(/[=:;]"[)\]]/) != -1)
+ type = "face-red";
+ else if (emoticon.search(/9[._]9/) != -1)
+ type = "face-rolleyes";
+ else if (emoticon.search(/[=:;][-^v]?[(\[]/) != -1)
+ type = "face-sad";
+ else if (emoticon.search(/[=:][-^v]?[)]/) != -1)
+ type = "face-smile";
+ else if (emoticon.search(/[=:;][-^v]?[0oO]/) != -1)
+ type = "face-surprised";
+ else if (emoticon.search(/[=:][-^v]?[\]]/) != -1)
+ type = "face-sweat_smile";
+ else if (emoticon.search(/[=:;][-^v]?[pP]/) != -1)
+ type = "face-tongue";
+ else if (emoticon.search(/;[-^v]?[)\]]/) != -1)
+ type = "face-wink";
+ else if (emoticon.search(/%[-^v][)\]]/) != -1)
+ type = "face-woozy";
+ else if (emoticon.search(/[=:;][-^v]?[\/\\]/) != -1)
+ type = "face-worried";
+ else if (emoticon.search(/[=:;][-^v]?[#]/) != -1)
+ type = "face-zipped";
+
+ let glyph = smilies[type];
+ if (!glyph) {
+ // We didn't actually match anything, so it'll be a too-generic match
+ // from the munger RegExp.
+ mungerEntry.enabled = false;
+ client.munger.munge(emoticon, containerTag, eventData);
+ mungerEntry.enabled = true;
+ return;
+ }
+
+ // Add spaces to beginning / end where appropriate.
+ if (emoticon.search(/^\s/) != -1)
+ glyph = " " + glyph;
+ if (emoticon.search(/\s$/) != -1)
+ glyph = glyph + " ";
+
+ // Create a span to hold the emoticon.
+ let span = document.createElementNS(XHTML_NS, "html:span");
+ span.appendChild(document.createTextNode(glyph));
+ span.setAttribute("class", "chatzilla-emote-txt");
+ // Add the title attribute (to show the original text in a tooltip) in case
+ // the replacement was done incorrectly.
+ span.setAttribute("title", emoticon);
+ span.setAttribute("type", type);
+ containerTag.appendChild(span);
+}
+
+function mircChangeColor (colorInfo, containerTag, data)
+{
+ /* If colors are disabled, the caller doesn't want colors specifically, or
+ * the caller doesn't want any state-changing effects, we drop out.
+ */
+ if (!client.enableColors ||
+ (("noMircColors" in data) && data.noMircColors) ||
+ (("noStateChange" in data) && data.noStateChange))
+ {
+ return;
+ }
+
+ // Entry 0 will contain all colors specified,
+ // entry 1 will have any specified foreground color or be undefined,
+ // entry 2 will have any specified background color or be undefined.
+ // Valid color codes are 0-99 with 99 having special meaning.
+ let ary = colorInfo.match(/^\x03(?:(\d\d?)(?:,(\d\d?))?)?/);
+
+ // If no foreground color specified or somehow the array does not have 3
+ // entries then it has invalid syntax.
+ if (ary.length != 3 || !ary[1]) {
+ delete data.currFgColor;
+ delete data.currBgColor;
+ return;
+ }
+
+ let fgColor = Number(ary[1]);
+
+ if (fgColor != 99) {
+ data.currFgColor = (fgColor % 16).toString().padStart(2, "0");
+ } else {
+ delete data.currFgColor;
+ }
+
+ // If no background color then default to 99.
+ let bgColor = Number(ary[2] || "99");
+
+ if (bgColor != 99) {
+ data.currBgColor = (bgColor % 16).toString().padStart(2, "0");
+ } else {
+ delete data.currBgColor;
+ }
+
+ // Only set hasColorInfo if we have something set.
+ if (fgColor != 99 || bgColor != 99) {
+ data.hasColorInfo = true;
+ }
+}
+
+function mircToggleBold (colorInfo, containerTag, data)
+{
+ if (!client.enableColors ||
+ (("noMircColors" in data) && data.noMircColors) ||
+ (("noStateChange" in data) && data.noStateChange))
+ {
+ return;
+ }
+
+ if ("isBold" in data)
+ delete data.isBold;
+ else
+ data.isBold = true;
+ data.hasColorInfo = true;
+}
+
+function mircToggleUnder (colorInfo, containerTag, data)
+{
+ if (!client.enableColors ||
+ (("noMircColors" in data) && data.noMircColors) ||
+ (("noStateChange" in data) && data.noStateChange))
+ {
+ return;
+ }
+
+ if ("isUnderline" in data)
+ delete data.isUnderline;
+ else
+ data.isUnderline = true;
+ data.hasColorInfo = true;
+}
+
+function mircResetColor (text, containerTag, data)
+{
+ if (!client.enableColors ||
+ (("noMircColors" in data) && data.noMircColors) ||
+ (("noStateChange" in data) && data.noStateChange) ||
+ !("hasColorInfo" in data))
+ {
+ return;
+ }
+
+ removeColorInfo(data);
+}
+
+function mircReverseColor (text, containerTag, data)
+{
+ if (!client.enableColors ||
+ (("noMircColors" in data) && data.noMircColors) ||
+ (("noStateChange" in data) && data.noStateChange))
+ {
+ return;
+ }
+
+ var tempColor = ("currFgColor" in data ? data.currFgColor : "");
+
+ if ("currBgColor" in data)
+ data.currFgColor = data.currBgColor;
+ else
+ delete data.currFgColor;
+ if (tempColor)
+ data.currBgColor = tempColor;
+ else
+ delete data.currBgColor;
+ data.hasColorInfo = true;
+}
+
+function ansiEscapeSGR(text, containerTag, data)
+{
+ if (!client.enableColors ||
+ (("noANSIColors" in data) && data.noANSIColors) ||
+ (("noStateChange" in data) && data.noStateChange))
+ {
+ return;
+ }
+
+ /* ANSI SGR (Select Graphic Rendition) escape support. Matched text may
+ * have any number of effects, each a number separated by a semicolon. If
+ * there are no effects listed, it is treated as effect "0" (reset/normal).
+ */
+
+ text = text.substr(2, text.length - 3) || "0";
+
+ const ansiToMircColor = [
+ "01", "05", "03", "07", "02", "06", "10", "15",
+ "14", "04", "09", "08", "12", "13", "11", "00"
+ ];
+
+ var effects = text.split(";");
+ for (var i = 0; i < effects.length; i++)
+ {
+ data.hasColorInfo = true;
+
+ switch (Number(effects[i]))
+ {
+ case 0: // Reset/normal.
+ removeColorInfo(data);
+ break;
+
+ case 1: // Intensity: bold.
+ data.isBold = true;
+ break;
+
+ case 3: // Italic: on.
+ data.isItalic = true;
+ break;
+
+ case 4: // Underline: single.
+ data.isUnderline = true;
+ break;
+
+ case 9: // Strikethrough: on.
+ data.isStrikethrough = true;
+ break;
+
+ case 22: // Intensity: normal.
+ delete data.isBold;
+ break;
+
+ case 23: // Italic: off.
+ delete data.isItalic;
+ break;
+
+ case 24: // Underline: off.
+ delete data.isUnderline;
+ break;
+
+ case 29: // Strikethrough: off.
+ delete data.isStrikethrough;
+ break;
+
+ case 53: // Overline: on.
+ data.isOverline = true;
+ break;
+
+ case 55: // Overline: off.
+ delete data.isOverline;
+ break;
+
+ case 30: // FG: Black.
+ case 31: // FG: Red.
+ case 32: // FG: Green.
+ case 33: // FG: Yellow.
+ case 34: // FG: Blue.
+ case 35: // FG: Magenta.
+ case 36: // FG: Cyan.
+ case 37: // FG: While (light grey).
+ data.currFgColor = ansiToMircColor[effects[i] - 30];
+ break;
+
+ case 39: // FG: default.
+ delete data.currFgColor;
+ break;
+
+ case 40: // BG: Black.
+ case 41: // BG: Red.
+ case 42: // BG: Green.
+ case 43: // BG: Yellow.
+ case 44: // BG: Blue.
+ case 45: // BG: Magenta.
+ case 46: // BG: Cyan.
+ case 47: // BG: While (light grey).
+ data.currBgColor = ansiToMircColor[effects[i] - 40];
+ break;
+
+ case 49: // BG: default.
+ delete data.currBgColor;
+ break;
+
+ case 90: // FG: Bright Black (dark grey).
+ case 91: // FG: Bright Red.
+ case 92: // FG: Bright Green.
+ case 93: // FG: Bright Yellow.
+ case 94: // FG: Bright Blue.
+ case 95: // FG: Bright Magenta.
+ case 96: // FG: Bright Cyan.
+ case 97: // FG: Bright While.
+ data.currFgColor = ansiToMircColor[effects[i] - 90 + 8];
+ break;
+
+ case 100: // BG: Bright Black (dark grey).
+ case 101: // BG: Bright Red.
+ case 102: // BG: Bright Green.
+ case 103: // BG: Bright Yellow.
+ case 104: // BG: Bright Blue.
+ case 105: // BG: Bright Magenta.
+ case 106: // BG: Bright Cyan.
+ case 107: // BG: Bright While.
+ data.currBgColor = ansiToMircColor[effects[i] - 100 + 8];
+ break;
+ }
+ }
+}
+
+function removeColorInfo(data)
+{
+ delete data.currFgColor;
+ delete data.currBgColor;
+ delete data.isBold;
+ delete data.isItalic;
+ delete data.isOverline;
+ delete data.isStrikethrough;
+ delete data.isUnderline;
+ delete data.hasColorInfo;
+}
+
+function showCtrlChar(c, containerTag)
+{
+ var span = document.createElementNS(XHTML_NS, "html:span");
+ span.setAttribute("class", "chatzilla-control-char");
+ if (c == "\t")
+ {
+ containerTag.appendChild(document.createTextNode(c));
+ return;
+ }
+
+ var ctrlStr = c.charCodeAt(0).toString(16);
+ if (ctrlStr.length < 2)
+ ctrlStr = "0" + ctrlStr;
+ span.appendChild(document.createTextNode("0x" + ctrlStr));
+ containerTag.appendChild(span);
+ containerTag.appendChild(document.createElementNS(XHTML_NS, "html:wbr"));
+}
+
+function insertText(text, containerTag, data)
+{
+ var newClass = "";
+ if (data && ("hasColorInfo" in data))
+ newClass = calcClass(data);
+ if (!newClass)
+ delete data.hasColorInfo;
+
+ if (newClass)
+ {
+ var spanTag = document.createElementNS(XHTML_NS, "html:span");
+ spanTag.setAttribute("class", newClass);
+ containerTag.appendChild(spanTag);
+ containerTag = spanTag;
+ }
+
+ var arg;
+ while ((arg = text.match(client.whitespaceRE)))
+ {
+ // Find the start of the match so we can insert the preceding text.
+ var start = text.indexOf(arg[0]);
+ if (start > 0)
+ containerTag.appendChild(document.createTextNode(text.substr(0, start)));
+
+ // Process the long word itself.
+ insertHyphenatedWord(arg[1], containerTag, { dontStyleText: true });
+
+ // Continue with the rest of the text.
+ text = text.substr(start + arg[0].length);
+ }
+
+ // Insert any left-over text on the end.
+ if (text)
+ containerTag.appendChild(document.createTextNode(text));
+}
+
+function insertHyphenatedWord(longWord, containerTag, data)
+{
+ var wordParts = splitLongWord(longWord, client.MAX_WORD_DISPLAY);
+
+ if (!data || !("dontStyleText" in data))
+ {
+ var newClass = "";
+ if (data && ("hasColorInfo" in data))
+ newClass = calcClass(data);
+ if (!newClass)
+ delete data.hasColorInfo;
+
+ if (newClass)
+ {
+ var spanTag = document.createElementNS(XHTML_NS, "html:span");
+ spanTag.setAttribute("class", newClass);
+ containerTag.appendChild(spanTag);
+ containerTag = spanTag;
+ }
+ }
+
+ var wbr = document.createElementNS(XHTML_NS, "html:wbr");
+ for (var i = 0; i < wordParts.length; ++i)
+ {
+ containerTag.appendChild(document.createTextNode(wordParts[i]));
+ containerTag.appendChild(wbr.cloneNode(true));
+ }
+}
+
+function insertInlineButton(text, containerTag, data)
+{
+ var ary = text.match(/\[\[([^\]]+)\]\[([^\]]+)\]\[([^\]]+)\]\]/);
+
+ if (!ary)
+ {
+ containerTag.appendChild(document.createTextNode(text));
+ return;
+ }
+
+ var label = ary[1];
+ var title = ary[2];
+ var command = ary[3];
+
+ var link = document.createElementNS(XHTML_NS, "html:a");
+ link.setAttribute("href", "x-cz-command:" + encodeURI(command));
+ link.setAttribute("title", title);
+ link.setAttribute("class", "chatzilla-link");
+ link.appendChild(document.createTextNode(label));
+
+ containerTag.appendChild(document.createTextNode("["));
+ containerTag.appendChild(link);
+ containerTag.appendChild(document.createTextNode("]"));
+}
+
+function calcClass(data)
+{
+ var className = "";
+ if ("hasColorInfo" in data)
+ {
+ if ("currFgColor" in data)
+ className += " chatzilla-fg" + data.currFgColor;
+ if ("currBgColor" in data)
+ className += " chatzilla-bg" + data.currBgColor;
+ if ("isBold" in data)
+ className += " chatzilla-bold";
+ if ("isItalic" in data)
+ className += " chatzilla-italic";
+ if ("isOverline" in data)
+ className += " chatzilla-overline";
+ if ("isStrikethrough" in data)
+ className += " chatzilla-strikethrough";
+ if ("isUnderline" in data)
+ className += " chatzilla-underline";
+ }
+ return className;
+}
+
diff --git a/comm/suite/chatzilla/xul/content/networks-edit.css b/comm/suite/chatzilla/xul/content/networks-edit.css
new file mode 100644
index 0000000000..f1f36e47e1
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/networks-edit.css
@@ -0,0 +1,15 @@
+/* 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/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+/* Set min-width on the network list. */
+#networkList {
+ min-width: 26ch;
+}
+
+/* Set min-width on the server list. */
+#serverList {
+ min-width: 28ch;
+}
diff --git a/comm/suite/chatzilla/xul/content/networks-edit.js b/comm/suite/chatzilla/xul/content/networks-edit.js
new file mode 100644
index 0000000000..4952783cc9
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/networks-edit.js
@@ -0,0 +1,390 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var ASSERT = function(cond, msg) { if (!cond) { alert(msg); } return cond; }
+var client;
+
+// To be able to load static.js, we need a few things defined first:
+function CIRCNetwork() {}
+function CIRCServer() {}
+function CIRCChannel() {}
+function CIRCUser() {}
+function CIRCChanUser() {}
+function CIRCDCCUser() {}
+function CIRCDCCChat() {}
+function CIRCDCCFile() {}
+function CIRCDCCFileTransfer() {}
+function CIRCSTS() {}
+
+// Actual network window itself.
+var gNetworkWindow = {
+ mBundle: null,
+ mServerList: null,
+ mNetworkList: null,
+
+ /* Stores all the network and server objects we're using.
+ */
+ networkList: null,
+
+ alert: function(aSubject, aVar) {
+ let title = this.mBundle.getString(aSubject + "Title");
+ let msg = this.mBundle.getFormattedString(aSubject, [aVar]);
+ Services.prompt.alert(window, title, msg);
+ },
+
+ confirmEx: function(aSubject, aVar) {
+ let title = this.mBundle.getString(aSubject + "Title");
+ let msg = aVar ? this.mBundle.getFormattedString(aSubject, [aVar])
+ : this.mBundle.getString(aSubject);
+ return Services.prompt.confirmEx(window, title, msg,
+ Services.prompt.STD_YES_NO_BUTTONS, null,
+ null, null, null, { });
+ },
+
+ prompt: function(aSubject, aInitial) {
+ let title = this.mBundle.getString(aSubject + "Title");
+ let msg = this.mBundle.getString(aSubject);
+ let rv = { value: aInitial };
+
+ if (!Services.prompt.prompt(window, title, msg, rv, null, {value: null})) {
+ return null;
+ }
+
+ return rv.value.toLowerCase().trim();
+ },
+
+ refreshNetworks: function(aNetwork) {
+ // Remove all children.
+ while (this.mNetworkList.hasChildNodes()) {
+ this.mNetworkList.lastChild.remove();
+ }
+
+ let hasChildren = false;
+ let network;
+ // Populate the network item list.
+ for (let name in this.networkList) {
+ let label = document.createElement("label");
+ label.setAttribute("value", name);
+ let listitem = document.createElement("listitem");
+ listitem.appendChild(label);
+ listitem.id = name;
+ if (aNetwork && (aNetwork == name)) {
+ network = listitem;
+ }
+ this.mNetworkList.appendChild(listitem);
+ hasChildren = true;
+ }
+
+ if (hasChildren) {
+ // If a network name was given and found select it,
+ // otherwise select the first item.
+ this.mNetworkList.selectItem(network || this.mNetworkList.firstChild);
+ } else {
+ this.onSelectNetwork();
+ }
+ this.updateNetworkButtons(hasChildren);
+ },
+
+ updateNetworkButtons: function(aSelected) {
+ let editButton = document.getElementById("networkListEditButton");
+ let removeButton = document.getElementById("networkListRemoveButton");
+ if (!aSelected) {
+ editButton.setAttribute("disabled", "true");
+ removeButton.setAttribute("disabled", "true");
+ } else {
+ editButton.removeAttribute("disabled");
+ removeButton.removeAttribute("disabled");
+ }
+ },
+
+ // Loads the networks list.
+ onLoad: function() {
+ client = window.arguments[0];
+
+ // Needed for ASSERT.
+ initMessages();
+
+ this.mBundle = document.getElementById("bundle_networks");
+ this.mServerList = document.getElementById("serverList");
+ this.mNetworkList = document.getElementById("networkList");
+
+ // The list of objects we're tracking.
+ this.networkList = networksToNetworkList();
+ this.refreshNetworks();
+
+ // Force the window to be the right size now, not later.
+ window.sizeToContent();
+ },
+
+ // Closing the window. Clean up.
+ onClose: function() {
+ },
+
+ // OK button.
+ onOK: function() {
+ // Save the list and update client.networks
+ try {
+ networksSaveList(this.networkList);
+ }
+ catch (e) {
+ this.alert("network-saveError", e);
+ return false;
+ }
+
+ networksSyncFromList(this.networkList);
+ window.close();
+ client.updateHeader();
+ client.dispatch("networks");
+ return true;
+ },
+
+ // Cancel button.
+ onCancel: function() {
+ window.close();
+ return true;
+ },
+
+ // Restore Defaults button.
+ onRestore: function() {
+ // Ask for confirmation.
+ if (this.confirmEx("network-confirmRestoreDefaults") != 0) {
+ return;
+ }
+
+ // Repopulate the network list.
+ this.networkList = networksGetDefaults();
+ this.refreshNetworks();
+ },
+
+ // Connect to Network button.
+ onConnect: function() {
+ let selection = this.mNetworkList.selectedItem;
+ if (!selection)
+ return;
+
+ let network = this.networkList[selection.id];
+ if (this.onOK()) {
+ if (networkHasSecure(network.servers)) {
+ client.dispatch("sslserver " + network.name);
+ } else {
+ client.dispatch("server " + network.name);
+ }
+ }
+ },
+
+ // Select a network listitem.
+ onSelectNetwork: function(aId = 0) {
+ let header = document.getElementById("network-header");
+
+ // Remove all children.
+ while (this.mServerList.hasChildNodes()) {
+ this.mServerList.lastChild.remove();
+ }
+
+ let selection = this.mNetworkList.selectedItem;
+ if (!selection) {
+ header.setAttribute("title",
+ this.mBundle.getString("network-headerDefault"));
+ this.updateServerButtons(null, true);
+ return;
+ }
+
+ // Make sure selected network item is visible.
+ this.mNetworkList.ensureElementIsVisible(selection);
+
+ let hasChildren = false;
+ let network = this.networkList[selection.id];
+ for (let i = 0; i < network.servers.length; i++) {
+ let server = network.servers[i];
+ let label = document.createElement("label");
+ label.setAttribute("value", server.hostname + ":" + server.port);
+ let listitem = document.createElement("listitem");
+ listitem.appendChild(label);
+ listitem.setAttribute("server_id", i);
+ listitem.id = network.name + "-" + i;
+ this.mServerList.appendChild(listitem);
+ hasChildren = true;
+ }
+
+ if (hasChildren) {
+ // Select the given id if it exists otherwise the first item.
+ this.mServerList.selectedIndex = aId;
+ } else {
+ this.onSelectServer();
+ }
+
+ header.setAttribute("title",
+ this.mBundle.getFormattedString("network-headerName",
+ [network.name]));
+ },
+
+ // Network Add button.
+ onAddNetwork: function() {
+ let name = this.prompt("network-add");
+ if (!name) {
+ return;
+ }
+
+ if (name in this.networkList) {
+ this.alert("network-nameError", name);
+ return;
+ }
+
+ // Create new network entry.
+ this.networkList[name] = { name: name, displayName: name, servers: [] };
+
+ this.refreshNetworks(name);
+ },
+
+ // Network Edit button.
+ onEditNetwork: function() {
+ let oldName = this.mNetworkList.selectedItem.id;
+ let name = this.prompt("network-edit", oldName);
+ if (!name || (name == oldName)) {
+ return;
+ }
+
+ if (name in this.networkList) {
+ this.alert("network-nameError", name);
+ return;
+ }
+
+ // Create new network entry.
+ this.networkList[name] = { name: name, displayName: name,
+ servers: this.networkList[oldName].servers };
+ // Remove old network entry.
+ delete this.networkList[oldName];
+
+ this.refreshNetworks(name);
+ },
+
+ // Network Remove button.
+ onRemoveNetwork: function() {
+ let selected = this.mNetworkList.selectedItem;
+
+ // Confirm definitely want to remove this network.
+ if (this.confirmEx("network-remove", selected.id) != 0) {
+ return;
+ }
+
+ // Remove network entry.
+ delete this.networkList[selected.id];
+
+ this.refreshNetworks();
+ },
+
+ // Move up / down buttons.
+ onMoveServer: function(aDir) {
+ let item = this.mServerList.selectedItem;
+ let network = this.mNetworkList.selectedItem.id;
+ let id = parseInt(item.getAttribute("server_id"));
+ let server = this.networkList[network].servers[id];
+ this.networkList[network].servers.splice(id, 1);
+ this.networkList[network].servers.splice(id + aDir, 0, server);
+
+ // Refresh the server list and select the server that has been moved.
+ this.onSelectNetwork(id + aDir);
+ },
+
+ // Add Server button.
+ onAddServer: function() {
+ this.openServerEditor(null);
+ },
+
+ // Edit Server button.
+ onEditServer: function() {
+ let item = this.mServerList.selectedItem;
+ if (!item) {
+ return;
+ }
+ this.openServerEditor(item);
+ },
+
+ // Remove Server button.
+ onRemoveServer: function() {
+ let item = this.mServerList.selectedItem;
+ let network = this.mNetworkList.selectedItem.id;
+ let id = item.getAttribute("server_id");
+ let server = this.networkList[network].servers[id];
+ let name = server.hostname + ":" + server.port;
+
+ // Confirm definitely want to remove this network.
+ if (this.confirmEx("server-remove", name) != 0) {
+ return;
+ }
+
+ this.networkList[network].servers.splice(id, 1);
+ this.onSelectNetwork();
+ },
+
+ onSelectServer: function() {
+ let server = this.mServerList.selectedItem;
+ this.updateServerButtons(server, false);
+ this.updateServerInfoBox(server);
+ },
+
+ openServerEditor: function(aItem) {
+ let network = this.mNetworkList.selectedItem.id;
+ let id;
+ let server;
+ if (aItem) {
+ id = aItem.getAttribute("server_id");
+ server = this.networkList[network].servers[id];
+ }
+
+ let args = { server: server, result: false };
+ window.openDialog("chrome://chatzilla/content/networks-server.xul",
+ "serverEdit", "chrome,titlebar,modal,centerscreen", args);
+ // Now update the server which was just added / edited and select it.
+ if (args.result) {
+ if (server) {
+ this.networkList[network].servers[id] = args.server;
+ } else {
+ id = this.networkList[network].servers.length;
+ this.networkList[network].servers.push(args.server);
+ }
+ this.refreshNetworks(network);
+ this.mServerList.selectedIndex = id;
+ }
+ },
+
+ updateServerButtons: function(aServer, aNone) {
+ this.disableButton("serverListUpButton", aNone || !aServer ||
+ !aServer.previousSibling);
+ this.disableButton("serverListDownButton", aNone || !aServer ||
+ !aServer.nextSibling);
+ this.disableButton("serverListAddButton", aNone);
+ this.disableButton("serverListEditButton", aNone || !aServer);
+ this.disableButton("serverListRemoveButton", aNone || !aServer);
+ },
+
+ disableButton: function(aButtonId, aDisable) {
+ let button = document.getElementById(aButtonId);
+ if (aDisable) {
+ button.setAttribute("disabled", "true");
+ } else {
+ button.removeAttribute("disabled");
+ }
+ },
+
+ updateServerInfoBox: function(aServer) {
+ let name = document.getElementById("nameValue");
+ let port = document.getElementById("portValue");
+ let connection = document.getElementById("connectionSecurityValue");
+ if (!aServer) {
+ name.value = "";
+ port.value = "";
+ connection.value = "";
+ return;
+ }
+
+ let network = this.mNetworkList.selectedItem.id;
+ let id = aServer.getAttribute("server_id");
+ let server = this.networkList[network].servers[id];
+ let type = "server-ConnectionSecurityType-" + (server.isSecure ? "3" : "0");
+ name.value = server.hostname;
+ port.value = server.port;
+ connection.value = this.mBundle.getString(type);
+ },
+};
diff --git a/comm/suite/chatzilla/xul/content/networks-edit.xul b/comm/suite/chatzilla/xul/content/networks-edit.xul
new file mode 100644
index 0000000000..97483230ba
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/networks-edit.xul
@@ -0,0 +1,155 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/networks.dtd">
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://chatzilla/skin/networks-edit.css"
+ type="text/css"?>
+<?xml-stylesheet href="chrome://chatzilla/content/networks-edit.css"
+ type="text/css"?>
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ id="chatzilla-window"
+ title="&networksEditDialog.title;"
+ size="&networksEditDialog.size;"
+ windowtype="irc:chatzilla:networks"
+ onload="gNetworkWindow.onLoad();"
+ onunload="gNetworkWindow.onClose();"
+ buttons="accept,cancel,extra1,extra2"
+ buttonlabelextra1="&connectNetwork.label;"
+ buttonaccesskeyextra1="&connectNetwork.accesskey;"
+ buttonlabelextra2="&restoreButton.label;"
+ buttonaccesskeyextra2="&restoreButton.accesskey;"
+ ondialogaccept="return gNetworkWindow.onOK();"
+ ondialogcancel="return gNetworkWindow.onCancel();"
+ ondialogextra1="gNetworkWindow.onConnect();"
+ ondialogextra2="gNetworkWindow.onRestore();"
+ persist="screenX screenY width height">
+
+ <script src="chrome://chatzilla/content/lib/js/utils.js"/>
+ <script src="chrome://chatzilla/content/lib/js/file-utils.js"/>
+ <script src="chrome://chatzilla/content/lib/js/json-serializer.js"/>
+ <script src="chrome://chatzilla/content/lib/js/message-manager.js"/>
+ <script src="chrome://chatzilla/content/lib/js/text-serializer.js"/>
+ <script src="chrome://chatzilla/content/messages.js"/>
+ <script src="chrome://chatzilla/content/networks.js"/>
+ <script src="chrome://chatzilla/content/networks-edit.js"/>
+ <script src="chrome://chatzilla/content/static.js"/>
+
+ <stringbundle id="bundle_networks"
+ src="chrome://chatzilla/locale/networks.properties"/>
+
+ <hbox flex="1">
+ <vbox id="networkListBox">
+ <listbox id="networkList"
+ onselect="gNetworkWindow.onSelectNetwork();"
+ ondblclick="gNetworkWindow.onEditNetwork();"
+ seltype="single"
+ flex="1"/>
+ <button id="networkListAddButton"
+ label="&networkListAdd.label;"
+ accesskey="&networkListAdd.accesskey;"
+ tooltiptext="&networkListAdd.tooltip;"
+ oncommand="gNetworkWindow.onAddNetwork();"/>
+ <button id="networkListEditButton"
+ label="&networkListEdit.label;"
+ accesskey="&networkListEdit.accesskey;"
+ tooltiptext="&networkListEdit.tooltip;"
+ oncommand="gNetworkWindow.onEditNetwork();"/>
+ <button id="networkListRemoveButton"
+ label="&networkListRemove.label;"
+ accesskey="&networkListRemove.accesskey;"
+ tooltiptext="&networkListRemove.tooltip;"
+ oncommand="gNetworkWindow.onRemoveNetwork();"/>
+ </vbox>
+ <vbox id="serverListBox" flex="1">
+ <dialogheader id="network-header" title=""/>
+ <hbox flex="1">
+ <listbox id="serverList"
+ onselect="gNetworkWindow.onSelectServer();"
+ ondblclick="gNetworkWindow.onEditServer();"
+ seltype="single"
+ flex="1"/>
+ <vbox id="serverListButtons">
+ <button id="serverListUpButton"
+ disabled="true"
+ label="&serverListUp.label;"
+ accesskey="&serverListUp.accesskey;"
+ tooltiptext="&serverListUp.tooltip;"
+ oncommand="gNetworkWindow.onMoveServer(-1);"/>
+ <button id="serverListDownButton"
+ disabled="true"
+ label="&serverListDown.label;"
+ accesskey="&serverListDown.accesskey;"
+ tooltiptext="&serverListDown.tooltip;"
+ oncommand="gNetworkWindow.onMoveServer(1);"/>
+ <spacer flex="1"/>
+ <button id="serverListAddButton"
+ label="&serverListAdd.label;"
+ accesskey="&serverListAdd.accesskey;"
+ tooltiptext="&serverListAdd.tooltip;"
+ oncommand="gNetworkWindow.onAddServer();"/>
+ <button id="serverListEditButton"
+ label="&serverListEdit.label;"
+ accesskey="&serverListEdit.accesskey;"
+ tooltiptext="&serverListEdit.tooltip;"
+ oncommand="gNetworkWindow.onEditServer();"/>
+ <separator/>
+ <button id="serverListRemoveButton"
+ disabled="true"
+ label="&serverListRemove.label;"
+ accesskey="&serverListRemove.accesskey;"
+ tooltiptext="&serverListRemove.tooltip;"
+ oncommand="gNetworkWindow.onRemoveServer();"/>
+ </vbox>
+ </hbox>
+
+ <separator/>
+
+ <label class="header">&serverDetails.label;</label>
+ <hbox id="serverInfoBox">
+ <stack flex="1" class="inset">
+ <spacer id="backgroundBox"/>
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row align="center">
+ <hbox pack="end">
+ <label id="nameLabel"
+ value="&serverName.label;"
+ control="nameValue"/>
+ </hbox>
+ <textbox id="nameValue" readonly="true" class="plain"/>
+ </row>
+ <row align="center">
+ <hbox pack="end">
+ <label id="portLabel"
+ value="&serverPort.label;"
+ control="portValue"/>
+ </hbox>
+ <textbox id="portValue" readonly="true" class="plain"/>
+ </row>
+ <row align="center">
+ <hbox pack="end">
+ <label id="connectionSecurityLabel"
+ value="&connectionSecurity.label;"
+ control="connectionSecurityValue"/>
+ </hbox>
+ <textbox id="connectionSecurityValue"
+ readonly="true"
+ class="plain"/>
+ </row>
+ </rows>
+ </grid>
+ </stack>
+ </hbox>
+ </vbox>
+ </hbox>
+</dialog>
diff --git a/comm/suite/chatzilla/xul/content/networks-server.js b/comm/suite/chatzilla/xul/content/networks-server.js
new file mode 100644
index 0000000000..fde299bf89
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/networks-server.js
@@ -0,0 +1,94 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { Services } =
+ ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { isLegalHostNameOrIP =
+ ChromeUtils.import("resource:///modules/hostnameUtils.jsm");
+
+var gNetworkServer;
+var gNetworksBundle;
+var gNameValue;
+var gPortValue;
+var gDefaultPort;
+var gSocketType;
+
+function onLoad(aEvent) {
+ gNetworkServer = window.arguments[0].server;
+
+ gNetworksBundle = document.getElementById("bundle_networks");
+ gNameValue = document.getElementById("nameValue");
+ gPortValue = document.getElementById("portValue");
+ gDefaultPort = document.getElementById("defaultPort");
+ gSocketType = document.getElementById("socketType");
+
+ // Set labels on socketType menuitems.
+ document.getElementById("socketSecurityType-0").label =
+ gNetworksBundle.getString("server-ConnectionSecurityType-0");
+ document.getElementById("socketSecurityType-3").label =
+ gNetworksBundle.getString("server-ConnectionSecurityType-3");
+
+ if (gNetworkServer) {
+ gNameValue.value = gNetworkServer.hostname;
+ gPortValue.value = gNetworkServer.port;
+ gSocketType.value = gNetworkServer.isSecure ? 3 : 0;
+ }
+ sslChanged(false);
+}
+
+function onAccept() {
+ let hostname = cleanUpHostName(gNameValue.value.toLowerCase());
+ if (!isLegalHostNameOrIP(hostname)) {
+ let alertTitle = gNetworksBundle.getString("invalidServerName");
+ let alertMsg = gNetworksBundle.getString("enterValidServerName");
+ Services.prompt.alert(window, alertTitle, alertMsg);
+
+ window.arguments[0].result = false;
+ return false;
+ }
+
+ // If we didn't have a server to initialize with, we must create one.
+ if (!gNetworkServer) {
+ gNetworkServer = {};
+ }
+
+ gNetworkServer.hostname = hostname;
+ gNetworkServer.port = gPortValue.value;
+ gNetworkServer.isSecure = gSocketType.value == 3;
+
+ window.arguments[0].server = gNetworkServer;
+ window.arguments[0].result = true;
+ return true;
+}
+
+/**
+ * Resets the default port to IRC or IRCS, dependending on the |gSocketType|
+ * value, and sets the port to use to this default, if that's appropriate.
+ *
+ * @param aUserAction false for dialog initialization,
+ * true for user action.
+ */
+function sslChanged(aUserAction) {
+ const DEFAULT_IRC_PORT = "6667";
+ const DEFAULT_IRCS_PORT = "6697";
+ let otherDefaultPort;
+ let prevDefaultPort = gDefaultPort.value;
+
+ if (gSocketType.value == 3) {
+ gDefaultPort.value = DEFAULT_IRCS_PORT;
+ otherDefaultPort = DEFAULT_IRC_PORT;
+ } else {
+ gDefaultPort.value = DEFAULT_IRC_PORT;
+ otherDefaultPort = DEFAULT_IRCS_PORT;
+ }
+
+ // If the port is not set, or the user is causing the default port to change,
+ // and the port is set to the default for the other protocol,
+ // then set the port to the default for the new protocol.
+ if ((gPortValue.value == 0) ||
+ (aUserAction && (gDefaultPort.value != prevDefaultPort) &&
+ (gPortValue.value == otherDefaultPort)))
+ gPortValue.value = gDefaultPort.value;
+}
diff --git a/comm/suite/chatzilla/xul/content/networks-server.xul b/comm/suite/chatzilla/xul/content/networks-server.xul
new file mode 100644
index 0000000000..af2341dae0
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/networks-server.xul
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE dialog SYSTEM "chrome://chatzilla/locale/networks.dtd">
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://chatzilla/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://chatzilla/content/" type="text/css"?>
+
+<dialog title="&serverEditDialog.title;"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="onLoad();"
+ ondialogaccept="return onAccept();">
+
+ <script src="chrome://chatzilla/content/networks-server.js"/>
+
+ <stringbundle id="bundle_networks"
+ src="chrome://chatzilla/locale/networks.properties"/>
+
+ <vbox id="serverEditor">
+ <groupbox>
+ <caption label="&settings.caption;"/>
+ <grid flex="1">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row align="center">
+ <label value="&serverName.label;"
+ accesskey="&serverName.accesskey;"
+ control="nameValue"/>
+ <textbox id="nameValue"
+ flex="1"
+ class="uri-element"/>
+ </row>
+ <row align="center">
+ <label value="&serverPort.label;"
+ accesskey="&serverPort.accesskey;"
+ control="portValue"/>
+ <hbox align="center">
+ <textbox id="portValue"
+ type="number"
+ min="0"
+ max="65535"
+ size="5"/>
+ <label value="&serverPortDefault.label;"/>
+ <label id="defaultPort"/>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <separator class="thin"/>
+
+ <groupbox>
+ <caption label="&security.caption;"/>
+
+ <grid flex="1">
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row align="center">
+ <label value="&connectionSecurity.label;"
+ accesskey="&connectionSecurity.accesskey;"
+ control="socketType"/>
+ <menulist id="socketType" oncommand="sslChanged(true);">
+ <menupopup id="socketTypePopup">
+ <menuitem id="socketSecurityType-0" value="0"/>
+ <menuitem id="socketSecurityType-3" value="3"/>
+ </menupopup>
+ </menulist>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+ </vbox>
+</dialog>
diff --git a/comm/suite/chatzilla/xul/content/networks.js b/comm/suite/chatzilla/xul/content/networks.js
new file mode 100644
index 0000000000..bffeab7c4a
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/networks.js
@@ -0,0 +1,228 @@
+/* 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/. */
+
+function initNetworks()
+{
+ let migrated = Services.prefs.getBoolPref("extensions.irc.network_migrated",
+ false);
+ let networksFile = new nsLocalFile(client.prefs["profilePath"]);
+ networksFile.append("networks." + (migrated ? "json" : "txt"));
+
+ let createDefault = !networksFile.exists();
+ let networkList = {};
+ // Populate networkList with defaults if no file exists or migrating from
+ // previous networks.txt file usage.
+ if (createDefault || !migrated)
+ {
+ networkList = networksGetDefaults();
+ }
+
+ if (!createDefault)
+ {
+ let userNetworkList = [];
+
+ let networksLoader = migrated ? new JSONSerializer(networksFile)
+ : new TextSerializer(networksFile);
+ if (networksLoader.open("<"))
+ {
+ let item = networksLoader.deserialize();
+ if (isinstance(item, Array))
+ userNetworkList = item;
+ else
+ dd("Malformed networks file!");
+ networksLoader.close();
+ }
+
+ // When migrating this merges the user's network list with the default
+ // ones otherwise this populates the empty networkList.
+ for (let network of userNetworkList)
+ {
+ let lowerNetName = network.name.toLowerCase();
+ if ((lowerNetName in networkList) && ("isDeleted" in network))
+ {
+ delete networkList[lowerNetName];
+ }
+ else if (!("isDeleted" in network))
+ {
+ networkList[lowerNetName] = network;
+ networkList[lowerNetName].name = lowerNetName;
+ }
+ }
+ }
+
+
+ if (!migrated)
+ {
+ Services.prefs.setBoolPref("extensions.irc.network_migrated", true);
+ }
+
+ // Sync to client.networks.
+ networksSyncFromList(networkList);
+
+ // If we created a new file with the defaults, save it.
+ if (createDefault || !migrated)
+ networksSaveList(networkList);
+}
+
+function networksGetDefaults()
+{
+ var networks = new Object();
+
+ // Set up default network list.
+ networks["libera.chat"] = {
+ displayName: "libera.chat",
+ servers: [{hostname: "irc.libera.chat", port:6697, isSecure: true},
+ {hostname: "irc.libera.chat", port:6667}]};
+ networks["slashnet"] = {
+ displayName: "slashnet",
+ servers: [{hostname: "irc.slashnet.org", port:6667}]};
+ networks["dalnet"] = {
+ displayName: "dalnet",
+ servers: [{hostname: "irc.dal.net", port:6667},
+ {hostname: "irc.dal.net", port:6697, isSecure: true},
+ {hostname: "irc.au.dal.net", port:6667},
+ {hostname: "irc.eu.dal.net", port:6667},
+ {hostname: "irc.us.dal.net", port:6667}]};
+ networks["undernet"] = {
+ displayName: "undernet",
+ servers: [{hostname: "irc.undernet.org", port:6667},
+ {hostname: "eu.undernet.org", port:6667},
+ {hostname: "us.undernet.org", port:6667}]};
+ networks["webbnet"] = {
+ displayName: "webbnet",
+ servers: [{hostname: "irc.webbnet.info", port:6667}]};
+ networks["quakenet"] = {
+ displayName: "quakenet",
+ servers: [{hostname: "irc.quakenet.org", port:6667},
+ {hostname: "se.quakenet.org", port:6667},
+ {hostname: "uk.quakenet.org", port:6667},
+ {hostname: "us.quakenet.org", port:6667}]};
+ networks["ircnet"] = {
+ displayName: "ircnet",
+ servers: [{hostname: "open.ircnet.net", port:6667},
+ {hostname: "au.ircnet.org", port:6667},
+ {hostname: "eu.ircnet.org", port:6667},
+ {hostname: "us.ircnet.org", port:6667}]};
+ networks["efnet"] = {
+ displayName: "efnet",
+ servers: [{hostname: "irc.efnet.org", port: 6667}]};
+ networks["hispano"] = {
+ displayName: "hispano",
+ servers: [{hostname: "irc.irc-hispano.org", port: 6667}]};
+ networks["freenode"] = {
+ displayName: "freenode",
+ servers: [{hostname: "chat.freenode.net", port:6697, isSecure: true},
+ {hostname: "chat.freenode.net", port:7000, isSecure: true},
+ {hostname: "chat.freenode.net", port:6667}]};
+
+ for (var name in networks)
+ networks[name].name = name;
+
+ return networks;
+}
+
+function networksToNetworkList()
+{
+ var networkList = {};
+
+ // Create a networkList from client.networks.
+ for (let name in client.networks)
+ {
+ let net = client.networks[name];
+ // Skip temporary networks, as they're created to wrap standalone
+ // servers only.
+ if (net.temporary)
+ continue;
+
+ let listNet = { name: net.canonicalName, displayName: net.unicodeName,
+ servers: [] };
+
+ // Populate server list (no merging here).
+ for (let i = 0; i < net.serverList.length; i++)
+ {
+ let serv = net.serverList[i];
+ let listServ = { hostname: serv.hostname, port: serv.port,
+ isSecure: serv.isSecure };
+ listNet.servers.push(listServ);
+ }
+ networkList[net.canonicalName] = listNet;
+ }
+
+ return networkList;
+}
+
+function networksSyncFromList(networkList)
+{
+ // Copy to and update client.networks from networkList.
+ for (let name in networkList)
+ {
+ let listNet = networkList[name];
+
+ // Create new network object if necessary.
+ if (!client.getNetwork(name))
+ client.addNetwork(name, []);
+
+ // Get network object and make sure server list is empty.
+ let net = client.getNetwork(name);
+ net.clearServerList();
+
+ // Update server list.
+ for (let listServ of listNet.servers)
+ {
+ // Make sure these exist.
+ if (!("isSecure" in listServ))
+ listServ.isSecure = false;
+
+ // NOTE: this must match the name given by CIRCServer.
+ let servName = ":" + listServ.hostname + ":" + listServ.port;
+
+ if (!(servName in net.servers))
+ {
+ net.addServer(listServ.hostname, listServ.port,
+ listServ.isSecure);
+ }
+ let serv = net.servers[servName];
+
+ serv.isSecure = listServ.isSecure;
+ }
+ }
+
+ // Remove network objects that aren't in networkList.
+ for (let name in client.networks)
+ {
+ // Skip temporary networks, as they don't matter.
+ let net = client.networks[name];
+ if (net.temporary)
+ continue;
+ if (!(net.canonicalName in networkList))
+ client.removeNetwork(net.canonicalName);
+ }
+}
+
+function networksSaveList(networkList)
+{
+ var networksFile = new nsLocalFile(client.prefs["profilePath"]);
+ networksFile.append("networks.json");
+ var networksLoader = new JSONSerializer(networksFile);
+ if (networksLoader.open(">"))
+ {
+ networksLoader.serialize(Object.values(networkList));
+ networksLoader.close();
+ }
+}
+
+function networkHasSecure(serverList)
+{
+ // Test to see if the network has a secure server.
+ let hasSecure = false;
+ for (let s in serverList)
+ {
+ if (serverList[s].isSecure)
+ {
+ hasSecure = true;
+ break;
+ }
+ }
+ return hasSecure;
+}
diff --git a/comm/suite/chatzilla/xul/content/output-base.css b/comm/suite/chatzilla/xul/content/output-base.css
new file mode 100644
index 0000000000..bdf9db3f2f
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/output-base.css
@@ -0,0 +1,528 @@
+/* -*- Mode: Text; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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/. */
+
+/*
+ * This file contains the CSS rules for the output window in ChatZilla.
+ * The output window is layed out as a table with two columns. The first
+ * column holds a message type or a nickname, depending on what view the
+ * message is contained by, and what kind of message it is. The second column
+ * contains the text of the message. For most message types, ChatZilla displays
+ * ascii-art instead of the actual code. For example, messages of type "JOIN"
+ * are displayed as "-->|", and most unclassified message types are displayed
+ * as "===". If you turn on debug messages (using the options->debug messages
+ * menuitem) ChatZilla will always show the actual message type. This can be
+ * helpful when styling a particular response from the IRC server. See the
+ * description of the msg-type attribute below.
+ *
+ * You can modify these styles on your local system by placing your desired
+ * styles in a file called chatzilla.css in your <profile>/chrome directory.
+ * (the file won't be there already, you have to create it.) Add the line
+ *
+ * @import url(chatzilla.css);
+ *
+ * to your userContent.css (also in your <profile>/chrome directory, and
+ * also not there unless you created it already.) End all CSS rules in your
+ * new chatzilla.css with !important to override any styles declared here.
+ * For example, on a Linux system, you would create a file called
+ * /home/<username>/.mozilla/<username>/chrome/userContent.css (if it
+ * doesn't already exist), and add the line @import url(chatzilla.css) to it.
+ * Next, create /home/<username>/.mozilla/<username>/chrome/chatzilla.css, and
+ * add the text:
+ *
+ * .msg {
+ * font-size: 14pt !important;
+ * }
+ *
+ * .chatzilla-body {
+ * background: green !important;
+ * }
+ *
+ * Close your browser and restart. When you bring up ChatZilla, it should have
+ * a 14pt font and a green background.
+ *
+ * To learn how to make more useful changes to the ChatZilla output style, read
+ * on.
+ *
+ * All of the output in the message window is contained in an html <TABLE>.
+ * New messages are composed of <TR> and <TD> elements inside this <TABLE>.
+ * The message table and it's children have the following CSS classes assigned
+ * to them:
+ *
+ * + .msg-table is used as the class for the surrounding <TABLE>.
+ * Styles applied to this class will affect all parts of all messages.
+ *
+ * + .msg-nested-table is used as the class for the surrounding <TABLE> for
+ * messages sent from users with long nicknames. A new table is created, and
+ * nested inside a <TR colspan="2"> of the .msg-table. The rows of this
+ * table have their classes set as if they were direct children of the
+ * .msg-table. Placing messages from users with long nicknames in a nested
+ * table keeps the nickname column from getting too wide.
+ *
+ * + .msg is used as the class for the surrounding <TR>. This means that
+ * any styles applied here will affect the entire message.
+ *
+ * + .msg-timestamp is used as the class for the <TD> that has all the time
+ * information on it. Styles on this class will affect the time stamps
+ * against messages (but not the format of the time).
+ *
+ * + .msg-type is used as the class for the <TD> surrounding the message type
+ * portion of messages. Styles applied here will only affect message
+ * types. ie. "-->|", or "[HELP]".
+ *
+ * + .msg-user is used as the class for the <TD> surrounding the nickname
+ * portion of messages. ChatZilla makes use of the :before and :after
+ * pseudoclasses to decorate nicknames with different characters depending
+ * on their message type. For example, when a user performs a /me, their
+ * nickname may be surrounded by asterisks.
+ *
+ * + .msg-data is used as the class for the <TD> surrounding the actual text
+ * of the message.
+ *
+ * In addition to CSS class properties, portions of a message may have one
+ * or mode of the following attributes set:
+ *
+ * + view-type is the type of view the message is contained in. The types
+ * are:
+ * "IRCClient" for the *client* view
+ * "IRCNetwork" for network and server views
+ * "IRCChannel" for channel views
+ * "IRCUser" for query views
+ *
+ * + msg-type is the message type described above. There are too many types
+ * to list here. Turn on debug messages to force message types to print
+ * in the left column of the output.
+ *
+ * + msg-user is the nickname (in lowercase) of the nickname who sent the
+ * message. If you sent the message, msg-user will be set to "ME!", so
+ * that you can style your messages regardless of your nickname.
+ *
+ * + msg-dest is the name of the object that the message is directed to. It
+ * could be a channel name, or a nickname (both in lowercase.)
+ *
+ * + dest-type is the type of object the message is directed to. The types
+ * are:
+ * "IRCChannel" for messages sent to a channel.
+ * "IRCUser" for messages sent directly to another user, including
+ * private messages that appear in a network or channel view (when
+ * a dedicated query view does not exist.)
+ *
+ * + mark is either the text "even" or "odd". When the first user speaks on
+ * a channel, that message is marked as "even". Messages will continue to
+ * be marked "even" until a different user speaks, when the mark switches
+ * to "odd". Each view maintains it's own mark state. An example of how
+ * ChatZilla marks messages would be:
+ *
+ * EVEN: <moe> this deep fat fry-o-later is great.
+ * EVEN: <moe> It'll deep fat fry a whole buffalo in 30 seconds.
+ * ODD: <homer> but I'm hungry *now*!
+ *
+ * + important is either the text "true", or it is not set at all. If
+ * important is true, then the message triggered ChatZilla /stalk function.
+ * This occurs when someone with a nickname matching a pattern in your
+ * /stalk list speaks, when someone says a word that matches a pattern in
+ * your /stalk list, or when someone says your nickname.
+ */
+
+#splash-wrapper {
+ display: flex;
+ height: 100vh;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden;
+}
+
+/******************************************************************************
+ * basic classes *
+ ******************************************************************************/
+
+.chatzilla-body { /* The topmost container in the ChatZilla */
+ margin: 0px 0px 0px 0px; /* output window. */
+ background: #FFFFFF;
+ color: #000000;
+}
+
+a:link {
+ color: #0000EE;
+}
+a:active {
+ color: #EE0000;
+}
+a:visited {
+ color: #551A8B;
+}
+
+/* links */
+a.chatzilla-link {
+ text-decoration: none;
+ direction: ltr;
+ unicode-bidi: embed;
+}
+
+/* link hover effect */
+a.chatzilla-link:hover {
+ text-decoration: underline;
+}
+
+/* line marker */
+.chatzilla-line-marker {
+ box-shadow: 0px 2px black;
+}
+
+/* basic styles */
+.chatzilla-highlight[name="Large"] {
+ font-size: larger;
+}
+
+.chatzilla-highlight[name="Small"] {
+ font-size: smaller;
+}
+
+.chatzilla-highlight[name="Bold text"],
+.chatzilla-bold, a.chatzilla-bold.chatzilla-link {
+ font-weight: bold;
+}
+
+.chatzilla-italic {
+ font-style: italic;
+}
+
+/* In CSS, text-decoration is a list of decorations to apply to the text.
+ * However, as it is just one property, there is no way to apply it additively;
+ * instead, we're forced to have all the 7 combinations (of 8 - one is none).
+ */
+
+.chatzilla-overline {
+ text-decoration: overline;
+}
+
+.chatzilla-strikethrough {
+ text-decoration: line-through;
+}
+
+.chatzilla-underline,
+a.chatzilla-underline.chatzilla-link {
+ text-decoration: underline;
+}
+
+.chatzilla-overline.chatzilla-strikethrough {
+ text-decoration: overline line-through;
+}
+
+.chatzilla-overline.chatzilla-underline,
+a.chatzilla-overline.chatzilla-underline.chatzilla-link {
+ text-decoration: overline underline;
+}
+
+.chatzilla-strikethrough.chatzilla-underline,
+a.chatzilla-strikethrough.chatzilla-underline.chatzilla-link {
+ text-decoration: line-through underline;
+}
+
+.chatzilla-overline.chatzilla-strikethrough.chatzilla-underline,
+a.chatzilla-overline.chatzilla-strikethrough.chatzilla-underline.chatzilla-link {
+ text-decoration: overline line-through underline;
+}
+
+.chatzilla-teletype {
+ font-family: monospace;
+}
+
+.chatzilla-rheet {
+ font-weight: bold;
+}
+
+.chatzilla-decor {
+ display: none;
+}
+
+/* mirc colors */
+.chatzilla-fg00, a.chatzilla-fg00.chatzilla-link {
+ color: #FFFFFF;
+}
+
+.chatzilla-fg01, a.chatzilla-fg01.chatzilla-link {
+ color: #000000;
+}
+
+.chatzilla-fg02, a.chatzilla-fg02.chatzilla-link {
+ color: #00007F;
+}
+
+.chatzilla-fg03, a.chatzilla-fg03.chatzilla-link {
+ color: #009300;
+}
+
+.chatzilla-fg04, a.chatzilla-fg04.chatzilla-link {
+ color: #FF0000;
+}
+
+.chatzilla-fg05, a.chatzilla-fg05.chatzilla-link {
+ color: #7F0000;
+}
+
+.chatzilla-fg06, a.chatzilla-fg06.chatzilla-link {
+ color: #9C009C;
+}
+
+.chatzilla-fg07, a.chatzilla-fg07.chatzilla-link {
+ color: #FC7F00;
+}
+
+.chatzilla-fg08, a.chatzilla-fg08.chatzilla-link {
+ color: #FFFF00;
+}
+
+.chatzilla-fg09, a.chatzilla-fg09.chatzilla-link {
+ color: #00FC00;
+}
+
+.chatzilla-fg10, a.chatzilla-fg10.chatzilla-link {
+ color: #009393;
+}
+
+.chatzilla-fg11, a.chatzilla-fg11.chatzilla-link {
+ color: #00FFFF;
+}
+
+.chatzilla-fg12, a.chatzilla-fg12.chatzilla-link {
+ color: #0000FC;
+}
+
+.chatzilla-fg13, a.chatzilla-fg13.chatzilla-link {
+ color: #FF00FF;
+}
+
+.chatzilla-fg14, a.chatzilla-fg14.chatzilla-link {
+ color: #7F7F7F;
+}
+
+.chatzilla-fg15, a.chatzilla-fg15.chatzilla-link {
+ color: #D2D2D2;
+}
+
+.chatzilla-bg00, a.chatzilla-bg00.chatzilla-link {
+ background-color: #FFFFFF;
+}
+
+.chatzilla-bg01, a.chatzilla-bg01.chatzilla-link {
+ background-color: #000000;
+}
+
+.chatzilla-bg02, a.chatzilla-bg02.chatzilla-link {
+ background-color: #00007F;
+}
+
+.chatzilla-bg03, a.chatzilla-bg03.chatzilla-link {
+ background-color: #009300;
+}
+
+.chatzilla-bg04, a.chatzilla-bg04.chatzilla-link {
+ background-color: #FF0000;
+}
+
+.chatzilla-bg05, a.chatzilla-bg05.chatzilla-link {
+ background-color: #7F0000;
+}
+
+.chatzilla-bg06, a.chatzilla-bg06.chatzilla-link {
+ background-color: #9C009C;
+}
+
+.chatzilla-bg07, a.chatzilla-bg07.chatzilla-link {
+ background-color: #FC7F00;
+}
+
+.chatzilla-bg08, a.chatzilla-bg08.chatzilla-link {
+ background-color: #FFFF00;
+}
+
+.chatzilla-bg09, a.chatzilla-bg09.chatzilla-link {
+ background-color: #00FC00;
+}
+
+.chatzilla-bg10, a.chatzilla-bg10.chatzilla-link {
+ background-color: #009393;
+}
+
+.chatzilla-bg11, a.chatzilla-bg11.chatzilla-link {
+ background-color: #00FFFF;
+}
+
+.chatzilla-bg12, a.chatzilla-bg12.chatzilla-link {
+ background-color: #0000FC;
+}
+
+.chatzilla-bg13, a.chatzilla-bg13.chatzilla-link {
+ background-color: #FF00FF;
+}
+
+.chatzilla-bg14, a.chatzilla-bg14.chatzilla-link {
+ background-color: #7F7F7F;
+}
+
+.chatzilla-bg15, a.chatzilla-bg15.chatzilla-link {
+ background-color: #D2D2D2;
+}
+
+.chatzilla-control-char:before {
+ content: "[\\";
+}
+
+.chatzilla-control-char:after {
+ content: "]";
+}
+
+/* smiley faces */
+.chatzilla-emote-txt { /* emoticon text inside */
+ font-size: larger;
+}
+
+/******************************************************************************
+ * message class base definitions *
+ ******************************************************************************/
+
+.msg-table { /* <TABLE> containing all of the */
+ width: 100%; /* messages. */
+}
+
+.msg-nested-table { /* <TABLE> nested inside */
+ width: 100%; /* .msg-table for users with long */
+ margin: 0px; /* nicknames. */
+ border: 0px;
+ border-spacing: 0px;
+ padding: 0px;
+}
+
+.msg { /* .msg = a single message in the */
+ width: 100%; /* output window */
+}
+
+.msg-timestamp { /* .msg-timestamp = timestamp for */
+ font-style: normal !important; /* the message, done using */
+ vertical-align: top; /* :before and content. */
+ white-space: nowrap;
+}
+
+.msg-type { /* .msg-type = message type */
+ font-variant: small-caps; /* indicator */
+ font-size: 90%;
+ padding-right: 10px;
+ text-align: right;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+.msg-user { /* msg-user = nickname portion of */
+ text-align: right; /* a message (channel and query */
+ vertical-align: top; /* views) */
+ white-space: nowrap;
+}
+
+.msg-data { /* .msg-data = the text portion */
+ padding: 1px 1px 1px 3px; /* of a message */
+ width: 100%;
+ white-space: pre-wrap;
+}
+
+
+/******************************************************************************
+ * message class specific definitions *
+ ******************************************************************************/
+
+/* msg-user is the nickname of the person who spoke, or "ME!" if you said it.
+ * msg-type is the type of the message, taken from the irc message. If you
+ * turn on debug messages (options->debug messages), the msg-types will be
+ * displayed to the left of the messages for all messages except:
+ * PRIVMSG: when a user sends you, or a channel you are on a message.
+ * ACTION: when a user performs a /me.
+ * NOTIFY: when a server or user sends you a notification.
+ */
+.msg[msg-user="|"] .msg-data, /* messages from common "bulk */
+.msg[msg-user="||"] .msg-data, /* paste" nicks */
+.msg[msg-user="|||"] .msg-data,
+.msg[msg-user="]"] .msg-data,
+.msg[msg-user="["] .msg-data,
+.msg[msg-type="372"] .msg-data, /* MOTD */
+.msg[msg-type="EVAL-IN"] .msg-data, /* /eval results */
+.msg[msg-type="EVAL-OUT"] .msg-data {
+ font-size: 90%;
+ font-family: monospace;
+}
+
+.msg[msg-type="USAGE"] .msg-data {
+ font-style: italic;
+}
+
+.msg[msg-type="HELP"] .msg-data {
+ font-weight: normal;
+}
+
+.msg[msg-type="ACTION"] .msg-user {
+ font-style: italic;
+}
+
+.msg[important="true"] .msg-user {
+ font-weight: bold;
+}
+
+/******************************************************************************
+ * nickname decorations *
+ ******************************************************************************/
+
+/* :before and :after pseudoclasses form the decorations around nicknames */
+.msg-user:before {
+ content: "<";
+}
+.msg-user:after {
+ content: ">";
+}
+.msg[important="true"] .msg-user:before {
+ font-weight: bold;
+}
+.msg[important="true"] .msg-user:after {
+ font-weight: bold;
+}
+.msg[msg-user$="ME!"] .msg-user:before {
+ content: "<";
+}
+.msg[msg-user$="ME!"] .msg-user:after {
+ content: ">";
+}
+.msg[msg-type="ACTION"] .msg-user:before,
+.msg[msg-type="ACTION"] .msg-user:after {
+ content: "";
+}
+.msg[msg-type="NOTICE"] .msg-user:before {
+ content: "[";
+}
+.msg[msg-type="NOTICE"] .msg-user:after {
+ content: "]";
+}
+
+/* private messages in a query window */
+.msg[view-type="IRCUser"] .msg-user:before {
+ content: "{";
+}
+.msg[view-type="IRCUser"] .msg-user:after {
+ content: "}";
+}
+.msg[view-type="IRCUser"][msg-dest$="ME!"] .msg-user:before {
+ content: "{";
+}
+.msg[view-type="IRCUser"][msg-dest$="ME!"] .msg-user:after {
+ content: "}";
+}
+
+/* messages 'to' or 'from' somewhere other than where displayed */
+.msg[to-other] .msg-user:before {
+ content: "to(";
+}
+.msg[from-other] .msg-user:before {
+ content: "from(";
+}
+.msg[to-other] .msg-user:after,
+.msg[from-other] .msg-user:after {
+ content: ")";
+}
diff --git a/comm/suite/chatzilla/xul/content/output-window.html b/comm/suite/chatzilla/xul/content/output-window.html
new file mode 100644
index 0000000000..2155c282c3
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/output-window.html
@@ -0,0 +1,209 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style type="text/css">
+ [hidden="true"] {
+ display: none;
+ }
+
+ .header-outer {
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ margin: 0px;
+ padding: 0px;
+ }
+
+ .header {
+ background-color: white;
+ color: black;
+ margin: 2px;
+ border: 1px black solid;
+ }
+
+ .h-table,
+ #net-url,
+ #ch-url,
+ #dcc-chat-title {
+ width: 100%;
+ }
+
+ #splash {
+ font-size: 24pt;
+ font-weight: bold;
+ text-align: center;
+ }
+
+ #cli-version-container {
+ text-align: center;
+ width: 100%;
+ }
+
+ #usr-descnodes,
+ #ch-topicnodes {
+ line-height: 110%;
+ }
+
+ #ch-usercount,
+ #ch-modestr,
+ #net-lag,
+ #dcc-file-progress {
+ white-space: nowrap;
+ }
+
+ .label {
+ font-weight: bold;
+ text-align: right;
+ vertical-align: top;
+ white-space: nowrap;
+ padding-right: 0.5em;
+ }
+
+ .value {
+ vertical-align: top;
+ padding-right: 1em;
+ }
+
+ #usr-title,
+ #usr-descnodes {
+ text-align: center;
+ }
+ </style>
+
+ <script type="application/x-javascript" src="chrome://chatzilla/content/output-window.js"></script>
+ </head>
+
+ <body class="chatzilla-body">
+
+ <div class="header-outer">
+
+ <div class="header" id="cli-container" hidden="true">
+ <table class="h-table">
+ <tbody>
+ <tr>
+ <td class="label" localize="output.knownnets"></td>
+ <td class="value" id="cli-netcount"></td>
+ <td class="label" id="cli-version-container"
+ condition="yellow">ChatZilla <span id="cli-version">error</span></td>
+ <td class="label" localize="output.connnets"></td>
+ <td class="value" id="cli-connectcount" localize="none"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="header" id="net-container" hidden="true">
+ <table class="h-table">
+ <tbody>
+ <tr>
+ <td class="label" id="net-url-l" localize="output.url"></td>
+ <td class="value crop-right" id="net-url">
+ <a id="net-url-anchor" class="chatzilla-link"
+ href="irc://foo/bar">irc://foo/bar</a>
+ </td>
+ <td class="value" id="net-status"
+ condition="red" localize="output.notconn"></td>
+ <td class="label" id="net-lag-l" localize="output.lag"></td>
+ <td class="value" id="net-lag" localize="unknown"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="header" id="ch-container" hidden="true">
+ <table class="h-table">
+ <tbody>
+ <tr>
+ <td class="label" id="ch-url-l" localize="output.url"></td>
+ <td class="value crop-right" id="ch-url">
+ <a id="ch-url-anchor" class="chatzilla-link"
+ href="irc://foo/bar">irc://foo/bar</a>
+ </td>
+ <td class="label" id="ch-modestr-l" localize="output.mode"></td>
+ <td class="value" id="ch-modestr" localize="none"></td>
+ <td class="label" id="ch-usercount-l" localize="output.users"></td>
+ <td class="value" id="ch-usercount" localize="none"></td>
+ </tr>
+ <tr onclick="onTopicNodesClick(event);" style="cursor:default">
+ <td class="label" id="ch-topicnodes-l" localize="output.topic"></td>
+ <td class="value" colspan="6">
+ <span id="ch-topicnodes" localize="none"></span>
+ <input hidden="true" id="ch-topicinput" style="width:90%"
+ onblur="cancelTopicEdit();"
+ onkeypress="onTopicKeypress(event);"/>
+ <input type="button" hidden="true" id="ch-topiccancel"
+ onclick="setTimeout(cancelTopicEdit, 0, true);"
+ localize="output.cancel"/>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="header" id="usr-container" hidden="true">
+ <table class="h-table">
+ <tbody>
+ <tr>
+ <td class="label" localize="output.url"></td>
+ <td class="value crop-right" width="100%">
+ <a id="usr-url-anchor" class="chatzilla-link"
+ href="irc://foo/bar">irc://foo/bar</a>
+ </td>
+ <td class="label" id="usr-serverstr-l" localize="output.via"></td>
+ <td class="value" id="usr-serverstr" localize="none"></td>
+ </tr>
+ <tr>
+ <td id="usr-title" colspan="4" localize="none"></td>
+ </tr>
+ <tr>
+ <td id="usr-descnodes" colspan="4" localize="none"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="header" id="dcc-chat-container" hidden="true">
+ <table class="h-table">
+ <tbody>
+ <tr>
+ <td id="dcc-chat-title" localize="none"></td>
+ <td class="label" id="dcc-chat-remotestr-l" localize="output.to"></td>
+ <td class="value" id="dcc-chat-remotestr" localize="none"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="header" id="dcc-file-container" hidden="true">
+ <table class="h-table">
+ <tbody>
+ <tr>
+ <td class="label" localize="output.file"></td>
+ <td class="value crop-right" id="dcc-file-file" width="100%"></td>
+ <td class="label" localize="output.progress"></td>
+ <td class="value" id="dcc-file-progress" localize="unknown"></td>
+ </tr>
+ <tr>
+ <td colspan="4" class="progress-bg">
+ <table id="dcc-file-progressbar" width="0%"><tbody><tr>
+ <td class="progress-fg">&nbsp;</td>
+ </tr></tbody></table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ <div id="messages-outer" hidden="true">
+ <div id="splash-wrapper"><div id="splash"></div></div>
+ <div id="output"></div>
+ </div>
+ </body>
+</html>
diff --git a/comm/suite/chatzilla/xul/content/output-window.js b/comm/suite/chatzilla/xul/content/output-window.js
new file mode 100644
index 0000000000..8f670ec3ec
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/output-window.js
@@ -0,0 +1,588 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var initialized = false;
+
+var view;
+var client;
+var mainWindow;
+var clickHandler;
+
+var dd;
+var getMsg;
+var getObjectDetails;
+
+var header = null;
+var headers = {
+ IRCClient: {
+ prefix: "cli-",
+ fields: ["container", "netcount", "version-container", "version",
+ "connectcount"],
+ update: updateClient
+ },
+
+ IRCNetwork: {
+ prefix: "net-",
+ fields: ["container", "url-anchor", "status", "lag"],
+ update: updateNetwork
+ },
+
+ IRCChannel: {
+ prefix: "ch-",
+ fields: ["container", "url-anchor", "modestr", "usercount",
+ "topicnodes", "topicinput", "topiccancel"],
+ update: updateChannel
+ },
+
+ IRCUser: {
+ prefix: "usr-",
+ fields: ["container", "url-anchor", "serverstr", "title",
+ "descnodes"],
+ update: updateUser
+ },
+
+ IRCDCCChat: {
+ prefix: "dcc-chat-",
+ fields: ["container", "remotestr", "title"],
+ update: updateDCCChat
+ },
+
+ IRCDCCFileTransfer: {
+ prefix: "dcc-file-",
+ fields: ["container", "file", "progress", "progressbar"],
+ update: updateDCCFile
+ }
+};
+
+var initOutputWindow = stock_initOutputWindow;
+
+function stock_initOutputWindow(newClient, newView, newClickHandler)
+{
+ function initHeader()
+ {
+ /* it's better if we wait a half a second before poking at these
+ * dom nodes. */
+ setHeaderState(view.prefs["displayHeader"]);
+ updateHeader();
+ var div = document.getElementById("messages-outer");
+ div.removeAttribute("hidden");
+ window.scrollTo(0, window.document.body.clientHeight);
+ };
+
+ client = newClient;
+ view = newView;
+ clickHandler = newClickHandler;
+ mainWindow = client.mainWindow;
+
+ client.messageManager.importBundle(client.defaultBundle, window);
+
+ getMsg = mainWindow.getMsg;
+ getObjectDetails = mainWindow.getObjectDetails;
+ dd = mainWindow.dd;
+
+ // Wheee... localize stuff!
+ //var nodes = document.getElementsByAttribute("localize", "*");
+ var nodes = document.getElementsByTagName("*");
+ for (var i = 0; i < nodes.length; i++)
+ {
+ if (nodes[i].hasAttribute("localize"))
+ {
+ var msg = nodes[i].getAttribute("localize");
+ msg = getMsg("msg." + msg);
+ if (nodes[i].nodeName.toLowerCase() == "input")
+ nodes[i].value = msg;
+ else
+ nodes[i].appendChild(document.createTextNode(msg));
+ }
+ }
+
+ changeCSS("chrome://chatzilla/content/output-base.css", "cz-css-base");
+ changeCSS(view.prefs["motif.current"]);
+ updateMotifSettings();
+
+ var output = document.getElementById("output");
+ output.appendChild(adoptNode(view.messages));
+
+ if (view.TYPE in headers)
+ {
+ header = cacheNodes(headers[view.TYPE].prefix,
+ headers[view.TYPE].fields);
+ // Turn off accessibility announcements: they're useless as all these
+ // changes are in the "log" as well, normally.
+ // We're setting the attribute here instead of in the HTML to cope with
+ // custom output windows and so we set it only on the Right header
+ // for this view.
+ header["container"].setAttribute("aria-live", "off");
+ header.update = headers[view.TYPE].update;
+ }
+
+ var name;
+ if ("unicodeName" in view)
+ name = view.unicodeName;
+ else
+ name = view.name;
+ updateSplash(name);
+
+ setTimeout(initHeader, 500);
+
+ initialized = true;
+}
+
+function onTopicNodesClick(e)
+{
+ if (!clickHandler(e))
+ {
+ if (e.which != 1)
+ return;
+
+ startTopicEdit();
+ }
+
+ e.stopPropagation();
+}
+
+function onTopicKeypress(e)
+{
+ switch (e.keyCode)
+ {
+ case 13: /* enter */
+ var topic = header["topicinput"].value;
+ topic = mainWindow.replaceColorCodes(topic);
+ view.setTopic(topic);
+ cancelTopicEdit(true);
+ view.dispatch("focus-input");
+ break;
+
+ case 27: /* esc */
+ cancelTopicEdit(true);
+ view.dispatch("focus-input");
+ break;
+
+ default:
+ client.mainWindow.onInputKeyPress(e);
+ }
+}
+
+function startTopicEdit()
+{
+ var me = view.getUser(view.parent.me.unicodeName);
+ if (!me || (!view.mode.publicTopic && !me.isOp && !me.isHalfOp) ||
+ !hasAttribute("topicinput", "hidden"))
+ {
+ return;
+ }
+
+ header["topicinput"].value = mainWindow.decodeColorCodes(view.topic);
+
+ header["topicnodes"].setAttribute("hidden", "true")
+ header["topicinput"].removeAttribute("hidden");
+ header["topiccancel"].removeAttribute("hidden");
+ header["topicinput"].focus();
+ header["topicinput"].selectionStart = 0;
+}
+
+function cancelTopicEdit(force)
+{
+ var originalTopic = mainWindow.decodeColorCodes(view.topic);
+ if (!hasAttribute("topicnodes", "hidden") ||
+ (!force && (header["topicinput"].value != originalTopic)))
+ {
+ return;
+ }
+
+ header["topicinput"].setAttribute("hidden", "true");
+ header["topiccancel"].setAttribute("hidden", "true");
+ header["topicnodes"].removeAttribute("hidden");
+}
+
+function cacheNodes(pfx, ary, nodes)
+{
+ if (!nodes)
+ nodes = new Object();
+
+ for (var i = 0; i < ary.length; ++i)
+ nodes[ary[i]] = document.getElementById(pfx + ary[i]);
+
+ return nodes;
+}
+
+function changeCSS(url, id)
+{
+ if (!id)
+ id = "main-css";
+
+ var node = document.getElementById(id);
+
+ if (!node)
+ {
+ node = document.createElement("link");
+ node.setAttribute("id", id);
+ node.setAttribute("rel", "stylesheet");
+ node.setAttribute("type", "text/css");
+ var head = document.getElementsByTagName("head")[0];
+ head.appendChild(node);
+ }
+ else
+ {
+ if (node.getAttribute("href") == url)
+ return;
+ }
+
+ node.setAttribute("href", url);
+ window.scrollTo(0, window.document.body.clientHeight);
+}
+
+function scrollToElement(element, position)
+{
+ /* The following values can be used for element:
+ * selection - current selected text.
+ * marker - the activity marker.
+ * [any DOM node] - anything :)
+ *
+ * The following values can be used for position:
+ * top - scroll so it is at the top.
+ * center - scroll so it is in the middle.
+ * bottom - scroll so it is at the bottom.
+ * inview - scroll so it is in view.
+ */
+ switch (element)
+ {
+ case "selection":
+ var sel = window.getSelection();
+ if (sel)
+ element = sel.anchorNode;
+ else
+ element = null;
+ break;
+
+ case "marker":
+ if ("getActivityMarker" in view)
+ element = view.getActivityMarker();
+ else
+ element = null;
+ break;
+ }
+ if (!element)
+ return;
+
+ // Calculate element's position in document.
+ var pos = { top: 0, center: 0, bottom: 0 };
+ // Find first parent with offset data.
+ while (element && !("offsetParent" in element))
+ element = element.parentNode;
+ var elt = element;
+ // Calc total offset data.
+ while (elt)
+ {
+ pos.top += 0 + elt.offsetTop;
+ elt = elt.offsetParent;
+ }
+ pos.center = pos.top + element.offsetHeight / 2;
+ pos.bottom = pos.top + element.offsetHeight;
+
+ // Store the positions to align the element with.
+ var cont = { top: 0, center: window.innerHeight / 2,
+ bottom: window.innerHeight };
+ if (!hasAttribute("container", "hidden"))
+ {
+ /* Offset height doesn't include the margins, so we get to do that
+ * ourselves via getComputedStyle(). We're assuming that will return
+ * a px value, which is all but guaranteed.
+ */
+ var headerHeight = header["container"].offsetHeight;
+ var css = getComputedStyle(header["container"], null);
+ headerHeight += parseInt(css.marginTop) + parseInt(css.marginBottom);
+ cont.top += headerHeight;
+ cont.center += headerHeight / 2;
+ }
+
+ // Pick between 'top' and 'bottom' for 'inview' position.
+ if (position == "inview")
+ {
+ if (pos.top - window.scrollY < cont.top)
+ position = "top";
+ else if (pos.bottom - window.scrollY > cont.bottom)
+ position = "bottom";
+ else
+ return;
+ }
+
+ window.scrollTo(0, pos[position] - cont[position]);
+}
+
+function updateMotifSettings(existingTimeout)
+{
+ // Try... catch with a repeat to cope with the style sheet not being loaded
+ const TIMEOUT = 100;
+ try
+ {
+ existingTimeout += TIMEOUT;
+ view.motifSettings = getMotifSettings();
+ }
+ catch(ex)
+ {
+ if (existingTimeout >= 30000) // Stop after trying for 30 seconds
+ return;
+ if (ex.name == "NS_ERROR_DOM_INVALID_ACCESS_ERR") //not ready, try again
+ setTimeout(updateMotifSettings, TIMEOUT, existingTimeout);
+ else // something else, panic!
+ dd(ex);
+ }
+}
+
+function getMotifSettings()
+{
+ var re = new RegExp("czsettings\\.(\\w*)", "i");
+ var rules = document.getElementById("main-css").sheet.cssRules;
+ var rv = new Object();
+ var ary;
+ // Copy any settings, which are available in the motif using the
+ // "CZSETTINGS" selector. We only store the regexp match after checking
+ // the rule type because selectorText is not defined on other rule types.
+ for (var i = 0; i < rules.length; i++)
+ {
+ if ((rules[i].type == CSSRule.STYLE_RULE) &&
+ ((ary = rules[i].selectorText.match(re)) != null))
+ {
+ rv[ary[1]] = true;
+ }
+ }
+ return rv;
+}
+
+function adoptNode(node)
+{
+ return client.adoptNode(node, document);
+}
+
+function setText(field, text, checkCondition)
+{
+ if (!header[field].firstChild)
+ header[field].appendChild(document.createTextNode(""));
+
+ if (typeof text != "string")
+ {
+ text = MSG_UNKNOWN;
+ if (checkCondition)
+ setAttribute(field, "condition", "red");
+ }
+ else if (checkCondition)
+ {
+ setAttribute(field, "condition", "green");
+ }
+
+ header[field].firstChild.data = text;
+}
+
+function setAttribute(field, name, value)
+{
+ if (!value)
+ value = "true";
+
+ header[field].setAttribute(name, value);
+}
+
+function removeAttribute(field, name)
+{
+ header[field].removeAttribute(name);
+}
+
+function hasAttribute(field, name)
+{
+ return header[field].hasAttribute(name);
+}
+
+function setHeaderState(state)
+{
+ if (header)
+ {
+ if (state)
+ {
+ removeAttribute("container", "hidden");
+ updateHeader();
+ }
+ else
+ {
+ setAttribute("container", "hidden");
+ }
+ }
+}
+
+function updateHeader()
+{
+ document.title = view.getURL();
+
+ if (!header || hasAttribute("container", "hidden"))
+ return;
+
+ for (var id in header)
+ {
+ var value;
+
+ if (id == "url-anchor")
+ {
+ value = view.getURL();
+ setAttribute("url-anchor", "href", value);
+ setText("url-anchor", value);
+ }
+ else if (id in view)
+ {
+ setText(id, view[id]);
+ }
+ }
+
+ if (header.update)
+ header.update();
+}
+
+function updateClient()
+{
+ var n = 0, c = 0;
+ for (name in client.networks)
+ {
+ ++n;
+ if (client.networks[name].isConnected())
+ ++c;
+ }
+
+ setAttribute("version-container", "title", client.userAgent);
+ setAttribute("version-container", "condition", mainWindow.__cz_condition);
+ setText("version", mainWindow.__cz_version);
+ setText("netcount", String(n));
+ setText("connectcount", String(c));
+}
+
+function updateNetwork()
+{
+ if (view.state == mainWindow.NET_CONNECTING)
+ {
+ setText("status", MSG_CONNECTING);
+ setAttribute("status","condition", "yellow");
+ removeAttribute("status", "title");
+ setText("lag", MSG_UNKNOWN);
+ }
+ else if (view.isConnected())
+ {
+ setText("status", MSG_CONNECTED);
+ setAttribute("status","condition", "green");
+ setAttribute("status", "title",
+ getMsg(MSG_CONNECT_VIA, view.primServ.unicodeName));
+ var lag = view.primServ.lag;
+ if (lag != -1)
+ setText("lag", getMsg(MSG_FMT_SECONDS, lag.toFixed(2)));
+ else
+ setText("lag", MSG_UNKNOWN);
+
+ }
+ else
+ {
+ setText("status", MSG_DISCONNECTED);
+ setAttribute("status","condition", "red");
+ removeAttribute("status", "title");
+ setText("lag", MSG_UNKNOWN);
+ }
+}
+
+function updateChannel()
+{
+ header["topicnodes"].removeChild(header["topicnodes"].firstChild);
+
+ if (view.active)
+ {
+ var str = view.mode.getModeStr();
+ if (!str)
+ str = MSG_NO_MODE;
+ setText("modestr", str);
+ setAttribute("modestr", "condition", "green");
+
+ setText("usercount", getMsg(MSG_FMT_USERCOUNT,
+ [view.getUsersLength(), view.opCount,
+ view.halfopCount, view.voiceCount]));
+ setAttribute("usercount", "condition", "green");
+
+ if (view.topic)
+ {
+ var data = getObjectDetails(view);
+ data.dontLogURLs = true;
+ var mailto = client.prefs["munger.mailto"];
+ client.munger.getRule(".mailto").enabled = mailto;
+ var nodes = client.munger.munge(view.topic, null, data);
+ client.munger.getRule(".mailto").enabled = false;
+ header["topicnodes"].appendChild(adoptNode(nodes));
+ }
+ else
+ {
+ setText("topicnodes", MSG_NONE);
+ }
+ }
+ else
+ {
+ setText("modestr", MSG_UNKNOWN);
+ setAttribute("modestr", "condition", "red");
+ setText("usercount", MSG_UNKNOWN);
+ setAttribute("usercount", "condition", "red");
+ setText("topicnodes", MSG_UNKNOWN);
+ }
+
+}
+
+function updateUser()
+{
+ var source;
+ if (view.name)
+ source = "<" + view.name + "@" + view.host + ">";
+ else
+ source = MSG_UNKNOWN;
+
+ if (view.parent.isConnected)
+ setText("serverstr", view.connectionHost, true);
+ else
+ setText("serverstr", null, true);
+
+ setText("title", getMsg(MSG_TITLE_USER, [view.unicodeName, source]));
+
+ header["descnodes"].removeChild(header["descnodes"].firstChild);
+ if (typeof view.desc != "undefined")
+ {
+ var data = getObjectDetails(view);
+ data.dontLogURLs = true;
+ var nodes = client.munger.munge(view.desc, null, data);
+ header["descnodes"].appendChild(adoptNode(nodes));
+ }
+ else
+ {
+ setText("descnodes", "");
+ }
+}
+
+function updateDCCChat()
+{
+ if (view.state.state == 4)
+ setText("remotestr", view.remoteIP + ":" + view.port, true);
+ else
+ setText("remotestr", null, true);
+
+ setText("title", getMsg(MSG_TITLE_DCCCHAT, view.user.unicodeName));
+}
+
+function updateDCCFile()
+{
+ var pcent = view.progress;
+
+ setText("file", view.filename);
+ setText("progress", getMsg(MSG_DCCFILE_PROGRESS,
+ [pcent, mainWindow.getSISize(view.position),
+ mainWindow.getSISize(view.size),
+ mainWindow.getSISpeed(view.speed)]));
+
+ setAttribute("progressbar", "width", pcent + "%");
+}
+
+function updateSplash(content)
+{
+ var splash = document.getElementById("splash");
+ splash.appendChild(document.createTextNode(content));
+} \ No newline at end of file
diff --git a/comm/suite/chatzilla/xul/content/popups.xul b/comm/suite/chatzilla/xul/content/popups.xul
new file mode 100644
index 0000000000..aed0200cdf
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/popups.xul
@@ -0,0 +1,124 @@
+<?xml version="1.0"?>
+
+<!--
+ -
+ - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/chatzilla.dtd" >
+
+<?xml-stylesheet href="chrome://chatzilla/content/output-base.css" type="text/css"?>
+
+<overlay id="chatzilla-popup-overlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <overlaytarget id="popup-overlay-target">
+
+ <tooltip id="percentTooltip">
+ <grid>
+ <columns>
+ <column />
+ <column />
+ </columns>
+ <rows>
+ <row>
+ <label value="%U"/>
+ <label value="&Underline.label;"/>
+ </row>
+ <row>
+ <label value="%B"/>
+ <label value="&Bold.label;"/>
+ </row>
+ <row>
+ <label value="%R"/>
+ <label value="&Reverse.label;"/>
+ </row>
+ <row>
+ <label value="%O"/>
+ <label value="&Normal.label;"/>
+ </row>
+ <row>
+ <label value="%C"/>
+ <label value="&Color.label;"/>
+ </row>
+ <row>
+ <label value="%%C"/>
+ <label value="%C"/>
+ </row>
+ </rows>
+ </grid>
+ </tooltip>
+ <tooltip id="colorTooltip" orient="vertical">
+ <label value="%Cxx[,yy] &ForeBack.label;"/>
+ <grid>
+ <columns>
+ <column />
+ <column />
+ <column />
+ <column />
+ <column />
+ <column />
+ <column />
+ <column />
+ </columns>
+ <rows>
+ <row>
+ <box class="colorGrid chatzilla-bg00 chatzilla-fg01">
+ <label value="0"/>
+ </box>
+ <box class="colorGrid chatzilla-bg01 chatzilla-fg00">
+ <label value="1"/>
+ </box>
+ <box class="colorGrid chatzilla-bg02 chatzilla-fg00">
+ <label value="2"/>
+ </box>
+ <box class="colorGrid chatzilla-bg03 chatzilla-fg00">
+ <label value="3"/>
+ </box>
+ <box class="colorGrid chatzilla-bg04 chatzilla-fg00">
+ <label value="4"/>
+ </box>
+ <box class="colorGrid chatzilla-bg05 chatzilla-fg00">
+ <label value="5"/>
+ </box>
+ <box class="colorGrid chatzilla-bg06 chatzilla-fg00">
+ <label value="6"/>
+ </box>
+ <box class="colorGrid chatzilla-bg07 chatzilla-fg00">
+ <label value="7"/>
+ </box>
+ </row>
+ <row>
+ <box class="colorGrid chatzilla-bg08 chatzilla-fg01">
+ <label value="8"/>
+ </box>
+ <box class="colorGrid chatzilla-bg09 chatzilla-fg01">
+ <label value="9"/>
+ </box>
+ <box class="colorGrid chatzilla-bg10 chatzilla-fg01">
+ <label value="10"/>
+ </box>
+ <box class="colorGrid chatzilla-bg11 chatzilla-fg01">
+ <label value="11"/>
+ </box>
+ <box class="colorGrid chatzilla-bg12 chatzilla-fg01">
+ <label value="12"/>
+ </box>
+ <box class="colorGrid chatzilla-bg13 chatzilla-fg01">
+ <label value="13"/>
+ </box>
+ <box class="colorGrid chatzilla-bg14 chatzilla-fg01">
+ <label value="14"/>
+ </box>
+ <box class="colorGrid chatzilla-bg15 chatzilla-fg01">
+ <label value="15"/>
+ </box>
+ </row>
+ </rows>
+ </grid>
+ </tooltip>
+
+ </overlaytarget>
+
+</overlay>
diff --git a/comm/suite/chatzilla/xul/content/pref-irc-toolkit.xul b/comm/suite/chatzilla/xul/content/pref-irc-toolkit.xul
new file mode 100644
index 0000000000..e69fb1a165
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/pref-irc-toolkit.xul
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/pref-irc.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="chatzilla_pane"
+ label="&pref-irc.window.title;">
+
+ <label>&pref-irc.open.desc;</label>
+ <separator/>
+ <hbox pack="center">
+ <button label="&pref-irc.open.label;" accesskey="&pref-irc.open.accesskey;"
+ oncommand="this.disabled = true;
+ window.openDialog('chrome://chatzilla/content/config.xul',
+ '', 'chrome,modal,resizable');
+ this.disabled = false;"/>
+ </hbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/chatzilla/xul/content/prefs.js b/comm/suite/chatzilla/xul/content/prefs.js
new file mode 100644
index 0000000000..f4ef4fc7de
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/prefs.js
@@ -0,0 +1,1213 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+const DEFAULT_NICK = "IRCMonkey"
+
+function initPrefs()
+{
+ function makeLogNameClient()
+ {
+ return makeLogName(client, "client");
+ };
+
+ client.prefManager = new PrefManager("extensions.irc.",
+ client.defaultBundle);
+ client.prefManagers = [client.prefManager];
+
+ client.prefs = client.prefManager.prefs;
+
+ var profilePath = getSpecialDirectory("ProfD");
+ profilePath.append("chatzilla");
+
+ client.prefManager.addPref("profilePath", profilePath.path, null, null,
+ "hidden");
+
+ profilePath = new nsLocalFile(client.prefs["profilePath"]);
+
+ if (!profilePath.exists())
+ mkdir(profilePath);
+
+ client.prefManager.profilePath = profilePath;
+
+ var scriptPath = profilePath.clone();
+ scriptPath.append("scripts");
+ if (!scriptPath.exists())
+ mkdir(scriptPath);
+
+ var logPath = profilePath.clone();
+ logPath.append("logs");
+ if (!logPath.exists())
+ mkdir(logPath);
+ client.prefManager.logPath = logPath;
+
+ var downloadsPath = profilePath.clone();
+ downloadsPath.append("downloads");
+ if (!downloadsPath.exists())
+ mkdir(downloadsPath);
+
+ var logDefault = client.prefManager.logPath.clone();
+ logDefault.append(escapeFileName("client.log"));
+
+ // Set up default nickname, if possible.
+ var defaultNick = DEFAULT_NICK;
+ var en = getService("@mozilla.org/process/environment;1", "nsIEnvironment");
+ if (en)
+ {
+ /* Get the enviroment variables used by various OSes:
+ * USER - Linux, macOS and other *nix-types.
+ * USERNAME - Windows.
+ * LOGNAME - *nix again.
+ */
+ const vars = ["USER", "USERNAME", "LOGNAME"];
+
+ for (var i = 0; i < vars.length; i++)
+ {
+ var nick = en.get(vars[i]);
+ if (nick)
+ {
+ defaultNick = nick.replace(/ /g, "_");
+ break;
+ }
+ }
+ }
+
+ // Set a property so network ident prefs get the same group later:
+ client.prefManager.identGroup = ".connect";
+ // Linux and OS X won't let non-root listen on port 113.
+ if ((client.platform == "Linux") || (client.platform == "Mac"))
+ client.prefManager.identGroup = "hidden";
+
+ var prefs =
+ [
+ ["activityFlashDelay", 200, "hidden"],
+ ["alert.overlapDelay", 50, "hidden"],
+ ["alert.floodDensity", 290, "hidden"],
+ ["alert.floodDispersion", 200, "hidden"],
+ ["alert.enabled", true, ".palert"],
+ ["alert.globalEnabled", true, "global.palertconfig"],
+ ["alert.clickable", true, "hidden"],
+ ["alert.nonFocusedOnly", true, "global.palertconfig"],
+ ["alert.channel.event", false, ".palert"],
+ ["alert.channel.chat", false, ".palert"],
+ ["alert.channel.stalk", true, ".palert"],
+ ["alert.user.chat", true, ".palert"],
+ ["aliases", [], "lists.aliases"],
+ ["autoAwayCap", 300, "global"],
+ ["autoAwayPeriod", 2, "appearance.misc"],
+ ["autoMarker", false, "appearance.misc"],
+ ["autoperform.channel", [], "lists.autoperform"],
+ ["autoperform.client", [], "lists.autoperform"],
+ ["autoperform.network", [], "lists.autoperform"],
+ ["autoperform.user", ["whois"], "lists.autoperform"],
+ ["autoRejoin", false, ".connect"],
+ ["away", "", "hidden"],
+ ["awayIdleMsg", "", ".ident"],
+ ["awayIdleTime", 0, ".ident"],
+ ["awayNick", "", ".ident"],
+ ["bugKeyword", "bug", "appearance.misc"],
+ ["bugURL", "https://bugzilla.mozilla.org/show_bug.cgi?id=%s",
+ "appearance.misc"],
+ ["bugURL.comment", "#c%s", "appearance.misc"],
+ ["channelHeader", true, "global.header"],
+ ["channelLog", false, "global.log"],
+ ["channelMaxLines", 500, "global.maxLines"],
+ ["charset", "utf-8", ".connect"],
+ ["clientMaxLines", 200, "global.maxLines"],
+ ["collapseActions", true, "appearance.misc"],
+ ["collapseMsgs", false, "appearance.misc"],
+ ["conference.limit", 150, "appearance.misc"],
+ ["connectTries", -1, ".connect"],
+ ["copyMessages", true, "global"],
+ ["dcc.autoAccept.delay", 10000, "hidden"],
+ ["dcc.downloadsFolder", getURLSpecFromFile(downloadsPath.path),
+ "dcc"],
+ ["dcc.enabled", true, "dcc"],
+ ["dcc.listenPorts", [], "dcc.ports"],
+ ["dcc.useServerIP", true, "dcc"],
+ ["dccUserHeader", true, "global.header"],
+ ["dccUserLog", false, "global.log"],
+ ["dccUserMaxLines", 500, "global.maxLines"],
+ ["debugMode", "", "hidden"],
+ ["defaultQuitMsg", "", ".connect"],
+ ["deleteOnPart", true, "global"],
+ ["desc", "New Now Know How", ".ident"],
+ ["displayHeader", true, "appearance.misc"],
+ ["font.family", "default", "appearance.misc"],
+ ["font.size", 0, "appearance.misc"],
+ ["guessCommands", true, "hidden"],
+ ["hasPrefs", false, "hidden"],
+ ["identd.enabled", false, client.prefManager.identGroup],
+ ["initialScripts", ["scripts/"], "startup.initialScripts"],
+ ["initialURLs", [], "startup.initialURLs"],
+ ["inputSpellcheck", true, "global"],
+ ["log", false,
+ ".log"],
+ ["logFile.channel", "$(network)/channels/$(channel).$y-$m-$d.log",
+ "hidden"],
+ ["logFile.client", "client.$y-$m-$d.log",
+ "hidden"],
+ ["logFile.dccuser", "dcc/$(user)/$(user).$y-$m-$d.log",
+ "hidden"],
+ ["logFile.network", "$(network)/$(network).$y-$m-$d.log",
+ "hidden"],
+ ["logFile.user", "$(network)/users/$(user).$y-$m-$d.log",
+ "hidden"],
+ ["logFileName", makeLogNameClient,
+ "hidden"],
+ ["logFolder", getURLSpecFromFile(logPath.path), ".log"],
+ ["login.promptToSave", true, "global.security"],
+ ["motif.current", "chrome://chatzilla/skin/output-light.css",
+ "appearance.motif"],
+ ["motif.dark", "chrome://chatzilla/skin/output-dark.css",
+ "appearance.motif"],
+ ["motif.light", "chrome://chatzilla/skin/output-light.css",
+ "appearance.motif"],
+ ["multiline", false, "hidden"],
+ ["munger.bold", true, "munger"],
+ ["munger.bugzilla-link", true, "munger"],
+ ["munger.channel-link",true, "munger"],
+ ["munger.colorCodes", true, "munger"],
+ ["munger.ctrl-char", true, "munger"],
+ ["munger.face", true, "munger"],
+ ["munger.italic", true, "munger"],
+ ["munger.link", true, "munger"],
+ ["munger.mailto", true, "munger"],
+ ["munger.quote", true, "munger"],
+ ["munger.rheet", true, "munger"],
+ ["munger.talkback-link", true, "munger"],
+ ["munger.teletype", true, "munger"],
+ ["munger.underline", true, "munger"],
+ ["munger.word-hyphenator", true, "munger"],
+ ["networkHeader", true, "global.header"],
+ ["networkLog", false, "global.log"],
+ ["networkMaxLines", 200, "global.maxLines"],
+ ["newTabLimit", 30, "global"],
+ ["nickCompleteStr", ":", "global"],
+ ["nickname", defaultNick, ".ident"],
+ ["nicknameList", [], "lists.nicknameList"],
+ ["notify.aggressive", true, "global"],
+ ["outgoing.colorCodes", true, "global"],
+ ["outputWindowURL", "chrome://chatzilla/content/output-window.html",
+ "hidden"],
+ ["proxy.typeOverride", "", ".connect"],
+ ["reconnect", true, ".connect"],
+ ["sasl.plain.enabled", false, ".ident"],
+ ["showModeSymbols", false, "appearance.userlist"],
+ ["sortUsersByMode", true, "appearance.userlist"],
+ // Chat == "Activity" activity.
+ // Event == "Superfluous" activity.
+ // Stalk == "Attention" activity.
+ // Start == When view it opened.
+ ["sound.channel.chat", "", ".soundEvts"],
+ ["sound.channel.event", "", ".soundEvts"],
+ ["sound.channel.stalk", "beep", ".soundEvts"],
+ ["sound.channel.start", "", ".soundEvts"],
+ ["sound.enabled", true, "global.sounds"],
+ ["sound.overlapDelay", 2000, "global.sounds"],
+ ["sound.user.stalk", "beep", ".soundEvts"],
+ ["sound.user.start", "beep beep", ".soundEvts"],
+ ["stalkWholeWords", true, "lists.stalkWords"],
+ ["stalkWords", [], "lists.stalkWords"],
+ ["sts.enabled", true, ".connect"],
+ ["tabLabel", "", "hidden"],
+ ["tabGotoKeyModifiers", 0, "hidden"],
+ ["timestamps", false, "appearance.timestamps"],
+ ["timestamps.display", "[%H:%M]", "appearance.timestamps"],
+ ["timestamps.log", "[%Y-%m-%d %H:%M:%S]", "hidden"],
+ ["upgrade-insecure", false, ".connect"],
+ ["urls.display", 10, "hidden"],
+ ["urls.store.max", 100, "global"],
+ ["userHeader", true, "global.header"],
+ ["userlistLeft", true, "appearance.userlist"],
+ ["userLog", false, "global.log"],
+ ["userMaxLines", 200, "global.maxLines"],
+ ["usermode", "+i", ".ident"],
+ ["username", "chatzilla", ".ident"],
+ ["warnOnClose", true, "global"]
+ ];
+
+ client.prefManager.addPrefs(prefs);
+ client.prefManager.addObserver({ onPrefChanged: onPrefChanged });
+
+ CIRCNetwork.prototype.stayingPower = client.prefs["reconnect"];
+ CIRCNetwork.prototype.MAX_CONNECT_ATTEMPTS = client.prefs["connectTries"];
+ CIRCNetwork.prototype.INITIAL_NICK = client.prefs["nickname"];
+ CIRCNetwork.prototype.INITIAL_NAME = client.prefs["username"];
+ CIRCNetwork.prototype.INITIAL_DESC = client.prefs["desc"];
+ CIRCNetwork.prototype.INITIAL_UMODE = client.prefs["usermode"];
+ CIRCNetwork.prototype.MAX_MESSAGES = client.prefs["networkMaxLines"];
+ CIRCNetwork.prototype.PROXY_TYPE_OVERRIDE = client.prefs["proxy.typeOverride"];
+ CIRCNetwork.prototype.USE_SASL = client.prefs["sasl.plain.enabled"];
+ CIRCNetwork.prototype.UPGRADE_INSECURE = client.prefs["upgrade-insecure"];
+ CIRCNetwork.prototype.STS_MODULE.ENABLED = client.prefs["sts.enabled"];
+ CIRCChannel.prototype.MAX_MESSAGES = client.prefs["channelMaxLines"];
+ CIRCUser.prototype.MAX_MESSAGES = client.prefs["userMaxLines"];
+ CIRCDCCChat.prototype.MAX_MESSAGES = client.prefs["dccUserMaxLines"];
+ CIRCDCCFileTransfer.prototype.MAX_MESSAGES = client.prefs["dccUserMaxLines"];
+ CIRCDCC.prototype.listenPorts = client.prefs["dcc.listenPorts"];
+ client.MAX_MESSAGES = client.prefs["clientMaxLines"];
+ client.charset = client.prefs["charset"];
+
+ initAliases();
+}
+
+function makeLogName(obj, type)
+{
+ function replaceNonPrintables(ch) {
+ var rv = ch.charCodeAt().toString(16);
+ if (rv.length == 1)
+ rv = "0" + rv;
+ else if (rv.length == 3)
+ rv = "u0" + rv;
+ else if (rv.length == 4)
+ rv = "u" + rv;
+
+ return "%" + rv;
+ };
+
+ function encode(text)
+ {
+ text = text.replace(/[^-A-Z0-9_#!.,'@~\[\]{}()%$"]/gi, replaceNonPrintables);
+
+ return encodeURIComponent(text);
+ };
+
+ /* /\$\(([^)]+)\)|\$(\w)/g *
+ * <-----> <--> *
+ * longName shortName *
+ */
+ function replaceParam(match, longName, shortName)
+ {
+ if (typeof longName != "undefined" && longName)
+ {
+ // Remember to encode these, don't want some dodgy # breaking stuff.
+ if (longName in longCodes)
+ return encode(longCodes[longName]);
+
+ dd("Unknown long code: " + longName);
+ }
+ else if (typeof shortName != "undefined" && shortName)
+ {
+ if (shortName in shortCodes)
+ return encode(shortCodes[shortName]);
+
+ dd("Unknown short code: " + shortName);
+ }
+ else
+ {
+ dd("Unknown match: " + match);
+ }
+
+ return match;
+ };
+
+ var base = client.prefs["logFolder"];
+ var specific = client.prefs["logFile." + type];
+
+ // Make sure we got ourselves a slash, or we'll be in trouble with the
+ // concatenation.
+ if (!base.match(/\/$/))
+ base = base + "/";
+ var file = base + specific;
+
+ // Get details for $-replacement variables.
+ var info = getObjectDetails(obj);
+
+ // Store the most specific time short code on the object.
+ obj.smallestLogInterval = "";
+ if (file.indexOf("$y") != -1)
+ obj.smallestLogInterval = "y";
+ if (file.indexOf("$m") != -1)
+ obj.smallestLogInterval = "m";
+ if (file.indexOf("$d") != -1)
+ obj.smallestLogInterval = "d";
+ if (file.indexOf("$h") != -1)
+ obj.smallestLogInterval = "h";
+
+ // Three longs codes: $(network), $(channel) and $(user).
+ // Each is available only if appropriate for the object.
+ var longCodes = new Object();
+ if (info.network)
+ longCodes["network"] = info.network.unicodeName;
+ if (info.channel)
+ longCodes["channel"] = info.channel.unicodeName;
+ if (info.user)
+ longCodes["user"] = info.user.unicodeName;
+
+ // 4 short codes: $y, $m, $d, $h.
+ // These are time codes, each replaced with a fixed-length number.
+ var d = new Date();
+ var shortCodes = { y: padNumber(d.getFullYear(), 4),
+ m: padNumber(d.getMonth() + 1, 2),
+ d: padNumber(d.getDate(), 2),
+ h: padNumber(d.getHours(), 2)
+ };
+
+ // Replace all $-variables in one go.
+ file = file.replace(/\$\(([^)]+)\)|\$(\w)/g, replaceParam);
+
+ // Convert from file: URL to local OS format.
+ try
+ {
+ file = getFileFromURLSpec(file).path;
+ }
+ catch(ex)
+ {
+ dd("Error converting '" + base + specific + "' to a local file path.");
+ }
+
+ return file;
+}
+
+function pref_mungeName(name)
+{
+ var safeName = name.replace(/\./g, "-").replace(/:/g, "_").toLowerCase();
+ return ecmaEscape(safeName);
+}
+
+function getNetworkPrefManager(network)
+{
+ function defer(prefName)
+ {
+ return client.prefs[prefName];
+ };
+
+ function makeLogNameNetwork()
+ {
+ return makeLogName(network, "network");
+ };
+
+ function onPrefChanged(prefName, newValue, oldValue)
+ {
+ onNetworkPrefChanged (network, prefName, newValue, oldValue);
+ };
+
+ var logDefault = client.prefManager.logPath.clone();
+ logDefault.append(escapeFileName(pref_mungeName(network.encodedName)) + ".log");
+
+ var prefs =
+ [
+ ["alert.enabled", defer, ".palert"],
+ ["alert.channel.event",defer, ".palert"],
+ ["alert.channel.chat", defer, ".palert"],
+ ["alert.channel.stalk",defer, ".palert"],
+ ["alert.user.chat", defer, ".palert"],
+ ["autoAwayPeriod", defer, "appearance.misc"],
+ ["autoMarker", defer, "appearance.misc"],
+ ["autoperform", [], "lists.autoperform"],
+ ["autoRejoin", defer, ".connect"],
+ ["away", defer, "hidden"],
+ ["awayNick", defer, ".ident"],
+ ["bugURL", defer, "appearance.misc"],
+ ["bugURL.comment", defer, "appearance.misc"],
+ ["charset", defer, ".connect"],
+ ["collapseActions", defer, "appearance.misc"],
+ ["collapseMsgs", defer, "appearance.misc"],
+ ["conference.limit", defer, "appearance.misc"],
+ ["connectTries", defer, ".connect"],
+ ["dcc.autoAccept.list", [], "dcc.autoAccept"],
+ ["dcc.downloadsFolder", defer, "dcc"],
+ ["dcc.useServerIP", defer, "dcc"],
+ ["defaultQuitMsg", defer, ".connect"],
+ ["desc", defer, ".ident"],
+ ["displayHeader", client.prefs["networkHeader"], "appearance.misc"],
+ ["font.family", defer, "appearance.misc"],
+ ["font.size", defer, "appearance.misc"],
+ ["hasPrefs", false, "hidden"],
+ ["identd.enabled", defer, client.prefManager.identGroup],
+ ["ignoreList", [], "hidden"],
+ ["log", client.prefs["networkLog"], ".log"],
+ ["logFileName", makeLogNameNetwork, "hidden"],
+ ["motif.current", defer, "appearance.motif"],
+ ["nickname", defer, ".ident"],
+ ["nicknameList", defer, "lists.nicknameList"],
+ ["notifyList", [], "lists.notifyList"],
+ ["outputWindowURL", defer, "hidden"],
+ ["proxy.typeOverride", defer, ".connect"],
+ ["reconnect", defer, ".connect"],
+ ["sasl.plain.enabled", defer, ".ident"],
+ ["sound.channel.chat", defer, ".soundEvts"],
+ ["sound.channel.event", defer, ".soundEvts"],
+ ["sound.channel.stalk", defer, ".soundEvts"],
+ ["sound.channel.start", defer, ".soundEvts"],
+ ["sound.user.stalk", defer, ".soundEvts"],
+ ["sound.user.start", defer, ".soundEvts"],
+ ["tabLabel", "", "hidden"],
+ ["timestamps", defer, "appearance.timestamps"],
+ ["timestamps.display", defer, "appearance.timestamps"],
+ ["timestamps.log", defer, "hidden"],
+ ["upgrade-insecure", defer, ".connect"],
+ ["usermode", defer, ".ident"],
+ ["username", defer, ".ident"]
+ ];
+
+ var branch = "extensions.irc.networks." + pref_mungeName(network.encodedName) +
+ ".";
+ var prefManager = new PrefManager(branch, client.defaultBundle);
+ prefManager.addPrefs(prefs);
+ prefManager.addObserver({ onPrefChanged: onPrefChanged });
+ client.prefManager.addObserver(prefManager);
+
+ var value = prefManager.prefs["nickname"];
+ if (value != CIRCNetwork.prototype.INITIAL_NICK)
+ network.INITIAL_NICK = value;
+
+ value = prefManager.prefs["username"];
+ if (value != CIRCNetwork.prototype.INITIAL_NAME)
+ network.INITIAL_NAME = value;
+
+ value = prefManager.prefs["desc"];
+ if (value != CIRCNetwork.prototype.INITIAL_DESC)
+ network.INITIAL_DESC = value;
+
+ value = prefManager.prefs["usermode"];
+ if (value != CIRCNetwork.prototype.INITIAL_UMODE)
+ network.INITIAL_UMODE = value;
+
+ value = prefManager.prefs["proxy.typeOverride"];
+ if (value != CIRCNetwork.prototype.PROXY_TYPE_OVERRIDE)
+ network.PROXY_TYPE_OVERRIDE = value;
+
+ value = prefManager.prefs["sasl.plain.enabled"];
+ if (value != CIRCNetwork.prototype.USE_SASL)
+ network.USE_SASL = value;
+
+ value = prefManager.prefs["upgrade-insecure"];
+ if (value != CIRCNetwork.prototype.UPGRADE_INSECURE)
+ network.UPGRADE_INSECURE = value;
+
+ network.stayingPower = prefManager.prefs["reconnect"];
+ network.MAX_CONNECT_ATTEMPTS = prefManager.prefs["connectTries"];
+
+ client.prefManagers.push(prefManager);
+
+ return prefManager;
+}
+
+function getChannelPrefManager(channel)
+{
+ var network = channel.parent.parent;
+
+ function defer(prefName)
+ {
+ return network.prefs[prefName];
+ };
+
+ function makeLogNameChannel()
+ {
+ return makeLogName(channel, "channel");
+ };
+
+ function onPrefChanged(prefName, newValue, oldValue)
+ {
+ onChannelPrefChanged (channel, prefName, newValue, oldValue);
+ };
+
+ var logDefault = client.prefManager.logPath.clone();
+ var filename = pref_mungeName(network.encodedName) + "," +
+ pref_mungeName(channel.encodedName);
+
+ logDefault.append(escapeFileName(filename) + ".log");
+
+ var prefs =
+ [
+ ["alert.enabled", defer, ".palert"],
+ ["alert.channel.event",defer, ".palert"],
+ ["alert.channel.chat", defer, ".palert"],
+ ["alert.channel.stalk",defer, ".palert"],
+ ["autoperform", [], "lists.autoperform"],
+ ["autoRejoin", defer, ".connect"],
+ ["autoMarker", defer, "appearance.misc"],
+ ["bugURL", defer, "appearance.misc"],
+ ["bugURL.comment", defer, "appearance.misc"],
+ ["charset", defer, ".connect"],
+ ["collapseActions", defer, "appearance.misc"],
+ ["collapseMsgs", defer, "appearance.misc"],
+ ["conference.enabled", false, "hidden"],
+ ["conference.limit", defer, "appearance.misc"],
+ ["displayHeader", client.prefs["channelHeader"], "appearance.misc"],
+ ["font.family", defer, "appearance.misc"],
+ ["font.size", defer, "appearance.misc"],
+ ["hasPrefs", false, "hidden"],
+ ["log", client.prefs["channelLog"], ".log"],
+ ["logFileName", makeLogNameChannel, "hidden"],
+ ["motif.current", defer, "appearance.motif"],
+ ["outputWindowURL", defer, "hidden"],
+ ["sound.channel.chat", defer, ".soundEvts"],
+ ["sound.channel.event", defer, ".soundEvts"],
+ ["sound.channel.stalk", defer, ".soundEvts"],
+ ["sound.channel.start", defer, ".soundEvts"],
+ ["tabLabel", "", "hidden"],
+ ["timestamps", defer, "appearance.timestamps"],
+ ["timestamps.display", defer, "appearance.timestamps"],
+ ["timestamps.log", defer, "hidden"]
+ ];
+
+ var branch = "extensions.irc.networks." + pref_mungeName(network.encodedName) +
+ ".channels." + pref_mungeName(channel.encodedName) + "."
+ var prefManager = new PrefManager(branch, client.defaultBundle);
+ prefManager.addPrefs(prefs);
+ prefManager.addObserver({ onPrefChanged: onPrefChanged });
+ network.prefManager.addObserver(prefManager);
+
+ client.prefManagers.push(prefManager);
+
+ return prefManager;
+}
+
+function getUserPrefManager(user)
+{
+ var network = user.parent.parent;
+
+ function defer(prefName)
+ {
+ return network.prefs[prefName];
+ };
+
+ function makeLogNameUser()
+ {
+ return makeLogName(user, "user");
+ };
+
+ function onPrefChanged(prefName, newValue, oldValue)
+ {
+ onUserPrefChanged (user, prefName, newValue, oldValue);
+ };
+
+ var logDefault = client.prefManager.logPath.clone();
+ var filename = pref_mungeName(network.encodedName);
+ filename += "," + pref_mungeName(user.encodedName);
+ logDefault.append(escapeFileName(filename) + ".log");
+
+ var prefs =
+ [
+ ["alert.enabled", defer, ".palert"],
+ ["alert.user.chat", defer, ".palert"],
+ ["autoperform", [], "lists.autoperform"],
+ ["charset", defer, ".connect"],
+ ["collapseActions", defer, "appearance.misc"],
+ ["collapseMsgs", defer, "appearance.misc"],
+ ["displayHeader", client.prefs["userHeader"], "appearance.misc"],
+ ["font.family", defer, "appearance.misc"],
+ ["font.size", defer, "appearance.misc"],
+ ["hasPrefs", false, "hidden"],
+ ["log", client.prefs["userLog"], ".log"],
+ ["logFileName", makeLogNameUser, "hidden"],
+ ["motif.current", defer, "appearance.motif"],
+ ["outputWindowURL", defer, "hidden"],
+ ["sound.user.stalk", defer, ".soundEvts"],
+ ["sound.user.start", defer, ".soundEvts"],
+ ["tabLabel", "", "hidden"],
+ ["timestamps", defer, "appearance.timestamps"],
+ ["timestamps.display", defer, "appearance.timestamps"],
+ ["timestamps.log", defer, "hidden"]
+ ];
+
+ var branch = "extensions.irc.networks." + pref_mungeName(network.encodedName) +
+ ".users." + pref_mungeName(user.encodedName) + ".";
+ var prefManager = new PrefManager(branch, client.defaultBundle);
+ prefManager.addPrefs(prefs);
+ prefManager.addObserver({ onPrefChanged: onPrefChanged });
+ network.prefManager.addObserver(prefManager);
+
+ client.prefManagers.push(prefManager);
+
+ return prefManager;
+}
+
+function getDCCUserPrefManager(user)
+{
+ function defer(prefName)
+ {
+ return client.prefs[prefName];
+ };
+
+ function makeLogNameUser()
+ {
+ return makeLogName(user, "dccuser");
+ };
+
+ function onPrefChanged(prefName, newValue, oldValue)
+ {
+ onDCCUserPrefChanged(user, prefName, newValue, oldValue);
+ };
+
+ var prefs =
+ [
+ ["alert.enabled", defer, ".palert"],
+ ["alert.user.chat", defer, ".palert"],
+ ["charset", defer, ".connect"],
+ ["collapseMsgs", defer, "appearance.misc"],
+ ["displayHeader", client.prefs["dccUserHeader"], "appearance.misc"],
+ ["font.family", defer, "appearance.misc"],
+ ["font.size", defer, "appearance.misc"],
+ ["hasPrefs", false, "hidden"],
+ ["log", client.prefs["dccUserLog"], ".log"],
+ ["logFileName", makeLogNameUser, "hidden"],
+ ["motif.current", defer, "appearance.motif"],
+ ["outputWindowURL", defer, "hidden"],
+ ["tabLabel", "", "hidden"],
+ ["timestamps", defer, "appearance.timestamps"],
+ ["timestamps.display", defer, "appearance.timestamps"],
+ ["timestamps.log", defer, "hidden"]
+ ];
+
+ var branch = "extensions.irc.dcc.users." +
+ pref_mungeName(user.canonicalName) + ".";
+ var prefManager = new PrefManager(branch, client.defaultBundle);
+ prefManager.addPrefs(prefs);
+ prefManager.addObserver({ onPrefChanged: onPrefChanged });
+ client.prefManager.addObserver(prefManager);
+
+ client.prefManagers.push(prefManager);
+
+ return prefManager;
+}
+
+function destroyPrefs()
+{
+ if ("prefManagers" in client)
+ {
+ for (var i = 0; i < client.prefManagers.length; ++i)
+ client.prefManagers[i].destroy();
+ client.prefManagers = [];
+ }
+}
+
+function onPrefChanged(prefName, newValue, oldValue)
+{
+ if (newValue == oldValue)
+ return;
+
+ switch (prefName)
+ {
+ case "awayIdleTime":
+ uninitIdleAutoAway(oldValue);
+ initIdleAutoAway(newValue);
+ break;
+
+ case "bugKeyword":
+ client.munger.delRule("bugzilla-link");
+ addBugzillaLinkMungerRule(newValue, 10, 10);
+ break;
+
+ case "channelMaxLines":
+ CIRCChannel.prototype.MAX_MESSAGES = newValue;
+ break;
+
+ case "charset":
+ client.charset = newValue;
+ break;
+
+ case "clientMaxLines":
+ client.MAX_MESSAGES = newValue;
+ break;
+
+ case "connectTries":
+ CIRCNetwork.prototype.MAX_CONNECT_ATTEMPTS = newValue;
+ break;
+
+ case "dcc.listenPorts":
+ CIRCDCC.prototype.listenPorts = newValue;
+ break;
+
+ case "dccUserMaxLines":
+ CIRCDCCFileTransfer.prototype.MAX_MESSAGES = newValue;
+ CIRCDCCChat.prototype.MAX_MESSAGES = newValue;
+ break;
+
+ case "font.family":
+ case "font.size":
+ client.dispatch("sync-font");
+ break;
+
+ case "proxy.typeOverride":
+ CIRCNetwork.prototype.PROXY_TYPE_OVERRIDE = newValue;
+ break;
+
+ case "reconnect":
+ CIRCNetwork.prototype.stayingPower = newValue;
+ break;
+
+ case "showModeSymbols":
+ if (newValue)
+ setListMode("symbol");
+ else
+ setListMode("graphic");
+ break;
+
+ case "sasl.plain.enabled":
+ CIRCNetwork.prototype.USE_SASL = newValue;
+ break;
+
+ case "upgrade-insecure":
+ CIRCNetwork.prototype.UPGRADE_INSECURE = newValue;
+ break;
+
+ case "sts.enabled":
+ CIRCNetwork.prototype.STS_MODULE.ENABLED = newValue;
+ break;
+
+ case "nickname":
+ CIRCNetwork.prototype.INITIAL_NICK = newValue;
+ break;
+
+ case "username":
+ CIRCNetwork.prototype.INITIAL_NAME = newValue;
+ break;
+
+ case "usermode":
+ CIRCNetwork.prototype.INITIAL_UMODE = newValue;
+ break;
+
+ case "userMaxLines":
+ CIRCUser.prototype.MAX_MESSAGES = newValue;
+ break;
+
+ case "userlistLeft":
+ updateUserlistSide(newValue);
+ break;
+
+ case "debugMode":
+ setDebugMode(newValue);
+ break;
+
+ case "desc":
+ CIRCNetwork.prototype.INITIAL_DESC = newValue;
+ break;
+
+ case "stalkWholeWords":
+ case "stalkWords":
+ updateAllStalkExpressions();
+ break;
+
+ case "sortUsersByMode":
+ if (client.currentObject.TYPE == "IRCChannel")
+ updateUserList();
+
+ case "motif.current":
+ client.dispatch("sync-motif");
+ break;
+
+ case "multiline":
+ multilineInputMode(newValue);
+ delete client.multiLineForPaste;
+ break;
+
+ case "munger.colorCodes":
+ client.enableColors = newValue;
+ break;
+
+ case "networkMaxLines":
+ CIRCNetwork.prototype.MAX_MESSAGES = newValue;
+ break;
+
+ case "outputWindowURL":
+ client.dispatch("sync-window");
+ break;
+
+ case "displayHeader":
+ client.dispatch("sync-header");
+ break;
+
+ case "tabLabel":
+ onTabLabelUpdate(client, newValue);
+ break;
+
+ case "timestamps":
+ case "timestamps.display":
+ case "collapseActions":
+ case "collapseMsgs":
+ client.dispatch("sync-timestamp");
+ break;
+
+ case "log":
+ client.dispatch("sync-log");
+ break;
+
+ case "alert.globalEnabled":
+ updateAlertIcon(false);
+ break;
+
+ case "alert.floodDensity":
+ if (client.alert && client.alert.floodProtector)
+ client.alert.floodProtector.floodDensity = newValue;
+ break;
+
+ case "alert.floodDispersion":
+ if (client.alert && client.alert.floodProtector)
+ client.alert.floodProtector.floodDispersion = newValue;
+ break;
+
+ case "aliases":
+ updateAliases();
+ break;
+
+ case "inputSpellcheck":
+ updateSpellcheck(newValue);
+ break;
+
+ case "urls.store.max":
+ if (client.urlLogger)
+ {
+ client.urlLogger.autoLimit = newValue;
+ client.urlLogger.limit(newValue);
+ }
+ break;
+
+ default:
+ // Make munger prefs apply without a restart
+ var m, rule;
+ if ((m = prefName.match(/^munger\.(\S+)$/)) &&
+ (rule = client.munger.getRule(m[1])))
+ {
+ rule.enabled = newValue;
+ }
+ }
+}
+
+function onNetworkPrefChanged(network, prefName, newValue, oldValue)
+{
+ if (network != client.networks[network.collectionKey])
+ {
+ /* this is a stale observer, remove it */
+ network.prefManager.destroy();
+ return;
+ }
+
+ if (newValue == oldValue)
+ return;
+
+ network.updateHeader();
+
+ switch (prefName)
+ {
+ case "nickname":
+ network.INITIAL_NICK = newValue;
+ break;
+
+ case "username":
+ network.INITIAL_NAME = newValue;
+ break;
+
+ case "usermode":
+ network.INITIAL_UMODE = newValue;
+ if (network.isConnected())
+ {
+ network.primServ.sendData("mode " + network.server.me + " :" +
+ newValue + "\n");
+ }
+ break;
+
+ case "desc":
+ network.INITIAL_DESC = newValue;
+ break;
+
+ case "proxy.typeOverride":
+ network.PROXY_TYPE_OVERRIDE = newValue;
+ break;
+
+ case "reconnect":
+ network.stayingPower = newValue;
+ break;
+
+ case "font.family":
+ case "font.size":
+ network.dispatch("sync-font");
+ break;
+
+ case "motif.current":
+ network.dispatch("sync-motif");
+ break;
+
+ case "notifyList":
+ if (!network.primServ.supports["monitor"])
+ break;
+ var adds = newValue.filter((el) =>
+ { return oldValue.indexOf(el) < 0; });
+ var subs = oldValue.filter((el) =>
+ { return newValue.indexOf(el) < 0; });
+ if (adds.length > 0)
+ network.primServ.sendMonitorList(adds, true);
+ if (subs.length > 0)
+ network.primServ.sendMonitorList(subs, false);
+ break;
+
+ case "outputWindowURL":
+ network.dispatch("sync-window");
+ break;
+
+ case "displayHeader":
+ network.dispatch("sync-header");
+ break;
+
+ case "tabLabel":
+ onTabLabelUpdate(network, newValue);
+ break;
+
+ case "timestamps":
+ case "timestamps.display":
+ case "collapseActions":
+ case "collapseMsgs":
+ network.dispatch("sync-timestamp");
+ break;
+
+ case "log":
+ network.dispatch("sync-log");
+ break;
+
+ case "connectTries":
+ network.MAX_CONNECT_ATTEMPTS = newValue;
+ break;
+
+ case "sasl.plain.enabled":
+ network.USE_SASL = newValue;
+ break;
+
+ case "upgrade-insecure":
+ network.UPGRADE_INSECURE = newValue;
+ break;
+ }
+}
+
+function onChannelPrefChanged(channel, prefName, newValue, oldValue)
+{
+ var network = channel.parent.parent;
+
+ if (network != client.networks[network.collectionKey] ||
+ channel.parent != network.primServ ||
+ channel != network.primServ.channels[channel.collectionKey])
+ {
+ /* this is a stale observer, remove it */
+ channel.prefManager.destroy();
+ return;
+ }
+
+ if (newValue == oldValue)
+ return;
+
+ channel.updateHeader();
+
+ switch (prefName)
+ {
+ case "conference.enabled":
+ // Wouldn't want to display a message to a hidden view.
+ if ("messages" in channel)
+ {
+ if (newValue)
+ channel.display(MSG_CONF_MODE_ON);
+ else
+ channel.display(MSG_CONF_MODE_OFF);
+ }
+ break;
+
+ case "conference.limit":
+ channel._updateConferenceMode();
+ break;
+
+ case "font.family":
+ case "font.size":
+ channel.dispatch("sync-font");
+ break;
+
+ case "motif.current":
+ channel.dispatch("sync-motif");
+ break;
+
+ case "outputWindowURL":
+ channel.dispatch("sync-window");
+ break;
+
+ case "displayHeader":
+ channel.dispatch("sync-header");
+ break;
+
+ case "tabLabel":
+ onTabLabelUpdate(channel, newValue);
+ break;
+
+ case "timestamps":
+ case "timestamps.display":
+ case "collapseActions":
+ case "collapseMsgs":
+ channel.dispatch("sync-timestamp");
+ break;
+
+ case "log":
+ channel.dispatch("sync-log");
+ break;
+ }
+}
+
+function onUserPrefChanged(user, prefName, newValue, oldValue)
+{
+ var network = user.parent.parent;
+
+ if (network != client.networks[network.collectionKey] ||
+ user.parent != network.primServ ||
+ user != network.primServ.users[user.collectionKey])
+ {
+ /* this is a stale observer, remove it */
+ user.prefManager.destroy();
+ return;
+ }
+
+ if (newValue == oldValue)
+ return;
+
+ user.updateHeader();
+
+ switch (prefName)
+ {
+ case "font.family":
+ case "font.size":
+ user.dispatch("sync-font");
+ break;
+
+ case "motif.current":
+ user.dispatch("sync-motif");
+ break;
+
+ case "outputWindowURL":
+ user.dispatch("sync-window");
+ break;
+
+ case "displayHeader":
+ user.dispatch("sync-header");
+ break;
+
+ case "tabLabel":
+ onTabLabelUpdate(user, newValue);
+ break;
+
+ case "timestamps":
+ case "timestamps.display":
+ case "collapseActions":
+ case "collapseMsgs":
+ user.dispatch("sync-timestamp");
+ break;
+
+ case "log":
+ user.dispatch("sync-log");
+ break;
+ }
+}
+
+function onDCCUserPrefChanged(user, prefName, newValue, oldValue)
+{
+ if (client.dcc.users[user.key] != user)
+ {
+ /* this is a stale observer, remove it */
+ user.prefManager.destroy();
+ return;
+ }
+
+ if (newValue == oldValue)
+ return;
+
+ // DCC Users are a pain, they can have multiple views!
+ function updateDCCView(view)
+ {
+ switch (prefName)
+ {
+ case "font.family":
+ case "font.size":
+ view.dispatch("sync-font");
+ break;
+
+ case "motif.current":
+ view.dispatch("sync-motif");
+ break;
+
+ case "outputWindowURL":
+ view.dispatch("sync-window");
+ break;
+
+ case "displayHeader":
+ view.dispatch("sync-header");
+ break;
+
+ case "tabLabel":
+ onTabLabelUpdate(user, newValue);
+ break;
+
+ case "timestamps":
+ case "timestamps.display":
+ case "collapseActions":
+ case "collapseMsgs":
+ view.dispatch("sync-timestamp");
+ break;
+
+ case "log":
+ view.dispatch("sync-log");
+ break;
+ }
+ };
+
+ for (var i = 0; client.dcc.chats.length; i++)
+ {
+ var chat = client.dcc.chats[i];
+ if (chat.user == user)
+ updateDCCView(chat);
+ }
+}
+
+function initAliases()
+{
+ client.commandManager.aliasList = new Object();
+ updateAliases();
+}
+
+function updateAliases()
+{
+ var aliasDefs = client.prefs["aliases"];
+
+ // Flag all aliases as 'removed' first.
+ for (var name in client.commandManager.aliasList)
+ client.commandManager.aliasList[name] = false;
+
+ for (var i = 0; i < aliasDefs.length; ++i)
+ {
+ var ary = aliasDefs[i].match(/^(.*?)\s*=\s*(.*)$/);
+ if (ary)
+ {
+ var name = ary[1];
+ var list = ary[2];
+
+ // Remove the alias, if it exists, or we'll keep stacking them.
+ if (name in client.commandManager.aliasList)
+ client.commandManager.removeCommand({name: name});
+ client.commandManager.defineCommand(name, list);
+ // Flag this alias as 'used'.
+ client.commandManager.aliasList[name] = true;
+ }
+ else
+ {
+ dd("Malformed alias: " + aliasDefs[i]);
+ }
+ }
+
+ // Purge any aliases that were defined but are no longer in the pref.
+ for (var name in client.commandManager.aliasList)
+ {
+ if (!client.commandManager.aliasList[name])
+ {
+ client.commandManager.removeCommand({name: name});
+ delete client.commandManager.aliasList[name];
+ }
+ }
+}
+
+function onTabLabelUpdate(sourceObject, newValue)
+{
+ var tab = getTabForObject(sourceObject);
+ if (tab)
+ {
+ tab.label = newValue || sourceObject.viewName;
+ tab.setAttribute("tooltiptext", sourceObject.viewName);
+ }
+}
+
diff --git a/comm/suite/chatzilla/xul/content/prefsOverlay.xul b/comm/suite/chatzilla/xul/content/prefsOverlay.xul
new file mode 100644
index 0000000000..946299216f
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/prefsOverlay.xul
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://chatzilla/locale/pref-irc.dtd">
+
+<overlay id="ovCZPrefs"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <treechildren id="prefsPanelChildren">
+ <treeitem id="chatzillaItem"
+ insertbefore="securityItem"
+ label="&pref-irc.window.title;"
+ prefpane="chatzilla_pane"
+ url="chrome://chatzilla/content/pref-irc-toolkit.xul"/>
+ </treechildren>
+
+ <preferences id="appearance_preferences">
+ <preference id="general.startup.chat"
+ name="general.startup.chat"
+ type="bool"/>
+ </preferences>
+ <!-- Startup checkbox -->
+ <groupbox id="generalStartupPreferences">
+ <checkbox id="generalStartupChat" label="&startup.chat.label;"
+ accesskey="&startup.chat.accesskey;" pref="true" preftype="bool"
+ prefstring="general.startup.chat" prefattribute="checked"
+ wsm_persist="true" preference="general.startup.chat"/>
+ </groupbox>
+</overlay>
diff --git a/comm/suite/chatzilla/xul/content/scripts.xul b/comm/suite/chatzilla/xul/content/scripts.xul
new file mode 100644
index 0000000000..6207970a07
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/scripts.xul
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+
+<!--
+ -
+ - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<overlay id="chatzilla-scripts-overlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <overlaytarget id="scripts-overlay-target">
+
+ <script src="chrome://global/content/globalOverlay.js"/>
+
+ <script><![CDATA[
+ /* utils.js overloads the standard JS messages with prompt service
+ * calls, which require the locale support to have loaded. This next
+ * line is needed so that the onLoad function in handlers.js can
+ * display the "error loading ChatZilla" message even if the locale
+ * support is what failed to load.
+ */
+ window.baseAlert = window.alert;
+ ]]></script>
+ <script src="chrome://chatzilla/content/lib/js/utils.js"/>
+ <script src="chrome://chatzilla/content/lib/js/connection-xpcom.js"/>
+ <script src="chrome://chatzilla/content/lib/js/events.js"/>
+ <script src="chrome://chatzilla/content/lib/js/command-manager.js"/>
+ <script src="chrome://chatzilla/content/lib/js/pref-manager.js"/>
+ <script src="chrome://chatzilla/content/lib/js/message-manager.js"/>
+ <script src="chrome://chatzilla/content/lib/js/menu-manager.js"/>
+ <script src="chrome://chatzilla/content/lib/js/irc.js"/>
+ <script src="chrome://chatzilla/content/lib/js/irc-debug.js"/>
+ <script src="chrome://chatzilla/content/lib/js/file-utils.js"/>
+ <script src="chrome://chatzilla/content/lib/js/dcc.js"/>
+ <script src="chrome://chatzilla/content/lib/js/ident.js"/>
+ <script src="chrome://chatzilla/content/lib/js/json-serializer.js"/>
+ <script src="chrome://chatzilla/content/lib/js/sts.js"/>
+ <script src="chrome://chatzilla/content/lib/js/text-serializer.js"/>
+ <script src="chrome://chatzilla/content/lib/js/text-logger.js"/>
+ <script src="chrome://chatzilla/content/lib/xul/munger.js"/>
+ <script src="chrome://chatzilla/content/lib/xul/tree-utils.js"/>
+
+ <script src="chrome://chatzilla/content/static.js"/>
+ <script src="chrome://chatzilla/content/commands.js"/>
+ <script src="chrome://chatzilla/content/menus.js"/>
+ <script src="chrome://chatzilla/content/prefs.js"/>
+ <script src="chrome://chatzilla/content/messages.js"/>
+ <script src="chrome://chatzilla/content/mungers.js"/>
+ <script src="chrome://chatzilla/content/handlers.js"/>
+ <script src="chrome://chatzilla/content/networks.js"/>
+
+ </overlaytarget>
+
+</overlay>
diff --git a/comm/suite/chatzilla/xul/content/static.js b/comm/suite/chatzilla/xul/content/static.js
new file mode 100644
index 0000000000..8ee7753210
--- /dev/null
+++ b/comm/suite/chatzilla/xul/content/static.js
@@ -0,0 +1,5639 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+ChromeUtils.defineModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+#expand const __cz_version = "__CHATZILLA_VERSION__";
+const __cz_condition = "green";
+
+var warn;
+var ASSERT;
+var TEST;
+
+if (DEBUG)
+{
+ _dd_pfx = "cz: ";
+ warn = function (msg) { dumpln ("** WARNING " + msg + " **"); }
+ TEST = ASSERT = function _assert(expr, msg) {
+ if (!expr) {
+ dd("** ASSERTION FAILED: " + msg + " **\n" +
+ getStackTrace() + "\n");
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
+else
+ dd = warn = TEST = ASSERT = function (){};
+
+var client = new Object();
+
+client.TYPE = "IRCClient";
+client.COMMAND_CHAR = "/";
+client.STEP_TIMEOUT = 500;
+client.MAX_MESSAGES = 200;
+client.MAX_HISTORY = 50;
+/* longest nick to show in display before forcing the message to a block level
+ * element */
+client.MAX_NICK_DISPLAY = 14;
+/* longest word to show in display before abbreviating */
+client.MAX_WORD_DISPLAY = 20;
+
+client.NOTIFY_TIMEOUT = 5 * 60 * 1000; /* update notify list every 5 minutes */
+
+// Check every minute which networks have away statuses that need an update.
+client.AWAY_TIMEOUT = 60 * 1000;
+
+client.SLOPPY_NETWORKS = true; /* true if msgs from a network can be displayed
+ * on the current object if it is related to
+ * the network (ie, /whois results will appear
+ * on the channel you're viewing, if that channel
+ * is on the network that the results came from)
+ */
+client.DOUBLETAB_TIME = 500;
+client.HIDE_CODES = true; /* true if you'd prefer to show numeric response
+ * codes as some default value (ie, "===") */
+client.DEFAULT_RESPONSE_CODE = "===";
+
+/* Maximum number of channels we'll try to list without complaining */
+client.SAFE_LIST_COUNT = 500;
+
+/* Minimum number of users above or below the conference limit the user count
+ * must go, before it is changed. This allows the user count to fluctuate
+ * around the limit without continously going on and off.
+ */
+client.CONFERENCE_LOW_PASS = 10;
+
+client.viewsArray = new Array();
+client.activityList = new Object();
+client.hostCompat = new Object();
+client.inputHistory = new Array();
+client.lastHistoryReferenced = -1;
+client.incompleteLine = "";
+client.lastTabUp = new Date();
+client.awayMsgs = new Array();
+client.awayMsgCount = 5;
+client.statusMessages = new Array();
+
+CIRCNetwork.prototype.INITIAL_CHANNEL = "";
+CIRCNetwork.prototype.STS_MODULE = new CIRCSTS();
+CIRCNetwork.prototype.MAX_MESSAGES = 100;
+CIRCNetwork.prototype.IGNORE_MOTD = false;
+CIRCNetwork.prototype.RECLAIM_WAIT = 15000;
+CIRCNetwork.prototype.RECLAIM_TIMEOUT = 400000;
+CIRCNetwork.prototype.MIN_RECONNECT_MS = 15 * 1000; // 15s
+CIRCNetwork.prototype.MAX_RECONNECT_MS = 2 * 60 * 60 * 1000; // 2h
+
+CIRCServer.prototype.READ_TIMEOUT = 0;
+CIRCServer.prototype.PRUNE_OLD_USERS = 0; // prune on user quit.
+
+CIRCUser.prototype.MAX_MESSAGES = 200;
+
+CIRCChannel.prototype.MAX_MESSAGES = 300;
+
+function init()
+{
+ if (("initialized" in client) && client.initialized)
+ return;
+
+ client.initialized = false;
+
+ client.networks = new Object();
+ client.entities = new Object();
+ client.eventPump = new CEventPump (200);
+
+ if (DEBUG)
+ {
+ /* hook all events EXCEPT server.poll and *.event-end types
+ * (the 4th param inverts the match) */
+ client.debugHook =
+ client.eventPump.addHook([{type: "poll", set:/^(server|dcc-chat)$/},
+ {type: "event-end"}], event_tracer,
+ "event-tracer", true /* negate */,
+ false /* disable */);
+ }
+
+ initApplicationCompatibility();
+ initMessages();
+
+ initCommands();
+ initPrefs();
+ initMunger();
+ initNetworks();
+ initMenus();
+ initStatic();
+ initHandlers();
+
+ // Create DCC handler.
+ client.dcc = new CIRCDCC(client);
+
+ client.ident = new IdentServer(client);
+
+ // Initialize the STS module.
+ var stsFile = new nsLocalFile(client.prefs["profilePath"]);
+ stsFile.append("sts.json");
+
+ client.sts = CIRCNetwork.prototype.STS_MODULE;
+ client.sts.init(stsFile);
+ client.sts.ENABLED = client.prefs["sts.enabled"];
+
+ // Start log rotation checking first. This will schedule the next check.
+ checkLogFiles();
+ // Start logging. Nothing should call display() before this point.
+ if (client.prefs["log"])
+ client.openLogFile(client);
+
+ // Make sure the userlist is on the correct side.
+ updateUserlistSide(client.prefs["userlistLeft"]);
+
+ client.display(MSG_WELCOME, "HELLO");
+ client.dispatch("set-current-view", { view: client });
+
+ /*
+ * Due to Firefox 44 changes regarding ES6 lexical scope, these 'const'
+ * items are no longer accessible from the global object ('window') but
+ * are required by the output window. The compromise is to copy them on
+ * to the global object so they can be used.
+ */
+ window.__cz_version = __cz_version;
+ window.__cz_condition = __cz_condition;
+ window.NET_CONNECTING = NET_CONNECTING;
+
+ importFromFrame("updateHeader");
+ importFromFrame("setHeaderState");
+ importFromFrame("changeCSS");
+ importFromFrame("scrollToElement");
+ importFromFrame("updateMotifSettings");
+ importFromFrame("addUsers");
+ importFromFrame("updateUsers");
+ importFromFrame("removeUsers");
+
+ processStartupScripts();
+
+ client.commandManager.installKeys(document);
+ createMenus();
+
+ client.busy = false;
+ updateProgress();
+ initOfflineIcon();
+ updateAlertIcon(false);
+ client.isIdleAway = false;
+ initIdleAutoAway(client.prefs["awayIdleTime"]);
+
+ client.initialized = true;
+
+ dispatch("help", { hello: true });
+ dispatch("networks");
+
+ setTimeout(function() {
+ dispatch("focus-input");
+ }, 0);
+ setTimeout(processStartupAutoperform, 0);
+ setTimeout(processStartupURLs, 0);
+}
+
+function initStatic()
+{
+ client.mainWindow = window;
+
+ try
+ {
+ const nsISound = Components.interfaces.nsISound;
+ client.sound =
+ Components.classes["@mozilla.org/sound;1"].createInstance(nsISound);
+
+ client.soundList = new Object();
+ }
+ catch (ex)
+ {
+ dd("Sound failed to initialize: " + ex);
+ }
+
+ try
+ {
+ const nsIAlertsService = Components.interfaces.nsIAlertsService;
+ client.alert = new Object();
+ client.alert.service =
+ Components.classes["@mozilla.org/alerts-service;1"].getService(nsIAlertsService);
+ client.alert.alertList = new Object();
+ client.alert.floodProtector = new FloodProtector(
+ client.prefs['alert.floodDensity'],
+ client.prefs['alert.floodDispersion']);
+ }
+ catch (ex)
+ {
+ dd("Alert service failed to initialize: " + ex);
+ client.alert = null;
+ }
+
+ try
+ {
+ // Mmmm, fun. This ONLY affects the ChatZilla window, don't worry!
+ Date.prototype.toStringInt = Date.prototype.toString;
+ Date.prototype.toString = function() {
+ let dtf = new Services.intl.DateTimeFormat(undefined,
+ { dateStyle: "full",
+ timeStyle: "long" });
+ return dtf.format(this);
+ }
+ }
+ catch (ex)
+ {
+ dd("Locale-correct date formatting failed to initialize: " + ex);
+ }
+
+ // XXX Bug 335998: See cmdHideView for usage of this.
+ client.hiddenDocument = document.implementation.createDocument(null, null, null);
+
+ multilineInputMode(client.prefs["multiline"]);
+ updateSpellcheck(client.prefs["inputSpellcheck"]);
+
+ // Initialize userlist stuff
+ if (client.prefs["showModeSymbols"])
+ setListMode("symbol");
+ else
+ setListMode("graphic");
+
+ var tree = document.getElementById('user-list');
+ tree.setAttribute("ondragstart", "userlistDNDObserver.onDragStart(event);");
+
+ setDebugMode(client.prefs["debugMode"]);
+
+ var version = getVersionInfo();
+ client.userAgent = getMsg(MSG_VERSION_REPLY, [version.cz, version.ua]);
+ CIRCServer.prototype.VERSION_RPLY = client.userAgent;
+ CIRCServer.prototype.HOST_RPLY = version.host;
+ CIRCServer.prototype.SOURCE_RPLY = MSG_SOURCE_REPLY;
+
+ client.statusBar = new Object();
+
+ client.statusBar["server-nick"] = document.getElementById("server-nick");
+
+ client.tabs = document.getElementById("views-tbar-inner");
+ client.tabDragBar = document.getElementById("tabs-drop-indicator-bar");
+ client.tabDragMarker = document.getElementById("tabs-drop-indicator");
+
+ client.statusElement = document.getElementById("status-text");
+ client.currentStatus = "";
+ client.defaultStatus = MSG_DEFAULT_STATUS;
+
+ client.progressPanel = document.getElementById("status-progress-panel");
+ client.progressBar = document.getElementById("status-progress-bar");
+
+ client.logFile = null;
+ setInterval(onNotifyTimeout, client.NOTIFY_TIMEOUT);
+ // Call every minute, will check only the networks necessary.
+ setInterval(onWhoTimeout, client.AWAY_TIMEOUT);
+
+ client.awayMsgs = [{ message: MSG_AWAY_DEFAULT }];
+ var awayFile = new nsLocalFile(client.prefs["profilePath"]);
+ awayFile.append("awayMsgs.txt");
+ if (awayFile.exists())
+ {
+ var awayLoader = new TextSerializer(awayFile);
+ if (awayLoader.open("<"))
+ {
+ // Load the first item from the file.
+ var item = awayLoader.deserialize();
+ if (isinstance(item, Array))
+ {
+ // If the first item is an array, it is the entire thing.
+ client.awayMsgs = item;
+ }
+ else if (item != null)
+ {
+ /* Not an array, so we have the old format of a single object
+ * per entry.
+ */
+ client.awayMsgs = [item];
+ while ((item = awayLoader.deserialize()))
+ client.awayMsgs.push(item);
+ }
+ awayLoader.close();
+
+ /* we have to close the file before we can move it,
+ * hence the second if statement */
+ if (item == null)
+ {
+ var invalidFile = new nsLocalFile(client.prefs["profilePath"]);
+ invalidFile.append("awayMsgs.invalid");
+ invalidFile.createUnique(FTYPE_FILE, 0o600);
+ var msg = getMsg(MSG_ERR_INVALID_FILE,
+ [awayFile.leafName, invalidFile.leafName]);
+ setTimeout(function() {
+ client.display(msg, MT_WARN);
+ }, 0);
+ awayFile.moveTo(null, invalidFile.leafName);
+ }
+ }
+ }
+
+ // Get back input history from previous session:
+ var inputHistoryFile = new nsLocalFile(client.prefs["profilePath"]);
+ inputHistoryFile.append("inputHistory.txt");
+ try
+ {
+ client.inputHistoryLogger = new TextLogger(inputHistoryFile.path,
+ client.MAX_HISTORY);
+ }
+ catch (ex)
+ {
+ msg = getMsg(MSG_ERR_INPUTHISTORY_NOT_WRITABLE, inputHistoryFile.path);
+ setTimeout(function() {
+ client.display(msg, MT_ERROR);
+ }, 0);
+ dd(formatException(ex));
+ client.inputHistoryLogger = null;
+ }
+ if (client.inputHistoryLogger)
+ client.inputHistory = client.inputHistoryLogger.read().reverse();
+
+ // Set up URL collector.
+ var urlsFile = new nsLocalFile(client.prefs["profilePath"]);
+ urlsFile.append("urls.txt");
+ try
+ {
+ client.urlLogger = new TextLogger(urlsFile.path,
+ client.prefs["urls.store.max"]);
+ }
+ catch (ex)
+ {
+ msg = getMsg(MSG_ERR_URLS_NOT_WRITABLE, urlsFile.path);
+ setTimeout(function() {
+ client.display(msg, MT_ERROR);
+ }, 0);
+ dd(formatException(ex));
+ client.urlLogger = null;
+ }
+
+ // Migrate old list preference to file.
+ try
+ {
+ // Throws if the preference doesn't exist.
+ if (client.urlLogger)
+ var urls = client.prefManager.prefBranch.getCharPref("urls.list");
+ }
+ catch (ex)
+ {
+ }
+ if (urls)
+ {
+ // Add the old URLs to the new file.
+ urls = client.prefManager.stringToArray(urls);
+ for (var i = 0; i < urls.length; i++)
+ client.urlLogger.append(urls[i]);
+ // Completely purge the old preference.
+ client.prefManager.prefBranch.clearUserPref("urls.list");
+ }
+
+ client.defaultCompletion = client.COMMAND_CHAR + "help ";
+
+ client.deck = document.getElementById('output-deck');
+}
+
+function getVersionInfo()
+{
+ var version = new Object();
+ version.cz = __cz_version;
+
+ var app = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
+ version.hostName = app.vendor + " " + app.name;
+ version.hostVersion = app.version;
+ version.host = version.hostName + " " + version.hostVersion + ", " +
+ client.platform;
+ version.hostBuildID = app.platformBuildID;
+ version.ua = app.name + " " + app.version + "/" + version.hostBuildID;
+
+ return version;
+}
+
+function initApplicationCompatibility()
+{
+ // This function does nothing more than tweak the UI based on the platform.
+
+ client.lineEnd = "\n";
+
+ // Set up simple platform information.
+ switch (AppConstants.platform) {
+ case "linux":
+ client.platform = "Linux";
+ break;
+ case "macosx":
+ client.platform = "Mac";
+ break;
+ case "win":
+ client.platform = "Windows";
+ // Windows likes \r\n line endings, as notepad can't cope with just
+ // \n logs.
+ client.lineEnd = "\r\n";
+ break;
+ default:
+ client.platform = "Unknown";
+ }
+
+ CIRCServer.prototype.OS_RPLY = navigator.oscpu + " (" +
+ navigator.platform + ")";
+}
+
+function getFindData(e)
+{
+ // findNext() wrapper to add our findStart/findEnd events.
+ function _cz_findNext() {
+ // Send start notification.
+ var ev = new CEvent("find", "findStart", e.sourceObject, "onFindStart");
+ client.eventPump.routeEvent(ev);
+
+ // Call the original findNext() and keep the result for later.
+ var rv = this.__proto__.findNext();
+
+ // Send end notification with result code.
+ var ev = new CEvent("find", "findEnd", e.sourceObject, "onFindEnd");
+ ev.findResult = rv;
+ client.eventPump.routeEvent(ev);
+
+ // Return the original findNext()'s result to keep up appearances.
+ return rv;
+ };
+
+ // Getter for webBrowserFind property.
+ function _cz_webBrowserFind() {
+ return this._cz_wbf;
+ };
+
+ var findData = new nsFindInstData();
+ findData.browser = e.sourceObject.frame;
+ findData.rootSearchWindow = getContentWindow(e.sourceObject.frame);
+ findData.currentSearchWindow = getContentWindow(e.sourceObject.frame);
+
+ /* Wrap up the webBrowserFind object so we get called for findNext(). Use
+ * __proto__ so that everything else is exactly like the original object.
+ */
+ findData._cz_wbf = { findNext: _cz_findNext };
+ findData._cz_wbf.__proto__ = findData.webBrowserFind;
+
+ /* Replace the nsFindInstData getter for webBrowserFind to call our
+ * function which in turn returns our object (_cz_wbf).
+ */
+ findData.__defineGetter__("webBrowserFind", _cz_webBrowserFind);
+
+ /* Yay, evil hacks! findData.init doesn't care about the findService, it
+ * gets option settings from webBrowserFind. As we want the wrap option *on*
+ * when we use /find foo, we set it on the findService there. However,
+ * restoring the original value afterwards doesn't help, because init() here
+ * overrides that value. Unless we make .init do something else, of course:
+ */
+ findData._init = findData.init;
+ findData.init =
+ function init()
+ {
+ this._init();
+ const FINDSVC_ID = "@mozilla.org/find/find_service;1";
+ var findService = getService(FINDSVC_ID, "nsIFindService");
+ this.webBrowserFind.wrapFind = findService.wrapFind;
+ };
+
+ return findData;
+}
+
+function importFromFrame(method)
+{
+ client.__defineGetter__(method, import_wrapper);
+ CIRCNetwork.prototype.__defineGetter__(method, import_wrapper);
+ CIRCChannel.prototype.__defineGetter__(method, import_wrapper);
+ CIRCUser.prototype.__defineGetter__(method, import_wrapper);
+ CIRCDCCChat.prototype.__defineGetter__(method, import_wrapper);
+ CIRCDCCFileTransfer.prototype.__defineGetter__(method, import_wrapper);
+
+ function import_wrapper()
+ {
+ var dummy = function(){};
+
+ if (!("frame" in this))
+ return dummy;
+
+ try
+ {
+ var window = getContentWindow(this.frame);
+ if (window && "initialized" in window && window.initialized &&
+ method in window)
+ {
+ return function import_wrapper_apply()
+ {
+ window[method].apply(this, arguments);
+ };
+ }
+ }
+ catch (ex)
+ {
+ ASSERT(0, "Caught exception calling: " + method + "\n" + ex);
+ }
+
+ return dummy;
+ };
+}
+
+function processStartupScripts()
+{
+ client.plugins = new Object();
+ var scripts = client.prefs["initialScripts"];
+ var basePath = getURLSpecFromFile(client.prefs["profilePath"]);
+ var baseURL = Services.io.newURI(basePath);
+ for (var i = 0; i < scripts.length; ++i)
+ {
+ try
+ {
+ var url = Services.io.newURI(scripts[i], null, baseURL);
+ var path = getFileFromURLSpec(url.spec);
+ }
+ catch(ex)
+ {
+ var params = ["initialScripts", scripts[i]];
+ display(getMsg(MSG_ERR_INVALID_PREF, params), MT_ERROR);
+ dd(formatException(ex));
+ continue;
+ }
+
+ if (url.scheme != "file" && url.scheme != "chrome")
+ {
+ display(getMsg(MSG_ERR_INVALID_SCHEME, scripts[i]), MT_ERROR);
+ continue;
+ }
+
+ if (!path.exists())
+ {
+ display(getMsg(MSG_ERR_ITEM_NOT_FOUND, url.spec), MT_WARN);
+ continue;
+ }
+
+ if (path.isDirectory())
+ loadPluginDirectory(path);
+ else
+ loadLocalFile(path);
+ }
+}
+
+function loadPluginDirectory(localPath, recurse)
+{
+ if (typeof recurse == "undefined")
+ recurse = 1;
+
+ var initPath = localPath.clone();
+ initPath.append("init.js");
+ if (initPath.exists())
+ loadLocalFile(initPath);
+
+ if (recurse < 1)
+ return;
+
+ var enumer = localPath.directoryEntries;
+ while (enumer.hasMoreElements())
+ {
+ var entry = enumer.getNext();
+ entry = entry.QueryInterface(Components.interfaces.nsIFile);
+ if (entry.isDirectory())
+ loadPluginDirectory(entry, recurse - 1);
+ }
+}
+
+function loadLocalFile(localFile)
+{
+ var url = getURLSpecFromFile(localFile);
+ var glob = new Object();
+ dispatch("load", {url: url, scope: glob});
+}
+
+function getPluginById(id)
+{
+ return client.plugins[id] || null;
+}
+
+function getPluginByURL(url)
+{
+ for (var k in client.plugins)
+ {
+ if (client.plugins[k].url == url)
+ return client.plugins[k];
+
+ }
+
+ return null;
+}
+
+function disablePlugin(plugin, destroy)
+{
+ if (!plugin.enabled)
+ {
+ display(getMsg(MSG_IS_DISABLED, plugin.id));
+ return true;
+ }
+
+ if (plugin.API > 0)
+ {
+ if (!plugin.disable())
+ {
+ display(getMsg(MSG_CANT_DISABLE, plugin.id));
+ return false;
+ }
+
+ if (destroy)
+ {
+ client.prefManager.removeObserver(plugin.prefManager);
+ plugin.prefManager.destroy();
+ }
+ else
+ {
+ plugin.prefs["enabled"] = false;
+ }
+ }
+ else if ("disablePlugin" in plugin.scope)
+ {
+ plugin.scope.disablePlugin();
+ }
+ else
+ {
+ display(getMsg(MSG_CANT_DISABLE, plugin.id));
+ return false;
+ }
+
+ display(getMsg(MSG_PLUGIN_DISABLED, plugin.id));
+ if (!destroy)
+ {
+ plugin.enabled = false;
+ }
+ return true;
+}
+
+function processStartupAutoperform()
+{
+ var cmdary = client.prefs["autoperform.client"];
+ for (var i = 0; i < cmdary.length; ++i)
+ {
+ if (cmdary[i][0] == "/")
+ client.dispatch(cmdary[i].substr(1));
+ else
+ client.dispatch(cmdary[i]);
+ }
+}
+
+function processStartupURLs()
+{
+ var wentSomewhere = false;
+
+ if ("arguments" in window &&
+ 0 in window.arguments && typeof window.arguments[0] == "object" &&
+ "url" in window.arguments[0])
+ {
+ var url = window.arguments[0].url;
+ if (url.search(/^ircs?:\/?\/?\/?$/i) == -1)
+ {
+ /* if the url is not irc: irc:/, irc://, or ircs equiv then go to it. */
+ gotoIRCURL(url);
+ wentSomewhere = true;
+ }
+ }
+ /* check to see whether the URL has been passed via the command line
+ instead. */
+ else if ("arguments" in window &&
+ 0 in window.arguments && typeof window.arguments[0] == "string")
+ {
+ var url = window.arguments[0]
+ var urlMatches = url.match(/^ircs?:\/\/\/?(.*)$/)
+ if (urlMatches)
+ {
+ if (urlMatches[1])
+ {
+ /* if the url is not "irc://", "irc:///" or an ircs equiv then
+ go to it. */
+ gotoIRCURL(url);
+ wentSomewhere = true;
+ }
+ }
+ else if (url)
+ {
+ /* URL parameter is not blank, but does not not conform to the
+ irc[s] scheme. */
+ display(getMsg(MSG_ERR_INVALID_SCHEME, url), MT_ERROR);
+ }
+ }
+
+ /* if we had nowhere else to go, connect to any default urls */
+ if (!wentSomewhere)
+ openStartupURLs();
+
+ if (client.viewsArray.length > 1 && !isStartupURL("irc://"))
+ dispatch("delete-view", { view: client });
+
+ /* XXX: If we have the "stop XBL breaking" hidden tab, remove it, to
+ * stop XBL breaking later. Oh, the irony.
+ */
+ if (client.tabs.firstChild.hidden)
+ {
+ client.tabs.removeChild(client.tabs.firstChild);
+ updateTabAttributes();
+ }
+}
+
+function openStartupURLs()
+{
+ var ary = client.prefs["initialURLs"];
+ for (var i = 0; i < ary.length; ++i)
+ {
+ if (ary[i] && ary[i] == "irc:///")
+ {
+ // Clean out "default network" entries, which we don't
+ // support any more; replace with the harmless irc:// URL.
+ ary[i] = "irc://";
+ client.prefs["initialURLs"].update();
+ }
+ if (ary[i] && ary[i] != "irc://")
+ gotoIRCURL(ary[i]);
+ }
+}
+
+function destroy()
+{
+ destroyPrefs();
+}
+
+function addURLToHistory(url) {
+ url = Services.io.newURI(url, "UTF-8");
+ PlacesUtils.history.insert({
+ url,
+ visits: [{
+ date: new Date(),
+ transition: PlacesUtils.history.TRANSITIONS.TYPED,
+ }],
+ });
+}
+
+function addStatusMessage(message)
+{
+ const DELAY_SCALE = 100;
+ const DELAY_MINIMUM = 5000;
+
+ var delay = message.length * DELAY_SCALE;
+ if (delay < DELAY_MINIMUM)
+ delay = DELAY_MINIMUM;
+
+ client.statusMessages.push({ message: message, delay: delay });
+ updateStatusMessages();
+}
+
+function updateStatusMessages()
+{
+ if (client.statusMessages.length == 0)
+ {
+ var status = client.currentStatus || client.defaultStatus;
+ client.statusElement.setAttribute("label", status);
+ client.statusElement.removeAttribute("notice");
+ return;
+ }
+
+ var now = Number(new Date());
+ var currentMsg = client.statusMessages[0];
+ if ("expires" in currentMsg)
+ {
+ if (now >= currentMsg.expires)
+ {
+ client.statusMessages.shift();
+ setTimeout(updateStatusMessages, 0);
+ }
+ else
+ {
+ setTimeout(updateStatusMessages, 1000);
+ }
+ }
+ else
+ {
+ currentMsg.expires = now + currentMsg.delay;
+ client.statusElement.setAttribute("label", currentMsg.message);
+ client.statusElement.setAttribute("notice", "true");
+ setTimeout(updateStatusMessages, currentMsg.delay);
+ }
+}
+
+
+function setStatus(str)
+{
+ client.currentStatus = str;
+ updateStatusMessages();
+ return str;
+}
+
+client.__defineSetter__("status", setStatus);
+
+function getStatus()
+{
+ return client.currentStatus;
+}
+
+client.__defineGetter__("status", getStatus);
+
+function isVisible (id)
+{
+ var e = document.getElementById(id);
+
+ if (!ASSERT(e,"Bogus id ``" + id + "'' passed to isVisible() **"))
+ return false;
+
+ return (e.getAttribute ("collapsed") != "true");
+}
+
+client.getConnectedNetworks =
+function getConnectedNetworks()
+{
+ var rv = [];
+ for (var n in client.networks)
+ {
+ if (client.networks[n].isConnected())
+ rv.push(client.networks[n]);
+ }
+ return rv;
+}
+
+function combineNicks(nickList, max)
+{
+ if (!max)
+ max = 4;
+
+ var combinedList = [];
+
+ for (var i = 0; i < nickList.length; i += max)
+ {
+ count = Math.min(max, nickList.length - i);
+ var nicks = nickList.slice(i, i + count);
+ var str = new String(nicks.join(" "));
+ str.count = count;
+ combinedList.push(str);
+ }
+
+ return combinedList;
+}
+
+function updateAllStalkExpressions()
+{
+ var list = client.prefs["stalkWords"];
+
+ for (var name in client.networks)
+ {
+ if ("stalkExpression" in client.networks[name])
+ updateStalkExpression(client.networks[name], list);
+ }
+}
+
+function updateStalkExpression(network)
+{
+ function escapeChar(ch)
+ {
+ return "\\" + ch;
+ };
+
+ var list = client.prefs["stalkWords"];
+
+ var ary = new Array();
+
+ ary.push(network.primServ.me.unicodeName.replace(/[^\w\d]/g, escapeChar));
+
+ for (var i = 0; i < list.length; ++i)
+ ary.push(list[i].replace(/[^\w\d]/g, escapeChar));
+
+ var re;
+ if (client.prefs["stalkWholeWords"])
+ re = "(^|[\\W\\s])((" + ary.join(")|(") + "))([\\W\\s]|$)";
+ else
+ re = "(" + ary.join(")|(") + ")";
+
+ network.stalkExpression = new RegExp(re, "i");
+}
+
+function getDefaultFontSize()
+{
+ const PREF_CTRID = "@mozilla.org/preferences-service;1";
+ const nsIPrefService = Components.interfaces.nsIPrefService;
+ const nsIPrefBranch = Components.interfaces.nsIPrefBranch;
+
+ var prefSvc = Components.classes[PREF_CTRID].getService(nsIPrefService);
+ var prefBranch = prefSvc.getBranch(null);
+
+ // PX size pref: font.size.variable.x-western
+ var pxSize = 16;
+ try
+ {
+ pxSize = prefBranch.getIntPref("font.size.variable.x-western");
+ }
+ catch(ex) { }
+
+ var dpi = 96;
+ try
+ {
+ // Get the DPI the fun way (make Mozilla do the work).
+ var b = document.createElement("box");
+ b.style.width = "1in";
+ dpi = window.getComputedStyle(b, null).width.match(/^\d+/);
+ }
+ catch(ex)
+ {
+ try
+ {
+ // Get the DPI the fun way (make Mozilla do the work).
+ b = document.createElementNS("box", XHTML_NS);
+ b.style.width = "1in";
+ dpi = window.getComputedStyle(b, null).width.match(/^\d+/);
+ }
+ catch(ex) { }
+ }
+
+ return Math.round((pxSize / dpi) * 72);
+}
+
+function getDefaultContext(cx)
+{
+ if (!cx)
+ cx = new Object();
+ /* Use __proto__ here and in all other get*Context so that the command can
+ * tell the difference between getObjectDetails and actual parameters. See
+ * cmdJoin for more details.
+ */
+ cx.__proto__ = getObjectDetails(client.currentObject);
+ return cx;
+}
+
+function getMessagesContext(cx, element)
+{
+ if (!cx)
+ cx = new Object();
+ cx.__proto__ = getObjectDetails(client.currentObject);
+ if (!element)
+ element = document.popupNode;
+
+ while (element)
+ {
+ switch (element.localName)
+ {
+ case "a":
+ var href = element.getAttribute("href");
+ cx.url = href;
+ break;
+
+ case "tr":
+ // NOTE: msg-user is the canonicalName.
+ cx.canonNick = element.getAttribute("msg-user");
+ if (!cx.canonNick)
+ break;
+
+ // Strip out a potential ME! suffix.
+ var ary = cx.canonNick.match(/([^ ]+)/);
+ cx.canonNick = ary[1];
+
+ if (!cx.network)
+ break;
+
+ if (cx.channel)
+ cx.user = cx.channel.getUser(cx.canonNick);
+ else
+ cx.user = cx.network.getUser(cx.canonNick);
+
+ if (cx.user)
+ cx.nickname = cx.user.unicodeName;
+ else
+ cx.nickname = toUnicode(cx.canonNick, cx.network);
+ break;
+ }
+
+ element = element.parentNode;
+ }
+
+ return cx;
+}
+
+function getTabContext(cx, element)
+{
+ if (!cx)
+ cx = new Object();
+ if (!element)
+ element = document.popupNode;
+
+ while (element)
+ {
+ if (element.localName == "tab")
+ {
+ cx.__proto__ = getObjectDetails(element.view);
+ return cx;
+ }
+ element = element.parentNode;
+ }
+
+ return cx;
+}
+
+function getUserlistContext(cx)
+{
+ if (!cx)
+ cx = new Object();
+ cx.__proto__ = getObjectDetails(client.currentObject);
+ if (!cx.channel)
+ return cx;
+
+ var user, tree = document.getElementById("user-list");
+ cx.userList = new Array();
+ cx.canonNickList = new Array();
+ cx.nicknameList = getSelectedNicknames(tree);
+
+ for (var i = 0; i < cx.nicknameList.length; ++i)
+ {
+ user = cx.channel.getUser(cx.nicknameList[i])
+ cx.userList.push(user);
+ cx.canonNickList.push(user.canonicalName);
+ if (i == 0)
+ {
+ cx.user = user;
+ cx.nickname = user.unicodeName;
+ cx.canonNick = user.canonicalName;
+ }
+ }
+ cx.userCount = cx.userList.length;
+
+ return cx;
+}
+
+function getViewsContext(cx)
+{
+ function addView(view)
+ {
+ // We only need the view to have messages, so we accept hidden views.
+ if (!("messages" in view))
+ return;
+
+ var url = view.getURL();
+ if (url in urls)
+ return;
+
+ var label = view.viewName;
+ if (!getTabForObject(view))
+ label = getMsg(MSG_VIEW_HIDDEN, [label]);
+
+ var types = ["IRCClient", "IRCNetwork", "IRCDCCChat",
+ "IRCDCCFileTransfer"];
+ var typesNetwork = ["IRCNetwork", "IRCChannel", "IRCUser"];
+ var group = String(arrayIndexOf(types, view.TYPE));
+ if (arrayIndexOf(typesNetwork, view.TYPE) != -1)
+ group = "1-" + getObjectDetails(view).network.viewName;
+
+ var sort = group + "-" + view.viewName;
+ if (view.TYPE == "IRCNetwork")
+ sort = group;
+
+ cx.views.push({url: url, label: label, group: group, sort: sort});
+ urls[url] = true
+ };
+
+ function sortViews(a, b)
+ {
+ if (a.sort < b.sort)
+ return -1;
+ if (a.sort > b.sort)
+ return 1;
+ return 0;
+ };
+
+ if (!cx)
+ cx = new Object();
+ cx.__proto__ = getObjectDetails(client.currentObject);
+
+ cx.views = new Array();
+ var urls = new Object();
+
+ /* XXX The code here works its way through all the open views *and* any
+ * possibly visible objects in the object model. This is necessary because
+ * occasionally objects get removed from the object model while still
+ * having a view open. See bug 459318 for one such case. Note that we
+ * won't be able to correctly switch to the "lost" view but showing it is
+ * less confusing than not.
+ */
+
+ for (var i in client.viewsArray)
+ addView(client.viewsArray[i].source);
+
+ addView(client);
+ for (var n in client.networks)
+ {
+ addView(client.networks[n]);
+ for (var s in client.networks[n].servers) {
+ var server = client.networks[n].servers[s];
+ for (var c in server.channels)
+ addView(server.channels[c]);
+ for (var u in server.users)
+ addView(server.users[u]);
+ }
+ }
+
+ for (var u in client.dcc.users)
+ addView(client.dcc.users[u]);
+ for (var i = 0; i < client.dcc.chats.length; i++)
+ addView(client.dcc.chats[i]);
+ for (var i = 0; i < client.dcc.files.length; i++)
+ addView(client.dcc.files[i]);
+
+ cx.views.sort(sortViews);
+
+ return cx;
+}
+
+function getSelectedNicknames(tree)
+{
+ var rv = [];
+ if (!tree || !tree.view || !tree.view.selection)
+ return rv;
+ var rangeCount = tree.view.selection.getRangeCount();
+
+ // Loop through the selection ranges.
+ for (var i = 0; i < rangeCount; ++i)
+ {
+ var start = {}, end = {};
+ tree.view.selection.getRangeAt(i, start, end);
+
+ // If they == -1, we've got no selection, so bail.
+ if ((start.value == -1) && (end.value == -1))
+ continue;
+ /* Workaround: Because we use select(-1) instead of clearSelection()
+ * (see bug 197667) the tree will then give us selection ranges
+ * starting from -1 instead of 0! (See bug 319066.)
+ */
+ if (start.value == -1)
+ start.value = 0;
+
+ // Loop through the contents of the current selection range.
+ for (var k = start.value; k <= end.value; ++k)
+ rv.push(getNicknameForUserlistRow(k));
+ }
+ return rv;
+}
+
+function getFontContext(cx)
+{
+ if (!cx)
+ cx = new Object();
+ cx.__proto__ = getObjectDetails(client.currentObject);
+ cx.fontSizeDefault = getDefaultFontSize();
+ var view = client;
+
+ if ("prefs" in cx.sourceObject)
+ {
+ cx.fontFamily = view.prefs["font.family"];
+ if (cx.fontFamily.match(/^(default|(sans-)?serif|monospace)$/))
+ delete cx.fontFamily;
+
+ cx.fontSize = view.prefs["font.size"];
+ if (cx.fontSize == 0)
+ delete cx.fontSize;
+ }
+
+ return cx;
+}
+
+function msgIsImportant(msg, sourceNick, network)
+{
+ var plainMsg = removeColorCodes(msg);
+
+ var re = network.stalkExpression;
+ if (plainMsg.search(re) != -1 || sourceNick && sourceNick.search(re) == 0)
+ return true;
+
+ return false;
+}
+
+function ensureCachedCanonicalURLs(array)
+{
+ if ("canonicalURLs" in array)
+ return;
+
+ /* Caching this on the array is safe because the PrefManager constructs
+ * a new array if the preference changes, but otherwise keeps the same
+ * one around.
+ */
+ array.canonicalURLs = new Array();
+ for (var i = 0; i < array.length; i++)
+ array.canonicalURLs.push(makeCanonicalIRCURL(array[i]));
+}
+
+function isStartupURL(url)
+{
+ // We canonicalize all URLs before we do the (string) comparison.
+ url = makeCanonicalIRCURL(url);
+ var list = client.prefs["initialURLs"];
+ ensureCachedCanonicalURLs(list);
+ return arrayContains(list.canonicalURLs, url);
+}
+
+function cycleView(amount)
+{
+ var len = client.viewsArray.length;
+ if (len <= 1)
+ return;
+
+ var tb = getTabForObject (client.currentObject);
+ if (!tb)
+ return;
+
+ var vk = Number(tb.getAttribute("viewKey"));
+ var destKey = (vk + amount) % len; /* wrap around */
+ if (destKey < 0)
+ destKey += len;
+
+ dispatch("set-current-view", { view: client.viewsArray[destKey].source });
+}
+
+// Plays the sound for a particular event on a type of object.
+function playEventSounds(type, event, source)
+{
+ if (!client.sound || !client.prefs["sound.enabled"])
+ return;
+
+ // Converts .TYPE values into the event object names.
+ // IRCChannel => channel, IRCUser => user, etc.
+ if (type.match(/^IRC/))
+ type = type.substr(3, type.length).toLowerCase();
+
+ // DCC Chat sessions should act just like user views.
+ if (type == "dccchat")
+ type = "user";
+
+ var ev = type + "." + event;
+
+ if (ev in client.soundList)
+ return;
+
+ var src = source ? source : client;
+
+ if (!(("sound." + ev) in src.prefs))
+ return;
+
+ var s = src.prefs["sound." + ev];
+
+ if (!s)
+ return;
+
+ if (client.prefs["sound.overlapDelay"] > 0)
+ {
+ client.soundList[ev] = true;
+ setTimeout(function() {
+ delete client.soundList[ev];
+ }, client.prefs["sound.overlapDelay"]);
+ }
+
+ if (event == "start")
+ {
+ blockEventSounds(type, "event");
+ blockEventSounds(type, "chat");
+ blockEventSounds(type, "stalk");
+ }
+
+ playSounds(s);
+}
+
+// Blocks a particular type of event sound occuring.
+function blockEventSounds(type, event)
+{
+ if (!client.sound || !client.prefs["sound.enabled"])
+ return;
+
+ // Converts .TYPE values into the event object names.
+ // IRCChannel => channel, IRCUser => user, etc.
+ if (type.match(/^IRC/))
+ type = type.substr(3, type.length).toLowerCase();
+
+ var ev = type + "." + event;
+
+ if (client.prefs["sound.overlapDelay"] > 0)
+ {
+ client.soundList[ev] = true;
+ setTimeout(function() {
+ delete client.soundList[ev];
+ }, client.prefs["sound.overlapDelay"]);
+ }
+}
+
+function playSounds(list)
+{
+ var ary = list.split (" ");
+ if (ary.length == 0)
+ return;
+
+ playSound(ary[0]);
+ for (var i = 1; i < ary.length; ++i)
+ setTimeout(playSound, 250 * i, ary[i]);
+}
+
+function playSound(file)
+{
+ if (!client.sound || !client.prefs["sound.enabled"] || !file)
+ return;
+
+ if (file == "beep")
+ {
+ client.sound.beep();
+ }
+ else
+ {
+ try
+ {
+ client.sound.play(Services.io.newURI(file));
+ }
+ catch (ex)
+ {
+ // ignore exceptions from this pile of code.
+ }
+ }
+}
+
+/* timer-based mainloop */
+function mainStep()
+{
+ try
+ {
+ var count = client.eventPump.stepEvents();
+ if (count > 0)
+ setTimeout(mainStep, client.STEP_TIMEOUT);
+ else
+ setTimeout(mainStep, client.STEP_TIMEOUT / 5);
+ }
+ catch(ex)
+ {
+ dd("Exception in mainStep!");
+ dd(formatException(ex));
+ setTimeout(mainStep, client.STEP_TIMEOUT);
+ }
+}
+
+function openQueryTab(server, nick)
+{
+ var user = server.addUser(nick);
+ addURLToHistory(user.getURL());
+ if (!("messages" in user))
+ {
+ var value = "";
+ var same = true;
+ for (var c in server.channels)
+ {
+ var chan = server.channels[c];
+ if (!(user.collectionKey in chan.users))
+ continue;
+ /* This takes a boolean value for each channel (true - channel has
+ * same value as first), and &&-s them all together. Thus, |same|
+ * will tell us, at the end, if all the channels found have the
+ * same value for charset.
+ */
+ if (value)
+ same = same && (value == chan.prefs["charset"]);
+ else
+ value = chan.prefs["charset"];
+ }
+ /* If we've got a value, and it's the same accross all channels,
+ * we use it as the *default* for the charset pref. If not, it'll
+ * just keep the "defer" default which pulls it off the network.
+ */
+ if (value && same)
+ {
+ user.prefManager.prefRecords["charset"].defaultValue = value;
+ }
+
+ dispatch("create-tab-for-view", { view: user });
+
+ user.doAutoPerform();
+ }
+ return user;
+}
+
+function arraySpeak (ary, single, plural)
+{
+ var rv = "";
+ var and = MSG_AND;
+
+ switch (ary.length)
+ {
+ case 0:
+ break;
+
+ case 1:
+ rv = ary[0];
+ if (single)
+ rv += " " + single;
+ break;
+
+ case 2:
+ rv = ary[0] + " " + and + " " + ary[1];
+ if (plural)
+ rv += " " + plural;
+ break;
+
+ default:
+ for (var i = 0; i < ary.length - 1; ++i)
+ rv += ary[i] + ", ";
+ rv += and + " " + ary[ary.length - 1];
+ if (plural)
+ rv += " " + plural;
+ break;
+ }
+
+ return rv;
+
+}
+
+function getObjectDetails (obj, rv)
+{
+ if (!rv)
+ rv = new Object();
+
+ if (!ASSERT(obj && typeof obj == "object",
+ "INVALID OBJECT passed to getObjectDetails (" + obj + "). **"))
+ {
+ return rv;
+ }
+
+ rv.sourceObject = obj;
+ rv.TYPE = obj.TYPE;
+ rv.parent = ("parent" in obj) ? obj.parent : null;
+ rv.user = null;
+ rv.channel = null;
+ rv.server = null;
+ rv.network = null;
+ if (window && window.content && window.content.getSelection() != "")
+ rv.selectedText = window.content.getSelection();
+
+ switch (obj.TYPE)
+ {
+ case "IRCChannel":
+ rv.viewType = MSG_CHANNEL;
+ rv.channel = obj;
+ rv.channelName = obj.unicodeName;
+ rv.server = rv.channel.parent;
+ rv.network = rv.server.parent;
+ break;
+
+ case "IRCUser":
+ rv.viewType = MSG_USER;
+ rv.user = obj;
+ rv.userName = rv.nickname = obj.unicodeName;
+ rv.server = rv.user.parent;
+ rv.network = rv.server.parent;
+ break;
+
+ case "IRCChanUser":
+ rv.viewType = MSG_USER;
+ rv.user = obj;
+ rv.userName = rv.nickname = obj.unicodeName;
+ rv.channel = rv.user.parent;
+ rv.server = rv.channel.parent;
+ rv.network = rv.server.parent;
+ break;
+
+ case "IRCNetwork":
+ rv.network = obj;
+ rv.viewType = MSG_NETWORK;
+ if ("primServ" in rv.network)
+ rv.server = rv.network.primServ;
+ else
+ rv.server = null;
+ break;
+
+ case "IRCClient":
+ rv.viewType = MSG_TAB;
+ break;
+
+ case "IRCDCCUser":
+ //rv.viewType = MSG_USER;
+ rv.user = obj;
+ rv.userName = obj.unicodeName;
+ break;
+
+ case "IRCDCCChat":
+ //rv.viewType = MSG_USER;
+ rv.chat = obj;
+ rv.user = obj.user;
+ rv.userName = obj.unicodeName;
+ break;
+
+ case "IRCDCCFileTransfer":
+ //rv.viewType = MSG_USER;
+ rv.file = obj;
+ rv.user = obj.user;
+ rv.userName = obj.unicodeName;
+ rv.fileName = obj.filename;
+ break;
+
+ default:
+ /* no setup for unknown object */
+ break;
+ }
+
+ if (rv.network)
+ rv.networkName = rv.network.unicodeName;
+
+ return rv;
+
+}
+
+function findDynamicRule (selector)
+{
+ var rules = frames[0].document.styleSheets[1].cssRules;
+
+ if (isinstance(selector, RegExp))
+ fun = "search";
+ else
+ fun = "indexOf";
+
+ for (var i = 0; i < rules.length; ++i)
+ {
+ var rule = rules.item(i);
+ if (rule.selectorText && rule.selectorText[fun](selector) == 0)
+ return {sheet: frames[0].document.styleSheets[1], rule: rule,
+ index: i};
+ }
+
+ return null;
+}
+
+function addDynamicRule (rule)
+{
+ var rules = frames[0].document.styleSheets[1];
+
+ var pos = rules.cssRules.length;
+ rules.insertRule (rule, pos);
+}
+
+function getCommandEnabled(command)
+{
+ try {
+ var dispatcher = document.commandDispatcher;
+ var controller = dispatcher.getControllerForCommand(command);
+
+ return controller.isCommandEnabled(command);
+ }
+ catch (e)
+ {
+ return false;
+ }
+}
+
+function doCommand(command)
+{
+ try {
+ var dispatcher = document.commandDispatcher;
+ var controller = dispatcher.getControllerForCommand(command);
+ if (controller && controller.isCommandEnabled(command))
+ controller.doCommand(command);
+ }
+ catch (e)
+ {
+ }
+}
+
+function doCommandWithParams(command, params)
+{
+ try {
+ var dispatcher = document.commandDispatcher;
+ var controller = dispatcher.getControllerForCommand(command);
+ controller.QueryInterface(Components.interfaces.nsICommandController);
+
+ if (!controller || !controller.isCommandEnabled(command))
+ return;
+
+ var cmdparams = newObject("@mozilla.org/embedcomp/command-params;1",
+ "nsICommandParams");
+ for (var i in params)
+ cmdparams.setISupportsValue(i, params[i]);
+
+ controller.doCommandWithParams(command, cmdparams);
+ }
+ catch (e)
+ {
+ }
+}
+
+var testURLs = [
+ "irc:",
+ "irc://",
+ "irc://foo",
+ "irc://foo/",
+ "irc://foo/,isserver",
+ "irc://foo/chatzilla",
+ "irc://foo/chatzilla/",
+ "irc://foo:6666",
+ "irc://foo:6666/",
+ "irc://irc.foo.org",
+ "irc://irc.foo.org/",
+ "irc://irc.foo.org/,needpass",
+ "irc://irc.foo.org/?msg=hello%20there",
+ "irc://irc.foo.org/?msg=hello%20there&ignorethis",
+ "irc://irc.foo.org/%23mozilla,needkey?msg=hello%20there&ignorethis",
+ "irc://libera.chat/",
+ "irc://libera.chat/,isserver",
+ "irc://[fe80::5d49:767b:4b68:1b17]",
+ "irc://[fe80::5d49:767b:4b68:1b17]/",
+ "irc://[fe80::5d49:767b:4b68:1b17]:6666",
+ "irc://[fe80::5d49:767b:4b68:1b17]:6666/"
+];
+
+var testFailURLs = [
+ "irc:///",
+ "irc:///help",
+ "irc:///help,needkey",
+ "irc://irc.foo.org/,isnick",
+ "invalids"
+];
+
+function doURLTest()
+{
+ var passed = 0, total = testURLs.length + testFailURLs.length;
+ for (var i = 0; i < testURLs.length; i++)
+ {
+ var o = parseIRCURL(testURLs[i]);
+ if (!o)
+ display("Parse of '" + testURLs[i] + "' failed.", MT_ERROR);
+ else
+ passed++;
+ }
+ for (var i = 0; i < testFailURLs.length; i++)
+ {
+ var o = parseIRCURL(testFailURLs[i]);
+ if (o)
+ display("Parse of '" + testFailURLs[i] + "' unexpectedly succeeded.", MT_ERROR);
+ else
+ passed++;
+ }
+ display("Passed " + passed + " out of " + total + " tests (" +
+ passed / total * 100 + "%).", MT_INFO);
+}
+
+var testIRCURLObjects = [
+ [{}, "irc://"],
+ [{host: "undernet"}, "irc://undernet/"],
+ [{host: "irc.undernet.org"}, "irc://irc.undernet.org/"],
+ [{host: "irc.undernet.org", isserver: true}, "irc://irc.undernet.org/"],
+ [{host: "undernet", isserver: true}, "irc://undernet/,isserver"],
+ [{host: "irc.undernet.org", port: 6667}, "irc://irc.undernet.org/"],
+ [{host: "irc.undernet.org", port: 1}, "irc://irc.undernet.org:1/"],
+ [{host: "irc.undernet.org", port: 1, scheme: "ircs"},
+ "ircs://irc.undernet.org:1/"],
+ [{host: "irc.undernet.org", port: 6697, scheme: "ircs"},
+ "ircs://irc.undernet.org/"],
+ [{host: "undernet", needpass: true}, "irc://undernet/,needpass"],
+ [{host: "undernet", pass: "cz"}, "irc://undernet/?pass=cz"],
+ [{host: "undernet", charset: "utf-8"}, "irc://undernet/?charset=utf-8"],
+ [{host: "undernet", target: "#foo"}, "irc://undernet/%23foo"],
+ [{host: "undernet", target: "#foo", needkey: true},
+ "irc://undernet/%23foo,needkey"],
+ [{host: "undernet", target: "John", isnick: true},
+ "irc://undernet/John,isnick"],
+ [{host: "undernet", target: "#foo", key: "cz"},
+ "irc://undernet/%23foo?key=cz"],
+ [{host: "undernet", charset: "utf-8"}, "irc://undernet/?charset=utf-8"],
+ [{host: "undernet", target: "John", msg: "spam!"},
+ "irc://undernet/John?msg=spam%21"],
+ [{host: "undernet", target: "foo", isnick: true, msg: "spam!", pass: "cz"},
+ "irc://undernet/foo,isnick?msg=spam%21&pass=cz"]
+];
+
+function doObjectURLtest()
+{
+ var passed = 0, total = testIRCURLObjects.length;
+ for (var i = 0; i < total; i++)
+ {
+ var obj = testIRCURLObjects[i][0];
+ var url = testIRCURLObjects[i][1];
+ var parsedURL = constructIRCURL(obj)
+ if (url != parsedURL)
+ {
+ display("Parsed IRC Object incorrectly! Expected '" + url +
+ "', got '" + parsedURL, MT_ERROR);
+ }
+ else
+ {
+ passed++;
+ }
+ }
+ display("Passed " + passed + " out of " + total + " tests (" +
+ passed / total * 100 + "%).", MT_INFO);
+}
+
+
+function gotoIRCURL(url, e)
+{
+ var urlspec = url;
+ if (typeof url == "string")
+ url = parseIRCURL(url);
+
+ if (!url)
+ {
+ window.alert(getMsg(MSG_ERR_BAD_IRCURL, urlspec));
+ return;
+ }
+
+ if (!url.host)
+ {
+ /* focus the *client* view for irc:, irc:/, and irc:// (the only irc
+ * urls that don't have a host. (irc:/// implies a connect to the
+ * default network.)
+ */
+ client.pendingViewContext = e;
+ dispatch("client");
+ delete client.pendingViewContext;
+ return;
+ }
+
+ let isSecure = url.scheme == "ircs";
+ let network;
+ // Make sure host is in lower case.
+ url.host = url.host.toLowerCase();
+
+ // Convert a request for a server to a network if we know it.
+ if (url.isserver)
+ {
+ for (var n in client.networks)
+ {
+ network = client.networks[n];
+ for (var s in network.servers)
+ {
+ let server = network.servers[s];
+ if ((server.hostname == url.host) &&
+ (server.isSecure == isSecure) &&
+ (!url.port || (server.port == url.port)))
+ {
+ url.isserver = false;
+ url.host = network.canonicalName;
+ if (!url.port)
+ url.port = server.port;
+ break;
+ }
+ }
+ if (!url.isserver)
+ break;
+ }
+ }
+
+ let name = url.host;
+ network = client.getNetwork(name);
+
+ if (url.isserver)
+ {
+ let found = false;
+ if (network) {
+ for (let s in network.servers) {
+ let server = network.servers[s];
+ if ((server.isSecure == isSecure) &&
+ (!url.port || (server.port == url.port))) {
+ found = true;
+ if (!url.port)
+ url.port = server.port;
+ break;
+ }
+ }
+ }
+
+ // If still no port set, use the default.
+ if (!url.port)
+ url.port = isSecure ? 6697 : 6667;
+
+ if (!found) {
+ name += ":" + url.port;
+
+ // If there is no temporary network for this server:port, create one.
+ if (!client.getNetwork(name)) {
+ let server = {name: url.host, port: url.port, isSecure: isSecure};
+ client.addNetwork(name, [server], true);
+ }
+ network = client.getNetwork(name);
+ }
+ }
+ else
+ {
+ // There is no network called this, sorry.
+ if (!network)
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_NETWORK, name));
+ return;
+ }
+ }
+
+ // We should only prompt for a password if we're not connected.
+ if (network.state == NET_OFFLINE)
+ {
+ // Check for a network password.
+ url.pass = client.tryToGetLogin(network.getURL(), "serv", "*",
+ url.pass, url.needpass,
+ getMsg(MSG_HOST_PASSWORD,
+ network.getURL()));
+ }
+
+ // Adjust secure setting for temporary networks (so user can override).
+ if (network.temporary)
+ network.serverList[0].isSecure = url.scheme == "ircs";
+
+ // Adjust password for all servers (so user can override).
+ if (url.pass)
+ {
+ for (var s in network.servers)
+ network.servers[s].password = url.pass;
+ }
+
+ // Start the connection and pend anything else if we're not ready.
+ if (network.state != NET_ONLINE)
+ {
+ client.pendingViewContext = e;
+ if (!network.isConnected())
+ {
+ client.connectToNetwork(network, url.scheme == "ircs");
+ }
+ else
+ {
+ dispatch("create-tab-for-view", { view: network });
+ dispatch("set-current-view", { view: network });
+ }
+ delete client.pendingViewContext;
+
+ if (!url.target)
+ return;
+
+ // We're not completely online, so everything else is pending.
+ if (!("pendingURLs" in network))
+ network.pendingURLs = new Array();
+ network.pendingURLs.unshift({ url: url, e: e });
+ return;
+ }
+
+ // We're connected now, process the target.
+ if (url.target)
+ {
+ var targetObject;
+ var ev;
+ if (url.isnick)
+ {
+ /* url points to a person. */
+ var nick = url.target;
+ var ary = url.target.split("!");
+ if (ary)
+ nick = ary[0];
+
+ client.pendingViewContext = e;
+ targetObject = network.dispatch("query", {nickname: nick});
+ delete client.pendingViewContext;
+ }
+ else
+ {
+ /* url points to a channel */
+ var key;
+ var serv = network.primServ;
+ var target = url.target;
+ if (url.charset)
+ {
+ var chan = new CIRCChannel(serv, target, fromUnicode(target, url.charset));
+ chan.prefs["charset"] = url.charset;
+ }
+ else
+ {
+ // Must do this the hard way... we have the server's format
+ // for the channel name here, and all our commands only work
+ // with the Unicode forms.
+
+ /* If we don't have a valid prefix, stick a "#" on it.
+ * NOTE: This is always a "#" so that URLs may be compared
+ * properly without involving the server (e.g. off-line).
+ */
+ if ((arrayIndexOf(["#", "&", "+", "!"], target[0]) == -1) &&
+ (arrayIndexOf(serv.channelTypes, target[0]) == -1))
+ {
+ target = "#" + target;
+ }
+
+ var chan = new CIRCChannel(serv, null, target);
+ }
+
+ if (url.needkey && !chan.joined)
+ {
+ if (url.key)
+ key = url.key;
+ else
+ key = window.promptPassword(getMsg(MSG_URL_KEY, url.spec));
+ }
+ client.pendingViewContext = e;
+ d = {channelToJoin: chan, key: key};
+ targetObject = network.dispatch("join", d);
+ delete client.pendingViewContext;
+
+ if (!targetObject)
+ return;
+ }
+
+ if (url.msg)
+ {
+ client.pendingViewContext = e;
+ var msg;
+ if (url.msg.indexOf("\01ACTION") == 0)
+ {
+ msg = filterOutput(url.msg, "ACTION", targetObject);
+ targetObject.display(msg, "ACTION", "ME!",
+ client.currentObject);
+ }
+ else
+ {
+ msg = filterOutput(url.msg, "PRIVMSG", targetObject);
+ targetObject.display(msg, "PRIVMSG", "ME!",
+ client.currentObject);
+ }
+ targetObject.say(msg);
+ dispatch("set-current-view", { view: targetObject });
+ delete client.pendingViewContext;
+ }
+ }
+ else
+ {
+ client.pendingViewContext = e;
+ dispatch("create-tab-for-view", { view: network });
+ dispatch("set-current-view", { view: network });
+ delete client.pendingViewContext;
+ }
+}
+
+function updateProgress()
+{
+ var busy;
+ var progress = -1;
+
+ if ("busy" in client.currentObject)
+ busy = client.currentObject.busy;
+
+ if ("progress" in client.currentObject)
+ progress = client.currentObject.progress;
+
+ if (!busy)
+ progress = 0;
+
+ client.progressPanel.collapsed = !busy;
+ client.progressBar.mode = (progress < 0 ? "undetermined" : "determined");
+ if (progress >= 0)
+ client.progressBar.value = progress;
+}
+
+function updateSecurityIcon()
+{
+ var o = getObjectDetails(client.currentObject);
+ var securityButton = window.document.getElementById("security-button");
+ securityButton.label = "";
+ securityButton.removeAttribute("level");
+ securityButton.removeAttribute("tooltiptext");
+ if (!o.server || !o.server.isConnected) // No server or connection?
+ {
+ securityButton.setAttribute("tooltiptext", MSG_SECURITY_INFO);
+ return;
+ }
+
+ let tooltiptext = MSG_SECURITY_INFO;
+ switch (o.server.connection.getSecurityState()) {
+ case STATE_IS_SECURE:
+ securityButton.setAttribute("level", "high");
+
+ // Update the tooltip.
+ var issuer = o.server.connection.getCertificate().issuerOrganization;
+ tooltiptext = getMsg(MSG_SECURE_CONNECTION, issuer);
+ break;
+ case STATE_IS_BROKEN:
+ securityButton.setAttribute("level", "broken");
+ break;
+ case STATE_IS_INSECURE:
+ default:
+ securityButton.setAttribute("level", "none");
+ }
+ securityButton.label = o.server.hostname;
+ securityButton.setAttribute("tooltiptext", tooltiptext);
+}
+
+function updateLoggingIcon()
+{
+ var state = client.currentObject.prefs["log"] ? "on" : "off";
+ var icon = window.document.getElementById("logging-status");
+ icon.setAttribute("loggingstate", state);
+ icon.setAttribute("tooltiptext", getMsg("msg.logging.icon." + state));
+}
+
+function updateAlertIcon(aToggle) {
+ let alertState = client.prefs["alert.globalEnabled"];
+ if (aToggle) {
+ alertState = !alertState;
+ client.prefs["alert.globalEnabled"] = alertState;
+ }
+ let state = alertState ? "on" : "off";
+ let icon = window.document.getElementById("alert-status");
+ icon.setAttribute("alertstate", state);
+ icon.setAttribute("tooltiptext", getMsg("msg.alert.icon." + state));
+}
+
+function initOfflineIcon()
+{
+ const PRBool_CID = "@mozilla.org/supports-PRBool;1";
+ const OS_CID = "@mozilla.org/observer-service;1";
+ const nsISupportsPRBool = Components.interfaces.nsISupportsPRBool;
+
+ client.offlineObserver = {
+ _element: document.getElementById("offline-status"),
+ state: function offline_state()
+ {
+ return (Services.io.offline ? "offline" : "online");
+ },
+ observe: function offline_observe(subject, topic, state)
+ {
+ if ((topic == "offline-requested") &&
+ (client.getConnectionCount() > 0))
+ {
+ var buttonAry = [MSG_REALLY_GO_OFFLINE, MSG_DONT_GO_OFFLINE];
+ var rv = confirmEx(MSG_GOING_OFFLINE, buttonAry);
+ if (rv == 1) // Don't go offline, please!
+ {
+ subject.QueryInterface(nsISupportsPRBool);
+ subject.data = true;
+ }
+ }
+ else if (topic == "network:offline-status-changed")
+ {
+ this.updateOfflineUI();
+ }
+ },
+ updateOfflineUI: function offline_uiUpdate()
+ {
+ this._element.setAttribute("offline", Services.io.offline);
+ var tooltipMsgId = "MSG_OFFLINESTATE_" + this.state().toUpperCase();
+ this._element.setAttribute("tooltiptext", window[tooltipMsgId]);
+ },
+ toggleOffline: function offline_toggle()
+ {
+ // Check whether people are OK with us going offline:
+ if (!Services.io.offline && !this.canGoOffline())
+ return;
+
+ // Stop automatic management of the offline status, if existing.
+ Services.io.manageOfflineStatus = false;
+
+ // Actually change the offline state.
+ Services.io.offline = !Services.io.offline;
+ },
+ canGoOffline: function offline_check()
+ {
+ try
+ {
+ var canGoOffline = newObject(PRBool_CID, "nsISupportsPRBool");
+ Services.obs.notifyObservers(canGoOffline, "offline-requested");
+ // Someone called for a halt
+ if (canGoOffline.data)
+ return false;
+ }
+ catch (ex)
+ {
+ dd("Exception when trying to ask if we could go offline:" + ex);
+ }
+ return true;
+ }
+ };
+
+ Services.obs.addObserver(client.offlineObserver, "offline-requested");
+ Services.obs.addObserver(client.offlineObserver,
+ "network:offline-status-changed");
+ client.offlineObserver.updateOfflineUI();
+}
+
+function uninitOfflineIcon()
+{
+ Services.obs.removeObserver(client.offlineObserver, "offline-requested");
+ Services.obs.removeObserver(client.offlineObserver,
+ "network:offline-status-changed");
+}
+
+client.idleObserver = {
+ QueryInterface: function io_qi(iid)
+ {
+ if (!iid || (!iid.equals(Components.interfaces.nsIObserver) &&
+ !iid.equals(Components.interfaces.nsISupports)))
+ {
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+ },
+ observe: function io_observe(subject, topic, data)
+ {
+ if ((topic == "idle") && !client.prefs["away"])
+ {
+ if (!client.prefs["awayIdleMsg"])
+ client.prefs["awayIdleMsg"] = MSG_AWAY_IDLE_DEFAULT;
+ client.dispatch("idle-away", {reason: client.prefs["awayIdleMsg"]});
+ client.isIdleAway = true;
+ }
+ else if ((topic == "back" || topic == "active") && client.isIdleAway)
+ {
+ client.dispatch("idle-back");
+ client.isIdleAway = false;
+ }
+ }
+};
+
+function initIdleAutoAway(timeout)
+{
+ // Don't try to do anything if we are disabled
+ if (!timeout)
+ return;
+
+ var is = getService("@mozilla.org/widget/idleservice;1", "nsIIdleService");
+ if (!is)
+ {
+ display(MSG_ERR_NO_IDLESERVICE, MT_WARN);
+ client.prefs["autoIdleTime"] = 0;
+ return;
+ }
+
+ try
+ {
+ is.addIdleObserver(client.idleObserver, timeout * 60);
+ }
+ catch (ex)
+ {
+ display(formatException(ex), MT_ERROR);
+ }
+}
+
+function uninitIdleAutoAway(timeout)
+{
+ // Don't try to do anything if we were disabled before
+ if (!timeout)
+ return;
+
+ var is = getService("@mozilla.org/widget/idleservice;1", "nsIIdleService");
+ if (!is)
+ return;
+
+ try
+ {
+ is.removeIdleObserver(client.idleObserver, timeout * 60);
+ }
+ catch (ex)
+ {
+ display(formatException(ex), MT_ERROR);
+ }
+}
+
+function updateAppMotif(motifURL)
+{
+ var node = document.firstChild;
+ while (node && ((node.nodeType != node.PROCESSING_INSTRUCTION_NODE) ||
+ !(/name="dyn-motif"/).test(node.data)))
+ {
+ node = node.nextSibling;
+ }
+
+ motifURL = motifURL.replace(/"/g, "%22");
+ var dataStr = "href=\"" + motifURL + "\" name=\"dyn-motif\"";
+ try
+ {
+ // No dynamic style node yet.
+ if (!node)
+ {
+ node = document.createProcessingInstruction("xml-stylesheet", dataStr);
+ document.insertBefore(node, document.firstChild);
+ }
+ else if (node.data != dataStr)
+ {
+ node.data = dataStr;
+ document.insertBefore(node, node.nextSibling);
+ }
+ }
+ catch (ex)
+ {
+ dd(formatException(ex));
+ var err = ex.name;
+ // Mozilla 1.0 doesn't like document.insertBefore(...,
+ // document.firstChild); though it has a prototype for it -
+ // check for the right error:
+ if (err == "NS_ERROR_NOT_IMPLEMENTED")
+ {
+ display(MSG_NO_DYNAMIC_STYLE, MT_INFO);
+ updateAppMotif = function() {};
+ }
+ }
+}
+
+function updateSpellcheck(value)
+{
+ value = value.toString();
+ document.getElementById("input").setAttribute("spellcheck", value);
+ document.getElementById("multiline-input").setAttribute("spellcheck",
+ value);
+}
+
+function updateNetwork()
+{
+ var o = getObjectDetails (client.currentObject);
+
+ var lag = MSG_UNKNOWN;
+ var nick = "";
+ if (o.server)
+ {
+ if (o.server.me)
+ nick = o.server.me.unicodeName;
+ lag = (o.server.lag != -1) ? o.server.lag.toFixed(2) : MSG_UNKNOWN;
+ }
+ client.statusBar["header-url"].setAttribute("value",
+ client.currentObject.getURL());
+ client.statusBar["header-url"].setAttribute("href",
+ client.currentObject.getURL());
+ client.statusBar["header-url"].setAttribute("name",
+ client.currentObject.unicodeName);
+}
+
+function updateTitle (obj)
+{
+ if (!(("currentObject" in client) && client.currentObject) ||
+ (obj && obj != client.currentObject))
+ return;
+
+ var tstring = MSG_TITLE_UNKNOWN;
+ var o = getObjectDetails(client.currentObject);
+ var net = o.network ? o.network.unicodeName : "";
+ var nick = "";
+ client.statusBar["server-nick"].disabled = false;
+
+ switch (client.currentObject.TYPE)
+ {
+ case "IRCNetwork":
+ var serv = "", port = "";
+ if (client.currentObject.isConnected())
+ {
+ serv = o.server.hostname;
+ port = o.server.port;
+ if (o.server.me)
+ nick = o.server.me.unicodeName;
+ tstring = getMsg(MSG_TITLE_NET_ON, [nick, net, serv, port]);
+ }
+ else
+ {
+ nick = client.currentObject.INITIAL_NICK;
+ tstring = getMsg(MSG_TITLE_NET_OFF, [nick, net]);
+ }
+ break;
+
+ case "IRCChannel":
+ var chan = "", mode = "", topic = "";
+ if ("me" in o.parent)
+ {
+ nick = o.parent.me.unicodeName;
+ if (o.parent.me.collectionKey in client.currentObject.users)
+ {
+ let cuser = client.currentObject.users[o.parent.me.collectionKey];
+ if (cuser.isFounder)
+ nick = "~" + nick;
+ else if (cuser.isAdmin)
+ nick = "&" + nick;
+ else if (cuser.isOp)
+ nick = "@" + nick;
+ else if (cuser.isHalfOp)
+ nick = "%" + nick;
+ else if (cuser.isVoice)
+ nick = "+" + nick;
+ }
+ }
+ else
+ {
+ nick = MSG_TITLE_NONICK;
+ }
+ chan = o.channel.unicodeName;
+ mode = o.channel.mode.getModeStr();
+ if (!mode)
+ mode = MSG_TITLE_NO_MODE;
+ topic = o.channel.topic ? o.channel.topic : MSG_TITLE_NO_TOPIC;
+ var re = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g;
+ topic = topic.replace(re, "");
+
+ tstring = getMsg(MSG_TITLE_CHANNEL, [nick, chan, mode, topic]);
+ break;
+
+ case "IRCUser":
+ nick = client.currentObject.unicodeName;
+ var source = "";
+ if (client.currentObject.name)
+ {
+ source = "<" + client.currentObject.name + "@" +
+ client.currentObject.host +">";
+ }
+ tstring = getMsg(MSG_TITLE_USER, [nick, source]);
+ nick = "me" in o.parent ? o.parent.me.unicodeName : MSG_TITLE_NONICK;
+ break;
+
+ case "IRCClient":
+ nick = client.prefs["nickname"];
+ break;
+
+ case "IRCDCCChat":
+ client.statusBar["server-nick"].disabled = true;
+ nick = o.chat.me.unicodeName;
+ tstring = getMsg(MSG_TITLE_DCCCHAT, o.userName);
+ break;
+
+ case "IRCDCCFileTransfer":
+ client.statusBar["server-nick"].disabled = true;
+ nick = o.file.me.unicodeName;
+ var data = [o.file.progress, o.file.filename, o.userName];
+ if (o.file.state.dir == 1)
+ tstring = getMsg(MSG_TITLE_DCCFILE_SEND, data);
+ else
+ tstring = getMsg(MSG_TITLE_DCCFILE_GET, data);
+ break;
+ }
+
+ if (0 && !client.uiState["tabstrip"])
+ {
+ var actl = new Array();
+ for (var i in client.activityList)
+ actl.push ((client.activityList[i] == "!") ?
+ (Number(i) + 1) + "!" : (Number(i) + 1));
+ if (actl.length > 0)
+ tstring = getMsg(MSG_TITLE_ACTIVITY,
+ [tstring, actl.join (", ")]);
+ }
+
+ document.title = tstring;
+ client.statusBar["server-nick"].setAttribute("label", nick);
+}
+
+// Where 'right' is orientation, not wrong/right:
+function updateUserlistSide(shouldBeLeft)
+{
+ var listParent = document.getElementById("tabpanels-contents-box");
+ var isLeft = (listParent.childNodes[0].id == "user-list-box");
+ if (isLeft == shouldBeLeft)
+ return;
+ if (shouldBeLeft) // Move from right to left.
+ {
+ listParent.insertBefore(listParent.childNodes[1], listParent.childNodes[0]);
+ listParent.insertBefore(listParent.childNodes[2], listParent.childNodes[0]);
+ listParent.childNodes[1].setAttribute("collapse", "before");
+ }
+ else // Move from left to right.
+ {
+ listParent.appendChild(listParent.childNodes[1]);
+ listParent.appendChild(listParent.childNodes[0]);
+ listParent.childNodes[1].setAttribute("collapse", "after");
+ }
+ var userlist = document.getElementById("user-list")
+ if (client.currentObject && (client.currentObject.TYPE == "IRCChannel"))
+ userlist.view = client.currentObject.userList;
+}
+
+function multilineInputMode (state)
+{
+ var multiInput = document.getElementById("multiline-input");
+ var multiInputBox = document.getElementById("multiline-box");
+ var singleInput = document.getElementById("input");
+ var singleInputBox = document.getElementById("singleline-box");
+ var splitter = document.getElementById("input-splitter");
+ var iw = document.getElementById("input-widgets");
+ var h;
+
+ client._mlMode = state;
+
+ if (state) /* turn on multiline input mode */
+ {
+
+ h = iw.getAttribute ("lastHeight");
+ if (h)
+ iw.setAttribute ("height", h); /* restore the slider position */
+
+ singleInputBox.setAttribute ("collapsed", "true");
+ splitter.setAttribute ("collapsed", "false");
+ multiInputBox.setAttribute ("collapsed", "false");
+ // multiInput should have the same direction as singleInput
+ multiInput.setAttribute("dir", singleInput.getAttribute("dir"));
+ multiInput.value = (client.input ? client.input.value : "");
+ client.input = multiInput;
+ }
+ else /* turn off multiline input mode */
+ {
+ h = iw.getAttribute ("height");
+ iw.setAttribute ("lastHeight", h); /* save the slider position */
+ iw.removeAttribute ("height"); /* let the slider drop */
+
+ splitter.setAttribute ("collapsed", "true");
+ multiInputBox.setAttribute ("collapsed", "true");
+ singleInputBox.setAttribute ("collapsed", "false");
+ // singleInput should have the same direction as multiInput
+ singleInput.setAttribute("dir", multiInput.getAttribute("dir"));
+ singleInput.value = (client.input ? client.input.value : "");
+ client.input = singleInput;
+ }
+
+ client.input.focus();
+}
+
+function displayCertificateInfo()
+{
+ var o = getObjectDetails(client.currentObject);
+ if (!o.server)
+ return;
+
+ if (!o.server.isSecure)
+ {
+ alert(getMsg(MSG_INSECURE_SERVER, o.server.hostname));
+ return;
+ }
+
+ viewCert(o.server.connection.getCertificate());
+}
+
+function onLoggingIcon() {
+ client.currentObject.dispatch("log", { state: "toggle" });
+}
+
+function newInlineText (data, className, tagName)
+{
+ if (typeof tagName == "undefined")
+ tagName = "html:span";
+
+ var a = document.createElementNS(XHTML_NS, tagName);
+ if (className)
+ a.setAttribute ("class", className);
+
+ switch (typeof data)
+ {
+ case "string":
+ a.appendChild (document.createTextNode (data));
+ break;
+
+ case "object":
+ for (var p in data)
+ if (p != "data")
+ a.setAttribute (p, data[p]);
+ else
+ a.appendChild (document.createTextNode (data[p]));
+ break;
+
+ case "undefined":
+ break;
+
+ default:
+ ASSERT(0, "INVALID TYPE ('" + typeof data + "') passed to " +
+ "newInlineText.");
+ break;
+
+ }
+
+ return a;
+
+}
+
+function stringToMsg (message, obj)
+{
+ var ary = message.split ("\n");
+ var span = document.createElementNS(XHTML_NS, "html:span");
+ var data = getObjectDetails(obj);
+
+ if (ary.length == 1)
+ client.munger.munge(ary[0], span, data);
+ else
+ {
+ for (var l = 0; l < ary.length - 1; ++l)
+ {
+ client.munger.munge(ary[l], span, data);
+ span.appendChild(document.createElementNS(XHTML_NS, "html:br"));
+ }
+ client.munger.munge(ary[l], span, data);
+ }
+
+ return span;
+}
+
+function getFrame()
+{
+ if (client.deck.childNodes.length == 0)
+ return undefined;
+ var panel = client.deck.selectedPanel;
+ return getContentWindow(panel);
+}
+
+client.__defineGetter__ ("currentFrame", getFrame);
+
+function setCurrentObject (obj)
+{
+ if (!ASSERT(obj.messages, "INVALID OBJECT passed to setCurrentObject **"))
+ return;
+
+ if ("currentObject" in client && client.currentObject == obj)
+ {
+ if (typeof client.pendingViewContext == "object")
+ dispatch("create-tab-for-view", { view: obj });
+ return;
+ }
+
+ // Set window.content to make screenreader apps find the chat content.
+ if (obj.frame && getContentWindow(obj.frame))
+ window.content = getContentWindow(obj.frame);
+
+ var tb, userList;
+ userList = document.getElementById("user-list");
+
+ if ("currentObject" in client && client.currentObject)
+ tb = getTabForObject(client.currentObject);
+ if (tb)
+ tb.setAttribute("state", "normal");
+
+ // If we're tracking last read lines, set a mark on the current view
+ // before switching to the new one.
+ if (tb && client.currentObject.prefs["autoMarker"])
+ client.currentObject.dispatch("marker-set");
+
+ client.currentObject = obj;
+
+ // Update userlist:
+ userList.view = null;
+ if (obj.TYPE == "IRCChannel")
+ {
+ userList.view = obj.userList;
+ updateUserList();
+ }
+
+ tb = dispatch("create-tab-for-view", { view: obj });
+ if (tb)
+ {
+ tb.parentNode.selectedItem = tb;
+ tb.setAttribute("state", "current");
+ }
+
+ var vk = Number(tb.getAttribute("viewKey"));
+ delete client.activityList[vk];
+ client.deck.selectedPanel = obj.frame;
+
+ // Style userlist and the like:
+ updateAppMotif(obj.prefs["motif.current"]);
+
+ updateTitle();
+ updateProgress();
+ updateSecurityIcon();
+ updateLoggingIcon();
+
+ scrollDown(obj.frame, false);
+
+ // Input area should have the same direction as the output area
+ if (("frame" in client.currentObject) &&
+ client.currentObject.frame &&
+ getContentDocument(client.currentObject.frame) &&
+ ("body" in getContentDocument(client.currentObject.frame)) &&
+ getContentDocument(client.currentObject.frame).body)
+ {
+ var contentArea = getContentDocument(client.currentObject.frame).body;
+ client.input.setAttribute("dir", contentArea.getAttribute("dir"));
+ }
+ client.input.focus();
+}
+
+function checkScroll(frame)
+{
+ var window = getContentWindow(frame);
+ if (!window || !window.document || !window.document.body)
+ return false;
+
+ return (window.document.body.clientHeight - window.innerHeight -
+ window.pageYOffset) < 160;
+}
+
+function scrollDown(frame, force)
+{
+ var window = getContentWindow(frame);
+ if (!window || !window.document || !window.document.body)
+ return;
+
+ if (force || checkScroll(frame))
+ window.scrollTo(0, window.document.body.clientHeight);
+}
+
+function advanceKeyboardFocus(amount)
+{
+ var contentWin = getContentWindow(client.currentObject.frame);
+ var contentDoc = getContentDocument(client.currentObject.frame);
+ var userList = document.getElementById("user-list");
+
+ // Focus userlist, inputbox and outputwindow in turn:
+ var focusableElems = [userList, client.input.inputField, contentWin];
+
+ var elem = document.commandDispatcher.focusedElement;
+ // Finding focus in the content window is "hard". It's going to be null
+ // if the window itself is focused, and "some element" inside of it if the
+ // user starts tabbing through.
+ if (!elem || (elem.ownerDocument == contentDoc))
+ elem = contentWin;
+
+ var newIndex = (arrayIndexOf(focusableElems, elem) * 1 + 3 + amount) % 3;
+ focusableElems[newIndex].focus();
+
+ // Make it obvious this element now has focus.
+ var outlinedElem;
+ if (focusableElems[newIndex] == client.input.inputField)
+ outlinedElem = client.input.parentNode.id;
+ else if (focusableElems[newIndex] == userList)
+ outlinedElem = "user-list-box"
+ else
+ outlinedElem = "browser-box";
+
+ // Do magic, and make sure it gets undone at the right time:
+ if (("focusedElemTimeout" in client) && client.focusedElemTimeout)
+ clearTimeout(client.focusedElemTimeout);
+ outlineFocusedElem(outlinedElem);
+ client.focusedElemTimeout = setTimeout(outlineFocusedElem, 1000, "");
+}
+
+function outlineFocusedElem(id)
+{
+ var outlinedElements = ["user-list-box", "browser-box", "multiline-hug-box",
+ "singleline-hug-box"];
+ for (var i = 0; i < outlinedElements.length; i++)
+ {
+ var elem = document.getElementById(outlinedElements[i]);
+ if (outlinedElements[i] == id)
+ elem.setAttribute("focusobvious", "true");
+ else
+ elem.removeAttribute("focusobvious");
+ }
+ client.focusedElemTimeout = 0;
+}
+
+/* valid values for |what| are "superfluous", "activity", and "attention".
+ * final value for state is dependant on priority of the current state, and the
+ * new state. the priority is: normal < superfluous < activity < attention.
+ */
+function setTabState(source, what, callback)
+{
+ if (typeof source != "object")
+ {
+ if (!ASSERT(source in client.viewsArray,
+ "INVALID SOURCE passed to setTabState"))
+ return;
+ source = client.viewsArray[source].source;
+ }
+
+ callback = callback || false;
+
+ var tb = source.dispatch("create-tab-for-view", { view: source });
+ var vk = Number(tb.getAttribute("viewKey"));
+
+ var current = ("currentObject" in client && client.currentObject == source);
+
+ /* We want to play sounds if they're from a non-current view, or we don't
+ * have focus at all. Also make sure stalk matches always play sounds.
+ * Also make sure we don't play on the 2nd half of the flash (Callback).
+ */
+ if (!callback && (!window.isFocused || !current || (what == "attention")))
+ {
+ if (what == "attention")
+ playEventSounds(source.TYPE, "stalk", source);
+ else if (what == "activity")
+ playEventSounds(source.TYPE, "chat", source);
+ else if (what == "superfluous")
+ playEventSounds(source.TYPE, "event", source);
+ }
+
+ // Only change the tab's colour if it's not the active view.
+ if (!current)
+ {
+ var state = tb.getAttribute("state");
+ if (state == what)
+ {
+ /* if the tab state has an equal priority to what we are setting
+ * then blink it */
+ if (client.prefs["activityFlashDelay"] > 0)
+ {
+ tb.setAttribute("state", "normal");
+ setTimeout(setTabState, client.prefs["activityFlashDelay"],
+ vk, what, true);
+ }
+ }
+ else
+ {
+ if (state == "normal" || state == "superfluous" ||
+ (state == "activity" && what == "attention"))
+ {
+ /* if the tab state has a lower priority than what we are
+ * setting, change it to the new state */
+ tb.setAttribute("state", what);
+ /* we only change the activity list if priority has increased */
+ if (what == "attention")
+ client.activityList[vk] = "!";
+ else if (what == "activity")
+ client.activityList[vk] = "+";
+ else
+ {
+ /* this is functionally equivalent to "+" for now */
+ client.activityList[vk] = "-";
+ }
+ updateTitle();
+ }
+ else
+ {
+ /* the current state of the tab has a higher priority than the
+ * new state.
+ * blink the new lower state quickly, then back to the old */
+ if (client.prefs["activityFlashDelay"] > 0)
+ {
+ tb.setAttribute("state", what);
+ setTimeout(setTabState,
+ client.prefs["activityFlashDelay"], vk,
+ state, true);
+ }
+ }
+ }
+ }
+}
+
+function notifyAttention (source)
+{
+ if (typeof source != "object")
+ source = client.viewsArray[source].source;
+
+ if (client.currentObject != source)
+ {
+ var tb = dispatch("create-tab-for-view", { view: source });
+ var vk = Number(tb.getAttribute("viewKey"));
+
+ tb.setAttribute ("state", "attention");
+ client.activityList[vk] = "!";
+ updateTitle();
+ }
+
+ if (client.prefs["notify.aggressive"])
+ window.getAttention();
+
+}
+
+function setDebugMode(mode)
+{
+ if (mode.indexOf("t") != -1)
+ client.debugHook.enabled = true;
+ else
+ client.debugHook.enabled = false;
+
+ if (mode.indexOf("c") != -1)
+ client.dbgContexts = true;
+ else
+ delete client.dbgContexts;
+
+ if (mode.indexOf("d") != -1)
+ client.dbgDispatch = true;
+ else
+ delete client.dbgDispatch;
+}
+
+function setListMode(mode)
+{
+ var elem = document.getElementById("user-list");
+ if (mode)
+ elem.setAttribute("mode", mode);
+ else
+ elem.removeAttribute("mode");
+ if (elem && elem.view && elem.treeBoxObject)
+ {
+ elem.treeBoxObject.clearStyleAndImageCaches();
+ elem.treeBoxObject.invalidate();
+ }
+}
+
+function updateUserList()
+{
+ var node, chan;
+
+ node = document.getElementById("user-list");
+ if (!node.view)
+ return;
+
+ if (("currentObject" in client) && client.currentObject &&
+ client.currentObject.TYPE == "IRCChannel")
+ {
+ reSortUserlist(client.currentObject);
+ }
+}
+
+function reSortUserlist(channel)
+{
+ if (!channel || !channel.userList)
+ return;
+ channel.userList.childData.reSort();
+}
+
+function getNicknameForUserlistRow(index)
+{
+ // This wouldn't be so hard if APIs didn't change so much... see bug 221619
+ var userlist = document.getElementById("user-list");
+ if (userlist.columns)
+ var col = userlist.columns.getNamedColumn("usercol");
+ else
+ col = "usercol";
+ return userlist.view.getCellText(index, col);
+}
+
+function getFrameForDOMWindow(window)
+{
+ var frame;
+ for (var i = 0; i < client.deck.childNodes.length; i++)
+ {
+ frame = client.deck.childNodes[i];
+ if (frame.contentWindow == window)
+ return frame;
+ }
+ return undefined;
+}
+
+function replaceColorCodes(msg)
+{
+ // Find things that look like URLs and escape the color code inside of those
+ // to prevent munging the URLs resulting in broken links. Leave codes at
+ // the start of the URL alone.
+ msg = msg.replace(new RegExp(client.linkRE.source, "g"), function(url, _foo, scheme) {
+ if (scheme && !client.checkURLScheme(scheme))
+ return url;
+ return url.replace(/%[BC][0-9A-Fa-f]/g, function(hex, index) {
+ // as JS does not support lookbehind and we don't want to consume the
+ // preceding character, we test for an existing %% manually
+ var needPercent = ("%" == url.substr(index - 1, 1)) || (index == 0);
+ return (needPercent ? "" : "%") + hex;
+ });
+ });
+
+ // mIRC codes: underline, bold, Original (reset), colors, reverse colors.
+ msg = msg.replace(/(^|[^%])%U/g, "$1\x1f");
+ msg = msg.replace(/(^|[^%])%B/g, "$1\x02");
+ msg = msg.replace(/(^|[^%])%O/g, "$1\x0f");
+ msg = msg.replace(/(^|[^%])%C/g, "$1\x03");
+ msg = msg.replace(/(^|[^%])%R/g, "$1\x16");
+ // %%[UBOCR] --> %[UBOCR].
+ msg = msg.replace(/%(%[UBOCR])/g, "$1");
+
+ return msg;
+}
+
+function decodeColorCodes(msg)
+{
+ // %[UBOCR] --> %%[UBOCR].
+ msg = msg.replace(/(%[UBOCR])/g, "%$1");
+ // Put %-codes back in place of special character codes.
+ msg = msg.replace(/\x1f/g, "%U");
+ msg = msg.replace(/\x02/g, "%B");
+ msg = msg.replace(/\x0f/g, "%O");
+ msg = msg.replace(/\x03/g, "%C");
+ msg = msg.replace(/\x16/g, "%R");
+
+ return msg;
+}
+
+function removeColorCodes(msg)
+{
+ msg = msg.replace(/[\x1f\x02\x0f\x16]/g, "");
+ // We need this to be global:
+ msg = msg.replace(new RegExp(client.colorRE.source, "g"), "");
+ return msg;
+}
+
+client.progressListener = new Object();
+
+client.progressListener.QueryInterface =
+function qi(iid)
+{
+ return this;
+}
+
+client.progressListener.onStateChange =
+function client_statechange (webProgress, request, stateFlags, status)
+{
+ const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
+ const START = nsIWebProgressListener.STATE_START;
+ const STOP = nsIWebProgressListener.STATE_STOP;
+ const IS_NETWORK = nsIWebProgressListener.STATE_IS_NETWORK;
+ const IS_DOCUMENT = nsIWebProgressListener.STATE_IS_DOCUMENT;
+ const IS_REQUEST = nsIWebProgressListener.STATE_IS_REQUEST;
+
+ var frame;
+ //dd("progressListener.onStateChange(" + stateFlags.toString(16) + ")");
+
+ // We only care about the initial start of loading, not the loading of
+ // and page sub-components (IS_DOCUMENT, etc.).
+ if ((stateFlags & START) && (stateFlags & IS_NETWORK) &&
+ (stateFlags & IS_DOCUMENT))
+ {
+ frame = getFrameForDOMWindow(webProgress.DOMWindow);
+ if (!frame)
+ {
+ dd("can't find frame for window (start)");
+ try
+ {
+ webProgress.removeProgressListener(this);
+ }
+ catch(ex)
+ {
+ dd("Exception removing progress listener (start): " + ex);
+ }
+ }
+ }
+ // We only want to know when the *network* stops, not the page's
+ // individual components (STATE_IS_REQUEST/STATE_IS_DOCUMENT/somesuch).
+ else if ((stateFlags & STOP) && (stateFlags & IS_NETWORK))
+ {
+ frame = getFrameForDOMWindow(webProgress.DOMWindow);
+ if (!frame)
+ {
+ dd("can't find frame for window (stop)");
+ try
+ {
+ webProgress.removeProgressListener(this);
+ }
+ catch(ex)
+ {
+ dd("Exception removing progress listener (stop): " + ex);
+ }
+ }
+ else
+ {
+ var cwin = getContentWindow(frame);
+ if (cwin && "initOutputWindow" in cwin)
+ {
+ if (!("_called_initOutputWindow" in cwin))
+ {
+ cwin._called_initOutputWindow = true;
+ cwin.initOutputWindow(client, frame.source, onMessageViewClick);
+ cwin.changeCSS(frame.source.getFontCSS("data"), "cz-fonts");
+ scrollDown(frame, true);
+ //dd("initOutputWindow(" + frame.source.getURL() + ")");
+ }
+ }
+ // XXX: For about:blank it won't find initOutputWindow. Cope.
+ else if (!cwin || !cwin.location ||
+ (cwin.location.href != "about:blank"))
+ {
+ // This should totally never ever happen. It will if we get in a
+ // fight with xpcnativewrappers, though. Oops:
+ dd("Couldn't find a content window or its initOutputWindow " +
+ "function. This is BAD!");
+ }
+ }
+ }
+ // Requests stopping are either the page, or its components loading. We're
+ // interested in its components.
+ else if ((stateFlags & STOP) && (stateFlags & IS_REQUEST))
+ {
+ frame = getFrameForDOMWindow(webProgress.DOMWindow);
+ if (frame)
+ {
+ var cwin = getContentWindow(frame);
+ if (cwin && ("_called_initOutputWindow" in cwin))
+ {
+ scrollDown(frame, false);
+ //dd("scrollDown(" + frame.source.getURL() + ")");
+ }
+ }
+
+ }
+}
+
+client.progressListener.onProgressChange =
+function client_progresschange (webProgress, request, currentSelf, totalSelf,
+ currentMax, selfMax)
+{
+}
+
+client.progressListener.onLocationChange =
+function client_locationchange (webProgress, request, uri)
+{
+}
+
+client.progressListener.onStatusChange =
+function client_statuschange (webProgress, request, status, message)
+{
+}
+
+client.progressListener.onSecurityChange =
+function client_securitychange (webProgress, request, state)
+{
+}
+
+client.installPlugin =
+function cli_installPlugin(name, source)
+{
+ function checkPluginInstalled(name, path)
+ {
+ var installed = path.exists();
+ installed |= (name in client.plugins);
+
+ if (installed)
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_ALREADY_INST, MT_ERROR);
+ throw CZ_PI_ABORT;
+ }
+ };
+ function getZipEntry(reader, entryEnum)
+ {
+ // nsIZipReader was rewritten...
+ var itemName = entryEnum.getNext();
+ if (typeof itemName != "string")
+ name = itemName.QueryInterface(nsIZipEntry).name;
+ return itemName;
+ };
+ function checkZipMore(items)
+ {
+ return (("hasMoreElements" in items) && items.hasMoreElements()) ||
+ (("hasMore" in items) && items.hasMore());
+ };
+
+ const DIRECTORY_TYPE = Components.interfaces.nsIFile.DIRECTORY_TYPE;
+ const CZ_PI_ABORT = "CZ_PI_ABORT";
+ const nsIZipEntry = Components.interfaces.nsIZipEntry;
+
+ var dest;
+ // Find a suitable location if there was none specified.
+ var destList = client.prefs["initialScripts"];
+ if ((destList.length == 0) ||
+ ((destList.length == 1) && /^\s*$/.test(destList[0])))
+ {
+ // Reset to default because it is empty.
+ try
+ {
+ client.prefManager.clearPref("initialScripts");
+ }
+ catch(ex) {/* If this really fails, we're mostly screwed anyway */}
+ destList = client.prefs["initialScripts"];
+ }
+
+ // URLs for initialScripts can be relative (the default is):
+ var profilePath = getURLSpecFromFile(client.prefs["profilePath"]);
+ profilePath = Services.io.newURI(profilePath);
+ for (var i = 0; i < destList.length; i++)
+ {
+ var destURL = Services.io.newURI(destList[i], null, profilePath);
+ var file = new nsLocalFile(getFileFromURLSpec(destURL.spec).path);
+ if (file.exists() && file.isDirectory()) {
+ // A directory that exists! We'll take it!
+ dest = file.clone();
+ break;
+ }
+ }
+ if (!dest) {
+ display(MSG_INSTALL_PLUGIN_ERR_INSTALL_TO, MT_ERROR);
+ return;
+ }
+
+ try {
+ if (typeof source == "string")
+ source = getFileFromURLSpec(source);
+ }
+ catch (ex)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_ERR_CHECK_SD, ex), MT_ERROR);
+ return;
+ }
+
+ display(getMsg(MSG_INSTALL_PLUGIN_INSTALLING, [source.path, dest.path]),
+ MT_INFO);
+
+ var ary;
+ if (source.path.match(/\.(jar|zip)$/i))
+ {
+ try
+ {
+ var zipReader = newObject("@mozilla.org/libjar/zip-reader;1",
+ "nsIZipReader");
+ zipReader.open(source);
+
+ // This is set to the base path found on ALL items in the zip file.
+ // when we extract, this WILL BE REMOVED from all paths.
+ var zipPathBase = "";
+ // This always points to init.js, even if we're messing with paths.
+ var initPath = "init.js";
+
+ // Look for init.js within a directory...
+ var items = zipReader.findEntries("*/init.js");
+ while (checkZipMore(items))
+ {
+ var itemName = getZipEntry(zipReader, items);
+ // Do we already have one?
+ if (zipPathBase)
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_MANY_INITJS, MT_WARN);
+ throw CZ_PI_ABORT;
+ }
+ zipPathBase = itemName.match(/^(.*\/)init.js$/)[1];
+ initPath = itemName;
+ }
+
+ if (zipPathBase)
+ {
+ // We have a base for init.js, assert that all files are inside
+ // it. If not, we drop the path and install exactly as-is
+ // instead (which will probably cause it to not work because the
+ // init.js isn't in the right place).
+ items = zipReader.findEntries("*");
+ while (checkZipMore(items))
+ {
+ itemName = getZipEntry(zipReader, items);
+ if (itemName.substr(0, zipPathBase.length) != zipPathBase)
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_MIXED_BASE, MT_WARN);
+ zipPathBase = "";
+ break;
+ }
+ }
+ }
+
+ // Test init.js for a plugin ID.
+ var initJSFile = getTempFile(client.prefs["profilePath"],
+ "install-plugin.temp");
+ zipReader.extract(initPath, initJSFile);
+ initJSFile.permissions = 438; // 0666
+ var initJSFileH = fopen(initJSFile, "<");
+ var initJSData = initJSFileH.read();
+ initJSFileH.close();
+ initJSFile.remove(false);
+
+ //XXXgijs: this is fragile. Anyone got a better idea?
+ ary = initJSData.match(/plugin\.id\s*=\s*(['"])(.*?)(\1)/);
+ if (ary && (name != ary[2]))
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_WARN_NAME, [name, ary[2]]),
+ MT_WARN);
+ name = ary[2];
+ }
+
+ dest.append(name);
+ checkPluginInstalled(name, dest);
+
+ dest.create(DIRECTORY_TYPE, 0o700);
+
+ // Actually extract files...
+ var destInit;
+ items = zipReader.findEntries("*");
+ while (checkZipMore(items))
+ {
+ itemName = getZipEntry(zipReader, items);
+ if (!itemName.match(/\/$/))
+ {
+ var dirs = itemName;
+ // Remove common path if there is one.
+ if (zipPathBase)
+ dirs = dirs.substring(zipPathBase.length);
+ dirs = dirs.split("/");
+
+ // Construct the full path for the extracted file...
+ var zipFile = dest.clone();
+ for (var i = 0; i < dirs.length - 1; i++)
+ {
+ zipFile.append(dirs[i]);
+ if (!zipFile.exists())
+ zipFile.create(DIRECTORY_TYPE, 0o700);
+ }
+ zipFile.append(dirs[dirs.length - 1]);
+
+ if (zipFile.leafName == "init.js")
+ destInit = zipFile;
+
+ zipReader.extract(itemName, zipFile);
+ zipFile.permissions = 438; // 0666
+ }
+ }
+
+ var rv = dispatch("load ", {url: getURLSpecFromFile(destInit)});
+ if (rv)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_DONE, name));
+ }
+ else
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_REMOVING, MT_ERROR);
+ dest.remove(true);
+ }
+ }
+ catch (ex)
+ {
+ if (ex != CZ_PI_ABORT)
+ display(getMsg(MSG_INSTALL_PLUGIN_ERR_EXTRACT, ex), MT_ERROR);
+ zipReader.close();
+ return;
+ }
+ try
+ {
+ zipReader.close();
+ }
+ catch (ex)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_ERR_EXTRACT, ex), MT_ERROR);
+ }
+ }
+ else if (source.path.match(/\.(js)$/i))
+ {
+ try
+ {
+ // Test init.js for a plugin ID.
+ var initJSFileH = fopen(source, "<");
+ var initJSData = initJSFileH.read();
+ initJSFileH.close();
+
+ ary = initJSData.match(/plugin\.id\s*=\s*(['"])(.*?)(\1)/);
+ if (ary && (name != ary[2]))
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_WARN_NAME, [name, ary[2]]),
+ MT_WARN);
+ name = ary[2];
+ }
+
+ dest.append(name);
+ checkPluginInstalled(name, dest);
+
+ dest.create(DIRECTORY_TYPE, 0o700);
+
+ dest.append("init.js");
+
+ var destFile = fopen(dest, ">");
+ destFile.write(initJSData);
+ destFile.close();
+
+ var rv = dispatch("load", {url: getURLSpecFromFile(dest)});
+ if (rv)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_DONE, name));
+ }
+ else
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_REMOVING, MT_ERROR);
+ // We've appended "init.js" before, so go back up one level:
+ dest.parent.remove(true);
+ }
+ }
+ catch(ex)
+ {
+ if (ex != CZ_PI_ABORT)
+ {
+ display(getMsg(MSG_INSTALL_PLUGIN_ERR_INSTALLING, ex),
+ MT_ERROR);
+ }
+ }
+ }
+ else
+ {
+ display(MSG_INSTALL_PLUGIN_ERR_FORMAT, MT_ERROR);
+ }
+}
+
+client.uninstallPlugin =
+function cli_uninstallPlugin(plugin)
+{
+ if (!disablePlugin(plugin, true))
+ return;
+ delete client.plugins[plugin.id];
+ let file = getFileFromURLSpec(plugin.cwd);
+ if (file.exists() && file.isDirectory())
+ {
+ // Delete the directory and contents.
+ file.remove(true);
+ }
+ display(getMsg(MSG_PLUGIN_UNINSTALLED, plugin.id));
+}
+
+function syncOutputFrame(obj, nesting)
+{
+ const nsIWebProgress = Components.interfaces.nsIWebProgress;
+ const WINDOW = nsIWebProgress.NOTIFY_STATE_WINDOW;
+ const NETWORK = nsIWebProgress.NOTIFY_STATE_NETWORK;
+ const ALL = nsIWebProgress.NOTIFY_ALL;
+
+ var iframe = obj.frame;
+
+ function tryAgain(nLevel)
+ {
+ syncOutputFrame(obj, nLevel);
+ };
+
+ if (typeof nesting != "number")
+ nesting = 0;
+
+ if (nesting > 10)
+ {
+ dd("Aborting syncOutputFrame, taken too many tries.");
+ return;
+ }
+
+ /* We leave the progress listener attached so try to remove it first,
+ * should we be called on an already-set-up view.
+ */
+ try
+ {
+ iframe.removeProgressListener(client.progressListener, ALL);
+ }
+ catch (ex)
+ {
+ }
+
+ try
+ {
+ if (getContentDocument(iframe) && ("webProgress" in iframe))
+ {
+ var url = obj.prefs["outputWindowURL"];
+ iframe.addProgressListener(client.progressListener, ALL);
+ iframe.loadURI(url);
+ }
+ else
+ {
+ setTimeout(tryAgain, 500, nesting + 1);
+ }
+ }
+ catch (ex)
+ {
+ dd("caught exception showing session view, will try again later.");
+ dd(dumpObjectTree(ex) + "\n");
+ setTimeout(tryAgain, 500, nesting + 1);
+ }
+}
+
+function createMessages(source)
+{
+ playEventSounds(source.TYPE, "start", source);
+
+ source.messages = document.createElementNS(XHTML_NS, "html:table");
+ source.messages.setAttribute("class", "msg-table");
+ source.messages.setAttribute("view-type", source.TYPE);
+ source.messages.setAttribute("role", "log");
+ source.messages.setAttribute("aria-live", "polite");
+
+ var tbody = document.createElementNS(XHTML_NS, "html:tbody");
+ source.messages.appendChild (tbody);
+ source.messageCount = 0;
+}
+
+/* Gets the <tab> element associated with a view object.
+ * If |create| is present, and true, tab is created if not found.
+ */
+function getTabForObject(source, create)
+{
+ var name;
+
+ if (!ASSERT(source, "UNDEFINED passed to getTabForObject"))
+ return null;
+
+ if (!ASSERT("viewName" in source, "INVALID OBJECT in getTabForObject"))
+ return null;
+
+ name = source.viewName;
+
+ var tb, id = "tb[" + name + "]";
+ var matches = 1;
+
+ for (var i in client.viewsArray)
+ {
+ if (client.viewsArray[i].source == source)
+ {
+ tb = client.viewsArray[i].tb;
+ break;
+ }
+ else
+ if (client.viewsArray[i].tb.getAttribute("id") == id)
+ id = "tb[" + name + "<" + (++matches) + ">]";
+ }
+
+ /* If we found a <tab>, are allowed to create it, and have a pending view
+ * context, then we're expected to move the existing tab according to said
+ * context. We do that by removing the tab here, and below the creation
+ * code (which is not run) we readd it in the correct location.
+ */
+ if (tb && create && (typeof client.pendingViewContext == "object"))
+ {
+ /* If we're supposed to insert before ourselves, or the next <tab>,
+ * then bail out (since it's a no-op).
+ */
+ var tabBefore = client.pendingViewContext.tabInsertBefore;
+ if (tabBefore)
+ {
+ var tbAfter = tb.nextSibling;
+ while (tbAfter && tbAfter.collapsed && tbAfter.hidden)
+ tbAfter = tbAfter.nextSibling;
+ if ((tabBefore == tb) || (tabBefore == tbAfter))
+ return tb;
+ }
+
+ var viewKey = Number(tb.getAttribute("viewKey"));
+ arrayRemoveAt(client.viewsArray, viewKey);
+ for (i = viewKey; i < client.viewsArray.length; i++)
+ client.viewsArray[i].tb.setAttribute("viewKey", i);
+ client.tabs.removeChild(tb);
+ }
+ else if (tb || (!tb && !create))
+ {
+ /* Either: we have a tab and don't want it moved, or didn't find one
+ * and don't wish to create one.
+ */
+ return tb;
+ }
+
+ // Didn't found a <tab>, but we're allowed to create one.
+ if (!tb && create)
+ {
+ if (!("messages" in source) || source.messages == null)
+ createMessages(source);
+
+ tb = document.createElement("tab");
+ tb.setAttribute("ondragstart", "tabsDNDObserver.onDragStart(event);");
+ tb.setAttribute("href", source.getURL());
+ tb.setAttribute("name", source.unicodeName);
+ tb.setAttribute("onclick", "onTabClick(event, this.id);");
+ // This wouldn't be here if there was a supported CSS property for it.
+ tb.setAttribute("crop", "center");
+ tb.setAttribute("context", "context:tab");
+ tb.setAttribute("class", "tab-bottom view-button");
+ tb.setAttribute("id", id);
+ tb.setAttribute("state", "normal");
+ name = source.prefs["tabLabel"] || name;
+ tb.setAttribute("label", name + (matches > 1 ? "<" + matches + ">" : ""));
+ tb.setAttribute("tooltiptext", source.viewName);
+ tb.view = source;
+
+ var browser = document.createElement("browser");
+ browser.setAttribute("class", "output-container");
+ browser.setAttribute("type", "content");
+ browser.setAttribute("flex", "1");
+ browser.setAttribute("tooltip", "html-tooltip-node");
+ browser.setAttribute("onclick",
+ "return onMessageViewClick(event)");
+ browser.setAttribute("onmousedown",
+ "return onMessageViewMouseDown(event)");
+ browser.setAttribute("oncontextmenu",
+ "return onMessageViewContextMenu(event)");
+ browser.setAttribute("ondragover",
+ "contentDNDObserver.onDragOver(event);");
+ browser.setAttribute("ondrop", "contentDNDObserver.onDrop(event);");
+ browser.source = source;
+ source.frame = browser;
+ ASSERT(client.deck, "no deck?");
+ client.deck.appendChild(browser);
+ syncOutputFrame(source);
+
+ if (!("userList" in source) && (source.TYPE == "IRCChannel"))
+ {
+ source.userListShare = new Object();
+ source.userList = new XULTreeView(source.userListShare);
+ source.userList.getRowProperties = ul_getrowprops;
+ source.userList.getCellProperties = ul_getcellprops;
+ source.userList.childData.setSortDirection(1);
+ }
+ }
+
+ var beforeTab = null;
+ if (typeof client.pendingViewContext == "object")
+ {
+ var c = client.pendingViewContext;
+ /* If we have a <tab> to insert before, and it is still in the tabs,
+ * move the newly-created <tab> into the right place.
+ */
+ if (c.tabInsertBefore && (c.tabInsertBefore.parentNode == client.tabs))
+ beforeTab = c.tabInsertBefore;
+ }
+
+ if (beforeTab)
+ {
+ var viewKey = beforeTab.getAttribute("viewKey");
+ arrayInsertAt(client.viewsArray, viewKey, {source: source, tb: tb});
+ for (i = viewKey; i < client.viewsArray.length; i++)
+ client.viewsArray[i].tb.setAttribute("viewKey", i);
+ client.tabs.insertBefore(tb, beforeTab);
+ }
+ else
+ {
+ client.viewsArray.push({source: source, tb: tb});
+ tb.setAttribute("viewKey", client.viewsArray.length - 1);
+ client.tabs.appendChild(tb);
+ }
+
+ updateTabAttributes();
+
+ return tb;
+}
+
+function updateTabAttributes()
+{
+ /* XXX: Workaround for Gecko bugs 272646 and 261826. Note that this breaks
+ * the location of the spacers before and after the tabs but, due to our
+ * own <spacer>, their flex was not being utilised anyway.
+ */
+ var tabOrdinal = 0;
+ for (var tab = client.tabs.firstChild; tab; tab = tab.nextSibling)
+ tab.ordinal = tabOrdinal++;
+
+ /* XXX: Workaround for tabbox.xml not coping with updating attributes when
+ * tabs are moved. We correct the "first-tab", "last-tab", "beforeselected"
+ * and "afterselected" attributes.
+ *
+ * "last-tab" and "beforeselected" are updated on each valid (non-collapsed
+ * and non-hidden) tab found, to avoid having to work backwards as well as
+ * forwards. "first-tab" and "afterselected" are just set the once each.
+ * |foundSelected| tracks where we are in relation to the selected tab.
+ */
+ var tabAttrs = {
+ "first-tab": null,
+ "last-tab": null,
+ "beforeselected": null,
+ "afterselected": null
+ };
+ var foundSelected = "before";
+ for (tab = client.tabs.firstChild; tab; tab = tab.nextSibling)
+ {
+ if (tab.collapsed || tab.hidden)
+ continue;
+
+ if (!tabAttrs["first-tab"])
+ tabAttrs["first-tab"] = tab;
+ tabAttrs["last-tab"] = tab;
+
+ if ((foundSelected == "before") && tab.selected)
+ foundSelected = "on";
+ else if (foundSelected == "on")
+ foundSelected = "after";
+
+ if (foundSelected == "before")
+ tabAttrs["beforeselected"] = tab;
+ if ((foundSelected == "after") && !tabAttrs["afterselected"])
+ tabAttrs["afterselected"] = tab;
+ }
+
+ // After picking a tab for each attribute, apply them to the tabs.
+ for (tab = client.tabs.firstChild; tab; tab = tab.nextSibling)
+ {
+ for (var attr in tabAttrs)
+ {
+ if (tabAttrs[attr] == tab)
+ tab.setAttribute(attr, "true");
+ else
+ tab.removeAttribute(attr);
+ }
+ }
+}
+
+// Properties getter for user list tree view
+function ul_getrowprops(index)
+{
+ if ((index < 0) || (index >= this.childData.childData.length))
+ {
+ return "";
+ }
+
+ // See bug 432482 - work around Gecko deficiency.
+ if (!this.selection.isSelected(index))
+ {
+ return "unselected";
+ }
+
+ return "";
+}
+
+// Properties getter for user list tree view
+function ul_getcellprops(index, column)
+{
+ if ((index < 0) || (index >= this.childData.childData.length))
+ {
+ return "";
+ }
+
+ var resultProps = [];
+
+ // See bug 432482 - work around Gecko deficiency.
+ if (!this.selection.isSelected(index))
+ resultProps.push("unselected");
+
+ var userObj = this.childData.childData[index]._userObj;
+
+ resultProps.push("voice-" + userObj.isVoice);
+ resultProps.push("op-" + userObj.isOp);
+ resultProps.push("halfop-" + userObj.isHalfOp);
+ resultProps.push("admin-" + userObj.isAdmin);
+ resultProps.push("founder-" + userObj.isFounder);
+ resultProps.push("away-" + userObj.isAway);
+
+ return resultProps.join(" ");
+}
+
+var contentDNDObserver = {
+ onDragOver(aEvent) {
+ if (aEvent.target == aEvent.dataTransfer.mozSourceNode)
+ return;
+
+ if (Services.droppedLinkHandler.canDropLink(aEvent, true))
+ aEvent.preventDefault();
+ },
+
+ onDrop(aEvent) {
+ aEvent.stopPropagation();
+
+ var url = Services.droppedLinkHandler.dropLink(aEvent, {});
+ if (!url || url.search(client.linkRE) == -1)
+ return;
+
+ if (url.search(/\.css$/i) != -1 && confirm(getMsg(MSG_TABDND_DROP, url)))
+ dispatch("motif", {"motif": url});
+ else if (url.search(/^ircs?:\/\//i) != -1)
+ dispatch("goto-url", {"url": url});
+ },
+}
+
+var tabsDNDObserver = {
+ onDragOver(aEvent) {
+ if (aEvent.target == aEvent.dataTransfer.mozSourceNode)
+ return;
+
+ // If we're not accepting the drag, don't show the marker either.
+ if (!Services.droppedLinkHandler.canDropLink(aEvent, true)) {
+ client.tabDragBar.collapsed = true;
+ return;
+ }
+
+ aEvent.preventDefault();
+
+ /* Locate the tab we're about to drop onto. We split tabs in half, dropping
+ * on the side closest to the mouse, or after the last tab if the mouse is
+ * somewhere beyond all the tabs.
+ */
+ var ltr = (window.getComputedStyle(client.tabs, null).direction == "ltr");
+ var newPosition = client.tabs.firstChild.boxObject.x;
+ for (var dropTab = client.tabs.firstChild; dropTab;
+ dropTab = dropTab.nextSibling)
+ {
+ if (dropTab.collapsed || dropTab.hidden)
+ continue;
+ var bo = dropTab.boxObject;
+ if ((ltr && (aEvent.screenX < bo.screenX + bo.width / 2)) ||
+ (!ltr && (aEvent.screenX > bo.screenX + bo.width / 2)))
+ {
+ break;
+ }
+ newPosition = bo.x + bo.width;
+ }
+
+ // Reposition the drop marker and show it. In that order.
+ client.tabDragMarker.style.MozMarginStart = newPosition + "px";
+ client.tabDragBar.collapsed = false;
+ },
+
+ onDragExit(aEvent) {
+ aEvent.stopPropagation();
+
+ /* We've either stopped being part of a drag operation, or the dragging is
+ * somewhere away from us.
+ */
+ client.tabDragBar.collapsed = true;
+ },
+
+ onDrop(aEvent) {
+ aEvent.stopPropagation();
+
+ // Dragging has finished.
+ client.tabDragBar.collapsed = true;
+
+ // See comment above |var tabsDropObserver|.
+ var url = Services.droppedLinkHandler.dropLink(aEvent, {});
+ if (!url || !(url.match(/^ircs?:/) || url.match(/^x-irc-dcc-(chat|file):/)))
+ return;
+
+ // Find the tab to insertBefore() the new one.
+ var ltr = (window.getComputedStyle(client.tabs, null).direction == "ltr");
+ for (var dropTab = client.tabs.firstChild; dropTab;
+ dropTab = dropTab.nextSibling)
+ {
+ if (dropTab.collapsed || dropTab.hidden)
+ continue;
+ var bo = dropTab.boxObject;
+ if ((ltr && (aEvent.screenX < bo.screenX + bo.width / 2)) ||
+ (!ltr && (aEvent.screenX > bo.screenX + bo.width / 2)))
+ {
+ break;
+ }
+ }
+
+ // Check if the URL is already in the views.
+ for (var i = 0; i < client.viewsArray.length; i++)
+ {
+ var view = client.viewsArray[i].source;
+ if (view.getURL() == url)
+ {
+ client.pendingViewContext = { tabInsertBefore: dropTab };
+ dispatch("create-tab-for-view", { view: view });
+ delete client.pendingViewContext;
+ return;
+ }
+ }
+
+ // URL not found in tabs, so force it into life - this may connect/rejoin.
+ if (url.substring(0, 3) == "irc")
+ gotoIRCURL(url, { tabInsertBefore: dropTab });
+ },
+
+ onDragStart(aEvent) {
+ var tb = aEvent.currentTarget;
+ var href = tb.getAttribute("href");
+ var name = tb.getAttribute("name");
+
+ /* x-moz-url has the format "<url>\n<name>", goodie */
+ aEvent.dataTransfer.setData("text/x-moz-url", href + "\n" + name);
+ aEvent.dataTransfer.setData("text/unicode", href);
+ aEvent.dataTransfer.setData("text/plain", href);
+ aEvent.dataTransfer.setData("text/html", "<a href='" + href + "'>" +
+ name + "</a>");
+ },
+}
+
+var userlistDNDObserver = {
+ onDragStart(aEvent) {
+ var col = {};
+ var row = {};
+ var cell = {};
+ var tree = document.getElementById('user-list');
+ tree.treeBoxObject.getCellAt(aEvent.clientX, aEvent.clientY,
+ row, col, cell);
+ // Check whether we're actually on a normal row and cell
+ if (!cell.value || (row.value == -1))
+ return;
+
+ var nickname = getNicknameForUserlistRow(row.value);
+ aEvent.dataTransfer.setData("text/unicode", nickname);
+ aEvent.dataTransfer.setData("text/plain", nickname);
+ },
+}
+
+function deleteTab(tb)
+{
+ if (!ASSERT(tb.hasAttribute("viewKey"),
+ "INVALID OBJECT passed to deleteTab (" + tb + ")"))
+ {
+ return null;
+ }
+
+ var key = Number(tb.getAttribute("viewKey"));
+
+ // Re-index higher tabs.
+ for (var i = key + 1; i < client.viewsArray.length; i++)
+ client.viewsArray[i].tb.setAttribute("viewKey", i - 1);
+ arrayRemoveAt(client.viewsArray, key);
+ client.tabs.removeChild(tb);
+ setTimeout(updateTabAttributes, 0);
+
+ return key;
+}
+
+function deleteFrame(view)
+{
+ const nsIWebProgress = Components.interfaces.nsIWebProgress;
+ const ALL = nsIWebProgress.NOTIFY_ALL;
+
+ // We leave the progress listener attached so try to remove it.
+ try
+ {
+ view.frame.removeProgressListener(client.progressListener, ALL);
+ }
+ catch (ex)
+ {
+ dd(formatException(ex));
+ }
+
+ client.deck.removeChild(view.frame);
+ delete view.frame;
+}
+
+function filterOutput(msg, msgtype, dest)
+{
+ if ("outputFilters" in client)
+ {
+ for (var f in client.outputFilters)
+ {
+ if (client.outputFilters[f].enabled)
+ msg = client.outputFilters[f].func(msg, msgtype, dest);
+ }
+ }
+
+ return msg;
+}
+
+function updateTimestamps(view)
+{
+ if (!("messages" in view))
+ return;
+
+ view._timestampLast = "";
+ var node = view.messages.firstChild.firstChild;
+ var nested;
+ while (node)
+ {
+ if(node.className == "msg-nested-tr")
+ {
+ nested = node.firstChild.firstChild.firstChild.firstChild;
+ while (nested)
+ {
+ updateTimestampFor(view, nested);
+ nested = nested.nextSibling;
+ }
+ }
+ else
+ {
+ updateTimestampFor(view, node);
+ }
+ node = node.nextSibling;
+ }
+}
+
+function updateTimestampFor(view, displayRow, forceOldStamp)
+{
+ var time = new Date(1 * displayRow.getAttribute("timestamp"));
+ var tsCell = displayRow.firstChild;
+ if (!tsCell)
+ return;
+
+ var fmt;
+ if (view.prefs["timestamps"])
+ fmt = strftime(view.prefs["timestamps.display"], time);
+
+ while (tsCell.lastChild)
+ tsCell.removeChild(tsCell.lastChild);
+
+ var needStamp = fmt && (forceOldStamp || !view.prefs["collapseMsgs"] ||
+ (fmt != view._timestampLast));
+ if (needStamp)
+ tsCell.appendChild(document.createTextNode(fmt));
+ if (!forceOldStamp)
+ view._timestampLast = fmt;
+}
+
+client.updateMenus =
+function c_updatemenus(menus)
+{
+ // Don't bother if the menus aren't even created yet.
+ if (!client.initialized)
+ return null;
+
+ return this.menuManager.updateMenus(document, menus);
+}
+
+client.checkURLScheme =
+function c_checkURLScheme(url)
+{
+ if (!("schemes" in client))
+ {
+ var pfx = "@mozilla.org/network/protocol;1?name=";
+ var len = pfx.length;
+
+ client.schemes = new Object();
+ for (var c in Components.classes)
+ {
+ if (c.indexOf(pfx) == 0)
+ client.schemes[c.substr(len)] = true;
+ }
+ }
+ return (url.toLowerCase() in client.schemes);
+}
+
+client.adoptNode =
+function cli_adoptnode(node, doc)
+{
+ try
+ {
+ doc.adoptNode(node);
+ }
+ catch(ex)
+ {
+ dd(formatException(ex));
+ var err = ex.name;
+ // TypeError from before adoptNode was added; NOT_IMPL after.
+ if ((err == "TypeError") || (err == "NS_ERROR_NOT_IMPLEMENTED"))
+ client.adoptNode = cli_adoptnode_noop;
+ }
+ return node;
+}
+
+function cli_adoptnode_noop(node, doc)
+{
+ return node;
+}
+
+client.addNetwork =
+function cli_addnet(name, serverList, temporary)
+{
+ let net = new CIRCNetwork(name, serverList, client.eventPump, temporary);
+ client.networks[net.collectionKey] = net;
+}
+
+client.getNetwork =
+function cli_getnet(name)
+{
+ return client.networks[":" + name] || null;
+}
+
+client.removeNetwork =
+function cli_removenet(name)
+{
+ let net = client.getNetwork(name);
+
+ // Allow network a chance to clean up any mess.
+ if (typeof net.destroy == "function")
+ net.destroy();
+
+ delete client.networks[net.collectionKey];
+}
+
+client.connectToNetwork =
+function cli_connect(networkOrName, requireSecurity)
+{
+ var network;
+ var name;
+
+
+ if (isinstance(networkOrName, CIRCNetwork))
+ {
+ network = networkOrName;
+ }
+ else
+ {
+ name = networkOrName;
+ network = client.getNetwork(name);
+
+ if (!network)
+ {
+ display(getMsg(MSG_ERR_UNKNOWN_NETWORK, name), MT_ERROR);
+ return null;
+ }
+ }
+ name = network.unicodeName;
+
+ dispatch("create-tab-for-view", { view: network });
+ dispatch("set-current-view", { view: network });
+
+ if (network.isConnected())
+ {
+ network.display(getMsg(MSG_ALREADY_CONNECTED, name));
+ return network;
+ }
+
+ if (network.state != NET_OFFLINE)
+ return network;
+
+ if (network.prefs["nickname"] == DEFAULT_NICK)
+ network.prefs["nickname"] = prompt(MSG_ENTER_NICK, DEFAULT_NICK);
+
+ if (!("connecting" in network))
+ network.display(getMsg(MSG_NETWORK_CONNECTING, name));
+
+ network.connect(requireSecurity);
+
+ network.updateHeader();
+ client.updateHeader();
+ updateTitle();
+
+ return network;
+}
+
+
+client.getURL =
+function cli_geturl ()
+{
+ return "irc://";
+}
+
+client.load =
+function cli_load(url, scope)
+{
+ if (!("_loader" in client))
+ {
+ const LOADER_CTRID = "@mozilla.org/moz/jssubscript-loader;1";
+ const mozIJSSubScriptLoader =
+ Components.interfaces.mozIJSSubScriptLoader;
+
+ var cls;
+ if ((cls = Components.classes[LOADER_CTRID]))
+ client._loader = cls.getService(mozIJSSubScriptLoader);
+ }
+
+ if (client._loader.loadSubScriptWithOptions)
+ {
+ var opts = {target: scope, ignoreCache: true};
+ return client._loader.loadSubScriptWithOptions(url, opts);
+ }
+
+ return client._loader.loadSubScript(url, scope);
+}
+
+client.sayToCurrentTarget =
+function cli_say(msg, isInteractive)
+{
+ if ("say" in client.currentObject)
+ {
+ client.currentObject.dispatch("say", {message: msg}, isInteractive);
+ return;
+ }
+
+ switch (client.currentObject.TYPE)
+ {
+ case "IRCClient":
+ dispatch("eval", {expression: msg}, isInteractive);
+ break;
+
+ default:
+ if (msg != "")
+ display(MSG_ERR_NO_DEFAULT, MT_ERROR);
+ break;
+ }
+}
+
+CIRCNetwork.prototype.__defineGetter__("prefs", net_getprefs);
+function net_getprefs()
+{
+ if (!("_prefs" in this))
+ {
+ this._prefManager = getNetworkPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefs;
+}
+
+CIRCNetwork.prototype.__defineGetter__("prefManager", net_getprefmgr);
+function net_getprefmgr()
+{
+ if (!("_prefManager" in this))
+ {
+ this._prefManager = getNetworkPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefManager;
+}
+
+CIRCServer.prototype.__defineGetter__("prefs", srv_getprefs);
+function srv_getprefs()
+{
+ return this.parent.prefs;
+}
+
+CIRCServer.prototype.__defineGetter__("prefManager", srv_getprefmgr);
+function srv_getprefmgr()
+{
+ return this.parent.prefManager;
+}
+
+CIRCChannel.prototype.__defineGetter__("prefs", chan_getprefs);
+function chan_getprefs()
+{
+ if (!("_prefs" in this))
+ {
+ this._prefManager = getChannelPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefs;
+}
+
+CIRCChannel.prototype.__defineGetter__("prefManager", chan_getprefmgr);
+function chan_getprefmgr()
+{
+ if (!("_prefManager" in this))
+ {
+ this._prefManager = getChannelPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefManager;
+}
+
+CIRCUser.prototype.__defineGetter__("prefs", usr_getprefs);
+function usr_getprefs()
+{
+ if (!("_prefs" in this))
+ {
+ this._prefManager = getUserPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefs;
+}
+
+CIRCUser.prototype.__defineGetter__("prefManager", usr_getprefmgr);
+function usr_getprefmgr()
+{
+ if (!("_prefManager" in this))
+ {
+ this._prefManager = getUserPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefManager;
+}
+
+CIRCDCCUser.prototype.__defineGetter__("prefs", dccusr_getprefs);
+function dccusr_getprefs()
+{
+ if (!("_prefs" in this))
+ {
+ this._prefManager = getDCCUserPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefs;
+}
+
+CIRCDCCUser.prototype.__defineGetter__("prefManager", dccusr_getprefmgr);
+function dccusr_getprefmgr()
+{
+ if (!("_prefManager" in this))
+ {
+ this._prefManager = getDCCUserPrefManager(this);
+ this._prefs = this._prefManager.prefs;
+ }
+
+ return this._prefManager;
+}
+
+CIRCDCCChat.prototype.__defineGetter__("prefs", dccchat_getprefs);
+function dccchat_getprefs()
+{
+ return this.user.prefs;
+}
+
+CIRCDCCChat.prototype.__defineGetter__("prefManager", dccchat_getprefmgr);
+function dccchat_getprefmgr()
+{
+ return this.user.prefManager;
+}
+
+CIRCDCCFileTransfer.prototype.__defineGetter__("prefs", dccfile_getprefs);
+function dccfile_getprefs()
+{
+ return this.user.prefs;
+}
+
+CIRCDCCFileTransfer.prototype.__defineGetter__("prefManager", dccfile_getprefmgr);
+function dccfile_getprefmgr()
+{
+ return this.user.prefManager;
+}
+
+/* Displays a network-centric message on the most appropriate view.
+ *
+ * When |client.SLOPPY_NETWORKS| is |true|, messages will be displayed on the
+ * *current* view instead of the network view, if the current view is part of
+ * the same network.
+ */
+CIRCNetwork.prototype.display =
+function net_display(message, msgtype, sourceObj, destObj, tags)
+{
+ var o = getObjectDetails(client.currentObject);
+ if (client.SLOPPY_NETWORKS && client.currentObject != this &&
+ o.network == this && o.server && o.server.isConnected)
+ {
+ client.currentObject.display(message, msgtype, sourceObj, destObj,
+ tags);
+ }
+ else
+ {
+ this.displayHere(message, msgtype, sourceObj, destObj, tags);
+ }
+}
+
+/* Displays a channel-centric message on the most appropriate view.
+ *
+ * If the channel view already exists (visible or hidden), messages are added
+ * to it; otherwise, messages go to the *network* view.
+ */
+CIRCChannel.prototype.display =
+function chan_display(message, msgtype, sourceObj, destObj, tags)
+{
+ if ("messages" in this)
+ this.displayHere(message, msgtype, sourceObj, destObj, tags);
+ else
+ this.parent.parent.displayHere(message, msgtype, sourceObj, destObj,
+ tags);
+}
+
+/* Displays a user-centric message on the most appropriate view.
+ *
+ * If the user view already exists (visible or hidden), messages are added to
+ * it; otherwise, it goes to the *current* view if the current view is part of
+ * the same network, or the *network* view if not.
+ */
+CIRCUser.prototype.display =
+function usr_display(message, msgtype, sourceObj, destObj, tags)
+{
+ if ("messages" in this)
+ {
+ this.displayHere(message, msgtype, sourceObj, destObj, tags);
+ }
+ else
+ {
+ var o = getObjectDetails(client.currentObject);
+ if (o.server && o.server.isConnected &&
+ o.network == this.parent.parent &&
+ client.currentObject.TYPE != "IRCUser")
+ client.currentObject.display(message, msgtype, sourceObj, destObj,
+ tags);
+ else
+ this.parent.parent.displayHere(message, msgtype, sourceObj,
+ destObj, tags);
+ }
+}
+
+/* Displays a DCC user/file transfer-centric message on the most appropriate view.
+ *
+ * If the DCC user/file transfer view already exists (visible or hidden),
+ * messages are added to it; otherwise, messages go to the *current* view.
+ */
+CIRCDCCChat.prototype.display =
+CIRCDCCFileTransfer.prototype.display =
+function dcc_display(message, msgtype, sourceObj, destObj)
+{
+ if ("messages" in this)
+ this.displayHere(message, msgtype, sourceObj, destObj);
+ else
+ client.currentObject.display(message, msgtype, sourceObj, destObj);
+}
+
+function feedback(e, message, msgtype, sourceObj, destObj)
+{
+ if ("isInteractive" in e && e.isInteractive)
+ display(message, msgtype, sourceObj, destObj);
+}
+
+CIRCChannel.prototype.feedback =
+CIRCNetwork.prototype.feedback =
+CIRCUser.prototype.feedback =
+CIRCDCCChat.prototype.feedback =
+CIRCDCCFileTransfer.prototype.feedback =
+client.feedback =
+function this_feedback(e, message, msgtype, sourceObj, destObj)
+{
+ if ("isInteractive" in e && e.isInteractive)
+ this.displayHere(message, msgtype, sourceObj, destObj);
+}
+
+function display (message, msgtype, sourceObj, destObj, tags)
+{
+ client.currentObject.display (message, msgtype, sourceObj, destObj, tags);
+}
+
+client.getFontCSS =
+CIRCNetwork.prototype.getFontCSS =
+CIRCChannel.prototype.getFontCSS =
+CIRCUser.prototype.getFontCSS =
+CIRCDCCChat.prototype.getFontCSS =
+CIRCDCCFileTransfer.prototype.getFontCSS =
+function this_getFontCSS(format)
+{
+ /* Wow, this is cool. We just put together a CSS-rule string based on the
+ * font preferences. *This* is what CSS is all about. :)
+ * We also provide a "data: URL" format, to simplify other code.
+ */
+ var css;
+ var fs;
+ var fn;
+
+ if (this.prefs["font.family"] != "default")
+ fn = "font-family: " + this.prefs["font.family"] + ";";
+ else
+ fn = "font-family: inherit;";
+ if (this.prefs["font.size"] != 0)
+ fs = "font-size: " + this.prefs["font.size"] + "pt;";
+ else
+ fs = "font-size: medium;";
+
+ css = ".chatzilla-body { " + fs + fn + " }";
+
+ if (format == "data")
+ return "data:text/css," + encodeURIComponent(css);
+ return css;
+}
+
+client.startMsgGroup =
+CIRCNetwork.prototype.startMsgGroup =
+CIRCChannel.prototype.startMsgGroup =
+CIRCUser.prototype.startMsgGroup =
+CIRCDCCChat.prototype.startMsgGroup =
+CIRCDCCFileTransfer.prototype.startMsgGroup =
+function __startMsgGroup(id, groupMsg, msgtype)
+{
+ // The given ID may not be unique, so append a timestamp to ensure it is.
+ var groupId = id + "-" + Date.now();
+
+ // Add the button to the end of the message.
+ var headerMsg = groupMsg + " " + getMsg(MSG_COLLAPSE_BUTTON,
+ [MSG_COLLAPSE_HIDE,
+ MSG_COLLAPSE_HIDETITLE,
+ groupId]);
+
+ // Show the group header message.
+ client.munger.getRule(".inline-buttons").enabled = true;
+ this.displayHere(headerMsg, msgtype);
+ client.munger.getRule(".inline-buttons").enabled = false;
+
+ // Add the group to a list of active message groups.
+ if (!this.msgGroups)
+ this.msgGroups = [];
+ this.msgGroups.push(groupId);
+
+ // Return the actual ID in case the caller wants to use it later.
+ return groupId;
+}
+
+function startMsgGroup(groupId, headerMsg, msgtype)
+{
+ client.currentObject.startMsgGroup(groupId, headerMsg, msgtype);
+}
+
+client.endMsgGroup =
+CIRCNetwork.prototype.endMsgGroup =
+CIRCChannel.prototype.endMsgGroup =
+CIRCUser.prototype.endMsgGroup =
+CIRCDCCChat.prototype.endMsgGroup =
+CIRCDCCFileTransfer.prototype.endMsgGroup =
+function __endMsgGroup(groupId, message)
+{
+ if (!this.msgGroups)
+ return;
+
+ // Remove the group from the list of active message groups.
+ this.msgGroups.pop();
+ if (this.msgGroups.length == 0)
+ delete this.msgGroups;
+}
+
+function endMsgGroup()
+{
+ client.currentObject.endMsgGroup();
+}
+
+client.display =
+client.displayHere =
+CIRCNetwork.prototype.displayHere =
+CIRCChannel.prototype.displayHere =
+CIRCUser.prototype.displayHere =
+CIRCDCCChat.prototype.displayHere =
+CIRCDCCFileTransfer.prototype.displayHere =
+function __display(message, msgtype, sourceObj, destObj, tags)
+{
+ // We need a message type, assume "INFO".
+ if (!msgtype)
+ msgtype = MT_INFO;
+
+ var msgprefix = "";
+ if (msgtype.indexOf("/") != -1)
+ {
+ var ary = msgtype.match(/^(.*)\/(.*)$/);
+ msgtype = ary[1];
+ msgprefix = ary[2];
+ }
+
+ var blockLevel = false; /* true if this row should be rendered at block
+ * level, (like, if it has a really long nickname
+ * that might disturb the rest of the layout) */
+ var o = getObjectDetails(this); /* get the skinny on |this| */
+
+ // Get the 'me' object, so we can be sure to get the attributes right.
+ var me;
+ if ("me" in this)
+ me = this.me;
+ else if (o.server && "me" in o.server)
+ me = o.server.me;
+
+ /* Allow for matching (but not identical) user objects here. This tends to
+ * happen with bouncers and proxies, when they send channel messages
+ * pretending to be from the user; the sourceObj is a CIRCChanUser
+ * instead of a CIRCUser so doesn't == 'me'.
+ */
+ if (me)
+ {
+ if (sourceObj && (sourceObj.canonicalName == me.canonicalName))
+ sourceObj = me;
+ if (destObj && (destObj.canonicalName == me.canonicalName))
+ destObj = me;
+ }
+
+ // Let callers get away with "ME!" and we have to substitute here.
+ if (sourceObj == "ME!")
+ sourceObj = me;
+ if (destObj == "ME!")
+ destObj = me;
+
+ // Get the TYPE of the source object.
+ var fromType = (sourceObj && sourceObj.TYPE) ? sourceObj.TYPE : "unk";
+ // Is the source a user?
+ var fromUser = (fromType.search(/IRC.*User/) != -1);
+ // Get some sort of "name" for the source.
+ var fromAttr = "";
+ if (sourceObj)
+ {
+ if ("canonicalName" in sourceObj)
+ fromAttr = sourceObj.canonicalName;
+ else if ("name" in sourceObj)
+ fromAttr = sourceObj.name;
+ else
+ fromAttr = sourceObj.viewName;
+ }
+
+ // Get the dest TYPE too...
+ var toType = (destObj) ? destObj.TYPE : "unk";
+ // Is the dest a user?
+ var toUser = (toType.search(/IRC.*User/) != -1);
+ // Get a dest name too...
+ var toAttr = "";
+ if (destObj)
+ {
+ if ("canonicalName" in destObj)
+ toAttr = destObj.canonicalName;
+ else if ("name" in destObj)
+ toAttr = destObj.name;
+ else
+ toAttr = destObj.viewName;
+ }
+
+ // Is the message 'to' or 'from' somewhere other than this view
+ var toOther = ((sourceObj == me) && destObj && (destObj != this));
+ var fromOther = (toUser && (destObj == me) && (sourceObj != this) &&
+ // Need extra check for DCC users:
+ !((this.TYPE == "IRCDCCChat") && (this.user == sourceObj)));
+
+ // Attach "ME!" if appropriate, so motifs can style differently.
+ if ((sourceObj == me) && !toOther)
+ fromAttr = fromAttr + " ME!";
+ if (destObj && destObj == me)
+ toAttr = me.canonicalName + " ME!";
+
+ /* isImportant means to style the messages as important, and flash the
+ * window, getAttention means just flash the window. */
+ var isImportant = false, getAttention = false, isSuperfluous = false;
+ var viewType = this.TYPE;
+ var code;
+ var time;
+ if (tags && ("time" in tags))
+ time = new Date(tags.time);
+ else
+ time = new Date();
+
+ var timeStamp = strftime(this.prefs["timestamps.log"], time);
+
+ // Statusbar text, and the line that gets saved to the log.
+ var statusString;
+ var logStringPfx = timeStamp + " ";
+ var logStrings = new Array();
+
+ if (fromUser)
+ {
+ statusString = getMsg(MSG_FMT_STATUS,
+ [timeStamp,
+ sourceObj.unicodeName + "!" +
+ sourceObj.name + "@" + sourceObj.host]);
+ }
+ else
+ {
+ var name;
+ if (sourceObj)
+ name = sourceObj.viewName;
+ else
+ name = this.viewName;
+
+ statusString = getMsg(MSG_FMT_STATUS,
+ [timeStamp, name]);
+ }
+
+ // The table row, and it's attributes.
+ var msgRow = document.createElementNS(XHTML_NS, "html:tr");
+ msgRow.setAttribute("class", "msg");
+ if (this.msgGroups)
+ msgRow.setAttribute("msg-groups", this.msgGroups.join(', '));
+ msgRow.setAttribute("msg-type", msgtype);
+ msgRow.setAttribute("msg-prefix", msgprefix);
+ msgRow.setAttribute("msg-dest", toAttr);
+ msgRow.setAttribute("dest-type", toType);
+ msgRow.setAttribute("view-type", viewType);
+ msgRow.setAttribute("status-text", statusString);
+ msgRow.setAttribute("timestamp", Number(time));
+ if (fromAttr)
+ {
+ if (fromUser)
+ {
+ msgRow.setAttribute("msg-user", fromAttr);
+ // Set some mode information for channel users
+ if (fromType == 'IRCChanUser')
+ msgRow.setAttribute("msg-user-mode", sourceObj.modes.join(" "));
+ }
+ else
+ {
+ msgRow.setAttribute("msg-source", fromAttr);
+ }
+ }
+ if (toOther)
+ msgRow.setAttribute("to-other", toOther);
+ if (fromOther)
+ msgRow.setAttribute("from-other", fromOther);
+
+ // Timestamp cell.
+ var msgRowTimestamp = document.createElementNS(XHTML_NS, "html:td");
+ msgRowTimestamp.setAttribute("class", "msg-timestamp");
+
+ var canMergeData;
+ var msgRowSource, msgRowType, msgRowData;
+ if (fromUser && msgtype.match(/^(PRIVMSG|ACTION|NOTICE|WALLOPS)$/))
+ {
+ var nick = sourceObj.unicodeName;
+ var decorSt = "";
+ var decorEn = "";
+
+ // Set default decorations.
+ if (msgtype == "ACTION")
+ {
+ decorSt = "* ";
+ }
+ else
+ {
+ decorSt = "<";
+ decorEn = ">";
+ }
+
+ var nickURL;
+ if ((sourceObj != me) && ("getURL" in sourceObj))
+ nickURL = sourceObj.getURL();
+ if (toOther && ("getURL" in destObj))
+ nickURL = destObj.getURL();
+
+ if (sourceObj != me)
+ {
+ // Not from us...
+ if (destObj == me)
+ {
+ // ...but to us. Messages from someone else to us.
+
+ getAttention = true;
+ this.defaultCompletion = "/msg " + nick + " ";
+
+ // If this is a private message, and it's not in a query view,
+ // use *nick* instead of <nick>.
+ if ((msgtype != "ACTION") && (this.TYPE != "IRCUser"))
+ {
+ decorSt = "*";
+ decorEn = "*";
+ }
+ }
+ else
+ {
+ // ...or to us. Messages from someone else to channel or similar.
+
+ if ((typeof message == "string") && me)
+ isImportant = msgIsImportant(message, nick, o.network);
+ else if (message.hasAttribute("isImportant") && me)
+ isImportant = true;
+
+ if (isImportant)
+ {
+ this.defaultCompletion = nick +
+ client.prefs["nickCompleteStr"] + " ";
+ }
+ }
+ }
+ else
+ {
+ // Messages from us, to somewhere other than this view
+ if (toOther)
+ {
+ nick = destObj.unicodeName;
+ decorSt = ">";
+ decorEn = "<";
+ }
+ }
+
+ // Log the nickname in the same format as we'll let the user copy.
+ // If the message has a prefix, show it after a "/".
+ if (msgprefix)
+ logStringPfx += decorSt + nick + "/" + msgprefix + decorEn + " ";
+ else
+ logStringPfx += decorSt + nick + decorEn + " ";
+
+ if (!("lastNickDisplayed" in this) || this.lastNickDisplayed != nick)
+ {
+ this.lastNickDisplayed = nick;
+ this.mark = (("mark" in this) && this.mark == "even") ? "odd" : "even";
+ }
+
+ msgRowSource = document.createElementNS(XHTML_NS, "html:td");
+ msgRowSource.setAttribute("class", "msg-user");
+
+ // Make excessive nicks get shunted.
+ if (nick && (nick.length > client.MAX_NICK_DISPLAY))
+ blockLevel = true;
+
+ if (decorSt)
+ msgRowSource.appendChild(newInlineText(decorSt, "chatzilla-decor"));
+ if (nickURL)
+ {
+ var nick_anchor = document.createElementNS(XHTML_NS, "html:a");
+ nick_anchor.setAttribute("class", "chatzilla-link");
+ nick_anchor.setAttribute("href", nickURL);
+ nick_anchor.appendChild(newInlineText(nick));
+ msgRowSource.appendChild(nick_anchor);
+ }
+ else
+ {
+ msgRowSource.appendChild(newInlineText(nick));
+ }
+ if (msgprefix)
+ {
+ /* We don't style the "/" with chatzilla-decor because the one
+ * thing we don't want is it disappearing!
+ */
+ msgRowSource.appendChild(newInlineText("/", ""));
+ msgRowSource.appendChild(newInlineText(msgprefix,
+ "chatzilla-prefix"));
+ }
+ if (decorEn)
+ msgRowSource.appendChild(newInlineText(decorEn, "chatzilla-decor"));
+ canMergeData = this.prefs["collapseMsgs"];
+ }
+ else if (msgprefix)
+ {
+ decorSt = "<";
+ decorEn = ">";
+
+ logStringPfx += decorSt + "/" + msgprefix + decorEn + " ";
+
+ msgRowSource = document.createElementNS(XHTML_NS, "html:td");
+ msgRowSource.setAttribute("class", "msg-user");
+
+ msgRowSource.appendChild(newInlineText(decorSt, "chatzilla-decor"));
+ msgRowSource.appendChild(newInlineText("/", ""));
+ msgRowSource.appendChild(newInlineText(msgprefix, "chatzilla-prefix"));
+ msgRowSource.appendChild(newInlineText(decorEn, "chatzilla-decor"));
+ canMergeData = this.prefs["collapseMsgs"];
+ }
+ else
+ {
+ isSuperfluous = true;
+ if (!client.debugHook.enabled && msgtype in client.responseCodeMap)
+ {
+ code = client.responseCodeMap[msgtype];
+ }
+ else
+ {
+ if (!client.debugHook.enabled && client.HIDE_CODES)
+ code = client.DEFAULT_RESPONSE_CODE;
+ else
+ code = "[" + msgtype + "]";
+ }
+
+ /* Display the message code */
+ msgRowType = document.createElementNS(XHTML_NS, "html:td");
+ msgRowType.setAttribute("class", "msg-type");
+
+ msgRowType.appendChild(newInlineText(code));
+ logStringPfx += code + " ";
+ }
+
+ if (message)
+ {
+ msgRowData = document.createElementNS(XHTML_NS, "html:td");
+ msgRowData.setAttribute("class", "msg-data");
+
+ var tmpMsgs = message;
+ if (typeof message == "string")
+ {
+ msgRowData.appendChild(stringToMsg(message, this));
+ }
+ else
+ {
+ msgRowData.appendChild(message);
+ tmpMsgs = tmpMsgs.innerHTML.replace(/<[^<]*>/g, "");
+ }
+ tmpMsgs = tmpMsgs.split(/\r?\n/);
+ for (var l = 0; l < tmpMsgs.length; l++)
+ logStrings[l] = logStringPfx + tmpMsgs[l];
+ }
+
+ if ("mark" in this)
+ msgRow.setAttribute("mark", this.mark);
+
+ if (isImportant)
+ {
+ if ("importantMessages" in this)
+ {
+ var importantId = "important" + (this.importantMessages++);
+ msgRow.setAttribute("id", importantId);
+ }
+ msgRow.setAttribute("important", "true");
+ msgRow.setAttribute("aria-live", "assertive");
+ }
+
+ // Timestamps first...
+ msgRow.appendChild(msgRowTimestamp);
+ // Now do the rest of the row, after block-level stuff.
+ if (msgRowSource)
+ msgRow.appendChild(msgRowSource);
+ else
+ msgRow.appendChild(msgRowType);
+ if (msgRowData)
+ msgRow.appendChild(msgRowData);
+ updateTimestampFor(this, msgRow);
+
+ if (blockLevel)
+ {
+ /* putting a div here crashes mozilla, so fake it with nested tables
+ * for now */
+ var tr = document.createElementNS(XHTML_NS, "html:tr");
+ tr.setAttribute ("class", "msg-nested-tr");
+ var td = document.createElementNS(XHTML_NS, "html:td");
+ td.setAttribute ("class", "msg-nested-td");
+ td.setAttribute ("colspan", "3");
+
+ tr.appendChild(td);
+ var table = document.createElementNS(XHTML_NS, "html:table");
+ table.setAttribute ("class", "msg-nested-table");
+ table.setAttribute("role", "presentation");
+
+ td.appendChild (table);
+ var tbody = document.createElementNS(XHTML_NS, "html:tbody");
+
+ tbody.appendChild(msgRow);
+ table.appendChild(tbody);
+ msgRow = tr;
+ }
+
+ // Actually add the item.
+ addHistory (this, msgRow, canMergeData);
+
+ // Update attention states...
+ if (isImportant || getAttention)
+ {
+ setTabState(this, "attention");
+ if (client.prefs["notify.aggressive"])
+ window.getAttention();
+ }
+ else
+ {
+ if (isSuperfluous)
+ {
+ setTabState(this, "superfluous");
+ }
+ else
+ {
+ setTabState(this, "activity");
+ }
+ }
+
+ // Copy Important Messages [to network view].
+ if (isImportant && client.prefs["copyMessages"] && (o.network != this))
+ {
+ if (importantId)
+ {
+ // Create the linked inline button
+ var msgspan = document.createElementNS(XHTML_NS, "html:span");
+ msgspan.setAttribute("isImportant", "true");
+
+ var cmd = "jump-to-anchor " + importantId + " " + this.unicodeName;
+ var prefix = getMsg(MSG_JUMPTO_BUTTON, [this.unicodeName, cmd]);
+
+ // Munge prefix as a button
+ client.munger.getRule(".inline-buttons").enabled = true;
+ client.munger.munge(prefix + " ", msgspan, o);
+
+ // Munge rest of message normally
+ client.munger.getRule(".inline-buttons").enabled = false;
+ client.munger.munge(message, msgspan, o);
+
+ o.network.displayHere(msgspan, msgtype, sourceObj, destObj);
+ }
+ else
+ {
+ o.network.displayHere(message, msgtype, sourceObj, destObj);
+ }
+ }
+
+ // Log file time!
+ if (this.prefs["log"])
+ {
+ if (!this.logFile)
+ client.openLogFile(this);
+
+ try
+ {
+ var LE = client.lineEnd;
+ for (var l = 0; l < logStrings.length; l++)
+ this.logFile.write(fromUnicode(logStrings[l] + LE, "utf-8"));
+ }
+ catch (ex)
+ {
+ // Stop logging before showing any messages!
+ this.prefs["log"] = false;
+ dd("Log file write error: " + formatException(ex));
+ this.displayHere(getMsg(MSG_LOGFILE_WRITE_ERROR, getLogPath(this)),
+ "ERROR");
+ }
+ }
+
+ /* We want to show alerts if they're from a non-current view (optional),
+ * or we don't have focus at all.
+ */
+ if (client.prefs["alert.globalEnabled"]
+ && this.prefs["alert.enabled"] && client.alert &&
+ (!window.isFocused
+ || (!client.prefs['alert.nonFocusedOnly'] &&
+ !("currentObject" in client && client.currentObject == this)
+ )
+ )
+ )
+ {
+ if (isImportant)
+ {
+ showEventAlerts(this.TYPE, "stalk", message, nick, o, this, msgtype);
+ }
+ else if (isSuperfluous)
+ {
+ showEventAlerts(this.TYPE, "event", message, nick, o, this, msgtype);
+ }
+ else
+ {
+ showEventAlerts(this.TYPE, "chat" , message, nick, o, this, msgtype);
+ }
+ }
+
+}
+
+function addHistory (source, obj, mergeData)
+{
+ if (!("messages" in source) || (source.messages == null))
+ createMessages(source);
+
+ var tbody = source.messages.firstChild;
+ var appendTo = tbody;
+
+ var needScroll = false;
+
+ if (mergeData)
+ {
+ var inobj = obj;
+ // This gives us the non-nested row when there is nesting.
+ if (inobj.className == "msg-nested-tr")
+ inobj = inobj.firstChild.firstChild.firstChild.firstChild;
+
+ var thisUserCol = inobj.firstChild;
+ while (thisUserCol && !thisUserCol.className.match(/^(msg-user|msg-type)$/))
+ thisUserCol = thisUserCol.nextSibling;
+
+ var thisMessageCol = inobj.firstChild;
+ while (thisMessageCol && !(thisMessageCol.className == "msg-data"))
+ thisMessageCol = thisMessageCol.nextSibling;
+
+ let columnInfo = findPreviousColumnInfo(source.messages);
+ let nickColumns = columnInfo.nickColumns;
+ let rowExtents = columnInfo.extents;
+ let nickColumnCount = nickColumns.length;
+
+ let lastRowSpan = 0;
+ let sameNick = false;
+ let samePrefix = false;
+ let sameDest = false;
+ let haveSameType = false;
+ let isAction = false;
+ let collapseActions;
+ let needSameType = false;
+ // 1 or messages, check for doubles.
+ if (nickColumnCount > 0)
+ {
+ var lastRow = nickColumns[nickColumnCount - 1].parentNode;
+ // What was the span last time?
+ lastRowSpan = Number(nickColumns[0].getAttribute("rowspan"));
+ // Are we the same user as last time?
+ sameNick = (lastRow.getAttribute("msg-user") ==
+ inobj.getAttribute("msg-user"));
+ // Do we have the same prefix as last time?
+ samePrefix = (lastRow.getAttribute("msg-prefix") ==
+ inobj.getAttribute("msg-prefix"));
+ // Do we have the same destination as last time?
+ sameDest = (lastRow.getAttribute("msg-dest") ==
+ inobj.getAttribute("msg-dest"));
+ // Is this message the same type as the last one?
+ haveSameType = (lastRow.getAttribute("msg-type") ==
+ inobj.getAttribute("msg-type"));
+ // Is either of the messages an action? We may not want to collapse
+ // depending on the collapseActions pref
+ isAction = ((inobj.getAttribute("msg-type") == "ACTION") ||
+ (lastRow.getAttribute("msg-type") == "ACTION"));
+ // Do we collapse actions?
+ collapseActions = source.prefs["collapseActions"];
+
+ // Does the motif collapse everything, regardless of type?
+ // NOTE: the collapseActions pref can override this for actions
+ needSameType = !(("motifSettings" in source) &&
+ source.motifSettings &&
+ ("collapsemore" in source.motifSettings));
+ }
+
+ if (sameNick && samePrefix && sameDest &&
+ (haveSameType || !needSameType) &&
+ (!isAction || collapseActions))
+ {
+ obj = inobj;
+ if (columnInfo.nested)
+ appendTo = source.messages.firstChild.lastChild.firstChild.firstChild.firstChild;
+
+ if (obj.getAttribute("important"))
+ {
+ nickColumns[nickColumnCount - 1].setAttribute("important",
+ true);
+ }
+
+ // Remove nickname column from new row.
+ obj.removeChild(thisUserCol);
+
+ // Expand previous grouping's nickname cell(s) to fill-in the gap.
+ for (var i = 0; i < nickColumns.length; ++i)
+ nickColumns[i].setAttribute("rowspan", rowExtents.length + 1);
+ }
+ }
+
+ if ("frame" in source)
+ needScroll = checkScroll(source.frame);
+ if (obj)
+ appendTo.appendChild(client.adoptNode(obj, appendTo.ownerDocument));
+
+ if (source.MAX_MESSAGES)
+ {
+ if (typeof source.messageCount != "number")
+ source.messageCount = 1;
+ else
+ source.messageCount++;
+
+ if (source.messageCount > source.MAX_MESSAGES)
+ removeExcessMessages(source);
+ }
+
+ if (needScroll)
+ scrollDown(source.frame, true);
+}
+
+function removeExcessMessages(source)
+{
+ var window = getContentWindow(source.frame);
+ var rows = source.messages.rows;
+ var lastItemOffset = rows[rows.length - 1].offsetTop;
+ var tbody = source.messages.firstChild;
+ while (source.messageCount > source.MAX_MESSAGES)
+ {
+ if (tbody.firstChild.className == "msg-nested-tr")
+ {
+ var table = tbody.firstChild.firstChild.firstChild;
+ var toBeRemoved = source.messageCount - source.MAX_MESSAGES;
+ // If we can remove the entire table, do that...
+ if (table.rows.length <= toBeRemoved)
+ {
+ tbody.removeChild(tbody.firstChild);
+ source.messageCount -= table.rows.length;
+ table = null; // Don't hang onto this.
+ continue;
+ }
+ // Otherwise, remove rows from this table instead:
+ tbody = table.firstChild;
+ }
+ var nextLastNode = tbody.firstChild.nextSibling;
+ // If the next node has only 2 childNodes,
+ // assume we're dealing with collapsed msgs,
+ // and move the nickname element:
+ if (nextLastNode.childNodes.length == 2)
+ {
+ var nickElem = tbody.firstChild.childNodes[1];
+ var rowspan = nickElem.getAttribute("rowspan") - 1;
+ tbody.firstChild.removeChild(nickElem);
+ nickElem.setAttribute("rowspan", rowspan);
+ nextLastNode.insertBefore(nickElem, nextLastNode.lastChild);
+ }
+ tbody.removeChild(tbody.firstChild);
+ --source.messageCount;
+ }
+ var oldestItem = rows[0];
+ if (oldestItem.className == "msg-nested-tr")
+ oldestItem = rows[0].firstChild.firstChild.firstChild.firstChild;
+ updateTimestampFor(source, oldestItem, true);
+
+ // Scroll by as much as the lowest item has moved up:
+ lastItemOffset -= rows[rows.length - 1].offsetTop;
+ var y = window.pageYOffset;
+ if (!checkScroll(source.frame) && (y > lastItemOffset))
+ window.scrollBy(0, -lastItemOffset);
+}
+
+function findPreviousColumnInfo(table)
+{
+ // All the rows in the grouping (for merged rows).
+ var extents = new Array();
+ // Get the last row in the table.
+ var tr = table.firstChild.lastChild;
+ // Bail if there's no rows.
+ if (!tr)
+ return {extents: [], nickColumns: [], nested: false};
+ // Get message type.
+ if (tr.className == "msg-nested-tr")
+ {
+ var rv = findPreviousColumnInfo(tr.firstChild.firstChild);
+ rv.nested = true;
+ return rv;
+ }
+ // Now get the read one...
+ var className = (tr && tr.childNodes[1]) ? tr.childNodes[1].getAttribute("class") : "";
+ // Keep going up rows until you find the first in a group.
+ // This will go up until it hits the top of a multiline/merged block.
+ while (tr && tr.childNodes[1] && className.search(/msg-user|msg-type/) == -1)
+ {
+ extents.push(tr);
+ tr = tr.previousSibling;
+ if (tr && tr.childNodes[1])
+ className = tr.childNodes[1].getAttribute("class");
+ }
+
+ // If we ran out of rows, or it's not a talking line, we're outta here.
+ if (!tr || className != "msg-user")
+ return {extents: [], nickColumns: [], nested: false};
+
+ extents.push(tr);
+
+ // Time to collect the nick data...
+ var nickCol = tr.firstChild;
+ // All the cells that contain nickname info.
+ var nickCols = new Array();
+ while (nickCol)
+ {
+ // Just collect nickname column cells.
+ if (nickCol.getAttribute("class") == "msg-user")
+ nickCols.push(nickCol);
+ nickCol = nickCol.nextSibling;
+ }
+
+ // And we're done.
+ return {extents: extents, nickColumns: nickCols, nested: false};
+}
+
+function getLogPath(obj)
+{
+ // If we're logging, return the currently-used URL.
+ if (obj.logFile)
+ return getURLSpecFromFile(obj.logFile.path);
+ // If not, return the ideal URL.
+ return getURLSpecFromFile(obj.prefs["logFileName"]);
+}
+
+client.getConnectionCount =
+function cli_gccount ()
+{
+ var count = 0;
+
+ for (var n in client.networks)
+ {
+ if (client.networks[n].isConnected())
+ ++count;
+ }
+
+ return count;
+}
+
+client.quit =
+function cli_quit (reason)
+{
+ var net, netReason;
+ for (var n in client.networks)
+ {
+ net = client.networks[n];
+ if (net.isConnected())
+ {
+ netReason = (reason ? reason : net.prefs["defaultQuitMsg"]);
+ netReason = (netReason ? netReason : client.userAgent);
+ net.quit(netReason);
+ }
+ }
+}
+
+client.wantToQuit =
+function cli_wantToQuit(reason, deliberate)
+{
+
+ var close = true;
+ if (client.prefs["warnOnClose"] && !deliberate)
+ {
+ const buttons = [MSG_QUIT_ANYWAY, MSG_DONT_QUIT];
+ var checkState = { value: true };
+ var rv = confirmEx(MSG_CONFIRM_QUIT, buttons, 0, MSG_WARN_ON_EXIT,
+ checkState);
+ close = (rv == 0);
+ client.prefs["warnOnClose"] = checkState.value;
+ }
+
+ if (close)
+ {
+ client.userClose = true;
+ display(MSG_CLOSING);
+ client.quit(reason);
+ }
+}
+
+client.promptToSaveLogin =
+function cli_promptToSaveLogin(url, type, username, password)
+{
+ var name = "";
+ switch (type)
+ {
+ case "nick":
+ case "oper":
+ case "sasl":
+ name = username;
+ break;
+ case "serv":
+ case "chan":
+ name = url;
+ username = "*";
+ break;
+ default:
+ display(getMsg(MSG_LOGIN_ERR_UNKNOWN_TYPE, type), MT_ERROR);
+ return;
+ }
+
+ const buttons = [MSG_LOGIN_SAVE, MSG_LOGIN_DONT];
+ var checkState = { value: true };
+ var rv = confirmEx(getMsg(MSG_LOGIN_CONFIRM, name), buttons, 0,
+ MSG_LOGIN_PROMPT, checkState);
+ if (rv == 0)
+ {
+ client.prefs["login.promptToSave"] = checkState.value;
+
+ var updated = addOrUpdateLogin(url, type, username, password);
+ if (updated) {
+ display(getMsg(MSG_LOGIN_UPDATED, name), MT_INFO);
+ } else {
+ display(getMsg(MSG_LOGIN_ADDED, name), MT_INFO);
+ }
+ }
+}
+
+client.tryToGetLogin =
+function cli_tryToGetLogin(url, type, username, existing, needpass,
+ promptstring)
+{
+ // Password is optional. If it is not given, we look for a saved password
+ // first. If there isn't one, we potentially use a safe prompt.
+ var info = getLogin(url, type, username);
+ var stored = (info && info.password) ? info.password : "";
+ var promptToSave = false;
+ if (!existing && stored) {
+ existing = stored;
+ } else if (!existing && needpass) {
+ existing = promptPassword(promptstring, "");
+ if (existing)
+ promptToSave = true;
+ } else if (existing && stored != existing) {
+ promptToSave = true;
+ }
+
+ if (promptToSave && client.prefs["login.promptToSave"])
+ client.promptToSaveLogin(url, type, username, existing);
+
+ return existing;
+}
+
+/* gets a tab-complete match for the line of text specified by |line|.
+ * wordStart is the position within |line| that starts the word being matched,
+ * wordEnd marks the end position. |cursorPos| marks the position of the caret
+ * in the textbox.
+ */
+client.performTabMatch =
+function gettabmatch_usr (line, wordStart, wordEnd, word, cursorPos)
+{
+ if (wordStart != 0 || line[0] != client.COMMAND_CHAR)
+ return null;
+
+ var matches = client.commandManager.listNames(word.substr(1), CMD_CONSOLE);
+ if (matches.length == 1 && wordEnd == line.length)
+ {
+ matches[0] = client.COMMAND_CHAR + matches[0] + " ";
+ }
+ else
+ {
+ for (var i in matches)
+ matches[i] = client.COMMAND_CHAR + matches[i];
+ }
+
+ return matches;
+}
+
+client.openLogFile =
+function cli_startlog(view, showMessage)
+{
+ function getNextLogFileDate()
+ {
+ var d = new Date();
+ d.setMilliseconds(0);
+ d.setSeconds(0);
+ d.setMinutes(0);
+ switch (view.smallestLogInterval)
+ {
+ case "h":
+ return d.setHours(d.getHours() + 1);
+ case "d":
+ d.setHours(0);
+ return d.setDate(d.getDate() + 1);
+ case "m":
+ d.setHours(0);
+ d.setDate(1);
+ return d.setMonth(d.getMonth() + 1);
+ case "y":
+ d.setHours(0);
+ d.setDate(1);
+ d.setMonth(0);
+ return d.setFullYear(d.getFullYear() + 1);
+ }
+ //XXXhack: This should work...
+ return Infinity;
+ };
+
+ const NORMAL_FILE_TYPE = Components.interfaces.nsIFile.NORMAL_FILE_TYPE;
+
+ try
+ {
+ var file = new LocalFile(view.prefs["logFileName"]);
+ if (!file.localFile.exists())
+ {
+ // futils.umask may be 0022. Result is 0644.
+ file.localFile.create(NORMAL_FILE_TYPE, 0o666 & ~futils.umask);
+ }
+ view.logFile = fopen(file.localFile, ">>");
+ // If we're here, it's safe to say when we should re-open:
+ view.nextLogFileDate = getNextLogFileDate();
+ }
+ catch (ex)
+ {
+ view.prefs["log"] = false;
+ dd("Log file open error: " + formatException(ex));
+ view.displayHere(getMsg(MSG_LOGFILE_ERROR, getLogPath(view)), MT_ERROR);
+ return;
+ }
+
+ if (showMessage)
+ view.displayHere(getMsg(MSG_LOGFILE_OPENED, getLogPath(view)));
+}
+
+client.closeLogFile =
+function cli_stoplog(view, showMessage)
+{
+ if (showMessage)
+ view.displayHere(getMsg(MSG_LOGFILE_CLOSING, getLogPath(view)));
+
+ if (view.logFile)
+ {
+ view.logFile.close();
+ view.logFile = null;
+ }
+}
+
+function checkLogFiles()
+{
+ // For every view that has a logfile, check if we need a different file
+ // based on the current date and the logfile preference. We close the
+ // current logfile, and display will open the new one based on the pref
+ // when it's needed.
+
+ var d = new Date();
+ for (var n in client.networks)
+ {
+ var net = client.networks[n];
+ if (net.logFile && (d > net.nextLogFileDate))
+ client.closeLogFile(net);
+ if (("primServ" in net) && net.primServ && ("channels" in net.primServ))
+ {
+ for (var c in net.primServ.channels)
+ {
+ var chan = net.primServ.channels[c];
+ if (chan.logFile && (d > chan.nextLogFileDate))
+ client.closeLogFile(chan);
+ }
+ }
+ if ("users" in net)
+ {
+ for (var u in net.users)
+ {
+ var user = net.users[u];
+ if (user.logFile && (d > user.nextLogFileDate))
+ client.closeLogFile(user);
+ }
+ }
+ }
+
+ for (var dc in client.dcc.chats)
+ {
+ var dccChat = client.dcc.chats[dc];
+ if (dccChat.logFile && (d > dccChat.nextLogFileDate))
+ client.closeLogFile(dccChat);
+ }
+ for (var df in client.dcc.files)
+ {
+ var dccFile = client.dcc.files[df];
+ if (dccFile.logFile && (d > dccFile.nextLogFileDate))
+ client.closeLogFile(dccFile);
+ }
+
+ // Don't forget about the client tab:
+ if (client.logFile && (d > client.nextLogFileDate))
+ client.closeLogFile(client);
+
+ /* We need to calculate the correct time for the next check. This is
+ * attempting to hit 2 seconds past the hour. We need the timezone offset
+ * here for when it is not a whole number of hours from UTC.
+ */
+ var shiftedDate = d.getTime() + d.getTimezoneOffset() * 60000;
+ setTimeout(checkLogFiles, 3602000 - (shiftedDate % 3600000));
+}
+
+CIRCChannel.prototype.getLCFunction =
+CIRCNetwork.prototype.getLCFunction =
+CIRCUser.prototype.getLCFunction =
+CIRCDCCChat.prototype.getLCFunction =
+CIRCDCCFileTransfer.prototype.getLCFunction =
+function getlcfn()
+{
+ var details = getObjectDetails(this);
+ var lcFn;
+
+ if (details.server)
+ {
+ lcFn = function(text)
+ {
+ return details.server.toLowerCase(text);
+ }
+ }
+
+ return lcFn;
+}
+
+CIRCChannel.prototype.performTabMatch =
+CIRCNetwork.prototype.performTabMatch =
+CIRCUser.prototype.performTabMatch =
+CIRCDCCChat.prototype.performTabMatch =
+CIRCDCCFileTransfer.prototype.performTabMatch =
+function gettabmatch_other (line, wordStart, wordEnd, word, cursorpos, lcFn)
+{
+ if (wordStart == 0 && line[0] == client.COMMAND_CHAR)
+ {
+ return client.performTabMatch(line, wordStart, wordEnd, word,
+ cursorpos);
+ }
+
+ var matchList = new Array();
+ var users;
+ var channels;
+ var userIndex = -1;
+
+ var details = getObjectDetails(this);
+
+ if (details.channel && word == details.channel.unicodeName[0])
+ {
+ /* When we have #<tab>, we just want the current channel,
+ if possible. */
+ matchList.push(details.channel.unicodeName);
+ }
+ else
+ {
+ /* Ok, not #<tab> or no current channel, so get the full list. */
+
+ if (details.channel)
+ users = details.channel.users;
+
+ if (details.server)
+ {
+ channels = details.server.channels;
+ for (var c in channels)
+ matchList.push(channels[c].unicodeName);
+ if (!users)
+ users = details.server.users;
+ }
+
+ if (users)
+ {
+ userIndex = matchList.length;
+ for (var n in users)
+ matchList.push(users[n].unicodeName);
+ }
+ }
+
+ var matches = matchEntry(word, matchList, lcFn);
+
+ var list = new Array();
+ for (var i = 0; i < matches.length; i++)
+ list.push(matchList[matches[i]]);
+
+ if (list.length == 1)
+ {
+ if (users && (userIndex >= 0) && (matches[0] >= userIndex))
+ {
+ if (wordStart == 0)
+ list[0] += client.prefs["nickCompleteStr"];
+ }
+
+ if (wordEnd == line.length)
+ {
+ /* add a space if the word is at the end of the line. */
+ list[0] += " ";
+ }
+ }
+
+ return list;
+}
+
+/*
+ * 290miliseconds for 1st derive is allowing about 3-4 events per
+ * second. 200ms for 2nd derivative allows max 200ms difference of
+ * frequency. This means when the flood is massive, this value is
+ * very closed to zero. But runtime issues should cause some delay
+ * in the core js, so zero value is not too good. We need increase
+ * this with a small, to make more strict. And when flood is done,
+ * we need detect it - based on arithmetic medium. When doesn't happen
+ * anything for a long time, perhaps for 2seconds the
+ * value - based on last 10 events - the 2nd value goes
+ * over 200ms average, so event should start again.
+ */
+
+function FloodProtector (density, dispersion)
+{
+ this.lastHit = Number(new Date());
+
+ if (density)
+ this.floodDensity = density;
+
+ if (dispersion)
+ this.floodDispersion = dispersion;
+}
+
+FloodProtector.prototype.requestedTotal = 0;
+FloodProtector.prototype.acceptedTotal = 0;
+FloodProtector.prototype.firedTotal = 0;
+FloodProtector.prototype.lastHit = 0;
+FloodProtector.prototype.derivative1 = 100;
+FloodProtector.prototype.derivative1Count = 100;
+FloodProtector.prototype.derivative2 = 0;
+FloodProtector.prototype.derivative2Count = 0;
+FloodProtector.prototype.floodDensity = 290;
+FloodProtector.prototype.floodDispersion = 200;
+
+FloodProtector.prototype.request = function ()
+{
+ this.requestedTotal++;
+ var current = Number(new Date());
+ var oldDerivative1 = this.derivative1;
+ this.derivative1 = current - this.lastHit;
+ this.derivative1Count = ((this.derivative1Count * 9) + this.derivative1) / 10;
+ this.derivative2 = Math.abs(this.derivative1 - oldDerivative1);
+ this.derivative2Count = ((this.derivative2Count * 9) + this.derivative2) / 10;
+ this.lastHit = current;
+}
+
+FloodProtector.prototype.accept = function ()
+{
+ this.acceptedTotal++;
+}
+
+FloodProtector.prototype.fire = function ()
+{
+ // There is no activity for 10 seconds - flood is possibly done.
+ // No need more recall. In other way the first normal activity
+ // overwrites it automatically earlier, if nessesary.
+ if ((Number(new Date()) - this.lastHit) > 10000)
+ return false;
+
+ // The activity is not too frequent or not massive so should not be fire.
+ if ((this.derivative1Count > this.floodDensity)
+ || (this.derivative2Count > this.floodDispersion))
+ {
+ return false;
+ }
+
+ this.firedTotal++;
+ return true;
+
+}
+
+
+function toasterPopupOverlapDelayReset (eventType)
+{
+ // it smells like a flood attack so rather wait more...
+ if (client.alert.floodProtector.fire())
+ {
+ setTimeout(
+ toasterPopupOverlapDelayReset,
+ client.prefs['alert.overlapDelay'], eventType);
+ }
+ else
+ {
+ delete client.alert.alertList[eventType];
+ }
+}
+
+var alertClickerObserver = {
+ observe: function(subject, topic, data)
+ {
+ if (topic == "alertclickcallback")
+ {
+ var tb = document.getElementById(data);
+ if (tb && tb.view)
+ {
+ tb.view.dispatch("set-current-view", {view: tb.view});
+ window.focus();
+ }
+ }
+ },
+
+ // Gecko 1.7.* rulez
+ onAlertClickCallback: function(data)
+ {
+ var tb = document.getElementById(data);
+ if (tb && tb.view)
+ {
+ tb.view.dispatch("set-current-view", {view: tb.view});
+ window.focus();
+ }
+ },
+
+ onAlertFinished: function(data)
+ {
+ }
+};
+
+
+// Show the alert for a particular event on a type of object.
+function showEventAlerts (type, event, message, nick, o, thisp, msgtype)
+{
+
+ // Converts .TYPE values into the event object names.
+ // IRCChannel => channel, IRCUser => user, etc.
+ type = type.replace(/^IRC/i,'').toLowerCase();
+
+ var source = type;
+ // DCC Chat sessions should act just like user views.
+ if (type == "dccchat") type = "user";
+
+ var ev = type + "." + event;
+ if (!(("alert."+ev) in thisp.prefs))
+ return;
+ if (!thisp.prefs["alert."+ev])
+ return;
+
+ client.alert.floodProtector.request();
+ if (ev in client.alert.alertList)
+ return;
+
+ client.alert.floodProtector.accept();
+ if(client.prefs['alert.overlapDelay'] > 0)
+ {
+ client.alert.alertList[ev] = true;
+ setTimeout(toasterPopupOverlapDelayReset,
+ client.prefs['alert.overlapDelay'], ev);
+ }
+
+ var clickable = client.prefs['alert.clickable'];
+ var tabId = clickable ? getTabForObject(thisp,false).id : "";
+ var listener = clickable ? alertClickerObserver : null;
+
+ message = removeColorCodes(message);
+ if (nick)
+ {
+ if (msgtype == "ACTION")
+ {
+ message = "* " + nick + " " + message;
+ }
+ else
+ {
+ message = "<" + nick + "> " + message;
+ }
+ }
+
+ if ((source == "channel") && o.channel)
+ {
+ source = o.channel.viewName;
+ }
+ else if ((source == "user") && o.network)
+ {
+ source = o.network.viewName;
+ }
+
+ // We can't be sure if it is a macOS and Growl is now turned off or not
+ try
+ {
+ client.alert.service.showAlertNotification(
+ "chrome://chatzilla/skin/images/logo.png",
+ "ChatZilla - " + source + " - " + event,
+ message, clickable, tabId, listener);
+ }
+ catch(ex)
+ {
+ // yup. it is probably a MAC or NsIAlertsService is not initialized
+ }
+}
diff --git a/comm/suite/chatzilla/xul/lib/munger.js b/comm/suite/chatzilla/xul/lib/munger.js
new file mode 100644
index 0000000000..cdbda15c54
--- /dev/null
+++ b/comm/suite/chatzilla/xul/lib/munger.js
@@ -0,0 +1,245 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+/* Constructs a new munger entry, using a regexp or lambda match function, and
+ * a class name (to be applied by the munger itself) or lambda replace
+ * function, and the default enabled state and a start priority (used if two
+ * rules match at the same index), as well as a default tag (when the munger
+ * adds it based on the class name) name.
+ *
+ * Regular Expressions for matching should ensure that the first capturing
+ * group is the one that contains the matched text. Non-capturing groups, of
+ * zero-width or otherwise can be used before and after, to ensure the right
+ * things are matched (e.g. to ensure whitespace before something).
+ *
+ * Note that for RegExp matching, the munger will search for the matched text
+ * (from the first capturing group) from the leftmost point of the entire
+ * match. This means that if the text that matched the first group occurs in
+ * any part of the match before the group, the munger will apply to the wrong
+ * bit. This is not usually a problem, but if it is you should use a
+ * lambdaMatch function and be sure to return the new style return value,
+ * which specifically indicates the start.
+ *
+ * The lambda match and lambda replace functions have this signature:
+ * lambdaMatch(text, containerTag, data, mungerEntry)
+ * lambdaReplace(text, containerTag, data, mungerEntry)
+ * - text is the entire text to find a match in/that has matched
+ * - containerTag is the element containing the text (not useful?)
+ * - data is a generic object containing properties kept throughout
+ * - mungerEntry is the CMungerEntry object for the munger itself
+ *
+ * The lambdaReplace function is expected to do everything needed to put
+ * |text| into |containerTab| ready for display.
+ *
+ * The return value for lambda match functions should be either:
+ * - (old style) just the text that matched
+ * (the munger will search for this text, and uses the first match)
+ * - (new style) an object with properties:
+ * - start (start index, 0 = first character)
+ * - text (matched text)
+ * (note that |text| must start at index |start|)
+ *
+ * The return value for lambda replace functions are not used.
+ *
+ */
+
+function CMungerEntry(name, regex, className, priority, startPriority,
+ enable, tagName)
+{
+ this.name = name;
+ if (name[0] != ".")
+ this.description = getMsg("munger." + name, null, null);
+ this.enabled = (typeof enable == "undefined" ? true : enable);
+ this.enabledDefault = this.enabled;
+ this.startPriority = (startPriority) ? startPriority : 0;
+ this.priority = priority;
+ this.tagName = (tagName) ? tagName : "html:span";
+
+ if (isinstance(regex, RegExp))
+ this.regex = regex;
+ else
+ this.lambdaMatch = regex;
+
+ if (typeof className == "function")
+ this.lambdaReplace = className;
+ else
+ this.className = className;
+}
+
+function CMunger(textMunger)
+{
+ this.entries = new Array();
+ this.tagName = "html:span";
+ this.enabled = true;
+ if (textMunger)
+ this.insertPlainText = textMunger;
+}
+
+CMunger.prototype.enabled = true;
+CMunger.prototype.insertPlainText = insertText;
+
+CMunger.prototype.getRule =
+function mng_getrule(name)
+{
+ for (var p in this.entries)
+ {
+ if (isinstance(this.entries[p], Object))
+ {
+ if (name in this.entries[p])
+ return this.entries[p][name];
+ }
+ }
+ return null;
+}
+
+CMunger.prototype.addRule =
+function mng_addrule(name, regex, className, priority, startPriority, enable)
+{
+ if (typeof this.entries[priority] != "object")
+ this.entries[priority] = new Object();
+ var entry = new CMungerEntry(name, regex, className, priority,
+ startPriority, enable);
+ this.entries[priority][name] = entry;
+}
+
+CMunger.prototype.delRule =
+function mng_delrule(name)
+{
+ for (var i in this.entries)
+ {
+ if (typeof this.entries[i] == "object")
+ {
+ if (name in this.entries[i])
+ delete this.entries[i][name];
+ }
+ }
+}
+
+CMunger.prototype.munge =
+function mng_munge(text, containerTag, data)
+{
+
+ if (!containerTag)
+ containerTag = document.createElementNS(XHTML_NS, this.tagName);
+
+ // Starting from the top, for each valid priority, check all the rules,
+ // return as soon as something matches.
+ if (this.enabled)
+ {
+ for (var i = this.entries.length - 1; i >= 0; i--)
+ {
+ if (i in this.entries)
+ {
+ if (this.mungePriority(i, text, containerTag, data))
+ return containerTag;
+ }
+ }
+ }
+
+ // If nothing matched, we don't have to do anything,
+ // just insert text (if any).
+ if (text)
+ this.insertPlainText(text, containerTag, data);
+ return containerTag;
+}
+
+CMunger.prototype.mungePriority =
+function mng_mungePriority(priority, text, containerTag, data)
+{
+ var matches = new Object();
+ var entry;
+ // Find all the matches in this priority
+ for (entry in this.entries[priority])
+ {
+ var munger = this.entries[priority][entry];
+ if (!munger.enabled)
+ continue;
+
+ var match = null;
+ if (typeof munger.lambdaMatch == "function")
+ {
+ var rval = munger.lambdaMatch(text, containerTag, data, munger);
+ if (typeof rval == "string")
+ match = { start: text.indexOf(rval), text: rval };
+ else if (typeof rval == "object")
+ match = rval;
+ }
+ else
+ {
+ var ary = text.match(munger.regex);
+ if ((ary != null) && (ary[1]))
+ match = { start: text.indexOf(ary[1]), text: ary[1] };
+ }
+
+ if (match && (match.start >= 0))
+ {
+ match.munger = munger;
+ matches[entry] = match;
+ }
+ }
+
+ // Find the first matching entry...
+ var firstMatch = { start: text.length, munger: null };
+ var firstPriority = 0;
+ for (entry in matches)
+ {
+ // If it matches before the existing first, or at the same spot but
+ // with a higher start-priority, this is a better match.
+ if (matches[entry].start < firstMatch.start ||
+ ((matches[entry].start == firstMatch.start) &&
+ (this.entries[priority][entry].startPriority > firstPriority)))
+ {
+ firstMatch = matches[entry];
+ firstPriority = this.entries[priority][entry].startPriority;
+ }
+ }
+
+ // Replace it.
+ if (firstMatch.munger)
+ {
+ var munger = firstMatch.munger;
+ firstMatch.end = firstMatch.start + firstMatch.text.length;
+
+ // Need to deal with the text before the match, if there is any.
+ var beforeText = text.substr(0, firstMatch.start);
+ if (firstMatch.start > 0)
+ this.munge(beforeText, containerTag, data);
+
+ if (typeof munger.lambdaReplace == "function")
+ {
+ // The munger rule itself should take care of munging the 'inside'
+ // of the match.
+ munger.lambdaReplace(firstMatch.text, containerTag, data, munger);
+ this.munge(text.substr(firstMatch.end), containerTag, data);
+
+ return containerTag;
+ }
+ else
+ {
+ var tag = document.createElementNS(XHTML_NS, munger.tagName);
+ tag.setAttribute("class", munger.className + calcClass(data));
+
+ // Don't let this rule match again when we recurse.
+ munger.enabled = false;
+ this.munge(firstMatch.text, tag, data);
+ munger.enabled = true;
+
+ containerTag.appendChild(tag);
+
+ this.munge(text.substr(firstMatch.end), containerTag, data);
+
+ return containerTag;
+ }
+ }
+ return null;
+}
+
+function insertText(text, containerTag, data)
+{
+ var textNode = document.createTextNode(text);
+ containerTag.appendChild(textNode);
+}
+
diff --git a/comm/suite/chatzilla/xul/lib/tree-utils.js b/comm/suite/chatzilla/xul/lib/tree-utils.js
new file mode 100644
index 0000000000..1e47aebee6
--- /dev/null
+++ b/comm/suite/chatzilla/xul/lib/tree-utils.js
@@ -0,0 +1,1716 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+/**
+ * An implemention of |nsITreeView| for a tree whose elements have no children.
+ *
+ * Code using BasicOView can override |getRowProperties|, |getColumnProperties|,
+ * |getCellProperties|, etc., as needed.
+ *
+ * Code using |BasicOView| will need to make the appropriate |myTree.tree
+ * .invalidate| calls when |myTree.data| changes.
+ *
+ * @syntax
+ * var myTree = new BasicOView()
+ * myTree.setColumnNames(["col 1", "col 2"]);
+ * myTree.data = [["row 1, col 1", "row 1, col 2"],
+ * ["row 2, col 1", "row 2, col 2"]];
+ * treeBoxObject.view = myTree;
+ */
+function BasicOView()
+{
+ this.tree = null;
+}
+
+/* functions *you* should call to initialize and maintain the tree state */
+
+/* scroll the line specified by |line| to the center of the tree */
+BasicOView.prototype.centerLine =
+function bov_ctrln (line)
+{
+ var first = this.tree.getFirstVisibleRow();
+ var last = this.tree.getLastVisibleRow();
+ this.scrollToRow(line - (last - first + 1) / 2);
+}
+
+/* call this to set the association between column names and data columns */
+BasicOView.prototype.setColumnNames =
+function bov_setcn (aryNames)
+{
+ this.columnNames = new Object();
+ for (var i = 0; i < aryNames.length; ++i)
+ this.columnNames[aryNames[i]] = i;
+}
+
+/*
+ * scroll the source so |line| is at either the top, center, or bottom
+ * of the view, depending on the value of |align|.
+ *
+ * line is the one based target line.
+ * if align is negative, the line will be scrolled to the top, if align is
+ * zero the line will be centered, and if align is greater than 0 the line
+ * will be scrolled to the bottom. 0 is the default.
+ */
+BasicOView.prototype.scrollTo =
+function bov_scrollto (line, align)
+{
+ if (!this.tree)
+ return;
+
+ var headerRows = 1;
+
+ var first = this.tree.getFirstVisibleRow();
+ var last = this.tree.getLastVisibleRow();
+ var viz = last - first + 1 - headerRows; /* total number of visible rows */
+
+ /* all rows are visible, nothing to scroll */
+ if (first == 0 && last >= this.rowCount)
+ return;
+
+ /* tree lines are 0 based, we accept one based lines, deal with it */
+ --line;
+
+ /* safety clamp */
+ if (line < 0)
+ line = 0;
+ if (line >= this.rowCount)
+ line = this.rowCount - 1;
+
+ if (align < 0)
+ {
+ if (line > this.rowCount - viz) /* overscroll, can't put a row from */
+ line = this.rowCount - viz; /* last page at the top. */
+ this.tree.scrollToRow(line);
+ }
+ else if (align > 0)
+ {
+ if (line < viz) /* underscroll, can't put a row from the first page */
+ line = 0; /* at the bottom. */
+ else
+ line = line - viz + headerRows;
+
+ this.tree.scrollToRow(line);
+ }
+ else
+ {
+ var half_viz = viz / 2;
+ /* lines past this line can't be centered without causing the tree
+ * to show more rows than we have. */
+ var lastCenterable = this.rowCount - half_viz;
+ if (line > half_viz)
+ line = lastCenterable;
+ /* lines before this can't be centered without causing the tree
+ * to attempt to display negative rows. */
+ else if (line < half_viz)
+ line = half_viz;
+ else
+ /* round the vizible rows down to a whole number, or we try to end up
+ * on a N + 0.5 row! */
+ half_viz = Math.floor(half_viz);
+
+ this.tree.scrollToRow(line - half_viz);
+ }
+}
+
+BasicOView.prototype.__defineGetter__("selectedIndex", bov_getsel);
+function bov_getsel()
+{
+ if (!this.tree || this.tree.view.selection.getRangeCount() < 1)
+ return -1;
+
+ var min = new Object();
+ this.tree.view.selection.getRangeAt(0, min, {});
+ return min.value;
+}
+
+BasicOView.prototype.__defineSetter__("selectedIndex", bov_setsel);
+function bov_setsel(i)
+{
+ if (i == -1)
+ this.tree.view.selection.clearSelection();
+ else
+ this.tree.view.selection.timedSelect (i, 500);
+ return i;
+}
+
+/*
+ * functions the tree will call to retrieve the list state (nsITreeView.)
+ */
+
+BasicOView.prototype.rowCount = 0;
+
+BasicOView.prototype.getCellProperties =
+function bov_cellprops (row, col, properties)
+{
+ return "";
+}
+
+BasicOView.prototype.getColumnProperties =
+function bov_colprops (col, properties)
+{
+ return "";
+}
+
+BasicOView.prototype.getRowProperties =
+function bov_rowprops (index, properties)
+{
+ return "";
+}
+
+BasicOView.prototype.isContainer =
+function bov_isctr (index)
+{
+ return false;
+}
+
+BasicOView.prototype.isContainerOpen =
+function bov_isctropen (index)
+{
+ return false;
+}
+
+BasicOView.prototype.isContainerEmpty =
+function bov_isctrempt (index)
+{
+ return false;
+}
+
+BasicOView.prototype.isSeparator =
+function bov_isseparator (index)
+{
+ return false;
+}
+
+BasicOView.prototype.isSorted =
+function bov_issorted (index)
+{
+ return false;
+}
+
+BasicOView.prototype.canDrop =
+function bov_drop (index, orientation)
+{
+ return false;
+}
+
+BasicOView.prototype.drop =
+function bov_drop (index, orientation)
+{
+ return false;
+}
+
+BasicOView.prototype.getParentIndex =
+function bov_getpi (index)
+{
+ if (index < 0)
+ return -1;
+
+ return 0;
+}
+
+BasicOView.prototype.hasNextSibling =
+function bov_hasnxtsib (rowIndex, afterIndex)
+{
+ return (afterIndex < (this.rowCount - 1));
+}
+
+BasicOView.prototype.getLevel =
+function bov_getlvl (index)
+{
+ return 0;
+}
+
+BasicOView.prototype.getImageSrc =
+function bov_getimgsrc (row, col)
+{
+}
+
+BasicOView.prototype.getProgressMode =
+function bov_getprgmode (row, col)
+{
+}
+
+BasicOView.prototype.getCellValue =
+function bov_getcellval (row, col)
+{
+}
+
+BasicOView.prototype.getCellText =
+function bov_getcelltxt (row, col)
+{
+ if (!this.columnNames)
+ return "";
+
+ if (typeof col == "object")
+ col = col.id;
+
+ var ary = col.match (/:(.*)/);
+ if (ary)
+ col = ary[1];
+
+ var colName = this.columnNames[col];
+
+ if (typeof colName == "undefined")
+ return "";
+
+ return this.data[row][colName];
+}
+
+BasicOView.prototype.setTree =
+function bov_seto (tree)
+{
+ this.tree = tree;
+}
+
+BasicOView.prototype.toggleOpenState =
+function bov_toggleopen (index)
+{
+}
+
+BasicOView.prototype.cycleHeader =
+function bov_cyclehdr (col)
+{
+}
+
+BasicOView.prototype.selectionChanged =
+function bov_selchg ()
+{
+}
+
+BasicOView.prototype.cycleCell =
+function bov_cyclecell (row, col)
+{
+}
+
+BasicOView.prototype.isEditable =
+function bov_isedit (row, col)
+{
+ return false;
+}
+
+BasicOView.prototype.isSelectable =
+function bov_isselect (row, col)
+{
+ return false;
+}
+
+BasicOView.prototype.setCellValue =
+function bov_setct (row, col, value)
+{
+}
+
+BasicOView.prototype.setCellText =
+function bov_setct (row, col, value)
+{
+}
+
+BasicOView.prototype.onRouteFocus =
+function bov_rfocus (event)
+{
+ if ("onFocus" in this)
+ this.onFocus(event);
+}
+
+BasicOView.prototype.onRouteBlur =
+function bov_rblur (event)
+{
+ if ("onBlur" in this)
+ this.onBlur(event);
+}
+
+BasicOView.prototype.onRouteDblClick =
+function bov_rdblclick (event)
+{
+ if (!("onRowCommand" in this) || event.target.localName != "treechildren")
+ return;
+
+ var rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex == -1 || rowIndex > this.rowCount)
+ return;
+ var rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+
+ this.onRowCommand(rec, event);
+}
+
+BasicOView.prototype.onRouteKeyPress =
+function bov_rkeypress (event)
+{
+ var rec;
+ var rowIndex;
+
+ if ("onRowCommand" in this && (event.keyCode == 13 || event.charCode == 32))
+ {
+ if (!this.selection)
+ return;
+
+ rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex == -1 || rowIndex > this.rowCount)
+ return;
+ rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+
+ this.onRowCommand(rec, event);
+ }
+ else if ("onKeyPress" in this)
+ {
+ rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex != -1 && rowIndex < this.rowCount)
+ {
+ rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+ }
+ else
+ {
+ rec = null;
+ }
+
+ this.onKeyPress(rec, event);
+ }
+}
+
+BasicOView.prototype.performAction =
+function bov_pact (action)
+{
+}
+
+BasicOView.prototype.performActionOnRow =
+function bov_pactrow (action)
+{
+}
+
+BasicOView.prototype.performActionOnCell =
+function bov_pactcell (action)
+{
+}
+
+/**
+ * A single entry in an |XULTreeView|.
+ *
+ * These things take care of keeping the |XULTreeView| properly informed of
+ * changes in value and child count. You shouldn't have to maintain tree state
+ * at all - just update the |XULTreeViewRecord| objects.
+ *
+ * @param share An otherwise empty object to store cache data. You should use
+ * the same object as the |share| for the |XULTreeView| that you
+ * indend to contain these records.
+ *
+ */
+function XULTreeViewRecord(share)
+{
+ this._share = share;
+ this.visualFootprint = 1;
+ this.isHidden = true; /* records are considered hidden until they are
+ * inserted into a live tree */
+}
+
+XULTreeViewRecord.prototype.isContainerOpen = false;
+
+/*
+ * walk the parent tree to find our tree container. return null if there is
+ * none
+ */
+XULTreeViewRecord.prototype.findContainerTree =
+function xtvr_gettree ()
+{
+ if (!("parentRecord" in this))
+ return null;
+ var parent = this.parentRecord;
+
+ while (parent)
+ {
+ if ("_treeView" in parent)
+ return parent._treeView;
+ if ("parentRecord" in parent)
+ parent = parent.parentRecord;
+ else
+ parent = null;
+ }
+
+ return null;
+}
+
+XULTreeViewRecord.prototype.__defineGetter__("childIndex", xtvr_getChildIndex);
+function xtvr_getChildIndex ()
+{
+ //dd ("getChildIndex {");
+
+ if (!("parentRecord" in this))
+ {
+ delete this._childIndex;
+ //dd ("} -1");
+ return -1;
+ }
+
+ if ("_childIndex" in this)
+ {
+ if ("childData" in this && this._childIndex in this.childData &&
+ this.childData[this._childIndex] == this)
+ {
+ //dd ("} " + this._childIndex);
+ return this._childIndex;
+ }
+ }
+
+ var childData = this.parentRecord.childData;
+ var len = childData.length;
+ for (var i = 0; i < len; ++i)
+ {
+ if (childData[i] == this)
+ {
+ this._childIndex = i;
+ //dd ("} " + this._childIndex);
+ return i;
+ }
+ }
+
+ delete this._childIndex;
+ //dd ("} -1");
+ return -1;
+}
+
+XULTreeViewRecord.prototype.__defineSetter__("childIndex", xtvr_setChildIndex);
+function xtvr_setChildIndex ()
+{
+ dd("xtvr: childIndex is read only, ignore attempt to write to it\n");
+ if (typeof getStackTrace == "function")
+ dd(getStackTrace());
+}
+
+/* count the number of parents, not including the root node */
+XULTreeViewRecord.prototype.__defineGetter__("level", xtvr_getLevel);
+function xtvr_getLevel ()
+{
+ if (!("parentRecord" in this))
+ return -1;
+
+ var rv = 0;
+ var parentRecord = this.parentRecord;
+ while ("parentRecord" in parentRecord &&
+ (parentRecord = parentRecord.parentRecord)) ++rv;
+ return rv;
+}
+
+/*
+ * associates a property name on this record, with a column in the tree. This
+ * method will set up a get/set pair for the property name you specify which
+ * will take care of updating the tree when the value changes. DO NOT try
+ * to change your mind later. Do not attach a different name to the same colID,
+ * and do not rename the colID. You have been warned.
+ */
+XULTreeViewRecord.prototype.setColumnPropertyName =
+function xtvr_setcol (colID, propertyName)
+{
+ function xtvr_getValueShim ()
+ {
+ return this._colValues[colID];
+ }
+ function xtvr_setValueShim (newValue)
+ {
+ this._colValues[colID] = newValue;
+ return newValue;
+ }
+
+ if (!("_colValues" in this))
+ this._colValues = new Object();
+
+ if (typeof propertyName == "function")
+ {
+ this._colValues.__defineGetter__(colID, propertyName);
+ }
+ else
+ {
+ this.__defineGetter__(propertyName, xtvr_getValueShim);
+ this.__defineSetter__(propertyName, xtvr_setValueShim);
+ }
+}
+
+XULTreeViewRecord.prototype.setColumnPropertyValue =
+function xtvr_setcolv (colID, value)
+{
+ this._colValues[colID] = value;
+}
+
+/*
+ * set the default sort column and reSort.
+ */
+XULTreeViewRecord.prototype.setSortColumn =
+function xtvr_setcol (colID, dir)
+{
+ //dd ("setting sort column to " + colID);
+ this._share.sortColumn = colID;
+ this._share.sortDirection = (typeof dir == "undefined") ? 1 : dir;
+ this.reSort();
+}
+
+/*
+ * set the default sort direction. 1 is ascending, -1 is descending, 0 is no
+ * sort. setting this to 0 will *not* recover the natural insertion order,
+ * it will only affect newly added items.
+ */
+XULTreeViewRecord.prototype.setSortDirection =
+function xtvr_setdir (dir)
+{
+ this._share.sortDirection = dir;
+}
+
+/*
+ * invalidate this row in the tree
+ */
+XULTreeViewRecord.prototype.invalidate =
+function xtvr_invalidate()
+{
+ var tree = this.findContainerTree();
+ if (tree)
+ {
+ var row = this.calculateVisualRow();
+ if (row != -1)
+ tree.tree.invalidateRow(row);
+ }
+}
+
+/*
+ * invalidate any data in the cache.
+ */
+XULTreeViewRecord.prototype.invalidateCache =
+function xtvr_killcache()
+{
+ this._share.rowCache = new Object();
+ this._share.lastComputedIndex = -1;
+ this._share.lastIndexOwner = null;
+}
+
+/*
+ * default comparator function for sorts. if you want a custom sort, override
+ * this method. We declare xtvr_sortcmp as a top level function, instead of
+ * a function expression so we can refer to it later.
+ */
+XULTreeViewRecord.prototype.sortCompare = xtvr_sortcmp;
+function xtvr_sortcmp (a, b)
+{
+ var sc = a._share.sortColumn;
+ var sd = a._share.sortDirection;
+
+ a = a[sc];
+ b = b[sc];
+
+ if (a < b)
+ return -1 * sd;
+
+ if (a > b)
+ return 1 * sd;
+
+ return 0;
+}
+
+/*
+ * this method will cause all child records to be reSorted. any records
+ * with the default sortCompare method will be sorted by the colID passed to
+ * setSortColumn.
+ *
+ * the local parameter is used internally to control whether or not the
+ * sorted rows are invalidated. don't use it yourself.
+ */
+XULTreeViewRecord.prototype.reSort =
+function xtvr_resort (leafSort)
+{
+ if (!("childData" in this) || this.childData.length < 1 ||
+ (this.childData[0].sortCompare == xtvr_sortcmp &&
+ !("sortColumn" in this._share) || this._share.sortDirection == 0))
+ {
+ /* if we have no children, or we have the default sort compare and no
+ * sort flags, then just exit */
+ return;
+ }
+
+ this.childData.sort(this.childData[0].sortCompare);
+
+ for (var i = 0; i < this.childData.length; ++i)
+ {
+ if ("isContainerOpen" in this.childData[i] &&
+ this.childData[i].isContainerOpen)
+ this.childData[i].reSort(true);
+ else
+ this.childData[i].sortIsInvalid = true;
+ }
+
+ if (!leafSort)
+ {
+ this.invalidateCache();
+ var tree = this.findContainerTree();
+ if (tree && tree.tree)
+ {
+ var rowIndex = this.calculateVisualRow();
+ /*
+ dd ("invalidating " + rowIndex + " - " +
+ (rowIndex + this.visualFootprint - 1));
+ */
+ tree.tree.invalidateRange (rowIndex,
+ rowIndex + this.visualFootprint - 1);
+ }
+ }
+ delete this.sortIsInvalid;
+}
+
+/*
+ * call this to indicate that this node may have children at one point. make
+ * sure to call it before adding your first child.
+ */
+XULTreeViewRecord.prototype.reserveChildren =
+function xtvr_rkids (always)
+{
+ if (!("childData" in this))
+ this.childData = new Array();
+ if (!("isContainerOpen" in this))
+ this.isContainerOpen = false;
+ if (always)
+ this.alwaysHasChildren = true;
+ else
+ delete this.alwaysHasChildren;
+}
+
+/*
+ * add a child to the end of the child list for this record. takes care of
+ * updating the tree as well.
+ */
+XULTreeViewRecord.prototype.appendChild =
+function xtvr_appchild (child)
+{
+ if (!isinstance(child, XULTreeViewRecord))
+ throw Components.results.NS_ERROR_INVALID_ARG;
+
+ child.isHidden = false;
+ child.parentRecord = this;
+ this.childData.push(child);
+
+ if ("isContainerOpen" in this && this.isContainerOpen)
+ {
+ //dd ("appendChild: " + xtv_formatRecord(child, ""));
+ if (this.calculateVisualRow() >= 0)
+ {
+ var tree = this.findContainerTree();
+ if (tree && tree.frozen)
+ this.needsReSort = true;
+ else
+ this.reSort(true); /* reSort, don't invalidate. we're going
+ * to do that in the
+ * onVisualFootprintChanged call. */
+ }
+ this.onVisualFootprintChanged(child.calculateVisualRow(),
+ child.visualFootprint);
+ }
+}
+
+/*
+ * add a list of children to the end of the child list for this record.
+ * faster than multiple appendChild() calls.
+ */
+XULTreeViewRecord.prototype.appendChildren =
+function xtvr_appchild (children)
+{
+ var delta = 0;
+ for (var i = 0; i < children.length; ++i)
+ {
+ var child = children[i];
+ child.isHidden = false;
+ child.parentRecord = this;
+ this.childData.push(child);
+ delta += child.visualFootprint;
+ }
+
+ if ("isContainerOpen" in this && this.isContainerOpen)
+ {
+ if (this.calculateVisualRow() >= 0)
+ {
+ this.reSort(true); /* reSort, don't invalidate. we're going to do
+ * that in the onVisualFootprintChanged call. */
+ }
+ this.onVisualFootprintChanged(this.childData[0].calculateVisualRow(),
+ delta);
+ }
+}
+
+/*
+ * Removes a single child from this record by index.
+ * @param index Index of the child record to remove.
+ */
+XULTreeViewRecord.prototype.removeChildAtIndex =
+function xtvr_remchild(index)
+{
+ var len = this.childData.length;
+ if (!ASSERT(index >= 0 && index < len, "index out of bounds"))
+ return;
+
+ var orphan = this.childData[index];
+ var delta = -orphan.visualFootprint;
+ var changeStart = orphan.calculateVisualRow();
+ delete orphan.parentRecord;
+ arrayRemoveAt(this.childData, index);
+
+ if (!orphan.isHidden && "isContainerOpen" in this && this.isContainerOpen)
+ this.onVisualFootprintChanged(changeStart, delta);
+}
+
+/*
+ * Removes a range of children from this record by index. Faster than multiple
+ * removeChildAtIndex() calls.
+ * @param index Index of the first child record to remove.
+ * @param count Number of child records to remove.
+ */
+XULTreeViewRecord.prototype.removeChildrenAtIndex =
+function xtvr_remchildren(index, count)
+{
+ var len = this.childData.length;
+ if (!ASSERT(index >= 0 && index < len, "index out of bounds"))
+ return;
+ if (!ASSERT(count > 0 && index + count <= len, "count out of bounds"))
+ return;
+
+ var delta = 0;
+ var changeStart = this.childData[index].calculateVisualRow();
+ for (var i = 0; i < count; ++i)
+ {
+ var orphan = this.childData[index + i];
+ if (!orphan.isHidden)
+ delta -= orphan.visualFootprint;
+ delete orphan.parentRecord;
+ }
+ this.childData.splice(index, count);
+
+ if ("isContainerOpen" in this && this.isContainerOpen)
+ this.onVisualFootprintChanged(changeStart, delta);
+}
+
+/*
+ * hide this record and all descendants.
+ */
+XULTreeViewRecord.prototype.hide =
+function xtvr_hide()
+{
+ if (this.isHidden)
+ return;
+
+ var tree = this.findContainerTree();
+ if (tree && tree.frozen)
+ {
+ this.isHidden = true;
+ if ("parentRecord" in this)
+ this.parentRecord.onVisualFootprintChanged(0, -this.visualFootprint);
+ return;
+ }
+
+ /* get the row before hiding */
+ var row = this.calculateVisualRow();
+ this.invalidateCache();
+ this.isHidden = true;
+ /* go right to the parent so we don't muck with our own visualFootprint
+ * record, we'll need it to be correct if we're ever unHidden. */
+ if ("parentRecord" in this)
+ this.parentRecord.onVisualFootprintChanged(row, -this.visualFootprint);
+}
+
+/*
+ * unhide this record and all descendants.
+ */
+XULTreeViewRecord.prototype.unHide =
+function xtvr_uhide()
+{
+ if (!this.isHidden)
+ return;
+
+ var tree = this.findContainerTree();
+ if (tree && tree.frozen)
+ {
+ this.isHidden = false;
+ if ("parentRecord" in this)
+ this.parentRecord.onVisualFootprintChanged(0, this.visualFootprint);
+ return;
+ }
+
+ this.isHidden = false;
+ this.invalidateCache();
+ var row = this.calculateVisualRow();
+ if (this.parentRecord)
+ this.parentRecord.onVisualFootprintChanged(row, this.visualFootprint);
+}
+
+/*
+ * open this record, exposing it's children. DONT call this method if the
+ * record has no children.
+ */
+XULTreeViewRecord.prototype.open =
+function xtvr_open ()
+{
+ if (this.isContainerOpen)
+ return;
+
+ if ("onPreOpen" in this)
+ this.onPreOpen();
+
+ this.isContainerOpen = true;
+ var delta = 0;
+ for (var i = 0; i < this.childData.length; ++i)
+ {
+ if (!this.childData[i].isHidden)
+ delta += this.childData[i].visualFootprint;
+ }
+
+ /* this reSort should only happen if the sort column changed */
+ this.reSort(true);
+ this.visualFootprint += delta;
+ if ("parentRecord" in this)
+ {
+ this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow(),
+ 0);
+ this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow() +
+ 1, delta);
+ }
+}
+
+/*
+ * close this record, hiding it's children. DONT call this method if the record
+ * has no children, or if it is already closed.
+ */
+XULTreeViewRecord.prototype.close =
+function xtvr_close ()
+{
+ if (!this.isContainerOpen)
+ return;
+
+ this.isContainerOpen = false;
+ var delta = 1 - this.visualFootprint;
+ this.visualFootprint += delta;
+ if ("parentRecord" in this)
+ {
+ this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow(),
+ 0);
+ this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow() +
+ 1, delta);
+ }
+
+ if ("onPostClose" in this)
+ this.onPostClose();
+}
+
+/*
+ * called when a node above this one grows or shrinks. we need to adjust
+ * our own visualFootprint to match the change, and pass the message on.
+ */
+XULTreeViewRecord.prototype.onVisualFootprintChanged =
+function xtvr_vpchange (start, amount)
+{
+ /* if we're not hidden, but this notification came from a hidden node
+ * (start == -1), ignore it, it doesn't affect us. */
+ if (start == -1 && !this.isHidden)
+ {
+
+ //dd ("vfp change (" + amount + ") from hidden node ignored.");
+ return;
+ }
+
+ this.visualFootprint += amount;
+
+ if ("parentRecord" in this)
+ this.parentRecord.onVisualFootprintChanged(start, amount);
+}
+
+/*
+ * calculate the "visual" row for this record. If the record isn't actually
+ * visible return -1.
+ * eg.
+ * Name Visual Row
+ * node1 0
+ * node11 1
+ * node12 2
+ * node2 3
+ * node21 4
+ * node3 5
+ */
+XULTreeViewRecord.prototype.calculateVisualRow =
+function xtvr_calcrow ()
+{
+ /* if this is the second time in a row that someone asked us, fetch the last
+ * result from the cache. */
+ if (this._share.lastIndexOwner == this)
+ return this._share.lastComputedIndex;
+
+ var vrow;
+
+ /* if this is an uninserted or hidden node, or... */
+ if (!("parentRecord" in this) || (this.isHidden) ||
+ /* if parent isn't open, or... */
+ (!this.parentRecord.isContainerOpen) ||
+ /* parent isn't visible */
+ ((vrow = this.parentRecord.calculateVisualRow()) == -1))
+ {
+ /* then we're not visible, return -1 */
+ //dd ("cvr: returning -1");
+ return -1;
+ }
+
+ /* parent is the root node XXX parent is not visible */
+ if (vrow == null)
+ vrow = 0;
+ else
+ /* parent is not the root node, add one for the space they take up. */
+ ++vrow;
+
+ /* add in the footprint for all of the earlier siblings */
+ var ci = this.childIndex;
+ for (var i = 0; i < ci; ++i)
+ {
+ if (!this.parentRecord.childData[i].isHidden)
+ vrow += this.parentRecord.childData[i].visualFootprint;
+ }
+
+ /* save this calculation to the cache. */
+ this._share.lastIndexOwner = this;
+ this._share.lastComputedIndex = vrow;
+
+ return vrow;
+}
+
+/*
+ * locates the child record for the visible row |targetRow|. DO NOT call this
+ * with a targetRow less than this record's visual row, or greater than this
+ * record's visual row + the number of visible children it has.
+ */
+XULTreeViewRecord.prototype.locateChildByVisualRow =
+function xtvr_find (targetRow, myRow)
+{
+ if (targetRow in this._share.rowCache)
+ return this._share.rowCache[targetRow];
+
+ /* if this is true, we *are* the index */
+ if (targetRow == myRow)
+ return (this._share.rowCache[targetRow] = this);
+
+ /* otherwise, we've got to search the kids */
+ var childStart = myRow; /* childStart represents the starting visual row
+ * for the child we're examining. */
+ for (var i = 0; i < this.childData.length; ++i)
+ {
+ var child = this.childData[i];
+ /* ignore hidden children */
+ if (child.isHidden)
+ continue;
+ /* if this kid is the targetRow, we're done */
+ if (childStart == targetRow)
+ return (this._share.rowCache[targetRow] = child);
+ /* if this kid contains the index, ask *it* to find the record */
+ else if (targetRow <= childStart + child.visualFootprint) {
+ /* this *has* to succeed */
+ var rv = child.locateChildByVisualRow(targetRow, childStart + 1);
+ //XXXASSERT (rv, "Can't find a row that *has* to be there.");
+ /* don't cache this, the previous call to locateChildByVisualRow
+ * just did. */
+ return rv;
+ }
+
+ /* otherwise, get ready to ask the next kid */
+ childStart += child.visualFootprint;
+ }
+
+ return null;
+}
+
+/**
+ * Used to drop a label into an arbitrary place in an arbitrary tree.
+ *
+ * Normally, specializations of |XULTreeViewRecord| are tied to a specific
+ * tree because of implementation details. |XTLabelRecords| are specially
+ * designed (err, hacked) to work around these details - this makes them
+ * slower, but more generic.
+ *
+ * We set up a getter for |_share| that defers to the parent object. This lets
+ * |XTLabelRecords| work in any tree.
+ */
+function XTLabelRecord (columnName, label, blankCols)
+{
+ this.setColumnPropertyName (columnName, "label");
+ this.label = label;
+ this.property = null;
+
+ if (typeof blankCols == "object")
+ {
+ for (var i in blankCols)
+ this._colValues[blankCols[i]] = "";
+ }
+}
+
+XTLabelRecord.prototype = new XULTreeViewRecord (null);
+
+XTLabelRecord.prototype.__defineGetter__("_share", tolr_getshare);
+function tolr_getshare()
+{
+ if ("parentRecord" in this)
+ return this.parentRecord._share;
+
+ ASSERT (0, "XTLabelRecord cannot be the root of a visible tree.");
+ return null;
+}
+
+// @internal
+function XTRootRecord (tree, share)
+{
+ this._share = share;
+ this._treeView = tree;
+ this.visualFootprint = 0;
+ this.isHidden = false;
+ this.reserveChildren();
+ this.isContainerOpen = true;
+}
+
+/* no cache passed in here, we set it in the XTRootRecord contructor instead. */
+XTRootRecord.prototype = new XULTreeViewRecord (null);
+
+XTRootRecord.prototype.open =
+XTRootRecord.prototype.close =
+function torr_notimplemented()
+{
+ /* don't do this on a root node */
+}
+
+XTRootRecord.prototype.calculateVisualRow =
+function torr_calcrow ()
+{
+ return null;
+}
+
+XTRootRecord.prototype.reSort =
+function torr_resort ()
+{
+ if ("_treeView" in this && this._treeView.frozen)
+ {
+ this._treeView.needsReSort = true;
+ return;
+ }
+
+ if (!("childData" in this) || this.childData.length < 1 ||
+ (this.childData[0].sortCompare == xtvr_sortcmp &&
+ !("sortColumn" in this._share) || this._share.sortDirection == 0))
+ {
+ /* if we have no children, or we have the default sort compare but we're
+ * missing a sort flag, then just exit */
+ return;
+ }
+
+ this.childData.sort(this.childData[0].sortCompare);
+
+ for (var i = 0; i < this.childData.length; ++i)
+ {
+ if ("isContainerOpen" in this.childData[i] &&
+ this.childData[i].isContainerOpen)
+ this.childData[i].reSort(true);
+ else
+ this.childData[i].sortIsInvalid = true;
+ }
+
+ if ("_treeView" in this && this._treeView.tree)
+ {
+ /*
+ dd ("root node: invalidating 0 - " + this.visualFootprint +
+ " for sort");
+ */
+ this.invalidateCache();
+ this._treeView.tree.invalidateRange (0, this.visualFootprint);
+ }
+}
+
+XTRootRecord.prototype.locateChildByVisualRow =
+function torr_find (targetRow)
+{
+ if (targetRow in this._share.rowCache)
+ return this._share.rowCache[targetRow];
+
+ var childStart = -1; /* childStart represents the starting visual row
+ * for the child we're examining. */
+ for (var i = 0; i < this.childData.length; ++i)
+ {
+ var child = this.childData[i];
+ /* ignore hidden children */
+ if (child.isHidden)
+ continue;
+ /* if this kid is the targetRow, we're done */
+ if (childStart == targetRow)
+ return (this._share.rowCache[targetRow] = child);
+ /* if this kid contains the index, ask *it* to find the record */
+ else if (targetRow <= childStart + child.visualFootprint) {
+ /* this *has* to succeed */
+ var rv = child.locateChildByVisualRow(targetRow, childStart + 1);
+ //XXXASSERT (rv, "Can't find a row that *has* to be there.");
+ /* don't cache this, the previous call to locateChildByVisualRow
+ * just did. */
+ return rv;
+ }
+
+ /* otherwise, get ready to ask the next kid */
+ childStart += child.visualFootprint;
+ }
+
+ return null;
+}
+
+XTRootRecord.prototype.onVisualFootprintChanged =
+function torr_vfpchange (start, amount)
+{
+ if (!this._treeView.frozen)
+ {
+ this.invalidateCache();
+ this.visualFootprint += amount;
+ if ("_treeView" in this && "tree" in this._treeView &&
+ this._treeView.tree)
+ {
+ if (amount != 0)
+ this._treeView.tree.rowCountChanged (start, amount);
+ else
+ this._treeView.tree.invalidateRow (start);
+ }
+ }
+ else
+ {
+ if ("changeAmount" in this._treeView)
+ this._treeView.changeAmount += amount;
+ else
+ this._treeView.changeAmount = amount;
+ if ("changeStart" in this._treeView)
+ this._treeView.changeStart =
+ Math.min (start, this._treeView.changeStart);
+ else
+ this._treeView.changeStart = start;
+ }
+}
+
+/**
+ * An implemention of |nsITreeView| for a tree whose elements have multiple
+ * levels of children.
+ *
+ * Code using |XULTreeView| can override |getRowProperties|, |getColumnProperties|,
+ * |getCellProperties|, etc., as needed.
+ *
+ * @param share An otherwise empty object to store cache data.
+ */
+function XULTreeView(share)
+{
+ if (!share)
+ share = new Object();
+ this.childData = new XTRootRecord(this, share);
+ this.childData.invalidateCache();
+ this.tree = null;
+ this.share = share;
+ this.frozen = 0;
+}
+
+/* functions *you* should call to initialize and maintain the tree state */
+
+/*
+ * Changes to the tree contents will not cause the tree to be invalidated
+ * until |thaw()| is called. All changes will be pooled into a single invalidate
+ * call.
+ *
+ * Freeze/thaws are nestable, the tree will not update until the number of
+ * |thaw()| calls matches the number of freeze() calls.
+ */
+XULTreeView.prototype.freeze =
+function xtv_freeze ()
+{
+ if (++this.frozen == 1)
+ {
+ this.changeStart = 0;
+ this.changeAmount = 0;
+ }
+}
+
+/*
+ * Reflect any changes to the tree content since the last freeze.
+ */
+XULTreeView.prototype.thaw =
+function xtv_thaw ()
+{
+ if (this.frozen == 0)
+ {
+ ASSERT (0, "not frozen");
+ return;
+ }
+
+ if (--this.frozen == 0 && "changeStart" in this)
+ {
+ this.childData.onVisualFootprintChanged(this.changeStart,
+ this.changeAmount);
+ }
+
+ if ("needsReSort" in this) {
+ this.childData.reSort();
+ delete this.needsReSort;
+ }
+
+
+ delete this.changeStart;
+ delete this.changeAmount;
+}
+
+XULTreeView.prototype.saveBranchState =
+function xtv_savebranch (target, source, recurse)
+{
+ var len = source.length;
+ for (var i = 0; i < len; ++i)
+ {
+ if (source[i].isContainerOpen)
+ {
+ target[i] = new Object();
+ target[i].name = source[i]._colValues["col-0"];
+ if (recurse)
+ this.saveBranchState (target[i], source[i].childData, true);
+ }
+ }
+}
+
+XULTreeView.prototype.restoreBranchState =
+function xtv_restorebranch (target, source, recurse)
+{
+ for (var i in source)
+ {
+ if (typeof source[i] == "object")
+ {
+ var name = source[i].name;
+ var len = target.length;
+ for (var j = 0; j < len; ++j)
+ {
+ if (target[j]._colValues["col-0"] == name &&
+ "childData" in target[j])
+ {
+ //dd ("opening " + name);
+ target[j].open();
+ if (recurse)
+ {
+ this.restoreBranchState (target[j].childData,
+ source[i], true);
+ }
+ break;
+ }
+ }
+ }
+ }
+}
+
+/* scroll the line specified by |line| to the center of the tree */
+XULTreeView.prototype.centerLine =
+function xtv_ctrln (line)
+{
+ var first = this.tree.getFirstVisibleRow();
+ var last = this.tree.getLastVisibleRow();
+ this.scrollToRow(line - (last - first + 1) / 2);
+}
+
+/*
+ * functions the tree will call to retrieve the list state (nsITreeView.)
+ */
+
+// @internal
+XULTreeView.prototype.__defineGetter__("rowCount", xtv_getRowCount);
+function xtv_getRowCount ()
+{
+ if (!this.childData)
+ return 0;
+
+ return this.childData.visualFootprint;
+}
+
+// @internal
+XULTreeView.prototype.isContainer =
+function xtv_isctr (index)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+
+ return Boolean(row && ("alwaysHasChildren" in row || "childData" in row));
+}
+
+// @internal
+XULTreeView.prototype.__defineGetter__("selectedIndex", xtv_getsel);
+function xtv_getsel()
+{
+ if (!this.tree || this.tree.view.selection.getRangeCount() < 1)
+ return -1;
+
+ var min = new Object();
+ this.tree.view.selection.getRangeAt(0, min, {});
+ return min.value;
+}
+
+// @internal
+XULTreeView.prototype.__defineSetter__("selectedIndex", xtv_setsel);
+function xtv_setsel(i)
+{
+ this.tree.view.selection.clearSelection();
+ if (i != -1)
+ this.tree.view.selection.timedSelect (i, 500);
+ return i;
+}
+
+// @internal
+XULTreeView.prototype.scrollTo = BasicOView.prototype.scrollTo;
+
+// @internal
+XULTreeView.prototype.isContainerOpen =
+function xtv_isctropen (index)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ return row && row.isContainerOpen;
+}
+
+// @internal
+XULTreeView.prototype.toggleOpenState =
+function xtv_toggleopen (index)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ //ASSERT(row, "bogus row");
+ if (row)
+ {
+ if (row.isContainerOpen)
+ row.close();
+ else
+ row.open();
+ }
+}
+
+// @internal
+XULTreeView.prototype.isContainerEmpty =
+function xtv_isctrempt (index)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ if ("alwaysHasChildren" in row)
+ return false;
+
+ if (!row || !("childData" in row))
+ return true;
+
+ return !row.childData.length;
+}
+
+// @internal
+XULTreeView.prototype.isSeparator =
+function xtv_isseparator (index)
+{
+ return false;
+}
+
+// @internal
+XULTreeView.prototype.getParentIndex =
+function xtv_getpi (index)
+{
+ if (index < 0)
+ return -1;
+
+ var row = this.childData.locateChildByVisualRow (index);
+
+ var rv = row.parentRecord.calculateVisualRow();
+ //dd ("getParentIndex: row " + index + " returning " + rv);
+ return (rv != null) ? rv : -1;
+}
+
+// @internal
+XULTreeView.prototype.hasNextSibling =
+function xtv_hasnxtsib (rowIndex, afterIndex)
+{
+ var row = this.childData.locateChildByVisualRow (rowIndex);
+ return row.childIndex < row.parentRecord.childData.length - 1;
+}
+
+// @internal
+XULTreeView.prototype.getLevel =
+function xtv_getlvl (index)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ if (!row)
+ return 0;
+
+ return row.level;
+}
+
+// @internal
+XULTreeView.prototype.getImageSrc =
+function xtv_getimgsrc (index, col)
+{
+}
+
+// @internal
+XULTreeView.prototype.getProgressMode =
+function xtv_getprgmode (index, col)
+{
+}
+
+// @internal
+XULTreeView.prototype.getCellValue =
+function xtv_getcellval (index, col)
+{
+}
+
+// @internal
+XULTreeView.prototype.getCellText =
+function xtv_getcelltxt (index, col)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ //ASSERT(row, "bogus row " + index);
+
+ if (typeof col == "object")
+ col = col.id;
+
+ var ary = col.match (/:(.*)/);
+ if (ary)
+ col = ary[1];
+
+ if (row && row._colValues && col in row._colValues)
+ return row._colValues[col];
+ else
+ return "";
+}
+
+// @internal
+XULTreeView.prototype.getCellProperties =
+function xtv_cellprops (row, col, properties)
+{
+ return "";
+}
+
+// @internal
+XULTreeView.prototype.getColumnProperties =
+function xtv_colprops (col, properties)
+{
+ return "";
+}
+
+// @internal
+XULTreeView.prototype.getRowProperties =
+function xtv_rowprops (index, properties)
+{
+ return "";
+}
+
+// @internal
+XULTreeView.prototype.isSorted =
+function xtv_issorted (index)
+{
+ return false;
+}
+
+// @internal
+XULTreeView.prototype.canDrop =
+function xtv_drop (index, orientation)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ //ASSERT(row, "bogus row " + index);
+ return (row && ("canDrop" in row) && row.canDrop(orientation));
+}
+
+// @internal
+XULTreeView.prototype.drop =
+function xtv_drop (index, orientation)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ //ASSERT(row, "bogus row " + index);
+ return (row && ("drop" in row) && row.drop(orientation));
+}
+
+// @internal
+XULTreeView.prototype.setTree =
+function xtv_seto (tree)
+{
+ this.childData.invalidateCache();
+ this.tree = tree;
+}
+
+// @internal
+XULTreeView.prototype.cycleHeader =
+function xtv_cyclehdr (col)
+{
+}
+
+// @internal
+XULTreeView.prototype.selectionChanged =
+function xtv_selchg ()
+{
+}
+
+// @internal
+XULTreeView.prototype.cycleCell =
+function xtv_cyclecell (row, col)
+{
+}
+
+// @internal
+XULTreeView.prototype.isEditable =
+function xtv_isedit (row, col)
+{
+ return false;
+}
+
+// @internal
+XULTreeView.prototype.isSelectable =
+function xtv_isselect (row, col)
+{
+ return false;
+}
+
+// @internal
+XULTreeView.prototype.setCellValue =
+function xtv_setct (row, col, value)
+{
+}
+
+// @internal
+XULTreeView.prototype.setCellText =
+function xtv_setct (row, col, value)
+{
+}
+
+// @internal
+XULTreeView.prototype.performAction =
+function xtv_pact (action)
+{
+}
+
+// @internal
+XULTreeView.prototype.performActionOnRow =
+function xtv_pactrow (action)
+{
+}
+
+// @internal
+XULTreeView.prototype.performActionOnCell =
+function xtv_pactcell (action)
+{
+}
+
+// @internal
+XULTreeView.prototype.onRouteFocus =
+function xtv_rfocus (event)
+{
+ if ("onFocus" in this)
+ this.onFocus(event);
+}
+
+// @internal
+XULTreeView.prototype.onRouteBlur =
+function xtv_rblur (event)
+{
+ if ("onBlur" in this)
+ this.onBlur(event);
+}
+
+// @internal
+XULTreeView.prototype.onRouteDblClick =
+function xtv_rdblclick (event)
+{
+ if (!("onRowCommand" in this) || event.target.localName != "treechildren")
+ return;
+
+ var rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex == -1 || rowIndex > this.rowCount)
+ return;
+ var rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+
+ this.onRowCommand(rec, event);
+}
+
+// @internal
+XULTreeView.prototype.onRouteKeyPress =
+function xtv_rkeypress (event)
+{
+ var rec;
+ var rowIndex;
+
+ if ("onRowCommand" in this && (event.keyCode == 13 || event.charCode == 32))
+ {
+ if (!this.selection)
+ return;
+
+ rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex == -1 || rowIndex > this.rowCount)
+ return;
+ rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+
+ this.onRowCommand(rec, event);
+ }
+ else if ("onKeyPress" in this)
+ {
+ rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex != -1 && rowIndex < this.rowCount)
+ {
+ rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+ }
+ else
+ {
+ rec = null;
+ }
+
+ this.onKeyPress(rec, event);
+ }
+}
+
+/******************************************************************************/
+
+function xtv_formatRecord (rec, indent)
+{
+ var str = "";
+
+ for (var i in rec._colValues)
+ str += rec._colValues[i] + ", ";
+
+ str += "[";
+
+ str += rec.calculateVisualRow() + ", ";
+ str += rec.childIndex + ", ";
+ str += rec.level + ", ";
+ str += rec.visualFootprint + ", ";
+ str += rec.isHidden + "]";
+
+ return (indent + str);
+}
+
+function xtv_formatBranch (rec, indent, recurse)
+{
+ var str = "";
+ for (var i = 0; i < rec.childData.length; ++i)
+ {
+ str += xtv_formatRecord (rec.childData[i], indent) + "\n";
+ if (recurse)
+ {
+ if ("childData" in rec.childData[i])
+ str += xtv_formatBranch(rec.childData[i], indent + " ",
+ --recurse);
+ }
+ }
+
+ return str;
+}
+
diff --git a/comm/suite/chatzilla/xul/skin/about.css b/comm/suite/chatzilla/xul/skin/about.css
new file mode 100644
index 0000000000..e4d935fa39
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/about.css
@@ -0,0 +1,44 @@
+/* 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/. */
+
+dialog {
+ padding-top: 0px;
+ padding-left: 0px;
+ padding-right: 0px;
+ width: 30em;
+}
+
+.box-padded {
+ background-color: Window;
+ color: WindowText;
+}
+
+.large-text {
+ font-size: large;
+}
+
+#logo {
+ background: url(chrome://chatzilla/skin/images/logo.png);
+ background-repeat: no-repeat;
+ width: 32px;
+ height: 32px;
+}
+
+#version {
+ color: GrayText;
+}
+
+.contributors-label {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.contributors label {
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+
+#groove {
+ margin-top: 0px;
+}
diff --git a/comm/suite/chatzilla/xul/skin/browserOverlay.css b/comm/suite/chatzilla/xul/skin/browserOverlay.css
new file mode 100644
index 0000000000..bd0abf011d
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/browserOverlay.css
@@ -0,0 +1,7 @@
+/* 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/. */
+
+#chatzilla-open {
+ list-style-image: url("chrome://chatzilla/skin/images/chatzilla-16.png");
+}
diff --git a/comm/suite/chatzilla/xul/skin/channels.css b/comm/suite/chatzilla/xul/skin/channels.css
new file mode 100644
index 0000000000..6fd6be8506
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/channels.css
@@ -0,0 +1,24 @@
+/* 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/. */
+
+#rightPanel {
+ width: 12em;
+}
+
+#bottomPanel {
+ width: 50em;
+ height: 20em;
+}
+
+.platform-Mac #rightPanel {
+ width: 18em;
+}
+
+#loadContainer {
+ margin: 0;
+}
+
+#loadBar {
+ -moz-appearance: none;
+}
diff --git a/comm/suite/chatzilla/xul/skin/chatzilla.css b/comm/suite/chatzilla/xul/skin/chatzilla.css
new file mode 100644
index 0000000000..63da06a05c
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/chatzilla.css
@@ -0,0 +1,305 @@
+/* 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/. */
+
+@import url(chrome://communicator/skin/);
+
+window {
+ width: 640px;
+ height: 480px;
+}
+
+#menu-view-none {
+ display: none;
+}
+
+#header-url:hover {
+ text-decoration: underline;
+}
+
+#outer-box {
+ margin: 5px;
+}
+
+.toolbarbutton-menu-dropmarker {
+ margin-left: 0.5em;
+}
+
+#input-widget,
+#input-widget-multiline {
+ border: thin silver inset;
+}
+
+#button-multiline-expand {
+ list-style-image:url("chrome://chatzilla/skin/images/multiline-expand.png");
+}
+
+#button-multiline-contract {
+ list-style-image:url("chrome://chatzilla/skin/images/multiline-contract.png");
+}
+
+#button-input {
+ list-style-image:url("chrome://chatzilla/skin/images/input-send.png");
+}
+
+.highlight-menu-item:hover {
+ color: white !important;
+ background: darkslategrey !important;
+}
+
+#user-list {
+ margin: 0;
+}
+
+#view-tabs {
+ overflow: hidden;
+}
+
+#views-tbar-spacer {
+ -moz-box-flex: 10000;
+}
+
+#tabs-drop-indicator-bar {
+ position: relative;
+ margin-top: -3px;
+ -moz-margin-start: -5px;
+ height: 3px;
+}
+
+#tabs-drop-indicator {
+ position: relative;
+ margin-right: -11px;
+ margin-bottom: -8px;
+ width: 11px;
+ height: 11px;
+ background: url(images/drop-indicator-bottom.png) 50% 50% no-repeat;
+}
+
+.view-button {
+ /* Box model is 5px 5px 5px 6px in aggregate. Extra pixel on the left is for the separator. */
+ -moz-appearance: none;
+ margin: 5px 0;
+ border: none;
+ border-left: 1px solid ButtonShadow;
+ padding: 0 5px;
+ background: Button;
+ color: ButtonText;
+ font: message-box;
+ text-shadow: none;
+ text-align: center;
+ max-width: 30ex;
+ -moz-box-flex: 1;
+ -moz-box-orient: vertical;
+ -moz-box-align: stretch;
+}
+
+.view-button:first-child {
+ border-left-color: transparent;
+}
+
+.view-button::before {
+ content: " ";
+ display: -moz-box;
+ margin: -5px -5px 0 -5px;
+ height: 4px;
+}
+
+.view-button[state="superfluous"]::before {
+ background-color: darkblue;
+}
+
+.view-button[state="activity"]::before {
+ background-color: darkgreen;
+}
+
+.view-button[state="attention"]::before {
+ background-color: red;
+}
+
+.view-button:hover,
+.view-button[state="current"] {
+ margin: 0 0 0 1px;
+ border-left: none;
+ border-radius: 0 0 6px 6px;
+ padding: 5px;
+ background: Window;
+ color: WindowText;
+}
+
+.view-button[state="current"] + .view-button,
+.view-button:hover + .view-button:not([state="current"]) {
+ border-left-color: transparent;
+}
+
+.view-button[state="current"] {
+ position: relative; /* So it visually appears above the tabs either side. */
+ border: 2px solid ButtonShadow;
+ border-top: none;
+ padding: 5px 3px 3px 3px;
+}
+
+treecol {
+ border: none;
+}
+
+/* ::::: Trees ::::: */
+
+treechildren::-moz-tree-row {
+ min-height: 18px;
+}
+
+/* The userlist can be in one of two state. In "symbol", the user's
+ * channel mode is shown as a @ or + image, while in "graphic" mode, the
+ * image is one of the LED images.
+ */
+
+/* no mode */
+treechildren::-moz-tree-image {
+ list-style-image: url(chrome://chatzilla/skin/images/no-symbol.png);
+}
+
+#user-list[mode="graphic"] treechildren::-moz-tree-image {
+ list-style-image: url(chrome://chatzilla/skin/images/no-graphic.png);
+}
+
+/* voice */
+treechildren::-moz-tree-image(voice-true) {
+ list-style-image: url(chrome://chatzilla/skin/images/voice-symbol.png);
+}
+
+#user-list[mode="graphic"] treechildren::-moz-tree-image(voice-true) {
+ list-style-image: url(chrome://chatzilla/skin/images/voice-graphic.png);
+}
+
+/* half-chanop */
+treechildren::-moz-tree-image(halfop-true) {
+ list-style-image: url(chrome://chatzilla/skin/images/halfop-symbol.png);
+}
+
+#user-list[mode="graphic"] treechildren::-moz-tree-image(halfop-true) {
+ list-style-image: url(chrome://chatzilla/skin/images/halfop-graphic.png);
+}
+
+/* chanop */
+treechildren::-moz-tree-image(op-true) {
+ list-style-image: url(chrome://chatzilla/skin/images/op-symbol.png);
+}
+
+#user-list[mode="graphic"] treechildren::-moz-tree-image(op-true) {
+ list-style-image: url(chrome://chatzilla/skin/images/op-graphic.png);
+}
+
+/* admin */
+treechildren::-moz-tree-image(admin-true) {
+ list-style-image: url(chrome://chatzilla/skin/images/admin-symbol.png);
+}
+
+#user-list[mode="graphic"] treechildren::-moz-tree-image(admin-true) {
+ list-style-image: url(chrome://chatzilla/skin/images/admin-graphic.png);
+}
+
+/* founder */
+treechildren::-moz-tree-image(founder-true) {
+ list-style-image: url(chrome://chatzilla/skin/images/founder-symbol.png);
+}
+
+#user-list[mode="graphic"] treechildren::-moz-tree-image(founder-true) {
+ list-style-image: url(chrome://chatzilla/skin/images/founder-graphic.png);
+}
+
+/* away */
+treechildren::-moz-tree-cell-text(away-true) {
+ color: GrayText;
+ font-style: italic;
+}
+
+treechildren::-moz-tree-cell-text(away-false) {
+}
+
+menuitem[header="true"] {
+ text-align: center;
+}
+
+.colorGrid
+{
+ width: 24px;
+ height: 16px;
+ border: 1px solid black;
+ -moz-box-align: center;
+ -moz-box-pack: center;
+}
+
+#colorTooltip
+{
+ padding: 0px;
+ -moz-box-align: center;
+ -moz-box-pack: center;
+}
+
+/* Feel the hacks. */
+progressmeter[mode="undetermined"] {
+ -moz-appearance: none;
+}
+
+[dir="ltr"] {
+ direction: ltr;
+}
+
+[dir="rtl"] {
+ direction: rtl;
+}
+
+#input-splitter {
+ margin: 4px 0 0 0;
+}
+
+#input-widgets {
+ margin: 4px 0;
+}
+
+#server-nick, #button-multiline-expand, #button-input, #button-multiline-contract {
+ margin: 0;
+}
+
+#input, #multiline-input {
+ margin: 0 4px;
+}
+
+/* Hack; Stop the status-bar from distorting without a security icon */
+#status-text {
+ min-height: 17px;
+}
+
+#status-text[notice="true"] {
+ font-weight: bold;
+}
+
+#security-button {
+ min-width: 20px;
+}
+
+#security-button:not([level="none"]):not([level="high"]):not([level="broken"]) {
+ display: none;
+}
+
+#alert-status[alertstate="off"] {
+ list-style-image: url("chrome://chatzilla/skin/images/spbubble-off.png");
+}
+
+#alert-status[alertstate="on"] {
+ list-style-image: url("chrome://chatzilla/skin/images/spbubble-on.png");
+}
+
+#logging-status[loggingstate="off"] {
+ list-style-image: url("chrome://chatzilla/skin/images/logging-off.png");
+}
+
+#logging-status[loggingstate="on"] {
+ list-style-image: url("chrome://chatzilla/skin/images/logging-on.png");
+}
+
+/* Focus styling for a11y reasons */
+#user-list-box[focusobvious="true"], #browser-box[focusobvious="true"],
+#multiline-hug-box[focusobvious="true"], #singleline-hug-box[focusobvious="true"] {
+ outline: 2px solid highlight;
+}
diff --git a/comm/suite/chatzilla/xul/skin/chatzillaOverlay.css b/comm/suite/chatzilla/xul/skin/chatzillaOverlay.css
new file mode 100644
index 0000000000..fef849df97
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/chatzillaOverlay.css
@@ -0,0 +1,7 @@
+/* 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/. */
+
+#mini-irc, #tasksMenuIRC {
+ list-style-image: url("chrome://chatzilla/skin/images/chatzilla-16.png");
+}
diff --git a/comm/suite/chatzilla/xul/skin/images/admin-graphic.png b/comm/suite/chatzilla/xul/skin/images/admin-graphic.png
new file mode 100644
index 0000000000..5f6c75ba82
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/admin-graphic.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/admin-symbol.png b/comm/suite/chatzilla/xul/skin/images/admin-symbol.png
new file mode 100644
index 0000000000..11da8ea98a
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/admin-symbol.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/arrow-down.png b/comm/suite/chatzilla/xul/skin/images/arrow-down.png
new file mode 100644
index 0000000000..6910fd19e6
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/arrow-down.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/chatzilla-16.png b/comm/suite/chatzilla/xul/skin/images/chatzilla-16.png
new file mode 100644
index 0000000000..3f462580a0
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/chatzilla-16.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/drop-indicator-bottom.png b/comm/suite/chatzilla/xul/skin/images/drop-indicator-bottom.png
new file mode 100644
index 0000000000..4196235cd3
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/drop-indicator-bottom.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/founder-graphic.png b/comm/suite/chatzilla/xul/skin/images/founder-graphic.png
new file mode 100644
index 0000000000..2328611847
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/founder-graphic.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/founder-symbol.png b/comm/suite/chatzilla/xul/skin/images/founder-symbol.png
new file mode 100644
index 0000000000..842230a849
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/founder-symbol.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/halfop-graphic.png b/comm/suite/chatzilla/xul/skin/images/halfop-graphic.png
new file mode 100644
index 0000000000..bc84fe7728
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/halfop-graphic.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/halfop-symbol.png b/comm/suite/chatzilla/xul/skin/images/halfop-symbol.png
new file mode 100644
index 0000000000..ca5654cd8e
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/halfop-symbol.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/input-send.png b/comm/suite/chatzilla/xul/skin/images/input-send.png
new file mode 100644
index 0000000000..fc1213f34a
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/input-send.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/logging-off.png b/comm/suite/chatzilla/xul/skin/images/logging-off.png
new file mode 100644
index 0000000000..e9f32a18ad
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/logging-off.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/logging-on.png b/comm/suite/chatzilla/xul/skin/images/logging-on.png
new file mode 100644
index 0000000000..349e23be6a
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/logging-on.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/logo.png b/comm/suite/chatzilla/xul/skin/images/logo.png
new file mode 100644
index 0000000000..f963f2c46c
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/logo.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/multiline-contract.png b/comm/suite/chatzilla/xul/skin/images/multiline-contract.png
new file mode 100644
index 0000000000..7ae5acd1ec
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/multiline-contract.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/multiline-expand.png b/comm/suite/chatzilla/xul/skin/images/multiline-expand.png
new file mode 100644
index 0000000000..0ada94b4ef
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/multiline-expand.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/no-graphic.png b/comm/suite/chatzilla/xul/skin/images/no-graphic.png
new file mode 100644
index 0000000000..cb27b2df0a
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/no-graphic.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/no-symbol.png b/comm/suite/chatzilla/xul/skin/images/no-symbol.png
new file mode 100644
index 0000000000..567ec2abf0
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/no-symbol.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/op-graphic.png b/comm/suite/chatzilla/xul/skin/images/op-graphic.png
new file mode 100644
index 0000000000..99a6c47ff8
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/op-graphic.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/op-symbol.png b/comm/suite/chatzilla/xul/skin/images/op-symbol.png
new file mode 100644
index 0000000000..84de14972c
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/op-symbol.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-off.png b/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-off.png
new file mode 100644
index 0000000000..0dde51eeaf
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-off.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-on.png b/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-on.png
new file mode 100644
index 0000000000..009a3d1f7b
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-on.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/source_svg/logging.svg b/comm/suite/chatzilla/xul/skin/images/source_svg/logging.svg
new file mode 100644
index 0000000000..b472e0bcf6
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/source_svg/logging.svg
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- 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/. -->
+
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="320"
+ height="160"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <g
+ transform="translate(0,-892.36218)"
+ id="layer1">
+ <path
+ d="M 95.147225,909.3621 133.18775,947.33552 125,957.36218 l -40,0 0,-40 10.147225,-8.00008 z"
+ id="rect2818-1"
+ style="fill:#dedede;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 25,907.36218 60,0 c 20,0 10,10 10,40 30,0 40,-10 40,10 l 0,90.00002 -110,0 0,-140.00002 z"
+ id="rect2818"
+ style="fill:#dedede;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 45,1027.3622 70,0"
+ id="path3615"
+ style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 45,1007.3622 70,0"
+ id="path3615-7"
+ style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 45,987.36218 70,0"
+ id="path3615-0"
+ style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 45,967.36218 70,0"
+ id="path3615-4"
+ style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 45,947.36218 70,0"
+ id="path3615-4-5"
+ style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 45,927.36218 50,0"
+ id="path3615-4-7"
+ style="fill:none;stroke:#000000;stroke-width:8.16496563;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 255.14722,909.3621 38.04053,37.97342 -8.18775,10.02666 -40,0 0,-40 10.14722,-8.00008 z"
+ id="rect2818-1-9"
+ style="fill:#dedede;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 185,907.36218 60,0 c 20,0 10,10 10,40 30,0 40,-10 40,10 l 0,90.00002 -110,0 0,-140.00002 z"
+ id="rect2818-9"
+ style="fill:#dedede;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ </g>
+</svg>
diff --git a/comm/suite/chatzilla/xul/skin/images/source_svg/userlist_icons.svg b/comm/suite/chatzilla/xul/skin/images/source_svg/userlist_icons.svg
new file mode 100644
index 0000000000..a5b507228c
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/source_svg/userlist_icons.svg
@@ -0,0 +1,636 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- 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/. -->
+
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ width="48"
+ height="72"
+ id="svg4597">
+ <defs
+ id="defs4599">
+ <linearGradient
+ x1="3.932596"
+ y1="4.0576153"
+ x2="11.188456"
+ y2="12.377568"
+ id="linearGradient6792"
+ xlink:href="#linearGradient4491"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.8333333,0,0,0.8333333,41.811051,54.89456)" />
+ <linearGradient
+ id="linearGradient4491">
+ <stop
+ id="stop4493"
+ style="stop-color:#008000;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop4495"
+ style="stop-color:#008000;stop-opacity:0"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ x1="3.932596"
+ y1="4.0576153"
+ x2="11.188456"
+ y2="12.377568"
+ id="linearGradient4678"
+ xlink:href="#linearGradient4491"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.8333333,0,0,0.8333333,41.811051,54.89456)" />
+ <filter
+ id="filter3268">
+ <feGaussianBlur
+ inkscape:collect="always"
+ id="feGaussianBlur3270"
+ stdDeviation="0.17400778" />
+ </filter>
+ <linearGradient
+ x1="5.8113861"
+ y1="12.271072"
+ x2="11.031619"
+ y2="7.3054457"
+ id="linearGradient5639"
+ xlink:href="#linearGradient5118"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient5118">
+ <stop
+ id="stop5120"
+ style="stop-color:#808080;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop5126"
+ style="stop-color:#b6b6b6;stop-opacity:1"
+ offset="1" />
+ <stop
+ id="stop5122"
+ style="stop-color:#808080;stop-opacity:0"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ x1="1.3302127"
+ y1="7.4045763"
+ x2="4.4953356"
+ y2="6.201149"
+ id="linearGradient5641"
+ xlink:href="#linearGradient3308"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient3308">
+ <stop
+ id="stop3310"
+ style="stop-color:#ffffff;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3312"
+ style="stop-color:#ffffff;stop-opacity:0"
+ offset="1" />
+ </linearGradient>
+ <filter
+ id="filter3362">
+ <feGaussianBlur
+ inkscape:collect="always"
+ id="feGaussianBlur3364"
+ stdDeviation="0.05131204" />
+ </filter>
+ <linearGradient
+ x1="5.7836757"
+ y1="2.3257139"
+ x2="5.6800237"
+ y2="5.6109982"
+ id="linearGradient5643"
+ xlink:href="#linearGradient3176"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient3176">
+ <stop
+ id="stop3178"
+ style="stop-color:#ffffff;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3180"
+ style="stop-color:#ffffff;stop-opacity:0"
+ offset="1" />
+ </linearGradient>
+ <filter
+ id="filter3290">
+ <feGaussianBlur
+ inkscape:collect="always"
+ id="feGaussianBlur3292"
+ stdDeviation="0.040874578" />
+ </filter>
+ <linearGradient
+ x1="5.8113861"
+ y1="12.520331"
+ x2="10.529106"
+ y2="7.3054457"
+ id="linearGradient5645"
+ xlink:href="#linearGradient5042"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient5042">
+ <stop
+ id="stop5044"
+ style="stop-color:#0066ff;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop5050"
+ style="stop-color:#22a0ff;stop-opacity:1"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ x1="1.3302127"
+ y1="7.4045763"
+ x2="4.4953356"
+ y2="6.201149"
+ id="linearGradient5647"
+ xlink:href="#linearGradient3308"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ x1="5.7836757"
+ y1="2.3257139"
+ x2="5.6800237"
+ y2="5.6109982"
+ id="linearGradient5649"
+ xlink:href="#linearGradient3176"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ x1="5.8113861"
+ y1="12.520331"
+ x2="11.026272"
+ y2="7.3054457"
+ id="linearGradient5651"
+ xlink:href="#linearGradient5221"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient5221">
+ <stop
+ id="stop5223"
+ style="stop-color:#00c800;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop5229"
+ style="stop-color:#2fff12;stop-opacity:1"
+ offset="1" />
+ <stop
+ id="stop5225"
+ style="stop-color:#00de00;stop-opacity:0"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ x1="1.3302127"
+ y1="7.4045763"
+ x2="4.4953356"
+ y2="6.201149"
+ id="linearGradient5653"
+ xlink:href="#linearGradient3308"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ x1="5.7836757"
+ y1="2.3257139"
+ x2="5.6800237"
+ y2="5.6109982"
+ id="linearGradient5655"
+ xlink:href="#linearGradient3176"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ x1="6.3511858"
+ y1="12.54519"
+ x2="11.05113"
+ y2="3.82529"
+ id="linearGradient5657"
+ xlink:href="#linearGradient4778"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient4778">
+ <stop
+ id="stop4780"
+ style="stop-color:#ff7700;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop4786"
+ style="stop-color:#ff9600;stop-opacity:1"
+ offset="0.96214288" />
+ <stop
+ id="stop4782"
+ style="stop-color:#ff7700;stop-opacity:0"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ x1="1.3302127"
+ y1="7.4045763"
+ x2="4.4953356"
+ y2="6.201149"
+ id="linearGradient5659"
+ xlink:href="#linearGradient3308"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ x1="5.766274"
+ y1="2.4264779"
+ x2="5.6800237"
+ y2="5.6109982"
+ id="linearGradient5661"
+ xlink:href="#linearGradient3176"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ x1="6.0393691"
+ y1="12.525679"
+ x2="10.796444"
+ y2="7.3054457"
+ id="linearGradient5663"
+ xlink:href="#linearGradient3141"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient3141">
+ <stop
+ id="stop3143"
+ style="stop-color:#7c0063;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop3149"
+ style="stop-color:#a60084;stop-opacity:1"
+ offset="1" />
+ <stop
+ id="stop3145"
+ style="stop-color:#7c0063;stop-opacity:0"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ x1="1.3302127"
+ y1="7.4045763"
+ x2="4.4953356"
+ y2="6.201149"
+ id="linearGradient5665"
+ xlink:href="#linearGradient3308"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ x1="5.7836757"
+ y1="2.3257139"
+ x2="5.6800237"
+ y2="5.6109982"
+ id="linearGradient5667"
+ xlink:href="#linearGradient3176"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ x1="5.8113861"
+ y1="12.520331"
+ x2="11.026272"
+ y2="7.3054457"
+ id="linearGradient5669"
+ xlink:href="#linearGradient5337-52-886-410"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ id="linearGradient5337-52-886-410">
+ <stop
+ id="stop4558"
+ style="stop-color:#005e00;stop-opacity:1"
+ offset="0" />
+ <stop
+ id="stop4560"
+ style="stop-color:#058c00;stop-opacity:1"
+ offset="1" />
+ <stop
+ id="stop4562"
+ style="stop-color:#005e00;stop-opacity:0"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ x1="1.3302127"
+ y1="7.4045763"
+ x2="4.4953356"
+ y2="6.201149"
+ id="linearGradient5671"
+ xlink:href="#linearGradient3308"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ x1="5.7836757"
+ y1="2.3257139"
+ x2="5.6800237"
+ y2="5.6109982"
+ id="linearGradient5673"
+ xlink:href="#linearGradient3176"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ x1="5.7836757"
+ y1="2.3257139"
+ x2="5.6800237"
+ y2="5.6109982"
+ id="linearGradient6033"
+ xlink:href="#linearGradient3176"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <g
+ id="layer2">
+ <rect
+ width="48"
+ height="72"
+ x="0"
+ y="1.4988011e-15"
+ id="rect4768"
+ style="opacity:1;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:35.43307114;marker:none;marker-start:none;marker-mid:none;marker-end:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ </g>
+ <g
+ id="layer1">
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHDSURB VCiRVZE9aFNhGIXPe7+bm6TXUlNM/KPUQtB2U1pw08WC+LModLBLpYuo2Tq4mHApCI7SRUGEgqmK Crp3KG6KWG0Q2iFoKhjvtalpk96f3O9+r0NISJ/Dmc573uUQM6PDq6nHxtCZkZv1NSfru37Fp+D1 jXd3bfRAncLKvfd36l+dXLjRPDk2O0Hm8EE0a7vhx+KHYuVzeXaenyoA0ABgee7t7R8vSw/t5Z+n jk6OkNIY0IDD40Oxi/evz6RPH/sGgABAnNfGE/anX4u11eoRBkCkQQURIk+C/QjGQAL9UV9mceHZ 2LnpC2/0vkFzZnNlIysgAAb+fKkg+Oci2Pa6pj0FY4suWZZ1SK+VndGWlKSD21LAdtmBZzfgOQ24 1QZ2Vh0YA2S6wJSupNqUkNgPg5suWqXfqJVs6BAIjzOUUnEt3p94EU+bXgiJtiPIrtsKEcHPMIQQ dW3ywbWqkUk+B9A96i2HiFA/EXA4Yawzc1EDgN3vW7com1iDTj3f2+W9Ycn+5aQrYmKhUCi0qHfp /JW5JfyVV9GITC0pgHQMdPbAOgl6lM/nn+xbuoNlWYNENM3Mgoh2UqnUUi6XCzr5f4P/6zdUCtoG AAAAAElFTkSuQmCC "
+ x="12"
+ y="0"
+ width="12"
+ height="12"
+ id="image4146"
+ style="display:inline" />
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAGzSURB VCiRZZFPaxNRFMV/782kqWltCmmqYCwJuCqCEPoFBCtIdyKYRcEvIPhvIwgZZhVwoxZcdOWqC4UW 0V0XfgBbqGCqFarSQuMkxJjGTGY6M2+eCzsY9cDhXrj87lkcobUmUeP+XHasVKhId7cQeWHTbUav Ckuf9xmSSIDDxxdvp/XXW+lJsyjKd2BihqjXihvryy/Vxkal9EKHABKg93Th7ni4+XBUO0Vx9jIE fQgHmNOzcqZSu9o/X962bVsCGDfPONkTvbfPRlQzh45BCFA+hB4oD1Lj5LLkfuxsXZi9tPjcTEXt 62nvSxED0BoONsHrgPsdvN82ww55oz1v2/ZpU/QbJaEBBWggjqG9C71v4DrQ24fWFlk5lQGumcoP D1CAOAaS6bvg1KFVBwH9uEgcxykZyZOv3aNMgDpOSRz/2XUEjprWQFfmH7zZc4L8qo7+AYb8sVvU 9dG5HSnligSQjb0b7wbnPhwF5n+ftw9Len1kwUXIR5ZlBWKoabF678raFJ35Cf0zMxBjOPKUfp8p f5KG+aRarS7/1XSiWq2W831/0TAMqZTqSilXLMsKkvsvIAPVEw1F5a4AAAAASUVORK5CYII= "
+ x="12"
+ y="12"
+ width="12"
+ height="12"
+ id="image4189"
+ style="display:inline" />
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHUSURB VCiRdZFPaxNRFMXPezNTdHQS61jpopUUqiIoEoiCuhAUqYWCG5EiSvwI4gfIZJeFi9KlCApKF/6p G7ELkUGKElottoK1pppx1ISEahibTpjOy3vXjdG48GzugXN/i3sPIyJ0lLmZSe47kBqvmt5AK4jr wddwppQtl9EtIgIR4ax7+mrm/X5vbOM4uXKGXtNLehDekSPuiWnkYXT2QEQ4545dS63a8d76Lrq/ eZfeygX6pEpUpyq9oTk69eRYKZ/PcyKCVjtSSy4mF27/sNZsBQmNcUQsQg8zoDMdSdYLNhjbxZW5 w+NHL97Thq6ksvM7ipcZA4gBZbmKFjbQYiEkk9jOEqjon7HoLw16D7/d0iuyOtRuK4D/vetVax6e /Ah/q4eS8Q6z7ClEr2YCOK+LSFSEIACq6xUc39HAc/UML3pcGAbHnvWDUEoZ3Iqtx+ZPMxaC0I4V hFC/J/3x8aZComYTgIC7F1y/r9I3DQL+ByVm++nQWnqFcz7FAcD/4meHl4aX9VD/F4oUdhYH6OTy SMgZn3AcJ2ZdTbPR66OPGlbjTHNL07Ta27C72U/pIP1B1/TJXC53AwC6AQBAoVCwoyi6pGkal1IG nPMpx3HiTv4Lj1QMG8iNFZwAAAAASUVORK5CYII= "
+ x="12"
+ y="24"
+ width="12"
+ height="12"
+ id="image4274"
+ style="display:inline" />
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAG4SURB VCiRVZExixpRFIXPez7fDDvgIII2BhJsQgqXYOEPkPgLUmwgso2dRUJ+gNNprEIKizQJCAnEIkQW 8gNstbCLIRJtAgqKi77MjG+cuSnCDJsPLqe458C9HEZEiGk0GnalUrkiouJ+v9+sVqtvg8HgF+7A 4kCn03lp2/aLUql0v1wuwzAMbLfbaDQafR0Oh1fT6TRIAr1e75VlWa9t205Xq1Vks1lIKSGlhOd5 6Pf7P6Moeug4TpQyDMNmjH2QUuYAgHMOIgLnHIwxSClRKBRyk8nksl6vf07VarVrImrExuPxiPP5 jCiKQEQQQiAIAszn83vj8fi9cF33gWEY0Fr/e4oxrNdrKKWglMLhcMBut4NlWRe+7z8Vnuf9FkIk 5hilFLTW2Gw2yS9ElOZhGN4opXQYhtBaIwiCROM5nU5wXZcA3DIiQrPZ/FQsFp8xxiCEQDqdhpQy 0cViQaZpfuecPxYAMJvNronoMp/PPzJNMzkviiIsl0sSQvzhnL9xHEezO02zVqv1hXP+hDF2wTkH AMpkMj+EEG/b7fa7/5qO6Xa7Od/3n6dSKR6G4S3n/KPjODre/wWkwN2ZHNKyngAAAABJRU5ErkJg gg== "
+ x="12"
+ y="48"
+ width="12"
+ height="12"
+ id="image4317"
+ style="display:inline" />
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHJSURB VCiRVZG/axNxGMY/971cbNJK2oZEW6IoZhAFq1CdXKoUcaqDoBWhq1MR/4BcM6WbiJOIOEgGhzgo VBDtIDhIJO1gNcVijbGQ1Dbmp7mcd/c6mGD6wPO+L7yfZ3h5NRGhp8nJh6H41Inr391AzNquluvr xaWN7NxX+iUiiAiXbr27fTpR2DyfduXRR5GX30SefGg6U/PLGVgwepwmIszMv7+TGz25yMiQMXsK DoUgFoLxQWg7YC4ufbkQyR43TdPTS6WzoVV//PFuMBL2BDQNbA86LnQ8GPSDMRYL597mJ2Znzj31 7erD1zbV+BHdBhFYKUKtA9U2/LKgZkNDD1AbiE4nk8mDvq3WvqOOoSMdoHv/Rhm2m/CzDaUmfK6B f2A0CFz1/ak3t5zgf1i6pQ6s2ZDfAZ8fjjkNPM8z1H6n+iJQ+WE7Njg2uJ1/3bHB7dppewxbJQGq avn5lUJkJ5cRx9sL980jq69lYqycV0qlFUDhzcpcvJj5pLcqe2HLI7r2Si6Gsy2ltLumadpa36e1 yzfuP6t40emGFwoOGb85YJTkzOHyus+n30skEg8A+gMApFKpsGVZN3VdV67rVpVSadM07d7+L8s2 8Sj9Mf2NAAAAAElFTkSuQmCC "
+ x="12"
+ y="36"
+ width="12"
+ height="12"
+ id="image4360"
+ style="display:inline" />
+ <text
+ x="25.851986"
+ y="22.37845"
+ id="text6605"
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-weight:normal;fill:#ff7f2a;fill-opacity:1;stroke:#4d4d4d;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;font-family:FranklinGotTDemCon"><tspan
+ x="25.851986"
+ y="22.37845"
+ id="tspan6607"
+ style="fill:#ff7f2a;stroke:#4d4d4d">&amp;</tspan></text>
+ <text
+ x="25.371283"
+ y="11.332599"
+ id="text6690"
+ xml:space="preserve"
+ style="font-size:15.14527416px;font-style:normal;font-weight:normal;fill:#ff00ff;fill-opacity:1;stroke:#800080;stroke-width:1.26210618px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;display:inline;font-family:FranklinGotTDemCon"><tspan
+ x="25.371283"
+ y="11.332599"
+ id="tspan6692"
+ style="stroke-width:1.26210618">~</tspan></text>
+ <text
+ x="23.484241"
+ y="32.947613"
+ id="text6738"
+ xml:space="preserve"
+ style="font-size:10.52353096px;font-style:normal;font-weight:normal;fill:#00ff00;fill-opacity:1;stroke:#4d4d4d;stroke-width:0.0876961;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;font-family:FranklinGotTDemCon"><tspan
+ x="23.484241"
+ y="32.947613"
+ id="tspan6740"
+ style="fill:#00ff00;stroke:#4d4d4d;stroke-width:0.0876961;stroke-miterlimit:4;stroke-dasharray:none">@</tspan></text>
+ <text
+ x="24.187044"
+ y="46.661697"
+ id="text6788"
+ xml:space="preserve"
+ style="font-size:12.85570812px;font-style:normal;font-weight:normal;fill:#008dff;fill-opacity:1;stroke:url(#linearGradient6792);stroke-width:0.10713092;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;font-family:FranklinGotTDemCon"><tspan
+ x="24.187044"
+ y="46.661697"
+ id="tspan6790"
+ style="fill:#008dff;fill-opacity:1;stroke:url(#linearGradient6792);stroke-width:0.10713092;stroke-miterlimit:4;stroke-dasharray:none">%</tspan></text>
+ <text
+ x="22.939159"
+ y="62.14077"
+ id="text6845"
+ xml:space="preserve"
+ style="font-size:22.86951828px;font-style:normal;font-weight:normal;fill:#00ff00;fill-opacity:1;stroke:#e3dbdb;stroke-width:0.19057931;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;font-family:FranklinGotTDemCon"><tspan
+ x="22.939159"
+ y="62.14077"
+ id="tspan6847"
+ style="fill:#808080;stroke:#e3dbdb;stroke-width:0.19057931;stroke-miterlimit:4;stroke-dasharray:none">+</tspan></text>
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADCSURB VCiRzZGhCsIAFEXPmxvDKAYtNoMg6A+YBD9C7P7GWBgD60DBYPUP/AQx2PwFlYGwNHEbY8/gBMVk ELzwyuUe7oUnqso3Mr5K/xTwxa8DmE/DFbcDTARpAQdF144650ACOyJaAHtgbpbhoYGxsbCsGjW5 cs1i4pkn3lbRNtAsgUeDIN2Cwk5JCQmpUpUGDSMhGeTkxMQA+dskQY6KjitUTjduo4xsqmhPkAsQ AKvXhl1B0XfUiUp+Wd6H5P8edwdEdUHuCYIzqwAAAABJRU5ErkJggg== "
+ x="36"
+ y="0"
+ width="12"
+ height="12"
+ id="image8696"
+ style="display:inline" />
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAFKSURB VCiRY/z//z8DKYAFXSA9PZ1V89ct73u/OfdNWrLtE7o8I7INjTEuWp6in9YzMf7/wMn0X/zwO+6k jHkH9yFrYELmWAl8jmNj+v/ApO+0+bd/TEe1eX+EotuAouHzX+b7oqx/jKcnOuQKsv62ePKD9Sxe DTuf/lr8/CfLm0TZt5MOveNbFDnzyBycfuhIdFTwEPqy6utfxg+CLH+l3/9huX/iA3e9MtfPqGVv pGtWrVr1HSWUdLh+prMw/mNY9N3E2/DvbTM/0Xcb/vxjWMDL+u8dTDGKk/hY/3H9Y2D6PnPmzN8Z c/Yd3fuWb5q90BedR9/ZLmJ1Um+ii2mkxJs997+z7f7xl+mWItfP2Mff2Z9p8n7X2fqKvyRx9qHp KDYUz99zetdr3nAWRsavvCz/FY594G2zn3TCfPtr/p7f/xjvYthALAAALLyYRtlkYjwAAAAASUVO RK5CYII= "
+ x="36"
+ y="12"
+ width="12"
+ height="12"
+ id="image8739"
+ style="display:inline" />
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHVSURB VCiRY/z//z8DKYAFmSO7U9bjo8bHNMZ/jBqs31l//OH8c4/rGdeeZ9bPZsDUMMEYEqcl8j5qfOzk esm1WmGngkPu6lwT0TOik38J/PIWuyDWBlPH+P//fwbVTaqGL/VfLhA9IRr6RemL2W/e38kM/xke cb7jfMj5ivPaO513Cfw3+Kff972/kYmBgYHhvfL7CN7HvDO/y3zX+SH2o1B6n3QW2xe2Gy9NX1az fWV7y/GWY+0n5U/WcCf94fhjJHhJ8NhX6a/J3E+5J1zOunxdaaPSIo63HJ8UTikcYv7O/PU/y39+ uAaW7ywSHI847v9j/6chfE74OgMDA8Mz+2dmTL+ZLm2btO3nV+mv7pwvOE8ibOD6c+ut3VsHxl+M F19ZvArTWqpl8VbrbRPrV9a34mfFm5h+Mkml7U9bANfA/ZJ7/0fVjxEK+xWqvgt/V3xh+iJP7LzY 1N88v4X/cfzjFj4vXL3QdGE0AwMDAwM04hhFLoms5LvHt0VrsZZtaGgo2////xnMO81lZPbKpPA+ 5L0teVjS4////5BgZWBgYGBkZGSUOSgT9V7pfRz7J3ZJ5t/M/35z//7L9JvpvNhpsa7rcddvweMB G2BkZGT8j0USAHiWxx8oaaFMAAAAAElFTkSuQmCC "
+ x="36"
+ y="24"
+ width="12"
+ height="12"
+ id="image8782"
+ style="display:inline" />
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAFeSURB VCiRlZDLK4RxFIaf8/1myi1JSRZyaSxFiWRDKEWyGkWSPwCFMdloTLkTwkbKhiTsrJSFRllZuqdE LCyUBSLzfcdiXGZYyLs5i3N730dUlf/IApDJmkQZ322X8VCPjHSmfTYl2JUtY+vzgkjMAvTPgZ0P z7m4Wqa/ziU0+LBSb5RvG65ISavgbakR5+GVeN+2bIjh0u/Baq7iaagYqmMtoW/XmIxk3JnpYN+q V22kzo99u6iB9cfoDJEPerKJqRhBwopzsSzDy4W4vaXc9bVDbUxo+aQkwW4PgAamLmRibxW93yd8 FcJd2gP6jIZm1O8/k59YZSxYhqlf4Km1iISVLeyDEJIUh8nLVF9Rm+sXaKnqhfNZOHKwkkoI73Rg pyRjytaisH7Mjg5WY8VnkdW0pAEN47wcQYEHU5ADr6dRWD9kynuxD2fUq3YExvEipnIYxME5H4A8 fmX4S+9JCYDPfaR61AAAAABJRU5ErkJggg== "
+ x="36"
+ y="36"
+ width="12"
+ height="12"
+ id="image8825"
+ style="display:inline" />
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADVSURB VCiRY/z//z8DCjhwgGXnly9RDAwMDO4+PksZGBj+IkszMaCBN5KSnNdu3Zpx7datac+fP2dHl8fQ QAjQXgPj+tWri1+8ehUJN4GRkeH1u3eGjAwM/4WEhC4wIAWKuLj4SpZvP37wv3z92hibaS9fvUIR 5+Xm3sV4bP9+ld+/f6vBBFlYWDiPnz27hImJ6b+psXH0v1+/fsLk2NnZbzOix8Obmzd5F2/d+pKB geFfRGSkmKSk5DeKPE2yBhZ0gR/c3H/ERUW3MzIy/v/69etfdHkAxMtOtq4bGL4AAAAASUVORK5C YII= "
+ x="36"
+ y="48"
+ width="12"
+ height="12"
+ id="image8868"
+ style="display:inline" />
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAXSURBVCiRY/z//z8DKYCJJNWjGkaQBgD1WAMVqVdojQAAAABJRU5ErkJggg== "
+ x="36"
+ y="60"
+ width="12"
+ height="12"
+ id="image4763" />
+ <image
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz AAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHbSURB VCiRVZGxaxNRAId/7717d00T6Uk1pInGWqSQ4iS4NIMhoOhcsFPBTf+ADqEmJhBQHN1cIrhLEQQj aEFLoZApFCXaWtGAtl4TrpdLLrnL3XsdNBK/5bd83/QjUkr8hWbvZ6/3ae8mDdHZaT5tWGbn5YeH m1WMQaSU2KpvxVZfrD7ao5+XF1PpUHYui1R0Ac12U6xX17cTrXO3K08qvwCAAmD3nt998FHs3FE0 HtK5Dk5VGPZvzEfn6crSStqKHb8rFAoXAYBtHG9c2xdfH/eFo/rCR4AAlmthEAygMAUhPokdp36m vW+GG7XGW8Uj3q2u3w0HQkBIF7sHu7AGFlpOC+bARGIqgR7vEaq5SwDWlI7TmYMgAAMkABcumkdN mH0Th/YBwpMR7FlfsCAunwKQUSIscig9CSkAgj8RAWB3bdiuDUVlCFgAXeoDIcRZSlzymgd8CB+Q Q2B8MQR8L4DW1ZBUZo8AfKO1Sq0a9aJvgP/l8eiKcxUzEzOblNJPFADiMr6cEqm6GqhyXFYdDYvd tMyczmwDeFoqlX6S0dP5fP58o91YM4bGDY94MZ3rE8lQshOPxF8JIZ6Vy+X3/54ekcvlphhjFyil lzjnzPO8H5zz78Vi0Rg5J1kK4/V0dy9HAAAAAElFTkSuQmCC "
+ x="12"
+ y="60"
+ width="12"
+ height="12"
+ id="image5815" />
+ <g
+ transform="translate(20,5e-7)"
+ id="g4788"
+ style="display:inline">
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(0.9638661,0.1841601,-0.1841601,0.963866,-17.87869,58.359647)"
+ id="path3043"
+ style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" />
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(0.9814535,0,0,0.9814535,-20.058185,58.569471)"
+ id="path3045"
+ style="fill:url(#linearGradient5669);fill-opacity:1;stroke:#003500;stroke-width:0.24858253;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z"
+ transform="matrix(0.5121728,0.8599464,-0.8599464,0.5121728,-11.048722,57.000296)"
+ id="path3047"
+ style="fill:none;stroke:url(#linearGradient5671);stroke-width:0.10042476;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" />
+ <path
+ d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z"
+ transform="matrix(0.9498646,0.5484046,-0.683379,1.1836471,-16.039471,55.616584)"
+ id="path3049"
+ style="fill:url(#linearGradient6033);fill-opacity:1;stroke:none;filter:url(#filter3290)" />
+ </g>
+ <g
+ transform="matrix(0.9705075,0,0,0.9705075,-0.146091,-0.1561012)"
+ id="g4816"
+ style="display:inline">
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(0.9876803,0.1887101,-0.1887101,0.9876802,2.38882,-1.5621111)"
+ id="path4818"
+ style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" />
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(1.0057022,0,0,1.0057022,0.1554762,-1.3471028)"
+ id="path4820"
+ style="fill:url(#linearGradient5663);fill-opacity:1;stroke:#7c0063;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z"
+ transform="matrix(0.524827,0.881193,-0.881193,0.524827,9.3875353,-2.9550479)"
+ id="path4822"
+ style="fill:none;stroke:url(#linearGradient5665);stroke-width:0.24374942;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" />
+ <path
+ d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z"
+ transform="matrix(0.9733328,0.561954,-0.7002632,1.2128914,4.2734795,-4.3729471)"
+ id="path4824"
+ style="fill:url(#linearGradient5667);fill-opacity:1;stroke:none;filter:url(#filter3290)" />
+ </g>
+ <g
+ transform="matrix(0.9758889,0,0,0.9758889,-0.209913,11.88922)"
+ id="g4826"
+ style="display:inline">
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(0.9876803,0.1887101,-0.1887101,0.9876802,2.3888195,-1.5621111)"
+ id="path4828"
+ style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" />
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(1.0057022,0,0,1.0057022,0.1554762,-1.3471028)"
+ id="path4830"
+ style="fill:url(#linearGradient5657);fill-opacity:1;stroke:#de5500;stroke-width:0.24858253;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z"
+ transform="matrix(0.524827,0.881193,-0.881193,0.524827,9.3875353,-2.9550479)"
+ id="path4832"
+ style="fill:none;stroke:url(#linearGradient5659);stroke-width:0.10068177;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" />
+ <path
+ d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z"
+ transform="matrix(0.9733328,0.561954,-0.7002632,1.2128914,4.2734795,-4.3729471)"
+ id="path4834"
+ style="fill:url(#linearGradient5661);fill-opacity:1;stroke:none;filter:url(#filter3290)" />
+ </g>
+ <g
+ transform="matrix(0.9758888,0,0,0.9758888,-0.209912,23.998424)"
+ id="g4836"
+ style="display:inline">
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(0.9876803,0.1887101,-0.1887101,0.9876802,2.3888195,-1.5621111)"
+ id="path4838"
+ style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" />
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(1.0057022,0,0,1.0057022,0.1554762,-1.3471028)"
+ id="path4840"
+ style="fill:url(#linearGradient5651);fill-opacity:1;stroke:#008000;stroke-width:0.24858253;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z"
+ transform="matrix(0.524827,0.881193,-0.881193,0.524827,9.387535,-2.9550479)"
+ id="path4842"
+ style="fill:none;stroke:url(#linearGradient5653);stroke-width:0.10068177;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" />
+ <path
+ d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z"
+ transform="matrix(0.9733328,0.561954,-0.7002632,1.2128914,4.2734795,-4.3729471)"
+ id="path4844"
+ style="fill:url(#linearGradient5655);fill-opacity:1;stroke:none;filter:url(#filter3290)" />
+ </g>
+ <g
+ transform="matrix(0.9758888,0,0,0.9758888,-0.209913,35.930851)"
+ id="g4846"
+ style="display:inline">
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(0.9876803,0.1887101,-0.1887101,0.9876802,2.38882,-1.5621111)"
+ id="path4848"
+ style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" />
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(1.0057022,0,0,1.0057022,0.1554762,-1.3471028)"
+ id="path4850"
+ style="fill:url(#linearGradient5645);fill-opacity:1;stroke:#000080;stroke-width:0.24858253;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z"
+ transform="matrix(0.524827,0.881193,-0.881193,0.524827,9.3875353,-2.9550479)"
+ id="path4852"
+ style="fill:none;stroke:url(#linearGradient5647);stroke-width:0.10042476;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" />
+ <path
+ d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z"
+ transform="matrix(0.9733328,0.561954,-0.7002632,1.2128914,4.2734795,-4.3729471)"
+ id="path4854"
+ style="fill:url(#linearGradient5649);fill-opacity:1;stroke:none;filter:url(#filter3290)" />
+ </g>
+ <g
+ transform="matrix(0.9758889,0,0,0.9758889,-0.209913,47.863277)"
+ id="g4856"
+ style="display:inline">
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(0.9876803,0.1887101,-0.1887101,0.9876802,2.3888195,-1.5621111)"
+ id="path4858"
+ style="fill:#808080;fill-opacity:1;stroke:#808080;stroke-width:0.25927806;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3268)" />
+ <path
+ d="m 10.90198,7.3054457 a 5.0905943,5.0905943 0 1 1 -10.18118818,0 5.0905943,5.0905943 0 1 1 10.18118818,0 z"
+ transform="matrix(1.0057022,0,0,1.0057022,0.1554762,-1.3471028)"
+ id="path4860"
+ style="fill:url(#linearGradient5639);fill-opacity:1;stroke:#333333;stroke-width:0.24858253;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 10.856926,7.3054457 a 5.0455399,5.0905943 0 1 1 -10.09107975,0 5.0455399,5.0905943 0 1 1 10.09107975,0 z"
+ transform="matrix(0.524827,0.881193,-0.881193,0.524827,9.3875353,-2.9550479)"
+ id="path4862"
+ style="fill:none;stroke:url(#linearGradient5641);stroke-width:0.10068177;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3362)" />
+ <path
+ d="m 3.0290077,4.1219881 c 0.0244451,-1.006404 1.4328056,-1.7962742 2.8378096,-1.7962742 1.405004,0 2.8555192,0.7621386 2.8788196,1.796285 0.0233005,1.0341464 -1.5961005,1.5010801 -2.9688013,1.4887622 -1.3046875,-0.0117076 -2.772273,-0.482369 -2.7478279,-1.488773 z"
+ transform="matrix(0.9733328,0.561954,-0.7002632,1.2128914,4.2734795,-4.3729471)"
+ id="path4864"
+ style="fill:url(#linearGradient5643);fill-opacity:1;stroke:none;filter:url(#filter3290)" />
+ </g>
+ </g>
+</svg>
diff --git a/comm/suite/chatzilla/xul/skin/images/spbubble-off.png b/comm/suite/chatzilla/xul/skin/images/spbubble-off.png
new file mode 100644
index 0000000000..d4c3fe2b0e
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/spbubble-off.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/spbubble-on.png b/comm/suite/chatzilla/xul/skin/images/spbubble-on.png
new file mode 100644
index 0000000000..f23d76ae0a
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/spbubble-on.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/voice-graphic.png b/comm/suite/chatzilla/xul/skin/images/voice-graphic.png
new file mode 100644
index 0000000000..77ee61dbad
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/voice-graphic.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/images/voice-symbol.png b/comm/suite/chatzilla/xul/skin/images/voice-symbol.png
new file mode 100644
index 0000000000..e1eb3a2481
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/images/voice-symbol.png
Binary files differ
diff --git a/comm/suite/chatzilla/xul/skin/install-plugin.css b/comm/suite/chatzilla/xul/skin/install-plugin.css
new file mode 100644
index 0000000000..d2fa232d97
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/install-plugin.css
@@ -0,0 +1,9 @@
+/* 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/. */
+
+window {
+ width: 35em;
+ padding: 0.5em;
+}
+
diff --git a/comm/suite/chatzilla/xul/skin/networks-edit.css b/comm/suite/chatzilla/xul/skin/networks-edit.css
new file mode 100644
index 0000000000..e60efe2ab4
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/networks-edit.css
@@ -0,0 +1,7 @@
+/* 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/. */
+
+#backgroundBox {
+ background-color: ThreeDLightShadow;
+}
diff --git a/comm/suite/chatzilla/xul/skin/output-dark.css b/comm/suite/chatzilla/xul/skin/output-dark.css
new file mode 100644
index 0000000000..39a4521e8c
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/output-dark.css
@@ -0,0 +1,226 @@
+/* -*- Mode: Text; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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/. */
+
+/*
+ * a dark background/light text version of the output window.
+ * see output-base.css for details.
+ */
+
+@import url(chrome://chatzilla/content/output-base.css);
+
+.chatzilla-body { /* The topmost container in the ChatZilla */
+ background: black; /* output window. */
+ color: lightgrey;
+}
+
+a.chatzilla-link {
+ color: #fffdd6;
+}
+
+a.chatzilla-link:visited {
+ color: lightgrey;
+}
+
+.chatzilla-line-marker {
+ box-shadow: 0px 2px darkgreen !important;
+}
+
+.header-outer {
+ background-color: black;
+}
+
+.header {
+ color: lightslategrey;
+ background-color: #333333;
+ border-radius: 7px;
+}
+
+.progress-fg {
+ background-color: silver;
+}
+
+.value {
+ color: silver;
+}
+
+/* Style userlist as white-on-black. */
+treechildren {
+ background: black;
+ color: white;
+}
+
+/* For Mac, who's theme sucks. */
+treechildren::-moz-tree-row(unselected),
+treechildren::-moz-tree-cell(unselected),
+treechildren::-moz-tree-cell-text(unselected) {
+ border-color: black !important;
+ background: black !important;
+ color: white !important;
+}
+
+#splash {
+ color: #444444;
+}
+
+#usr-descnodes,
+#ch-topicnodes {
+ color: white;
+}
+
+[condition] {
+ font-weight: bold;
+}
+
+[condition="red"] {
+ color: red;
+}
+
+[condition="yellow"] {
+ color: yellow;
+}
+
+[condition="green"] {
+ color: lightgreen;
+}
+
+.msg[msg-type="JOIN"] .msg-data a.chatzilla-link,
+.msg[msg-type="PART"] .msg-data a.chatzilla-link {
+ color: lightcyan;
+}
+
+.msg[msg-type="KICK"] .msg-data a.chatzilla-link {
+ color: #ff5700;
+}
+
+.chatzilla-rheet {
+ color: magenta !important;
+}
+
+.chatzilla-highlight[name="Slate"] {
+ color: white;
+ background: #15272d;
+}
+
+.chatzilla-highlight[name="Plum"] {
+ color: white;
+ background: #442144;
+}
+
+.chatzilla-highlight[name="Brown"] {
+ color: white;
+ background: #562a14;
+}
+
+.msg-type { /* .msg-type = message type */
+ color: silver; /* indicator */
+}
+
+.msg-user a.chatzilla-link,
+.msg-user { /* msg-user = nickname portion of */
+ color: white !important; /* a message (channel and query */
+} /* views) */
+
+.msg[mark="even"] .msg-data { /* use even/odd marks to create a */
+ color: white; /* subtle brightness change when */
+} /* the speaker changes. */
+
+.msg[msg-type="JOIN"] .msg-data,
+.msg[msg-type="PART"] .msg-data {
+ color: lightblue;
+}
+
+.msg[msg-type="PART"] .msg-data {
+ color: lightblue;
+}
+
+.msg[msg-type="HELLO"] .msg-data {
+ color: yellow;
+}
+
+.msg[msg-type="ERROR"] .msg-data,
+.msg[msg-type="DISCONNECT"] .msg-data {
+ background: red;
+ color: white;
+}
+
+.msg[msg-type="USAGE"] .msg-data {
+ color: white;
+}
+
+.msg[msg-type="ACTION"] .msg-data {
+ color: #6ac9ee;
+}
+
+.msg[msg-type="NICK"] .msg-data {
+ color: #96fa94;
+}
+
+.msg[msg-type="NOTICE"] .msg-data,
+.msg[msg-type="MODE"] .msg-data {
+ color: #60e066;
+}
+
+.msg[msg-type="NOTICE"] .msg-data a.chatzilla-link,
+.msg[msg-type="MODE"] .msg-data a.chatzilla-link {
+ color: #6dff74;
+}
+
+.msg[msg-type="KICK"] .msg-data {
+ color: #d85d24;
+}
+
+.msg[msg-type="QUIT"] .msg-data {
+ color: #f7b183;
+}
+
+/* important="true" means that the message has text from your /stalk list in
+ * it, has your nickname in it, or was spoken by someone in your /stalk list.
+ */
+.msg[important="true"] .msg-user,
+.msg[important="true"] .msg-data {
+ background: #333333 !important;
+}
+
+.msg-user:before,
+.msg-user:after {
+ color: blue;
+}
+
+.msg[msg-user$="ME!"] .msg-user:before,
+.msg[msg-user$="ME!"] .msg-user:after {
+ color: #6afc73;
+}
+
+.msg[msg-type="ACTION"] .msg-user:before,
+.msg[msg-type="ACTION"] .msg-user:after {
+ color: cyan;
+}
+
+.msg[msg-type="NOTICE"] .msg-user:before,
+.msg[msg-type="NOTICE"] .msg-user:after {
+ color: #6afc73;
+}
+
+/* private messages *not* in a query window */
+.msg[dest-type="IRCUser"] .msg-user:before,
+.msg[dest-type="IRCUser"] .msg-user:after {
+ color: #6afc73;
+}
+
+.msg[msg-dest$="ME!"] .msg-user:before,
+.msg[msg-dest$="ME!"] .msg-user:after {
+ color: magenta;
+}
+
+/* private messages in a query window */
+.msg[view-type="IRCUser"] .msg-user:before,
+.msg[view-type="IRCUser"] .msg-user:after {
+ color: white;
+}
+
+.msg[view-type="IRCUser"][msg-user$="ME!"] .msg-user:before,
+.msg[view-type="IRCUser"][msg-user$="ME!"] .msg-user:after {
+ color: #6afc73;
+}
diff --git a/comm/suite/chatzilla/xul/skin/output-default.css b/comm/suite/chatzilla/xul/skin/output-default.css
new file mode 100644
index 0000000000..af5c850a12
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/output-default.css
@@ -0,0 +1,67 @@
+/* -*- Mode: Text; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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/. */
+
+/*
+ * a version of the output window that uses only your default foreground and
+ * backgrond colors. Message types are distinguished by font changes.
+ * see output-base.css for details.
+ */
+
+@import url(chrome://chatzilla/content/output-base.css);
+
+.header-outer {
+ background-color: white;
+}
+
+.progress-fg {
+ background-color: grey;
+}
+
+.msg[msg-type="ACTION"] .msg-data {
+ font-style: italic;
+}
+
+.msg[msg-type="JOIN"] .msg-type,
+.msg[msg-type="PART"] .msg-type,
+.msg[msg-type="QUIT"] .msg-type {
+ font-weight: bold;
+}
+
+.msg[msg-type="QUIT"] .msg-data {
+ font-variant: small-caps;
+ font-weight: bold;
+}
+
+.msg[msg-type="JOIN"] .msg-data,
+.msg[msg-type="PART"] .msg-data {
+ font-variant: small-caps;
+}
+
+.msg[msg-type="HELLO"] .msg-data,
+.msg[msg-type="NICK"] .msg-type,
+.msg[msg-type="NOTICE"] .msg-data {
+ font-weight: bold;
+}
+
+.msg[msg-type="NICK"] .msg-data {
+ font-family: monospace;
+}
+
+/* :before and :after pseudoclasses form the decorations around nicknames. */
+.msg-user:before,
+.msg-user:after {
+ font-size: 100%;
+ font-family: monospace;
+ font-weight: bolder;
+}
+
+.msg[dest-type="IRCUser"] .msg-user,
+.msg[dest-type="IRCUser"][msg-dest$="ME!"] .msg-user {
+ font-style: italic;
+}
+
+.msg[msg-user$="ME!"] .msg-user {
+ font-weight: bold;
+}
diff --git a/comm/suite/chatzilla/xul/skin/output-light.css b/comm/suite/chatzilla/xul/skin/output-light.css
new file mode 100644
index 0000000000..c2a346333a
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/output-light.css
@@ -0,0 +1,217 @@
+/* -*- Mode: Text; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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/. */
+
+/*
+ * a light background/dark text version of the output window.
+ * see output-base.css for details.
+ */
+
+@import url(chrome://chatzilla/content/output-base.css);
+
+.chatzilla-body { /* The topmost container in the ChatZilla */
+ background: white; /* output window. */
+ color: #222222;
+}
+
+a.chatzilla-link {
+ font-weight: bold;
+ color: #342ecc;
+}
+
+.chatzilla-line-marker {
+ box-shadow: 0px 2px lightgray !important;
+}
+
+.header-outer {
+ background-color: #d1d0ea;
+}
+
+.header {
+ color: darkslategrey;
+ background-color: #EEEEEE;
+ border: 1px #777499 solid;
+ border-radius: 7px;
+}
+
+#splash {
+ color: #DDDDDD;
+}
+
+#usr-descnodes,
+#ch-topicnodes {
+ color: black;
+}
+
+.progress-fg {
+ background-color: darkslategrey;
+}
+
+[condition] {
+ font-weight: bold;
+}
+
+[condition="red"] {
+ color: red;
+}
+
+[condition="yellow"] {
+ color: orange;
+}
+
+[condition="green"] {
+ color: #2ec908;
+}
+
+.msg[msg-type="PRIVMSG"] .msg-data,
+.msg[msg-type="ACTION"] .msg-data {
+ background-color: #F0F0F0;
+}
+
+.msg[msg-type="HELLO"] .msg-data a.chatzilla-link {
+ color: #d7d9dd;
+}
+
+.msg[msg-type="JOIN"] .msg-data a.chatzilla-link,
+.msg[msg-type="PART"] .msg-data a.chatzilla-link {
+ font-weight: bold;
+ color: #11c411;
+}
+
+.msg[msg-type="ERROR"] .msg-data a.chatzilla-link,
+.msg[msg-type="DISCONNECT"] .msg-data a.chatzilla-link {
+ font-weight: bold;
+ color: white;
+}
+
+.msg[msg-type="KICK"] .msg-data a.chatzilla-link {
+ color: #aa0d08;
+}
+
+.msg[msg-type="NOTICE"] .msg-data a.chatzilla-link {
+ color: #d64444;
+}
+
+.msg[msg-type="QUIT"] .msg-data a.chatzilla-link {
+ color: #c46907;
+}
+
+.chatzilla-rheet {
+ color: #e25e00 !important;
+}
+
+.chatzilla-highlight[name="Slate"] {
+ color: black;
+ background: #b8c4e0;
+}
+
+.chatzilla-highlight[name="Plum"] {
+ color: black;
+ background: #ddb8d0;
+}
+
+.chatzilla-highlight[name="Brown"] {
+ color: black;
+ background: #ffbf77;
+}
+
+.msg-type { /* .msg-type = message type */
+ color: #686699; /* indicator */
+ font-weight: bold;
+}
+
+.msg-user a.chatzilla-link,
+.msg-user { /* msg-user = nickname portion of */
+ color: black !important; /* a message (channel and query */
+ font-weight: bold; /* views) */
+}
+
+.msg[mark="even"] .msg-data { /* use even/odd marks to create a */
+ color: #555555; /* subtle brightness change when */
+} /* the speaker changes. */
+
+.msg[msg-type="JOIN"] .msg-data,
+.msg[msg-type="PART"] .msg-data {
+ color: #0e9e0e;
+ background-color: #c3f7c3;
+ font-weight: bold;
+ border-radius: 5px 5px 5px 5px;
+}
+
+.msg[msg-type="QUIT"] .msg-data {
+ background: #fff196;
+ color: #ff8d02;
+ font-weight: bold;
+ border-radius: 5px 5px 5px 5px;
+}
+
+.msg[msg-type="HELLO"] .msg-data {
+ background: #1342a5;
+ color: white;
+ border-radius: 5px 5px 5px 5px;
+ font-weight: bold;
+}
+
+.msg[msg-type="ERROR"] .msg-data,
+.msg[msg-type="DISCONNECT"] .msg-data {
+ border-radius: 5px 5px 5px 5px;
+ background: #a8221e;
+ color: white;
+}
+
+.msg[msg-type="USAGE"] .msg-data {
+ color: black;
+}
+
+.msg[msg-type="ACTION"] .msg-data {
+ color: black;
+ font-style: italic;
+}
+
+.msg[msg-type="NICK"] .msg-data {
+ color: #4e8387;
+ background-color: #d5e9ea;
+ font-weight: bold;
+}
+
+.msg[msg-type="NOTICE"] .msg-data {
+ color: #ae4141;
+ font-weight: bold;
+}
+
+.msg[msg-type="MODE"] .msg-data {
+ color: #2709ed;
+ font-weight: bold;
+}
+
+.msg[msg-type="KICK"] .msg-data {
+ color: #ff1a0a;
+ background: #ffdbcc;
+ font-weight: bold;
+ border-radius: 5px 5px 5px 5px;
+}
+
+/* important="true" means that the message has text from your /stalk list in
+ * it, has your nickname in it, or was spoken by someone in your /stalk list.
+ */
+.msg[important="true"] .msg-user {
+ background: #d4d8d4;
+ border-radius: 5px 0px 0px 5px;
+}
+
+.msg[important="true"] .msg-data {
+ background: #eaefeb;
+}
+
+
+/* :before and :after pseudoclasses form the decorations around nicknames. */
+.msg-user:before,
+.msg-user:after {
+ color: #777499;
+}
+
+.msg[msg-user$="ME!"] .msg-user:before, /* the decoration around MY */
+.msg[msg-user$="ME!"] .msg-user:after { /* nick */
+ color: #843c6c;
+}
diff --git a/comm/suite/chatzilla/xul/skin/output-loud.css b/comm/suite/chatzilla/xul/skin/output-loud.css
new file mode 100644
index 0000000000..3bf7a94416
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/output-loud.css
@@ -0,0 +1,202 @@
+/* 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/. */
+
+body {
+
+ margin: 0px 0px 0px 0px;
+ background: black;
+
+}
+
+.bold {
+
+ font-weight: bold;
+
+}
+
+.italic {
+
+ font-style: italic;
+
+}
+
+.underline {
+
+ text-decoration: underline;
+
+}
+
+.strikethrough {
+
+ text-decoration: line-through;
+
+}
+
+.teletype {
+
+ font-family: monospace;
+
+}
+
+.smallcap {
+
+ font-variant: small-caps;
+
+}
+
+.rheet {
+
+ font-size: 14pt;
+ font-weight: bold;
+ color: magenta;
+
+}
+
+/* output from a chat session (contains msgs) */
+.chat-view {
+
+ vertical-align: text-bottom;
+
+}
+
+/* common container for all portions of a message
+ * (contains msg-*s) */
+.msg {
+
+ font-family: sans-serif;
+
+}
+
+.msg[user="!ME"] {
+
+ background: lightgrey;
+
+}
+
+/* message data in output window */
+.msg-data {
+
+ font-weight: bold;
+ color: lightgrey;
+ background: #1a2a44;
+
+}
+
+/* message data in output window */
+
+.msg-data[user="!ME"]{
+
+ background: black;
+
+}
+
+.msg-data[msgtype="JOIN"],
+.msg-data[msgtype="PART"] {
+
+ width: 100%;
+ font-variant: small-caps;
+ background: lightgray;
+ color: black;
+
+}
+
+.msg-data[msgtype="HELLO"] {
+
+ background: white;
+ color: darkgreen;
+
+}
+
+.msg-data[msgtype="ERROR"],
+.msg-data[msgtype="DISCONNECT"] {
+
+ background: red;
+ color: white;
+
+}
+
+.msg-data[msgtype="USAGE"] {
+
+ font-style: italic;
+ color: white;
+
+}
+
+.msg-data[msgtype="HELP"] {
+
+ font-weight: normal;
+
+}
+
+.msg-data[msgtype="ACTION"] {
+
+ color: cyan;
+
+}
+
+.msg-data[msgtype="NOTICE"] {
+
+ color: yellow;
+
+}
+
+.msg-data[msgtype="KICK"] {
+
+ background: orange;
+ color: yellow;
+
+}
+
+.msg-data[msgtype="QUIT"] {
+
+ background: lightgrey;
+ color: brown;
+
+}
+
+/* nickname field in output */
+.msg-user {
+
+ text-align: center;
+ vertical-align: middle;
+ color: lightgrey;
+ font-weight: bold;
+ background: grey;
+
+}
+
+.msg-user[parity="odd"]{
+
+ background: black;
+
+}
+
+.msg-user[user="!ME"] {
+
+ color : white;
+
+}
+
+.msg-user[msgtype="ACTION"] {
+
+ font-style: italic;
+
+}
+
+/* Message type indicator in output window */
+.msg-type {
+
+ text-align: center;
+ vertical-align: middle;
+ color: brown;
+ font-weight: bold;
+ background: lightgrey;
+
+}
+
+.msg-type[user="!ME"] {
+
+ background: silver;
+
+}
diff --git a/comm/suite/chatzilla/xul/skin/output-marble.css b/comm/suite/chatzilla/xul/skin/output-marble.css
new file mode 100644
index 0000000000..0de0dc1409
--- /dev/null
+++ b/comm/suite/chatzilla/xul/skin/output-marble.css
@@ -0,0 +1,148 @@
+/* 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/. */
+
+body {
+
+ margin: 0px 0px 0px 0px;
+ background: url(chrome://chatzilla/skin/images/xtal.jpg);
+
+}
+
+.bold {
+
+ font-weight: bold;
+
+}
+
+.italic {
+
+ font-style: italic;
+
+}
+
+.underline {
+
+ text-decoration: underline;
+
+}
+
+.strikethrough {
+
+ text-decoration: line-through;
+
+}
+
+.teletype {
+
+ font-family: monospace;
+
+}
+
+.smallcap {
+
+ font-variant: small-caps;
+
+}
+
+.rheet {
+
+ font-size: 14pt;
+ font-weight: bold;
+ color: magenta;
+
+}
+
+/* output from a chat session (contains msgs) */
+.chat-view {
+
+ vertical-align: text-bottom;
+
+}
+
+/* common container for all portions of a message
+ * (contains msg-*s) */
+.msg {
+
+ font-family: sans-serif;
+
+}
+
+.msg[user="!ME"] {
+
+ background: lightgrey;
+
+}
+
+/* message data in output window */
+.msg-data {
+
+ font-weight: bold;
+
+}
+
+.msg-data[msgtype="JOIN"],
+.msg-data[msgtype="PART"] {
+
+ font-variant: small-caps;
+ color: darkslategrey;
+
+}
+
+.msg-data[msgtype="ACTION"] {
+
+ color: darkred;
+
+}
+
+.msg-data[msgtype="NOTICE"] {
+
+ color: green;
+
+}
+
+.msg-data[msgtype="KICK"] {
+
+ color: slategrey;
+
+}
+
+.msg-data[msgtype="QUIT"] {
+
+ color: brown;
+
+}
+
+/* nickname field in output */
+.msg-user {
+
+ text-align: center;
+ vertical-align: middle;
+ color: blue;
+ font-weight: bold;
+ background: grey;
+
+}
+
+.msg-user[user="!ME"] {
+
+ color: green;
+
+}
+
+.msg-user[msgtype="ACTION"] {
+
+ font-style: italic;
+
+}
+
+/* Message type indicator in output window */
+.msg-type {
+
+ text-align: center;
+ vertical-align: middle;
+ color: brown;
+ font-weight: bold;
+ background: lightgrey;
+
+}