diff options
Diffstat (limited to '')
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="©version.label;"/> + </hbox> + </vbox> + </hbox> + <description id="description">&description.label;</description> + <label class="contributors-label header" value="§ion.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="§ion.locale.label;"/> + <vbox class="contributors" id="localizers"> + <!-- These are inserted from onLoad(), as read from locale file. --> + </vbox> + <label class="contributors-label header" value="§ion.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, """); + content = content.replace(/</g, "<").replace(/>/g, ">"); + 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"> </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 Binary files differnew file mode 100644 index 0000000000..5f6c75ba82 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/admin-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/admin-symbol.png b/comm/suite/chatzilla/xul/skin/images/admin-symbol.png Binary files differnew file mode 100644 index 0000000000..11da8ea98a --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/admin-symbol.png diff --git a/comm/suite/chatzilla/xul/skin/images/arrow-down.png b/comm/suite/chatzilla/xul/skin/images/arrow-down.png Binary files differnew file mode 100644 index 0000000000..6910fd19e6 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/arrow-down.png diff --git a/comm/suite/chatzilla/xul/skin/images/chatzilla-16.png b/comm/suite/chatzilla/xul/skin/images/chatzilla-16.png Binary files differnew file mode 100644 index 0000000000..3f462580a0 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/chatzilla-16.png diff --git a/comm/suite/chatzilla/xul/skin/images/drop-indicator-bottom.png b/comm/suite/chatzilla/xul/skin/images/drop-indicator-bottom.png Binary files differnew file mode 100644 index 0000000000..4196235cd3 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/drop-indicator-bottom.png diff --git a/comm/suite/chatzilla/xul/skin/images/founder-graphic.png b/comm/suite/chatzilla/xul/skin/images/founder-graphic.png Binary files differnew file mode 100644 index 0000000000..2328611847 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/founder-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/founder-symbol.png b/comm/suite/chatzilla/xul/skin/images/founder-symbol.png Binary files differnew file mode 100644 index 0000000000..842230a849 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/founder-symbol.png diff --git a/comm/suite/chatzilla/xul/skin/images/halfop-graphic.png b/comm/suite/chatzilla/xul/skin/images/halfop-graphic.png Binary files differnew file mode 100644 index 0000000000..bc84fe7728 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/halfop-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/halfop-symbol.png b/comm/suite/chatzilla/xul/skin/images/halfop-symbol.png Binary files differnew file mode 100644 index 0000000000..ca5654cd8e --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/halfop-symbol.png diff --git a/comm/suite/chatzilla/xul/skin/images/input-send.png b/comm/suite/chatzilla/xul/skin/images/input-send.png Binary files differnew file mode 100644 index 0000000000..fc1213f34a --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/input-send.png diff --git a/comm/suite/chatzilla/xul/skin/images/logging-off.png b/comm/suite/chatzilla/xul/skin/images/logging-off.png Binary files differnew file mode 100644 index 0000000000..e9f32a18ad --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/logging-off.png diff --git a/comm/suite/chatzilla/xul/skin/images/logging-on.png b/comm/suite/chatzilla/xul/skin/images/logging-on.png Binary files differnew file mode 100644 index 0000000000..349e23be6a --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/logging-on.png diff --git a/comm/suite/chatzilla/xul/skin/images/logo.png b/comm/suite/chatzilla/xul/skin/images/logo.png Binary files differnew file mode 100644 index 0000000000..f963f2c46c --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/logo.png diff --git a/comm/suite/chatzilla/xul/skin/images/multiline-contract.png b/comm/suite/chatzilla/xul/skin/images/multiline-contract.png Binary files differnew file mode 100644 index 0000000000..7ae5acd1ec --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/multiline-contract.png diff --git a/comm/suite/chatzilla/xul/skin/images/multiline-expand.png b/comm/suite/chatzilla/xul/skin/images/multiline-expand.png Binary files differnew file mode 100644 index 0000000000..0ada94b4ef --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/multiline-expand.png diff --git a/comm/suite/chatzilla/xul/skin/images/no-graphic.png b/comm/suite/chatzilla/xul/skin/images/no-graphic.png Binary files differnew file mode 100644 index 0000000000..cb27b2df0a --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/no-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/no-symbol.png b/comm/suite/chatzilla/xul/skin/images/no-symbol.png Binary files differnew file mode 100644 index 0000000000..567ec2abf0 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/no-symbol.png diff --git a/comm/suite/chatzilla/xul/skin/images/op-graphic.png b/comm/suite/chatzilla/xul/skin/images/op-graphic.png Binary files differnew file mode 100644 index 0000000000..99a6c47ff8 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/op-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/op-symbol.png b/comm/suite/chatzilla/xul/skin/images/op-symbol.png Binary files differnew file mode 100644 index 0000000000..84de14972c --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/op-symbol.png 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 Binary files differnew file mode 100644 index 0000000000..0dde51eeaf --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-off.png 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 Binary files differnew file mode 100644 index 0000000000..009a3d1f7b --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/source_png/spbubble-on.png 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">&</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 Binary files differnew file mode 100644 index 0000000000..d4c3fe2b0e --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/spbubble-off.png diff --git a/comm/suite/chatzilla/xul/skin/images/spbubble-on.png b/comm/suite/chatzilla/xul/skin/images/spbubble-on.png Binary files differnew file mode 100644 index 0000000000..f23d76ae0a --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/spbubble-on.png diff --git a/comm/suite/chatzilla/xul/skin/images/voice-graphic.png b/comm/suite/chatzilla/xul/skin/images/voice-graphic.png Binary files differnew file mode 100644 index 0000000000..77ee61dbad --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/voice-graphic.png diff --git a/comm/suite/chatzilla/xul/skin/images/voice-symbol.png b/comm/suite/chatzilla/xul/skin/images/voice-symbol.png Binary files differnew file mode 100644 index 0000000000..e1eb3a2481 --- /dev/null +++ b/comm/suite/chatzilla/xul/skin/images/voice-symbol.png 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; + +} |