Revision control

Copy as Markdown

Other Tools

/* -*- 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/. */
ChromeUtils.defineModuleGetter(this, "AppConstants",
ChromeUtils.defineModuleGetter(this, "PlacesUtils",
#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://",
];
var testFailURLs = [
"irc:///",
"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"},
[{host: "irc.undernet.org", port: 6697, scheme: "ircs"},
[{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},
[{host: "undernet", target: "John", isnick: true},
[{host: "undernet", target: "#foo", key: "cz"},
[{host: "undernet", charset: "utf-8"}, "irc://undernet/?charset=utf-8"],
[{host: "undernet", target: "John", msg: "spam!"},
[{host: "undernet", target: "foo", isnick: true, msg: "spam!", 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(
"ChatZilla - " + source + " - " + event,
message, clickable, tabId, listener);
}
catch(ex)
{
// yup. it is probably a MAC or NsIAlertsService is not initialized
}
}