Skip to content
Snippets Groups Projects
DDMsgAreaChooser.js 121 KiB
Newer Older
/* This is a script that lets the user choose a message area,
 * with either a lightbar or traditional user interface.
 *
 * Date       User          Version Description
 * 2010-02-05 Eric Oulashin 0.90    Started
 * 2010-02-18 to
 * 2010-02-27 Eric Oulashin         Continued work.
 *                                  Added the first & lasg page functionality
 *                                  to the lightbar interface.
 * 2010-03-13 Eric Oulashin 1.00    Added the ability to load settings from a
 *                                  configuration file.
 * 2011-04-22 Eric Oulashin 1.01    Fixed the wording when choosing a message
 *                                  group - It now says "group #" instead
 *                                  of "sub-board #".
 * 2012-10-06 Eric Oulashin 1.02    For lightbar mode, updated to display the
 *                                  page number in the header at the top (in
 *                                  addition to the total number of pages,
 *                                  which it was already displaying).
 * 2012-11-30 Eric Oulashin 1.03    Bug fix: After leaving the help screen
 *                                  from the sub-board list, the top line is
 *                                  now correctly written with the page
 *                                  information as "Page # of #".
 * 2013-05-04 Eric Oulashin 1.04    Updated to dynamically adjust the length
 *                                  of the # messages column based on the
 *                                  greatest number of messages of all
 *                                  sub-boards within a message group so
 *                                  that the formatting still looks good.
 * 2013-05-10 Eric Oulashin 1.05    Updated the version to match the
 *                                  version in DDFileAreaChooser (a bug
 *                                  was fixed there, but DDMsgAreaChooser
 *                                  didn't have the corresponding bug).
 * 2014-09-14 Eric Oulashin 1.06    Bug fix: Updated the highlight (lightbar)
 *                                  format string in the
 *                                  DDMsgAreaChooser_buildSubBoardPrintfInfoForGrp()
 *                                  function to include a normal attribute at
 *                                  the end to avoid color issues when clearing
 *                                  the screen, etc.  Bug reported by Psi-Jack.
 * 2014-12-22 Eric Oulashin 1.07    Bug fix: Made this.colors.subBoardHeader apply
 *                                  to the whole line rather than just the page
 *                                  number.
 *                                  Bug fix: The initial display of the page number
 *                                  is now correct (previously, it would start out
 *                                  saying page 1, even if on another page).
 * 2015-04-19 Eric Oulashin 1.08    Added color settings for the lightbar help text
 *                                  at the bottom of the screen.  Also, added the
 *									                ability to use the PageUp & PageDown keys instead
 *                                  of P and N in the lightbar lists.
 * 2016-01-17 Eric Oulashin 1.09    Updated to allow choosing only the sub-board within
 *                                  the user's current message group.  The first command-
 *                                  line argument now specifies whether or not to
 *                                  allow choosing the message group, and it defaults
 *                                  to true.
 * 2016-02-12 Eric Oulashin 1.10Beta Started working on adding the ability to display
 *                                  a header ANSI/ASCII file above the list.
 * 2016-02-15 Eric Oulashin 1.10    Releasing this version
 * 2016-02-19 Eric Oulashin 1.11    Bug fix: The page number wasn't being updated
 *                                  when changing pages in the message groups
 *                                  when using the arrow keys to scroll between
 *                                  pages
 * 2016-11-20 Eric Oulashin 1.12    Started working on updating to handle null
 *                                  message headers, which could happen with the
 *                                  new voting feature in Synchronet 3.17.
 * 2016-11-22 Eric Oulashin 1.12    Releasing this version
 * 2016-12-11 Eric Oulashin 1.13    Updated to show the number of readable messages rather than
 *                                  the actual total number of messages in the sub-boards (in
 *                                  case some messages are deleted, unverified, etc.)
 * 2017-12-18 Eric Oulashin 1.15    Updated the definitions of the KEY_PAGE_UP and KEY_PAGE_DOWN
 *                                  variables to match what they are in sbbsdefs.js (if defined)
 *                                  from December 18, 2017 so that the PageUp and PageDown keys
 *                                  continue to work properly.  This script should still also work
 *                                  with older builds of Synchronet.
 * 2018-03-09 Eric Oulashin 1.16B   Bug fix for off-by-one when a message group has no sub-boards.
 * 2018-06-25 Eric Oulashin 1.17    Added a new configuration file option, showDatesInSubBoardList,
 *                                  that specifies whether or not to show the date & time of
 *                                  the latest message in the sub-boards.
 * 2019-08-08 Eric Oulashin 1.18 Beta Started working on message area searching.
 *                                  Also, improved the time to display sub-boards
 *                                  with the latest message date & time.
 * 2019-08-22 Eric Oulashin 1.18    Releasing this version
 * 2019-08-24 Eric Oulashin 1.19    Fixed a bug with 'Next' search when
 *                                  returning from sub-board choosing
   1 (argv[0]): Boolean - Whether or not to choose a message group first (default).  If
                false, the user will only be able to choose a different sub-board within
				their current message group.
   2 (argv[1]): Boolean - Whether or not to run the area chooser (if false,
                then this file will just provide the DDMsgAreaChooser class).
*/

var requireFnExists = (typeof(require) === "function");
if (requireFnExists)
	require("sbbsdefs.js", "K_NOCRLF");
else
	load("sbbsdefs.js");

// This script requires Synchronet version 3.14 or higher.
// Exit if the Synchronet version is below the minimum.
if (system.version_num < 31400)
{
	var message = "\1n\1h\1y\1i* Warning:\1n\1h\1w Digital Distortion Message Lister "
	             + "requires version \1g3.14\1w or\r\n"
	             + "higher of Synchronet.  This BBS is using version \1g" + system.version
	             + "\1w.  Please notify the sysop.";
	console.crlf();
	console.print(message);
	console.crlf();
	console.pause();
	exit();
}

// Version & date variables
var DD_MSG_AREA_CHOOSER_VERSION = "1.19";
var DD_MSG_AREA_CHOOSER_VER_DATE = "2019-08-24";
var CTRL_M = "\x0d";
var KEY_ENTER = CTRL_M;
var BACKSPACE = CTRL_H;
var CTRL_F = "\x06";
// PageUp & PageDown keys - Synchronet 3.17 as of about December 18, 2017
// use CTRL-P and CTRL-N for PageUp and PageDown, respectively.  sbbsdefs.js
// defines them as KEY_PAGEUP and KEY_PAGEDN; I've used slightly different names
// in this script so that this script will work with Synchronet systems before
// and after the update containing those key definitions.
var KEY_PAGE_UP = "\x10"; // Ctrl-P
var KEY_PAGE_DOWN = "\x0e"; // Ctrl-N
// Ensure KEY_PAGE_UP and KEY_PAGE_DOWN are set to what's defined in sbbs.js
// for KEY_PAGEUP and KEY_PAGEDN in case they change
if (typeof(KEY_PAGEUP) === "string")
	KEY_PAGE_UP = KEY_PAGEUP;
if (typeof(KEY_PAGEDN) === "string")
	KEY_PAGE_DOWN = KEY_PAGEDN;

// Key codes for display
var UP_ARROW = ascii(24);
var DOWN_ARROW = ascii(25);

// Misc. defines
var ERROR_WAIT_MS = 1500;
var SEARCH_TIMEOUT_MS = 10000;

// Determine the script's startup directory.
// This code is a trick that was created by Deuce, suggested by Rob Swindell
// as a way to detect which directory the script was executed in.  I've
// shortened the code a little.
var gStartupPath = '.';
try { throw dig.dist(dist); } catch(e) { gStartupPath = e.fileName; }
gStartupPath = backslash(gStartupPath.replace(/[\/\\][^\/\\]*$/,''));

// gIsSysop stores whether or not the user is a sysop.
var gIsSysop = user.compare_ars("SYSOP"); // Whether or not the user is a sysop

// 1st command-line argument: Whether or not to choose a message group first (if
// false, then only choose a sub-board within the user's current group).  This
// can be true or false.
var chooseMsgGrp = true;
if (typeof(argv[0]) == "boolean")
	chooseMsgGrp = argv[0];
else if (typeof(argv[0]) == "string")
	chooseMsgGrp = (argv[0].toLowerCase() == "true");

// 2nd command-line argument: Determine whether or not to execute the message listing
// code (true/false)
if (typeof(argv[1]) == "boolean")
	executeThisScript = argv[1];
else if (typeof(argv[1]) == "string")
	executeThisScript = (argv[1].toLowerCase() == "true");

// If executeThisScript is true, then create a DDMsgAreaChooser object and use
// it to let the user choose a message area.
if (executeThisScript)
{
	var msgAreaChooser = new DDMsgAreaChooser();
	msgAreaChooser.SelectMsgArea(chooseMsgGrp);
}

// End of script execution

///////////////////////////////////////////////////////////////////////////////////
// DDMsgAreaChooser class stuff

function DDMsgAreaChooser()
{
	// this.colors will be an associative array of colors (indexed by their
	// usage) used for the message group/sub-board lists.
	// Colors for the file & message area lists
	this.colors = new Object();
	this.colors.areaNum = "\1n\1w\1h";
	this.colors.desc = "\1n\1c";
	this.colors.numItems = "\1b\1h";
	this.colors.header = "\1n\1y\1h";
	this.colors.subBoardHeader = "\1n\1g";
	this.colors.areaMark = "\1g\1h";
	this.colors.latestDate = "\1n\1g";
	this.colors.latestTime = "\1n\1m";
	// Highlighted colors (for lightbar mode)
	this.colors.bkgHighlight = "\1" + "4"; // Blue background
	this.colors.areaNumHighlight = "\1w\1h";
	this.colors.descHighlight = "\1c";
	this.colors.dateHighlight = "\1w\1h";
	this.colors.timeHighlight = "\1w\1h";
	this.colors.numItemsHighlight = "\1w\1h";
	// Lightbar help line colors
	this.colors.lightbarHelpLineBkg = "\1" + "7";
	this.colors.lightbarHelpLineGeneral = "\1b";
	this.colors.lightbarHelpLineHotkey = "\1r";
	this.colors.lightbarHelpLineParen = "\1m";

	// showImportDates is a boolean to specify whether or not to display the
	// message import dates.  If false, the message written dates will be
	// displayed instead.
	this.showImportDates = true;

	// useLightbarInterface specifies whether or not to use the lightbar
	// interface.  The lightbar interface will still only be used if the
	// user's terminal supports ANSI.
	this.useLightbarInterface = true;

	// Filename base of a header to display above the area list
	this.areaChooserHdrFilenameBase = "msgAreaChgHeader";
	this.areaChooserHdrMaxLines = 5;

	// Whether or not to show the latest message date/time in the
	// sub-board list
	this.showDatesInSubBoardList = true;

	// Set the function pointers for the object
	this.ReadConfigFile = DDMsgAreaChooser_ReadConfigFile;
	this.WriteKeyHelpLine = DDMsgAreaChooser_writeKeyHelpLine;
	this.WriteGrpListHdrLine = DDMsgAreaChooser_writeGrpListTopHdrLine;
	this.WriteSubBrdListHdr1Line = DMsgAreaChooser_writeSubBrdListHdr1Line;
	this.SelectMsgArea = DDMsgAreaChooser_selectMsgArea;
	this.SelectMsgArea_Lightbar = DDMsgAreaChooser_selectMsgArea_Lightbar;
	this.SelectSubBoard_Lightbar = DDMsgAreaChooser_selectSubBoard_Lightbar;
	this.SelectMsgArea_Traditional = DDMsgAreaChooser_selectMsgArea_Traditional;
	this.SelectSubBoard_Traditional = DDMsgAreaChooser_selectSubBoard_Traditional;
	this.ListMsgGrps = DDMsgAreaChooser_listMsgGrps_Traditional;
	this.ListSubBoardsInMsgGroup = DDMsgAreaChooser_listSubBoardsInMsgGroup_Traditional;
	// Lightbar-specific functions
	this.ListScreenfulOfMsgGrps = DDMsgAreaChooser_listScreenfulOfMsgGrps;
	this.WriteMsgGroupLine = DDMsgAreaChooser_writeMsgGroupLine;
	this.updatePageNumInHeader = DDMsgAreaChooser_updatePageNumInHeader;
	this.ListScreenfulOfSubBrds = DDMsgAreaChooser_listScreenfulOfSubBrds;
	this.WriteMsgSubBoardLine = DDMsgAreaChooser_writeMsgSubBrdLine;
	// Help screen
	this.ShowHelpScreen = DDMsgAreaChooser_showHelpScreen;
	// Function to build the sub-board printf information for a message
	// group
	this.BuildSubBoardPrintfInfoForGrp = DDMsgAreaChooser_buildSubBoardPrintfInfoForGrp;
	this.DisplayAreaChgHdr = DDMsgAreaChooser_DisplayAreaChgHdr;
	this.WriteLightbarKeyHelpErrorMsg = DDMsgAreaChooser_WriteLightbarKeyHelpErrorMsg;

	// Read the settings from the config file.
	this.ReadConfigFile();
	
	// These variables store the lengths of the various columns displayed in
	// the message group/sub-board lists.
	// Sub-board info field lengths
	this.areaNumLen = 4;
	this.numItemsLen = 4;
	this.dateLen = 10; // i.e., YYYY-MM-DD
	this.timeLen = 8;  // i.e., HH:MM:SS
	// Sub-board name length - This should be 47 for an 80-column display.
	this.subBoardNameLen = console.screen_columns - this.areaNumLen - this.numItemsLen - 5;
	if (this.showDatesInSubBoardList)
		this.subBoardNameLen -= (this.dateLen + this.timeLen + 2);
	// Message group description length (67 chars on an 80-column screen)
	this.msgGrpDescLen = console.screen_columns - this.areaNumLen - this.numItemsLen - 5;

	// printf strings for various things
	// Message group information (printf strings)
	this.msgGrpListPrintfStr = "\1n " + this.colors.areaNum + "%" + this.areaNumLen
	                         + "d " + this.colors.desc + "%-"
	                         + this.msgGrpDescLen + "s " + this.colors.numItems
	                         + "%" + this.numItemsLen + "d";
	this.msgGrpListHilightPrintfStr = "\1n" + this.colors.bkgHighlight + " "
	                                + "\1n" + this.colors.bkgHighlight
	                                + this.colors.areaNumHighlight + "%" + this.areaNumLen
	                                + "d \1n" + this.colors.bkgHighlight
	                                + this.colors.descHighlight + "%-"
	                                + this.msgGrpDescLen + "s \1n" + this.colors.bkgHighlight
	                                + this.colors.numItemsHighlight + "%" + this.numItemsLen
	                                + "d";
	// Message group list header (printf string)
	this.msgGrpListHdrPrintfStr = this.colors.header + "%6s %-"
	                            + +(this.msgGrpDescLen-8) + "s %-12s";
	// Sub-board information header (printf string)
	this.subBoardListHdrPrintfStr = this.colors.header + " %5s %-"
	                              + +(this.subBoardNameLen-3) + "s %-7s";
	if (this.showDatesInSubBoardList)
		this.subBoardListHdrPrintfStr += " %-19s";
	// Lightbar mode key help line
	this.lightbarKeyHelpText = "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + UP_ARROW
				  + "\1n" + this.colors.lightbarHelpLineGeneral
				  + this.colors.lightbarHelpLineBkg + ", "
				  + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + DOWN_ARROW
				  + "\1n" + this.colors.lightbarHelpLineGeneral
				  + this.colors.lightbarHelpLineBkg + ", "
				  + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + "HOME"
				  + "\1n" + this.colors.lightbarHelpLineGeneral
				  + this.colors.lightbarHelpLineBkg + ", "
				  + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + "END"
				  + "\1n" + this.colors.lightbarHelpLineGeneral
				  + this.colors.lightbarHelpLineBkg + ", "
				  + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + "#"
				  + "\1n" + this.colors.lightbarHelpLineGeneral
				  + this.colors.lightbarHelpLineBkg + ", "
				  + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + "PgUp"
				  + "\1n" + this.colors.lightbarHelpLineGeneral
				  + this.colors.lightbarHelpLineBkg + "/"
				  + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + "Dn"
				  + "\1n" + this.colors.lightbarHelpLineGeneral
				  + this.colors.lightbarHelpLineBkg + ", "
				  + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + "F"
				  + "\1n" + this.colors.lightbarHelpLineParen
				  + this.colors.lightbarHelpLineBkg + ")"
				  + "\1n" + this.colors.lightbarHelpLineGeneral
				  + this.colors.lightbarHelpLineBkg + "irst pg, "
				  + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + "L"
				  + "\1n" + this.colors.lightbarHelpLineParen
				  + this.colors.lightbarHelpLineBkg + ")"
				  + "\1n" + this.colors.lightbarHelpLineGeneral
				  + this.colors.lightbarHelpLineBkg + "ast pg, "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "CTRL-F"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "/"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
	              + this.colors.lightbarHelpLineBkg + "N"
	              + "\1n" + this.colors.lightbarHelpLineGeneral
	              + this.colors.lightbarHelpLineBkg + ", "
	              + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + "Q"
				  + "\1n" + this.colors.lightbarHelpLineParen
				  + this.colors.lightbarHelpLineBkg + ")"
				  + "\1n" + this.colors.lightbarHelpLineGeneral
				  + this.colors.lightbarHelpLineBkg + "uit, "
				  + "\1n" + this.colors.lightbarHelpLineHotkey
				  + this.colors.lightbarHelpLineBkg + "?";
	// Pad the lightbar key help text on either side to center it on the screen
	// (but leave off the last character to avoid screen drawing issues)
	var helpTextLen = console.strlen(this.lightbarKeyHelpText);
	var helpTextStartCol = (console.screen_columns/2) - (helpTextLen/2);
	this.lightbarKeyHelpText = "\1n" + this.colors.lightbarHelpLineBkg
	                         + format("%" + +(helpTextStartCol) + "s", "")
							 + this.lightbarKeyHelpText + "\1n"
							 + this.colors.lightbarHelpLineBkg;
	var numTrailingChars = console.screen_columns - (helpTextStartCol+helpTextLen) - 1;
	this.lightbarKeyHelpText += format("%" + +(numTrailingChars) + "s", "") + "\1n";
	// this.subBoardListPrintfInfo will be an array of printf strings
	// for the sub-boards in the message groups.  The index is the
	// message group index.  The sub-board printf information is created
	// on the fly the first time the user lists sub-boards for a message
	// group.
	this.subBoardListPrintfInfo = new Array();

	// areaChangeHdrLines is an array of text lines to use as a header to display
	// above the message area changer lists.
	this.areaChangeHdrLines = loadTextFileIntoArray(this.areaChooserHdrFilenameBase, this.areaChooserHdrMaxLines);
}

// For the DDMsgAreaChooser class: Writes the line of key help at the bottom
// row of the screen.
function DDMsgAreaChooser_writeKeyHelpLine()
{
	console.gotoxy(1, console.screen_rows);
	console.print(this.lightbarKeyHelpText);
}

// For the DDMsgAreaChooser class: Outputs the header line to appear above
// the list of message groups.
//
// Parameters:
//  pNumPages: The number of pages.  This is optional; if this is
//             not passed, then it won't be used.
//  pPageNum: The page number.  This is optional; if this is not passed,
//            then it won't be used.
function DDMsgAreaChooser_writeGrpListTopHdrLine(pNumPages, pPageNum)
{
	var descStr = "Description";
	if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
		descStr += "    (Page " + pPageNum + " of " + pNumPages + ")";
	else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
		descStr += "    (Page " + pPageNum + ")";
	else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
		descStr += "    (" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
	printf(this.msgGrpListHdrPrintfStr, "Group#", descStr, "# Sub-Boards");
	console.cleartoeol("\1n");
}

// For the DDMsgAreaChooser class: Outputs the first header line to appear
// above the sub-board list for a message group.
//
// Parameters:
//  pGrpIndex: The index of the message group (assumed to be valid)
//  pNumPages: The number of pages.  This is optional; if this is
//             not passed, then it won't be used.
//  pPageNum: The page number.  This is optional; if this is not passed,
//            then it won't be used.
function DMsgAreaChooser_writeSubBrdListHdr1Line(pGrpIndex, pNumPages, pPageNum)
{
	var descFormatStr = "\1n" + this.colors.subBoardHeader + "Sub-boards of \1h%-25s     \1n"
	                  + this.colors.subBoardHeader;
	if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
		descFormatStr += "(Page " + pPageNum + " of " + pNumPages + ")";
	else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
		descFormatStr += "(Page " + pPageNum + ")";
	else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
		descFormatStr += "(" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
	printf(descFormatStr, msg_area.grp_list[pGrpIndex].description.substr(0, 25));
	console.cleartoeol("\1n");
}

// For the DDMsgAreaChooser class: Lets the user choose a message group and
// sub-board via numeric input, using a lightbar interface (if enabled and
// if the user's terminal uses ANSI) or a traditional user interface.
//
// Parameters:
//  pChooseGroup: Boolean - Whether or not to choose the message group.  If false,
//                then this will allow choosing a sub-board within the user's
//                current message group.  This is optional; defaults to true.
function DDMsgAreaChooser_selectMsgArea(pChooseGroup)
{
	if (this.useLightbarInterface && console.term_supports(USER_ANSI))
		this.SelectMsgArea_Lightbar(pChooseGroup);
		this.SelectMsgArea_Traditional(pChooseGroup);
}

// For the DDMsgAreaChooser class: Lets the user choose a message group and
// sub-board via numeric input, using a lightbar user interface.
//
// Parameters:
//  pChooseGroup: Boolean - Whether or not to choose the message group.  If false,
//                then this will allow choosing a sub-board within the user's
//                current message group.  This is optional; defaults to true.
function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup)
	// If there are no message groups, then don't let the user
	// choose one.
	if (msg_area.grp_list.length == 0)
	{
		console.clear("\1n");
		console.print("\1y\1hThere are no message groups.\r\n\1p");
		return;
	}
	var chooseGroup = (typeof(pChooseGroup) == "boolean" ? pChooseGroup : true);
	if (chooseGroup)
	{
		// Returns the index of the bottommost message group that can be displayed
		// on the screen.
		//
		// Parameters:
		//  pTopGrpIndex: The index of the topmost message group displayed on screen
		//  pNumItemsPerPage: The number of items per page
		function getBottommostGrpIndex(pTopGrpIndex, pNumItemsPerPage)
		{
			var bottomGrpIndex = pTopGrpIndex + pNumItemsPerPage - 1;
			// If bottomGrpIndex is beyond the last index, then adjust it.
			if (bottomGrpIndex >= msg_area.grp_list.length)
				bottomGrpIndex = msg_area.grp_list.length - 1;
			return bottomGrpIndex;
		}
		// For doing the "next" search result
		function nextGrpSearchFoundItem(searchObj, numPages, listStartRow, listEndRow, selectedGrpIndex, topMsgGrpIndex, chooserObj)
		{
			var retObj = {
				differentPage: false,
				topMsgGrpIndex: srchObj.pageTopIdx,
				selectedGrpIndex: srchObj.itemIdx
			};

			// For screen refresh optimization, don't redraw the whole
			// list if the result is on the same page
			if (srchObj.pageTopIdx != topMsgGrpIndex)
			{
				retObj.differentPage = true;
				chooserObj.updatePageNumInHeader(srchObj.pageNum, numPages, true, false);
				chooserObj.ListScreenfulOfMsgGrps(retObj.topMsgGrpIndex, listStartRow, listEndRow, false, true, srchObj.itemIdx);
			}
			else
			{
				if (srchObj.itemIdx != selectedGrpIndex)
				{
					var screenY = listStartRow + (selectedGrpIndex - topMsgGrpIndex);
					console.gotoxy(1, screenY);
					chooserObj.WriteMsgGroupLine(selectedGrpIndex, false);
					retObj.selectedGrpIndex = srchObj.itemIdx;
					screenY = listStartRow + (retObj.selectedGrpIndex - topMsgGrpIndex);
					console.gotoxy(1, screenY);
					chooserObj.WriteMsgGroupLine(retObj.selectedGrpIndex, true);
				}
			}

			return retObj;
		}

		// Figure out the index of the user's currently-selected message group
		var selectedGrpIndex = 0;
		if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
			selectedGrpIndex = msg_area.sub[bbs.cursub_code].grp_index;
		var listStartRow = 2 + this.areaChangeHdrLines.length; // The row on the screen where the list will start
		var listEndRow = console.screen_rows - 1; // Row on screen where list will end
		var topMsgGrpIndex = 0;    // The index of the message group at the top of the list
		// Figure out the index of the last message group to appear on the screen.
		var numItemsPerPage = listEndRow - listStartRow + 1;
		var bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
		// Figure out how many pages are needed to list all the sub-boards.
		var numPages = Math.ceil(msg_area.grp_list.length / numItemsPerPage);
		// Figure out the top index for the last page.
		var topIndexForLastPage = (numItemsPerPage * numPages) - numItemsPerPage;
		// If the highlighted row is beyond the current screen, then
		// go to the appropriate page.
		if (selectedGrpIndex > bottomMsgGrpIndex)
		{
			var nextPageTopIndex = 0;
			while (selectedGrpIndex > bottomMsgGrpIndex)
			{
				nextPageTopIndex = topMsgGrpIndex + numItemsPerPage;
				if (nextPageTopIndex < msg_area.grp_list.length)
				{
					// Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
					// refresh the list on the screen.
					topMsgGrpIndex = nextPageTopIndex;
					bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
				}
				else
					break;
			}
			// If we didn't find the correct page for some reason, then set the
			// variables to display page 1 and select the first message group.
			var foundCorrectPage = ((topMsgGrpIndex < msg_area.grp_list.length) &&
									(selectedGrpIndex >= topMsgGrpIndex) && (selectedGrpIndex <= bottomMsgGrpIndex));
			if (!foundCorrectPage)
			{
				topMsgGrpIndex = 0;
				bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
				selectedGrpIndex = 0;
			}
		}
		// Clear the screen, write the header, help line, and group list header, and output
		// a screenful of message groups.
		console.clear("\1n");
		var curpos = {
			x: 1,
			y: 1 + this.areaChangeHdrLines.length
		};
		console.gotoxy(curpos);
		var pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
		this.WriteGrpListHdrLine(numPages, pageNum);
		this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, false);
		// Start of the input loop.
		var highlightScrenRow = 0; // The row on the screen for the highlighted group
		var userInput = "";        // Will store a keypress from the user
		var retObj = null;        // To store the return value of choosing a sub-board
		var lastSearchText = "";
		var lastSearchFoundIdx = -1;
		var continueChoosingMsgArea = true;
		while (continueChoosingMsgArea)
		{
			// Highlight the currently-selected message group
			highlightScrenRow = listStartRow + (selectedGrpIndex - topMsgGrpIndex);
			curpos.y = highlightScrenRow;
			if ((highlightScrenRow > 0) && (highlightScrenRow < console.screen_rows))
			{
				console.gotoxy(1, highlightScrenRow);
				this.WriteMsgGroupLine(selectedGrpIndex, true);
			}
			// Get a key from the user (upper-case) and take action based upon it.
			userInput = getKeyWithESCChars(K_UPPER | K_NOCRLF);
			switch (userInput)
			{
				case KEY_UP: // Move up one message group in the list
					if (selectedGrpIndex > 0)
					{
						// If the previous group index is on the previous page, then
						// display the previous page.
						var previousGrpIndex = selectedGrpIndex - 1;
						if (previousGrpIndex < topMsgGrpIndex)
						{
							// Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
							// refresh the list on the screen.
							topMsgGrpIndex -= numItemsPerPage;
							bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
							this.updatePageNumInHeader(pageNum, numPages, true, false);
							this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true);
						}
						else
						{
							// Display the current line un-highlighted.
							console.gotoxy(1, curpos.y);
							this.WriteMsgGroupLine(selectedGrpIndex, false);
						}
						selectedGrpIndex = previousGrpIndex;
					}
					break;
				case KEY_DOWN: // Move down one message group in the list
					if (selectedGrpIndex < msg_area.grp_list.length - 1)
					{
						// If the next group index is on the next page, then display
						// the next page.
						var nextGrpIndex = selectedGrpIndex + 1;
						if (nextGrpIndex > bottomMsgGrpIndex)
						{
							// Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
							// refresh the list on the screen.
							topMsgGrpIndex += numItemsPerPage;
							bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
							this.updatePageNumInHeader(pageNum+1, numPages, true, false);
							this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow,
														listEndRow, false, true);
						}
						else
						{
							// Display the current line un-highlighted.
							// TODO: Something is wrong with curpos.y here if
							// you do a search and press next, especially after
							// going to the next page
							console.gotoxy(1, curpos.y);
							this.WriteMsgGroupLine(selectedGrpIndex, false);
						}
						selectedGrpIndex = nextGrpIndex;
					}
					break;
				case KEY_HOME: // Go to the top message group on the screen
					if (selectedGrpIndex > topMsgGrpIndex)
					{
						// Display the current line un-highlighted, then adjust
						// selectedGrpIndex.
						console.gotoxy(1, curpos.y);
						this.WriteMsgGroupLine(selectedGrpIndex, false);
						selectedGrpIndex = topMsgGrpIndex;
						// Note: curpos.y is set at the start of the while loop.
					}
					break;
				case KEY_END: // Go to the bottom message group on the screen
					if (selectedGrpIndex < bottomMsgGrpIndex)
					{
						// Display the current line un-highlighted, then adjust
						// selectedGrpIndex.
						console.gotoxy(1, curpos.y);
						this.WriteMsgGroupLine(selectedGrpIndex, false);
						selectedGrpIndex = bottomMsgGrpIndex;
						// Note: curpos.y is set at the start of the while loop.
					}
					break;
				case KEY_ENTER: // Select the currently-highlighted message group
					retObj = this.SelectSubBoard_Lightbar(selectedGrpIndex);
					// If the user chose a sub-board, then set the user's message
					// sub-board, and don't continue the input loop anymore.
					if (retObj.subBoardChosen)
					{
						continueChoosingMsgArea = false;
						bbs.cursub_code = retObj.subBoardCode;
					}
					else
					{
						// A sub-board was not chosen, so we'll have to re-draw
						// the header and list of message groups.
						this.DisplayAreaChgHdr(1);
						console.gotoxy(1, 1+this.areaChangeHdrLines.length);
						this.WriteGrpListHdrLine(numPages, pageNum);
						this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true);
					}
					break;
				case KEY_PAGE_DOWN: // Go to the next page
					var nextPageTopIndex = topMsgGrpIndex + numItemsPerPage;
					if (nextPageTopIndex < msg_area.grp_list.length)
					{
						// Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
						// refresh the list on the screen.
						topMsgGrpIndex = nextPageTopIndex;
						pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
						bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
						this.updatePageNumInHeader(pageNum, numPages, true, false);
						this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true);
					break;
				case KEY_PAGE_UP: // Go to the previous page
					var prevPageTopIndex = topMsgGrpIndex - numItemsPerPage;
					if (prevPageTopIndex >= 0)
					{
						// Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
						// refresh the list on the screen.
						topMsgGrpIndex = prevPageTopIndex;
						pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
						bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
						this.updatePageNumInHeader(pageNum, numPages, true, false);
						this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true);
						selectedGrpIndex = topMsgGrpIndex;
					break;
				case 'F': // Go to the first page
					if (topMsgGrpIndex > 0)
						topMsgGrpIndex = 0;
						pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
						bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
						this.updatePageNumInHeader(pageNum, numPages, true, false);
						this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true);
						selectedGrpIndex = 0;
					break;
				case 'L': // Go to the last page
					if (topMsgGrpIndex < topIndexForLastPage)
					{
						topMsgGrpIndex = topIndexForLastPage;
						pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
						bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
						this.updatePageNumInHeader(pageNum, numPages, true, false);
						this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true);
						selectedGrpIndex = topIndexForLastPage;
					}
					break;
				case 'Q': // Quit
					break;
				case '?': // Show help
					this.ShowHelpScreen(true, true);
					console.pause();
					// Refresh the screen
					this.WriteKeyHelpLine();
					this.DisplayAreaChgHdr(1);
					console.gotoxy(1, 1+this.areaChangeHdrLines.length);
					this.WriteGrpListHdrLine(numPages, pageNum);
					this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true);
				case '/': // Start of find (search)
				case CTRL_F: // Start of find
					console.gotoxy(1, console.screen_rows);
					console.cleartoeol("\1n");
					console.gotoxy(1, console.screen_rows);
					var promptText = "Search: ";
					console.print(promptText);
					var searchText = getStrWithTimeout(K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE, console.screen_columns - promptText.length - 1, SEARCH_TIMEOUT_MS);
					// If the user entered text, then do the search, and if found,
					// found, go to the page and select the item indicated by the
					// search.
					if (searchText.length > 0)
					{
						var srchObj = getPageNumFromSearch(searchText, numItemsPerPage, false, 0);
						if (srchObj.pageNum > 0)
						{
							lastSearchText = searchText;
							lastSearchFoundIdx = srchObj.itemIdx;

							// For screen refresh optimization, don't redraw the whole
							// list if the result is on the same page
							if (srchObj.pageTopIdx != topMsgGrpIndex)
							{
								topMsgGrpIndex = srchObj.pageTopIdx;
								selectedGrpIndex = srchObj.itemIdx;
								bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
								pageNum = srchObj.pageNum;
								this.updatePageNumInHeader(pageNum, numPages, true, false);
								this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true);
							}
							else
							{
								if (srchObj.itemIdx != selectedGrpIndex)
								{
									var screenY = listStartRow + (selectedGrpIndex - topMsgGrpIndex);
									console.gotoxy(1, screenY);
									this.WriteMsgGroupLine(selectedGrpIndex, false);
									selectedGrpIndex = srchObj.itemIdx;
									screenY = listStartRow + (selectedGrpIndex - topMsgGrpIndex);
									console.gotoxy(1, screenY);
									this.WriteMsgGroupLine(selectedGrpIndex, true);
								}
							}
						}
						else
							this.WriteLightbarKeyHelpErrorMsg("Not found", false);
					}
					this.WriteKeyHelpLine();
					break;
				case 'N': // Next search result (requires an existing search term)
					if ((lastSearchText.length > 0) && (lastSearchFoundIdx > -1))
					{
						// Do the search, and if found, go to the page and select the item
						// indicated by the search.
						var srchObj = getPageNumFromSearch(lastSearchText, numItemsPerPage, false, lastSearchFoundIdx+1);
						lastSearchFoundIdx = srchObj.itemIdx;
						if (srchObj.pageNum > 0)
						{
							var foundItemRetObj = nextGrpSearchFoundItem(srchObj, numPages, listStartRow, listEndRow, selectedGrpIndex, topMsgGrpIndex, this);
							if (foundItemRetObj.differentPage)
							{
								pageNum = srchObj.pageNum;
								topMsgGrpIndex = foundItemRetObj.topMsgGrpIndex;
								bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
							}
							selectedGrpIndex = foundItemRetObj.selectedGrpIndex;
						}
						else
						{
							// Not found - Wrap around and start at 0 again
							var srchObj = getPageNumFromSearch(lastSearchText, numItemsPerPage, false, 0);
							lastSearchFoundIdx = srchObj.itemIdx;
							var foundItemRetObj = nextGrpSearchFoundItem(srchObj, numPages, listStartRow, listEndRow, selectedGrpIndex, topMsgGrpIndex, this);
							if (foundItemRetObj.selectedGrpIndex != selectedGrpIndex)
							{
								if (foundItemRetObj.differentPage)
								{
									pageNum = srchObj.pageNum;
									topMsgGrpIndex = foundItemRetObj.topMsgGrpIndex;
									bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
								}
								selectedGrpIndex = foundItemRetObj.selectedGrpIndex;
							}
							else
								this.WriteLightbarKeyHelpErrorMsg("No others found", true);
						}
					}
					else
						this.WriteLightbarKeyHelpErrorMsg("There is no previous search", true);
					break;
				default:
					// If the user entered a numeric digit, then treat it as
					// the start of the message group number.
					if (userInput.match(/[0-9]/))
						var originalCurpos = curpos;

						// Put the user's input back in the input buffer to
						// be used for getting the rest of the message number.
						console.ungetstr(userInput);
						// Move the cursor to the bottom of the screen and
						// prompt the user for the message number.
						console.gotoxy(1, console.screen_rows);
						console.clearline("\1n");
						console.print("\1cChoose group #: \1h");
						userInput = console.getnum(msg_area.grp_list.length);
						// If the user made a selection, then let them choose a
						// sub-board from the group.
						if (userInput > 0)
							var msgGroupIndex = userInput - 1;
							retObj = this.SelectSubBoard_Lightbar(msgGroupIndex);
							// If the user chose a sub-board, then set the user's
							// message sub-board, and don't continue the input loop anymore.
							if (retObj.subBoardChosen)
							{
								continueChoosingMsgArea = false;
								bbs.cursub_code = retObj.subBoardCode;
							}
							else
							{
								// A sub-board was not chosen, so we'll have to re-draw
								// the header and list of message groups.
								//this.DisplayAreaChgHdr(1);
								console.gotoxy(1, 1+this.areaChangeHdrLines.length);
								this.WriteGrpListHdrLine(numPages, pageNum);
								this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true);
							}
							// The user didn't make a selection.  So, we need to refresh
							// the screen due to everything being moved up one line.
							this.WriteKeyHelpLine();
							//this.DisplayAreaChgHdr(1);
							console.gotoxy(1, 1+this.areaChangeHdrLines.length);
							this.WriteGrpListHdrLine(numPages, pageNum);
							this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true);
						}
					}
			}
		}
	}
	else
	{
		// Don't choose a group, just a sub-board within the user's current group.
		var grpIndex = 0;
		if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
			grpIndex =  msg_area.sub[bbs.cursub_code].grp_index;
		retObj = this.SelectSubBoard_Lightbar(grpIndex);
		// If the user chose a sub-board, then set the user's sub-board
			bbs.cursub_code = retObj.subBoardCode;
}

// For the DDMsgAreaChooser class: Lets the user choose a sub-board within a
// message group, with a lightbar interface.  Does not set the user's sub-board.
//
// Parameters:
//  pGrpIndex: The index of the message group to choose from.  This is
//             optional; if not specified, the user's current group index will be used.
//  pMarkIndex: An index of a message group to display the "current" mark
//              next to.  This is optional; if left off, this will default to
//              the current sub-board.
//
// Return value: An object containing the following values:
//               subBoardChosen: Boolean - Whether or not a sub-board was chosen.
//               subBoardIndex: Numeric - The sub-board that was chosen (if any).
//                              Will be -1 if none chosen.
//               subBoardCode: The internal code of the chosen sub-board ("" if none chosen)
function DDMsgAreaChooser_selectSubBoard_Lightbar(pGrpIndex, pMarkIndex)
{
	var retObj = {
		subBoardChosen: false,
		subBoardIndex: -1,
		subBoardCode: ""
	};

	var usersCurrentIdxVals = getGrpAndSubIdxesFromCode(bbs.cursub_code, true);
	var grpIndex = 0;
	if (typeof(pGrpIndex) == "number")
		grpIndex = pGrpIndex;
	else
		grpIndex = usersCurrentIdxVals.grpIdx;
	// Double-check grpIndex
	if (grpIndex < 0)
		grpIndex = 0;
	else if (grpIndex >= msg_area.grp_list.length)
		grpIndex = msg_area.grp_list.length - 1;
	var markIndex = 0;
	if ((pMarkIndex != null) && (typeof(pMarkIndex) == "number"))
		markIndex = pMarkIndex;
	else if (pGrpIndex == usersCurrentIdxVals.grpIdx)
		markIndex = usersCurrentIdxVals.subIdx;
	// Double-check markIndex
	if (markIndex < 0)
		markIndex = 0;
	else if (markIndex >= msg_area.grp_list[grpIndex].sub_list.length)
		markIndex = msg_area.grp_list[grpIndex].sub_list.length - 1;
	// Ensure that the sub-board printf information is created for
	// this message group.
	this.BuildSubBoardPrintfInfoForGrp(grpIndex);
	// If there are no sub-boards in the given message group, then show
	// an error and return.
	if (msg_area.grp_list[grpIndex].sub_list.length == 0)
	{
		console.clear("\1n");
		console.print("\1y\1hThere are no sub-boards in the chosen group.\r\n\1p");
		return retObj;
	}
	// Returns the index of the bottommost sub-board that can be displayed on
	// the screen.
	//
	// Parameters:
	//  pTopSubIndex: The index of the topmost sub-board displayed on screen
	//  pNumItemsPerPage: The number of items per page
	function getBottommostSubIndex(pTopSubIndex, pNumItemsPerPage)
	{
		var bottomGrpIndex = topSubIndex + pNumItemsPerPage - 1;
		// If bottomGrpIndex is beyond the last index, then adjust it.
		if (bottomGrpIndex >= msg_area.grp_list[grpIndex].sub_list.length)
			bottomGrpIndex = msg_area.grp_list[grpIndex].sub_list.length - 1;
		return bottomGrpIndex;
	}
	// For doing the "next" search result
	function nextSubSearchFoundItem(grpIndex, searchObj, numPages, listStartRow, listEndRow, selectedSubIndex, topSubIndex, chooserObj)
	{
		var retObj = {
			differentPage: false,
			topSubIndex: srchObj.pageTopIdx,