[extra] Import Firefox 3.0 beta 5 tarball
[mozilla-extra.git] / extensions / irc / xul / content / static.js
blobd7ab9823ac61aae3046e721808d65d3cbf5f2099
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2  *
3  * ***** BEGIN LICENSE BLOCK *****
4  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
5  *
6  * The contents of this file are subject to the Mozilla Public License Version
7  * 1.1 (the "License"); you may not use this file except in compliance with
8  * the License. You may obtain a copy of the License at
9  * http://www.mozilla.org/MPL/
10  *
11  * Software distributed under the License is distributed on an "AS IS" basis,
12  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13  * for the specific language governing rights and limitations under the
14  * License.
15  *
16  * The Original Code is ChatZilla.
17  *
18  * The Initial Developer of the Original Code is
19  * Netscape Communications Corporation.
20  * Portions created by the Initial Developer are Copyright (C) 1998
21  * the Initial Developer. All Rights Reserved.
22  *
23  * Contributor(s):
24  *   Robert Ginda, <rginda@netscape.com>, original author
25  *   Chiaki Koufugata chiaki@mozilla.gr.jp UI i18n
26  *   Samuel Sieb, samuel@sieb.net, MIRC color codes, munger menu, and various
27  *   James Ross, silver@warwickcompsoc.co.uk
28  *
29  * Alternatively, the contents of this file may be used under the terms of
30  * either the GNU General Public License Version 2 or later (the "GPL"), or
31  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
32  * in which case the provisions of the GPL or the LGPL are applicable instead
33  * of those above. If you wish to allow use of your version of this file only
34  * under the terms of either the GPL or the LGPL, and not to allow others to
35  * use your version of this file under the terms of the MPL, indicate your
36  * decision by deleting the provisions above and replace them with the notice
37  * and other provisions required by the GPL or the LGPL. If you do not delete
38  * the provisions above, a recipient may use your version of this file under
39  * the terms of any one of the MPL, the GPL or the LGPL.
40  *
41  * ***** END LICENSE BLOCK ***** */
43 const __cz_version   = "0.9.81";
44 const __cz_condition = "green";
45 const __cz_suffix    = "";
46 const __cz_guid      = "59c81df5-4b7a-477b-912d-4e0fdf64e5f2";
47 const __cz_locale    = "0.9.81";
49 var warn;
50 var ASSERT;
51 var TEST;
53 if (DEBUG)
55     _dd_pfx = "cz: ";
56     warn = function (msg) { dumpln ("** WARNING " + msg + " **"); }
57     TEST = ASSERT = function _assert(expr, msg) {
58                  if (!expr) {
59                      dd("** ASSERTION FAILED: " + msg + " **\n" +
60                         getStackTrace() + "\n");
61                      return false;
62                  } else {
63                      return true;
64                  }
65              }
67 else
68     dd = warn = TEST = ASSERT = function (){};
70 var client = new Object();
72 client.TYPE = "IRCClient";
73 client.COMMAND_CHAR = "/";
74 client.STEP_TIMEOUT = 500;
75 client.MAX_MESSAGES = 200;
76 client.MAX_HISTORY = 50;
77 /* longest nick to show in display before forcing the message to a block level
78  * element */
79 client.MAX_NICK_DISPLAY = 14;
80 /* longest word to show in display before abbreviating */
81 client.MAX_WORD_DISPLAY = 20;
83 client.NOTIFY_TIMEOUT = 5 * 60 * 1000; /* update notify list every 5 minutes */
85 // Check every minute which networks have away statuses that need an update.
86 client.AWAY_TIMEOUT = 60 * 1000;
88 client.SLOPPY_NETWORKS = true; /* true if msgs from a network can be displayed
89                                 * on the current object if it is related to
90                                 * the network (ie, /whois results will appear
91                                 * on the channel you're viewing, if that channel
92                                 * is on the network that the results came from)
93                                 */
94 client.DOUBLETAB_TIME = 500;
95 client.HIDE_CODES = true;      /* true if you'd prefer to show numeric response
96                                 * codes as some default value (ie, "===") */
97 client.DEFAULT_RESPONSE_CODE = "===";
99 /* Maximum number of channels we'll try to list without complaining */
100 client.SAFE_LIST_COUNT = 500;
102 /* Minimum number of users above or below the conference limit the user count
103  * must go, before it is changed. This allows the user count to fluctuate
104  * around the limit without continously going on and off.
105  */
106 client.CONFERENCE_LOW_PASS = 10;
108 // Namespaces we happen to need:
109 const XHTML_NS = "http://www.w3.org/1999/xhtml";
111 client.viewsArray = new Array();
112 client.activityList = new Object();
113 client.hostCompat = new Object();
114 client.inputHistory = new Array();
115 client.lastHistoryReferenced = -1;
116 client.incompleteLine = "";
117 client.lastTabUp = new Date();
118 client.awayMsgs = new Array();
119 client.awayMsgCount = 5;
121 CIRCNetwork.prototype.INITIAL_CHANNEL = "";
122 CIRCNetwork.prototype.MAX_MESSAGES = 100;
123 CIRCNetwork.prototype.IGNORE_MOTD = false;
124 CIRCNetwork.prototype.RECLAIM_WAIT = 15000;
125 CIRCNetwork.prototype.RECLAIM_TIMEOUT = 400000;
126 CIRCNetwork.prototype.MIN_RECONNECT_MS = 15 * 1000;             // 15s
127 CIRCNetwork.prototype.MAX_RECONNECT_MS = 2 * 60 * 60 * 1000;    // 2h
129 CIRCServer.prototype.READ_TIMEOUT = 0;
130 CIRCServer.prototype.PRUNE_OLD_USERS = 0; // prune on user quit.
132 CIRCUser.prototype.MAX_MESSAGES = 200;
134 CIRCChannel.prototype.MAX_MESSAGES = 300;
136 CIRCChanUser.prototype.MAX_MESSAGES = 200;
138 function init()
140     if (("initialized" in client) && client.initialized)
141         return;
143     client.initialized = false;
145     client.networks = new Object();
146     client.entities = new Object();
147     client.eventPump = new CEventPump (200);
149     if (DEBUG)
150     {
151         /* hook all events EXCEPT server.poll and *.event-end types
152          * (the 4th param inverts the match) */
153         client.debugHook =
154             client.eventPump.addHook([{type: "poll", set:/^(server|dcc-chat)$/},
155                                     {type: "event-end"}], event_tracer,
156                                     "event-tracer", true /* negate */,
157                                     false /* disable */);
158     }
160     initApplicationCompatibility();
161     initMessages();
162     if (client.host == "")
163         showErrorDlg(getMsg(MSG_ERR_UNKNOWN_HOST, client.unknownUID));
165     initCommands();
166     initPrefs();
167     initMunger();
168     initNetworks();
169     initMenus();
170     initStatic();
171     initHandlers();
173     // Create DCC handler.
174     client.dcc = new CIRCDCC(client);
176     client.ident = new IdentServer(client);
178     // start logging.  nothing should call display() before this point.
179     if (client.prefs["log"])
180         client.openLogFile(client);
181     // kick-start a log-check interval to make sure we change logfiles in time:
182     // It will fire 2 seconds past the next full hour.
183     setTimeout("checkLogFiles()", 3602000 - (Number(new Date()) % 3600000));
185     // Make sure the userlist is on the correct side.
186     updateUserlistSide(client.prefs["userlistLeft"]);
188     client.display(MSG_WELCOME, "HELLO");
189     client.dispatch("set-current-view", { view: client });
191     importFromFrame("updateHeader");
192     importFromFrame("setHeaderState");
193     importFromFrame("changeCSS");
194     importFromFrame("updateMotifSettings");
195     importFromFrame("addUsers");
196     importFromFrame("updateUsers");
197     importFromFrame("removeUsers");
199     processStartupScripts();
201     client.commandManager.installKeys(document);
202     createMenus();
204     initIcons();
206     client.busy = false;
207     updateProgress();
208     initOfflineIcon();
209     client.isIdleAway = false;
210     initIdleAutoAway(client.prefs["awayIdleTime"]);
212     client.initialized = true;
214     dispatch("help", { hello: true });
215     dispatch("networks");
217     initInstrumentation();
218     setTimeout("dispatch('focus-input')", 0);
219     setTimeout(processStartupURLs, 0);
222 function initStatic()
224     client.mainWindow = window;
226     try
227     {
228         var io = Components.classes['@mozilla.org/network/io-service;1'];
229         client.iosvc = io.getService(Components.interfaces.nsIIOService);
230     }
231     catch (ex)
232     {
233         dd("IO service failed to initialize: " + ex);
234     }
235     
236     // Need this for the userlist
237     client.atomSvc = getService("@mozilla.org/atom-service;1", "nsIAtomService");
239     try
240     {
241         const nsISound = Components.interfaces.nsISound;
242         client.sound =
243             Components.classes["@mozilla.org/sound;1"].createInstance(nsISound);
245         client.soundList = new Object();
246     }
247     catch (ex)
248     {
249         dd("Sound failed to initialize: " + ex);
250     }
252     try
253     {
254         const nsIGlobalHistory = Components.interfaces.nsIGlobalHistory;
255         const GHIST_CONTRACTID = "@mozilla.org/browser/global-history;1";
256         client.globalHistory =
257             Components.classes[GHIST_CONTRACTID].getService(nsIGlobalHistory);
258     }
259     catch (ex)
260     {
261         dd("Global History failed to initialize: " + ex);
262     }
264     try
265     {
266         const nsISDateFormat = Components.interfaces.nsIScriptableDateFormat;
267         const DTFMT_CID = "@mozilla.org/intl/scriptabledateformat;1";
268         client.dtFormatter =
269             Components.classes[DTFMT_CID].createInstance(nsISDateFormat);
271         // Mmmm, fun. This ONLY affects the ChatZilla window, don't worry!
272         Date.prototype.toStringInt = Date.prototype.toString;
273         Date.prototype.toString = function() {
274             var dtf = client.dtFormatter;
275             return dtf.FormatDateTime("", dtf.dateFormatLong,
276                                       dtf.timeFormatSeconds,
277                                       this.getFullYear(), this.getMonth() + 1,
278                                       this.getDate(), this.getHours(),
279                                       this.getMinutes(), this.getSeconds()
280                                      );
281         }
282     }
283     catch (ex)
284     {
285         dd("Locale-correct date formatting failed to initialize: " + ex);
286     }
288     // XXX Bug 335998: See cmdHideView for usage of this.
289     client.hiddenDocument = document.implementation.createDocument(null, null, null);
291     multilineInputMode(client.prefs["multiline"]);
292     updateSpellcheck(client.prefs["inputSpellcheck"]);
293     if (client.prefs["showModeSymbols"])
294         setListMode("symbol");
295     else
296         setListMode("graphic");
298     var tree = document.getElementById('user-list');
299     tree.setAttribute("ondraggesture",
300                       "nsDragAndDrop.startDrag(event, userlistDNDObserver);");
302     setDebugMode(client.prefs["debugMode"]);
304     var ver = __cz_version + (__cz_suffix ? "-" + __cz_suffix : "");
306     var ua = navigator.userAgent;
307     var app = getService("@mozilla.org/xre/app-info;1", "nsIXULAppInfo");
308     if (app)
309     {
310         // Use the XUL host app info, and Gecko build ID.
311         if (app.ID == "{" + __cz_guid + "}")
312         {
313             // We ARE the app, in other words, we're running in XULrunner.
314             // Because of this, we must disregard app.(name|vendor|version).
315             // "XULRunner 1.7+/2005071506"
316             ua = "XULRunner " + app.platformVersion + "/" + app.platformBuildID;
318             // "XULRunner 1.7+/2005071506, Windows"
319             CIRCServer.prototype.HOST_RPLY = ua + ", " + client.platform;
320         }
321         else
322         {
323             // "Firefox 1.0+/2005071506"
324             ua = app.name + " " + app.version + "/";
325             if ("platformBuildID" in app) // 1.1 and up
326                 ua += app.platformBuildID;
327             else if ("geckoBuildID" in app) // 1.0 - 1.1 trunk only
328                 ua += app.geckoBuildID;
329             else // Uh oh!
330                 ua += "??????????";
332             // "Mozilla Firefox 1.0+, Windows"
333             CIRCServer.prototype.HOST_RPLY = app.vendor + " " + app.name + " " +
334                                              app.version + ", " + client.platform;
335         }
336     }
337     else
338     {
339         // Extract the revision number, and Gecko build ID.
340         var ary = navigator.userAgent.match(/(rv:[^;)\s]+).*?Gecko\/(\d+)/);
341         if (ary)
342         {
343             if (navigator.vendor)
344                 ua = navigator.vendor + " " + navigator.vendorSub; // FF 1.0
345             else
346                 ua = client.entities.brandShortName + " " + ary[1]; // Suite
347             ua = ua + "/" + ary[2];
348         }
349         CIRCServer.prototype.HOST_RPLY = client.entities.brandShortName + ", " +
350                                          client.platform;
351     }
353     client.userAgent = getMsg(MSG_VERSION_REPLY, [ver, ua]);
354     CIRCServer.prototype.VERSION_RPLY = client.userAgent;
355     CIRCServer.prototype.SOURCE_RPLY = MSG_SOURCE_REPLY;
357     client.statusBar = new Object();
359     client.statusBar["server-nick"] = document.getElementById("server-nick");
361     client.tabs = document.getElementById("views-tbar-inner");
362     client.tabDragBar = document.getElementById("tabs-drop-indicator-bar");
363     client.tabDragMarker = document.getElementById("tabs-drop-indicator");
365     client.statusElement = document.getElementById("status-text");
366     client.defaultStatus = MSG_DEFAULT_STATUS;
368     client.progressPanel = document.getElementById("status-progress-panel");
369     client.progressBar = document.getElementById("status-progress-bar");
371     client.logFile = null;
372     setInterval("onNotifyTimeout()", client.NOTIFY_TIMEOUT);
373     // Call every minute, will check only the networks necessary.
374     setInterval("onWhoTimeout()", client.AWAY_TIMEOUT);
376     client.awayMsgs = [{ message: MSG_AWAY_DEFAULT }];
377     var awayFile = new nsLocalFile(client.prefs["profilePath"]);
378     awayFile.append("awayMsgs.txt");
379     if (awayFile.exists())
380     {
381         var awayLoader = new TextSerializer(awayFile);
382         if (awayLoader.open("<"))
383         {
384             // Load the first item from the file.
385             var item = awayLoader.deserialize();
386             if (isinstance(item, Array))
387             {
388                 // If the first item is an array, it is the entire thing.
389                 client.awayMsgs = item;
390             }
391             else
392             {
393                 /* Not an array, so we have the old format of a single object
394                  * per entry.
395                  */
396                 client.awayMsgs = [item];
397                 while ((item = awayLoader.deserialize()))
398                     client.awayMsgs.push(item);
399             }
400             awayLoader.close();
401         }
402     }
404     client.defaultCompletion = client.COMMAND_CHAR + "help ";
406     client.deck = document.getElementById('output-deck');
409 function initApplicationCompatibility()
411     // This function does nothing more than tweak the UI based on the host
412     // application.
414     // Set up simple host and platform information.
415     client.host = "Unknown";
416     // Do we need to copy the icons? (not necessary on Gecko 1.8 and onwards,
417     // and install.js does it for us on SeaMonkey)
418     client.hostCompat.needToCopyIcons = false;
420     var app = getService("@mozilla.org/xre/app-info;1", "nsIXULAppInfo");
421     // nsIXULAppInfo wasn't implemented before 1.8...
422     if (app)
423     {
424         // Use the XULAppInfo.ID to find out what host we run on.
425         switch (app.ID)
426         {
427             case "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}":
428                 client.host = "Firefox";
429                 break;
430             case "{" + __cz_guid + "}":
431                 // We ARE the app, in other words, we're running in XULrunner.
432                 client.host = "XULrunner";
433                 break;
434             case "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": // SeaMonkey
435                 client.host = "Mozilla";
436                 break;
437             case "{a463f10c-3994-11da-9945-000d60ca027b}": // Flock
438                 client.host = "Flock";
439                 break;
440             case "{3db10fab-e461-4c80-8b97-957ad5f8ea47}": // Netscape
441                 client.host = "Netscape";
442                 break;
443             case "songbird@songbirdnest.com": // Songbird
444                 client.host = "Songbird";
445                 break;
446             default:
447                 client.unknownUID = app.ID;
448                 client.host = ""; // Unknown host, show an error later.
449         }
450     }
451     else if ("getBrowserURL" in window)
452     {
453         var url = getBrowserURL();
454         if (url == "chrome://navigator/content/navigator.xul")
455         {
456             client.host = "Mozilla";
457         }
458         else if (url == "chrome://browser/content/browser.xul")
459         {
460             client.hostCompat.needToCopyIcons = true;
461             client.host = "Firefox";
462         }
463         else
464         {
465             client.host = ""; // We don't know this host. Show an error later.
466             client.unknownUID = url;
467         }
468     }
470     client.platform = "Unknown";
471     if (navigator.platform.search(/mac/i) > -1)
472         client.platform = "Mac";
473     if (navigator.platform.search(/win/i) > -1)
474         client.platform = "Windows";
475     if (navigator.platform.search(/linux/i) > -1)
476         client.platform = "Linux";
477     if (navigator.platform.search(/os\/2/i) > -1)
478         client.platform = "OS/2";
480     client.hostPlatform = client.host + client.platform;
482     CIRCServer.prototype.OS_RPLY = navigator.oscpu + " (" +
483                                    navigator.platform + ")";
485     // Windows likes \r\n line endings, as wussy-notepad can't cope with just
486     // \n logs.
487     if (client.platform == "Windows")
488         client.lineEnd = "\r\n";
489     else
490         client.lineEnd = "\n";
493 function initIcons()
495     // Make sure we got the ChatZilla icon(s) in place first.
496     const iconName = "chatzilla-window";
497     const suffixes = [".ico", ".xpm", "16.xpm"];
499     /* when installing on Mozilla, the XPI has the power to put the icons where
500      * they are needed - in older versions of Firefox, it doesn't.
501      */
502     if (!client.hostCompat.needToCopyIcons)
503         return;
505     var sourceDir = getSpecialDirectory("ProfD");
506     sourceDir.append("extensions");
507     sourceDir.append("{" + __cz_guid + "}");
508     sourceDir.append("chrome");
509     sourceDir.append("icons");
510     sourceDir.append("default");
512     var destDir = getSpecialDirectory("AChrom");
513     destDir.append("icons");
514     destDir.append("default");
515     if (!destDir.exists())
516     {
517         try
518         {
519             mkdir(destDir);
520         }
521         catch(ex)
522         {
523             return;
524         }
525     }
527     for (var i = 0; i < suffixes.length; i++)
528     {
529         var iconDest = destDir.clone();
530         iconDest.append(iconName + suffixes[i]);
531         var iconSrc = sourceDir.clone();
532         iconSrc.append(iconName + suffixes[i]);
534         if (iconSrc.exists() && !iconDest.exists())
535         {
536             try
537             {
538                 iconSrc.copyTo(iconDest.parent, iconDest.leafName);
539             }
540             catch(ex){}
541         }
542     }
545 function initInstrumentation()
547     // Make sure we assign the user a random key - this is not used for
548     // anything except percentage chance of participation.
549     if (client.prefs["instrumentation.key"] == 0)
550     {
551         var rand = 1 + Math.round(Math.random() * 10000);
552         client.prefs["instrumentation.key"] = rand;
553     }
555     runInstrumentation("inst1");
558 function runInstrumentation(name, firstRun)
560     if (!/^inst\d+$/.test(name))
561         return;
563     // Values:
564     //   0 = not answered question
565     //   1 = allowed inst
566     //   2 = denied inst
568     if (client.prefs["instrumentation." + name] == 0)
569     {
570         // We only want 1% of people to be asked here.
571         if (client.prefs["instrumentation.key"] > 100)
572             return;
574         // User has not seen the info about this system. Show them the info.
575         var cmdYes = "allow-" + name;
576         var cmdNo = "deny-" + name;
577         var btnYes = getMsg(MSG_INST1_COMMAND_YES, cmdYes);
578         var btnNo  = getMsg(MSG_INST1_COMMAND_NO,  cmdNo);
579         client.munger.getRule(".inline-buttons").enabled = true;
580         client.display(getMsg("msg." + name + ".msg1", [btnYes, btnNo]));
581         client.display(getMsg("msg." + name + ".msg2", [cmdYes, cmdNo]));
582         client.munger.getRule(".inline-buttons").enabled = false;
584         // Don't hide *client* if we're asking the user about the startup ping.
585         client.lockView = true;
586         return;
587     }
589     if (client.prefs["instrumentation." + name] != 1)
590         return;
592     if (name == "inst1")
593         runInstrumentation1(firstRun);
596 function runInstrumentation1(firstRun)
598     function inst1onLoad()
599     {
600         if (/OK/.test(req.responseText))
601             client.display(MSG_INST1_MSGRPLY2);
602         else
603             client.display(getMsg(MSG_INST1_MSGRPLY1, MSG_UNKNOWN));
604     };
606     function inst1onError()
607     {
608         client.display(getMsg(MSG_INST1_MSGRPLY1, req.statusText));
609     };
611     try
612     {
613         const baseURI = "http://silver.warwickcompsoc.co.uk/" +
614                         "mozilla/chatzilla/instrumentation/startup?";
616         if (firstRun)
617         {
618             // Do a first-run ping here.
619             var frReq = new XMLHttpRequest();
620             frReq.open("GET", baseURI + "first-run");
621             frReq.send(null);
622         }
624         var data = new Array();
625         data.push("ver=" + encodeURIComponent(CIRCServer.prototype.VERSION_RPLY));
626         data.push("host=" + encodeURIComponent(client.hostPlatform));
627         data.push("chost=" + encodeURIComponent(CIRCServer.prototype.HOST_RPLY));
628         data.push("cos=" + encodeURIComponent(CIRCServer.prototype.OS_RPLY));
630         var url = baseURI + data.join("&");
632         var req = new XMLHttpRequest();
633         req.onload = inst1onLoad;
634         req.onerror = inst1onError;
635         req.open("GET", url);
636         req.send(null);
637     }
638     catch (ex)
639     {
640         client.display(getMsg(MSG_INST1_MSGRPLY1, formatException(ex)));
641     }
644 function getFindData(e)
646     var findData = new nsFindInstData();
647     findData.browser = e.sourceObject.frame;
648     findData.rootSearchWindow = getContentWindow(e.sourceObject.frame);
649     findData.currentSearchWindow = getContentWindow(e.sourceObject.frame);
651     /* Yay, evil hacks! findData.init doesn't care about the findService, it
652      * gets option settings from webBrowserFind. As we want the wrap option *on*
653      * when we use /find foo, we set it on the findService there. However,
654      * restoring the original value afterwards doesn't help, because init() here
655      * overrides that value. Unless we make .init do something else, of course:
656      */
657     findData._init = findData.init;
658     findData.init =
659         function init()
660         {
661             this._init();
662             const FINDSVC_ID = "@mozilla.org/find/find_service;1";
663             var findService = getService(FINDSVC_ID, "nsIFindService");
664             this.webBrowserFind.wrapFind = findService.wrapFind;
665         };
667     return findData;
670 function importFromFrame(method)
672     client.__defineGetter__(method, import_wrapper);
673     CIRCNetwork.prototype.__defineGetter__(method, import_wrapper);
674     CIRCChannel.prototype.__defineGetter__(method, import_wrapper);
675     CIRCUser.prototype.__defineGetter__(method, import_wrapper);
676     CIRCDCCChat.prototype.__defineGetter__(method, import_wrapper);
677     CIRCDCCFileTransfer.prototype.__defineGetter__(method, import_wrapper);
679     function import_wrapper()
680     {
681         var dummy = function(){};
683         if (!("frame" in this))
684             return dummy;
686         try
687         {
688             var window = getContentWindow(this.frame);
689             if (window && "initialized" in window && window.initialized &&
690                 method in window)
691             {
692                 return function import_wrapper_apply()
693                 {
694                     window[method].apply(this, arguments);
695                 };
696             }
697         }
698         catch (ex)
699         {
700             ASSERT(0, "Caught exception calling: " + method + "\n" + ex);
701         }
703         return dummy;
704     };
707 function processStartupScripts()
709     client.plugins = new Array();
710     var scripts = client.prefs["initialScripts"];
711     for (var i = 0; i < scripts.length; ++i)
712     {
713         if (scripts[i].search(/^file:|chrome:/i) != 0)
714         {
715             display(getMsg(MSG_ERR_INVALID_SCHEME, scripts[i]), MT_ERROR);
716             continue;
717         }
719         var path = getFileFromURLSpec(scripts[i]);
721         if (!path.exists())
722         {
723             display(getMsg(MSG_ERR_ITEM_NOT_FOUND, scripts[i]), MT_WARN);
724             continue;
725         }
727         if (path.isDirectory())
728             loadPluginDirectory(path);
729         else
730             loadLocalFile(path);
731     }
734 function loadPluginDirectory(localPath, recurse)
736     if (typeof recurse == "undefined")
737         recurse = 1;
739     var initPath = localPath.clone();
740     initPath.append("init.js");
741     if (initPath.exists())
742         loadLocalFile(initPath);
744     if (recurse < 1)
745         return;
747     var enumer = localPath.directoryEntries;
748     while (enumer.hasMoreElements())
749     {
750         var entry = enumer.getNext();
751         entry = entry.QueryInterface(Components.interfaces.nsILocalFile);
752         if (entry.isDirectory())
753             loadPluginDirectory(entry, recurse - 1);
754     }
757 function loadLocalFile(localFile)
759     var url = getURLSpecFromFile(localFile);
760     var glob = new Object();
761     dispatch("load", {url: url, scope: glob});
764 function getPluginById(id)
766     for (var i = 0; i < client.plugins.length; ++i)
767     {
768         if (client.plugins[i].id == id)
769             return client.plugins[i];
771     }
773     return null;
776 function getPluginIndexById(id)
778     for (var i = 0; i < client.plugins.length; ++i)
779     {
780         if (client.plugins[i].id == id)
781             return i;
783     }
785     return -1;
788 function getPluginByURL(url)
790     for (var i = 0; i < client.plugins.length; ++i)
791     {
792         if (client.plugins[i].url == url)
793             return client.plugins[i];
795     }
797     return null;
800 function getPluginIndexByURL(url)
802     for (var i = 0; i < client.plugins.length; ++i)
803     {
804         if (client.plugins[i].url == url)
805             return i;
807     }
809     return -1;
812 function processStartupURLs()
814     var wentSomewhere = false;
816     if ("arguments" in window &&
817         0 in window.arguments && typeof window.arguments[0] == "object" &&
818         "url" in window.arguments[0])
819     {
820         var url = window.arguments[0].url;
821         if (url.search(/^ircs?:\/?\/?\/?$/i) == -1)
822         {
823             /* if the url is not irc: irc:/, irc://, or ircs equiv then go to it. */
824             gotoIRCURL(url);
825             wentSomewhere = true;
826         }
827     }
828     /* check to see whether the URL has been passed via the command line
829        instead. */
830     else if ("arguments" in window &&
831         0 in window.arguments && typeof window.arguments[0] == "string")
832     {
833         var url = window.arguments[0]
834         var urlMatches = url.match(/^ircs?:\/\/\/?(.*)$/)
835         if (urlMatches)
836         {
837             if (urlMatches[1])
838             {
839                 /* if the url is not "irc://", "irc:///" or an ircs equiv then
840                    go to it. */
841                 gotoIRCURL(url);
842                 wentSomewhere = true;
843             }
844         }
845         else if (url)
846         {
847             /* URL parameter is not blank, but does not not conform to the
848                irc[s] scheme. */
849             display(getMsg(MSG_ERR_INVALID_SCHEME, url), MT_ERROR);
850         }
851     }
853     if (!wentSomewhere)
854     {
855         /* if we had nowhere else to go, connect to any default urls */
856         var ary = client.prefs["initialURLs"];
857         for (var i = 0; i < ary.length; ++i)
858         {
859             if (ary[i] && ary[i] == "irc:///")
860             {
861                 // Clean out "default network" entries, which we don't
862                 // support any more; replace with the harmless irc:// URL.
863                 ary[i] = "irc://";
864                 client.prefs["initialURLs"].update();
865             }
866             if (ary[i] && ary[i] != "irc://")
867                 gotoIRCURL(ary[i]);
868         }
869     }
871     if (client.viewsArray.length > 1 && !isStartupURL("irc://"))
872         dispatch("delete-view", { view: client });
874     /* XXX: If we have the "stop XBL breaking" hidden tab, remove it, to
875      * stop XBL breaking later. Oh, the irony.
876      */
877     if (client.tabs.firstChild.hidden)
878     {
879         client.tabs.removeChild(client.tabs.firstChild);
880         updateTabAttributes();
881     }
884 function destroy()
886     destroyPrefs();
889 function setStatus (str)
891     client.statusElement.setAttribute ("label", str);
892     return str;
895 client.__defineSetter__ ("status", setStatus);
897 function getStatus ()
899     return client.statusElement.getAttribute ("label");
902 client.__defineGetter__ ("status", getStatus);
904 function isVisible (id)
906     var e = document.getElementById(id);
908     if (!ASSERT(e,"Bogus id ``" + id + "'' passed to isVisible() **"))
909         return false;
911     return (e.getAttribute ("collapsed") != "true");
914 client.getConnectedNetworks =
915 function getConnectedNetworks()
917     var rv = [];
918     for (var n in client.networks)
919     {
920         if (client.networks[n].isConnected())
921             rv.push(client.networks[n]);
922     }
923     return rv;
926 function combineNicks(nickList, max)
928     if (!max)
929         max = 4;
931     var combinedList = [];
933     for (var i = 0; i < nickList.length; i += max)
934     {
935         count = Math.min(max, nickList.length - i);
936         var nicks = nickList.slice(i, i + count);
937         var str = new String(nicks.join(" "));
938         str.count = count;
939         combinedList.push(str);
940     }
942     return combinedList;
945 function updateAllStalkExpressions()
947     var list = client.prefs["stalkWords"];
949     for (var name in client.networks)
950     {
951         if ("stalkExpression" in client.networks[name])
952             updateStalkExpression(client.networks[name], list);
953     }
956 function updateStalkExpression(network)
958     function escapeChar(ch)
959     {
960         return "\\" + ch;
961     };
963     var list = client.prefs["stalkWords"];
965     var ary = new Array();
967     ary.push(network.primServ.me.unicodeName.replace(/[^\w\d]/g, escapeChar));
969     for (var i = 0; i < list.length; ++i)
970         ary.push(list[i].replace(/[^\w\d]/g, escapeChar));
972     var re;
973     if (client.prefs["stalkWholeWords"])
974         re = "(^|[\\W\\s])((" + ary.join(")|(") + "))([\\W\\s]|$)";
975     else
976         re = "(" + ary.join(")|(") + ")";
978     network.stalkExpression = new RegExp(re, "i");
981 function getDefaultFontSize()
983     const PREF_CTRID = "@mozilla.org/preferences-service;1";
984     const nsIPrefService = Components.interfaces.nsIPrefService;
985     const nsIPrefBranch = Components.interfaces.nsIPrefBranch;
987     var prefSvc = Components.classes[PREF_CTRID].getService(nsIPrefService);
988     var prefBranch = prefSvc.getBranch(null);
990     // PX size pref: font.size.variable.x-western
991     var pxSize = 16;
992     try
993     {
994         pxSize = prefBranch.getIntPref("font.size.variable.x-western");
995     }
996     catch(ex) { }
998     var dpi = 96;
999     try
1000     {
1001         // Get the DPI the fun way (make Mozilla do the work).
1002         var b = document.createElement("box");
1003         b.style.width = "1in";
1004         dpi = window.getComputedStyle(b, null).width.match(/^\d+/);
1005     }
1006     catch(ex)
1007     {
1008         try
1009         {
1010             // Get the DPI the fun way (make Mozilla do the work).
1011             b = document.createElementNS("box", XHTML_NS);
1012             b.style.width = "1in";
1013             dpi = window.getComputedStyle(b, null).width.match(/^\d+/);
1014         }
1015         catch(ex) { }
1016     }
1018     return Math.round((pxSize / dpi) * 72);
1021 function getDefaultContext(cx)
1023     if (!cx)
1024         cx = new Object();
1025     /* Use __proto__ here and in all other get*Context so that the command can
1026      * tell the difference between getObjectDetails and actual parameters. See
1027      * cmdJoin for more details.
1028      */
1029     cx.__proto__ = getObjectDetails(client.currentObject);
1030     return cx;
1033 function getMessagesContext(cx, element)
1035     if (!cx)
1036         cx = new Object();
1037     cx.__proto__ = getObjectDetails(client.currentObject);
1038     if (!element)
1039         element = document.popupNode;
1041     while (element)
1042     {
1043         switch (element.localName)
1044         {
1045             case "a":
1046                 var href = element.getAttribute("href");
1047                 cx.url = href;
1048                 break;
1050             case "tr":
1051                 var nickname = element.getAttribute("msg-user");
1052                 if (!nickname)
1053                     break;
1055                 // strip out  a potential ME! suffix
1056                 var ary = nickname.match(/([^ ]+)/);
1057                 nickname = ary[1];
1059                 if (!cx.network)
1060                     break;
1062                 // NOTE: nickname is the unicodeName here!
1063                 if (cx.channel)
1064                     cx.user = cx.channel.getUser(nickname);
1065                 else
1066                     cx.user = cx.network.getUser(nickname);
1068                 if (cx.user)
1069                 {
1070                     cx.nickname = cx.user.unicodeName;
1071                     cx.canonNick = cx.user.canonicalName;
1072                 }
1073                 else
1074                 {
1075                     cx.nickname = nickname;
1076                 }
1077                 break;
1078         }
1080         element = element.parentNode;
1081     }
1083     return cx;
1086 function getTabContext(cx, element)
1088     if (!cx)
1089         cx = new Object();
1090     if (!element)
1091         element = document.popupNode;
1093     while (element)
1094     {
1095         if (element.localName == "tab")
1096         {
1097             cx.__proto__ = getObjectDetails(element.view);
1098             return cx;
1099         }
1100         element = element.parentNode;
1101     }
1103     return cx;
1106 function getUserlistContext(cx)
1108     if (!cx)
1109         cx = new Object();
1110     cx.__proto__ = getObjectDetails(client.currentObject);
1111     if (!cx.channel)
1112         return cx;
1114     var user, tree = document.getElementById("user-list");
1115     cx.userList = new Array();
1116     cx.canonNickList = new Array();
1117     cx.nicknameList = getSelectedNicknames(tree);
1119     for (var i = 0; i < cx.nicknameList.length; ++i)
1120     {
1121         user = cx.channel.getUser(cx.nicknameList[i])
1122         cx.userList.push(user);
1123         cx.canonNickList.push(user.canonicalName);
1124         if (i == 0)
1125         {
1126             cx.user = user;
1127             cx.nickname = user.unicodeName;
1128             cx.canonNick = user.canonicalName;
1129         }
1130     }
1131     cx.userCount = cx.userList.length;
1133     return cx;
1136 function getSelectedNicknames(tree)
1138     var rv = [];
1139     if (!tree || !tree.view || !tree.view.selection)
1140         return rv;
1141     var rangeCount = tree.view.selection.getRangeCount();
1143     // Loop through the selection ranges.
1144     for (var i = 0; i < rangeCount; ++i)
1145     {
1146         var start = {}, end = {};
1147         tree.view.selection.getRangeAt(i, start, end);
1149         // If they == -1, we've got no selection, so bail.
1150         if ((start.value == -1) && (end.value == -1))
1151             continue;
1152         /* Workaround: Because we use select(-1) instead of clearSelection()
1153          * (see bug 197667) the tree will then give us selection ranges
1154          * starting from -1 instead of 0! (See bug 319066.)
1155          */
1156         if (start.value == -1)
1157             start.value = 0;
1159         // Loop through the contents of the current selection range.
1160         for (var k = start.value; k <= end.value; ++k)
1161             rv.push(getNicknameForUserlistRow(k));
1162     }
1163     return rv;
1166 function getFontContext(cx)
1168     if (!cx)
1169         cx = new Object();
1170     cx.__proto__ = getObjectDetails(client.currentObject);
1171     cx.fontSizeDefault = getDefaultFontSize();
1172     var view = client;
1174     if ("prefs" in cx.sourceObject)
1175     {
1176         cx.fontFamily = view.prefs["font.family"];
1177         if (cx.fontFamily.match(/^(default|(sans-)?serif|monospace)$/))
1178             delete cx.fontFamily;
1180         cx.fontSize = view.prefs["font.size"];
1181         if (cx.fontSize == 0)
1182             delete cx.fontSize;
1183     }
1185     return cx;
1188 function msgIsImportant(msg, sourceNick, network)
1190     var plainMsg = removeColorCodes(msg);
1192     var re = network.stalkExpression;
1193     if (plainMsg.search(re) != -1 || sourceNick && sourceNick.search(re) == 0)
1194         return true;
1196     return false;
1199 function ensureCachedCanonicalURLs(array)
1201     if ("canonicalURLs" in array)
1202         return;
1204     /* Caching this on the array is safe because the PrefManager constructs
1205      * a new array if the preference changes, but otherwise keeps the same
1206      * one around.
1207      */
1208     array.canonicalURLs = new Array();
1209     for (var i = 0; i < array.length; i++)
1210         array.canonicalURLs.push(makeCanonicalIRCURL(array[i]));
1213 function isStartupURL(url)
1215     // We canonicalize all URLs before we do the (string) comparison.
1216     url = makeCanonicalIRCURL(url);
1217     var list = client.prefs["initialURLs"];
1218     ensureCachedCanonicalURLs(list);
1219     return arrayContains(list.canonicalURLs, url);
1222 function cycleView(amount)
1224     var len = client.viewsArray.length;
1225     if (len <= 1)
1226         return;
1228     var tb = getTabForObject (client.currentObject);
1229     if (!tb)
1230         return;
1232     var vk = Number(tb.getAttribute("viewKey"));
1233     var destKey = (vk + amount) % len; /* wrap around */
1234     if (destKey < 0)
1235         destKey += len;
1237     dispatch("set-current-view", { view: client.viewsArray[destKey].source });
1240 // Plays the sound for a particular event on a type of object.
1241 function playEventSounds(type, event)
1243     if (!client.sound || !client.prefs["sound.enabled"])
1244         return;
1246     // Converts .TYPE values into the event object names.
1247     // IRCChannel => channel, IRCUser => user, etc.
1248     if (type.match(/^IRC/))
1249         type = type.substr(3, type.length).toLowerCase();
1251     var ev = type + "." + event;
1253     if (ev in client.soundList)
1254         return;
1256     if (!(("sound." + ev) in client.prefs))
1257         return;
1259     var s = client.prefs["sound." + ev];
1261     if (!s)
1262         return;
1264     if (client.prefs["sound.overlapDelay"] > 0)
1265     {
1266         client.soundList[ev] = true;
1267         setTimeout("delete client.soundList['" + ev + "']",
1268                    client.prefs["sound.overlapDelay"]);
1269     }
1271     if (event == "start")
1272     {
1273         blockEventSounds(type, "event");
1274         blockEventSounds(type, "chat");
1275         blockEventSounds(type, "stalk");
1276     }
1278     playSounds(s);
1281 // Blocks a particular type of event sound occuring.
1282 function blockEventSounds(type, event)
1284     if (!client.sound || !client.prefs["sound.enabled"])
1285         return;
1287     // Converts .TYPE values into the event object names.
1288     // IRCChannel => channel, IRCUser => user, etc.
1289     if (type.match(/^IRC/))
1290         type = type.substr(3, type.length).toLowerCase();
1292     var ev = type + "." + event;
1294     if (client.prefs["sound.overlapDelay"] > 0)
1295     {
1296         client.soundList[ev] = true;
1297         setTimeout("delete client.soundList['" + ev + "']",
1298                    client.prefs["sound.overlapDelay"]);
1299     }
1302 function playSounds(list)
1304     var ary = list.split (" ");
1305     if (ary.length == 0)
1306         return;
1308     playSound(ary[0]);
1309     for (var i = 1; i < ary.length; ++i)
1310         setTimeout(playSound, 250 * i, ary[i]);
1313 function playSound(file)
1315     if (!client.sound || !client.prefs["sound.enabled"] || !file)
1316         return;
1318     if (file == "beep")
1319     {
1320         client.sound.beep();
1321     }
1322     else
1323     {
1324         try
1325         {
1326             var uri = client.iosvc.newURI(file, null, null);
1327             client.sound.play(uri);
1328         }
1329         catch (ex)
1330         {
1331             // ignore exceptions from this pile of code.
1332         }
1333     }
1336 /* timer-based mainloop */
1337 function mainStep()
1339     try
1340     {
1341         var count = client.eventPump.stepEvents();
1342         if (count > 0)
1343             setTimeout("mainStep()", client.STEP_TIMEOUT);
1344         else
1345             setTimeout("mainStep()", client.STEP_TIMEOUT / 5);
1346     }
1347     catch(ex)
1348     {
1349         dd("Exception in mainStep!");
1350         dd(formatException(ex));
1351         setTimeout("mainStep()", client.STEP_TIMEOUT);
1352     }
1355 function openQueryTab(server, nick)
1357     var user = server.addUser(nick);
1358     if (client.globalHistory)
1359         client.globalHistory.addPage(user.getURL());
1360     if (!("messages" in user))
1361     {
1362         var value = "";
1363         var same = true;
1364         for (var c in server.channels)
1365         {
1366             var chan = server.channels[c];
1367             if (!(user.canonicalName in chan.users))
1368                 continue;
1369             /* This takes a boolean value for each channel (true - channel has
1370              * same value as first), and &&-s them all together. Thus, |same|
1371              * will tell us, at the end, if all the channels found have the
1372              * same value for charset.
1373              */
1374             if (value)
1375                 same = same && (value == chan.prefs["charset"]);
1376             else
1377                 value = chan.prefs["charset"];
1378         }
1379         /* If we've got a value, and it's the same accross all channels,
1380          * we use it as the *default* for the charset pref. If not, it'll
1381          * just keep the "defer" default which pulls it off the network.
1382          */
1383         if (value && same)
1384         {
1385             user.prefManager.prefRecords["charset"].defaultValue = value;
1386         }
1388         user.displayHere (getMsg(MSG_QUERY_OPENED, user.unicodeName));
1389     }
1390     user.whois();
1391     return user;
1394 function arraySpeak (ary, single, plural)
1396     var rv = "";
1397     var and = MSG_AND;
1399     switch (ary.length)
1400     {
1401         case 0:
1402             break;
1404         case 1:
1405             rv = ary[0];
1406             if (single)
1407                 rv += " " + single;
1408             break;
1410         case 2:
1411             rv = ary[0] + " " + and + " " + ary[1];
1412             if (plural)
1413                 rv += " " + plural;
1414             break;
1416         default:
1417             for (var i = 0; i < ary.length - 1; ++i)
1418                 rv += ary[i] + ", ";
1419             rv += and + " " + ary[ary.length - 1];
1420             if (plural)
1421                 rv += " " + plural;
1422             break;
1423     }
1425     return rv;
1429 function getObjectDetails (obj, rv)
1431     if (!rv)
1432         rv = new Object();
1434     if (!ASSERT(obj && typeof obj == "object",
1435                 "INVALID OBJECT passed to getObjectDetails (" + obj + "). **"))
1436     {
1437         return rv;
1438     }
1440     rv.sourceObject = obj;
1441     rv.TYPE = obj.TYPE;
1442     rv.parent = ("parent" in obj) ? obj.parent : null;
1443     rv.user = null;
1444     rv.channel = null;
1445     rv.server = null;
1446     rv.network = null;
1448     switch (obj.TYPE)
1449     {
1450         case "IRCChannel":
1451             rv.viewType = MSG_CHANNEL;
1452             rv.channel = obj;
1453             rv.channelName = obj.unicodeName;
1454             rv.server = rv.channel.parent;
1455             rv.network = rv.server.parent;
1456             break;
1458         case "IRCUser":
1459             rv.viewType = MSG_USER;
1460             rv.user = obj;
1461             rv.userName = obj.unicodeName;
1462             rv.server = rv.user.parent;
1463             rv.network = rv.server.parent;
1464             break;
1466         case "IRCChanUser":
1467             rv.viewType = MSG_USER;
1468             rv.user = obj;
1469             rv.userName = obj.unicodeName;
1470             rv.channel = rv.user.parent;
1471             rv.server = rv.channel.parent;
1472             rv.network = rv.server.parent;
1473             break;
1475         case "IRCNetwork":
1476             rv.network = obj;
1477             rv.viewType = MSG_NETWORK;
1478             if ("primServ" in rv.network)
1479                 rv.server = rv.network.primServ;
1480             else
1481                 rv.server = null;
1482             break;
1484         case "IRCClient":
1485             rv.viewType = MSG_TAB;
1486             break;
1488         case "IRCDCCUser":
1489             //rv.viewType = MSG_USER;
1490             rv.user = obj;
1491             rv.userName = obj.unicodeName;
1492             break;
1494         case "IRCDCCChat":
1495             //rv.viewType = MSG_USER;
1496             rv.chat = obj;
1497             rv.user = obj.user;
1498             rv.userName = obj.unicodeName;
1499             break;
1501         case "IRCDCCFileTransfer":
1502             //rv.viewType = MSG_USER;
1503             rv.file = obj;
1504             rv.user = obj.user;
1505             rv.userName = obj.unicodeName;
1506             rv.fileName = obj.filename;
1507             break;
1509         default:
1510             /* no setup for unknown object */
1511             break;
1512     }
1514     if (rv.network)
1515         rv.networkName = rv.network.unicodeName;
1517     return rv;
1521 function findDynamicRule (selector)
1523     var rules = frames[0].document.styleSheets[1].cssRules;
1525     if (isinstance(selector, RegExp))
1526         fun = "search";
1527     else
1528         fun = "indexOf";
1530     for (var i = 0; i < rules.length; ++i)
1531     {
1532         var rule = rules.item(i);
1533         if (rule.selectorText && rule.selectorText[fun](selector) == 0)
1534             return {sheet: frames[0].document.styleSheets[1], rule: rule,
1535                     index: i};
1536     }
1538     return null;
1541 function addDynamicRule (rule)
1543     var rules = frames[0].document.styleSheets[1];
1545     var pos = rules.cssRules.length;
1546     rules.insertRule (rule, pos);
1549 function getCommandEnabled(command)
1551     try {
1552         var dispatcher = document.commandDispatcher;
1553         var controller = dispatcher.getControllerForCommand(command);
1555         return controller.isCommandEnabled(command);
1556     }
1557     catch (e)
1558     {
1559         return false;
1560     }
1563 function doCommand(command)
1565     try {
1566         var dispatcher = document.commandDispatcher;
1567         var controller = dispatcher.getControllerForCommand(command);
1568         if (controller && controller.isCommandEnabled(command))
1569             controller.doCommand(command);
1570     }
1571     catch (e)
1572     {
1573     }
1576 function doCommandWithParams(command, params)
1578     try {
1579         var dispatcher = document.commandDispatcher;
1580         var controller = dispatcher.getControllerForCommand(command);
1581         controller.QueryInterface(Components.interfaces.nsICommandController);
1583         if (!controller || !controller.isCommandEnabled(command))
1584             return;
1586         var cmdparams = newObject("@mozilla.org/embedcomp/command-params;1",
1587                                   "nsICommandParams");
1588         for (var i in params)
1589             cmdparams.setISupportsValue(i, params[i]);
1591         controller.doCommandWithParams(command, cmdparams);
1592     }
1593     catch (e)
1594     {
1595     }
1598 var testURLs =
1599     ["irc:", "irc://", "irc:///", "irc:///help", "irc:///help,needkey",
1600     "irc://irc.foo.org", "irc://foo:6666",
1601     "irc://foo", "irc://irc.foo.org/", "irc://foo:6666/", "irc://foo/",
1602     "irc://irc.foo.org/,needpass", "irc://foo/,isserver",
1603     "irc://moznet/,isserver", "irc://moznet/",
1604     "irc://foo/chatzilla", "irc://foo/chatzilla/",
1605     "irc://irc.foo.org/?msg=hello%20there",
1606     "irc://irc.foo.org/?msg=hello%20there&ignorethis",
1607     "irc://irc.foo.org/%23mozilla,needkey?msg=hello%20there&ignorethis",
1608     "invalids",
1609     "irc://irc.foo.org/,isnick"];
1611 function doURLTest()
1613     for (var u in testURLs)
1614     {
1615         dd("testing url \"" + testURLs[u] + "\"");
1616         var o = parseIRCURL(testURLs[u]);
1617         if (!o)
1618             dd("PARSE FAILED!");
1619         else
1620             dd(dumpObjectTree(o));
1621         dd("---");
1622     }
1625 var testIRCURLObjects =
1626     [
1627      [{}, "irc://"],
1628      [{host: "undernet"},                                    "irc://undernet/"],
1629      [{host: "irc.undernet.org"},                    "irc://irc.undernet.org/"],
1630      [{host: "irc.undernet.org", isserver: true},    "irc://irc.undernet.org/"],
1631      [{host: "undernet", isserver: true},           "irc://undernet/,isserver"],
1632      [{host: "irc.undernet.org", port: 6667},        "irc://irc.undernet.org/"],
1633      [{host: "irc.undernet.org", port: 1},         "irc://irc.undernet.org:1/"],
1634      [{host: "irc.undernet.org", port: 1, scheme: "ircs"},
1635                                                   "ircs://irc.undernet.org:1/"],
1636      [{host: "irc.undernet.org", port: 9999, scheme: "ircs"},
1637                                                     "ircs://irc.undernet.org/"],
1638      [{host: "undernet", needpass: true},           "irc://undernet/,needpass"],
1639      [{host: "undernet", pass: "cz"},                "irc://undernet/?pass=cz"],
1640      [{host: "undernet", charset: "utf-8"},    "irc://undernet/?charset=utf-8"],
1641      [{host: "undernet", target: "#foo"},              "irc://undernet/%23foo"],
1642      [{host: "undernet", target: "#foo", needkey: true},
1643                                                "irc://undernet/%23foo,needkey"],
1644      [{host: "undernet", target: "John", isnick: true},
1645                                                   "irc://undernet/John,isnick"],
1646      [{host: "undernet", target: "#foo", key: "cz"},
1647                                                 "irc://undernet/%23foo?key=cz"],
1648      [{host: "undernet", charset: "utf-8"},    "irc://undernet/?charset=utf-8"],
1649      [{host: "undernet", target: "John", msg: "spam!"},
1650                                              "irc://undernet/John?msg=spam%21"],
1651      [{host: "undernet", target: "foo", isnick: true, msg: "spam!", pass: "cz"},
1652                                "irc://undernet/foo,isnick?msg=spam%21&pass=cz"]
1653     ];
1655 function doObjectURLtest()
1657     var passed = 0, total = testIRCURLObjects.length;
1658     for (var i = 0; i < total; i++)
1659     {
1660         var obj = testIRCURLObjects[i][0];
1661         var url = testIRCURLObjects[i][1];
1662         var parsedURL = constructIRCURL(obj)
1663         if (url != parsedURL)
1664         {
1665             display("Parsed IRC Object incorrectly! Expected '" + url +
1666                     "', got '" + parsedURL, MT_ERROR);
1667         }
1668         else
1669         {
1670             passed++;
1671         }
1672     }
1673     display("Passed " + passed + " out of " + total + " tests (" +
1674             passed / total * 100 + "%).", MT_INFO);
1678 function gotoIRCURL(url, e)
1680     var urlspec = url;
1681     if (typeof url == "string")
1682         url = parseIRCURL(url);
1684     if (!url)
1685     {
1686         window.alert(getMsg(MSG_ERR_BAD_IRCURL, urlspec));
1687         return;
1688     }
1690     if (!url.host)
1691     {
1692         /* focus the *client* view for irc:, irc:/, and irc:// (the only irc
1693          * urls that don't have a host.  (irc:/// implies a connect to the
1694          * default network.)
1695          */
1696         client.pendingViewContext = e;
1697         dispatch("client");
1698         delete client.pendingViewContext;
1699         return;
1700     }
1702     var network;
1704     if (url.isserver)
1705     {
1706         var alreadyThere = false;
1707         var gettingThere = false;
1708         for (var n in client.networks)
1709         {
1710             if ((client.networks[n].isConnected()) &&
1711                 (client.networks[n].primServ.hostname == url.host) &&
1712                 (client.networks[n].primServ.port == url.port))
1713             {
1714                 /* already connected to this server/port */
1715                 network = client.networks[n];
1716                 gettingThere = true;
1717                 // Have we actually had the 001 reply yet?
1718                 alreadyThere = (client.networks[n].state == NET_ONLINE);
1719                 break;
1720             }
1721         }
1723         if (!alreadyThere)
1724         {
1725             if (!gettingThere)
1726             {
1727                 /*
1728                 dd ("gotoIRCURL: not yet had connected to server" + url.host +
1729                     " trying to connect...");
1730                 */
1732                 // Do we have a password, if needed?
1733                 var pass = "";
1734                 if (url.needpass)
1735                 {
1736                     if (url.pass)
1737                     {
1738                         pass = url.pass;
1739                     }
1740                     else
1741                     {
1742                         pass = promptPassword(getMsg(MSG_HOST_PASSWORD,
1743                                                      url.host));
1744                     }
1745                 }
1747                 client.pendingViewContext = e;
1748                 var params = {hostname: url.host, port: url.port,
1749                               password: pass};
1750                 var cmd = (url.scheme == "ircs" ? "sslserver" : "server");
1751                 network = dispatch(cmd, params);
1752                 delete client.pendingViewContext;
1753             }
1755             if (!url.target)
1756                 return;
1758             if (!("pendingURLs" in network))
1759                 network.pendingURLs = new Array();
1760             network.pendingURLs.unshift({ url: url, e: e });
1761             return;
1762         }
1763     }
1764     else
1765     {
1766         /* parsed as a network name */
1767         if (!(url.host in client.networks))
1768         {
1769             display(getMsg(MSG_ERR_UNKNOWN_NETWORK, url.host));
1770             return;
1771         }
1773         network = client.networks[url.host];
1774         if (network.state != NET_ONLINE)
1775         {
1776             /*
1777             dd ("gotoIRCURL: not already connected to " +
1778                 "network " + url.host + " trying to connect...");
1779             */
1780             var secure = (url.scheme == "ircs" ? true : false);
1781             if (!network.isConnected())
1782             {
1783                 client.pendingViewContext = e;
1784                 client.connectToNetwork(network, secure);
1785                 delete client.pendingViewContext;
1786             }
1788             if (!url.target)
1789                 return;
1791             if (!("pendingURLs" in network))
1792                 network.pendingURLs = new Array();
1793             network.pendingURLs.unshift({ url: url, e: e });
1794             return;
1795         }
1796     }
1798     /* already connected, do whatever comes next in the url */
1799     //dd ("gotoIRCURL: connected, time to finish parsing ``" + url + "''");
1800     if (url.target)
1801     {
1802         var targetObject;
1803         var ev;
1804         if (url.isnick)
1805         {
1806             /* url points to a person. */
1807             var nick = url.target;
1808             var ary = url.target.split("!");
1809             if (ary)
1810                 nick = ary[0];
1812             client.pendingViewContext = e;
1813             targetObject = network.dispatch("query", {nickname: nick});
1814             delete client.pendingViewContext;
1815         }
1816         else
1817         {
1818             /* url points to a channel */
1819             var key;
1820             if (url.needkey)
1821             {
1822                 if (url.key)
1823                     key = url.key;
1824                 else
1825                     key = window.promptPassword(getMsg(MSG_URL_KEY, url.spec));
1826             }
1828             if (url.charset)
1829             {
1830                 client.pendingViewContext = e;
1831                 var d = { channelName: url.target, key: key,
1832                           charset: url.charset };
1833                 targetObject = network.dispatch("join", d);
1834                 delete client.pendingViewContext;
1835             }
1836             else
1837             {
1838                 // Must do this the hard way... we have the server's format
1839                 // for the channel name here, and all our commands only work
1840                 // with the Unicode forms.
1841                 var serv = network.primServ;
1842                 var target = url.target;
1844                 /* If we don't have a valid prefix, stick a "#" on it.
1845                  * NOTE: This is always a "#" so that URLs may be compared
1846                  * properly without involving the server (e.g. off-line).
1847                  */
1848                 if ((arrayIndexOf(["#", "&", "+", "!"], target[0]) == -1) &&
1849                     (arrayIndexOf(serv.channelTypes, target[0]) == -1))
1850                 {
1851                     target = "#" + target;
1852                 }
1854                 var chan = new CIRCChannel(serv, null, target);
1856                 client.pendingViewContext = e;
1857                 d = {channelToJoin: chan, key: key};
1858                 targetObject = network.dispatch("join", d);
1859                 delete client.pendingViewContext;
1860             }
1862             if (!targetObject)
1863                 return;
1864         }
1866         if (url.msg)
1867         {
1868             client.pendingViewContext = e;
1869             var msg;
1870             if (url.msg.indexOf("\01ACTION") == 0)
1871             {
1872                 msg = filterOutput(url.msg, "ACTION", targetObject);
1873                 targetObject.display(msg, "ACTION", "ME!",
1874                                      client.currentObject);
1875             }
1876             else
1877             {
1878                 msg = filterOutput(url.msg, "PRIVMSG", targetObject);
1879                 targetObject.display(msg, "PRIVMSG", "ME!",
1880                                      client.currentObject);
1881             }
1882             targetObject.say(msg);
1883             dispatch("set-current-view", { view: targetObject });
1884             delete client.pendingViewContext;
1885         }
1886     }
1887     else
1888     {
1889         client.pendingViewContext = e;
1890         if (!network.messages)
1891             network.displayHere(getMsg(MSG_NETWORK_OPENED, network.unicodeName));
1892         dispatch("set-current-view", { view: network });
1893         delete client.pendingViewContext;
1894     }
1897 function updateProgress()
1899     var busy;
1900     var progress = -1;
1902     if ("busy" in client.currentObject)
1903         busy = client.currentObject.busy;
1905     if ("progress" in client.currentObject)
1906         progress = client.currentObject.progress;
1908     if (!busy)
1909         progress = 0;
1911     client.progressPanel.collapsed = !busy;
1912     client.progressBar.mode = (progress < 0 ? "undetermined" : "determined");
1913     if (progress >= 0)
1914         client.progressBar.value = progress;
1917 function updateSecurityIcon()
1919     var o = getObjectDetails(client.currentObject);
1920     var securityButton = window.document.getElementById("security-button");
1921     securityButton.firstChild.value = "";
1922     securityButton.removeAttribute("level");
1923     securityButton.removeAttribute("tooltiptext");
1924     if (!o.server || !o.server.isConnected) // No server or connection?
1925     {
1926         securityButton.setAttribute("tooltiptext", MSG_SECURITY_INFO);
1927         return;
1928     }
1930     var securityState = o.server.connection.getSecurityState()
1931     switch (securityState[0])
1932     {
1933         case STATE_IS_SECURE:
1934             securityButton.firstChild.value = o.server.hostname;
1935             if (securityState[1] == STATE_SECURE_HIGH)
1936                 securityButton.setAttribute("level", "high");
1937             else // Because low security is the worst we have when being secure
1938                 securityButton.setAttribute("level", "low");
1940             // Add the tooltip:
1941             var issuer = o.server.connection.getCertificate().issuerOrganization;
1942             var tooltiptext = getMsg(MSG_SECURE_CONNECTION, issuer);
1943             securityButton.setAttribute("tooltiptext", tooltiptext);
1944             securityButton.firstChild.setAttribute("tooltiptext", tooltiptext);
1945             securityButton.lastChild.setAttribute("tooltiptext", tooltiptext);
1946             break;
1947         case STATE_IS_BROKEN:
1948             securityButton.setAttribute("level", "broken");
1949             // No break to make sure we get the correct tooltip
1950         case STATE_IS_INSECURE:
1951         default:
1952             securityButton.setAttribute("tooltiptext", MSG_SECURITY_INFO);
1953     }
1956 function initOfflineIcon()
1958     const IOSVC2_CID = "@mozilla.org/network/io-service;1";
1959     const PRBool_CID = "@mozilla.org/supports-PRBool;1";
1960     const OS_CID = "@mozilla.org/observer-service;1";
1961     const nsISupportsPRBool = Components.interfaces.nsISupportsPRBool;
1963     client.offlineObserver = {
1964         _element: document.getElementById("offline-status"),
1965         _getNewIOSvc: function offline_getNewIOSvc()
1966         {
1967             try
1968             {
1969                 return getService(IOSVC2_CID, "nsIIOService2");
1970             }
1971             catch (ex) {}
1973             // If it failed, it's probably just not there. We don't care.
1974             return null;
1975         },
1976         state: function offline_state()
1977         {
1978             return (client.iosvc.offline ? "offline" : "online");
1979         },
1980         observe: function offline_observe(subject, topic, state)
1981         {
1982             if ((topic == "offline-requested") &&
1983                 (client.getConnectionCount() > 0))
1984             {
1985                 var buttonAry = [MSG_REALLY_GO_OFFLINE, MSG_DONT_GO_OFFLINE];
1986                 var rv = confirmEx(MSG_GOING_OFFLINE, buttonAry);
1987                 if (rv == 1) // Don't go offline, please!
1988                 {
1989                     subject.QueryInterface(nsISupportsPRBool);
1990                     subject.data = true;
1991                 }
1992             }
1993             else if (topic == "network:offline-status-changed")
1994             {
1995                 this.updateOfflineUI();
1996             }
1997         },
1998         updateOfflineUI: function offline_uiUpdate()
1999         {
2000             this._element.setAttribute("offlinestate", this.state());
2001             var tooltipMsgId = "MSG_OFFLINESTATE_" + this.state().toUpperCase();
2002             this._element.setAttribute("tooltiptext", window[tooltipMsgId]);
2003         },
2004         toggleOffline: function offline_toggle()
2005         {
2006             // Check whether people are OK with us going offline:
2007             if (!client.iosvc.offline && !this.canGoOffline())
2008                 return;
2010             // Stop automatic management of the offline status, if existing.
2011             try
2012             {
2013                 var ioSvc2 = this._getNewIOSvc();
2014                 if (ioSvc2 && ("manageOfflineStatus" in ioSvc2))
2015                     ioSvc2.manageOfflineStatus = false;
2016             }
2017             catch (ex)
2018             {
2019                 dd("Turning off managed offline status failed!\n" + ex);
2020             }
2022             // Actually change the offline state.
2023             client.iosvc.offline = !client.iosvc.offline;
2024             // Update the pref:
2025             this.updatePrefFromOffline();
2026         },
2027         canGoOffline: function offline_check()
2028         {
2029             try
2030             {
2031                 var os = getService(OS_CID, "nsIObserverService");
2032                 var canGoOffline = newObject(PRBool_CID, "nsISupportsPRBool");
2033                 os.notifyObservers(canGoOffline, "offline-requested", null);
2034                 // Someone called for a halt
2035                 if (canGoOffline.data)
2036                     return false;
2037             }
2038             catch (ex)
2039             {
2040                 dd("Exception when trying to ask if we could go offline:" + ex);
2041             }
2042             return true;
2043         },
2044         updateOfflineFromPref: function offline_syncFromPref()
2045         {
2046             // On toolkit, we might have smart management of offline mode.
2047             // Don't interfere.
2048             var ioSvc2 = this._getNewIOSvc();
2049             if (ioSvc2 && ioSvc2.manageOfflineStatus)
2050                 return;
2052             // This is app-managed, or should be, on startup:
2053             if (client.host == "Mozilla")
2054                 return;
2056             var isOffline = false;
2057             var prefSvc = getService("@mozilla.org/preferences-service;1",
2058                                      "nsIPrefBranch");
2059             // Let the app-specific hacks begin:
2060             try {
2061                 if (client.host == "XULrunner")
2062                     isOffline = !prefSvc.getBoolPref("network.online");
2063                 else // Toolkit based, but not standalone
2064                     isOffline = prefSvc.getBoolPref("browser.offline");
2065             }
2066             catch (ex) { /* Whatever. */ }
2068             // Actually do it:
2069             client.iosvc.offline = isOffline;
2070         },
2071         updatePrefFromOffline: function offline_syncToPref()
2072         {
2073             // This is app-managed, or should be.
2074             if (client.host == "Mozilla")
2075                 return;
2077             var isOffline = client.iosvc.offline;
2078             var prefSvc = getService("@mozilla.org/preferences-service;1",
2079                                      "nsIPrefBranch");
2080             // Let the app-specific hacks begin:
2081             try {
2082                 if (client.host == "XULrunner")
2083                     prefSvc.setBoolPref("network.online", !isOffline);
2084                 else // Toolkit based, but not standalone
2085                     prefSvc.setBoolPref("browser.offline", isOffline);
2086             }
2087             catch (ex)
2088             {
2089                 dd("Couldn't set offline pref! Error:" + ex);
2090             }
2091         }
2092     };
2094     try
2095     {
2096         var os = getService(OS_CID, "nsIObserverService");
2097         os.addObserver(client.offlineObserver, "offline-requested", false);
2098         os.addObserver(client.offlineObserver,
2099                        "network:offline-status-changed", false);
2100     }
2101     catch (ex)
2102     {
2103         dd("Exception when trying to register offline observers: " + ex);
2104     }
2106     var elem = client.offlineObserver._element;
2107     elem.setAttribute("onclick", "client.offlineObserver.toggleOffline()");
2108     client.offlineObserver.updateOfflineFromPref();
2109     client.offlineObserver.updateOfflineUI();
2111     // Don't leak:
2112     delete os;
2113     delete elem;
2116 function uninitOfflineIcon()
2118     const OS_CID = "@mozilla.org/observer-service;1";
2119     try
2120     {
2121         var os = getService(OS_CID, "nsIObserverService");
2122         os.removeObserver(client.offlineObserver, "offline-requested", false);
2123         os.removeObserver(client.offlineObserver,
2124                           "network:offline-status-changed", false);
2125     }
2126     catch (ex)
2127     {
2128         dd("Exception when trying to unregister offline observers: " + ex);
2129     }
2132 client.idleObserver = {
2133     QueryInterface: function io_qi(iid)
2134     {
2135         if (!iid || (!iid.equals(Components.interfaces.nsIObserver) &&
2136                      !iid.equals(Components.interfaces.nsISupports)))
2137         {
2138             throw Components.results.NS_ERROR_NO_INTERFACE;
2139         }
2140         return this;
2141     },
2142     observe: function io_observe(subject, topic, data)
2143     {
2144         if ((topic == "idle") && !client.prefs["away"])
2145         {
2146             if (!client.prefs["awayIdleMsg"])
2147                 client.prefs["awayIdleMsg"] = MSG_AWAY_IDLE_DEFAULT;
2148             client.dispatch("idle-away", {reason: client.prefs["awayIdleMsg"]});
2149             client.isIdleAway = true;
2150         }
2151         else if ((topic == "back") && client.isIdleAway)
2152         {
2153             client.dispatch("idle-back");
2154             client.isIdleAway = false;
2155         }
2156     }
2159 function initIdleAutoAway(timeout)
2161     // Don't try to do anything if we are disabled
2162     if (!timeout)
2163         return;
2165     var is = getService("@mozilla.org/widget/idleservice;1", "nsIIdleService");
2166     if (!is)
2167     {
2168         display(MSG_ERR_NO_IDLESERVICE, MT_WARN);
2169         client.prefs["autoIdleTime"] = 0;
2170         return;
2171     }
2173     try
2174     {
2175         is.addIdleObserver(client.idleObserver, timeout * 60);
2176     }
2177     catch (ex)
2178     {
2179         display(formatException(ex), MT_ERROR);
2180     }
2183 function uninitIdleAutoAway(timeout)
2185     // Don't try to do anything if we were disabled before
2186     if (!timeout)
2187         return;
2189     var is = getService("@mozilla.org/widget/idleservice;1", "nsIIdleService");
2190     if (!is)
2191         return;
2193     try
2194     {
2195         is.removeIdleObserver(client.idleObserver, timeout * 60);
2196     }
2197     catch (ex)
2198     {
2199         display(formatException(ex), MT_ERROR);
2200     }
2203 function updateAppMotif(motifURL)
2205     var node = document.firstChild;
2206     while (node && ((node.nodeType != node.PROCESSING_INSTRUCTION_NODE) ||
2207                     !(/name="dyn-motif"/).test(node.data)))
2208     {
2209         node = node.nextSibling;
2210     }
2212     motifURL = motifURL.replace(/"/g, "%22");
2213     var dataStr = "href=\"" + motifURL + "\" name=\"dyn-motif\"";
2214     try 
2215     {
2216         // No dynamic style node yet.
2217         if (!node)
2218         {
2219             node = document.createProcessingInstruction("xml-stylesheet", dataStr);
2220             document.insertBefore(node, document.firstChild);
2221         }
2222         else
2223         {
2224             node.data = dataStr;
2225         }
2226     }
2227     catch (ex)
2228     {
2229         dd(formatException(ex));
2230         var err = ex.name;
2231         // Mozilla 1.0 doesn't like document.insertBefore(...,
2232         // document.firstChild); though it has a prototype for it -
2233         // check for the right error:
2234         if (err == "NS_ERROR_NOT_IMPLEMENTED")
2235         {
2236             display(MSG_NO_DYNAMIC_STYLE, MT_INFO);
2237             updateAppMotif = function() {};
2238         }
2239     }
2242 function updateSpellcheck(value)
2244     value = value.toString();
2245     document.getElementById("input").setAttribute("spellcheck", value);
2246     document.getElementById("multiline-input").setAttribute("spellcheck",
2247                                                             value);
2250 function updateNetwork()
2252     var o = getObjectDetails (client.currentObject);
2254     var lag = MSG_UNKNOWN;
2255     var nick = "";
2256     if (o.server)
2257     {
2258         if (o.server.me)
2259             nick = o.server.me.unicodeName;
2260         lag = (o.server.lag != -1) ? o.server.lag : MSG_UNKNOWN;
2261     }
2262     client.statusBar["header-url"].setAttribute("value",
2263                                                  client.currentObject.getURL());
2264     client.statusBar["header-url"].setAttribute("href",
2265                                                  client.currentObject.getURL());
2266     client.statusBar["header-url"].setAttribute("name",
2267                                                  client.currentObject.unicodeName);
2270 function updateTitle (obj)
2272     if (!(("currentObject" in client) && client.currentObject) ||
2273         (obj && obj != client.currentObject))
2274         return;
2276     var tstring = MSG_TITLE_UNKNOWN;
2277     var o = getObjectDetails(client.currentObject);
2278     var net = o.network ? o.network.unicodeName : "";
2279     var nick = "";
2280     client.statusBar["server-nick"].disabled = false;
2282     switch (client.currentObject.TYPE)
2283     {
2284         case "IRCNetwork":
2285             var serv = "", port = "";
2286             if (client.currentObject.isConnected())
2287             {
2288                 serv = o.server.hostname;
2289                 port = o.server.port;
2290                 if (o.server.me)
2291                     nick = o.server.me.unicodeName;
2292                 tstring = getMsg(MSG_TITLE_NET_ON, [nick, net, serv, port]);
2293             }
2294             else
2295             {
2296                 nick = client.currentObject.INITIAL_NICK;
2297                 tstring = getMsg(MSG_TITLE_NET_OFF, [nick, net]);
2298             }
2299             break;
2301         case "IRCChannel":
2302             var chan = "", mode = "", topic = "";
2303             if ("me" in o.parent)
2304             {
2305                 nick = o.parent.me.unicodeName;
2306                 if (o.parent.me.canonicalName in client.currentObject.users)
2307                 {
2308                     var cuser = client.currentObject.users[o.parent.me.canonicalName];
2309                     if (cuser.isFounder)
2310                         nick = "~" + nick;
2311                     else if (cuser.isAdmin)
2312                         nick = "&" + nick;
2313                     else if (cuser.isOp)
2314                         nick = "@" + nick;
2315                     else if (cuser.isHalfOp)
2316                         nick = "%" + nick;
2317                     else if (cuser.isVoice)
2318                         nick = "+" + nick;
2319                 }
2320             }
2321             else
2322             {
2323                 nick = MSG_TITLE_NONICK;
2324             }
2325             chan = o.channel.unicodeName;
2326             mode = o.channel.mode.getModeStr();
2327             if (!mode)
2328                 mode = MSG_TITLE_NO_MODE;
2329             topic = o.channel.topic ? o.channel.topic : MSG_TITLE_NO_TOPIC;
2330             var re = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g;
2331             topic = topic.replace(re, "");
2333             tstring = getMsg(MSG_TITLE_CHANNEL, [nick, chan, mode, topic]);
2334             break;
2336         case "IRCUser":
2337             nick = client.currentObject.unicodeName;
2338             var source = "";
2339             if (client.currentObject.name)
2340             {
2341                 source = "<" + client.currentObject.name + "@" +
2342                     client.currentObject.host +">";
2343             }
2344             tstring = getMsg(MSG_TITLE_USER, [nick, source]);
2345             nick = "me" in o.parent ? o.parent.me.unicodeName : MSG_TITLE_NONICK;
2346             break;
2348         case "IRCClient":
2349             nick = client.prefs["nickname"];
2350             break;
2352         case "IRCDCCChat":
2353             client.statusBar["server-nick"].disabled = true;
2354             nick = o.chat.me.unicodeName;
2355             tstring = getMsg(MSG_TITLE_DCCCHAT, o.userName);
2356             break;
2358         case "IRCDCCFileTransfer":
2359             client.statusBar["server-nick"].disabled = true;
2360             nick = o.file.me.unicodeName;
2361             var data = [o.file.progress, o.file.filename, o.userName];
2362             if (o.file.state.dir == 1)
2363                 tstring = getMsg(MSG_TITLE_DCCFILE_SEND, data);
2364             else
2365                 tstring = getMsg(MSG_TITLE_DCCFILE_GET, data);
2366             break;
2367     }
2369     if (0 && !client.uiState["tabstrip"])
2370     {
2371         var actl = new Array();
2372         for (var i in client.activityList)
2373             actl.push ((client.activityList[i] == "!") ?
2374                        (Number(i) + 1) + "!" : (Number(i) + 1));
2375         if (actl.length > 0)
2376             tstring = getMsg(MSG_TITLE_ACTIVITY,
2377                              [tstring, actl.join (MSG_COMMASP)]);
2378     }
2380     document.title = tstring;
2381     client.statusBar["server-nick"].setAttribute("label", nick);
2384 // Where 'right' is orientation, not wrong/right:
2385 function updateUserlistSide(shouldBeLeft)
2387     var listParent = document.getElementById("tabpanels-contents-box");
2388     var isLeft = (listParent.childNodes[0].id == "user-list-box");
2389     if (isLeft == shouldBeLeft)
2390         return;
2391     if (shouldBeLeft) // Move from right to left.
2392     {
2393         listParent.insertBefore(listParent.childNodes[1], listParent.childNodes[0]);
2394         listParent.insertBefore(listParent.childNodes[2], listParent.childNodes[0]);
2395         listParent.childNodes[1].setAttribute("collapse", "before");
2396     }
2397     else // Move from left to right.
2398     {
2399         listParent.appendChild(listParent.childNodes[1]);
2400         listParent.appendChild(listParent.childNodes[0]);
2401         listParent.childNodes[1].setAttribute("collapse", "after");
2402     }
2403     var userlist = document.getElementById("user-list")
2404     if (client.currentObject && (client.currentObject.TYPE == "IRCChannel"))
2405         userlist.view = client.currentObject.userList;
2408 function multilineInputMode (state)
2410     var multiInput = document.getElementById("multiline-input");
2411     var multiInputBox = document.getElementById("multiline-box");
2412     var singleInput = document.getElementById("input");
2413     var singleInputBox = document.getElementById("singleline-box");
2414     var splitter = document.getElementById("input-splitter");
2415     var iw = document.getElementById("input-widgets");
2416     var h;
2418     client._mlMode = state;
2420     if (state)  /* turn on multiline input mode */
2421     {
2423         h = iw.getAttribute ("lastHeight");
2424         if (h)
2425             iw.setAttribute ("height", h); /* restore the slider position */
2427         singleInputBox.setAttribute ("collapsed", "true");
2428         splitter.setAttribute ("collapsed", "false");
2429         multiInputBox.setAttribute ("collapsed", "false");
2430         // multiInput should have the same direction as singleInput
2431         multiInput.setAttribute("dir", singleInput.getAttribute("dir"));
2432         multiInput.value = (client.input ? client.input.value : "");
2433         client.input = multiInput;
2434     }
2435     else  /* turn off multiline input mode */
2436     {
2437         h = iw.getAttribute ("height");
2438         iw.setAttribute ("lastHeight", h); /* save the slider position */
2439         iw.removeAttribute ("height");     /* let the slider drop */
2441         splitter.setAttribute ("collapsed", "true");
2442         multiInputBox.setAttribute ("collapsed", "true");
2443         singleInputBox.setAttribute ("collapsed", "false");
2444         // singleInput should have the same direction as multiInput
2445         singleInput.setAttribute("dir", multiInput.getAttribute("dir"));
2446         singleInput.value = (client.input ? client.input.value : "");
2447         client.input = singleInput;
2448     }
2450     client.input.focus();
2453 function displayCertificateInfo()
2455     var o = getObjectDetails(client.currentObject);
2456     if (!o.server)
2457         return;
2459     if (!o.server.isSecure)
2460     {
2461         alert(getMsg(MSG_INSECURE_SERVER, o.server.hostname));
2462         return;
2463     }
2465     viewCert(o.server.connection.getCertificate());
2468 function newInlineText (data, className, tagName)
2470     if (typeof tagName == "undefined")
2471         tagName = "html:span";
2473     var a = document.createElementNS(XHTML_NS, tagName);
2474     if (className)
2475         a.setAttribute ("class", className);
2477     switch (typeof data)
2478     {
2479         case "string":
2480             a.appendChild (document.createTextNode (data));
2481             break;
2483         case "object":
2484             for (var p in data)
2485                 if (p != "data")
2486                     a.setAttribute (p, data[p]);
2487                 else
2488                     a.appendChild (document.createTextNode (data[p]));
2489             break;
2491         case "undefined":
2492             break;
2494         default:
2495             ASSERT(0, "INVALID TYPE ('" + typeof data + "') passed to " +
2496                    "newInlineText.");
2497             break;
2499     }
2501     return a;
2505 function stringToMsg (message, obj)
2507     var ary = message.split ("\n");
2508     var span = document.createElementNS(XHTML_NS, "html:span");
2509     var data = getObjectDetails(obj);
2511     if (ary.length == 1)
2512         client.munger.munge(ary[0], span, data);
2513     else
2514     {
2515         for (var l = 0; l < ary.length - 1; ++l)
2516         {
2517             client.munger.munge(ary[l], span, data);
2518             span.appendChild(document.createElementNS(XHTML_NS, "html:br"));
2519         }
2520         client.munger.munge(ary[l], span, data);
2521     }
2523     return span;
2526 function getFrame()
2528     if (client.deck.childNodes.length == 0)
2529         return undefined;
2530     var panel = client.deck.selectedPanel;
2531     return getContentWindow(panel);
2534 client.__defineGetter__ ("currentFrame", getFrame);
2536 function setCurrentObject (obj)
2538     if (!ASSERT(obj.messages, "INVALID OBJECT passed to setCurrentObject **"))
2539         return;
2541     if ("currentObject" in client && client.currentObject == obj)
2542     {
2543         if (typeof client.pendingViewContext == "object")
2544             dispatch("create-tab-for-view", { view: obj });
2545         return;
2546     }
2548     // Set window.content to make screenreader apps find the chat content.
2549     if (obj.frame && getContentWindow(obj.frame))
2550         window.content = getContentWindow(obj.frame);
2552     var tb, userList;
2553     userList = document.getElementById("user-list");
2555     if ("currentObject" in client && client.currentObject)
2556         tb = getTabForObject(client.currentObject);
2557     if (tb)
2558         tb.setAttribute("state", "normal");
2560     client.currentObject = obj;
2562     // Update userlist:
2563     userList.view = null;
2564     if (obj.TYPE == "IRCChannel")
2565     {
2566         userList.view = obj.userList;
2567         updateUserList();
2568     }
2570     tb = dispatch("create-tab-for-view", { view: obj });
2571     if (tb)
2572     {
2573         tb.parentNode.selectedItem = tb;
2574         tb.setAttribute("state", "current");
2575     }
2577     var vk = Number(tb.getAttribute("viewKey"));
2578     delete client.activityList[vk];
2579     client.deck.selectedPanel = obj.frame;
2581     // Style userlist and the like:
2582     updateAppMotif(obj.prefs["motif.current"]);
2584     updateTitle();
2585     updateProgress();
2586     updateSecurityIcon();
2588     scrollDown(obj.frame, false);
2590     // Input area should have the same direction as the output area
2591     if (("frame" in client.currentObject) &&
2592         client.currentObject.frame &&
2593         getContentDocument(client.currentObject.frame) &&
2594         ("body" in getContentDocument(client.currentObject.frame)) &&
2595         getContentDocument(client.currentObject.frame).body)
2596     {
2597         var contentArea = getContentDocument(client.currentObject.frame).body;
2598         client.input.setAttribute("dir", contentArea.getAttribute("dir"));
2599     }
2600     client.input.focus();
2603 function checkScroll(frame)
2605     var window = getContentWindow(frame);
2606     if (!window || !("document" in window))
2607         return false;
2609     return (window.document.height - window.innerHeight -
2610             window.pageYOffset) < 160;
2613 function scrollDown(frame, force)
2615     var window = getContentWindow(frame);
2616     if (window && (force || checkScroll(frame)))
2617         window.scrollTo(0, window.document.height);
2620 function advanceKeyboardFocus(amount)
2622     var contentWin = getContentWindow(client.currentObject.frame);
2623     var contentDoc = getContentDocument(client.currentObject.frame);
2624     var userList = document.getElementById("user-list");
2626     // Focus userlist, inputbox and outputwindow in turn:
2627     var focusableElems = [userList, client.input.inputField, contentWin];
2629     var elem = document.commandDispatcher.focusedElement;
2630     // Finding focus in the content window is "hard". It's going to be null
2631     // if the window itself is focused, and "some element" inside of it if the
2632     // user starts tabbing through.
2633     if (!elem || (elem.ownerDocument == contentDoc))
2634         elem = contentWin;
2636     var newIndex = (arrayIndexOf(focusableElems, elem) * 1 + 3 + amount) % 3;
2637     focusableElems[newIndex].focus();
2639     // Make it obvious this element now has focus.
2640     var outlinedElem;
2641     if (focusableElems[newIndex] == client.input.inputField)
2642         outlinedElem = client.input.parentNode.id;
2643     else if (focusableElems[newIndex] == userList)
2644         outlinedElem = "user-list-box"
2645     else
2646         outlinedElem = "browser-box";
2648     // Do magic, and make sure it gets undone at the right time:
2649     if (("focusedElemTimeout" in client) && client.focusedElemTimeout)
2650         clearTimeout(client.focusedElemTimeout);
2651     outlineFocusedElem(outlinedElem);
2652     client.focusedElemTimeout = setTimeout(outlineFocusedElem, 1000, "");
2655 function outlineFocusedElem(id)
2657     var outlinedElements = ["user-list-box", "browser-box", "multiline-hug-box",
2658                             "singleline-hug-box"];
2659     for (var i = 0; i < outlinedElements.length; i++)
2660     {
2661         var elem = document.getElementById(outlinedElements[i]);
2662         if (outlinedElements[i] == id)
2663             elem.setAttribute("focusobvious", "true");
2664         else
2665             elem.removeAttribute("focusobvious");
2666     }
2667     client.focusedElemTimeout = 0;
2670 /* valid values for |what| are "superfluous", "activity", and "attention".
2671  * final value for state is dependant on priority of the current state, and the
2672  * new state. the priority is: normal < superfluous < activity < attention.
2673  */
2674 function setTabState(source, what, callback)
2676     if (typeof source != "object")
2677     {
2678         if (!ASSERT(source in client.viewsArray,
2679                     "INVALID SOURCE passed to setTabState"))
2680             return;
2681         source = client.viewsArray[source].source;
2682     }
2684     callback = callback || false;
2686     var tb = source.dispatch("create-tab-for-view", { view: source });
2687     var vk = Number(tb.getAttribute("viewKey"));
2689     var current = ("currentObject" in client && client.currentObject == source);
2691     /* We want to play sounds if they're from a non-current view, or we don't
2692      * have focus at all. Also make sure stalk matches always play sounds.
2693      * Also make sure we don't play on the 2nd half of the flash (Callback).
2694      */
2695     if (!callback && (!window.isFocused || !current || (what == "attention")))
2696     {
2697         if (what == "attention")
2698             playEventSounds(source.TYPE, "stalk");
2699         else if (what == "activity")
2700             playEventSounds(source.TYPE, "chat");
2701         else if (what == "superfluous")
2702             playEventSounds(source.TYPE, "event");
2703     }
2705     // Only change the tab's colour if it's not the active view.
2706     if (!current)
2707     {
2708         var state = tb.getAttribute("state");
2709         if (state == what)
2710         {
2711             /* if the tab state has an equal priority to what we are setting
2712              * then blink it */
2713             if (client.prefs["activityFlashDelay"] > 0)
2714             {
2715                 tb.setAttribute("state", "normal");
2716                 setTimeout(setTabState, client.prefs["activityFlashDelay"],
2717                            vk, what, true);
2718             }
2719         }
2720         else
2721         {
2722             if (state == "normal" || state == "superfluous" ||
2723                (state == "activity" && what == "attention"))
2724             {
2725                 /* if the tab state has a lower priority than what we are
2726                  * setting, change it to the new state */
2727                 tb.setAttribute("state", what);
2728                 /* we only change the activity list if priority has increased */
2729                 if (what == "attention")
2730                    client.activityList[vk] = "!";
2731                 else if (what == "activity")
2732                     client.activityList[vk] = "+";
2733                 else
2734                 {
2735                    /* this is functionally equivalent to "+" for now */
2736                    client.activityList[vk] = "-";
2737                 }
2738                 updateTitle();
2739             }
2740             else
2741             {
2742                 /* the current state of the tab has a higher priority than the
2743                  * new state.
2744                  * blink the new lower state quickly, then back to the old */
2745                 if (client.prefs["activityFlashDelay"] > 0)
2746                 {
2747                     tb.setAttribute("state", what);
2748                     setTimeout(setTabState,
2749                                client.prefs["activityFlashDelay"], vk,
2750                                state, true);
2751                 }
2752             }
2753         }
2754     }
2757 function notifyAttention (source)
2759     if (typeof source != "object")
2760         source = client.viewsArray[source].source;
2762     if (client.currentObject != source)
2763     {
2764         var tb = dispatch("create-tab-for-view", { view: source });
2765         var vk = Number(tb.getAttribute("viewKey"));
2767         tb.setAttribute ("state", "attention");
2768         client.activityList[vk] = "!";
2769         updateTitle();
2770     }
2772     if (client.prefs["notify.aggressive"])
2773         window.getAttention();
2777 function setDebugMode(mode)
2779     if (mode.indexOf("t") != -1)
2780         client.debugHook.enabled = true;
2781     else
2782         client.debugHook.enabled = false;
2784     if (mode.indexOf("c") != -1)
2785         client.dbgContexts = true;
2786     else
2787         delete client.dbgContexts;
2789     if (mode.indexOf("d") != -1)
2790         client.dbgDispatch = true;
2791     else
2792         delete client.dbgDispatch;
2795 function setListMode(mode)
2797     var elem = document.getElementById("user-list");
2798     if (mode)
2799         elem.setAttribute("mode", mode);
2800     else
2801         elem.removeAttribute("mode");
2802     if (elem && elem.view && elem.treeBoxObject)
2803     {
2804         elem.treeBoxObject.clearStyleAndImageCaches();
2805         elem.treeBoxObject.invalidate();
2806     }
2809 function updateUserList()
2811     var node, chan;
2813     node = document.getElementById("user-list");
2814     if (!node.view)
2815         return;
2817     if (("currentObject" in client) && client.currentObject &&
2818         client.currentObject.TYPE == "IRCChannel")
2819     {
2820         reSortUserlist(client.currentObject);
2821     }
2824 function reSortUserlist(channel)
2826     if (!channel || !channel.userList)
2827         return;
2828     channel.userList.childData.reSort();
2831 function getNicknameForUserlistRow(index)
2833     // This wouldn't be so hard if APIs didn't change so much... see bug 221619
2834     var userlist = document.getElementById("user-list");
2835     if (userlist.columns)
2836         var col = userlist.columns.getNamedColumn("usercol");
2837     else
2838         col = "usercol";
2839     return userlist.view.getCellText(index, col);
2842 function getFrameForDOMWindow(window)
2844     var frame;
2845     for (var i = 0; i < client.deck.childNodes.length; i++)
2846     {
2847         frame = client.deck.childNodes[i];
2848         if (getContentWindow(frame) == window)
2849             return frame;
2850     }
2851     return undefined;
2854 function replaceColorCodes(msg)
2856     // Find things that look like URLs and escape the color code inside of those
2857     // to prevent munging the URLs resulting in broken links. Leave codes at
2858     // the start of the URL alone.
2859     msg = msg.replace(new RegExp(client.linkRE.source, "g"), function(url) {
2860         return url.replace(/%[BC][0-9A-Fa-f]/g, function(hex, index) {
2861             // as JS does not support lookbehind and we don't want to consume the
2862             // preceding character, we test for an existing %% manually
2863             var needPercent = ("%" == url.substr(index - 1, 1)) || (index == 0);
2864             return (needPercent ? "" : "%") + hex;
2865         });
2866     });
2867     
2868     // mIRC codes: underline, bold, Original (reset), colors, reverse colors.
2869     msg = msg.replace(/(^|[^%])%U/g, "$1\x1f");
2870     msg = msg.replace(/(^|[^%])%B/g, "$1\x02");
2871     msg = msg.replace(/(^|[^%])%O/g, "$1\x0f");
2872     msg = msg.replace(/(^|[^%])%C/g, "$1\x03");
2873     msg = msg.replace(/(^|[^%])%R/g, "$1\x16");
2874     // %%[UBOCR] --> %[UBOCR].
2875     msg = msg.replace(/%(%[UBOCR])/g, "$1");
2877     return msg;
2880 function decodeColorCodes(msg)
2882     // %[UBOCR] --> %%[UBOCR].
2883     msg = msg.replace(/(%[UBOCR])/g, "%$1");
2884     // Put %-codes back in place of special character codes.
2885     msg = msg.replace(/\x1f/g, "%U");
2886     msg = msg.replace(/\x02/g, "%B");
2887     msg = msg.replace(/\x0f/g, "%O");
2888     msg = msg.replace(/\x03/g, "%C");
2889     msg = msg.replace(/\x16/g, "%R");
2891     return msg;
2894 function removeColorCodes(msg)
2896     msg = msg.replace(/[\x1f\x02\x0f\x16]/g, "");
2897     // We need this to be global:
2898     msg = msg.replace(new RegExp(client.colorRE.source, "g"), "");
2899     return msg;
2902 client.progressListener = new Object();
2904 client.progressListener.QueryInterface =
2905 function qi(iid)
2907     return this;
2910 client.progressListener.onStateChange =
2911 function client_statechange (webProgress, request, stateFlags, status)
2913     const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
2914     const START = nsIWebProgressListener.STATE_START;
2915     const STOP = nsIWebProgressListener.STATE_STOP;
2916     const IS_NETWORK = nsIWebProgressListener.STATE_IS_NETWORK;
2917     const IS_DOCUMENT = nsIWebProgressListener.STATE_IS_DOCUMENT;
2919     var frame;
2921     // We only care about the initial start of loading, not the loading of
2922     // and page sub-components (IS_DOCUMENT, etc.).
2923     if ((stateFlags & START) && (stateFlags & IS_NETWORK) &&
2924         (stateFlags & IS_DOCUMENT))
2925     {
2926         frame = getFrameForDOMWindow(webProgress.DOMWindow);
2927         if (!frame)
2928         {
2929             dd("can't find frame for window (start)");
2930             try
2931             {
2932                 webProgress.removeProgressListener(this);
2933             }
2934             catch(ex)
2935             {
2936                 dd("Exception removing progress listener (start): " + ex);
2937             }
2938         }
2939     }
2940     // We only want to know when the *network* stops, not the page's
2941     // individual components (STATE_IS_REQUEST/STATE_IS_DOCUMENT/somesuch).
2942     else if ((stateFlags & STOP) && (stateFlags & IS_NETWORK))
2943     {
2944         frame = getFrameForDOMWindow(webProgress.DOMWindow);
2945         if (!frame)
2946         {
2947             dd("can't find frame for window (stop)");
2948             try
2949             {
2950                 webProgress.removeProgressListener(this);
2951             }
2952             catch(ex)
2953             {
2954                 dd("Exception removing progress listener (stop): " + ex);
2955             }
2956         }
2957         else
2958         {
2959             var cwin = getContentWindow(frame);
2960             if (cwin && "initOutputWindow" in cwin)
2961             {
2962                 cwin.getMsg = getMsg;
2963                 cwin.initOutputWindow(client, frame.source, onMessageViewClick);
2964                 cwin.changeCSS(frame.source.getFontCSS("data"), "cz-fonts");
2965                 scrollDown(frame, true);
2967                 try
2968                 {
2969                     webProgress.removeProgressListener(this);
2970                 }
2971                 catch(ex)
2972                 {
2973                     dd("Exception removing progress listener (done): " + ex);
2974                 }
2975             }
2976             // XXX: For about:blank it won't find initOutputWindow. Cope.
2977             else if (!cwin || !cwin.location ||
2978                      (cwin.location.href != "about:blank"))
2979             {
2980                 // This should totally never ever happen. It will if we get in a
2981                 // fight with xpcnativewrappers, though. Oops:
2982                 dd("Couldn't find a content window or its initOutputWindow " + 
2983                    "function. This is BAD!");
2984             }
2985         }
2986     }
2989 client.progressListener.onProgressChange =
2990 function client_progresschange (webProgress, request, currentSelf, totalSelf,
2991                                 currentMax, selfMax)
2995 client.progressListener.onLocationChange =
2996 function client_locationchange (webProgress, request, uri)
3000 client.progressListener.onStatusChange =
3001 function client_statuschange (webProgress, request, status, message)
3005 client.progressListener.onSecurityChange =
3006 function client_securitychange (webProgress, request, state)
3010 function syncOutputFrame(obj, nesting)
3012     const nsIWebProgress = Components.interfaces.nsIWebProgress;
3013     const WINDOW = nsIWebProgress.NOTIFY_STATE_WINDOW;
3014     const NETWORK = nsIWebProgress.NOTIFY_STATE_NETWORK;
3015     const ALL = nsIWebProgress.NOTIFY_ALL;
3017     var iframe = obj.frame;
3019     function tryAgain(nLevel)
3020     {
3021         syncOutputFrame(obj, nLevel);
3022     };
3024     if (typeof nesting != "number")
3025         nesting = 0;
3027     if (nesting > 10)
3028     {
3029         dd("Aborting syncOutputFrame, taken too many tries.");
3030         return;
3031     }
3033     try
3034     {
3035         if (getContentDocument(iframe) && ("webProgress" in iframe))
3036         {
3037             var url = obj.prefs["outputWindowURL"];
3038             iframe.addProgressListener(client.progressListener, ALL);
3039             iframe.loadURI(url);
3040         }
3041         else
3042         {
3043             setTimeout(tryAgain, 500, nesting + 1);
3044         }
3045     }
3046     catch (ex)
3047     {
3048         dd("caught exception showing session view, will try again later.");
3049         dd(dumpObjectTree(ex) + "\n");
3050         setTimeout(tryAgain, 500, nesting + 1);
3051     }
3054 function createMessages(source)
3056     playEventSounds(source.TYPE, "start");
3058     source.messages = document.createElementNS(XHTML_NS, "html:table");
3059     source.messages.setAttribute("class", "msg-table");
3060     source.messages.setAttribute("view-type", source.TYPE);
3061     source.messages.setAttribute("role", "log");
3062     source.messages.setAttribute("aria-live", "polite");
3064     var tbody = document.createElementNS(XHTML_NS, "html:tbody");
3065     source.messages.appendChild (tbody);
3066     source.messageCount = 0;
3069 /* Gets the <tab> element associated with a view object.
3070  * If |create| is present, and true, tab is created if not found.
3071  */
3072 function getTabForObject(source, create)
3074     var name;
3076     if (!ASSERT(source, "UNDEFINED passed to getTabForObject"))
3077         return null;
3079     if (!ASSERT("viewName" in source, "INVALID OBJECT in getTabForObject"))
3080         return null;
3082     name = source.viewName;
3084     var tb, id = "tb[" + name + "]";
3085     var matches = 1;
3087     for (var i in client.viewsArray)
3088     {
3089         if (client.viewsArray[i].source == source)
3090         {
3091             tb = client.viewsArray[i].tb;
3092             break;
3093         }
3094         else
3095             if (client.viewsArray[i].tb.getAttribute("id") == id)
3096                 id = "tb[" + name + "<" + (++matches) + ">]";
3097     }
3099     /* If we found a <tab>, are allowed to create it, and have a pending view
3100      * context, then we're expected to move the existing tab according to said
3101      * context. We do that by removing the tab here, and below the creation
3102      * code (which is not run) we readd it in the correct location.
3103      */
3104     if (tb && create && (typeof client.pendingViewContext == "object"))
3105     {
3106         /* If we're supposed to insert before ourselves, or the next <tab>,
3107          * then bail out (since it's a no-op).
3108          */
3109         var tabBefore = client.pendingViewContext.tabInsertBefore;
3110         if (tabBefore)
3111         {
3112             var tbAfter = tb.nextSibling;
3113             while (tbAfter && tbAfter.collapsed && tbAfter.hidden)
3114                 tbAfter = tbAfter.nextSibling;
3115             if ((tabBefore == tb) || (tabBefore == tbAfter))
3116                 return tb;
3117         }
3119         var viewKey = Number(tb.getAttribute("viewKey"));
3120         arrayRemoveAt(client.viewsArray, viewKey);
3121         for (i = viewKey; i < client.viewsArray.length; i++)
3122             client.viewsArray[i].tb.setAttribute("viewKey", i);
3123         client.tabs.removeChild(tb);
3124     }
3125     else if (tb || (!tb && !create))
3126     {
3127         /* Either: we have a tab and don't want it moved, or didn't find one
3128          * and don't wish to create one.
3129          */
3130         return tb;
3131     }
3133     // Didn't found a <tab>, but we're allowed to create one.
3134     if (!tb && create)
3135     {
3136         if (!("messages" in source) || source.messages == null)
3137             createMessages(source);
3139         tb = document.createElement("tab");
3140         tb.setAttribute("ondraggesture",
3141                         "nsDragAndDrop.startDrag(event, tabDNDObserver);");
3142         tb.setAttribute("href", source.getURL());
3143         tb.setAttribute("name", source.unicodeName);
3144         tb.setAttribute("onclick", "onTabClick(event, " + id.quote() + ");");
3145         // This wouldn't be here if there was a supported CSS property for it.
3146         tb.setAttribute("crop", "center");
3147         tb.setAttribute("context", "context:tab");
3148         tb.setAttribute("tooltip", "xul-tooltip-node");
3149         tb.setAttribute("class", "tab-bottom view-button");
3150         tb.setAttribute("id", id);
3151         tb.setAttribute("state", "normal");
3152         tb.setAttribute("label", name + (matches > 1 ? "<" + matches + ">" : ""));
3153         tb.view = source;
3155         var browser = document.createElement("browser");
3156         browser.setAttribute("class", "output-container");
3157         browser.setAttribute("type", "content");
3158         browser.setAttribute("flex", "1");
3159         browser.setAttribute("tooltip", "html-tooltip-node");
3160         browser.setAttribute("context", "context:messages");
3161         //browser.setAttribute("onload", "scrollDown(true);");
3162         browser.setAttribute("onclick",
3163                              "return onMessageViewClick(event)");
3164         browser.setAttribute("onmousedown",
3165                              "return onMessageViewMouseDown(event)");
3166         browser.setAttribute("ondragover",
3167                              "nsDragAndDrop.dragOver(event, " +
3168                              "contentDropObserver);");
3169         browser.setAttribute("ondragdrop",
3170                              "nsDragAndDrop.drop(event, contentDropObserver);");
3171         browser.setAttribute("ondraggesture",
3172                              "nsDragAndDrop.startDrag(event, " +
3173                              "contentAreaDNDObserver);");
3174         browser.source = source;
3175         source.frame = browser;
3176         ASSERT(client.deck, "no deck?");
3177         client.deck.appendChild(browser);
3178         syncOutputFrame(source);
3180         if (!("userList" in source) && (source.TYPE == "IRCChannel"))
3181         {
3182             source.userListShare = new Object();
3183             source.userList = new XULTreeView(source.userListShare);
3184             source.userList.getCellProperties = ul_getcellprops;
3185             source.userList.childData.setSortDirection(1);
3186         }
3187     }
3189     var beforeTab = null;
3190     if (typeof client.pendingViewContext == "object")
3191     {
3192         var c = client.pendingViewContext;
3193         /* If we have a <tab> to insert before, and it is still in the tabs,
3194          * move the newly-created <tab> into the right place.
3195          */
3196         if (c.tabInsertBefore && (c.tabInsertBefore.parentNode == client.tabs))
3197             beforeTab = c.tabInsertBefore;
3198     }
3200     if (beforeTab)
3201     {
3202         var viewKey = beforeTab.getAttribute("viewKey");
3203         arrayInsertAt(client.viewsArray, viewKey, {source: source, tb: tb});
3204         for (i = viewKey; i < client.viewsArray.length; i++)
3205             client.viewsArray[i].tb.setAttribute("viewKey", i);
3206         client.tabs.insertBefore(tb, beforeTab);
3207     }
3208     else
3209     {
3210         client.viewsArray.push({source: source, tb: tb});
3211         tb.setAttribute("viewKey", client.viewsArray.length - 1);
3212         client.tabs.appendChild(tb);
3213     }
3215     updateTabAttributes();
3217     return tb;
3220 function updateTabAttributes()
3222     /* XXX: Workaround for Gecko bugs 272646 and 261826. Note that this breaks
3223      * the location of the spacers before and after the tabs but, due to our
3224      * own <spacer>, their flex was not being utilised anyway.
3225      */
3226     var tabOrdinal = 0;
3227     for (var tab = client.tabs.firstChild; tab; tab = tab.nextSibling)
3228         tab.ordinal = tabOrdinal++;
3230     /* XXX: Workaround for tabbox.xml not coping with updating attributes when
3231      * tabs are moved. We correct the "first-tab", "last-tab", "beforeselected"
3232      * and "afterselected" attributes.
3233      *
3234      * "last-tab" and "beforeselected" are updated on each valid (non-collapsed
3235      * and non-hidden) tab found, to avoid having to work backwards as well as
3236      * forwards. "first-tab" and "afterselected" are just set the once each.
3237      * |foundSelected| tracks where we are in relation to the selected tab.
3238      */
3239     var tabAttrs = {
3240         "first-tab": null,
3241         "last-tab": null,
3242         "beforeselected": null,
3243         "afterselected": null
3244     };
3245     var foundSelected = "before";
3246     for (tab = client.tabs.firstChild; tab; tab = tab.nextSibling)
3247     {
3248         if (tab.collapsed || tab.hidden)
3249             continue;
3251         if (!tabAttrs["first-tab"])
3252             tabAttrs["first-tab"] = tab;
3253         tabAttrs["last-tab"] = tab;
3255         if ((foundSelected == "before") && tab.selected)
3256             foundSelected = "on";
3257         else if (foundSelected == "on")
3258             foundSelected = "after";
3260         if (foundSelected == "before")
3261             tabAttrs["beforeselected"] = tab;
3262         if ((foundSelected == "after") && !tabAttrs["afterselected"])
3263             tabAttrs["afterselected"] = tab;
3264     }
3266     // After picking a tab for each attribute, apply them to the tabs.
3267     for (tab = client.tabs.firstChild; tab; tab = tab.nextSibling)
3268     {
3269         for (var attr in tabAttrs)
3270         {
3271             if (tabAttrs[attr] == tab)
3272                 tab.setAttribute(attr, "true");
3273             else
3274                 tab.removeAttribute(attr);
3275         }
3276     }
3279 // Properties getter for user list tree view
3280 function ul_getcellprops(index, column, properties)
3282     if ((index < 0) || (index >= this.childData.childData.length) ||
3283         !properties)
3284     {
3285         return;
3286     }
3288     var userObj = this.childData.childData[index]._userObj;
3290     properties.AppendElement(client.atomSvc.getAtom("voice-" + userObj.isVoice));
3291     properties.AppendElement(client.atomSvc.getAtom("op-" + userObj.isOp));
3292     properties.AppendElement(client.atomSvc.getAtom("halfop-" + userObj.isHalfOp));
3293     properties.AppendElement(client.atomSvc.getAtom("admin-" + userObj.isAdmin));
3294     properties.AppendElement(client.atomSvc.getAtom("founder-" + userObj.isFounder));
3295     properties.AppendElement(client.atomSvc.getAtom("away-" + userObj.isAway));
3298 var contentDropObserver = new Object();
3300 contentDropObserver.onDragOver =
3301 function cdnd_dover(aEvent, aFlavour, aDragSession)
3303     if (aEvent.getPreventDefault())
3304         return;
3306     if (aDragSession.sourceDocument == aEvent.view.document)
3307     {
3308         aDragSession.canDrop = false;
3309         return;
3310     }
3313 contentDropObserver.onDrop =
3314 function cdnd_drop(aEvent, aXferData, aDragSession)
3316     var url = transferUtils.retrieveURLFromData(aXferData.data,
3317                                                 aXferData.flavour.contentType);
3318     if (!url || url.search(client.linkRE) == -1)
3319         return;
3321     if (url.search(/\.css$/i) != -1  && confirm(getMsg(MSG_TABDND_DROP, url)))
3322         dispatch("motif", {"motif": url});
3323     else if (url.search(/^ircs?:\/\//i) != -1)
3324         dispatch("goto-url", {"url": url});
3327 contentDropObserver.getSupportedFlavours =
3328 function cdnd_gsf()
3330     var flavourSet = new FlavourSet();
3331     flavourSet.appendFlavour("text/x-moz-url");
3332     flavourSet.appendFlavour("application/x-moz-file", "nsIFile");
3333     flavourSet.appendFlavour("text/unicode");
3334     return flavourSet;
3337 /* Drag and Drop handler for the <tabs> element.
3339  * XXX: Some of the code below has to work around specific limitations in how
3340  * the nsDragAndDrop.js wrapper works. The wrapper greatly simplifies the DnD
3341  * code, though, so it's still worth using.
3342  * 
3343  * XXX: canDrop checks if there is a supported flavour of data because
3344  * nsDragAndDrop does not. This will prevent the drag service from thinking
3345  * we accept any old data when we don't.
3346  * 
3347  * XXX: nsDragAndDrop.checkCanDrop does this:
3348  *     mDragSession.canDrop = mDragSession.sourceNode != aEvent.target;
3349  *     mDragSession.canDrop &= aDragDropObserver.canDrop(...);
3350  * As a result, canDrop cannot override the false canDrop value when the source
3351  * and target are the same (i.e. the same <tab>). Thus, we override this check
3352  * inside onDragOver instead, which is called after canDrop (even if that says
3353  * it can't be dropped, luckily). As a result, after nsDragAndDrop has called
3354  * canDrop and onDragOver, the drag service's canDrop value is true iff there
3355  * is a supported flavour.
3356  * 
3357  * XXX: onDrop is the only place which checks we're getting an IRC URL, as
3358  * accessing the drag data at any other time is both tedious and could
3359  * significantly impact the performance of the drag (getting the data can be
3360  * very slow).
3361  */
3362 var tabsDropObserver = new Object();
3364 tabsDropObserver.canDrop =
3365 function tabdnd_candrop(aEvent, aDragSession)
3367     if (aEvent.getPreventDefault())
3368         return false;
3370     // See comment above |var tabsDropObserver|.
3371     var flavourSet = this.getSupportedFlavours();
3372     for (var flavour in flavourSet.flavourTable)
3373     {
3374         if (aDragSession.isDataFlavorSupported(flavour))
3375             return true;
3376     }
3377     return false;
3380 tabsDropObserver.onDragOver =
3381 function tabdnd_dover(aEvent, aFlavour, aDragSession)
3383     if (aEvent.getPreventDefault())
3384         return;
3386     // See comment above |var tabsDropObserver|.
3387     if (aDragSession.sourceNode == aEvent.target)
3388         aDragSession.canDrop = true;
3390     // If we're not accepting the drag, don't show the marker either.
3391     if (!aDragSession.canDrop)
3392     {
3393         client.tabDragBar.collapsed = true;
3394         return;
3395     }
3397     /* Locate the tab we're about to drop onto. We split tabs in half, dropping
3398      * on the side closest to the mouse, or after the last tab if the mouse is
3399      * somewhere beyond all the tabs.
3400      */
3401     var ltr = (window.getComputedStyle(client.tabs, null).direction == "ltr");
3402     var newPosition = client.tabs.firstChild.boxObject.x;
3403     for (var dropTab = client.tabs.firstChild; dropTab;
3404          dropTab = dropTab.nextSibling)
3405     {
3406         if (dropTab.collapsed || dropTab.hidden)
3407             continue;
3408         var bo = dropTab.boxObject;
3409         if ((ltr && (aEvent.screenX < bo.screenX + bo.width / 2)) ||
3410             (!ltr && (aEvent.screenX > bo.screenX + bo.width / 2)))
3411         {
3412             break;
3413         }
3414         newPosition = bo.x + bo.width;
3415     }
3417     // Reposition the drop marker and show it. In that order.
3418     client.tabDragMarker.style.MozMarginStart = newPosition + "px";
3419     client.tabDragBar.collapsed = false;
3422 tabsDropObserver.onDragExit =
3423 function tabdnd_dexit(aEvent, aDragSession)
3425     if (aEvent.getPreventDefault())
3426         return;
3428     /* We've either stopped being part of a drag operation, or the dragging is
3429      * somewhere away from us.
3430      */
3431     client.tabDragBar.collapsed = true;
3434 tabsDropObserver.onDrop =
3435 function tabdnd_drop(aEvent, aXferData, aDragSession)
3437     // See comment above |var tabsDropObserver|.
3438     var url = transferUtils.retrieveURLFromData(aXferData.data,
3439                                                 aXferData.flavour.contentType);
3440     if (!url || !url.match(/^ircs?:/))
3441         return;
3443     // Find the tab to insertBefore() the new one.
3444     var ltr = (window.getComputedStyle(client.tabs, null).direction == "ltr");
3445     for (var dropTab = client.tabs.firstChild; dropTab;
3446          dropTab = dropTab.nextSibling)
3447     {
3448         if (dropTab.collapsed || dropTab.hidden)
3449             continue;
3450         var bo = dropTab.boxObject;
3451         if ((ltr && (aEvent.screenX < bo.screenX + bo.width / 2)) ||
3452             (!ltr && (aEvent.screenX > bo.screenX + bo.width / 2)))
3453         {
3454             break;
3455         }
3456     }
3458     // Check if the URL is already in the views.
3459     for (var i = 0; i < client.viewsArray.length; i++)
3460     {
3461         var view = client.viewsArray[i].source;
3462         if (view.getURL() == url)
3463         {
3464             client.pendingViewContext = { tabInsertBefore: dropTab };
3465             dispatch("create-tab-for-view", { view: view });
3466             delete client.pendingViewContext;
3467             return;
3468         }
3469     }
3471     // URL not found in tabs, so force it into life - this may connect/rejoin.
3472     gotoIRCURL(url, { tabInsertBefore: dropTab });
3475 tabsDropObserver.getSupportedFlavours =
3476 function tabdnd_gsf()
3478     var flavourSet = new FlavourSet();
3479     flavourSet.appendFlavour("text/x-moz-url");
3480     flavourSet.appendFlavour("text/unicode");
3481     return flavourSet;
3484 var tabDNDObserver = new Object();
3486 tabDNDObserver.onDragStart =
3487 function tabdnd_dstart (aEvent, aXferData, aDragAction)
3489     var tb = aEvent.currentTarget;
3490     var href = tb.getAttribute("href");
3491     var name = tb.getAttribute("name");
3493     aXferData.data = new TransferData();
3494     /* x-moz-url has the format "<url>\n<name>", goodie */
3495     aXferData.data.addDataForFlavour("text/x-moz-url", href + "\n" + name);
3496     aXferData.data.addDataForFlavour("text/unicode", href);
3497     aXferData.data.addDataForFlavour("text/html", "<a href='" + href + "'>" +
3498                                      name + "</a>");
3501 var userlistDNDObserver = new Object();
3503 userlistDNDObserver.onDragStart =
3504 function userlistdnd_dstart(event, transferData, dragAction)
3506     var col = new Object(), row = new Object(), cell = new Object();
3507     var tree = document.getElementById('user-list');
3508     tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, cell);
3509     // Check whether we're actually on a normal row and cell
3510     if (!cell.value || (row.value == -1))
3511         return;
3513     var nickname = getNicknameForUserlistRow(row.value);
3514     transferData.data = new TransferData();
3515     transferData.data.addDataForFlavour("text/unicode", nickname);
3518 function deleteTab(tb)
3520     if (!ASSERT(tb.hasAttribute("viewKey"),
3521                 "INVALID OBJECT passed to deleteTab (" + tb + ")"))
3522     {
3523         return null;
3524     }
3526     var key = Number(tb.getAttribute("viewKey"));
3528     // Re-index higher tabs.
3529     for (var i = key + 1; i < client.viewsArray.length; i++)
3530         client.viewsArray[i].tb.setAttribute("viewKey", i - 1);
3531     arrayRemoveAt(client.viewsArray, key);
3532     client.tabs.removeChild(tb);
3533     updateTabAttributes();
3535     return key;
3538 function filterOutput(msg, msgtype, dest)
3540     if ("outputFilters" in client)
3541     {
3542         for (var f in client.outputFilters)
3543         {
3544             if (client.outputFilters[f].enabled)
3545                 msg = client.outputFilters[f].func(msg, msgtype, dest);
3546         }
3547     }
3549     return msg;
3552 function updateTimestamps(view)
3554     if (!("messages" in view))
3555         return;
3557     view._timestampLast = "";
3558     var node = view.messages.firstChild.firstChild;
3559     var nested;
3560     while (node)
3561     {
3562         if(node.className == "msg-nested-tr")
3563         {
3564             nested = node.firstChild.firstChild.firstChild.firstChild;
3565             while (nested)
3566             {
3567                 updateTimestampFor(view, nested);
3568                 nested = nested.nextSibling;
3569             }
3570         }
3571         else
3572         {
3573             updateTimestampFor(view, node);
3574         }
3575         node = node.nextSibling;
3576     }
3579 function updateTimestampFor(view, displayRow)
3581     var time = new Date(1 * displayRow.getAttribute("timestamp"));
3582     var tsCell = displayRow.firstChild;
3583     if (!tsCell)
3584         return;
3586     var fmt;
3587     if (view.prefs["timestamps"])
3588         fmt = strftime(view.prefs["timestamps.display"], time);
3590     while (tsCell.lastChild)
3591         tsCell.removeChild(tsCell.lastChild);
3593     if (fmt && (!client.prefs["collapseMsgs"] || (fmt != view._timestampLast)))
3594         tsCell.appendChild(document.createTextNode(fmt));
3595     view._timestampLast = fmt;
3598 client.updateMenus =
3599 function c_updatemenus(menus)
3601     // Don't bother if the menus aren't even created yet.
3602     if (!client.initialized)
3603         return null;
3605     return this.menuManager.updateMenus(document, menus);
3608 client.adoptNode =
3609 function cli_adoptnode(node, doc)
3611     try
3612     {
3613         doc.adoptNode(node);
3614     }
3615     catch(ex)
3616     {
3617         dd(formatException(ex));
3618         var err = ex.name;
3619         // TypeError from before adoptNode was added; NOT_IMPL after.
3620         if ((err == "TypeError") || (err == "NS_ERROR_NOT_IMPLEMENTED"))
3621             client.adoptNode = cli_adoptnode_noop;
3622     }
3623     return node;
3626 function cli_adoptnode_noop(node, doc)
3628     return node;
3631 client.addNetwork =
3632 function cli_addnet(name, serverList, temporary)
3634     client.networks[name] =
3635         new CIRCNetwork(name, serverList, client.eventPump, temporary);
3638 client.removeNetwork =
3639 function cli_removenet(name)
3641     // Allow network a chance to clean up any mess.
3642     if (typeof client.networks[name].destroy == "function")
3643         client.networks[name].destroy();
3645     delete client.networks[name];
3648 client.connectToNetwork =
3649 function cli_connect(networkOrName, requireSecurity)
3651     var network;
3652     var name;
3655     if (isinstance(networkOrName, CIRCNetwork))
3656     {
3657         network = networkOrName;
3658     }
3659     else
3660     {
3661         name = networkOrName;
3663         if (!(name in client.networks))
3664         {
3665             display(getMsg(MSG_ERR_UNKNOWN_NETWORK, name), MT_ERROR);
3666             return null;
3667         }
3669         network = client.networks[name];
3670     }
3671     name = network.unicodeName;
3673     if (!("messages" in network))
3674         network.displayHere(getMsg(MSG_NETWORK_OPENED, name));
3676     dispatch("set-current-view", { view: network });
3678     if (network.isConnected())
3679     {
3680         network.display(getMsg(MSG_ALREADY_CONNECTED, name));
3681         return network;
3682     }
3684     if (network.state != NET_OFFLINE)
3685         return network;
3687     if (network.prefs["nickname"] == DEFAULT_NICK)
3688         network.prefs["nickname"] = prompt(MSG_ENTER_NICK, DEFAULT_NICK);
3690     if (!("connecting" in network))
3691         network.display(getMsg(MSG_NETWORK_CONNECTING, name));
3693     network.connect(requireSecurity);
3695     network.updateHeader();
3696     client.updateHeader();
3697     updateTitle();
3699     return network;
3703 client.getURL =
3704 function cli_geturl ()
3706     return "irc://";
3709 client.load =
3710 function cli_load(url, scope)
3712     if (!("_loader" in client))
3713     {
3714         const LOADER_CTRID = "@mozilla.org/moz/jssubscript-loader;1";
3715         const mozIJSSubScriptLoader =
3716             Components.interfaces.mozIJSSubScriptLoader;
3718         var cls;
3719         if ((cls = Components.classes[LOADER_CTRID]))
3720             client._loader = cls.getService(mozIJSSubScriptLoader);
3721     }
3723     return client._loader.loadSubScript(url, scope);
3726 client.sayToCurrentTarget =
3727 function cli_say(msg, isInteractive)
3729     if ("say" in client.currentObject)
3730     {
3731         client.currentObject.dispatch("say", {message: msg}, isInteractive);
3732         return;
3733     }
3735     switch (client.currentObject.TYPE)
3736     {
3737         case "IRCClient":
3738             dispatch("eval", {expression: msg}, isInteractive);
3739             break;
3741         default:
3742             if (msg != "")
3743                 display(MSG_ERR_NO_DEFAULT, MT_ERROR);
3744             break;
3745     }
3748 CIRCNetwork.prototype.__defineGetter__("prefs", net_getprefs);
3749 function net_getprefs()
3751     if (!("_prefs" in this))
3752     {
3753         this._prefManager = getNetworkPrefManager(this);
3754         this._prefs = this._prefManager.prefs;
3755     }
3757     return this._prefs;
3760 CIRCNetwork.prototype.__defineGetter__("prefManager", net_getprefmgr);
3761 function net_getprefmgr()
3763     if (!("_prefManager" in this))
3764     {
3765         this._prefManager = getNetworkPrefManager(this);
3766         this._prefs = this._prefManager.prefs;
3767     }
3769     return this._prefManager;
3772 CIRCServer.prototype.__defineGetter__("prefs", srv_getprefs);
3773 function srv_getprefs()
3775     return this.parent.prefs;
3778 CIRCServer.prototype.__defineGetter__("prefManager", srv_getprefmgr);
3779 function srv_getprefmgr()
3781     return this.parent.prefManager;
3784 CIRCChannel.prototype.__defineGetter__("prefs", chan_getprefs);
3785 function chan_getprefs()
3787     if (!("_prefs" in this))
3788     {
3789         this._prefManager = getChannelPrefManager(this);
3790         this._prefs = this._prefManager.prefs;
3791     }
3793     return this._prefs;
3796 CIRCChannel.prototype.__defineGetter__("prefManager", chan_getprefmgr);
3797 function chan_getprefmgr()
3799     if (!("_prefManager" in this))
3800     {
3801         this._prefManager = getChannelPrefManager(this);
3802         this._prefs = this._prefManager.prefs;
3803     }
3805     return this._prefManager;
3808 CIRCUser.prototype.__defineGetter__("prefs", usr_getprefs);
3809 function usr_getprefs()
3811     if (!("_prefs" in this))
3812     {
3813         this._prefManager = getUserPrefManager(this);
3814         this._prefs = this._prefManager.prefs;
3815     }
3817     return this._prefs;
3820 CIRCUser.prototype.__defineGetter__("prefManager", usr_getprefmgr);
3821 function usr_getprefmgr()
3823     if (!("_prefManager" in this))
3824     {
3825         this._prefManager = getUserPrefManager(this);
3826         this._prefs = this._prefManager.prefs;
3827     }
3829     return this._prefManager;
3832 CIRCDCCUser.prototype.__defineGetter__("prefs", dccusr_getprefs);
3833 function dccusr_getprefs()
3835     if (!("_prefs" in this))
3836     {
3837         this._prefManager = getDCCUserPrefManager(this);
3838         this._prefs = this._prefManager.prefs;
3839     }
3841     return this._prefs;
3844 CIRCDCCUser.prototype.__defineGetter__("prefManager", dccusr_getprefmgr);
3845 function dccusr_getprefmgr()
3847     if (!("_prefManager" in this))
3848     {
3849         this._prefManager = getDCCUserPrefManager(this);
3850         this._prefs = this._prefManager.prefs;
3851     }
3853     return this._prefManager;
3856 CIRCDCCChat.prototype.__defineGetter__("prefs", dccchat_getprefs);
3857 function dccchat_getprefs()
3859     return this.user.prefs;
3862 CIRCDCCChat.prototype.__defineGetter__("prefManager", dccchat_getprefmgr);
3863 function dccchat_getprefmgr()
3865     return this.user.prefManager;
3868 CIRCDCCFileTransfer.prototype.__defineGetter__("prefs", dccfile_getprefs);
3869 function dccfile_getprefs()
3871     return this.user.prefs;
3874 CIRCDCCFileTransfer.prototype.__defineGetter__("prefManager", dccfile_getprefmgr);
3875 function dccfile_getprefmgr()
3877     return this.user.prefManager;
3880 CIRCNetwork.prototype.display =
3881 function net_display (message, msgtype, sourceObj, destObj)
3883     var o = getObjectDetails(client.currentObject);
3885     if (client.SLOPPY_NETWORKS && client.currentObject != this &&
3886         o.network == this && o.server && o.server.isConnected)
3887     {
3888         client.currentObject.display (message, msgtype, sourceObj, destObj);
3889     }
3890     else
3891     {
3892         this.displayHere (message, msgtype, sourceObj, destObj);
3893     }
3896 CIRCUser.prototype.display =
3897 function usr_display(message, msgtype, sourceObj, destObj)
3899     if ("messages" in this)
3900     {
3901         this.displayHere (message, msgtype, sourceObj, destObj);
3902     }
3903     else
3904     {
3905         var o = getObjectDetails(client.currentObject);
3906         if (o.server && o.server.isConnected &&
3907             o.network == this.parent.parent &&
3908             client.currentObject.TYPE != "IRCUser")
3909             client.currentObject.display (message, msgtype, sourceObj, destObj);
3910         else
3911             this.parent.parent.displayHere (message, msgtype, sourceObj,
3912                                             destObj);
3913     }
3916 CIRCDCCChat.prototype.display =
3917 CIRCDCCFileTransfer.prototype.display =
3918 function dcc_display(message, msgtype, sourceObj, destObj)
3920     var o = getObjectDetails(client.currentObject);
3922     if ("messages" in this)
3923         this.displayHere(message, msgtype, sourceObj, destObj);
3924     else
3925         client.currentObject.display(message, msgtype, sourceObj, destObj);
3928 function feedback(e, message, msgtype, sourceObj, destObj)
3930     if ("isInteractive" in e && e.isInteractive)
3931         display(message, msgtype, sourceObj, destObj);
3934 CIRCChannel.prototype.feedback =
3935 CIRCNetwork.prototype.feedback =
3936 CIRCUser.prototype.feedback =
3937 CIRCDCCChat.prototype.feedback =
3938 CIRCDCCFileTransfer.prototype.feedback =
3939 client.feedback =
3940 function this_feedback(e, message, msgtype, sourceObj, destObj)
3942     if ("isInteractive" in e && e.isInteractive)
3943         this.displayHere(message, msgtype, sourceObj, destObj);
3946 function display (message, msgtype, sourceObj, destObj)
3948     client.currentObject.display (message, msgtype, sourceObj, destObj);
3951 client.getFontCSS =
3952 CIRCNetwork.prototype.getFontCSS =
3953 CIRCChannel.prototype.getFontCSS =
3954 CIRCUser.prototype.getFontCSS =
3955 CIRCDCCChat.prototype.getFontCSS =
3956 CIRCDCCFileTransfer.prototype.getFontCSS =
3957 function this_getFontCSS(format)
3959     /* Wow, this is cool. We just put together a CSS-rule string based on the
3960      * font preferences. *This* is what CSS is all about. :)
3961      * We also provide a "data: URL" format, to simplify other code.
3962      */
3963     var css;
3964     var fs;
3965     var fn;
3967     if (this.prefs["font.family"] != "default")
3968         fn = "font-family: " + this.prefs["font.family"] + ";";
3969     else
3970         fn = "font-family: inherit;";
3971     if (this.prefs["font.size"] != 0)
3972         fs = "font-size: " + this.prefs["font.size"] + "pt;";
3973     else
3974         fs = "font-size: medium;";
3976     css = "body.chatzilla-body { " + fs + fn + " }";
3978     if (format == "data")
3979         return "data:text/css," + encodeURIComponent(css);
3980     return css;
3983 client.display =
3984 client.displayHere =
3985 CIRCNetwork.prototype.displayHere =
3986 CIRCChannel.prototype.display =
3987 CIRCChannel.prototype.displayHere =
3988 CIRCUser.prototype.displayHere =
3989 CIRCDCCChat.prototype.displayHere =
3990 CIRCDCCFileTransfer.prototype.displayHere =
3991 function __display(message, msgtype, sourceObj, destObj)
3993     // We need a message type, assume "INFO".
3994     if (!msgtype)
3995         msgtype = MT_INFO;
3997     var msgprefix = "";
3998     if (msgtype.indexOf("/") != -1)
3999     {
4000         var ary = msgtype.match(/^(.*)\/(.*)$/);
4001         msgtype = ary[1];
4002         msgprefix = ary[2];
4003     }
4005     var blockLevel = false; /* true if this row should be rendered at block
4006                              * level, (like, if it has a really long nickname
4007                              * that might disturb the rest of the layout)     */
4008     var o = getObjectDetails(this);           /* get the skinny on |this|     */
4010     // Get the 'me' object, so we can be sure to get the attributes right.
4011     var me;
4012     if ("me" in this)
4013         me = this.me;
4014     else if (o.server && "me" in o.server)
4015         me = o.server.me;
4017     // Let callers get away with "ME!" and we have to substitute here.
4018     if (sourceObj == "ME!")
4019         sourceObj = me;
4020     if (destObj == "ME!")
4021         destObj = me;
4023     // Get the TYPE of the source object.
4024     var fromType = (sourceObj && sourceObj.TYPE) ? sourceObj.TYPE : "unk";
4025     // Is the source a user?
4026     var fromUser = (fromType.search(/IRC.*User/) != -1);
4027     // Get some sort of "name" for the source.
4028     var fromAttr = "";
4029     if (sourceObj)
4030     {
4031         if ("unicodeName" in sourceObj)
4032             fromAttr = sourceObj.unicodeName;
4033         else if ("name" in sourceObj)
4034             fromAttr = sourceObj.name;
4035         else
4036             fromAttr = sourceObj.viewName;
4037     }
4038     // Attach "ME!" if appropriate, so motifs can style differently.
4039     if (sourceObj == me)
4040         fromAttr = fromAttr + " ME!";
4042     // Get the dest TYPE too...
4043     var toType = (destObj) ? destObj.TYPE : "unk";
4044     // Is the dest a user?
4045     var toUser = (toType.search(/IRC.*User/) != -1);
4046     // Get a dest name too...
4047     var toAttr = "";
4048     if (destObj)
4049     {
4050         if ("unicodeName" in destObj)
4051             toAttr = destObj.unicodeName;
4052         else if ("name" in destObj)
4053             toAttr = destObj.name;
4054         else
4055             toAttr = destObj.viewName;
4056     }
4057     // Also do "ME!" work for the dest.
4058     if (destObj && destObj == me)
4059         toAttr = me.unicodeName + " ME!";
4061     /* isImportant means to style the messages as important, and flash the
4062      * window, getAttention means just flash the window. */
4063     var isImportant = false, getAttention = false, isSuperfluous = false;
4064     var viewType = this.TYPE;
4065     var code;
4066     var time = new Date();
4068     var timeStamp = strftime(this.prefs["timestamps.log"], time);
4070     // Statusbar text, and the line that gets saved to the log.
4071     var statusString;
4072     var logStringPfx = timeStamp + " ";
4073     var logStrings = new Array();
4075     if (fromUser)
4076     {
4077         statusString = getMsg(MSG_FMT_STATUS,
4078                               [timeStamp,
4079                                sourceObj.unicodeName + "!" +
4080                                sourceObj.name + "@" + sourceObj.host]);
4081     }
4082     else
4083     {
4084         var name;
4085         if (sourceObj)
4086             name = sourceObj.viewName;
4087         else
4088             name = this.viewName;
4090         statusString = getMsg(MSG_FMT_STATUS,
4091                               [timeStamp, name]);
4092     }
4094     // The table row, and it's attributes.
4095     var msgRow = document.createElementNS(XHTML_NS, "html:tr");
4096     msgRow.setAttribute("class", "msg");
4097     msgRow.setAttribute("msg-type", msgtype);
4098     msgRow.setAttribute("msg-prefix", msgprefix);
4099     msgRow.setAttribute("msg-dest", toAttr);
4100     msgRow.setAttribute("dest-type", toType);
4101     msgRow.setAttribute("view-type", viewType);
4102     msgRow.setAttribute("statusText", statusString);
4103     msgRow.setAttribute("timestamp", Number(time));
4104     if (fromAttr)
4105     {
4106         if (fromUser)
4107             msgRow.setAttribute("msg-user", fromAttr);
4108         else
4109             msgRow.setAttribute("msg-source", fromAttr);
4110     }
4112     // Timestamp cell.
4113     var msgRowTimestamp = document.createElementNS(XHTML_NS, "html:td");
4114     msgRowTimestamp.setAttribute("class", "msg-timestamp");
4116     var canMergeData;
4117     var msgRowSource, msgRowType, msgRowData;
4118     if (fromUser && msgtype.match(/^(PRIVMSG|ACTION|NOTICE|WALLOPS)$/))
4119     {
4120         var nick = sourceObj.unicodeName;
4121         var decorSt = "";
4122         var decorEn = "";
4124         // Set default decorations.
4125         if (msgtype == "ACTION")
4126         {
4127             decorSt = "* ";
4128         }
4129         else
4130         {
4131             decorSt = "<";
4132             decorEn = ">";
4133         }
4135         var nickURL;
4136         if ((sourceObj != me) && ("getURL" in sourceObj))
4137             nickURL = sourceObj.getURL();
4139         if (sourceObj != me)
4140         {
4141             // Not from us...
4142             if (destObj == me)
4143             {
4144                 // ...but to us. Messages from someone else to us.
4146                 getAttention = true;
4147                 this.defaultCompletion = "/msg " + nick + " ";
4149                 // If this is a private message, and it's not in a query view,
4150                 // use *nick* instead of <nick>.
4151                 if ((msgtype != "ACTION") && (this.TYPE != "IRCUser"))
4152                 {
4153                     decorSt = "*";
4154                     decorEn = "*";
4155                 }
4156             }
4157             else
4158             {
4159                 // ...or to us. Messages from someone else to channel or similar.
4161                 if ((typeof message == "string") && me)
4162                 {
4163                     isImportant = msgIsImportant(message, nick, o.network);
4164                     if (isImportant)
4165                     {
4166                         this.defaultCompletion = nick +
4167                             client.prefs["nickCompleteStr"] + " ";
4168                     }
4169                 }
4170             }
4171         }
4172         else
4173         {
4174             // Messages from us, on a channel or network view, to a user
4175             if (toUser && (this.TYPE != "IRCUser"))
4176             {
4177                 nick = destObj.unicodeName;
4178                 decorSt = ">";
4179                 decorEn = "<";
4180             }
4181         }
4183         // Log the nickname in the same format as we'll let the user copy.
4184         // If the message has a prefix, show it after a "/".
4185         if (msgprefix)
4186             logStringPfx += decorSt + nick + "/" + msgprefix + decorEn + " ";
4187         else
4188             logStringPfx += decorSt + nick + decorEn + " ";
4190         if (!("lastNickDisplayed" in this) || this.lastNickDisplayed != nick)
4191         {
4192             this.lastNickDisplayed = nick;
4193             this.mark = (("mark" in this) && this.mark == "even") ? "odd" : "even";
4194         }
4196         msgRowSource = document.createElementNS(XHTML_NS, "html:td");
4197         msgRowSource.setAttribute("class", "msg-user");
4199         // Make excessive nicks get shunted.
4200         if (nick && (nick.length > client.MAX_NICK_DISPLAY))
4201             blockLevel = true;
4203         if (decorSt)
4204             msgRowSource.appendChild(newInlineText(decorSt, "chatzilla-decor"));
4205         if (nickURL)
4206         {
4207             var nick_anchor = document.createElementNS(XHTML_NS, "html:a");
4208             nick_anchor.setAttribute("class", "chatzilla-link");
4209             nick_anchor.setAttribute("href", nickURL);
4210             nick_anchor.appendChild(newInlineText(nick));
4211             msgRowSource.appendChild(nick_anchor);
4212         }
4213         else
4214         {
4215             msgRowSource.appendChild(newInlineText(nick));
4216         }
4217         if (msgprefix)
4218         {
4219             /* We don't style the "/" with chatzilla-decor because the one
4220              * thing we don't want is it disappearing!
4221              */
4222             msgRowSource.appendChild(newInlineText("/", ""));
4223             msgRowSource.appendChild(newInlineText(msgprefix,
4224                                                    "chatzilla-prefix"));
4225         }
4226         if (decorEn)
4227             msgRowSource.appendChild(newInlineText(decorEn, "chatzilla-decor"));
4228         canMergeData = this.prefs["collapseMsgs"];
4229     }
4230     else if (msgprefix)
4231     {
4232         decorSt = "<";
4233         decorEn = ">";
4235         logStringPfx += decorSt + "/" + msgprefix + decorEn + " ";
4237         msgRowSource = document.createElementNS(XHTML_NS, "html:td");
4238         msgRowSource.setAttribute("class", "msg-user");
4240         msgRowSource.appendChild(newInlineText(decorSt, "chatzilla-decor"));
4241         msgRowSource.appendChild(newInlineText("/", ""));
4242         msgRowSource.appendChild(newInlineText(msgprefix, "chatzilla-prefix"));
4243         msgRowSource.appendChild(newInlineText(decorEn, "chatzilla-decor"));
4244         canMergeData = this.prefs["collapseMsgs"];
4245     }
4246     else
4247     {
4248         isSuperfluous = true;
4249         if (!client.debugHook.enabled && msgtype in client.responseCodeMap)
4250         {
4251             code = client.responseCodeMap[msgtype];
4252         }
4253         else
4254         {
4255             if (!client.debugHook.enabled && client.HIDE_CODES)
4256                 code = client.DEFAULT_RESPONSE_CODE;
4257             else
4258                 code = "[" + msgtype + "]";
4259         }
4261         /* Display the message code */
4262         msgRowType = document.createElementNS(XHTML_NS, "html:td");
4263         msgRowType.setAttribute("class", "msg-type");
4265         msgRowType.appendChild(newInlineText(code));
4266         logStringPfx += code + " ";
4267     }
4269     if (message)
4270     {
4271         msgRowData = document.createElementNS(XHTML_NS, "html:td");
4272         msgRowData.setAttribute("class", "msg-data");
4274         var tmpMsgs = message;
4275         if (typeof message == "string")
4276         {
4277             msgRowData.appendChild(stringToMsg(message, this));
4278         }
4279         else
4280         {
4281             msgRowData.appendChild(message);
4282             tmpMsgs = tmpMsgs.innerHTML.replace(/<[^<]*>/g, "");
4283         }
4284         tmpMsgs = tmpMsgs.split(/\r?\n/);
4285         for (var l = 0; l < tmpMsgs.length; l++)
4286             logStrings[l] = logStringPfx + tmpMsgs[l];
4287     }
4289     if ("mark" in this)
4290         msgRow.setAttribute("mark", this.mark);
4292     if (isImportant)
4293     {
4294         msgRow.setAttribute("important", "true");
4295         msgRow.setAttribute("aria-channel", "notify");
4296     }
4298     // Timestamps first...
4299     msgRow.appendChild(msgRowTimestamp);
4300     // Now do the rest of the row, after block-level stuff.
4301     if (msgRowSource)
4302         msgRow.appendChild(msgRowSource);
4303     else
4304         msgRow.appendChild(msgRowType);
4305     if (msgRowData)
4306         msgRow.appendChild(msgRowData);
4307     updateTimestampFor(this, msgRow);
4309     if (blockLevel)
4310     {
4311         /* putting a div here crashes mozilla, so fake it with nested tables
4312          * for now */
4313         var tr = document.createElementNS(XHTML_NS, "html:tr");
4314         tr.setAttribute ("class", "msg-nested-tr");
4315         var td = document.createElementNS(XHTML_NS, "html:td");
4316         td.setAttribute ("class", "msg-nested-td");
4317         td.setAttribute ("colspan", "3");
4319         tr.appendChild(td);
4320         var table = document.createElementNS(XHTML_NS, "html:table");
4321         table.setAttribute ("class", "msg-nested-table");
4322         table.setAttribute("role", "presentation");
4324         td.appendChild (table);
4325         var tbody =  document.createElementNS(XHTML_NS, "html:tbody");
4327         tbody.appendChild(msgRow);
4328         table.appendChild(tbody);
4329         msgRow = tr;
4330     }
4332     // Actually add the item.
4333     addHistory (this, msgRow, canMergeData);
4335     // Update attention states...
4336     if (isImportant || getAttention)
4337     {
4338         setTabState(this, "attention");
4339         if (client.prefs["notify.aggressive"])
4340             window.getAttention();
4341     }
4342     else
4343     {
4344         if (isSuperfluous)
4345         {
4346             setTabState(this, "superfluous");
4347         }
4348         else
4349         {
4350             setTabState(this, "activity");
4351         }
4352     }
4354     // Copy Important Messages [to network view].
4355     if (isImportant && client.prefs["copyMessages"] && (o.network != this))
4356     {
4357         o.network.displayHere("{" + this.unicodeName + "} " + message, msgtype,
4358                               sourceObj, destObj);
4359     }
4361     // Log file time!
4362     if (this.prefs["log"])
4363     {
4364         if (!this.logFile)
4365             client.openLogFile(this);
4367         try
4368         {
4369             var LE = client.lineEnd;
4370             for (var l = 0; l < logStrings.length; l++)
4371                 this.logFile.write(fromUnicode(logStrings[l] + LE, "utf-8"));
4372         }
4373         catch (ex)
4374         {
4375             // Stop logging before showing any messages!
4376             this.prefs["log"] = false;
4377             dd("Log file write error: " + formatException(ex));
4378             this.displayHere(getMsg(MSG_LOGFILE_WRITE_ERROR, getLogPath(this)),
4379                              "ERROR");
4380         }
4381     }
4384 function addHistory (source, obj, mergeData)
4386     if (!("messages" in source) || (source.messages == null))
4387         createMessages(source);
4389     var tbody = source.messages.firstChild;
4390     var appendTo = tbody;
4392     var needScroll = false;
4394     if (mergeData)
4395     {
4396         var inobj = obj;
4397         // This gives us the non-nested row when there is nesting.
4398         if (inobj.className == "msg-nested-tr")
4399             inobj = inobj.firstChild.firstChild.firstChild.firstChild;
4401         var thisUserCol = inobj.firstChild;
4402         while (thisUserCol && !thisUserCol.className.match(/^(msg-user|msg-type)$/))
4403             thisUserCol = thisUserCol.nextSibling;
4405         var thisMessageCol = inobj.firstChild;
4406         while (thisMessageCol && !(thisMessageCol.className == "msg-data"))
4407             thisMessageCol = thisMessageCol.nextSibling;
4409         var ci = findPreviousColumnInfo(source.messages);
4410         var nickColumns = ci.nickColumns;
4411         var rowExtents = ci.extents;
4412         var nickColumnCount = nickColumns.length;
4414         var lastRowSpan, sameNick, sameDest, haveSameType, needSameType;
4415         var isAction, collapseActions;
4416         if (nickColumnCount == 0) // No message to collapse to.
4417         {
4418             sameNick = sameDest = needSameType = haveSameType = false;
4419             lastRowSpan = 0;
4420         }
4421         else // 1 or more messages, check for doubles
4422         {
4423             var lastRow = nickColumns[nickColumnCount - 1].parentNode;
4424             // What was the span last time?
4425             lastRowSpan = Number(nickColumns[0].getAttribute("rowspan"));
4426             // Are we the same user as last time?
4427             sameNick = (lastRow.getAttribute("msg-user") ==
4428                         inobj.getAttribute("msg-user"));
4429             // Do we have the same prefix as last time?
4430             samePrefix = (lastRow.getAttribute("msg-prefix") ==
4431                           inobj.getAttribute("msg-prefix"));
4432             // Do we have the same destination as last time?
4433             sameDest = (lastRow.getAttribute("msg-dest") ==
4434                         inobj.getAttribute("msg-dest"));
4435             // Is this message the same type as the last one?
4436             haveSameType = (lastRow.getAttribute("msg-type") ==
4437                             inobj.getAttribute("msg-type"));
4438             // Is either of the messages an action? We may not want to collapse
4439             // depending on the collapseActions pref
4440             isAction = ((inobj.getAttribute("msg-type") == "ACTION") ||
4441                         (lastRow.getAttribute("msg-type") == "ACTION"));
4442             // Do we collapse actions?
4443             collapseActions = source.prefs["collapseActions"];
4445             // Does the motif collapse everything, regardless of type?
4446             // NOTE: the collapseActions pref can override this for actions
4447             needSameType = !(("motifSettings" in source) &&
4448                              source.motifSettings &&
4449                              ("collapsemore" in source.motifSettings));
4450         }
4452         if (sameNick && samePrefix && sameDest &&
4453             (haveSameType || !needSameType) &&
4454             (!isAction || collapseActions))
4455         {
4456             obj = inobj;
4457             if (ci.nested)
4458                 appendTo = source.messages.firstChild.lastChild.firstChild.firstChild.firstChild;
4460             if (obj.getAttribute("important"))
4461             {
4462                 nickColumns[nickColumnCount - 1].setAttribute("important",
4463                                                               true);
4464             }
4466             // Remove nickname column from new row.
4467             obj.removeChild(thisUserCol);
4469             // Expand previous grouping's nickname cell(s) to fill-in the gap.
4470             for (var i = 0; i < nickColumns.length; ++i)
4471                 nickColumns[i].setAttribute("rowspan", rowExtents.length + 1);
4472         }
4473     }
4475     if ("frame" in source)
4476         needScroll = checkScroll(source.frame);
4477     if (obj)
4478         appendTo.appendChild(client.adoptNode(obj, appendTo.ownerDocument));
4480     if (source.MAX_MESSAGES)
4481     {
4482         if (typeof source.messageCount != "number")
4483             source.messageCount = 1;
4484         else
4485             source.messageCount++;
4487         if (source.messageCount > source.MAX_MESSAGES)
4488         {
4489             // Get the top of row 2, and subtract the top of row 1.
4490             var height = tbody.firstChild.nextSibling.firstChild.offsetTop -
4491                          tbody.firstChild.firstChild.offsetTop;
4492             var window = getContentWindow(source.frame);
4493             var x = window.pageXOffset;
4494             var y = window.pageYOffset;
4495             tbody.removeChild (tbody.firstChild);
4496             --source.messageCount;
4497             while (tbody.firstChild && tbody.firstChild.childNodes[1] &&
4498                    tbody.firstChild.childNodes[1].getAttribute("class") ==
4499                    "msg-data")
4500             {
4501                 --source.messageCount;
4502                 tbody.removeChild (tbody.firstChild);
4503             }
4504             if (!checkScroll(source.frame) && (y > height))
4505                 window.scrollTo(x, y - height);
4506         }
4507     }
4509     if (needScroll)
4510     {
4511         scrollDown(source.frame, true);
4512         setTimeout(scrollDown, 500, source.frame, false);
4513         setTimeout(scrollDown, 1000, source.frame, false);
4514         setTimeout(scrollDown, 2000, source.frame, false);
4515     }
4518 function findPreviousColumnInfo(table)
4520     // All the rows in the grouping (for merged rows).
4521     var extents = new Array();
4522     // Get the last row in the table.
4523     var tr = table.firstChild.lastChild;
4524     // Bail if there's no rows.
4525     if (!tr)
4526         return {extents: [], nickColumns: [], nested: false};
4527     // Get message type.
4528     if (tr.className == "msg-nested-tr")
4529     {
4530         var rv = findPreviousColumnInfo(tr.firstChild.firstChild);
4531         rv.nested = true;
4532         return rv;
4533     }
4534     // Now get the read one...
4535     var className = (tr && tr.childNodes[1]) ? tr.childNodes[1].getAttribute("class") : "";
4536     // Keep going up rows until you find the first in a group.
4537     // This will go up until it hits the top of a multiline/merged block.
4538     while (tr && tr.childNodes[1] && className.search(/msg-user|msg-type/) == -1)
4539     {
4540         extents.push(tr);
4541         tr = tr.previousSibling;
4542         if (tr && tr.childNodes[1])
4543             className = tr.childNodes[1].getAttribute("class");
4544     }
4546     // If we ran out of rows, or it's not a talking line, we're outta here.
4547     if (!tr || className != "msg-user")
4548         return {extents: [], nickColumns: [], nested: false};
4550     extents.push(tr);
4552     // Time to collect the nick data...
4553     var nickCol = tr.firstChild;
4554     // All the cells that contain nickname info.
4555     var nickCols = new Array();
4556     while (nickCol)
4557     {
4558         // Just collect nickname column cells.
4559         if (nickCol.getAttribute("class") == "msg-user")
4560             nickCols.push(nickCol);
4561         nickCol = nickCol.nextSibling;
4562     }
4564     // And we're done.
4565     return {extents: extents, nickColumns: nickCols, nested: false};
4568 function getLogPath(obj)
4570     // If we're logging, return the currently-used URL.
4571     if (obj.logFile)
4572         return getURLSpecFromFile(obj.logFile.path);
4573     // If not, return the ideal URL.
4574     return getURLSpecFromFile(obj.prefs["logFileName"]);
4577 client.getConnectionCount =
4578 function cli_gccount ()
4580     var count = 0;
4582     for (var n in client.networks)
4583     {
4584         if (client.networks[n].isConnected())
4585             ++count;
4586     }
4588     return count;
4591 client.quit =
4592 function cli_quit (reason)
4594     var net, netReason;
4595     for (var n in client.networks)
4596     {
4597         net = client.networks[n];
4598         if (net.isConnected())
4599         {
4600             netReason = (reason ? reason : net.prefs["defaultQuitMsg"]);
4601             netReason = (netReason ? netReason : client.userAgent);
4602             net.quit(netReason);
4603         }
4604     }
4607 client.wantToQuit =
4608 function cli_wantToQuit(reason, deliberate)
4611     var close = true;
4612     if (client.prefs["warnOnClose"] && !deliberate)
4613     {
4614         const buttons = [MSG_QUIT_ANYWAY, MSG_DONT_QUIT];
4615         var checkState = { value: true };
4616         var rv = confirmEx(MSG_CONFIRM_QUIT, buttons, 0, MSG_WARN_ON_EXIT,
4617                            checkState);
4618         close = (rv == 0);
4619         client.prefs["warnOnClose"] = checkState.value;
4620     }
4622     if (close)
4623     {
4624         client.userClose = true;
4625         display(MSG_CLOSING);
4626         client.quit(reason);
4627     }
4630 /* gets a tab-complete match for the line of text specified by |line|.
4631  * wordStart is the position within |line| that starts the word being matched,
4632  * wordEnd marks the end position.  |cursorPos| marks the position of the caret
4633  * in the textbox.
4634  */
4635 client.performTabMatch =
4636 function gettabmatch_usr (line, wordStart, wordEnd, word, cursorPos)
4638     if (wordStart != 0 || line[0] != client.COMMAND_CHAR)
4639         return null;
4641     var matches = client.commandManager.listNames(word.substr(1), CMD_CONSOLE);
4642     if (matches.length == 1 && wordEnd == line.length)
4643     {
4644         matches[0] = client.COMMAND_CHAR + matches[0] + " ";
4645     }
4646     else
4647     {
4648         for (var i in matches)
4649             matches[i] = client.COMMAND_CHAR + matches[i];
4650     }
4652     return matches;
4655 client.openLogFile =
4656 function cli_startlog (view)
4658     function getNextLogFileDate()
4659     {
4660         var d = new Date();
4661         d.setMilliseconds(0);
4662         d.setSeconds(0);
4663         d.setMinutes(0);
4664         switch (view.smallestLogInterval)
4665         {
4666             case "h":
4667                 return d.setHours(d.getHours() + 1);
4668             case "d":
4669                 d.setHours(0);
4670                 return d.setDate(d.getDate() + 1);
4671             case "m":
4672                 d.setHours(0);
4673                 d.setDate(1);
4674                 return d.setMonth(d.getMonth() + 1);
4675             case "y":
4676                 d.setHours(0);
4677                 d.setDate(1);
4678                 d.setMonth(0);
4679                 return d.setFullYear(d.getFullYear() + 1);
4680         }
4681         //XXXhack: This should work...
4682         return Infinity;
4683     };
4685     const NORMAL_FILE_TYPE = Components.interfaces.nsIFile.NORMAL_FILE_TYPE;
4687     try
4688     {
4689         var file = new LocalFile(view.prefs["logFileName"]);
4690         if (!file.localFile.exists())
4691         {
4692             // futils.umask may be 0022. Result is 0644.
4693             file.localFile.create(NORMAL_FILE_TYPE, 0666 & ~futils.umask);
4694         }
4695         view.logFile = fopen(file.localFile, ">>");
4696         // If we're here, it's safe to say when we should re-open:
4697         view.nextLogFileDate = getNextLogFileDate();
4698     }
4699     catch (ex)
4700     {
4701         view.prefs["log"] = false;
4702         dd("Log file open error: " + formatException(ex));
4703         view.displayHere(getMsg(MSG_LOGFILE_ERROR, getLogPath(view)), MT_ERROR);
4704         return;
4705     }
4707     if (!("logFileWrapping" in view) || !view.logFileWrapping)
4708         view.displayHere(getMsg(MSG_LOGFILE_OPENED, getLogPath(view)));
4709     view.logFileWrapping = false;
4712 client.closeLogFile =
4713 function cli_stoplog(view, wrapping)
4715     if ("frame" in view && !wrapping)
4716         view.displayHere(getMsg(MSG_LOGFILE_CLOSING, getLogPath(view)));
4718     view.logFileWrapping = Boolean(wrapping);
4720     if (view.logFile)
4721     {
4722         view.logFile.close();
4723         view.logFile = null;
4724     }
4727 function checkLogFiles()
4729     // For every view that has a logfile, check if we need a different file
4730     // based on the current date and the logfile preference. We close the
4731     // current logfile, and display will open the new one based on the pref
4732     // when it's needed.
4734     var d = new Date();
4735     for (var n in client.networks)
4736     {
4737         var net = client.networks[n];
4738         if (net.logFile && (d > net.nextLogFileDate))
4739             client.closeLogFile(net, true);
4740         if (("primServ" in net) && net.primServ && ("channels" in net.primServ))
4741         {
4742             for (var c in net.primServ.channels)
4743             {
4744                 var chan = net.primServ.channels[c];
4745                 if (chan.logFile && (d > chan.nextLogFileDate))
4746                     client.closeLogFile(chan, true);
4747             }
4748         }
4749         if ("users" in net)
4750         {
4751             for (var u in net.users)
4752             {
4753                 var user = net.users[u];
4754                 if (user.logFile && (d > user.nextLogFileDate))
4755                     client.closeLogFile(user, true);
4756             }
4757         }
4758     }
4760     for (var dc in client.dcc.chats)
4761     {
4762         var dccChat = client.dcc.chats[dc];
4763         if (dccChat.logFile && (d > dccChat.nextLogFileDate))
4764             client.closeLogFile(dccChat, true);
4765     }
4766     for (var df in client.dcc.files)
4767     {
4768         var dccFile = client.dcc.files[df];
4769         if (dccFile.logFile && (d > dccFile.nextLogFileDate))
4770             client.closeLogFile(dccFile, true);
4771     }
4773     // Don't forget about the client tab:
4774     if (client.logFile && (d > client.nextLogFileDate))
4775         client.closeLogFile(client, true);
4777     // We use the same line again to make sure we keep a constant offset
4778     // from the full hour, in case the timers go crazy at some point.
4779     setTimeout("checkLogFiles()", 3602000 - (Number(new Date()) % 3600000));
4782 CIRCChannel.prototype.getLCFunction =
4783 CIRCNetwork.prototype.getLCFunction =
4784 CIRCUser.prototype.getLCFunction    =
4785 CIRCDCCChat.prototype.getLCFunction =
4786 CIRCDCCFileTransfer.prototype.getLCFunction =
4787 function getlcfn()
4789     var details = getObjectDetails(this);
4790     var lcFn;
4792     if (details.server)
4793     {
4794         lcFn = function(text)
4795             {
4796                 return details.server.toLowerCase(text);
4797             }
4798     }
4800     return lcFn;
4803 CIRCChannel.prototype.performTabMatch =
4804 CIRCNetwork.prototype.performTabMatch =
4805 CIRCUser.prototype.performTabMatch    =
4806 CIRCDCCChat.prototype.performTabMatch =
4807 CIRCDCCFileTransfer.prototype.performTabMatch =
4808 function gettabmatch_other (line, wordStart, wordEnd, word, cursorpos, lcFn)
4810     if (wordStart == 0 && line[0] == client.COMMAND_CHAR)
4811     {
4812         return client.performTabMatch(line, wordStart, wordEnd, word,
4813                                       cursorpos);
4814     }
4816     var matchList = new Array();
4817     var users;
4818     var channels;
4819     var userIndex = -1;
4821     var details = getObjectDetails(this);
4823     if (details.channel && word == details.channel.unicodeName[0])
4824     {
4825         /* When we have #<tab>, we just want the current channel,
4826            if possible. */
4827         matchList.push(details.channel.unicodeName);
4828     }
4829     else
4830     {
4831         /* Ok, not #<tab> or no current channel, so get the full list. */
4833         if (details.channel)
4834             users = details.channel.users;
4836         if (details.server)
4837         {
4838             channels = details.server.channels;
4839             for (var c in channels)
4840                 matchList.push(channels[c].unicodeName);
4841             if (!users)
4842                 users = details.server.users;
4843         }
4845         if (users)
4846         {
4847             userIndex = matchList.length;
4848             for (var n in users)
4849                 matchList.push(users[n].unicodeName);
4850         }
4851     }
4853     var matches = matchEntry(word, matchList, lcFn);
4855     var list = new Array();
4856     for (var i = 0; i < matches.length; i++)
4857         list.push(matchList[matches[i]]);
4859     if (list.length == 1)
4860     {
4861         if (users && (userIndex >= 0) && (matches[0] >= userIndex))
4862         {
4863             if (wordStart == 0)
4864                 list[0] += client.prefs["nickCompleteStr"];
4865         }
4867         if (wordEnd == line.length)
4868         {
4869             /* add a space if the word is at the end of the line. */
4870             list[0] += " ";
4871         }
4872     }
4874     return list;