862 lines
25 KiB
JavaScript
862 lines
25 KiB
JavaScript
/* ***** 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, <silver@warwickcompsoc.co.uk>
|
|
*
|
|
* 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 serverChannelPrefixes;
|
|
|
|
var channelTreeShare = new Object();
|
|
var channelTreeView, channelTreeBoxObject, channelLoadLabel, channelLoadBar,
|
|
channelLoadBarDesk, channelFilterText, channelSearchTopics, channelMinUsers,
|
|
channelMaxUsers, channelJoinBtn, channelRefreshBtn;
|
|
|
|
|
|
// Create list of operations. These are handled by common code.
|
|
const OPS = new Array();
|
|
OPS.push({ key: "noop", ignore: true });
|
|
OPS.push({ key: "list", canStop: false });
|
|
OPS.push({ key: "load", canStop: true });
|
|
OPS.push({ key: "filter", canStop: true });
|
|
|
|
|
|
// Define constants for each operation.
|
|
// JavaScript won't let you delete things declared with "var", workaround:
|
|
// NOTE: This order MUST be the same as those above!
|
|
window.s = 0;
|
|
const OP_LIST = ++s; // A /list operation on the server.
|
|
const OP_LOAD = ++s; // Loading the saved file.
|
|
const OP_FILTER = ++s; // Filtering the loaded list.
|
|
|
|
|
|
// Define constants for the valid states of each operation.
|
|
// All states before STATE_START must be idle (stopped) states.
|
|
// All states from STATE_START onwards must be busy (running) states.
|
|
s = 0;
|
|
const STATE_IDLE = ++s; // Not doing this operation.
|
|
const STATE_ERROR = ++s; // Error occurred: don't try do to any more.
|
|
const STATE_START = ++s; // Starting an operation.
|
|
const STATE_RUN = ++s; // Running...
|
|
const STATE_STOP = ++s; // Clean-up/ending operation.
|
|
delete window.s;
|
|
|
|
|
|
// Store all the operation data here.
|
|
var data = {
|
|
list: { state: STATE_IDLE },
|
|
load: { state: STATE_IDLE },
|
|
filter: { state: STATE_IDLE }
|
|
};
|
|
|
|
|
|
// This should keep things responsive enough, for the user to click buttons and
|
|
// edit the filter text and options, without giving up too much time to letting
|
|
// Gecko catch up.
|
|
const PROCESS_TIME_MAX = 200;
|
|
const PROCESS_DELAY = 50;
|
|
|
|
const colIDToSortKey = { chanColName: "name",
|
|
chanColUsers: "users",
|
|
chanColTopic: "topic" };
|
|
const sortKeyToColID = { name: "chanColName",
|
|
users: "chanColUsers",
|
|
topic: "chanColTopic" };
|
|
|
|
function onLoad()
|
|
{
|
|
function ondblclick(event) { 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;
|
|
|
|
client.ceip.logEvent({type: "dialog", dialog: "channels", event: "open"});
|
|
|
|
serverChannelPrefixes = network.primServ.channelTypes;
|
|
|
|
window.dd = client.mainWindow.dd;
|
|
window.ASSERT = client.mainWindow.ASSERT;
|
|
window.toUnicode = client.mainWindow.toUnicode;
|
|
window.getMsg = client.mainWindow.getMsg;
|
|
window.MSG_CHANNEL_OPENED = client.mainWindow.MSG_CHANNEL_OPENED;
|
|
window.MSG_FMT_JSEXCEPTION = client.mainWindow.MSG_FMT_JSEXCEPTION;
|
|
window.MT_INFO = client.mainWindow.MT_INFO;
|
|
|
|
// Import "MSG_CD_*"...
|
|
for (var m in client.mainWindow)
|
|
{
|
|
if (m.substr(0, 7) == "MSG_CD_")
|
|
window[m] = client.mainWindow[m];
|
|
}
|
|
|
|
// Set the <dialog>'s class so we can do platform-specific CSS.
|
|
var dialog = document.getElementById("chatzilla-window");
|
|
dialog.className = "platform-" + client.platform;
|
|
|
|
var tree = document.getElementById("channelList");
|
|
channelTreeBoxObject = tree.treeBoxObject;
|
|
|
|
channelTreeView = new XULTreeView(channelTreeShare);
|
|
channelTreeView.onRowCommand = doJoin;
|
|
channelTreeView.cycleHeader = changeSort;
|
|
channelTreeBoxObject.view = channelTreeView;
|
|
|
|
channelLoadLabel = document.getElementById("loadLabel");
|
|
channelLoadBar = document.getElementById("loadBar");
|
|
channelLoadBarDesk = document.getElementById("loadBarDeck");
|
|
channelFilterText = document.getElementById("filterText");
|
|
channelSearchTopics = document.getElementById("searchTopics");
|
|
channelMinUsers = document.getElementById("minUsers");
|
|
channelMaxUsers = document.getElementById("maxUsers");
|
|
channelJoinBtn = document.getElementById("joinBtn");
|
|
channelRefreshBtn = document.getElementById("refreshNow");
|
|
|
|
// If the new "search" binding is not working (i.e. doesn't exist)...
|
|
if (!("searchButton" in channelFilterText))
|
|
{
|
|
// ...restore the text boxes to their former selves.
|
|
channelFilterText.setAttribute("timeout", "500");
|
|
channelFilterText.setAttribute("type", "timed");
|
|
channelMinUsers.setAttribute("timeout", "500");
|
|
channelMinUsers.setAttribute("type", "timed");
|
|
channelMaxUsers.setAttribute("timeout", "500");
|
|
channelMaxUsers.setAttribute("type", "timed");
|
|
}
|
|
|
|
// 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()]);
|
|
|
|
var opener = window.arguments[0].opener;
|
|
if (opener)
|
|
{
|
|
// Force the window to be the right size now, not later.
|
|
window.sizeToContent();
|
|
|
|
// Position it centered over, but never up or left of parent.
|
|
var sx = Math.max((opener.outerWidth - window.outerWidth ) / 2, 0);
|
|
var sy = Math.max((opener.outerHeight - window.outerHeight) / 2, 0);
|
|
window.moveTo(opener.screenX + sx, opener.screenY + sy);
|
|
}
|
|
|
|
setTimeout(updateOperations, PROCESS_DELAY);
|
|
startOperation(OP_LOAD);
|
|
}
|
|
|
|
function onUnload()
|
|
{
|
|
client.ceip.logEvent({type: "dialog", dialog: "channels", event: "close"});
|
|
delete network.joinDialog;
|
|
}
|
|
|
|
function onKeyPress(event)
|
|
{
|
|
if (event.keyCode == event.DOM_VK_RETURN)
|
|
{
|
|
startOperation(OP_FILTER);
|
|
if (joinChannel())
|
|
window.close();
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
}
|
|
else if (event.keyCode == event.DOM_VK_UP)
|
|
{
|
|
if (channelTreeView.selectedIndex > 0)
|
|
{
|
|
channelTreeView.selectedIndex = channelTreeView.selectedIndex - 1;
|
|
ensureRowIsVisible();
|
|
}
|
|
event.preventDefault();
|
|
}
|
|
else if (event.keyCode == event.DOM_VK_DOWN)
|
|
{
|
|
if (channelTreeView.selectedIndex < channelTreeView.rowCount - 1)
|
|
{
|
|
channelTreeView.selectedIndex = channelTreeView.selectedIndex + 1;
|
|
ensureRowIsVisible();
|
|
}
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
function onSelectionChange()
|
|
{
|
|
var index = channelTreeView.selectedIndex;
|
|
if (index == -1)
|
|
{
|
|
channelJoinBtn.disabled = true;
|
|
return;
|
|
}
|
|
var row = channelTreeView.childData.locateChildByVisualRow(index);
|
|
channelJoinBtn.disabled = ((index == 0) && !row.name);
|
|
}
|
|
|
|
function onFilter()
|
|
{
|
|
startOperation(OP_FILTER);
|
|
}
|
|
|
|
function joinChannel()
|
|
{
|
|
var index = channelTreeView.selectedIndex;
|
|
if (index == -1)
|
|
return false;
|
|
var row = channelTreeView.childData.locateChildByVisualRow(index);
|
|
if ((index == 0) && !row.name)
|
|
return false;
|
|
|
|
/* Calculate the row index AS IF the 'create' row is visible. We're going
|
|
* to use this so that the index chosen by the user is always consistent,
|
|
* whatever the visibility of the 'create' row - an index of 0 is ALWAYS
|
|
* the 'create' row, and >= 1 is ALWAYS the searched rows.
|
|
*/
|
|
var realIndex = index + (createChannelItem.isHidden ? 1 : 0);
|
|
client.ceip.logEvent({type: "dialog", dialog: "channels", event: "join",
|
|
index: realIndex});
|
|
|
|
network.dispatch("join", { channelName: row.name });
|
|
|
|
return true;
|
|
}
|
|
|
|
function focusSearch()
|
|
{
|
|
channelFilterText.focus();
|
|
}
|
|
|
|
function refreshList()
|
|
{
|
|
startOperation(OP_LIST);
|
|
}
|
|
|
|
function updateProgress(label, pro)
|
|
{
|
|
if (label)
|
|
{
|
|
channelLoadLabel.value = label;
|
|
}
|
|
else
|
|
{
|
|
var msg = getMsg(MSG_CD_SHOWING,
|
|
[(channelTreeView.rowCount - (createChannelItem.isHidden ? 0 : 1)),
|
|
channels.length]);
|
|
channelLoadLabel.value = msg;
|
|
}
|
|
|
|
var loadBarDeckIndex = ((typeof pro == "undefined") ? 1 : 0);
|
|
channelLoadBarDesk.selectedIndex = loadBarDeckIndex;
|
|
|
|
if ((typeof pro == "undefined") || (pro == -1))
|
|
{
|
|
channelLoadBar.mode = "undetermined";
|
|
}
|
|
else
|
|
{
|
|
channelLoadBar.mode = "determined";
|
|
channelLoadBar.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);
|
|
}
|
|
}
|
|
|
|
|
|
// ***** BEGIN OPERATIONS CODE *****
|
|
|
|
|
|
/* Return the static data about an operation (e.g. whether it can be
|
|
* stopped, etc.). The data returned is always the same for a given op code.
|
|
*/
|
|
function getOperation(op)
|
|
{
|
|
ASSERT(op in OPS, "Invalid op-code: " + op);
|
|
return OPS[op];
|
|
}
|
|
|
|
/* Returns the live data about an operation (e.g. current state). Accepts
|
|
* either the op ID or the static data (as returned from getOperation(op)).
|
|
*/
|
|
function getOperationData(op)
|
|
{
|
|
if (typeof op == "object")
|
|
return data[op.key];
|
|
return data[getOperation(op).key];
|
|
}
|
|
|
|
// Returns the current state of an operation; accepts same as getOperationData.
|
|
function getOperationState(op)
|
|
{
|
|
return getOperationData(op).state;
|
|
}
|
|
|
|
function startOperation(op)
|
|
{
|
|
var ops = getOperation(op);
|
|
if (ops.ignore)
|
|
return;
|
|
|
|
var dbg = "startOperation(" + ops.key + ")";
|
|
var opData = getOperationData(ops);
|
|
|
|
// STATE_ERROR operations must not do anything. Assert and bail.
|
|
if (!ASSERT(opData.state != STATE_ERROR, dbg + " in STATE_ERROR"))
|
|
return;
|
|
|
|
// Check we can stop a non-idle operation.
|
|
if (!ASSERT((opData.state == STATE_IDLE) || ops.canStop,
|
|
dbg + " not in STATE_IDLE and can't stop"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Stop the current operation.
|
|
if (opData.state != STATE_IDLE)
|
|
stopOperation(op);
|
|
|
|
// Begin!
|
|
var opData = getOperationData(op);
|
|
opData.state = STATE_START;
|
|
processOperation(op);
|
|
ASSERT(opData.state == STATE_RUN, dbg + " didn't enter STATE_RUN");
|
|
}
|
|
|
|
function updateOperations()
|
|
{
|
|
for (var i = 1; i < OPS.length; i++)
|
|
{
|
|
var state = getOperationState(i);
|
|
if ((state == STATE_RUN) || (state == STATE_STOP))
|
|
processOperation(i);
|
|
}
|
|
|
|
setTimeout(updateOperations, PROCESS_DELAY);
|
|
}
|
|
|
|
function processOperation(op)
|
|
{
|
|
var ops = getOperation(op);
|
|
if (ops.ignore)
|
|
return;
|
|
|
|
var dbg = "processOperation(" + ops.key + ")";
|
|
var opData = getOperationData(ops);
|
|
|
|
var fn = "processOp";
|
|
fn += ops.key[0].toUpperCase() + ops.key.substr(1);
|
|
if (opData.state == STATE_START)
|
|
fn += "Start";
|
|
else if (opData.state == STATE_RUN)
|
|
fn += "Run";
|
|
else if (opData.state == STATE_STOP)
|
|
fn += "Stop";
|
|
// assert and return if we're in a different state:
|
|
else if (!ASSERT(false, dbg + " invalid state: " + opData.state))
|
|
return;
|
|
|
|
try
|
|
{
|
|
var newState = window[fn](opData);
|
|
if (typeof newState != "undefined")
|
|
opData.state = newState;
|
|
}
|
|
catch(ex)
|
|
{
|
|
/* If an error has occured, we display it (updateProgress) and then
|
|
* halt our opperations to prevent further damage.
|
|
*/
|
|
dd("Exception in channels.js: " + dbg + ": " + fn + ": " + formatException(ex));
|
|
updateProgress(formatException(ex));
|
|
opData.state = STATE_ERROR;
|
|
}
|
|
}
|
|
|
|
function stopOperation(op)
|
|
{
|
|
var ops = getOperation(op);
|
|
if (ops.ignore)
|
|
return;
|
|
|
|
var dbg = "stopOperation(" + ops.key + ")";
|
|
var opData = getOperationData(ops);
|
|
|
|
// STATE_ERROR operations must not do anything. Assert and bail.
|
|
if (!ASSERT(opData.state != STATE_ERROR, dbg + " in STATE_ERROR"))
|
|
return;
|
|
|
|
// Nothing to do for STATE_IDLE. We shouldn't really be here, so assert.
|
|
if (!ASSERT(opData.state != STATE_IDLE, dbg + " in STATE_IDLE"))
|
|
return;
|
|
|
|
// Force the end and process synchronously.
|
|
opData.state = STATE_STOP;
|
|
processOperation(op);
|
|
ASSERT(opData.state == STATE_IDLE, dbg + " didn't enter STATE_IDLE");
|
|
}
|
|
|
|
// ***** END OPERATIONS CODE *****
|
|
|
|
|
|
// ***** BEGIN OPERATION HANDLERS *****
|
|
|
|
function processOpListStart(opData)
|
|
{
|
|
// Doing the list should disable the refresh button.
|
|
channelRefreshBtn.disabled = true;
|
|
|
|
// Show a general message until we get some data.
|
|
updateProgress(MSG_CD_FETCHING, -1);
|
|
|
|
// Get the file we're going to save to, and start the /list.
|
|
var file = getListFile();
|
|
network.list("", file.path);
|
|
|
|
return STATE_RUN;
|
|
}
|
|
|
|
function processOpListRun(opData)
|
|
{
|
|
// Update the progress and end if /list done for "list only" state.
|
|
updateProgress(getMsg(MSG_CD_FETCHED, network._list.count), -1);
|
|
|
|
// Stop if the network's /list has finished.
|
|
return (network._list.done ? STATE_STOP : STATE_RUN);
|
|
}
|
|
|
|
function processOpListStop(opData)
|
|
{
|
|
// Reset refresh button.
|
|
channelRefreshBtn.disabled = false;
|
|
|
|
// Check that /list finished ok if we're just doing a list.
|
|
if ("error" in network._list)
|
|
{
|
|
updateProgress(MSG_CD_ERROR_LIST);
|
|
}
|
|
else
|
|
{
|
|
updateProgress();
|
|
if (getOperationState(OP_LOAD) == STATE_IDLE)
|
|
startOperation(OP_LOAD);
|
|
}
|
|
|
|
return STATE_IDLE;
|
|
}
|
|
|
|
function processOpLoadStart(opData)
|
|
{
|
|
var file = getListFile();
|
|
if (!file.exists())
|
|
{
|
|
// We tried to do a load, but the file does not exist. Start a list to
|
|
// fill up the file.
|
|
startOperation(OP_LIST);
|
|
|
|
// File still doesn't exist, just give up.
|
|
if (!file.exists())
|
|
return STATE_IDLE;
|
|
}
|
|
|
|
// Nuke 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.
|
|
opData.loadFile = new LocalFile(file, "<");
|
|
opData.loadPendingData = "";
|
|
opData.loadChunk = 10000;
|
|
opData.loadedSoFar = 0;
|
|
|
|
return STATE_RUN;
|
|
}
|
|
|
|
function processOpLoadRun(opData)
|
|
{
|
|
// All states before STATE_START are "not running" states.
|
|
var opListRunning = (getOperationState(OP_LIST) >= STATE_START);
|
|
|
|
var end = Number(new Date()) + PROCESS_TIME_MAX;
|
|
while (Number(new Date()) < end)
|
|
{
|
|
var nlIndex = opData.loadPendingData.indexOf("\n");
|
|
if (nlIndex == -1)
|
|
{
|
|
opData.loadedSoFar += opData.loadChunk;
|
|
var newChunk = opData.loadFile.read(opData.loadChunk);
|
|
if (newChunk)
|
|
opData.loadPendingData += newChunk;
|
|
nlIndex = opData.loadPendingData.indexOf("\n");
|
|
if (nlIndex == -1)
|
|
break;
|
|
}
|
|
|
|
var line = opData.loadPendingData.substr(0, nlIndex);
|
|
opData.loadPendingData = opData.loadPendingData.substr(nlIndex + 1);
|
|
|
|
line = toUnicode(line, "UTF-8");
|
|
var ary = line.match(/^([^ ]+) ([^ ]+) (.*)$/);
|
|
if (ary)
|
|
{
|
|
var chan = new ChannelEntry(ary[1], ary[2], ary[3]);
|
|
channels.push(chan);
|
|
}
|
|
}
|
|
|
|
var dataLeft = opData.loadFile.inputStream.available();
|
|
|
|
// We shouldn't update the display when listing as well, as we're not
|
|
// going to show anything useful (always 100% or near to it, and
|
|
// replaces the 'fetching' message).
|
|
if (!opListRunning)
|
|
{
|
|
var pro = opData.loadedSoFar / (opData.loadedSoFar + dataLeft);
|
|
pro = Math.round(100 * pro);
|
|
updateProgress(getMsg(MSG_CD_LOADED, channels.length), pro);
|
|
}
|
|
|
|
// Done if there is no more data, and we're not *expecting* any more.
|
|
if ((dataLeft == 0) && !opListRunning)
|
|
return STATE_STOP;
|
|
|
|
return STATE_RUN;
|
|
}
|
|
|
|
function processOpLoadStop(opData)
|
|
{
|
|
if (channels.length > 0)
|
|
channelTreeView.childData.appendChildren(channels);
|
|
opData.loadFile.close();
|
|
delete opData.loadFile;
|
|
delete opData.loadPendingData;
|
|
delete opData.loadChunk;
|
|
delete opData.loadedSoFar;
|
|
delete opData.loadNeverComplete;
|
|
updateProgress();
|
|
|
|
startOperation(OP_FILTER);
|
|
|
|
return STATE_IDLE;
|
|
}
|
|
|
|
function processOpFilterStart(opData)
|
|
{
|
|
// Catch filtering with the same options on the same channels:
|
|
var newOptions = {text: channelFilterText.value.toLowerCase(),
|
|
min: channelMinUsers.value * 1,
|
|
max: channelMaxUsers.value * 1,
|
|
listLen: channels.length,
|
|
searchTopics: channelSearchTopics.checked};
|
|
|
|
if (("filterOptions" in window) &&
|
|
equalsObject(window.filterOptions, newOptions))
|
|
{
|
|
return STATE_IDLE;
|
|
}
|
|
|
|
window.filterOptions = newOptions;
|
|
|
|
opData.text = newOptions.text;
|
|
opData.searchTopics = newOptions.searchTopics;
|
|
opData.minUsers = newOptions.min;
|
|
opData.maxUsers = newOptions.max;
|
|
opData.exactMatch = null;
|
|
opData.currentIndex = 0;
|
|
opData.channelText = opData.text;
|
|
|
|
// Log the filter, indicating which features the user is using.
|
|
var filters = new Array();
|
|
if (opData.channelText)
|
|
filters.push("name");
|
|
if (opData.searchTopics)
|
|
filters.push("topics");
|
|
if (opData.minUsers)
|
|
filters.push("min-users");
|
|
if (opData.maxUsers)
|
|
filters.push("max-users");
|
|
|
|
if (opData.channelText &&
|
|
(arrayIndexOf(["#", "&", "+", "!"], opData.channelText[0]) == -1) &&
|
|
(arrayIndexOf(serverChannelPrefixes, opData.channelText[0]) == -1))
|
|
{
|
|
opData.channelText = serverChannelPrefixes[0] + opData.channelText;
|
|
}
|
|
else
|
|
{
|
|
// Log that user has specified an explicit prefix.
|
|
filters.push("prefix");
|
|
}
|
|
|
|
client.ceip.logEvent({type: "dialog", dialog: "channels", event: "filter",
|
|
filters: filters.join(",")});
|
|
|
|
// Update special "create channel" row, and select it.
|
|
createChannelItem.name = opData.channelText;
|
|
if (createChannelItem.isHidden)
|
|
createChannelItem.unHide();
|
|
|
|
// Scroll to the top and select the "create channel" row.
|
|
channelTreeView.selectedIndex = 0;
|
|
channelTreeBoxObject.invalidateRow(0);
|
|
channelTreeBoxObject.scrollToRow(0);
|
|
ensureRowIsVisible();
|
|
|
|
updateProgress(getMsg(MSG_CD_FILTERING, [0, channels.length]), 0);
|
|
|
|
return STATE_RUN;
|
|
}
|
|
|
|
function processOpFilterRun(opData)
|
|
{
|
|
var end = Number(new Date()) + PROCESS_TIME_MAX;
|
|
var more = false;
|
|
|
|
// Save selection because freeze/thaw screws it up.
|
|
// Note that we only save the item if it isn't the "create channel" row.
|
|
var index = channelTreeView.selectedIndex;
|
|
var item = null;
|
|
if (index > 0)
|
|
item = channelTreeView.childData.locateChildByVisualRow(index);
|
|
|
|
channelTreeView.freeze();
|
|
for (var i = opData.currentIndex; i < channels.length; i++)
|
|
{
|
|
var c = channels[i];
|
|
|
|
var match = (c.nameLC.indexOf(opData.text) != -1) ||
|
|
(opData.searchTopics &&
|
|
(c.topicLC.indexOf(opData.text) != -1));
|
|
|
|
if (opData.minUsers && (c.users < opData.minUsers))
|
|
match = false;
|
|
if (opData.maxUsers && (c.users > opData.maxUsers))
|
|
match = false;
|
|
|
|
if (c.isHidden && match)
|
|
c.unHide();
|
|
if (!c.isHidden && !match)
|
|
c.hide();
|
|
|
|
if (match && (c.nameLC == opData.channelText))
|
|
opData.exactMatch = c;
|
|
|
|
opData.currentIndex = i;
|
|
if ((new Date()) > end)
|
|
{
|
|
more = true;
|
|
break;
|
|
}
|
|
}
|
|
channelTreeView.thaw();
|
|
|
|
// No item selected by user, so use our exact match instead.
|
|
if (!item && opData.exactMatch)
|
|
item = opData.exactMatch;
|
|
|
|
// Restore selected item.
|
|
if (item)
|
|
channelTreeView.selectedIndex = item.calculateVisualRow();
|
|
else
|
|
channelTreeView.selectedIndex = 0;
|
|
|
|
ensureRowIsVisible();
|
|
|
|
updateProgress(getMsg(MSG_CD_FILTERING,
|
|
[opData.currentIndex, channels.length]),
|
|
100 * opData.currentIndex / channels.length);
|
|
|
|
return (more ? STATE_RUN : STATE_STOP);
|
|
}
|
|
|
|
function processOpFilterStop(opData)
|
|
{
|
|
if (opData.exactMatch)
|
|
{
|
|
if (!createChannelItem.isHidden)
|
|
createChannelItem.hide();
|
|
}
|
|
// If nothing is selected, select the "create channel" row.
|
|
else if (channelTreeView.selectedIndex < 0)
|
|
{
|
|
channelTreeView.selectedIndex = 0;
|
|
}
|
|
|
|
ensureRowIsVisible();
|
|
|
|
delete opData.text;
|
|
delete opData.searchTopics;
|
|
delete opData.minUsers;
|
|
delete opData.maxUsers;
|
|
delete opData.exactMatch;
|
|
delete opData.currentIndex;
|
|
delete opData.channelText;
|
|
updateProgress();
|
|
|
|
return STATE_IDLE;
|
|
}
|
|
|
|
|
|
// ***** END OPERATION HANDLERS *****
|
|
|
|
|
|
function ensureRowIsVisible()
|
|
{
|
|
if (channelTreeView.selectedIndex >= 0)
|
|
channelTreeBoxObject.ensureRowIsVisible(channelTreeView.selectedIndex);
|
|
else
|
|
channelTreeBoxObject.ensureRowIsVisible(0);
|
|
}
|
|
|
|
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;
|
|
|
|
this.nameLC = this.name.toLowerCase();
|
|
this.topicLC = this.topic.toLowerCase();
|
|
}
|
|
|
|
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;
|
|
}
|