/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is ChatZilla. * * The Initial Developer of the Original Code is James Ross. * Portions created by the Initial Developer are Copyright (C) 2004 * the Initial Developer. All Rights Reserved. * * Contributor(s): * James Ross, * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ var client; var network; var channels = new Array(); var createChannelItem; var channelTreeShare = new Object(); var channelTreeView; var s = 0; const STATE_IDLE = ++s; // Doing nothing. const STATE_LOAD = ++s; // Loading from saved file only. const STATE_LIST_AND_LOAD = ++s; // Doing /list and loading simultaneously. const STATE_LIST_THEN_LOAD = ++s; // Doing /list, after which do load. s = 0; const SUBSTATE_START = ++s; // Starting an operation. const SUBSTATE_RUN = ++s; // Running... const SUBSTATE_END = ++s; // Clean-up/ending operation. const SUBSTATE_ERROR = ++s; // Error occurred: don't try do to any more. delete s; var state = { state: STATE_IDLE, substate: 0 }; const STATE_DELAY = 1000; const colIDToSortKey = { chanColName: "name", chanColUsers: "users", chanColTopic: "topic" }; const sortKeyToColID = { name: "chanColName", users: "chanColUsers", topic: "chanColTopic" }; function onLoad() { function ondblclick(event) { channelTreeView.onRouteDblClick(event); }; function onkeypress(event) { channelTreeView.onRouteKeyPress(event); }; function onfocus(event) { channelTreeView.onRouteFocus(event); }; function onblur(event) { channelTreeView.onRouteBlur(event); }; function doJoin() { if (joinChannel()) window.close(); }; client = window.arguments[0].client; network = window.arguments[0].network; network.joinDialog = window; window.dd = client.mainWindow.dd; window.toUnicode = client.mainWindow.toUnicode; window.getMsg = client.mainWindow.getMsg; window.MSG_CHANNEL_OPENED = client.mainWindow.MSG_CHANNEL_OPENED; window.MSG_FMT_JSEXCEPTION = client.mainWindow.MSG_FMT_JSEXCEPTION; window.MT_INFO = client.mainWindow.MT_INFO; // Import "MSG_CD_*"... for (var m in client.mainWindow) { if (m.substr(0, 7) == "MSG_CD_") window[m] = client.mainWindow[m]; } var tree = document.getElementById("channelList"); channelTreeView = new XULTreeView(channelTreeShare); channelTreeView.onRowCommand = doJoin; channelTreeView.cycleHeader = changeSort; tree.treeBoxObject.view = channelTreeView; // Sort by user count, decending. changeSort("chanColUsers"); tree.addEventListener("dblclick", ondblclick, false); tree.addEventListener("keypress", onkeypress, false); tree.addEventListener("focus", onfocus, false); tree.addEventListener("blur", onblur, false); createChannelItem = new ChannelEntry("", "", MSG_CD_CREATE); createChannelItem.first = true; channelTreeView.childData.appendChild(createChannelItem); document.title = getMsg(MSG_CD_TITLE, [network.unicodeName, network.getURL()]); setInterval(doCurrentStatus, STATE_DELAY); setState(STATE_LOAD); } function onUnload() { delete network.joinDialog; } function onKeyPress(event) { if (event.keyCode == event.DOM_VK_UP) { if (channelTreeView.selectedIndex > 0) channelTreeView.selectedIndex = channelTreeView.selectedIndex - 1; event.preventDefault(); } else if (event.keyCode == event.DOM_VK_DOWN) { if (channelTreeView.selectedIndex < channelTreeView.rowCount - 1) channelTreeView.selectedIndex = channelTreeView.selectedIndex + 1; event.preventDefault(); } } function onSelectionChange() { var joinBtn = document.getElementById("joinBtn"); joinBtn.disabled = (channelTreeView.selectedIndex == -1); } function runFilter() { function process() { var exactMatch = null; var channelText = text; if (!channelText.match(/^[#&+!]/)) channelText = "#" + channelText; // Should ideally be calling freeze()/thaw() here, but unfortunately they // mess up the selection, even if they make this so much faster. for (var i = 0; i < channels.length; i++) { var match = (channels[i].name.toLowerCase().indexOf(text) != -1) || (searchTopics && (channels[i].topic.toLowerCase().indexOf(text) != -1)); if (minUsers && (channels[i].users < minUsers)) match = false; if (maxUsers && (channels[i].users > maxUsers)) match = false; if (channels[i].isHidden) { if (match) channels[i].unHide(); } else { if (!match) channels[i].hide(); } if (match && (channels[i].name.toLowerCase() == channelText)) exactMatch = channels[i]; if ((i % 100) == 0) updateProgress(MSG_CD_FILTERING, 100 * i / channels.length); } updateProgress(); createChannelItem.name = channelText; var tree = document.getElementById("channelList"); tree.treeBoxObject.invalidateRow(0); if (exactMatch) { if (!createChannelItem.isHidden) createChannelItem.hide(); channelTreeView.selectedIndex = exactMatch.calculateVisualRow(); } else { if (createChannelItem.isHidden) createChannelItem.unHide(); if (channelTreeView.selectedIndex == -1) { // The special 'create' row is visible, so prefer the first // real channel over it. if (channelTreeView.rowCount >= 2) channelTreeView.selectedIndex = 1; else if (channelTreeView.rowCount >= 1) channelTreeView.selectedIndex = 0; } } }; var text = document.getElementById("filterText").value.toLowerCase(); var searchTopics = document.getElementById("searchTopics").checked; var minUsers = document.getElementById("minUsers").value * 1; var maxUsers = document.getElementById("maxUsers").value * 1; if (channels.length > 1000) updateProgress(getMsg(MSG_CD_FILTERING1, channels.length)); else updateProgress(getMsg(MSG_CD_FILTERING2, channels.length)); setTimeout(process, 100); } function joinChannel() { var index = channelTreeView.selectedIndex; if (index == -1) return false; var row = channelTreeView.childData.locateChildByVisualRow(index); network.dispatch("join", { channelName: row.name }); return true; } function focusSearch() { document.getElementById("filterText").focus(); } function refreshList() { setState(STATE_LIST_THEN_LOAD); } function updateProgress(label, pro) { if (label) { document.getElementById("loadLabel").value = label; } else { var msg = getMsg(MSG_CD_SHOWING, [(channelTreeView.rowCount - (createChannelItem.isHidden ? 0 : 1)), channels.length]); document.getElementById("loadLabel").value = msg; } var loadBarDeckIndex = ((typeof pro == "undefined") ? 1 : 0); document.getElementById("loadBarDeck").selectedIndex = loadBarDeckIndex; if ((typeof pro == "undefined") || (pro == -1)) { document.getElementById("loadBar").mode = "undetermined"; } else { document.getElementById("loadBar").mode = "determined"; document.getElementById("loadBar").value = pro; } } function changeSort(col) { if (typeof col == "object") col = col.id; col = colIDToSortKey[col]; // Users default to decending, others accending. var dir = (col == "users" ? -1 : 1); if (col == channelTreeShare.sortColumn) dir = -channelTreeShare.sortDirection; var colID = sortKeyToColID[channelTreeShare.sortColumn]; var colNode = document.getElementById(colID); if (colNode) { colNode.removeAttribute("sortActive"); colNode.removeAttribute("sortDirection"); } channelTreeView.childData.setSortColumn(col, dir); colID = sortKeyToColID[channelTreeShare.sortColumn]; colNode = document.getElementById(colID); if (colNode) { colNode.setAttribute("sortActive", "true"); var sortDir = (dir > 0 ? "ascending" : "descending"); colNode.setAttribute("sortDirection", sortDir); } } function setState(newState) { state.state = newState; if (newState == STATE_IDLE) state.substate = 0; else state.substate = SUBSTATE_START; // Force an update when the state changes. doCurrentStatus(); } function setSubstate(newSubstate) { state.substate = newSubstate; } function doCurrentStatus() { var st = Number(new Date()); try { switch (state.substate) { case SUBSTATE_START: doCurrentStatusStart(); break; case SUBSTATE_RUN: doCurrentStatusRun(); break; case SUBSTATE_END: doCurrentStatusEnd(); break; } } catch(ex) { /* If an error has occured, we display it (updateProgress) and then * halt our opperations to prevent further damage. */ dd("Exception in channels.js:doCurrentStatus: " + formatException(ex)); updateProgress(formatException(ex)); state.substate = SUBSTATE_ERROR; } var en = Number(new Date()); state.timeTaken = (en - st); } function doCurrentStatusStart() { // Only the list has an initial message. if (state.state == STATE_LIST_THEN_LOAD) updateProgress(MSG_CD_FETCHING, -1); // The file is used in all 3 cases. var file = getListFile(); // Doing the list should disable the refresh button. if ((state.state == STATE_LIST_AND_LOAD) || (state.state == STATE_LIST_THEN_LOAD)) { document.getElementById("refreshNow").disabled = true; network.list("", file.path); } // Do file handling setup for both loading states. if ((state.state == STATE_LOAD) || (state.state == STATE_LIST_AND_LOAD)) { if (!file.exists()) { // We tried to do a load, but the file does not exist. setState(STATE_LIST_AND_LOAD); return; } // Nuke contents. channelTreeView.selectedIndex = -1; channelTreeView.freeze(); while (channelTreeView.childData.childData.length > 1) channelTreeView.childData.removeChildAtIndex(1); channelTreeView.thaw(); // Nuke more stuff. channels = new Array(); // And... here we go. state.loadFile = new LocalFile(file, "<"); state.loadPendingData = ""; state.loadChunk = 10000; state.loadedSoFar = 0; // The loading from the file will never complete whilst this is true. state.loadNeverComplete = (state.state == STATE_LIST_AND_LOAD); } setSubstate(SUBSTATE_RUN); } function doCurrentStatusRun() { if (state.state == STATE_LIST_THEN_LOAD) { // Update the progress and end if /list done for "list only" state. updateProgress(getMsg(MSG_CD_FETCHED, network._list.count), -1); if (network._list.done) setSubstate(SUBSTATE_END); } /* If we're doing the list+load state, flag is as ok to stop loading the * file when the network has finished doing the /list. */ if ((state.state == STATE_LIST_AND_LOAD) && network._list.done) state.loadNeverComplete = false; // Do file handling for loading states. if ((state.state == STATE_LOAD) || (state.state == STATE_LIST_AND_LOAD)) { // Adjust chunk size to keep time per-chunk between 750ms and 1000ms. if ((state.timeTaken > 1.25 * STATE_DELAY) && (state.loadChunk >= 2000)) state.loadChunk -= 1000; else if ((state.timeTaken > STATE_DELAY) && (state.loadChunk >= 1100)) state.loadChunk -= 100; if (state.timeTaken < 0.5 * STATE_DELAY) state.loadChunk += 1000; else if (state.timeTaken < 0.75 * STATE_DELAY) state.loadChunk += 100; state.loadedSoFar += state.loadChunk; state.loadPendingData += state.loadFile.read(state.loadChunk); while (state.loadPendingData.indexOf("\n") != -1) { var lines = state.loadPendingData.split("\n"); state.loadPendingData = lines.pop(); for (var i = 0; i < lines.length; i++) { var ary = lines[i].match(/^([^ ]+) ([^ ]+) (.*)$/); if (ary) { var chan = new ChannelEntry(toUnicode(ary[1], "UTF-8"), ary[2], toUnicode(ary[3], "UTF-8")); channels.push(chan); } } } var dataLeft = state.loadFile.inputStream.available(); var pro = state.loadedSoFar / (state.loadedSoFar + dataLeft); pro = Math.round(100 * pro); updateProgress(getMsg(MSG_CD_LOADED, channels.length), pro); // Done if there is no more data, and we're not *expecting* any more. if ((dataLeft == 0) && !state.loadNeverComplete) setSubstate(SUBSTATE_END); } } function doCurrentStatusEnd() { // Reset refresh button. if ((state.state == STATE_LIST_AND_LOAD) || (state.state == STATE_LIST_THEN_LOAD)) document.getElementById("refreshNow").disabled = false; // Check that /list finished ok if we're just doing a list. if (state.state == STATE_LIST_THEN_LOAD) { if ("error" in network._list) { updateProgress(MSG_CD_ERROR_LIST); // Can't seem to "undefine" the parameters using the function format. setTimeout("updateProgress()", 10000); // Bail out if there was an error! return; } else { // Replace files. updateProgress(); } } // Finish file handling work. if ((state.state == STATE_LOAD) || (state.state == STATE_LIST_AND_LOAD)) { if (channels.length > 0) channelTreeView.childData.appendChildren(channels); state.loadFile.close(); delete state.loadFile; delete state.loadPendingData; delete state.loadChunk; delete state.loadedSoFar; delete state.loadNeverComplete; updateProgress(); runFilter(); } if (state.state == STATE_LIST_THEN_LOAD) setState(STATE_LOAD); else setState(STATE_IDLE); } function getListFile(temp) { var file = new LocalFile(network.prefs["logFileName"]); if (temp) file.localFile.leafName = "list.temp"; else file.localFile.leafName = "list.txt"; return file.localFile; } // Tree ChannelEntry objects // function ChannelEntry(name, users, topic) { this.setColumnPropertyName("chanColName", "name"); this.setColumnPropertyName("chanColUsers", "users"); this.setColumnPropertyName("chanColTopic", "topic"); // Nuke color codes and bold etc. topic = topic.replace(/[\x1F\x02\x0F\x16]/g, ""); topic = topic.replace(/\x03\d{1,2}(?:,\d{1,2})?/g, ""); this.name = name; this.users = users; this.topic = topic; } ChannelEntry.prototype = new XULTreeViewRecord(channelTreeShare); ChannelEntry.prototype.sortCompare = function chanentry_sortcmp(a, b) { var sc = a._share.sortColumn; var sd = a._share.sortDirection; // Make sure the special 'first' row is always first. if ("first" in a) return -1; if ("first" in b) return 1; if (sc == "users") { // Force a numeric comparison. a = 1 * a[sc]; b = 1 * b[sc]; } else { // Case-insensitive, please. a = a[sc].toLowerCase(); b = b[sc].toLowerCase(); } if (a < b) return -1 * sd; if (a > b) return 1 * sd; return 0; }