Skip to content
Snippets Groups Projects
DDMsgReader.js 805 KiB
Newer Older
/* This is a message reader/lister door for Synchronet.  Features include:
 * - Listing messages in the user's current message area with the ability to
 *   navigate forwards & backwards through the list (and for ANSI users, a
 *   lightbar interface will be used, or optionally can be set to use a more
 *   traditional interface for ANSI users)
 * - The user can select a message from the list to read and optionally reply to
 * - For ANSI users, reading messages is done with an enhanced user interface,
 *   with the ability to scroll up & down through the message, move to the next
 *   or previous message using the right & left arrow keys, display the message
 *   list to choose another message to read, etc.
 * - The ability to start up with the message list or reading messages in the
 *   user's current message area (AKA sub-board)
 * - Message searching
 *
 * Author: Eric Oulashin (AKA Nightfox)
 * BBS: Digital Distortion
 * BBS address: digitaldistortionbbs.com (or digdist.bbsindex.com)
 *
 * Date       Author            Description
 * 2014-09-13 Eric Oulashin     Started (based on my message lister script)
 * 2022-03-14 Eric Oulashin     Version 1.47
 *                              Updated to make DDMsgReader can be called directly as a
 *                              loadable module by Synchronet (work started on March 8).
 *                              Also, refactored to use attr_conv.js and removed the
 *                              attribute conversion functions from this script.
 * 2022-03-23 Eric Oulashin     Version 1.47a
 *                              Now calls bbs.edit_msg() to edit an existing message (if
 *                              that function exists - It was added in Synchronet 3.18).
 * 2022-06-12 Eric Oulashin     Version 1.48
 *                              Improved display of ANSI messages via the use of the Graphic object
 * 2022-06-13 Eric Oulashin     Version 1.49
 *                              Refactor: Simplified saving a message to BBS machine for sysop
 *                              (as-is, less processing); removed attachment stuff for pre-Synchronet
 *                              3.17; moved hasSyncAttrCodes() to attr_conv.js because that's where it
 *                              needs to be.
 * 2022-06-13 Eric Oulashin     Version 1.50
 *                              When doing a text search, it now ignores the user scan configuration for
 *                              sub-boards, to ensure it will show any results of the text search.
 *                              Graphic is now only used when using the scrollable interface. Also,
 *                              when creating the Graphic, now subtracting 1 from the reading area height
 *                              to avoid making the Graphic one line too tall to avoid unnecessary scrolling.
 *                              When saving messages with ANSI codes, Graphic is only used if the message has
 *                              any ASCII drawing characters. (not sure if this really matters much though).
 *                              Also, applied "use strict" and made some changes as necessary.
 * 2022-07-09 Eric Oulashin     Version 1.52
 *                              Mouse click support for the bottom help lines in scrollable mode
 *                              (thanks to help from Nelgin)
 * 2022-07-18 Eric Oulashin     Version 1.53
 *                              Deleted messages can now be un-marked for deletion from the message
 *                              list with the U key (if the user has delete permissions). Also, the reader now
 *                              honors the system setting for whether users can view deleted messages.
 * 2022-08-06 Eric Oulashin     Version 1.54
 *                              Users now have a personal twit list (configurable via Ctrl-U, user settings).
 * 2022-09-23 Eric Oulashin     Version 1.55
 *                              Refactored how email replies are done (passing the header to the appropriate
 *                              functions, not using ungetstr() when prompting for the message subject)
 * 2022-11-25 Eric Oulashin     Version 1.56
 *                              Fixed bug startup mode for scanning all groups for un-read messages to you where
 *                              the reader was bringing up personal email instead.
 * 2022-12-02 Eric Oulashin     Version 1.57
 *                              @-codes were only expanded when reading personal mail; now, DDMsgReader
 *                              also checks to make sure the sender is a sysop.  Also, used putmsg() in
 *                              place of this script's own @-message parsing when displaying some of the
 *                              configured text strings.
 * 2022-12-12 Eric Oulashin     Fix for "assignment to undeclared variable" error in GetMsgSubBrdLine();
 *                              appeared when changing to a different message area from the reader
 * 2012-12-14 Eric Oulashin     Version 1.58
 *                              When writing QUOTES.TXT, quote lines are now wrapped if the user's
 *                              external editor configuration is configured to do so.
 * 2022-12-29 Eric Oulashin     Version 1.59
 *                              For Synchronet above 3.20, read the external editor quote wrap setting
 *                              from xtrn.ini.  Below version 3.20, read it from xtrn.cnf.
 *                              Also, there's a new user setting to toggle whether or not to use the scrollbar
 *                              in the scrolling reader. Currently there is no alternate progress displayed
 *                              if not using the scrollbar, but that is planned for a future update.
 * 2023-01-20 Eric Oulashin     Version 1.60
 *                              DDMsgReader can now optionally convert Y-style MCI attribute codes to
 *                              to Synchronet attribute codes, with the new configuration setting
 *                              convertYStyleMCIAttrsToSync (true/false). Requires the updated attr_conv.js
 *                              in sbbs/exec/load.
 * 2023-01-22 Eric Oulashin     Version 1.61
 *                              Fix: When replying to an email with an unknown sender (empty),
 *                              no longer gives the error "Invalid user field: 0"; also, if the sender is
 *                              unknown, prompts the user for a user name/number/email address to send
 *                              the reply to.
 * 2023-01-30 Eric Oulashin     Version 1.62
 *                              (Hopefully) Improved display of ANSI messages which would previously look
 *                              bad with empty lines evrey other line
 * 2023-02-01 Eric Oulashin     Version 1.63
 *                              Fix for reading colors from the theme file. Also, the theme file now
 *                              no longer needs the control character for color codes.
// TODO: In the message list, add the ability to search with / similar to my area chooser
/* Command-line arguments (in -arg=val format, or -arg format to enable an
   option):
   -search: A search type.  Available options:
            keyword_search: Do a keyword search in message subject/body text (current message area)
            from_name_search: 'From' name search (current message area)
            to_name_search: 'To' name search (current message area)
            to_user_search: To user search (current message area)
            new_msg_scan: New message scan (prompt for current sub-board, current
                          group, or all)
            new_msg_scan_all: New message scan (all sub-boards)
			new_msg_scan_cur_grp: New message scan (current message group only)
            new_msg_scan_cur_sub: New message scan (current sub-board only).  This
			                      can (optionally) be used with the -subBoard
								  command-line parameter, which specifies an internal
								  code for a sub-board, which may be different from
								  the user's currently selected sub-board.
			to_user_new_scan: Scan for new (unread) messages to the user (prompt
			                  for current sub-board, current group, or all)
			to_user_new_scan_all: Scan for new (unread) messages to the user
			                      (all sub-boards)
			to_user_new_scan_cur_grp: Scan for new (unread) messages to the user
			                          (current group)
            to_user_new_scan_cur_sub: Scan for new (unread) messages to the user
			                          (current sub-board)
			to_user_all_scan: Scan for all messages to the user (prompt for current
			                  sub-board, current group, or all)
            prompt: Prompt the user for one of several search/scan options to
                    choose from
        Note that if the -personalEmail option is specified (to read personal
        email), the only valid search types are keyword_search and
        from_name_search.
   -suppressSearchTypeText: Disable the search type text that would appear
                            above searches or scans (such as "New To You
                            Message Scan", etc.)
   -startMode: Startup mode.  Available options:
               list (or lister): Message list mode
               read (or reader): Message read mode
   -configFilename: Specifies the name of the configuration file to use.
                    Defaults to DDMsgReader.cfg.
   -personalEmail: Read personal email to the user.  This is a true/false value.
                   It doesn't need to explicitly have a =true or =false afterward;
                   simply including -personalEmail will enable it.  If this is specified,
				   the -chooseAreaFirst and -subBoard options will be ignored.
   -personalEmailSent: Read personal email to the user.  This is a true/false
                       value.  It doesn't need to explicitly have a =true or =false
                       afterward; simply including -personalEmailSent will enable it.
   -allPersonalEmail: Read all personal email (to/from all)
   -userNum: Specify a user number (for the personal email options)
   -chooseAreaFirst: Display the message area chooser before reading/listing
                     messages.  This is a true/false value.  It doesn't need
                     to explicitly have a =true or =false afterward; simply
                     including -chooseAreaFirst will enable it.  If -personalEmail
					 or -subBoard is specified, then this option won't have any
                     effect.
	-subBoard: The sub-board (internal code or number) to read, other than the user's
               current sub-board. If this is specified, the -chooseAreaFirst option
			   will be ignored.
	-verboseLogging: Enable logging to the system log & node log.  Currently, there
	                 isn't much that will be logged, but more log messages could be
					 added in the future.
*/

// - For pageUp & pageDown, enable alternate keys:
//  - When reading a message - scrollTextLines()
//  - When listing messages
//  - When listing message groups & sub-boards for sub-board selection
// - For sub-board area search:
//  - Enable searching in traditional interface
//  - Update the keys in the lightbar help line and traditional interface
// This script requires Synchronet version 3.18 or higher (for mouse hotspot support).
// Exit if the Synchronet version is below the minimum.
	var message = "\x01n\x01h\x01y\x01i* Warning:\x01n\x01h\x01w Digital Distortion Message Reader "
	             + "requires version \x01g3.18\x01w or\r\n"
	             + "higher of Synchronet.  This BBS is using version \x01g" + system.version
	             + "\x01w.  Please notify the sysop.";
	console.crlf();
	console.print(message);
	console.crlf();
	console.pause();
	exit();
}

require("sbbsdefs.js", "K_UPPER");
require("text.js", "Email"); // Text string definitions (referencing text.dat)
require("utf8_cp437.js", "utf8_cp437");
require("userdefs.js", "USER_UTF8");
require("dd_lightbar_menu.js", "DDLightbarMenu");
require("html2asc.js", 'html2asc');
require("attr_conv.js", "convertAttrsToSyncPerSysCfg");
require("graphic.js", 'Graphic');
load('822header.js');
var ansiterm = require("ansiterm_lib.js", 'expand_ctrl_a');


var READER_VERSION = "1.63";
var READER_DATE = "2023-02-01";
// Keyboard key codes for displaying on the screen
var UP_ARROW = ascii(24);
var DOWN_ARROW = ascii(25);
var LEFT_ARROW = ascii(17);
var RIGHT_ARROW = ascii(16);
// Ctrl keys for input
var CTRL_A = "\x01";
var CTRL_B = "\x02";
var CTRL_C = "\x03";
var CTRL_D = "\x04";
var CTRL_E = "\x05";
var CTRL_F = "\x06";
var CTRL_G = "\x07";
var BEEP = CTRL_G;
var CTRL_H = "\x08";
var BACKSPACE = CTRL_H;
var CTRL_I = "\x09";
var TAB = CTRL_I;
var CTRL_J = "\x0a";
var CTRL_K = "\x0b";
var CTRL_L = "\x0c";
var CTRL_M = "\x0d";
var CTRL_N = "\x0e";
var CTRL_O = "\x0f";
var CTRL_P = "\x10";
var CTRL_Q = "\x11";
var XOFF = CTRL_Q;
var CTRL_R = "\x12";
var CTRL_S = "\x13";
var XON = CTRL_S;
var CTRL_T = "\x14";
var CTRL_U = "\x15";
var CTRL_V = "\x16";
var KEY_INSERT = CTRL_V;
var CTRL_W = "\x17";
var CTRL_X = "\x18";
var CTRL_Y = "\x19";
var CTRL_Z = "\x1a";
//var KEY_ESC = "\x1b";
var KEY_ESC = ascii(27);
var KEY_ENTER = CTRL_M;
// 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 = CTRL_P;
var KEY_PAGE_DOWN = 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;
	
// These are defined in sbbsdefs.js:
//var	  KEY_UP		='\x1e';	// ctrl-^ (up arrow)
//var	  KEY_DOWN		='\x0a';	// ctrl-j (dn arrow)
//var   KEY_RIGHT		='\x06';	// ctrl-f (rt arrow)
//var	  KEY_LEFT		='\x1d';	// ctrl-] (lf arrow)
//var	  KEY_HOME		='\x02';	// ctrl-b (home)
//var   KEY_END       ='\x05';	// ctrl-e (end)
//var   KEY_DEL       ='\x7f';    // (del)
// These were added to sbbsdef.js around December 17, 2017:
//var		KEY_PAGEUP	='\x10';	/* ctrl-p (Page Up)							*/
//var		KEY_PAGEDN	='\x0e';	/* ctrl-n (Page Down)						*/

// Characters for display
// Box-drawing/border characters: Single-line
var UPPER_LEFT_SINGLE = "\xDA";
var HORIZONTAL_SINGLE = "\xC4";
var UPPER_RIGHT_SINGLE = "\xBF";
var VERTICAL_SINGLE = "\xB3";
var LOWER_LEFT_SINGLE = "\xC0";
var LOWER_RIGHT_SINGLE = "\xD9";
var T_SINGLE = "\xC2";
var LEFT_T_SINGLE = "\xC3";
var RIGHT_T_SINGLE = "\xB4";
var BOTTOM_T_SINGLE = "\xC1";
var CROSS_SINGLE = "\xC5";
// Box-drawing/border characters: Double-line
var UPPER_LEFT_DOUBLE = "\xC9";
var HORIZONTAL_DOUBLE = "\xCD";
var UPPER_RIGHT_DOUBLE = "\xBB";
var VERTICAL_DOUBLE = "\xBA";
var LOWER_LEFT_DOUBLE = "\xC8";
var LOWER_RIGHT_DOUBLE = "\xBC";
var T_DOUBLE = "\xCB";
var LEFT_T_DOUBLE = "\xCC";
var RIGHT_T_DOUBLE = "\xB9";
var BOTTOM_T_DOUBLE = "\xCA";
var CROSS_DOUBLE = "\xCE";
// Box-drawing/border characters: Vertical single-line with horizontal double-line
var UPPER_LEFT_VSINGLE_HDOUBLE = "\xD5";
var UPPER_RIGHT_VSINGLE_HDOUBLE = "\xB8";
var LOWER_LEFT_VSINGLE_HDOUBLE = "\xD4";
var LOWER_RIGHT_VSINGLE_HDOUBLE = "\xBE";
var THIN_RECTANGLE_LEFT = "\xDD";
var THIN_RECTANGLE_RIGHT = "\xDE";
var BLOCK1 = "\xB0"; // Dimmest block
var BLOCK2 = "\xB1";
var BLOCK3 = "\xB2";
var BLOCK4 = "\xDB"; // Brightest block
var MID_BLOCK = "\xDC";
var TALL_UPPER_MID_BLOCK = "\xFE";
var UPPER_CENTER_BLOCK = "\xDF";
var LOWER_CENTER_BLOCK = "\xDC";


const ERROR_PAUSE_WAIT_MS = 1500;

// Reader mode definitions:
const READER_MODE_LIST = 0;
const READER_MODE_READ = 1;
// Search types
const SEARCH_NONE = -1;
const SEARCH_KEYWORD = 2;
const SEARCH_FROM_NAME = 3;
const SEARCH_TO_NAME_CUR_MSG_AREA = 4;
const SEARCH_TO_USER_CUR_MSG_AREA = 5;
const SEARCH_MSG_NEWSCAN = 6;
const SEARCH_MSG_NEWSCAN_CUR_SUB = 7;
const SEARCH_MSG_NEWSCAN_CUR_GRP = 8;
const SEARCH_MSG_NEWSCAN_ALL = 9;
const SEARCH_TO_USER_NEW_SCAN = 10;
const SEARCH_TO_USER_NEW_SCAN_CUR_SUB = 11;
const SEARCH_TO_USER_NEW_SCAN_CUR_GRP = 12;
const SEARCH_TO_USER_NEW_SCAN_ALL = 13;
const SEARCH_ALL_TO_USER_SCAN = 14;

const THREAD_BY_ID = 15;
const THREAD_BY_TITLE = 16;
const THREAD_BY_AUTHOR = 17;
const THREAD_BY_TO_USER = 18;

const ACTION_NONE = 19;
const ACTION_GO_NEXT_MSG = 20;
const ACTION_GO_PREVIOUS_MSG = 21;
const ACTION_GO_SPECIFIC_MSG = 22;
const ACTION_GO_FIRST_MSG = 23;
const ACTION_GO_LAST_MSG = 24;
const ACTION_DISPLAY_MSG_LIST = 25;
const ACTION_CHG_MSG_AREA = 26;
const ACTION_GO_PREV_MSG_AREA = 27;
const ACTION_GO_NEXT_MSG_AREA = 28;
const ACTION_QUIT = 29;
// Definitions for help line refresh parameters for error functions
const REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE = 0;

// Message list sort types
const MSG_LIST_SORT_DATETIME_RECEIVED = 0;
const MSG_LIST_SORT_DATETIME_WRITTEN = 1;

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

// Strings for the various message attributes (used by makeAllAttrStr(),
// makeMainMsgAttrStr(), makeAuxMsgAttrStr(), and makeNetMsgAttrStr())
var gMainMsgAttrStrs = {
	MSG_DELETE: "Del",
	MSG_PRIVATE: "Priv",
	MSG_READ: "Read",
	MSG_PERMANENT: "Perm",
	MSG_LOCKED: "Lock",
	MSG_ANONYMOUS: "Anon",
	MSG_KILLREAD: "Killread",
	MSG_MODERATED: "Mod",
	MSG_VALIDATED: "Valid",
	MSG_REPLIED: "Repl",
	MSG_NOREPLY: "NoRepl"
};
var gAuxMsgAttrStrs = {
	MSG_FILEREQUEST: "Freq",
	MSG_FILEATTACH: "Attach",
	MSG_KILLFILE: "KillFile",
	MSG_RECEIPTREQ: "RctReq",
	MSG_CONFIRMREQ: "ConfReq",
	MSG_NODISP: "NoDisp"
};
if (typeof(MSG_TRUNCFILE) != "undefined")
	gAuxMsgAttrStrs.MSG_TRUNCFILE = "TruncFile";
var gNetMsgAttrStrs = {
	MSG_LOCAL: "FromLocal",
	MSG_INTRANSIT: "Transit",
	MSG_SENT: "Sent",
	MSG_KILLSENT: "KillSent",
	MSG_ARCHIVESENT: "ArcSent",
	MSG_HOLD: "Hold",
	MSG_CRASH: "Crash",
	MSG_IMMEDIATE: "Now",
	MSG_DIRECT: "Direct"
};
if (typeof(MSG_GATE) != "undefined")
	gNetMsgAttrStrs.MSG_GATE = "Gate";
if (typeof(MSG_ORPHAN) != "undefined")
	gNetMsgAttrStrs.MSG_ORPHAN = "Orphan";
if (typeof(MSG_FPU) != "undefined")
if (typeof(MSG_TYPELOCAL) != "undefined")
	gNetMsgAttrStrs.MSG_TYPELOCAL = "ForLocal";
if (typeof(MSG_TYPEECHO) != "undefined")
	gNetMsgAttrStrs.MSG_TYPEECHO = "ForEcho";
if (typeof(MSG_TYPENET) != "undefined")
	gNetMsgAttrStrs.MSG_TYPENET = "ForNetmail";
if (typeof(MSG_MIMEATTACH) != "undefined")
	gNetMsgAttrStrs.MSG_MIMEATTACH = "MimeAttach";
// A regular expression to check whether a string is an email address
var gEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
// A regular expression to check whether a string is a FidoNet email address
var gFTNEmailRegex = /^.*@[0-9]+:[0-9]+\/[0-9]+$/;
// An array of regular expressions for checking for ANSI codes (globally in a string & ignore case)
var gANSIRegexes = [ new RegExp(ascii(27) + "\[[0-9]+[mM]", "gi"),
                     new RegExp(ascii(27) + "\[[0-9]+(;[0-9]+)+[mM]", "gi"),
                     new RegExp(ascii(27) + "\[[0-9]+[aAbBcCdD]", "gi"),
                     new RegExp(ascii(27) + "\[[0-9]+;[0-9]+[hHfF]", "gi"),
                     new RegExp(ascii(27) + "\[[sSuUkK]", "gi"),
                     new RegExp(ascii(27) + "\[2[jJ]", "gi") ];

// 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(/[\/\\][^\/\\]*$/,''));

// See if we're running in Windows or not.  Until early 2015, the word_wrap()
// function seemed to have a bug where the wrapping length in Linux was one
// less than what it uses in Windows).  That seemed to be fixed in one of the
// Synchronet 3.16 builds in early 2015.
var gRunningInWindows = /^WIN/.test(system.platform.toUpperCase());

// Temporary directory (in the logged-in user's node directory) to store
// file attachments, etc.
var gFileAttachDir = backslash(system.node_dir + "DDMsgReader_Attachments");
// If the temporary attachments directory exists, then delete it (in case the last
// user hung up while running this script, etc.)
if (file_exists(gFileAttachDir))
	deltree(gFileAttachDir);

// See if the avatar support files are available, and load them if so
var gAvatar = null;
if (file_exists(backslash(system.exec_dir) + "load/smbdefs.js") && file_exists(backslash(system.exec_dir) + "load/avatar_lib.js"))
{
	require("smbdefs.js", "SMB_POLL_ANSWER");
// User twitlist filename (and settings filename)
var gUserTwitListFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".DDMsgReader_twitlist";
var gUserSettingsFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".DDMsgReader_Settings";
/////////////////////////////////////////////
// Script execution code

// Parse the command-line arguments
var gCmdLineArgVals = parseArgs(argv);
if (gCmdLineArgVals.exitNow)
	exit(0);
var gAllPersonalEmailOptSpecified = (gCmdLineArgVals.hasOwnProperty("allpersonalemail") && gCmdLineArgVals.allpersonalemail);
// Check to see if the command-line argument for reading personal email is enabled
var gListPersonalEmailCmdLineOpt = ((gCmdLineArgVals.hasOwnProperty("personalemail") && gCmdLineArgVals.personalemail) ||
                                    (gCmdLineArgVals.hasOwnProperty("personalemailsent") && gCmdLineArgVals.personalemailsent) ||
									gAllPersonalEmailOptSpecified);
// If the command-line parameter "search" is specified as "prompt", then
// prompt the user for the type of search to perform.
var gDoDDMR = true; // If the user doesn't choose a search type, this will be set to false
if (gCmdLineArgVals.hasOwnProperty("search") && (gCmdLineArgVals["search"].toLowerCase() == "prompt"))
{
	console.print("\x01cMessage search:");
	console.crlf();
	var allowedKeys = "";
	if (!gListPersonalEmailCmdLineOpt)
	{
		allowedKeys = "ANKFTYUS";
		console.print(" \x01g\x01hN\x01y = \x01n\x01cNew message scan");
		console.print(" \x01g\x01hK\x01y = \x01n\x01cKeyword");
		console.print(" \x01h\x01gF\x01y = \x01n\x01cFrom name");
		console.print(" \x01h\x01gT\x01y = \x01n\x01cTo name");
		console.print(" \x01h\x01gY\x01y = \x01n\x01cTo you");
		console.print(" \x01h\x01gU\x01y = \x01n\x01cUnread (new) messages to you");
		console.print(" \x01h\x01gS\x01y = \x01n\x01cScan for msgs to you");
		console.crlf();
	}
	else
	{
		// Reading personal email - Allow fewer choices
		allowedKeys = "KF";
		console.print(" \x01g\x01hK\x01y = \x01n\x01cKeyword");
		console.print(" \x01h\x01gF\x01y = \x01n\x01cFrom name");
	console.print(" \x01h\x01gA\x01y = \x01n\x01cAbort");
	console.print("\x01n\x01cMake a selection\x01g\x01h: \x01c");
	// TODO: Check to see if keyword & from name search work when reading
	// personal email
	switch (console.getkeys(allowedKeys))
	{
		case "N":
			gCmdLineArgVals["search"] = "new_msg_scan";
			break;
		case "K":
			gCmdLineArgVals["search"] = "keyword_search";
			break;
		case "F":
			gCmdLineArgVals["search"] = "from_name_search";
			break;
		case "T":
			gCmdLineArgVals["search"] = "to_name_search";
			break;
		case "Y":
			gCmdLineArgVals["search"] = "to_user_search";
			break;
		case "U":
			gCmdLineArgVals["search"] = "to_user_new_scan";
			break;
		case "S":
			gCmdLineArgVals["search"] = "to_user_all_scan";
			break;
		case "A": // Abort
		default:
			console.print("\x01n\x01h\x01y\x01iAborted\x01n");
	// Write the user's default twitlist if it doesn't already exist
	writeDefaultUserTwitListIfNotExist();

	// When exiting this script, make sure to set the ctrl key pasthru back to what it was originally
	js.on_exit("console.ctrlkey_passthru = " + console.ctrlkey_passthru);
	// Set a control key pass-thru so we can capture certain control keys that we normally wouldn't be able to
	var gOldCtrlKeyPassthru = console.ctrlkey_passthru; // Backup to be restored later
	console.ctrlkey_passthru = "+ACGKLOPQRTUVWXYZ_";

	// Create an instance of the DigDistMsgReader class and use it to read/list the
	// messages in the user's current sub-board.  Pass the parsed command-line
	// argument values object to its constructor.
	var readerSubCode = (gListPersonalEmailCmdLineOpt ? "mail" : bbs.cursub_code);
	// If the -subBoard option was specified and the "read personal email" option was
	// not specified, then use the sub-board specified by the -subBoard command-line
	// option.
	if (gCmdLineArgVals.hasOwnProperty("subboard") && !gListPersonalEmailCmdLineOpt)
	{
		// If the specified sub-board option is all digits, then treat it as the
		// sub-board number.  Otherwise, treat it as an internal sub-board code.
		if (/^[0-9]+$/.test(gCmdLineArgVals["subboard"]))
			readerSubCode = getSubBoardCodeFromNum(Number(gCmdLineArgVals["subboard"]));
		else
			readerSubCode = gCmdLineArgVals["subboard"];
	}
	var msgReader = new DigDistMsgReader(readerSubCode, gCmdLineArgVals);
		// If the option to choose a message area first was enabled on the command-line
		// (and neither the -subBoard nor the -personalEmail options were specified),
		// then let the user choose a sub-board now.
		if (gCmdLineArgVals.hasOwnProperty("chooseareafirst") && gCmdLineArgVals["chooseareafirst"] && !gCmdLineArgVals.hasOwnProperty("subboard") && !gListPersonalEmailCmdLineOpt)
			msgReader.SelectMsgArea();
		// Back up the user's current sub-board so that we can change back
		// to it after searching is done, if a search is done.
		var originalMsgGrpIdx = bbs.curgrp;
		var originalSubBoardIdx = bbs.cursub;
		var restoreOriginalSubCode = true;
		// Based on the reader's start mode/search type, do the appropriate thing.
		switch (msgReader.searchType)
		{
			case SEARCH_NONE:
				restoreOriginalSubCode = false;
				if (msgReader.subBoardCode != "mail")
				{
					console.print("\x01n");
					console.crlf();
					console.print("Loading " + subBoardGrpAndName(msgReader.subBoardCode) + "....");
					console.line_counter = 0; // To prevent a pause before the message list comes up
				}
				msgReader.ReadOrListSubBoard();
				break;
			case SEARCH_KEYWORD:
				var txtToSearch = (gCmdLineArgVals.hasOwnProperty("searchtext") ? gCmdLineArgVals.searchtext : null);
				var subBoardCode = (gCmdLineArgVals.hasOwnProperty("subboard") ? gCmdLineArgVals.subboard : null);
				msgReader.SearchMsgScan("keyword_search", txtToSearch, subBoardCode);
				break;
			case SEARCH_FROM_NAME:
				msgReader.SearchMessages("from_name_search");
				break;
			case SEARCH_TO_NAME_CUR_MSG_AREA:
				msgReader.SearchMessages("to_name_search");
				break;
			case SEARCH_TO_USER_CUR_MSG_AREA:
				msgReader.SearchMessages("to_user_search");
				break;
			case SEARCH_MSG_NEWSCAN:
				if (!gCmdLineArgVals.suppresssearchtypetext)
				{
					console.crlf();
					console.putmsg(msgReader.text.newMsgScanText);
				}
				msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW);
				break;
			case SEARCH_MSG_NEWSCAN_CUR_SUB:
				msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW, "S");
				break;
			case SEARCH_MSG_NEWSCAN_CUR_GRP:
				msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW, "G");
				break;
			case SEARCH_MSG_NEWSCAN_ALL:
				msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW, "A");
				break;
			case SEARCH_TO_USER_NEW_SCAN:
				if (!gCmdLineArgVals.suppresssearchtypetext)
				{
					console.crlf();
					console.putmsg(msgReader.text.newToYouMsgScanText);
				}
				msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD);
				break;
			case SEARCH_TO_USER_NEW_SCAN_CUR_SUB:
				msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD, "S");
				break;
			case SEARCH_TO_USER_NEW_SCAN_CUR_GRP:
				msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD, "G");
				break;
			case SEARCH_TO_USER_NEW_SCAN_ALL:
				msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD, "A");
				break;
			case SEARCH_ALL_TO_USER_SCAN:
				if (!gCmdLineArgVals.suppresssearchtypetext)
				{
					console.crlf();
					console.putmsg(msgReader.text.allToYouMsgScanText);
				}
				msgReader.MessageAreaScan(SCAN_CFG_TOYOU, SCAN_TOYOU);
				break;
		}

		// If we should restore the user's original message area, then do so.
		if (restoreOriginalSubCode)
		{
			bbs.cursub = 0;
			bbs.curgrp = originalMsgGrpIdx;
			bbs.cursub = originalSubBoardIdx;
		}
	// Remove the temporary attachments & ANSI temp directories if they exists
	deltree(backslash(system.node_dir + "DDMsgReaderANSIMsgTemp"));
	// Before this script finishes, make sure the terminal attributes are set back
	// to normal (in case there are any attributes left on, such as background,
	// blink, etc.)
// Generates an internal enhanced reader header line for the 'To' user.
//
// Parameters:
//  pColors: A JSON object containing the color settings read from the
//           theme configuration file.  This function will use the
//           'msgHdrToColor' or 'msgHdrToUserColor' property, depending
//           on the pToReadingUser property.
//  pToReadingUser: Boolean - Whether or not to generate the line with
//                  the color/attribute for the reading user
//
// Return value: A string containing the internal enhanced reader header
//               line specifying the 'to' user
function genEnhHdrToUserLine(pColors, pToReadingUser)
{
	var toHdrLine = "\x01n\x01h\x01k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3
		          + "\x01gT\x01n\x01go  \x01h\x01c: " +
		          (pToReadingUser ? pColors.msgHdrToUserColor : pColors.msgHdrToColor) +
		          "@MSG_TO-L";
	var numChars = console.screen_columns - 21;
	for (var i = 0; i < numChars; ++i)
		toHdrLine += "#";
	toHdrLine += "@\x01k" + VERTICAL_SINGLE;
///////////////////////////////////////////////////////////////////////////////////
// DigDistMsgReader class stuff

// DigDistMsgReader class constructor: Constructs a
// DigDistMsgReader object, to be used for listing messages
// in a message area.
//
// Parameters:
//  pSubBoardCode: Optional - The Synchronet sub-board code, or "mail"
//                 for personal email.
//  pScriptArgs: Optional - An object containing key/value pairs representing
//               the command-line arguments & values, as returned by parseArgs().
function DigDistMsgReader(pSubBoardCode, pScriptArgs)
{
	// Set the methods for the object
	this.setSubBoardCode = DigDistMsgReader_SetSubBoardCode;
	this.RecalcMsgListWidthsAndFormatStrs = DigDistMsgReader_RecalcMsgListWidthsAndFormatStrs;
	this.NumMessages = DigDistMsgReader_NumMessages;
	this.SearchingAndResultObjsDefinedForCurSub = DigDistMsgReader_SearchingAndResultObjsDefinedForCurSub;
	this.PopulateHdrsForCurrentSubBoard = DigDistMsgReader_PopulateHdrsForCurrentSubBoard;
	this.FilterMsgHdrsIntoHdrsForCurrentSubBoard = DigDistMsgReader_FilterMsgHdrsIntoHdrsForCurrentSubBoard;
	this.GetMsgIdx = DigDistMsgReader_GetMsgIdx;
	this.RefreshSearchResultMsgHdr = DigDistMsgReader_RefreshSearchResultMsgHdr;   // Refreshes a message header in the search results
	this.SearchMessages = DigDistMsgReader_SearchMessages; // Prompts the user for search text, then lists/reads messages, performing the search
	this.SearchMsgScan = DigDistMsgReader_SearchMsgScan;
	this.RefreshHdrInSubBoardHdrs = DigDistMsgReader_RefreshHdrInSubBoardHdrs;
	this.RefreshHdrInSavedArrays = DigDistMsgReader_RefreshHdrInSavedArrays;
	this.ReadMessages = DigDistMsgReader_ReadMessages;
	this.DisplayEnhancedMsgReadHelpLine = DigDistMsgReader_DisplayEnhancedMsgReadHelpLine;
	this.GoToPrevSubBoardForEnhReader = DigDistMsgReader_GoToPrevSubBoardForEnhReader;
	this.GoToNextSubBoardForEnhReader = DigDistMsgReader_GoToNextSubBoardForEnhReader;
	this.SetUpTraditionalMsgListVars = DigDistMsgReader_SetUpTraditionalMsgListVars;
	this.SetUpLightbarMsgListVars = DigDistMsgReader_SetUpLightbarMsgListVars;
	this.ListMessages = DigDistMsgReader_ListMessages;
	this.ListMessages_Traditional = DigDistMsgReader_ListMessages_Traditional;
	this.ListMessages_Lightbar = DigDistMsgReader_ListMessages_Lightbar;
	this.CreateLightbarMsgListMenu = DigDistMsgReader_CreateLightbarMsgListMenu;
	this.CreateLightbarMsgGrpMenu = DigDistMsgReader_CreateLightbarMsgGrpMenu;
	this.CreateLightbarSubBoardMenu = DigDistMsgReader_CreateLightbarSubBoardMenu;
	this.AdjustLightbarMsgListMenuIdxes = DigDistMsgReader_AdjustLightbarMsgListMenuIdxes;
	this.ClearSearchData = DigDistMsgReader_ClearSearchData;
	this.ReadOrListSubBoard = DigDistMsgReader_ReadOrListSubBoard;
	this.PopulateHdrsIfSearch_DispErrorIfNoMsgs = DigDistMsgReader_PopulateHdrsIfSearch_DispErrorIfNoMsgs;
	this.SearchTypePopulatesSearchResults = DigDistMsgReader_SearchTypePopulatesSearchResults;
	this.SearchTypeRequiresSearchText = DigDistMsgReader_SearchTypeRequiresSearchText;
	this.MessageAreaScan = DigDistMsgReader_MessageAreaScan;
	this.PromptContinueOrReadMsg = DigDistMsgReader_PromptContinueOrReadMsg;
	this.WriteMsgListScreenTopHeader = DigDistMsgReader_WriteMsgListScreenTopHeader;
	this.ReadMessageEnhanced = DigDistMsgReader_ReadMessageEnhanced;
	this.ReadMessageEnhanced_Scrollable = DigDistMsgReader_ReadMessageEnhanced_Scrollable;
	this.ScrollableReaderNextReadableMessage = DigDistMsgReader_ScrollableReaderNextReadableMessage;
	this.ScrollReaderDetermineClickCoordAction = DigDistMsgReader_ScrollReaderDetermineClickCoordAction;
	this.ReadMessageEnhanced_Traditional = DigDistMsgReader_ReadMessageEnhanced_Traditional;
	this.EnhReaderPrepLast2LinesForPrompt = DigDistMsgReader_EnhReaderPrepLast2LinesForPrompt;
	this.LookForNextOrPriorNonDeletedMsg = DigDistMsgReader_LookForNextOrPriorNonDeletedMsg;
	this.PrintMessageInfo = DigDistMsgReader_PrintMessageInfo;
	this.ListScreenfulOfMessages = DigDistMsgReader_ListScreenfulOfMessages;
	this.DisplayMsgListHelp = DigDistMsgReader_DisplayMsgListHelp;
	this.DisplayTraditionalMsgListHelp = DigDistMsgReader_DisplayTraditionalMsgListHelp;
	this.DisplayLightbarMsgListHelp = DigDistMsgReader_DisplayLightbarMsgListHelp;
	this.DisplayMessageListNotesHelp = DigDistMsgReader_DisplayMessageListNotesHelp;
	this.SetMsgListPauseTextAndLightbarHelpLine = DigDistMsgReader_SetMsgListPauseTextAndLightbarHelpLine;
	this.SetEnhancedReaderHelpLine = DigDistMsgReader_SetEnhancedReaderHelpLine;
	this.EditExistingMsg = DigDistMsgReader_EditExistingMsg;
	this.EditExistingMessageOldWay = DigDistMsgReader_EditExistingMessageOldWay;
	this.CanDelete = DigDistMsgReader_CanDelete;
	this.CanDeleteLastMsg = DigDistMsgReader_CanDeleteLastMsg;
	this.CanEdit = DigDistMsgReader_CanEdit;
	this.CanQuote = DigDistMsgReader_CanQuote;
	this.ReadConfigFile = DigDistMsgReader_ReadConfigFile;
	this.ReadUserSettingsFile = DigDistMsgReader_ReadUserSettingsFile;
	this.WriteUserSettingsFile = DigDistMsgReader_WriteUserSettingsFile;
	// TODO: Is this.DisplaySyncMsgHeader even needed anymore?  Looks like it's not being called.
	this.DisplaySyncMsgHeader = DigDistMsgReader_DisplaySyncMsgHeader;
	this.GetMsgHdrFilenameFull = DigDistMsgReader_GetMsgHdrFilenameFull;
	this.GetMsgHdrByIdx = DigDistMsgReader_GetMsgHdrByIdx;
	this.GetMsgHdrByMsgNum = DigDistMsgReader_GetMsgHdrByMsgNum;
	this.GetMsgHdrByAbsoluteNum = DigDistMsgReader_GetMsgHdrByAbsoluteNum;
	this.AbsMsgNumToIdx = DigDistMsgReader_AbsMsgNumToIdx;
	this.IdxToAbsMsgNum = DigDistMsgReader_IdxToAbsMsgNum;
	this.NonDeletedMessagesExist = DigDistMsgReader_NonDeletedMessagesExist;
	this.HighestMessageNum = DigDistMsgReader_HighestMessageNum;
	this.IsValidMessageNum = DigDistMsgReader_IsValidMessageNum;
	this.PromptForMsgNum = DigDistMsgReader_PromptForMsgNum;
	this.ParseMsgAtCodes = DigDistMsgReader_ParseMsgAtCodes;
	this.ReplaceMsgAtCodeFormatStr = DigDistMsgReader_ReplaceMsgAtCodeFormatStr;
	this.FindNextNonDeletedMsgIdx = DigDistMsgReader_FindNextNonDeletedMsgIdx;
	this.ChangeSubBoard = DigDistMsgReader_ChangeSubBoard;
	this.EnhancedReaderChangeSubBoard = DigDistMsgReader_EnhancedReaderChangeSubBoard;
	this.ReplyToMsg = DigDistMsgReader_ReplyToMsg;
	this.DoPrivateReply = DigDistMsgReader_DoPrivateReply;
	this.DisplayEnhancedReaderHelp = DigDistMsgReader_DisplayEnhancedReaderHelp;
	this.DisplayEnhancedMsgHdr = DigDistMsgReader_DisplayEnhancedMsgHdr;
	this.DisplayAreaChgHdr = DigDistMsgReader_DisplayAreaChgHdr;
	this.DisplayEnhancedReaderWholeScrollbar = DigDistMsgReader_DisplayEnhancedReaderWholeScrollbar;
	this.UpdateEnhancedReaderScollbar = DigDistMsgReader_UpdateEnhancedReaderScollbar;
	this.MessageIsDeleted = DigDistMsgReader_MessageIsDeleted;
	this.MessageIsLastFromUser = DigDistMsgReader_MessageIsLastFromUser;
	this.DisplayEnhReaderError = DigDistMsgReader_DisplayEnhReaderError;
	this.EnhReaderPromptYesNo = DigDistMsgReader_EnhReaderPromptYesNo;
	this.PromptAndDeleteOrUndeleteMessage = DigDistMsgReader_PromptAndDeleteOrUndeleteMessage;
	this.PromptAndDeleteOrUndeleteSelectedMessages = DigDistMsgReader_PromptAndDeleteOrUndeleteSelectedMessages;
	this.GetExtdMsgHdrInfo = DigDistMsgReader_GetExtdMsgHdrInfo;
	this.GetMsgInfoForEnhancedReader = DigDistMsgReader_GetMsgInfoForEnhancedReader;
	this.GetLastReadMsgIdxAndNum = DigDistMsgReader_GetLastReadMsgIdxAndNum;
	this.GetScanPtrMsgIdx = DigDistMsgReader_GetScanPtrMsgIdx;
	this.RemoveFromSearchResults = DigDistMsgReader_RemoveFromSearchResults;
	this.FindThreadNextOffset = DigDistMsgReader_FindThreadNextOffset;
	this.FindThreadPrevOffset = DigDistMsgReader_FindThreadPrevOffset;
	this.SaveMsgToFile = DigDistMsgReader_SaveMsgToFile;
	this.ToggleSelectedMessage = DigDistMsgReader_ToggleSelectedMessage;
	this.MessageIsSelected = DigDistMsgReader_MessageIsSelected;
	this.AllSelectedMessagesCanBeDeleted = DigDistMsgReader_AllSelectedMessagesCanBeDeleted;
	this.DeleteOrUndeleteSelectedMessages = DigDistMsgReader_DeleteOrUndeleteSelectedMessages;
	this.NumSelectedMessages = DigDistMsgReader_NumSelectedMessages;
	this.ForwardMessage = DigDistMsgReader_ForwardMessage;
	this.VoteOnMessage = DigDistMsgReader_VoteOnMessage;
	this.HasUserVotedOnMsg = DigDistMsgReader_HasUserVotedOnMsg;
	this.GetUpvoteAndDownvoteInfo = DigDistMsgReader_GetUpvoteAndDownvoteInfo;
	this.GetMsgBody = DigDistMsgReader_GetMsgBody;
	this.RefreshMsgHdrInArrays = DigDistMsgReader_RefreshMsgHdrInArrays;
	this.WriteLightbarKeyHelpErrorMsg = DigDistMsgReader_WriteLightbarKeyHelpErrorMsg;
	// startMode specifies the mode for the reader to start in - List mode
	// or reader mode, etc.  This is a setting that is read from the configuration
	// file.  The configuration file can be either READER_MODE_READ or
nightfox's avatar
nightfox committed
	// READER_MODE_LIST, but the optional "mode" parameter in the command-line
	// arguments can specify another mode.
	// hdrsForCurrentSubBoard is an array that will be populated with the
	// message headers for the current sub-board.
	// hdrsForCurrentSubBoardByMsgNum is an object that maps absolute message numbers
	// to their index to hdrsForCurrentSubBoard
	this.hdrsForCurrentSubBoardByMsgNum = {};
	// msgSearchHdrs is an object containing message headers found via searching.
	// It is indexed by internal message area code.  Each internal code index
	// will specify an object containing the following properties:
	//  indexed: A standard 0-based array containing message headers
	this.searchString = ""; // To be used for message searching
	// this.searchType will specify the type of search:
	//  SEARCH_NONE (-1): No search
	//  SEARCH_KEYWORD: Keyword search in message subject & body
	//  SEARCH_FROM_NAME: Search by 'from' name
	//  SEARCH_TO_NAME_CUR_MSG_AREA: Search by 'to' name
	//  SEARCH_TO_USER_CUR_MSG_AREA: Search by 'to' name, to the current user
	//  SEARCH_MSG_NEWSCAN: New (unread) message scan (prompt the user for sub, group, or all)
	//  SEARCH_MSG_NEWSCAN_CUR_SUB: New (unread) message scan (current sub-board)
	//  SEARCH_MSG_NEWSCAN_CUR_GRP: New (unread) message scan (current message group)
	//  SEARCH_MSG_NEWSCAN_ALL: New (unread) message scan (all message sub-boards)
	//  SEARCH_TO_USER_NEW_SCAN: New (unread) messages to the current user (prompt the user for sub, group, or all)
	//  SEARCH_TO_USER_NEW_SCAN_CUR_SUB: New (unread) messages to the current user (current sub-board)
	//  SEARCH_TO_USER_NEW_SCAN_CUR_GRP: New (unread) messages to the current user (current group)
	//  SEARCH_TO_USER_NEW_SCAN_ALL: New (unread) messages to the current user (all sub-board)
	//  SEARCH_ALL_TO_USER_SCAN: All messages to the current user
	this.searchType = SEARCH_NONE;
	this.doingMsgScan = false; // Set to true in MessageAreaScan()

	this.subBoardCode = bbs.cursub_code; // The message sub-board code
	this.readingPersonalEmail = false;

	// this.colors will be an array of colors to use in the message list
	this.colors = getDefaultColors();
	this.readingPersonalEmailFromUser = false;
	if (typeof(pSubBoardCode) == "string")
		var subCodeLowerCase = pSubBoardCode.toLowerCase();
		if (subBoardCodeIsValid(subCodeLowerCase))
		{
			this.setSubBoardCode(subCodeLowerCase);
			if (gCmdLineArgVals.hasOwnProperty("personalemailsent") && gCmdLineArgVals.personalemailsent)
				this.readingPersonalEmailFromUser = true;
		}
	}

	// This property controls whether or not the user will be prompted to
	// continue listing messages after selecting a message to read.  Only for
	// regular reading, not for newscans etc.
	this.promptToContinueListingMessages = false;
	// Whether or not to prompt the user to confirm to read a message
	this.promptToReadMessage = false;
	// For enhanced reader mode (reading only, not for newscan, etc.): Whether or
	// not to ask the user whether to post on the sub-board in reader mode after
	// reading the last message instead of prompting to go to the next sub-board.
	// This is like the stock Synchronet behavior.
	this.readingPostOnSubBoardInsteadOfGoToNext = false;

	// String lengths for the columns to write
	// Fixed field widths: Message number, date, and time
	// TODO: It might be good to figure out the longest message number for a
	// sub-board and set the message number length dynamically.  It would have
	// to change whenever the user changes to a different sub-board, and the
	// message list format string would have to change too.
	//this.MSGNUM_LEN = 4;
	this.MSGNUM_LEN = 5;
	this.DATE_LEN = 10; // i.e., YYYY-MM-DD
	this.TIME_LEN = 8;  // i.e., HH:MM:SS
	// Variable field widths: From, to, and subject
	this.FROM_LEN = (console.screen_columns * (15/console.screen_columns)).toFixed(0);
	this.TO_LEN = (console.screen_columns * (15/console.screen_columns)).toFixed(0);
	//var colsLeftForSubject = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-6; // 6 to account for the spaces
	//this.SUBJ_LEN = (console.screen_columns * (colsLeftForSubject/console.screen_columns)).toFixed(0);
	this.SUBJ_LEN = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-6; // 6 to account for the spaces
	// For showing message scores in the message list
	this.SCORE_LEN = 4;
	// Whether or not to show message scores in the message list: Only if the terminal
	// is at least 86 characters wide and if vote functions exist in the running build
	// of Synchronet
	this.showScoresInMsgList = ((console.screen_columns >= 86) && (typeof((new MsgBase("mail")).vote_msg) === "function"));
	if (this.showScoresInMsgList)
		this.SUBJ_LEN -= (this.SCORE_LEN + 1); // + 1 to account for a space

	// Whether or not the user chose to read a message
	this.readAMessage = false;
	// Whether or not the user denied confirmation to read a message
	this.deniedReadingMessage = false;

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

	// Whether or not to use the scrolling interface when reading a message
	// (will only be used for ANSI terminals).
	this.scrollingReaderInterface = true;

	// reverseListOrder stores whether or not to arrange the message list descending

	// displayBoardInfoInHeader specifies whether or not to display
	// the message group and sub-board lines in the header at the
	// top of the screen (an additional 2 lines).
	this.displayBoardInfoInHeader = false;

	// msgList_displayMessageDateImported specifies whether or not to use the
	// message import date as the date displayed in the message list.  If false,
	// the message written date will be displayed.
	this.msgList_displayMessageDateImported = true;

	// The number of spaces to use for tab characters - Used in the
	// extended read mode
	this.numTabSpaces = 3;

	// this.text is an object containing text used for various prompts & functions.
	this.text = {
		scrollbarBGChar: BLOCK1,
		scrollbarScrollBlockChar: BLOCK2,
		goToPrevMsgAreaPromptText: "\x01n\x01c\x01hGo to the previous message area",
		goToNextMsgAreaPromptText: "\x01n\x01c\x01hGo to the next message area",
		newMsgScanText: "\x01c\x01hN\x01n\x01cew \x01hM\x01n\x01cessage \x01hS\x01n\x01ccan",
		newToYouMsgScanText: "\x01c\x01hN\x01n\x01cew \x01hT\x01n\x01co \x01hY\x01n\x01cou \x01hM\x01n\x01cessage \x01hS\x01n\x01ccan",
		allToYouMsgScanText: "\x01c\x01hA\x01n\x01cll \x01hM\x01n\x01cessages \x01hT\x01n\x01co \x01hY\x01n\x01cou \x01hS\x01n\x01ccan",
		goToMsgNumPromptText: "\x01n\x01cGo to message # (or \x01hENTER\x01n\x01c to cancel)\x01g\x01h: \x01c",
		msgScanAbortedText: "\x01n\x01h\x01cM\x01n\x01cessage scan \x01h\x01y\x01iaborted\x01n",
		deleteMsgNumPromptText: "\x01n\x01cNumber of the message to be deleted (or \x01hENTER\x01n\x01c to cancel)\x01g\x01h: \x01c",
		editMsgNumPromptText: "\x01n\x01cNumber of the message to be edited (or \x01hENTER\x01n\x01c to cancel)\x01g\x01h: \x01c",
		searchingSubBoardAbovePromptText: "\x01n\x01cSearching (current sub-board: \x01b\x01h%s\x01n\x01c)",
		searchingSubBoardText: "\x01n\x01cSearching \x01h%s\x01n\x01c...",
		noMessagesInSubBoardText: "\x01n\x01h\x01bThere are no messages in the area \x01w%s\x01b.",
		noSearchResultsInSubBoardText: "\x01n\x01h\x01bNo messages were found in the area \x01w%s\x01b with the given search criteria.",
		msgScanCompleteText: "\x01n\x01h\x01cM\x01n\x01cessage scan complete\x01h\x01g.\x01n",
		invalidMsgNumText: "\x01n\x01y\x01hInvalid message number: %d",
		readMsgNumPromptText: "\x01n\x01g\x01h\x01i* \x01n\x01cRead message #: \x01h",
		msgHasBeenDeletedText: "\x01n\x01h\x01g* \x01yMessage #\x01w%d \x01yhas been deleted.",
		noKludgeLinesForThisMsgText: "\x01n\x01h\x01yThere are no kludge lines for this message.",
		searchingPersonalMailText: "\x01w\x01hSearching personal mail\x01n",
		searchTextPromptText: "\x01cEnter the search text\x01g\x01h:\x01n\x01c ",
		fromNamePromptText: "\x01cEnter the 'from' name to search for\x01g\x01h:\x01n\x01c ",
		toNamePromptText: "\x01cEnter the 'to' name to search for\x01g\x01h:\x01n\x01c ",
		abortedText: "\x01n\x01y\x01h\x01iAborted\x01n",
		loadingPersonalMailText: "\x01n\x01cLoading %s...",
		msgDelConfirmText: "\x01n\x01h\x01yDelete\x01n\x01c message #\x01h%d\x01n\x01c: Are you sure",
		msgUndelConfirmText: "\x01n\x01h\x01yUndelete\x01n\x01c message #\x01h%d\x01n\x01c: Are you sure",
		delSelectedMsgsConfirmText: "\x01n\x01h\x01yDelete selected messages: Are you sure",
		undelSelectedMsgsConfirmText: "\x01n\x01h\x01yUndelete selected messages: Are you sure",
		msgDeletedText: "\x01n\x01cMessage #\x01h%d\x01n\x01c has been marked for deletion.",
		msgUndeletedText: "\x01n\x01cMessage #\x01h%d\x01n\x01c has been unmarked for deletion.",
		selectedMsgsDeletedText: "\x01n\x01cSelected messages have been marked for deletion.",
		selectedMsgsUndeletedText: "\x01n\x01cSelected messages have been unmarked for deletion.",
		cannotDeleteMsgText_notYoursNotASysop: "\x01n\x01h\x01wCannot delete message #\x01y%d \x01wbecause it's not yours or you're not a sysop.",
		cannotDeleteMsgText_notLastPostedMsg: "\x01n\x01h\x01g* \x01yCannot delete message #%d. You can only delete your last message in this area.\x01n",
		cannotDeleteAllSelectedMsgsText: "\x01n\x01y\x01h* Cannot delete all selected messages",
		msgEditConfirmText: "\x01n\x01cEdit message #\x01h%d\x01n\x01c: Are you sure",
		noPersonalEmailText: "\x01n\x01cYou have no messages.",
		postOnSubBoard: "\x01n\x01gPost on %s %s"


	// These two variables keep track of whether we're doing a message scan that spans
	// multiple sub-boards so that the enhanced reader function can enable use of
	// the > key to go to the next sub-board.
	this.doingMultiSubBoardScan = false;

	// An option for using the scrollable interface for messages with ANSI
	// content - The sysop can set this to false if the sysop thinks the
	// scrolling ANSI interface (using frame.js and scrollbar.js) doesn't
	// look good enough
	this.useScrollingInterfaceForANSIMessages = true;

	// Whether or not to pause (with a message) after doing a new message scan
	this.pauseAfterNewMsgScan = true;

nightfox's avatar
nightfox committed
	// For the message area chooser header filename & maximum number of
	// area chooser header lines to display
	this.areaChooserHdrFilenameBase = "areaChgHeader";
	this.areaChooserHdrMaxLines = 5;
	
	// Some key bindings for enhanced reader mode
	this.enhReaderKeys = {
		reply: "R",
		privateReply: "I",
		editMsg: "E",
		showHelp: "?",
		postMsg: "P",
		nextMsg: KEY_RIGHT,
		previousMsg: KEY_LEFT,
		firstMsg: "F",
		lastMsg: "L",
		showKludgeLines: "K",
		showHdrInfo: "H",
		showMsgList: "M",
		chgMsgArea: "C",
		userEdit: "U",
		quit: "Q",
		prevMsgByTitle: "<",
		nextMsgByTitle: ">",
		prevMsgByAuthor: "{",
		nextMsgByAuthor: "}",
		prevMsgByToUser: "[",
		nextMsgByToUser: "]",
		prevMsgByThreadID: "(",
		nextMsgByThreadID: ")",
		prevSubBoard: "-",
		nextSubBoard: "+",
		downloadAttachments: CTRL_A,
		saveToBBSMachine: CTRL_S,
		deleteMessage: KEY_DEL,
		selectMessage: " ",
		batchDelete: CTRL_D,
		forwardMsg: "O",
		vote: "V",
		threadView: "*" // TODO: Implement this
		this.enhReaderKeys.validateMsg = "A";
	// Some key bindings for the message list (not necessarily all of them)
	this.msgListKeys = {
		deleteMessage: KEY_DEL,
		undeleteMessage: "U",
		batchDelete: CTRL_D,
		editMsg: "E",
		goToMsg: "G",
		chgMsgArea: "C",
nightfox's avatar
nightfox committed

	// Whether or not to display avatars
	this.displayAvatars = true;
	// Message list sort option
	this.msgListSort = MSG_LIST_SORT_DATETIME_RECEIVED;

	// Whether or not to convert Y-style MCI attribute codes to Synchronet attribute codes
	this.convertYStyleMCIAttrsToSync = false;

	// Whether or not to use the scrollbar in the enhanced message reader
	this.useEnhReaderScrollbar = true;

	this.cfgFilename = "DDMsgReader.cfg";
	// Check the command-line arguments for a custom configuration file name
	// before reading the configuration file.
	var scriptArgsIsValid = (typeof(pScriptArgs) == "object");
	if (scriptArgsIsValid && pScriptArgs.hasOwnProperty("configfilename"))
		this.cfgFilename = pScriptArgs["configfilename"];
	// Read the settings from the config file
	this.cfgFileSuccessfullyRead = false;
	this.ReadConfigFile();
	// Set any other values specified by the command-line parameters
	// Reader start mode - Read or list mode
	if (scriptArgsIsValid)
	{
		if (pScriptArgs.hasOwnProperty("startmode"))
		{
			var readerStartMode = readerModeStrToVal(pScriptArgs["startmode"]);
			if (readerStartMode != -1)
				this.startMode = readerStartMode;
		}
		// Search mode
		if (pScriptArgs.hasOwnProperty("search"))
		{
			var searchType = searchTypeStrToVal(pScriptArgs["search"]);
			if (searchType != SEARCH_NONE)
				this.searchType = searchType;
		}
	}
	// Color value adjusting (must be done after reading the config file in case
	// the color settings were changed from defaults)
	// Message list highlight colors: For each (except for the background),
	// prepend the normal attribute and append the background attribute to the end.
	// This is to ensure that high attributes don't affect the rest of the line and
	// the background attribute stays for the rest of the line.
	this.colors.msgListMsgNumHighlightColor = "\x01n" + this.colors.msgListMsgNumHighlightColor + this.colors.msgListHighlightBkgColor;
	this.colors.msgListFromHighlightColor = "\x01n" + this.colors.msgListFromHighlightColor + this.colors.msgListHighlightBkgColor;
	this.colors.msgListToHighlightColor = "\x01n" + this.colors.msgListToHighlightColor + this.colors.msgListHighlightBkgColor;
	this.colors.msgListSubjHighlightColor = "\x01n" + this.colors.msgListSubjHighlightColor + this.colors.msgListHighlightBkgColor;
	this.colors.msgListDateHighlightColor = "\x01n" + this.colors.msgListDateHighlightColor + this.colors.msgListHighlightBkgColor;
	this.colors.msgListTimeHighlightColor = "\x01n" + this.colors.msgListTimeHighlightColor + this.colors.msgListHighlightBkgColor;
	// Similar for the area chooser lightbar highlight colors
	this.colors.areaChooserMsgAreaNumHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaNumHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
	this.colors.areaChooserMsgAreaDescHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaDescHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
	this.colors.areaChooserMsgAreaDateHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaDateHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
	this.colors.areaChooserMsgAreaTimeHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaTimeHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
	this.colors.areaChooserMsgAreaNumItemsHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaNumItemsHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
	// Similar for the enhanced reader help line colors
	this.colors.enhReaderHelpLineGeneralColor = "\x01n" + this.colors.enhReaderHelpLineGeneralColor + this.colors.enhReaderHelpLineBkgColor;
	this.colors.enhReaderHelpLineHotkeyColor = "\x01n" + this.colors.enhReaderHelpLineHotkeyColor + this.colors.enhReaderHelpLineBkgColor;
	this.colors.enhReaderHelpLineParenColor = "\x01n" + this.colors.enhReaderHelpLineParenColor + this.colors.enhReaderHelpLineBkgColor;
	// Similar for the lightbar message list help line colors
	this.colors.lightbarMsgListHelpLineGeneralColor = "\x01n" + this.colors.lightbarMsgListHelpLineGeneralColor + this.colors.lightbarMsgListHelpLineBkgColor;
	this.colors.lightbarMsgListHelpLineHotkeyColor = "\x01n" + this.colors.lightbarMsgListHelpLineHotkeyColor + this.colors.lightbarMsgListHelpLineBkgColor;
	this.colors.lightbarMsgListHelpLineParenColor = "\x01n" + this.colors.lightbarMsgListHelpLineParenColor + this.colors.lightbarMsgListHelpLineBkgColor;
	// Similar for the lightbar area chooser help line colors
	this.colors.lightbarAreaChooserHelpLineGeneralColor = "\x01n" + this.colors.lightbarAreaChooserHelpLineGeneralColor + this.colors.lightbarAreaChooserHelpLineBkgColor;
	this.colors.lightbarAreaChooserHelpLineHotkeyColor = "\x01n" + this.colors.lightbarAreaChooserHelpLineHotkeyColor + this.colors.lightbarAreaChooserHelpLineBkgColor;
	this.colors.lightbarAreaChooserHelpLineParenColor = "\x01n" + this.colors.lightbarAreaChooserHelpLineParenColor + this.colors.lightbarAreaChooserHelpLineBkgColor;
	// Prepend most of the text strings with the normal attribute (if they don't
	// have it already) to make sure the correct colors are used.
	for (var prop in this.text)
	{
		if ((prop != "scrollbarBGChar") && (prop != "scrollbarScrollBlockChar"))
		{
			if ((this.text[prop].length > 0) && (this.text[prop].charAt(0) != "\x01n"))
				this.text[prop] = "\x01n" + this.text[prop];
		}
	}

	// this.tabReplacementText will be the text that tabs will be replaced
	// with in enhanced reader mode
	this.tabReplacementText = format("%" + this.numTabSpaces + "s", "");

	// Calculate the message list widths and format strings based on the current
	// sub-board code and color settings.  Start with a message # field length
	// of 4 characters.  This will be re-calculated later after message headers
	// are loaded.
	this.RecalcMsgListWidthsAndFormatStrs(4);

	// If the user's terminal doesn't support ANSI, then append a newline to
	// the end of the format string (we won't be able to move the cursor).
	if (!canDoHighASCIIAndANSI())
	{
		this.sMsgInfoFormatStr += "\r\n";
		this.sMsgInfoToUserFormatStr += "\r\n";
		this.sMsgInfoFromUserFormatStr += "\r\n";
		this.sMsgInfoFormatHighlightStr += "\r\n";
	}
	// Enhanced reader help line (will be set up in
	// DigDistMsgReader_SetEnhancedReaderHelpLine())
	this.enhReadHelpLine = "";

	// Read the enhanced message header file and populate this.enhMsgHeaderLines,
	// the header text for enhanced reader mode.  The enhanced reader header file
	// name will start with 'enhMsgHeader', and there can be multiple versions for
	// different terminal widths (i.e., msgHeader_80.ans for an 80-column console
	// and msgHeader_132 for a 132-column console).
	this.enhMsgHeaderLines = loadTextFileIntoArray("enhMsgHeader", 10);
	// this.enhMsgHeaderLinesToReadingUser will be a copy of this.endMsgReaderLines
	// but with the 'To' user line changed to highlight the name for messages to
	// the logged-on reading user
	this.enhMsgHeaderLinesToReadingUser = [];
	// If the header file didn't exist, then populate the enhanced reader header
	// array with default lines.
	this.usingInternalEnhMsgHdr = (this.enhMsgHeaderLines.length == 0);
	if (this.usingInternalEnhMsgHdr)
	{
		// Group name: 20% of console width
		// Sub-board name: 34% of console width
		var msgGrpNameLen = Math.floor(console.screen_columns * 0.2);
		var subBoardNameLen = Math.floor(console.screen_columns * 0.34);
		var hdrLine1 = "\x01n\x01h\x01c" + UPPER_LEFT_SINGLE + HORIZONTAL_SINGLE + "\x01n\x01c"
		             + HORIZONTAL_SINGLE + " \x01h@GRP-L";
		var numChars = msgGrpNameLen - 7;
		for (var i = 0; i < numChars; ++i)
			hdrLine1 += "#";
		hdrLine1 += "@ @SUB-L";
		numChars = subBoardNameLen - 7;
		for (var i = 0; i < numChars; ++i)
			hdrLine1 += "#";
		numChars = console.screen_columns - console.strlen(hdrLine1) - 4;
		for (var i = 0; i < numChars; ++i)
			hdrLine1 += HORIZONTAL_SINGLE;
		hdrLine1 += "\x01n\x01c" + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\x01h"
		         + HORIZONTAL_SINGLE + UPPER_RIGHT_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine1);
		this.enhMsgHeaderLinesToReadingUser.push(hdrLine1);
		var hdrLine2 = "\x01n\x01c" + VERTICAL_SINGLE + "\x01h\x01k" + BLOCK1 + BLOCK2
		             + BLOCK3 + "\x01gM\x01n\x01gsg#\x01h\x01c: " + this.colors.msgHdrMsgNumColor + "@MSG_NUM_AND_TOTAL-L";
		numChars = console.screen_columns - 32;
		for (var i = 0; i < numChars; ++i)
			hdrLine2 += "#";
		hdrLine2 += "@\x01n\x01c" + VERTICAL_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine2);
		this.enhMsgHeaderLinesToReadingUser.push(hdrLine2);
		var hdrLine3 = "\x01n\x01h\x01k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3
					 + "\x01gF\x01n\x01grom\x01h\x01c: " + this.colors.msgHdrFromColor + "@MSG_FROM_AND_FROM_NET-L";
		numChars = console.screen_columns - 36;
		for (var i = 0; i < numChars; ++i)
			hdrLine3 += "#";
		hdrLine3 += "@\x01k" + VERTICAL_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine3);
		this.enhMsgHeaderLinesToReadingUser.push(hdrLine3);
		this.enhMsgHeaderLines.push(genEnhHdrToUserLine(this.colors, false));
		this.enhMsgHeaderLinesToReadingUser.push(genEnhHdrToUserLine(this.colors, true));
		var hdrLine5 = "\x01n\x01h\x01k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3
		             + "\x01gS\x01n\x01gubj\x01h\x01c: " + this.colors.msgHdrSubjColor + "@MSG_SUBJECT-L";
		numChars = console.screen_columns - 26;
		for (var i = 0; i < numChars; ++i)
			hdrLine5 += "#";
		hdrLine5 += "@\x01k" + VERTICAL_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine5);
		this.enhMsgHeaderLinesToReadingUser.push(hdrLine5);
		var hdrLine6 = "\x01n\x01c" + VERTICAL_SINGLE + "\x01h\x01k" + BLOCK1 + BLOCK2 + BLOCK3
		             + "\x01gD\x01n\x01gate\x01h\x01c: " + this.colors.msgHdrDateColor + "@MSG_DATE-L";
		//numChars = console.screen_columns - 23;
		numChars = console.screen_columns - 67;
		for (var i = 0; i < numChars; ++i)
			hdrLine6 += "#";
		//hdrLine6 += "@\x01n\x01c" + VERTICAL_SINGLE;
		hdrLine6 += "@ @MSG_TIMEZONE@\x01n";
		for (var i = 0; i < 40; ++i)
			hdrLine6 += " ";
		hdrLine6 += "\x01n\x01c" + VERTICAL_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine6);
		this.enhMsgHeaderLinesToReadingUser.push(hdrLine6);
		var hdrLine7 = "\x01n\x01h\x01c" + BOTTOM_T_SINGLE + HORIZONTAL_SINGLE + "\x01n\x01c"
		             + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\x01h\x01k";
		numChars = console.screen_columns - 8;
		for (var i = 0; i < numChars; ++i)
			hdrLine7 += HORIZONTAL_SINGLE;
		hdrLine7 += "\x01n\x01c" + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\x01h"
		         + HORIZONTAL_SINGLE + BOTTOM_T_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine7);
		this.enhMsgHeaderLinesToReadingUser.push(hdrLine7);
	}
	else
	{
		// We loaded the enhanced message header lines from a custom file.
		// Copy from this.enhMsgHeaderLines to this.enhMsgHeaderLinesToReadingUser
		// but change any 'To:' line to highlight the 'to' username.
		this.enhMsgHeaderLinesToReadingUser = this.enhMsgHeaderLines.slice();
		// Go through the header lines and ensure the 'To' line has a different
		// color
		for (var lineIdx = 0; lineIdx < this.enhMsgHeaderLinesToReadingUser.length; ++lineIdx)
			this.enhMsgHeaderLinesToReadingUser[lineIdx] = syncAttrCodesToANSI(strWithToUserColor(this.enhMsgHeaderLinesToReadingUser[lineIdx], this.colors.msgHdrToUserColor));
	// Save the enhanced reader header width.  This will be the length of the longest
	// line in the header.
	this.enhMsgHeaderWidth = 0;
	if (this.enhMsgHeaderLines.length > 0)
	{
		var lineLen = 0;
		for (var i = 0; i < this.enhMsgHeaderLines.length; ++i)
		{
			lineLen = console.strlen(this.enhMsgHeaderLines[i]);
			if (lineLen > this.enhMsgHeaderWidth)
				this.enhMsgHeaderWidth = lineLen;
		}
	}

	// Message display area information
	this.msgAreaTop = this.enhMsgHeaderLines.length + 1;
	this.msgAreaBottom = console.screen_rows-1;  // The last line of the message area
	// msgAreaLeft and msgAreaRight are the rightmost and leftmost columns of the
	// message area, respectively.  These are 1-based.  1 is subtracted from
	// msgAreaRight to leave room for the scrollbar in enhanced reader mode.
	this.msgAreaLeft = 1;
	this.msgAreaRight = console.screen_columns - 1;
	this.msgAreaWidth = this.msgAreaRight - this.msgAreaLeft + 1;
	this.msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1;

	//////////////////////////////////////////////
	// Things related to changing to a different message group & sub-board

	// In the message area lists (for changing to another message area), the
	// date & time of the last-imported message will be shown.
	// msgAreaList_lastImportedMsg_showImportTime is a boolean to specify
	// whether or not to use the import time for the last-imported message.
	// If false, the message written time will be used.
	this.msgAreaList_lastImportedMsg_showImportTime = true;

	// 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 - this.dateLen - this.timeLen - 7;
	// Message group description length (67 chars on an 80-column screen)
	this.msgGrpDescLen = console.screen_columns - this.areaNumLen - this.numItemsLen - 5;

	// Some methods for choosing the message area
	this.WriteChgMsgAreaKeysHelpLine = DigDistMsgReader_WriteLightbarChgMsgAreaKeysHelpLine;
	this.WriteGrpListHdrLine1 = DigDistMsgReader_WriteGrpListTopHdrLine1;
	this.WriteSubBrdListHdrLine = DigDistMsgReader_WriteSubBrdListHdrLine;
	this.SelectMsgArea = DigDistMsgReader_SelectMsgArea;
	this.SelectMsgArea_Lightbar = DigDistMsgReader_SelectMsgArea_Lightbar;
	this.SelectMsgArea_Traditional = DigDistMsgReader_SelectMsgArea_Traditional;
	this.ListMsgGrps = DigDistMsgReader_ListMsgGrps_Traditional;
	this.ListSubBoardsInMsgGroup = DigDistMsgReader_ListSubBoardsInMsgGroup_Traditional;
	// Lightbar-specific methods
	this.WriteMsgGroupLine = DigDistMsgReader_writeMsgGroupLine;
	this.UpdateMsgAreaPageNumInHeader = DigDistMsgReader_updateMsgAreaPageNumInHeader;
	this.GetMsgSubBoardLine = DigDistMsgReader_GetMsgSubBrdLine;
	// Choose Message Area help screen
	this.ShowChooseMsgAreaHelpScreen = DigDistMsgReader_showChooseMsgAreaHelpScreen;
	// Method to build the sub-board printf information for a message
	// group
	this.BuildSubBoardPrintfInfoForGrp = DigDistMsgReader_BuildSubBoardPrintfInfoForGrp;
	// Methods for calculating a page number for a message list item
	this.CalcTraditionalMsgListTopIdx = DigDistMsgReader_CalcTraditionalMsgListTopIdx;
	this.CalcLightbarMsgListTopIdx = DigDistMsgReader_CalcLightbarMsgListTopIdx;
	this.CalcMsgListScreenIdxVarsFromMsgNum = DigDistMsgReader_CalcMsgListScreenIdxVarsFromMsgNum;
	// A method for validating a user's choice of message area
	this.ValidateMsgAreaChoice = DigDistMsgReader_ValidateMsgAreaChoice;
	this.ValidateMsg = DigDistMsgReader_ValidateMsg;
	this.GetGroupNameAndDesc = DigDistMsgReader_GetGroupNameAndDesc;
	this.DoUserSettings_Scrollable = DigDistMsgReader_DoUserSettings_Scrollable;
	this.DoUserSettings_Traditional = DigDistMsgReader_DoUserSettings_Traditional;
	this.RefreshMsgAreaRectangle = DigDistMsgReader_RefreshMsgAreaRectangle;
	this.MsgHdrFromOrToInUserTwitlist = DigDistMsgReader_MsgHdrFromOrToInUserTwitlist;
	// For indexed mode
	this.DoIndexedMode = DigDistMsgReader_DoIndexedMode;
	this.DoIndexedModeLightbar = DigDistMsgReader_DoIndexedModeLightbar;
	this.DoIndexedModeTraditional = DigDistMsgReader_DoIndexedModeTraditional;
	this.MakeLightbarIndexedModeMenu = DigDistMsgReader_MakeLightbarIndexedModeMenu;

	// printf strings for message group/sub-board lists
	// Message group information (printf strings)
	this.msgGrpListPrintfStr = "\x01n " + this.colors.areaChooserMsgAreaNumColor + "%" + this.areaNumLen
	                         + "d " + this.colors.areaChooserMsgAreaDescColor + "%-"
	                         + this.msgGrpDescLen + "s " + this.colors.areaChooserMsgAreaNumItemsColor
	                         + "%" + this.numItemsLen + "d";
	this.msgGrpListHilightPrintfStr = "\x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor + " "
	                                + "\x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor
	                                + this.colors.areaChooserMsgAreaNumHighlightColor + "%" + this.areaNumLen
	                                + "d \x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor
	                                + this.colors.areaChooserMsgAreaDescHighlightColor + "%-"
	                                + this.msgGrpDescLen + "s \x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor
	                                + this.colors.areaChooserMsgAreaNumItemsHighlightColor + "%" + this.numItemsLen
	                                + "d";
	// Message group list header (printf string)
	this.msgGrpListHdrPrintfStr = this.colors.areaChooserMsgAreaHeaderColor + "%6s %-"
	                            + +(this.msgGrpDescLen-8) + "s %-12s";
	// Sub-board information header (printf string)
	this.subBoardListHdrPrintfStr = this.colors.areaChooserMsgAreaHeaderColor + " %5s %-"
	                              + +(this.subBoardNameLen-3) + "s %-7s %-19s";
	// Lightbar area chooser help line text
	// For PageUp, normally I'd think KEY_PAGEUP should work, but that triggers sending a telegram instead.  \x1b[V seems to work though.
	this.lightbarAreaChooserHelpLine = "\x01n"
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@CLEAR_HOT@@`" + UP_ARROW + "`" + KEY_UP + "@"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`END`" + KEY_END + "@"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "#"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + "/"
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`Dn`" + KEY_PAGEDN + "@"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`F`F@"
	                          + this.colors.lightbarAreaChooserHelpLineParenColor + ")"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + "irst pg, "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`L`L@"
	                          + this.colors.lightbarAreaChooserHelpLineParenColor + ")"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + "ast pg, "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`CTRL-F`" + CTRL_F + "@"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`/`/@"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`N`N@"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`Q`Q@"
	                          + this.colors.lightbarAreaChooserHelpLineParenColor + ")"
	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + "uit, "
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`?`?@";
	var lbAreaChooserHelpLineLen = 72;
	// 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 padLen = console.screen_columns - lbAreaChooserHelpLineLen - 1;
	var leftPadLen = Math.floor(padLen/2);
	this.lightbarAreaChooserHelpLine = this.colors.lightbarAreaChooserHelpLineGeneralColor
	                                 + format("%" + leftPadLen + "s", "")
	                                 + this.lightbarAreaChooserHelpLine
	                                 + this.colors.lightbarAreaChooserHelpLineGeneralColor
	                                 + format("%" + rightPadLen + "s", "") + "\x01n";
	// 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.

	// Variables to save the top message index for the traditional & lightbar
	// message lists.  Initialize them to -1 to mean the message list hasn't been
	// displayed yet - In that case, the lister will use the user's last
	// read pointer.
	this.tradListTopMsgIdx = -1;
	this.tradMsgListNumLines = console.screen_rows-3;
	if (this.displayBoardInfoInHeader)
		this.tradMsgListNumLines -= 2;
	this.lightbarListTopMsgIdx = -1;
	this.lightbarMsgListNumLines = console.screen_rows-2;
	this.lightbarMsgListStartScreenRow = 2; // The first line number on the screen for the message list
	// If we will be displaying the message group and sub-board in the
	// header at the top of the screen (an additional 2 lines), then
	// update this.lightbarMsgListNumLines and this.lightbarMsgListStartScreenRow to
	// account for this.
	if (this.displayBoardInfoInHeader)
	{
		this.lightbarMsgListNumLines -= 2;
		this.lightbarMsgListStartScreenRow += 2;
	}
	// The selected message index for the lightbar message list (initially -1, will
	// be set in the lightbar list method)
	this.lightbarListSelectedMsgIdx = -1;
	// The selected message cursor position for the lightbar message list (initially
	// null, will be set in the lightbar list message)
	this.lightbarListCurPos = null;

	// selectedMessages will be an object (indexed by sub-board internal code)
	// containing objects that contain message indexes (as properties) for the
	// sub-boards.  Messages can be selected by the user for doing things such
	// as a batch delete, etc.
nightfox's avatar
nightfox committed

	// 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);

	// pausePromptText is the text that will be used for some of the pause
	// prompts.  It's loaded from text.dat, but in case that text contains
	// "@EXEC:" (to execute a script), this script will default to a "press
	// a key" message.
	this.pausePromptText = bbs.text(Pause);
	if (this.pausePromptText.toUpperCase().indexOf("@EXEC:") > -1)
		this.pausePromptText = "\x01n\x01c[ Press a key ] ";
}

// For the DigDistMsgReader class: Sets the subBoardCode property and also
// sets the readingPersonalEmail property, a boolean for whether or not
// personal email is being read (whether the sub-board code is "mail")
//
// Parameters:
//  pSubCode: The sub-board code to set in the object
function DigDistMsgReader_SetSubBoardCode(pSubCode)
{
	this.subBoardCode = pSubCode;
	this.readingPersonalEmail = (this.subBoardCode.toLowerCase() == "mail");
// For the DigDistMsgReader class: Populates the hdrsForCurrentSubBoard
// array with message headers from the current sub-board.  Filters out
// messages that are deleted, unvalidated, private, and voting messages.
function DigDistMsgReader_PopulateHdrsForCurrentSubBoard()
{
	if (this.subBoardCode == "mail")
		this.hdrsForCurrentSubBoardByMsgNum = {};
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
		// First get all headers in a temporary array, then filter them into
		// this.hdrsForCurrentSubBoard.
		// Note: get_all_msg_headers() was added in Synchronet 3.16.  DDMsgReader requires a minimum
		// of 3.18, so we're okay to use it.
		// The first parameter is whether to include votes (the parameter was introduced in Synchronet 3.17+).
		// We used to pass false here.
		tmpHdrs = msgbase.get_all_msg_headers(true);

	// Filter the headers into this.hdrsForCurrentSubBoard
	this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpHdrs, true);
}

// For the DigDistMsgReader class: Takes an array of message headers in the current
// sub-board and filters them into this.hdrsForCurrentSubBoard and
// this.hdrsForCurrentSubBoardByMsgNum based on which messages are readable to the
// user.
//
// Parameters:
//  pMsgHdrs: An array/object of message header objects
//  pClearFirst: Boolean - Whether or not to empty this.hdrsForCurrentSubBoard
//               and this.hdrsForCurrentSubBoardByMsgNum first.
function DigDistMsgReader_FilterMsgHdrsIntoHdrsForCurrentSubBoard(pMsgHdrs, pClearFirst)
{
	if (pClearFirst)
	{
		this.hdrsForCurrentSubBoard = [];
		this.hdrsForCurrentSubBoardByMsgNum = {};
		// Only add the message header if the message is readable to the user
		// and the from & to name isn't in the user's personal twitlist.
		// this.hdrsForCurrentSubBoardByMsgNum also has to be populated, but
		// that's done later in this function, in case this.hdrsForCurrentSubBoard
		// needs to be sorted.
		if (isReadableMsgHdr(pMsgHdrs[prop], this.subBoardCode) && !this.MsgHdrFromOrToInUserTwitlist(pMsgHdrs[prop]))
		{
			this.hdrsForCurrentSubBoard.push(pMsgHdrs[prop]);
			// This isn't done right here anymore due to the possibility of
			// this.hdrsForCurrentSubBoard being sorted
			//this.hdrsForCurrentSubBoardByMsgNum[pMsgHdrs[prop].number] = this.hdrsForCurrentSubBoard.length - 1;

	// If the sort type is date/time written, then sort the message header
	// array as such
	if (this.msgListSort == MSG_LIST_SORT_DATETIME_WRITTEN)
		this.hdrsForCurrentSubBoard.sort(sortMessageHdrsByDateTime);

	// Populate this.hdrsForCurrentSubBoardByMsgNum (this needs to be done here
	// based on the order of this.hdrsForCurrentSubBoard)
	for (var idx = 0; idx < this.hdrsForCurrentSubBoard.length; ++idx)
		this.hdrsForCurrentSubBoardByMsgNum[this.hdrsForCurrentSubBoard[idx].number] = idx;
}

// For the DigDistMsgReader class: Gets the message offset (index) for a message, given
// a message header.  Returns -1 on failure.  The returned index is for the object's
// message header array(s), if populated, in the priority of search headers, then
// hdrsForCurrentSubBoard.  If neither of those are populated, the offset of the header
// in the messagebase will be returned.
//
// Parameters:
//  pHdrOrMsgNum: Can either be a message header object or a message number.
//
// Return value: The message index (or offset in the messagebase)
function DigDistMsgReader_GetMsgIdx(pHdrOrMsgNum)
{
	if (typeof(pHdrOrMsgNum) == "object")
		msgNum = pHdrOrMsgNum.number;
	else if (typeof(pHdrOrMsgNum) == "number")
		msgNum = pHdrOrMsgNum;
	else
	var msgIdx = 0;
	if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
	    (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0))
	{
		for (var i = 0; i < this.msgSearchHdrs[this.subBoardCode].indexed.length; ++i)
		{
			if (this.msgSearchHdrs[this.subBoardCode].indexed[i].number == msgNum)
			{
				msgIdx = i;
				break;
			}
		}
	}
	else if (this.hdrsForCurrentSubBoard.length > 0)
		if (this.hdrsForCurrentSubBoardByMsgNum.hasOwnProperty(msgNum))
			msgIdx = this.hdrsForCurrentSubBoardByMsgNum[msgNum];
		else
		{
			msgIdx = msgNumToIdxFromMsgbase(this.subBoardCode, msgNum);
			if (msgIdx != -1)
				this.hdrsForCurrentSubBoardByMsgNum[msgNum] = msgIdx;
		}
		msgIdx = msgNumToIdxFromMsgbase(this.subBoardCode, msgNum);
	return msgIdx;
}

// Given a sub-board code and message number, this function gets the index
// of that message from the Synchronet messagebase.  Returns -1 if not found.
//
// Parameters:
//  pSubCode: The sub-board code
//  pMsgNum: The message number
//
// Return value: The index of the message, or -1 if not found.
function msgNumToIdxFromMsgbase(pSubCode, pMsgNum)
{
	var msgIdx = -1;

	var msgbase = new MsgBase(pSubCode);
	if (msgbase.open())
		var msgHdr =  msgbase.get_msg_header(false, pMsgNum, false);
		if (msgHdr != null)
			msgIdx = msgHdr.offset;
		msgbase.close();
	return msgIdx;
}

// For the DigDistMsgReader class: Refreshes a message header in the message header
// arrays in this.msgSearchHdrs.
//
// Parameters:
//  pMsgIndex: The index (0-based) of the message header
//  pAttrib: Optional - An attribute to apply.  If this is is not specified,
//           then the message header will be retrieved from the message base.
//  pApply: Optional boolean - Whether or not to apply the attribute or remove it. Defaults to true.
//  pSubBoardCode: Optional - An internal sub-board code.  If not specified, then
//                 this method will default to this.subBoardCode.
function DigDistMsgReader_RefreshSearchResultMsgHdr(pMsgIndex, pAttrib, pApply, pSubBoardCode)
	if (typeof(pMsgIndex) != "number")
		return;
	var applyAttr = (typeof(pApply) === "boolean" ? pApply : true);
	var subCode = (typeof(pSubBoardCode) == "string" ? pSubBoardCode : this.subBoardCode);
	var msgbase = new MsgBase(subCode);
	if (msgbase.open())
		if (this.msgSearchHdrs.hasOwnProperty(subCode))
			var msgNum = pMsgIndex + 1;
			if (typeof(pAttrib) != "undefined")
				if (this.msgSearchHdrs[this.subBoardCode].indexed.hasOwnProperty(pMsgIndex))
				{
					if (applyAttr)
						this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr | pAttrib;
					else
						this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr ^ pAttrib;
					var msgOffsetFromHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].offset;
					msgbase.put_msg_header(true, msgOffsetFromHdr, this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex]);
				}
				var msgHeader = this.GetMsgHdrByIdx(pMsgIndex);
				if (this.msgSearchHdrs[this.subBoardCode].indexed.hasOwnProperty(pMsgIndex))
				{
					this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex] = msgHeader;
					msgbase.put_msg_header(true, msgHeader.offset, msgHeader);
				}
// For the DigDistMsgReader class: Refreshes a message header in the message header
// array for the current sub-board.
//
// Parameters:
//  pMsgIndex: The index (0-based) of the message header
//  pAttrib: Optional - An attribute to apply.  If this is is not specified,
//           then the message header will be retrieved from the message base.
//  pApply: Optional boolean - Whether or not to apply the attribute or remove it. Defaults to true.
function DigDistMsgReader_RefreshHdrInSubBoardHdrs(pMsgIndex, pAttrib, pApply)
	if ((pMsgIndex >= 0) && (pMsgIndex < this.hdrsForCurrentSubBoard.length))
	{
		var applyAttr = (typeof(pApply) === "boolean" ? pApply : true);
		if (applyAttr)
			this.hdrsForCurrentSubBoard[pMsgIndex].attr = this.hdrsForCurrentSubBoard[pMsgIndex].attr | pAttrib;
		else
			this.hdrsForCurrentSubBoard[pMsgIndex].attr = this.hdrsForCurrentSubBoard[pMsgIndex].attr ^ pAttrib;
	}
}

// For the DigDistMsgReader class: Refreshes a message header in the saved message
// header arrays.
//
// Parameters:
//  pMsgIndex: The index (0-based) of the message header
//  pAttrib: Optional - An attribute to apply.  If this is is not specified,
//           then the message header will be retrieved from the message base.
//  pApply: Optional boolean - Whether or not to apply the attribute or remove it. Defaults to true.
//  pSubBoardCode: Optional - An internal sub-board code.  If not specified, then
//                 this method will default to this.subBoardCode.
function DigDistMsgReader_RefreshHdrInSavedArrays(pMsgIndex, pAttrib, pApply, pSubBoardCode)
	var applyAttr = (typeof(pApply) === "boolean" ? pApply : true);
	this.RefreshSearchResultMsgHdr(pMsgIndex, pAttrib, applyAttr, pSubBoardCode);
	this.RefreshHdrInSubBoardHdrs(pMsgIndex, pAttrib, applyAttr);
// For the DigDistMsgReader class: Inputs search text from the user, then reads/lists
// messages, which will perform the search.
//
// Paramters:
//  pSearchModeStr: A string to specify the lister mode to use - This can
//                  be one of the search modes to specify how to search:
//                  "keyword_search": Search the message subjects & bodies by keyword
//                  "from_name_search": Search messages by from name
//                  "to_name_search": Search messages by to name
//                  "to_user_search": Search messages by to name, to the logged-in user
//  pSubBoardCode: Optional - The Synchronet sub-board code, or "mail"
//                 for personal email.  Or, this can be the a boolean false for scan
//                 search mode to scan through sub-boards while searching each of them.
//  pScanScopeChar: Optional string with a character specifying "A" to scan all sub-boards,
//                  "G" for the current message group, or "S" for the user's current sub-board.
//                  If this is not specified, the current sub-board will be used.
//  pTxtToSearch: Optional - Text to search for (if specified, this won't prompt the user for search text)
//  pSkipSubBoardScanCfgCheck: Optional boolean - Whether or not to skip the sub-board scan config check for
//                                                  each sub-board.  Defaults to false.
function DigDistMsgReader_SearchMessages(pSearchModeStr, pSubBoardCode, pScanScopeChar, pTxtToSearch, pSkipSubBoardScanCfgCheck)
	var searchTextProvided = (typeof(pTxtToSearch) === "string" && pTxtToSearch != "");
	var skipSubBoardScanCfgCheck = (typeof(pSkipSubBoardScanCfgCheck) === "boolean" ? pSkipSubBoardScanCfgCheck : false);
	// Convert the search mode string to an integer representing the search
	// mode.  If we get back -1, that means the search mode string was invalid.
	// If that's the case, simply list messages.  Otherwise, do the search.
	this.searchType = searchTypeStrToVal(pSearchModeStr);
	if (this.searchType == SEARCH_NONE) // No search; search mode string was invalid
	{
		// Clear the search information and read/list messages.
		this.ClearSearchData();
		this.ReadOrListSubBoard(pSubBoardCode);
	}
	else
	{
		// The search mode string was valid, so go ahead and search.
		var subCode = "";
		if (typeof(pScanScopeChar) !== "string")
		{
			subCode = (typeof(pSubBoardCode) === "string" ? pSubBoardCode : this.subBoardCode);
			if (subCode == "mail")
			{
				//console.print("\x01n" + replaceAtCodesInStr(this.text.searchingPersonalMailText));
				console.putmsg("\x01n" + this.text.searchingPersonalMailText);
			}
			else
			{
				var formattedText = format(this.text.searchingSubBoardAbovePromptText, subBoardGrpAndName(bbs.cursub_code));
				//console.print("\x01n" + replaceAtCodesInStr(formattedText) + "\x01n");
				console.putmsg("\x01n" + formattedText + "\x01n");
		// Output the prompt text to the user (for modes where a prompt is needed)
		switch (this.searchType)
		{
				if (!searchTextProvided)
				{
					//console.print("\x01n" + replaceAtCodesInStr(this.text.searchTextPromptText));
					console.putmsg("\x01n" + this.text.searchTextPromptText);
				}
					console.print("\x01n\x01gSearching for: \x01c" + pTxtToSearch + "\x01n\r\n");
				if (!searchTextProvided)
				{
					//console.print("\x01n" + replaceAtCodesInStr(this.text.fromNamePromptText));
					console.putmsg("\x01n" + this.text.fromNamePromptText);
				}
					console.print("\x01n\x01gSearching for: \x01c" + pTxtToSearch + "\x01n\r\n");
				if (!searchTextProvided)
				{
					//console.print("\x01n" + replaceAtCodesInStr(this.text.toNamePromptText));
					console.putmsg("\x01n" + this.text.toNamePromptText);
				}
					console.print("\x01n\x01gSearching for: \x01c" + pTxtToSearch + "\x01n\r\n");
				break;
			case SEARCH_TO_USER_CUR_MSG_AREA:
				// Note: No prompt needed for this - Will search for the user's name/handle
				console.line_counter = 0; // To prevent a pause before the message list comes up
				break;
			default:
				break;
		}
		//var promptUserForText = this.SearchTypePopulatesSearchResults();
		var promptUserForText = this.SearchTypeRequiresSearchText();
		// Get the search text from the user
		if (promptUserForText)
		{
			if (searchTextProvided)
				this.searchString = pTxtToSearch;
			else
				this.searchString = console.getstr(512, K_UPPER);
		}
		// If the user was prompted for search text but no search text was entered,
		// then show an abort message and don't do anything.  Otherwise, go ahead
		// and list/read messages.
		if (promptUserForText && (this.searchString.length == 0))
		{
			this.ClearSearchData();
			//console.print("\x01n" + replaceAtCodesInStr(this.text.abortedText));
			//console.crlf();
			console.putmsg("\x01n" + this.text.abortedText);
			// If pScanScopeChar is a string, then do scan search mode.  Otherwise,
			// scan/search the current sub-board.
			if (typeof(pScanScopeChar) === "string" && (pScanScopeChar === "S" || pScanScopeChar === "G" || pScanScopeChar === "A"))
			{
				var subBoardCodeBackup = this.subBoardCode;
				var subBoardsToScan = getSubBoardsToScanArray(pScanScopeChar);
				this.doingMsgScan = true;
				var continueScan = true;
				var userAborted = false;
				this.doingMultiSubBoardScan = (subBoardsToScan.length > 1);
				// If the sub-board's access requirements allows the user to read it
				// and it's enabled in the user's message scan configuration, then go
				// ahead with this sub-board.
				// Note: Used to use this to determine whether the user could access the
				// sub-board:
				//user.compare_ars(msg_area.grp_list[grpIndex].sub_list[subIndex].ars)
				// Now using the can_read property.
				for (var subCodeIdx = 0; (subCodeIdx < subBoardsToScan.length) && continueScan; ++subCodeIdx)
				{
					subCode = subBoardsToScan[subCodeIdx];
					if (skipSubBoardScanCfgCheck || (msg_area.sub[subCode].can_read && ((msg_area.sub[subCode].scan_cfg & SCAN_CFG_NEW) == SCAN_CFG_NEW)))
					{
						// Force garbage collection to ensure enough memory is available to continue
						js.gc(true);
						// Set the console line counter to 0 to prevent screen pausing
						// when the "Searching ..." and "No messages were found" text is
						// displayed repeatedly
						console.line_counter = 0;
						// let the user read the sub-board (and toggle betweeen reading and
						// listing)
						var readOrListRetObj = this.ReadOrListSubBoard(subCode, null, true, true, false, true, READER_MODE_READ);
						console.crlf();
						//if (this.SearchTypePopulatesSearchResults())
						//	console.print("\x01n\r\nSearching...");
						console.line_counter = 0;
						if (readOrListRetObj.stoppedReading)
							break;
					}
				}
				this.subBoardCode = subBoardCodeBackup;
			// Clear the search data so that subsequent listing or reading sessions
			// don't repeat the same search
			this.ClearSearchData();
		}
	}
// For the DigDistMsgReader class: Performs a message search scan through sub-boards.
// Prompts the user for Sub-board/Group/All, then inputs search text from the user, then
// reads/lists messages through the sub-boards, performing the search in each sub-board.
//
// Paramters:
//  pSearchModeStr: A string to specify the lister mode to use - This can
//                  be one of the search modes to specify how to search:
//                  "keyword_search": Search the message subjects & bodies by keyword
//                  "from_name_search": Search messages by from name
//                  "to_name_search": Search messages by to name
//                  "to_user_search": Search messages by to name, to the logged-in user
//  pTxtToSearch: Optional - Text to search for (if specified, this won't prompt the user for search text)
//  pSubCode: Optional - An internal code of a sub-board if scanning just one sub-board
function DigDistMsgReader_SearchMsgScan(pSearchModeStr, pTxtToSearch, pSubCode)
{
	if (typeof(pSearchModeStr) !== "string" || pSearchModeStr.length == 0)
		return;

	// If the given sub-board code is valid, then use that and scan only in that
	// sub-board.  Otherwise, prompt the user for sub-board, group, or all, then
	// call SearchMessages to do the search.
	var scanScopeChar = "";
	var previousSubBoardCode = null;
	if (typeof(pSubCode) === "string" && subBoardCodeIsValid(pSubCode))
	{
		var previousSubBoardCode = this.subBoardCode;
		this.subBoardCode = pSubCode;
		scanScopeChar = "S";
	}
	else
	{
		console.mnemonics(bbs.text(SubGroupOrAll));
		scanScopeChar = console.getkeys("SGAC").toString();
	}
		this.SearchMessages(pSearchModeStr, null, scanScopeChar, pTxtToSearch, true); // Skip/ignore scan config checks
		//console.print(replaceAtCodesInStr(this.text.msgScanAbortedText));
		//console.crlf();
		console.putmsg(this.text.msgScanAbortedText);
	// Restore this.subBoardCode if necessary
	if (typeof(previousSubBoardCode) === "string")
		this.subBoardCode = previousSubBoardCode;
// This function clears the search data from the object.
function DigDistMsgReader_ClearSearchData()
{
   this.searchType = SEARCH_NONE;
   this.searchString == "";
   if (this.msgSearchHdrs != null)
   {
		for (var subCode in this.msgSearchHdrs)
		{
			delete this.msgSearchHdrs[subCode].indexed;
			delete this.msgSearchHdrs[subCode];
		}
		delete this.msgSearchHdrs;
   }
}

// For the DigDistMsgReader class: Performs message reading/listing.
// Depending on the value of this.startMode, starts in either reader
// mode or lister mode.  Uses an input loop to let the user switch
// between the two modes.
//
// Parameters:
//  pSubBoardCode: Optional - The internal code of a sub-board to read.
//                 If not specified, the internal sub-board code specified
//                 when creating the object will be used.
//  pStartingMsgOffset: Optional - The offset of a message to start at
//  pAllowChgArea: Optional boolean - Whether or not to allow changing the
//                 message area
//  pReturnOnNextAreaNav: Optional boolean - Whether or not this method should
//                        return when it would move to the next message area due
//                        navigation from the user (i.e., with the right arrow key)
//  pPauseOnNoMsgSrchResults: Optional boolean - Whether or not to pause when
//                            a message search doesn't find any search results
//                            in the current sub-board.  Defaults to true.
//  pPromptToGoNextIfNoResults: Optional boolean - Whether or not to prompt the user
//                         to go onto the next/previous sub-board if there are no
//                         search results in the current sub-board.  Defaults to true.
//  pInitialModeOverride: Optional (numeric) to override the initial mode in this
//                        function (READER_MODE_READ or READER_MODE_LIST).  If not
//                        specified, defaults to this.startMode.
//
// Return value: An object with the following properties:
//               stoppedReading: Boolean - Whether or not the user stopped reading.
//                               This can also be true if there is an error.
function DigDistMsgReader_ReadOrListSubBoard(pSubBoardCode, pStartingMsgOffset,
                                             pAllowChgArea, pReturnOnNextAreaNav,
	// Set the sub-board code if applicable
	var previousSubBoardCode = this.subBoardCode;
	if (typeof(pSubBoardCode) == "string")
	{
		if (subBoardCodeIsValid(pSubBoardCode))
			this.setSubBoardCode(pSubBoardCode);
		else
		{
			console.print("\x01n\x01h\x01yWarning: \x01wThe Message Reader connot continue because an invalid");
			console.crlf();
			console.print("sub-board code was specified (" + pSubBoardCode + "). Please notify the sysop.");
			console.crlf();
			console.pause();
			retObj.stoppedReading = true;
			return retObj;
		}
	}

	// If the user doesn't have permission to read the current sub-board, then
	// don't allow the user to read it.
		if (!msg_area.sub[this.subBoardCode].can_read)
		{
			var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name);
			console.print("\x01n" + errorMsg);
			console.pause();
			retObj.stoppedReading = true;
			return retObj;
	}

	// Populate this.msgSearchHdrs for the current sub-board if there is a search
	// specified.  If there are no messages to read in the current sub-board, then
	// just return.
	var pauseOnNoSearchResults = (typeof(pPauseOnNoMsgSrchResults) == "boolean" ? pPauseOnNoMsgSrchResults : true);
	if (!this.PopulateHdrsIfSearch_DispErrorIfNoMsgs(true, true, pauseOnNoSearchResults))
	{
		retObj.stoppedReading = false;
		return retObj;
	}
	// If not searching, then populate the array of all readable headers for the
	// current sub-board.
	if (!this.SearchingAndResultObjsDefinedForCurSub())
		this.PopulateHdrsForCurrentSubBoard();

	// Check the pAllowChgArea parameter.  If it's a boolean, then use it.  If
	// not, then check to see if we're reading personal mail - If not, then allow
	// the user to change to a different message area.
	var allowChgMsgArea = true;
	if (typeof(pAllowChgArea) == "boolean")
		allowChgMsgArea = pAllowChgArea;
	else
		allowChgMsgArea = (this.subBoardCode != "mail");
	// If reading personal email and messages haven't been collected (searched)
	// yet, then do so now.
	if (this.readingPersonalEmail && (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode)))
		this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser);

	// Determine whether to start in list or reader mode, depending
	// on the value of this.startMode.
	var readerMode = this.startMode;
	// If an initial mode override was specified and is valid, then use it.
	if (typeof(pInitialModeOverride) === "number" && (pInitialModeOverride == READER_MODE_READ || pInitialModeOverride == READER_MODE_LIST))
		readerMode = pInitialModeOverride;
	// User input loop
	var selectedMessageOffset = 0;
	if (typeof(pStartingMsgOffset) == "number")
		selectedMessageOffset = pStartingMsgOffset;
	else if (this.SearchingAndResultObjsDefinedForCurSub())
	{
		// If reading personal mail, start at the first unread message index
		// (or the last message, if all messages have been read)
		if (this.readingPersonalEmail)
		{
			selectedMessageOffset = this.GetLastReadMsgIdxAndNum(false).lastReadMsgIdx; // Used to be true
			if ((selectedMessageOffset > -1) && (selectedMessageOffset < this.NumMessages() - 1))
				++selectedMessageOffset;
		}
		else
			selectedMessageOffset = 0;
	}
	else if (this.hdrsForCurrentSubBoard.length > 0)
	{
		selectedMessageOffset = this.GetMsgIdx(GetScanPtrOrLastMsgNum(this.subBoardCode));
		if (selectedMessageOffset < 0)
			selectedMessageOffset = 0;
		else if (selectedMessageOffset >= this.hdrsForCurrentSubBoard.length)
			selectedMessageOffset = this.hdrsForCurrentSubBoard.length - 1;
	}
	else
		selectedMessageOffset = -1;
	var otherRetObj = null;
	var continueOn = true;
	while (continueOn)
	{
		switch (readerMode)
		{
			case READER_MODE_READ:
				// Call the ReadMessages method - DOn't change the sub-board,
				// and pass the selected index of the message to read.  If that
				// index is -1, the ReadMessages method will use the user's
				// last-read message index.
				otherRetObj = this.ReadMessages(null, selectedMessageOffset, true, allowChgMsgArea,
				                                pReturnOnNextAreaNav, pPromptToGoNextIfNoResults);
				// If the user wants to quit or if there was an error, then stop
				// the input loop.
				if (otherRetObj.stoppedReading)
				{
					retObj.stoppedReading = true;
					continueOn = false;
				}
				// If we're set to return on navigation to the next message area and
				// the user's last keypress was the right arrow key or next action
				// was to go to the next message area, then don't continue the input
				// loop, and also say that the user didn't stop reading.
				else if (pReturnOnNextAreaNav &&
				         ((otherRetObj.lastUserInput == KEY_RIGHT) || (otherRetObj.lastUserInput == KEY_ENTER) || (otherRetObj.lastAction == ACTION_GO_NEXT_MSG_AREA)))
				{
					retObj.stoppedReading = false;
					continueOn = false;
				}
				else if (otherRetObj.messageListReturn)
					readerMode = READER_MODE_LIST;
			case READER_MODE_LIST:
				// Note: Doing the message list is also handled in this.ReadMessages().
				// This code is here in case the reader is configured to start up
				// in list mode first.
				// List messages
				otherRetObj = this.ListMessages(null, pAllowChgArea);
				// If the user wants to quit, set continueOn to false to get out
				// of the loop.  Otherwise, set the selected message offset to
				// what the user chose from the list.
				if (otherRetObj.lastUserInput == "Q")
				{
					retObj.stoppedReading = true;
					continueOn = false;
				}
				else
				{
					selectedMessageOffset = otherRetObj.selectedMsgOffset;
					readerMode = READER_MODE_READ;
				}
				break;
			default:
				break;
		}
	}


	return retObj;
}
// Helper for DigDistMsgReader_ReadOrListSubBoard(): Populates this.msgSearchHdrs
// if an applicable search type is specified; also, if there are no messages in
// the current sub-board, outputs an error to the user.
//
// Parameters:
//  pCloseMsgbaseAndSetNullIfNoMsgs: Optional boolean - Whether or not to close the message
//                         base if there are no messages.  Defaults to true.
//  pOutputMessages: Boolean - Whether or not to output messages to the screen.
//                   Defaults to true.
//  pPauseOnNoMsgError: Optional boolean - Whether or not to pause for a keypress
//                      after displaying the "no messages" error.  Defaults to true.
//
// Return value: Boolean - Whether or not there are messages to read in the current
//               sub-board
function DigDistMsgReader_PopulateHdrsIfSearch_DispErrorIfNoMsgs(pCloseMsgbaseAndSetNullIfNoMsgs,
                                                 pOutputMessages, pPauseOnNoMsgError)
{
	var thereAreMessagesToRead = true;

	var outputMessages = (typeof(pOutputMessages) == "boolean" ? pOutputMessages : true);

	// If a search is is specified that would populate the search results, then
	// perform the message search for the current sub-board.
	if (this.SearchTypePopulatesSearchResults())
	{
		if (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))
		{
			// TODO: In case new messages were posted in this sub-board, it might help
			// to check the current number of messages vs. the previous number of messages
			// and search the new messages if there are more.
			if (outputMessages)
			{
				console.crlf();
					formattedText = format(this.text.loadingPersonalMailText, subBoardGrpAndName(this.subBoardCode));
					formattedText = format(this.text.searchingSubBoardText, subBoardGrpAndName(this.subBoardCode));
				//console.print("\x01n" + replaceAtCodesInStr(formattedText) + "\x01n");
				console.putmsg("\x01n" + formattedText + "\x01n");
			this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser);
		}
	}
	else
	{
		// There is no search is specified, so clear the search results for the
		// current sub-board to help ensure that there are messages to read.
		if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))
		{
			delete this.msgSearchHdrs[this.subBoardCode].indexed;
			delete this.msgSearchHdrs[this.subBoardCode];
		}
	}

	// If there are no messages to display in the current sub-board, then set the
	// return value and let the user know (if outputMessages is true).
	if (this.NumMessages() == 0)
	{
		thereAreMessagesToRead = false;
		if (outputMessages)
		{
			console.crlf();
			if (this.readingPersonalEmail)
			{
				//console.print(replaceAtCodesInStr(this.text.noPersonalEmailText));
				console.putmsg(this.text.noPersonalEmailText);
			}
				if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))
					formattedText = format(this.text.noSearchResultsInSubBoardText, subBoardGrpAndName(this.subBoardCode));
					formattedText = format(this.text.noMessagesInSubBoardText, subBoardGrpAndName(this.subBoardCode));
				//console.print(replaceAtCodesInStr(formattedText));
				console.putmsg(formattedText);
			}
			console.crlf();
			var pauseOnNoMsgsError = (typeof(pPauseOnNoMsgError) == "boolean" ? pPauseOnNoMsgError : true);
			if (pauseOnNoMsgsError)
				console.pause();
		}
	}

	return thereAreMessagesToRead;
}

// For the DigDistMsgReader class: Returns whether the search type is a type
// that would result in the search results structure being populated.  Search
// types where that wouldn't happen are SEARCH_NONE (no search) and any of the
// message scan search types.
function DigDistMsgReader_SearchTypePopulatesSearchResults()
{
	return (this.readingPersonalEmail || searchTypePopulatesSearchResults(this.searchType));
}

// For the DigDistMsgReader class: Returns whether the search type is a type
// that requires search text.  Search types that require search text are the
// keyword search, from name search, and to name search.  Search types that
// don't require search text are SEARCH_NONE (no search) & the message scan search
// types.
function DigDistMsgReader_SearchTypeRequiresSearchText()
{
	return searchTypeRequiresSearchText(this.searchType);
}

// Returns whether a search type value would populate search results.
//
// Parameters:
//  pSearchType: A search type integer value
//
// Return value: Boolean - Whether or not the search type would populate search
//               results
function searchTypePopulatesSearchResults(pSearchType)
{
	return ((pSearchType == SEARCH_KEYWORD) ||
	        (pSearchType == SEARCH_FROM_NAME) ||
	        (pSearchType == SEARCH_TO_NAME_CUR_MSG_AREA) ||
	        (pSearchType == SEARCH_TO_USER_CUR_MSG_AREA) ||
	        (pSearchType == SEARCH_TO_USER_NEW_SCAN) ||
			(pSearchType == SEARCH_TO_USER_NEW_SCAN_CUR_SUB) ||
	        (pSearchType == SEARCH_TO_USER_NEW_SCAN_CUR_GRP) ||
	        (pSearchType == SEARCH_TO_USER_NEW_SCAN_ALL) ||
	        (pSearchType == SEARCH_ALL_TO_USER_SCAN));
}

// Returns whether a search type value requires search text.
//
// Parameters:
//  pSearchType: A search type integer value
//
// Return value: Boolean - Whether or not the search type requires search text
function searchTypeRequiresSearchText(pSearchType)
{
	return ((pSearchType == SEARCH_KEYWORD) ||
	         (pSearchType == SEARCH_FROM_NAME) ||
	         (pSearchType == SEARCH_TO_NAME_CUR_MSG_AREA));
}

// For the DigDistMsgReader class: Scans the message area(s) for new messages,
// unread messages to the user, or all messages to the user.
//
// Parameters:
//  pScanCfgOpt: The scan configuration option to check for in the sub-boards
//               (from sbbsdefs.js). Supported values are SCAN_CFG_NEW (new
//               message scan), and SCAN_CFG_TOYOU (messages to the user)
//  pScanMode: The scan mode (from sbbsdefs.js).  Supported values are SCAN_NEW
//             (new message scan), SCAN_TOYOU (scan for all messages to the
//             user), and SCAN_UNREAD (scan for new messages to the user).
//  pScanScopeChar: Optional - A character (as a string) representing the scan
//                  scope: "S" for sub-board, "G" for group, or "A" for all.
//                  If this is not specified, the user will be prompted for the
//                  scan scope.
function DigDistMsgReader_MessageAreaScan(pScanCfgOpt, pScanMode, pScanScopeChar)
{
	var scanScopeChar = "";
	if ((typeof(pScanScopeChar) == "string") && /^[SGA]$/.test(pScanScopeChar))
		scanScopeChar = pScanScopeChar;
	else
	{
		// Prompt the user to scan in the current sub-board, the current message group,
		// or all.  Default to all.
		console.mnemonics(bbs.text(SubGroupOrAll));
		scanScopeChar = console.getkeys("SGAC").toString();
		// If the user just pressed Enter without choosing anything, then abort and return.
		if (scanScopeChar.length == 0)
		{
			console.crlf();
			//console.print(replaceAtCodesInStr(this.text.msgScanAbortedText));
			//console.crlf();
			console.putmsg(this.text.msgScanAbortedText);
			console.pause();
			return;
		}
	}

	// Do some logging if verbose logging is enabled
	if (gCmdLineArgVals.verboselogging)
	{
		var logMessage = "Doing a message area scan (";
		if (pScanCfgOpt == SCAN_CFG_NEW)
		{
			// The only valid value for pScanMode in this case is SCAN_NEW, so no
			// need to check pScanMode to append more to the log message.
			logMessage += "new";
		}
		else if (pScanCfgOpt == SCAN_CFG_TOYOU)
		{
			// Valid values for pScanMode in this case are SCAN_UNREAD and SCAN_TOYOU.
			if (pScanMode == SCAN_UNREAD)
				logMessage += "unread messages to the user";
			else if (pScanMode == SCAN_TOYOU)
				logMessage += "all messages to the user";
		}
		if (scanScopeChar == "A") // All sub-boards
			logMessage += ", all sub-boards";
		else if (scanScopeChar == "G") // Current message group
		{
			logMessage += ", current message group (" +
			              msg_area.grp_list[bbs.curgrp].description + ")";
		}
		else if (scanScopeChar == "S") // Current sub-board
		{
			logMessage += ", current sub-board (" +
			              msg_area.grp_list[bbs.curgrp].description + " - " +
						  msg_area.grp_list[bbs.curgrp].sub_list[bbs.cursub].description + ")";
		}
		logMessage += ")";
		writeToSysAndNodeLog(logMessage);
	}

	// Save the original search type, sub-board code, searched message headers,
	// etc. to be restored later
	var originalSearchType = this.searchType;
	var originalSubBoardCode = this.subBoardCode;
	var originalBBSCurGrp = bbs.curgrp;
	var originalBBSCurSub = bbs.cursub;
	var originalMsgSrchHdrs = this.msgSearchHdrs;

	// Make sure there is no search data
	this.ClearSearchData();

	// Create an array of internal codes of sub-boards to scan
	var subBoardsToScan = getSubBoardsToScanArray(scanScopeChar);
	// Scan through the sub-boards
	this.doingMsgScan = true;
	var continueNewScan = true;
	var userAborted = false;
	this.doingMultiSubBoardScan = (subBoardsToScan.length > 1);
	for (var subCodeIdx = 0; (subCodeIdx < subBoardsToScan.length) && continueNewScan; ++subCodeIdx)
	{
		// Force garbage collection to ensure enough memory is available to continue
		js.gc(true);
		// Set the console line counter to 0 to prevent screen pausing
		// when the "Searching ..." and "No messages were found" text is
		// displayed repeatedly
		console.line_counter = 0;
		// If the sub-board's access requirements allows the user to read it
		// and it's enabled in the user's message scan configuration, then go
		// ahead with this sub-board.
		// Note: Used to use this to determine whether the user could access the
		// sub-board:
		//user.compare_ars(msg_area.grp_list[grpIndex].sub_list[subIndex].ars)
		// Now using the can_read property.
		this.setSubBoardCode(subBoardsToScan[subCodeIdx]); // Needs to be set before getting the last read/scan pointer index
		if (msg_area.sub[this.subBoardCode].can_read && ((msg_area.sub[this.subBoardCode].scan_cfg & pScanCfgOpt) == pScanCfgOpt))
			var grpIndex = msg_area.sub[this.subBoardCode].grp_index;
			var subIndex = msg_area.sub[this.subBoardCode].index;
			// Sub-board description: msg_area.grp_list[grpIndex].sub_list[subIndex].description
			// Open the sub-board and check for unread messages.  If there are any, then let
			// the user read the messages in the sub-board.
			//var msgbase = new MsgBasesubBoardsToScan[subCodeIdx]);
			var msgbase = new MsgBase(this.subBoardCode);
			if (msgbase.open())
				// Get a filtered list of messages for this sub-board
				this.PopulateHdrsForCurrentSubBoard();

				//this.setSubBoardCode(subBoardsToScan[subCodeIdx]); // Needs to be set before getting the last read/scan pointer index
				// If the current sub-board contains only deleted messages,
				// or if the user has already read the last message in this
				// sub-board, then skip it.
				var scanPtrMsgIdx = this.GetScanPtrMsgIdx();
				var nonDeletedMsgsExist = (this.FindNextNonDeletedMsgIdx(scanPtrMsgIdx-1, true) > -1);
				var userHasReadLastMessage = false;
				if (this.subBoardCode != "mail")
				{
					// What if newest_message_header.number is invalid  (e.g. NaN or 0xffffffff or >
					// msgbase.last_msg)?
					if (this.hdrsForCurrentSubBoard.length > 0)
					{
						if ((msg_area.sub[this.subBoardCode].last_read == this.hdrsForCurrentSubBoard[this.hdrsForCurrentSubBoard.length-1].number) ||
						    (scanPtrMsgIdx == this.hdrsForCurrentSubBoard.length-1))
				if (!nonDeletedMsgsExist || userHasReadLastMessage)
					if (msgbase != null)
						msgbase.close();
					continue;
				}

				// In the switch cases below, bbs.curgrp and bbs.cursub are
				// temporarily changed the user's sub-board to the current
				// sub-board so that certain @-codes (such as @GRP-L@, etc.)
				// are displayed by Synchronet correctly.

				// We might want the starting message index to be different
				// depending on the scan mode.
				switch (pScanMode)
				{
					case SCAN_NEW:
						// Make sure the sub-board has some messages.  Let the user read it if
						// the scan pointer index is -1 (one unread message) or if it points to
						// a message within the number of messages in the sub-board.
						var totalNumMsgs = msgbase.total_msgs;
						if ((totalNumMsgs > 0) && ((scanPtrMsgIdx == -1) || (scanPtrMsgIdx < totalNumMsgs-1)))
						{
							bbs.curgrp = grpIndex;
							bbs.cursub = subIndex;
							// Start at the scan pointer
							var startMsgIdx = scanPtrMsgIdx;
							// If the message has already been read, then start at the next message
							var tmpMsgHdr = this.GetMsgHdrByIdx(startMsgIdx);
							if ((tmpMsgHdr != null) && (msg_area.sub[this.subBoardCode].last_read == tmpMsgHdr.number) && (startMsgIdx < this.NumMessages(true) - 1))
								++startMsgIdx;
							// Allow the user to read messages in this sub-board.  Don't allow
							// the user to change to a different message area, don't pause
							// when there's no search results in a sub-board, and return
							// instead of going to the next sub-board via navigation.
							var readRetObj = this.ReadOrListSubBoard(null, startMsgIdx, false, true, false);
							// If the user stopped reading & decided to quit, then exit the
							// message scan loops.
							if (readRetObj.stoppedReading)
								continueNewScan = false;
								userAborted = true;
						}
						break;
					case SCAN_TOYOU: // All messages to the user
						bbs.curgrp = grpIndex;
						bbs.cursub = subIndex;
						// Search for messages to the user in the current sub-board
						// and let the user read the sub-board if messages are
						// found.  Don't allow the user to change to a different
						// message area, don't pause when there's no search results
						// in a sub-board, and return instead of going to the next
						// sub-board via navigation.
						this.searchType = SEARCH_TO_USER_CUR_MSG_AREA;
						var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, false);
						// If the user stopped reading & decided to quit, then exit the
						// message scan loops.
						if (readRetObj.stoppedReading)
						{
							continueNewScan = false;
							userAborted = true;
						}
						break;
					case SCAN_UNREAD: // New (unread) messages to the user
						bbs.curgrp = grpIndex;
						bbs.cursub = subIndex;
						// Search for messages to the user in the current sub-board
						// and let the user read the sub-board if messages are
						// found.  Don't allow the user to change to a different
						// message area, don't pause when there's no search results
						// in a sub-board, and return instead of going to the next
						// sub-board via navigation.
						this.searchType = SEARCH_TO_USER_NEW_SCAN;
						var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, false);
						// If the user stopped reading & decided to quit, then exit the
						// message scan loops.
						if (readRetObj.stoppedReading)
						{
							continueNewScan = false;
							userAborted = true;
						}
						break;
					default:
						break;
				if (msgbase != null)
					msgbase.close();
		// Pause for a short moment to avoid causing CPU usage going to 99%
		mswait(10);

	// Restore the original sub-board code, searched message headers, etc.
	this.searchType = originalSearchType;
	this.setSubBoardCode(originalSubBoardCode);
	this.msgSearchHdrs = originalMsgSrchHdrs;
	bbs.curgrp = originalBBSCurGrp;
	bbs.cursub = originalBBSCurSub;
	if ((msgbase != null) && msgbase.is_open)
		msgbase.close();
	this.doingMultiSubBoardScan = false;
	if (this.pauseAfterNewMsgScan)
	{
		console.crlf();
		if (userAborted)
		{
			//console.print("\x01n" + replaceAtCodesInStr(this.text.msgScanAbortedText) + "\x01n");
			console.putmsg("\x01n" + this.text.msgScanAbortedText + "\x01n");
		}
		{
			//console.print("\x01n" + replaceAtCodesInStr(this.text.msgScanCompleteText) + "\x01n");
			console.putmsg("\x01n" + this.text.msgScanCompleteText + "\x01n");
		}
}

// For the DigDistMsgReader class: Performs the message reading activity.
//
// Parameters:
//  pSubBoardCode: Optional - The internal code of a sub-board to read.
//                 If not specified, the internal sub-board code specified
//                 when creating the object will be used.
//  pStartingMsgOffset: Optional - The offset of a message to start at
//  pReturnOnMessageList: Optional boolean - Whether or not to quit when the
//                      user wants to list messages (used when this method
//                      is called from ReadOrListSubBoard()).
//  pAllowChgArea: Optional boolean - Whether or not to allow changing the
//                 message area
//  pReturnOnNextAreaNav: Optional boolean - Whether or not this method should
//                        return when it would move to the next message area due
//                        navigation from the user (i.e., with the right arrow
//                        key or with < (go to previous message area) or > (go
//                        to next message area))
//  pPromptToGoToNextAreaIfNoSearchResults: Optional boolean - Whether or not to
//                          prompt the user to go to the next/previous sub-board
//                          when there are no search results
//
// Return value: An object that has the following properties:
//               lastUserInput: The user's last keypress/input
//               lastAction: The last action chosen by the user based on their
//                           last keypress, etc.
//               stoppedReading: Boolean - Whether reading has stopped
//                               (due to user quitting, error, or otherwise)
//               messageListReturn: Boolean - Whether this method is returning for
//                                  the caller to display the message list.  This
//                                  will only be true when the pReturnOnMessageList
//                                  parameter is true and the user wants to list
//                                  messages.
function DigDistMsgReader_ReadMessages(pSubBoardCode, pStartingMsgOffset, pReturnOnMessageList,
                                       pAllowChgArea, pReturnOnNextAreaNav, pPromptToGoToNextAreaIfNoSearchResults)
	var retObj = {
		lastUserInput: "",
		lastAction: ACTION_NONE,
		stoppedReading: false,
		messageListReturn: false
	};

	// If the passed-in sub-board code was different than what was set in the object before,
	// then open the new message sub-board.
	var previousSubBoardCode = this.subBoardCode;
	if (typeof(pSubBoardCode) == "string")
	{
		if (subBoardCodeIsValid(pSubBoardCode))
			this.setSubBoardCode(pSubBoardCode);
			console.print("\x01n\x01h\x01yWarning: \x01wThe Message Reader connot continue because an invalid");
			console.crlf();
			console.print("sub-board code was specified (" + pSubBoardCode + "). Please notify the sysop.");
			console.crlf();
			console.pause();
			retObj.stoppedReading = true;
			return retObj;
		}
	}
	if (this.subBoardCode.length == 0)
	{
		console.print("\x01n\x01h\x01yWarning: \x01wThe Message Reader connot continue because no message");
		console.crlf();
		console.print("sub-board was specified. Please notify the sysop.");
		console.crlf();
		console.pause();
		retObj.stoppedReading = true;
		return retObj;
	}

	// If the user doesn't have permission to read the current sub-board, then
	// don't allow the user to read it.
		if (!msg_area.sub[this.subBoardCode].can_read)
		{
			var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name);
			console.print("\x01n" + errorMsg);
			console.pause();
			retObj.stoppedReading = true;
			return retObj;
		}
	var msgbase = new MsgBase(this.subBoardCode);
	// If the message base was not opened, then output an error and return.
		console.print("\x01h\x01y* \x01wUnable to open message sub-board:");
		console.crlf();
		console.print(subBoardGrpAndName(this.subBoardCode));
		console.crlf();
		console.pause();
		retObj.stoppedReading = true;
		return retObj;
	}

	// If there are no messages to display in the current sub-board, then let the
	// user know and exit.
	var numOfMessages = this.NumMessages(msgbase);
	msgbase.close();
	if (numOfMessages == 0)
		console.clear("\x01n");
		console.center("\x01n\x01h\x01yThere are no messages to display.");
		console.crlf();
		console.pause();
		retObj.stoppedReading = true;
		return retObj;
	}

	// Check the pAllowChgArea parameter.  If it's a boolean, then use it.  If
	// not, then check to see if we're reading personal mail - If not, then allow
	// the user to change to a different message area.
	var allowChgMsgArea = true;
	if (typeof(pAllowChgArea) == "boolean")
		allowChgMsgArea = pAllowChgArea;
	else
		allowChgMsgArea = (this.subBoardCode != "mail");
	// If reading personal email and messages haven't been collected (searched)
	// yet, then do so now.
	if (this.readingPersonalEmail && (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode)))
		this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser);

	// Determine the index of the message to start at.  This will be
	// pStartingMsgOffset if pStartingMsgOffset is valid, or the index
	// of the user's last-read message in this sub-board.
	var msgIndex = 0;
	if ((typeof(pStartingMsgOffset) == "number") && (pStartingMsgOffset >= 0) && (pStartingMsgOffset < this.NumMessages()))
		msgIndex = pStartingMsgOffset;
	else if (this.SearchingAndResultObjsDefinedForCurSub())
		msgIndex = 0;
	else
	{
		msgIndex = this.GetLastReadMsgIdxAndNum().lastReadMsgIdx;
		if (msgIndex == -1)
			msgIndex = 0;
	}

	// If the current message index is for a message that has been
	// deleted, then find the next non-deleted message.
	var testMsgHdr = this.GetMsgHdrByIdx(msgIndex);
	if ((testMsgHdr == null) || ((testMsgHdr.attr & MSG_DELETE) == MSG_DELETE))
	{
		// First try going forward
		var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(msgIndex, true);
		// If a non-deleted message was not found, then try going backward.
		if (nonDeletedMsgIdx == -1)
			nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(msgIndex, false);
		// If a non-deleted message was found, then set msgIndex to it.
		// Otherwise, tell the user there are no messages in this sub-board
		// and return.
		if (nonDeletedMsgIdx > -1)
			msgIndex = nonDeletedMsgIdx;
		else
		{
			console.clear("\x01n");
			console.center("\x01h\x01yThere are no messages to display.");
			console.crlf();
			console.pause();
			retObj.stoppedReading = true;
			return retObj;
		}
	}

	// Construct the hotkey help line (needs to be done after the message
	// base is open so that the delete & edit keys can be added correctly).
	this.SetEnhancedReaderHelpLine();

	// Get the screen ready for reading messages - First, clear the screen.
	// Display the help line at the bottom of the screen
	if (this.scrollingReaderInterface && console.term_supports(USER_ANSI))
		this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
	// Input loop
	var msgHdr = null;
	var dateTimeStr = null;
	var screenY = 1; // For screen updates requiring more than one line
	var continueOn = true;
	var readMsgRetObj = null;
	// previousNextAction will store the next action from the previous iteration.
	// It is useful for some checks, such as when the current message is deleted,
	// we'll want to see if the user wanted to go to the previous message/area
	// for navigation purposes.
	var previousNextAction = ACTION_NONE;
	while (continueOn && (msgIndex >= 0) && (msgIndex < this.NumMessages()))
	{
		// Display the message with the enhanced read method
		readMsgRetObj = this.ReadMessageEnhanced(msgIndex, allowChgMsgArea);
		retObj.lastUserInput = readMsgRetObj.lastKeypress;
		retObj.lastAction = readMsgRetObj.nextAction;
		// If we should refresh the enhanced reader help line on the screen (and
		// the returned message offset is valid and the user's terminal supports ANSI),
		// then refresh the help line.
		if (readMsgRetObj.refreshEnhancedRdrHelpLine && readMsgRetObj.offsetValid && console.term_supports(USER_ANSI))
			this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
		// If the returned message offset is invalid, then quit.
		if (!readMsgRetObj.offsetValid)
		{
			continueOn = false;
			retObj.stoppedReading = true;
			break;
		}
		// If the message is not readable to the user, then go to the
		{
			// If the user's next action in the last iteration was to go to the
			// previous message, then go backwards; otherwise, go forward.
			if (previousNextAction == ACTION_GO_PREVIOUS_MSG)
				msgIndex = this.FindNextNonDeletedMsgIdx(msgIndex, false);
				msgIndex = this.FindNextNonDeletedMsgIdx(msgIndex, true);
			continueOn = ((msgIndex >= 0) && (msgIndex < this.NumMessages()));
		}
		else if (readMsgRetObj.nextAction == ACTION_QUIT) // Quit
		{
			// Quit
			continueOn = false;
			retObj.stoppedReading = true;
			break;
		}
		else if (readMsgRetObj.lastKeypress == "R")
		{
			// Replying to the message is handled in ReadMessageEnhanced().
			// The help line at the bottom of the screen needs to be redrawn though,
			// for ANSI users.
			if (this.scrollingReaderInterface && console.term_supports(USER_ANSI))
				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
		}
		else if (readMsgRetObj.nextAction == ACTION_GO_PREVIOUS_MSG) // Go to previous message/area
		{
			// TODO: There is some opportunity for screen redraw optimization - If
			// already at the first readable sub-board, this would redraw the
			// screen unnecessarily.  Similar for the right arrow key too.

			// The newMsgOffset value will be 0 or more if a prior non-deleted
			// message was found.  If it's -1, then allow going to the previous
			// message sub-board/group.
			if (readMsgRetObj.newMsgOffset > -1)
				msgIndex = readMsgRetObj.newMsgOffset;
			else
			{
				// The user is at the beginning of the current sub-board.
				if (allowChgMsgArea)
				{
					if (this.SearchTypePopulatesSearchResults())
						console.print("\x01n\r\nLoading messages...");
					var goToPrevRetval = this.GoToPrevSubBoardForEnhReader(allowChgMsgArea, pPromptToGoToNextAreaIfNoSearchResults);
					retObj.stoppedReading = goToPrevRetval.shouldStopReading;
					// If we're going to stop reading, then 
					if (retObj.stoppedReading)
						msgIndex = 0;
					else if (goToPrevRetval.changedMsgArea)
				}

				// If the caller wants this method to return instead of going to the next
				// sub-board with messages, then do so.
				if (pReturnOnNextAreaNav)
			}
		}
		// Go to next message action - This can happen with the right arrow key or
		// if the user deletes the message in the ReadMessageEnhanced() method.
		else if (readMsgRetObj.nextAction == ACTION_GO_NEXT_MSG)
		{
			// The newMsgOffset value will be 0 or more if a later non-deleted
			// message was found.  If it's -1, then allow going to the next
			// message sub-board/group.
			if (readMsgRetObj.newMsgOffset > -1)
				msgIndex = readMsgRetObj.newMsgOffset;
			else
			{
				// The user is at the end of the current sub-board.
				if (allowChgMsgArea && !pReturnOnNextAreaNav)
				{
					if (this.SearchTypePopulatesSearchResults())
						console.print("\x01n\r\nLoading messages...");
					var goToNextRetval = this.GoToNextSubBoardForEnhReader(allowChgMsgArea, pPromptToGoToNextAreaIfNoSearchResults);
					retObj.stoppedReading = goToNextRetval.shouldStopReading;
					// If we're going to stop reading, then 
					if (retObj.stoppedReading)
						msgIndex = 0;
					else if (goToNextRetval.changedMsgArea)
				}
				// If the caller wants this method to return instead of going to the next
				// sub-board with messages, then do so.
				if (pReturnOnNextAreaNav)
			}
		}
		else if (readMsgRetObj.nextAction == ACTION_GO_FIRST_MSG) // Go to the first message
		{
			// Go to the first message that's not marked as deleted.  This passes -1 as the
			// starting message index because FindNextNonDeletedMsgIdx() will increment it
			// before searching in order to find the "next" message.
			msgIndex = this.FindNextNonDeletedMsgIdx(-1, true);
		}
		else if (readMsgRetObj.nextAction == ACTION_GO_LAST_MSG) // Go to the last message
		{
			// Go to the last message that's not marked as deleted
			msgIndex = this.FindNextNonDeletedMsgIdx(this.NumMessages(), false);
		}
		else if (readMsgRetObj.nextAction == ACTION_CHG_MSG_AREA) // Change message area, if allowed
		{
			if (allowChgMsgArea)
			{
				// Change message sub-board.  If a different sub-board was
				// chosen, then change some variables to use the new
				// chosen sub-board.
				var oldSubBoardCode = this.subBoardCode;
				if (this.subBoardCode != oldSubBoardCode)
					this.PopulateHdrsForCurrentSubBoard();
				var chgSubBoardRetObj = this.EnhancedReaderChangeSubBoard(bbs.cursub_code);
				if (chgSubBoardRetObj.succeeded)
				{
					// Set the message index, etc.
					// If there are search results, then set msgIndex to the first
					// message.  Otherwise (if there is no search specified), then
					// set the message index to the user's last read message.
					if (this.SearchingAndResultObjsDefinedForCurSub())
						msgIndex = chgSubBoardRetObj.lastReadMsgIdx;
					// If the current message index is for a message that has been
					// deleted, then find the next non-deleted message.
					testMsgHdr = this.GetMsgHdrByIdx(msgIndex);
					if ((testMsgHdr == null) || ((testMsgHdr.attr & MSG_DELETE) == MSG_DELETE))
					{
						// First try going forward
						var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(msgIndex, true);
						// If a non-deleted message was not found, then try going backward.
						if (nonDeletedMsgIdx == -1)
							nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(msgIndex, false);
						// If a non-deleted message was found, then set msgIndex to it.
						// Otherwise, return.
						// Note: If there are no messages in the chosen sub-board at all,
						// then the error would have already been shown.
						if (nonDeletedMsgIdx > -1)
						else
						{
							if (this.NumMessages() != 0)
							{
								// There are messages, but none that are not deleted.
								console.clear("\x01n");
								console.center("\x01h\x01yThere are no messages to display.");
								console.crlf();
								console.pause();
							}
							retObj.stoppedReading = true;
							return retObj;
						}
					}
					// Set the hotkey help line again, since the new sub-board might have
					// different settings for whether messages can be edited or deleted,
					// then refresh it on the screen.
					var oldHotkeyHelpLine = this.enhReadHelpLine;
					this.SetEnhancedReaderHelpLine();
					if ((oldHotkeyHelpLine != this.enhReadHelpLine) && this.scrollingReaderInterface && console.term_supports(USER_ANSI))
						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
				}
				else
				{
					retObj.stoppedReading = false;
					return retObj;
				}
			}
		}
		else if (readMsgRetObj.nextAction == ACTION_GO_PREV_MSG_AREA) // Go to the previous message area
		{
			// The user is at the beginning of the current sub-board.
			if (allowChgMsgArea)
			{
				var goToPrevRetval = this.GoToPrevSubBoardForEnhReader(allowChgMsgArea, pPromptToGoToNextAreaIfNoSearchResults);
				retObj.stoppedReading = goToPrevRetval.shouldStopReading;
				if (retObj.stoppedReading)
					msgIndex = 0;
				else if (goToPrevRetval.changedMsgArea)
			}
			// If the caller wants this method to return instead of going to the next
			// sub-board with messages, then do so.
			if (pReturnOnNextAreaNav)
		}
		else if (readMsgRetObj.nextAction == ACTION_GO_NEXT_MSG_AREA) // Go to the next message area
		{
			if (allowChgMsgArea && !pReturnOnNextAreaNav)
			{
				var goToNextRetval = this.GoToNextSubBoardForEnhReader(allowChgMsgArea, pPromptToGoToNextAreaIfNoSearchResults);
				retObj.stoppedReading = goToNextRetval.shouldStopReading;
				if (retObj.stoppedReading)
					msgIndex = 0;
				else if (goToNextRetval.changedMsgArea)
			}
			// If the caller wants this method to return instead of going to the next
			// sub-board with messages, then do so.
			if (pReturnOnNextAreaNav)
		}
		else if (readMsgRetObj.nextAction == ACTION_DISPLAY_MSG_LIST) // Display message list
		{
			// If we need to return to the caller for this, then do so.
			if (pReturnOnMessageList)
			{
				retObj.messageListReturn = true;
				return retObj;
			}
			else
			{
				// If this.reverseListOrder is the string "ASK", the user will be prompted
				// on the last line of the screen for whether they want to list the
				// messages in reverse order.  So, erase the help line on the bottom of
				// the screen.
				if ((typeof(this.reverseListOrder) == "string") && (this.reverseListOrder.toUpperCase() == "ASK"))
				{
					if (this.scrollingReaderInterface && console.term_supports(USER_ANSI))
					{
						console.gotoxy(1, console.screen_rows);
						console.cleartoeol("\x01n");
				// List messages
				var listRetObj = this.ListMessages(null, pAllowChgArea);
				// If the user wants to quit, then stop the input loop.
				if (listRetObj.lastUserInput == "Q")
				{
					continueOn = false;
					retObj.stoppedReading = true;
				}
				// If the user chose a different message, then set the message index
				else if ((listRetObj.selectedMsgOffset > -1) && (listRetObj.selectedMsgOffset < this.NumMessages()))
					msgIndex = listRetObj.selectedMsgOffset;
			}
		}
		// Go to specific message & new message offset is valid: Read the new
		// message
		else if ((readMsgRetObj.nextAction == ACTION_GO_SPECIFIC_MSG) && (readMsgRetObj.newMsgOffset > -1))
			msgIndex = readMsgRetObj.newMsgOffset;

		// Save this iteration's next action for the "previous" next action for the next iteration
		previousNextAction = readMsgRetObj.nextAction;
	}

	return retObj;
}

// For the DigDistMsgReader class: Performs the message listing, given a
// sub-board code.
//
// Paramters:
//  pSubBoardCode: Optional - The internal sub-board code, or "mail"
//                 for personal email.
//  pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow
//                     changing to another sub-board.  Defaults to true.
// Return value: An object containing the following properties:
//               lastUserInput: The user's last keypress/input
//               selectedMsgOffset: The index of the message selected to read,
//                                  if one was selected.  If none was selected,
//                                  this will be -1.
function DigDistMsgReader_ListMessages(pSubBoardCode, pAllowChgSubBoard)
	var retObj = {
		lastUserInput: "",
		selectedMsgOffset: -1
	};
	// If the passed-in sub-board code was different than what was set in the object before,
	// then open the new message sub-board.
	var previousSubBoardCode = this.subBoardCode;
	if (typeof(pSubBoardCode) == "string")
	{
		if (subBoardCodeIsValid(pSubBoardCode))
			this.setSubBoardCode(pSubBoardCode);
		else
		{
			console.print("\x01n\x01h\x01yWarning: \x01wThe Message Reader connot continue because an invalid");
			console.crlf();
			console.print("sub-board code was specified (" + pSubBoardCode + "). Please notify the sysop.");
			console.crlf();
			console.pause();
			return retObj;
		}
	}
	if (this.subBoardCode.length == 0)
	{
		console.print("\x01n\x01h\x01yWarning: \x01wThe Message Reader connot continue because no message\r\n");
		console.print("sub-board was specified. Please notify the sysop.\r\n\x01p");

	// If the user doesn't have permission to read the current sub-board, then
	// don't allow the user to read it.
		if (!msg_area.sub[this.subBoardCode].can_read)
		{
			var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name);
			console.print("\x01n" + errorMsg);
	// If there are no messages to display in the current sub-board, then let the
	// user know and exit.
	if (this.NumMessages() == 0)
		console.clear("\x01n");
		console.center("\x01n\x01h\x01yThere are no messages to display.\r\n\x01p");
	// Construct the traditional UI pause text and the line of help text for lightbar
	// mode.  This adds the delete and edit keys if the user is allowed to delete & edit
	// messages.
	this.SetMsgListPauseTextAndLightbarHelpLine();

	// If this.reverseListOrder is the string "ASK", prompt the user for whether
	// they want to list the messages in reverse order.
	if ((typeof(this.reverseListOrder) == "string") && (this.reverseListOrder.toUpperCase() == "ASK"))
	{
		if (numMessages(bbs.cursub_code) > 0)
			this.reverseListOrder = !console.noyes("\x01n\x01cList in reverse (newest on top)");
	// List the messages using the lightbar or traditional interface, depending on
	// what this.msgListUseLightbarListInterface is set to.  The lightbar interface requires ANSI.
	if (this.msgListUseLightbarListInterface && canDoHighASCIIAndANSI())
		retObj = this.ListMessages_Lightbar(pAllowChgSubBoard);
		retObj = this.ListMessages_Traditional(pAllowChgSubBoard);
	return retObj;
}
// For the DigDistMsgReader class: Performs the message listing, given a
// sub-board code.  This version uses a traditional user interface, prompting
// the user at the end of each page to continue, quit, or read a message.
//
// Parameters:
//  pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow
//                     changing to another sub-board.  Defaults to true.
//
// Return value: An object containing the following properties:
//               lastUserInput: The user's last keypress/input
//               selectedMsgOffset: The index of the message selected to read,
//                                  if one was selected.  If none was selected,
//                                  this will be -1.
function DigDistMsgReader_ListMessages_Traditional(pAllowChgSubBoard)
	var retObj = {
		lastUserInput: "",
		selectedMsgOffset: -1
	};
	// If the user doesn't have permission to read the current sub-board, then
	// don't allow the user to read it.
		if (!msg_area.sub[this.subBoardCode].can_read)
		{
			var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name);
			console.print("\x01n" + errorMsg);
	// Reset this.readAMessage and deniedReadingmessage to false, in case the
	// message listing has previously ended with them set to true.
	this.readAMessage = false;
	this.deniedReadingMessage = false;

	var msgbase = new MsgBase(this.subBoardCode);
	if (!msgbase.open())
		console.center("\x01n\x01h\x01yError: \x01wUnable to open the sub-board.\r\n\x01p");
	var allowChgSubBoard = (typeof(pAllowChgSubBoard) == "boolean" ? pAllowChgSubBoard : true);

	// this.tradMsgListNumLines stores the maximum number of lines to write.  It's the number
	// of rows on the user's screen - 3 to make room for the header line
	// at the top, the question line at the bottom, and 1 extra line at
	// the bottom of the screen so that displaying carriage returns
	// doesn't mess up the position of the header lines at the top.
	this.tradMsgListNumLines = console.screen_rows-3;
	var nListStartLine = 2; // The first line number on the screen for the message list
	// If we will be displaying the message group and sub-board in the
	// header at the top of the screen (an additional 2 lines), then
	// update this.tradMsgListNumLines and nListStartLine to account for this.
	if (this.displayBoardInfoInHeader)
	{
		this.tradMsgListNumLines -= 2;
		nListStartLine += 2;
	}
	// If the user's terminal doesn't support ANSI, then re-calculate
	// this.tradMsgListNumLines - we won't be keeping the headers at the top of the
	// screen.
	if (!canDoHighASCIIAndANSI()) // Could also be !console.term_supports(USER_ANSI)
		this.tradMsgListNumLines = console.screen_rows - 2;
	this.RecalcMsgListWidthsAndFormatStrs();

	// Clear the screen and write the header at the top

	// If this.tradListTopMsgIdx hasn't been set yet, then get the index of the user's
	// last read message and figure out which page it's on and set the top message
	// index accordingly.
	if (this.tradListTopMsgIdx == -1)
	// Write the message list
	var continueOn = true;
	var retvalObj = null;
	var curpos = null; // Current character position
	var lastScreen = false;
	while (continueOn)
	{
		// Go to the top and write the current page of message information,
		// then update curpos.
		console.gotoxy(1, nListStartLine);
		lastScreen = this.ListScreenfulOfMessages(this.tradListTopMsgIdx, this.tradMsgListNumLines);
		curpos = console.getxy();
		clearToEOS(curpos.y);
		console.gotoxy(curpos);
		// Prompt the user whether or not to continue or to read a message
		// (by message number).
			retvalObj = this.PromptContinueOrReadMsg((this.tradListTopMsgIdx == this.NumMessages()-1), lastScreen, allowChgSubBoard);
			retvalObj = this.PromptContinueOrReadMsg((this.tradListTopMsgIdx == 0), lastScreen, allowChgSubBoard);
		retObj.lastUserInput = retvalObj.userInput;
		retObj.selectedMsgOffset = retvalObj.selectedMsgOffset;

		continueOn = retvalObj.continueOn;
		// TODO: Update this to use PageUp & PageDown keys for paging?  It would
		// require updating PromptContinueOrReadMsg(), which would be non-trivial
		// because that method uses console.getkeys() with a list of allowed keys
		// and a message number limit.
		if (continueOn)
		{
			// If the user chose to go to the previous page of listings,
			// then subtract the appropriate number of messages from
			// this.tradListTopMsgIdx in order to do so.
			if (retvalObj.userInput == "P")
			{
				{
					this.tradListTopMsgIdx += this.tradMsgListNumLines;
					// If we go past the beginning, then we need to reset
					// msgNum so we'll be at the beginning of the list.
					var totalNumMessages = this.NumMessages();
					if (this.tradListTopMsgIdx >= totalNumMessages)
						this.tradListTopMsgIdx = totalNumMessages - 1;
				}
				else
				{
					this.tradListTopMsgIdx -= this.tradMsgListNumLines;
					// If we go past the beginning, then we need to reset
					// msgNum so we'll be at the beginning of the list.
					if (this.tradListTopMsgIdx < 0)
						this.tradListTopMsgIdx = 0;
				}
			}
			// If the user chose to go to the next page, update
			// this.tradListTopMsgIdx appropriately.
			else if (retvalObj.userInput == "N")
			{
					this.tradListTopMsgIdx -= this.tradMsgListNumLines;
				else
					this.tradListTopMsgIdx += this.tradMsgListNumLines;
			}
			// First page
			else if (retvalObj.userInput == "F")
			{
					this.tradListTopMsgIdx = this.NumMessages() - 1;
				else
					this.tradListTopMsgIdx = 0;
			}
			// Last page
			else if (retvalObj.userInput == "L")
			{
				{
					this.tradListTopMsgIdx = (this.NumMessages() % this.tradMsgListNumLines) - 1;
					// If this.tradListTopMsgIdx is now invalid (below 0), then adjust it
					// to properly display the last page of messages.
					if (this.tradListTopMsgIdx < 0)
						this.tradListTopMsgIdx = this.tradMsgListNumLines - 1;
				}
				else
				{
					var totalNumMessages = this.NumMessages();
					this.tradListTopMsgIdx = totalNumMessages - (totalNumMessages % this.tradMsgListNumLines);
					if (this.tradListTopMsgIdx >= totalNumMessages)
						this.tradListTopMsgIdx = totalNumMessages - this.tradMsgListNumLines;
				}
			}
			// D: Delete a message
			else if (retvalObj.userInput == "D" || retvalObj.userInput == this.msgListKeys.deleteMessage || retvalObj.userInput == '\x7f' || retvalObj.userInput == '\x08')
				if (retvalObj.userInput == '\x08')
					console.crlf();
				if (this.CanDelete() || this.CanDeleteLastMsg())
				{
					var msgNum = this.PromptForMsgNum({ x: curpos.x, y: curpos.y+1 }, replaceAtCodesInStr(this.text.deleteMsgNumPromptText), false, ERROR_PAUSE_WAIT_MS, false);
					// If the user enters a valid message number, then call the
					// DeleteMessage() method, which will prompt the user for
					// confirmation and delete the message if confirmed.
					if (msgNum > 0)
						this.PromptAndDeleteOrUndeleteMessage(msgNum-1, null, true);
					// Refresh the top header on the screen for continuing to list
					// messages.
					this.WriteMsgListScreenTopHeader();
				}
			}
			// E: Edit a message
			else if (retvalObj.userInput == this.msgListKeys.editMsg) // "E"
					var msgNum = this.PromptForMsgNum({ x: curpos.x, y: curpos.y+1 }, replaceAtCodesInStr(this.text.editMsgNumPromptText), false, ERROR_PAUSE_WAIT_MS, false);
					// If the user entered a valid message number, then let the
					// user edit the message.
					if (msgNum > 0)
					{
						// See if the current message header has our "isBogus" property and it's true.
						// Only let the user edit the message if it's not a bogus message header.
						// The message header could have the "isBogus" property, for instance, if
						// it's a vote message (introduced in Synchronet 3.17).
						var tmpMsgHdr = this.GetMsgHdrByIdx(msgNum-1);
						var hdrIsBogus = (tmpMsgHdr.hasOwnProperty("isBogus") ? tmpMsgHdr.isBogus : false);
						if (!hdrIsBogus)
							var returnObj = this.EditExistingMsg(msgNum-1);
						else
						{
							console.print("\x01n\r\n\x01h\x01yThat message isn't editable.\n");

					// Refresh the top header on the screen for continuing to list
					// messages.
					this.WriteMsgListScreenTopHeader();
				}
			}
			// G: Go to a specific message by # (place that message on the top)
			else if (retvalObj.userInput == "G")
			{
				var msgNum = this.PromptForMsgNum(curpos, "\x01n" + replaceAtCodesInStr(this.text.goToMsgNumPromptText), false, ERROR_PAUSE_WAIT_MS, false);
				// Refresh the top header on the screen for continuing to list
				// messages.
				this.WriteMsgListScreenTopHeader();
			}
			// ?: Display help
			else if (retvalObj.userInput == "?")
			{
				this.DisplayMsgListHelp(allowChgSubBoard, true);
				this.WriteMsgListScreenTopHeader();
			}
			// C: Change to another message area (sub-board)
			else if (retvalObj.userInput == this.msgListKeys.chgMsgArea) // "C"
			{
				if (allowChgSubBoard && (this.subBoardCode != "mail"))
				{
					// Store the current sub-board code so we can see if it changed
					var oldSubCode = bbs.cursub_code;
					// Let the user choose another message area.  If they chose
					// a different message area, then set up the message base
					// object accordingly.
					this.SelectMsgArea();
					if (bbs.cursub_code != oldSubCode)
					{
						var chgSubRetval = this.ChangeSubBoard(bbs.cursub_code);
						continueOn = chgSubRetval.succeeded;
					}
					// Update the traditional list variables and refresh the screen
					if (continueOn)
					{
						this.SetUpTraditionalMsgListVars();
			// S: Select message(s)
			else if (retvalObj.userInput == "S")
			{
				// Input the message number list from the user
				console.print("\x01n\x01cNumber(s) of message(s) to select, (\x01hA\x01n\x01c=All, \x01hN\x01n\x01c=None, \x01hENTER\x01n\x01c=cancel)\x01g\x01h: \x01c");
				var userNumberList = console.getstr(128, K_UPPER);
				// If the user entered A or N, then select/un-select all messages.
				// Otherwise, select only the messages that the user entered.
				if ((userNumberList == "A") || (userNumberList == "N"))
				{
					var messageSelectToggle = (userNumberList == "A");
					var totalNumMessages = this.NumMessages();
					for (var msgIdx = 0; msgIdx < totalNumMessages; ++msgIdx)
						this.ToggleSelectedMessage(this.subBoardCode, msgIdx, messageSelectToggle);
				}
				else
				{
					if (userNumberList.length > 0)
					{
						var numArray = parseNumberList(userNumberList);
						for (var numIdx = 0; numIdx < numArray.length; ++numIdx)
							this.ToggleSelectedMessage(this.subBoardCode, numArray[numIdx]-1);
					}
				}
				// Refresh the top header on the screen for continuing to list
				// messages.
				this.WriteMsgListScreenTopHeader();
			}
			// Ctrl-D: Batch delete (for selected messages)
			else if (retvalObj.userInput == this.msgListKeys.batchDelete) // CTRL_D
				console.crlf();
				if (this.NumSelectedMessages() > 0)
				{
					// The PromptAndDeleteOrUndeleteSelectedMessages() method will prompt the user for confirmation
					// to delete the message and then delete it if confirmed.
					this.PromptAndDeleteOrUndeleteSelectedMessages(null, true);
					// In case all messages were deleted, if the user can't view deleted messages,
					// show an appropriate message and don't continue listing messages.
					if (!this.NonDeletedMessagesExist() && !canViewDeletedMsgs())
					{
						continueOn = false;
						// Note: The following doesn't seem to be necessary, since
						// the ReadOrListSubBoard() method will show a message saying
						// there are no messages to read and then will quit out.
						
						//msgbase.close();
						//msgbase = null;
						//console.clear("\x01n");
						//console.center("\x01n\x01h\x01yThere are no messages to display.");
						//console.crlf();
						//console.pause();
						
					}
					else
					{
						// There are still messages to list, so refresh the top
						// header on the screen for continuing to list messages.
						this.WriteMsgListScreenTopHeader();
					}
				}
				else
				{
					// There are no selected messages
					console.print("\x01n\x01h\x01yThere are no selected messages.");
					mswait(ERROR_PAUSE_WAIT_MS);
					// Refresh the top header on the screen for continuing to list messages.
			// User settings
			else if (retvalObj.userInput == this.msgListKeys.userSettings)
			{
				/*
				var continueOn = true;
				var retvalObj = null;
				var curpos = null; // Current character position
				var lastScreen = false;

				this.RecalcMsgListWidthsAndFormatStrs();
				if (this.tradListTopMsgIdx == -1)
					this.SetUpTraditionalMsgListVars();
				this.WriteMsgListScreenTopHeader();
				*/
				var userSettingsRetObj = this.DoUserSettings_Traditional();
				retvalObj.userInput = "";
				//drawMenu = userSettingsRetObj.needWholeScreenRefresh;
				// In case the user changed their twitlist, re-filter the messages for this sub-board
				if (userSettingsRetObj.userTwitListChanged)
				{
					console.gotoxy(1, console.screen_rows);
					console.crlf();
					console.print("\x01nTwitlist changed; re-filtering..");
					var tmpMsgbase = new MsgBase(this.subBoardCode);
					if (tmpMsgbase.open())
					{
						var tmpAllMsgHdrs = tmpMsgbase.get_all_msg_headers(true);
						tmpMsgbase.close();
						this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpAllMsgHdrs, true);
					}
					else
						console.print("\x01y\x01hFailed to open the messagbase!\x01\r\n\x01p");
					this.RecalcMsgListWidthsAndFormatStrs();
					if (this.tradListTopMsgIdx == -1)
						this.SetUpTraditionalMsgListVars();
					// If there are still messages in this sub-board, and the message offset is beyond the last
					// message, then adjust the top message index as necessary.
					if (this.hdrsForCurrentSubBoard.length > 0)
					{
						if (this.tradListTopMsgIdx > this.hdrsForCurrentSubBoard.length)
							this.tradListTopMsgIdx = this.hdrsForCurrentSubBoard.length - this.tradMsgListNumLines;
					}
					else
					{
						continueOn = false;
						retObj.selectedMsgOffset = -1;
					}
				}
				if (userSettingsRetObj.needWholeScreenRefresh)
					this.WriteMsgListScreenTopHeader();
			}
				// If a message has been selected, exit out of this input loop
				// so we can return from this method - The calling method will
				// call the enhanced reader method.
				if (retObj.selectedMsgOffset >= 0)
}
// For the DigDistMsgReader class: Performs the message listing, given a
// sub-board code.  This verison uses a lightbar interface for message
//  pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow
//                     changing to another sub-board.  Defaults to true.
//
// Return value: An object containing the following properties:
//               lastUserInput: The user's last keypress/input
//               selectedMsgOffset: The index of the message selected to read,
//                                  if one was selected.  If none was selected,
//                                  this will be -1.
function DigDistMsgReader_ListMessages_Lightbar(pAllowChgSubBoard)
	var retObj = {
		lastUserInput: "",
		selectedMsgOffset: -1
	};
	// If the user doesn't have permission to read the current sub-board, then
	// don't allow the user to read it.
		if (!msg_area.sub[this.subBoardCode].can_read)
		{
			var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name);
			console.print("\x01n" + errorMsg);
	// This method is only supported if the user's terminal supports
	// ANSI.
	if (!canDoHighASCIIAndANSI()) // Could also be !console.term_supports(USER_ANSI)
	{
		console.print("\r\n\x01h\x01ySorry, an ANSI terminal is required for this operation.\x01n\x01w\r\n");
		console.pause();
		return retObj;
	}

	// Reset this.readAMessage and deniedReadingMessage to false, in case the
	// message listing has previously ended with them set to true.
	this.readAMessage = false;
	this.deniedReadingMessage = false;

	this.RecalcMsgListWidthsAndFormatStrs();

	var allowChgSubBoard = (typeof(pAllowChgSubBoard) == "boolean" ? pAllowChgSubBoard : true);
	// This function will be used for displaying the help line at
	// the bottom of the screen.
	function DisplayHelpLine(pHelpLineText)
	{
		console.gotoxy(1, console.screen_rows);
		// Mouse: console.print replaced with console.putmsg for mouse click hotspots
		//console.print(pHelpLineText);
		console.putmsg(pHelpLineText); // console.putmsg() can process @-codes, which we use for mouse click tracking
		console.cleartoeol("\x01n");
	// Clear the screen and write the header at the top
	this.WriteMsgListScreenTopHeader();
	DisplayHelpLine(this.msgListLightbarModeHelpLine);

	// If the lightbar message list index & cursor position variables haven't been
	// set yet, then set them.
	if ((this.lightbarListTopMsgIdx == -1) || (this.lightbarListSelectedMsgIdx == -1) ||
	    (this.lightbarListCurPos == null))
	{
		this.SetUpLightbarMsgListVars();
	}

	// Create a DDLightbarMenu for the message list and list messages
	// and let the user choose one
	var msgListMenu = this.CreateLightbarMsgListMenu();
		var userChoice = msgListMenu.GetVal(drawMenu);
		drawMenu = true;
		var lastUserInputUpper = (typeof(msgListMenu.lastUserInput) == "string" ? msgListMenu.lastUserInput.toUpperCase() : msgListMenu.lastUserInput);
		// If the user's last input is null, then something bad/weird must have
		// happened, so don't continue the input loop.
		if (lastUserInputUpper == null)
		{
			continueOn = false;
			break;
		}
		this.lightbarListSelectedMsgIdx = msgListMenu.selectedItemIdx;
		// If userChoice is a number, then it will be a message number for a message to read
		if (typeof(userChoice) == "number")
			// The user choice a message to read
			this.lightbarListSelectedMsgIdx = msgListMenu.selectedItemIdx;
			msgHeader = this.GetMsgHdrByIdx(this.lightbarListSelectedMsgIdx, this.showScoresInMsgList);
			this.PrintMessageInfo(msgHeader, true, this.lightbarListSelectedMsgIdx+1);
			console.gotoxy(this.lightbarListCurPos); // Make sure the cursor is still in the right place
			var hdrIsBogus = (msgHeader.hasOwnProperty("isBogus") ? msgHeader.isBogus : false);
			if (!hdrIsBogus)
				// Allow the user to read the current message.
				var readMsg = true;
				if (this.promptToReadMessage)
				{
					// Confirm with the user whether to read the message.
					var sReadMsgConfirmText = this.colors.readMsgConfirmColor
											+ this.colors.readMsgConfirmNumberColor
											+ +(this.GetMsgIdx(msgHeader.number) + 1)
											+ ": Are you sure";
					console.gotoxy(1, console.screen_rows);
					console.clearline();
					readMsg = console.yesno(sReadMsgConfirmText);
				}
				if (readMsg)
				{
					// If there is a search specified and the search result objects are
					// set up for the current sub-board, then the selected message offset
					// should be the search result array index.  Otherwise (if not
					// searching), the message offset should be the actual message offset
					// in the message base.
					if (this.SearchingAndResultObjsDefinedForCurSub())
						retObj.selectedMsgOffset = this.lightbarListSelectedMsgIdx;
					else
					{
						//retObj.selectedMsgOffset = msgHeader.offset;
						retObj.selectedMsgOffset = this.GetMsgIdx(msgHeader.number);
						if (retObj.selectedMsgOffset < 0)
							retObj.selectedMsgOffset = 0;
					// Return from here so that the calling function can switch into
					// reader mode.
					continueOn = false;
					return retObj;
				}
				// Ask the user if  they want to continue reading messages
				if (this.promptToContinueListingMessages)
					continueOn = console.yesno(this.colors["afterReadMsg_ListMorePromptColor"] + "Continue listing messages");
				// If the user chose to continue reading messages, then refresh
				// the screen.  Even if the user chooses not to read the message,
				// the screen needs to be re-drawn so it appears properly.
				if (continueOn)
				{
					this.WriteMsgListScreenTopHeader();
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
		// If userChoice is not a number, then it should be null in this case,
		// and the user would have pressed one of the additional quit keys set
		// up for the menu.  So look at the menu's lastUserInput and do the
		// appropriate thing.
		else if ((lastUserInputUpper == this.msgListKeys.quit) || (lastUserInputUpper == KEY_ESC)) // Quit
			continueOn = false;
			retObj.lastUserInput = "Q"; // So the reader will quit out
		}
		// Numeric digit: The start of a number of a message to read
		else if (lastUserInputUpper.match(/[0-9]/))
		{
			// Put the user's input back in the input buffer to
			// be used for getting the rest of the message number.
			console.ungetstr(lastUserInputUpper);
			// Move the cursor to the bottom of the screen and
			// prompt the user for the message number.
			console.gotoxy(1, console.screen_rows);
			var userInput = this.PromptForMsgNum({ x: 1, y: console.screen_rows }, replaceAtCodesInStr(this.text.readMsgNumPromptText), true, ERROR_PAUSE_WAIT_MS, false);
				// See if the current message header has our "isBogus" property and it's true.
				// Only let the user read the message if it's not a bogus message header.
				// The message header could have the "isBogus" property, for instance, if
				// it's a vote message (introduced in Synchronet 3.17).
				//GetMsgHdrByIdx(pMsgIdx, pExpandFields)
				var tmpMsgHdr = this.GetMsgHdrByIdx(+(userInput-1), false);
				var hdrIsBogus = (tmpMsgHdr.hasOwnProperty("isBogus") ? tmpMsgHdr.isBogus : false);
				if (!hdrIsBogus)
					// Confirm with the user whether to read the message
					var readMsg = true;
					if (this.promptToReadMessage)
					{
						var sReadMsgConfirmText = this.colors.readMsgConfirmColor
												+ this.colors.readMsgConfirmNumberColor
												+ userInput + this.colors.readMsgConfirmColor
												readMsg = console.yesno(sReadMsgConfirmText);
					}
					if (readMsg)
					{
						// Update the message list screen variables
						this.CalcMsgListScreenIdxVarsFromMsgNum(+userInput);
						retObj.selectedMsgOffset = userInput - 1;
						// Return from here so that the calling function can switch
						// into reader mode.
						return retObj;
					}
					else
						this.deniedReadingMessage = true;

					// Prompt the user whether or not to continue listing
					// messages.
					if (this.promptToContinueListingMessages)
						continueOn = console.yesno(this.colors.afterReadMsg_ListMorePromptColor + "Continue listing messages");
					writeWithPause(1, console.screen_rows, "\x01n\x01h\x01yThat's not a readable message.", ERROR_PAUSE_WAIT_MS, "\x01n", true);
			// If the user chose to continue listing messages, then re-draw
			// the screen.
			if (continueOn)
			{
				this.WriteMsgListScreenTopHeader();
				DisplayHelpLine(this.msgListLightbarModeHelpLine);
			}
		}
		// DEL key: Delete a message
		else if (lastUserInputUpper == this.msgListKeys.deleteMessage || lastUserInputUpper == '\x7f' || lastUserInputUpper == '\x08')
		{
			if (this.CanDelete() || this.CanDeleteLastMsg())
			{
				console.gotoxy(1, console.screen_rows);
				// The PromptAndDeleteOrUndeleteMessage() method will prompt the user for confirmation
				// to delete the message and then delete it if confirmed.
				this.PromptAndDeleteOrUndeleteMessage(this.lightbarListSelectedMsgIdx, { x: 1, y: console.screen_rows}, true);
				// In case all messages were deleted, if the user can't view deleted messages,
				// show an appropriate message and don't continue listing messages.
				if (!this.NonDeletedMessagesExist() && !canViewDeletedMsgs())
					continueOn = false;
				else
				{
					// There are still some messages to show, so refresh the screen.
					this.WriteMsgListScreenTopHeader();
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
				}
		else if (lastUserInputUpper == this.msgListKeys.editMsg)
				// See if the current message header has our "isBogus" property and it's true.
				// Only let the user edit the message if it's not a bogus message header.
				// The message header could have the "isBogus" property, for instance, if
				// it's a vote message (introduced in Synchronet 3.17).
				var tmpMsgHdr = this.GetMsgHdrByIdx(this.lightbarListSelectedMsgIdx, false);
				var hdrIsBogus = (tmpMsgHdr.hasOwnProperty("isBogus") ? tmpMsgHdr.isBogus : false);
				if (!hdrIsBogus)
				{
					// Ask the user if they really want to edit the message
					console.gotoxy(1, console.screen_rows);
					console.clearline();
					// Let the user edit the message
					//var returnObj = this.EditExistingMsg(tmpMsgHdr.offset);
					var returnObj = this.EditExistingMsg(this.lightbarListSelectedMsgIdx);
					this.WriteMsgListScreenTopHeader();
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
				}
			else
				drawMenu = false; // No need to re-draw the menu
		}
		// G: Go to a specific message by # (highlight or place that message on the top)
		else if (lastUserInputUpper == this.msgListKeys.goToMsg)
		{
			// Move the cursor to the bottom of the screen and
			// prompt the user for a message number.
			console.gotoxy(1, console.screen_rows);
			var userMsgNum = this.PromptForMsgNum({ x: 1, y: console.screen_rows }, "\n" + replaceAtCodesInStr(this.text.goToMsgNumPromptText), true, ERROR_PAUSE_WAIT_MS, false);
				// Make sure the message number is for a valid message (i.e., it
				// could be an invalid message number if there is a search, where
				// not all message numbers are consecutive).
				if (this.GetMsgHdrByMsgNum(userMsgNum) != null)
				{
					// If the message is on the current page, then just go to and
					// highlight it.  Otherwise, set the user's selected message on the
					// top of the page.  We also have to make sure that this.lightbarListCurPos.y and
					// originalCurpos.y are set correctly.  Also, account for search
					// results if there are any (we'll need to have the correct array
					// index for the search results).
					var chosenMsgIndex = userMsgNum - 1;
					msgListMenu.selectedItemIdx = chosenMsgIndex;
					if ((chosenMsgIndex < msgListMenu.NumItems()) && (chosenMsgIndex >= this.lightbarListTopMsgIdx))
					{
						this.lightbarListSelectedMsgIdx = chosenMsgIndex;
						msgListMenu.selectedItemIdx = this.lightbarListSelectedMsgIdx;
					}
					else
					{
						this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx = chosenMsgIndex;
						msgListMenu.topItemIdx = this.lightbarListTopMsgIdx;
					}
				}
				else
				{
					// The user entered an invalid message number
					console.print("\x01n" + replaceAtCodesInStr(format(this.text.invalidMsgNumText, userMsgNum)) + "\x01n");
					console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS);
				}
			this.WriteMsgListScreenTopHeader();
			DisplayHelpLine(this.msgListLightbarModeHelpLine);
		}
		// C: Change to another message area (sub-board)
		else if (lastUserInputUpper == this.msgListKeys.chgMsgArea)
		{
			if (allowChgSubBoard && (this.subBoardCode != "mail"))
			{
				// Store the current sub-board code so we can see if it changed
				var oldSubCode = bbs.cursub_code;
				// Let the user choose another message area.  If they chose
				// a different message area, then set up the message base
				// object accordingly.
				this.SelectMsgArea();
				if (bbs.cursub_code != oldSubCode)
				{
					var chgSubRetval = this.ChangeSubBoard(bbs.cursub_code);
					continueOn = chgSubRetval.succeeded;
						console.gotoxy(1, console.screen_rows);
						console.cleartoeol("\x01n");
						console.gotoxy(1, console.screen_rows);
						console.print("Loading...");
						this.PopulateHdrsForCurrentSubBoard();
						this.SetUpLightbarMsgListVars();
					}
				// Update the lightbar list variables and refresh the header & help lines
					// Adjust the menu indexes to ensure they're correct for the current sub-board
					this.AdjustLightbarMsgListMenuIdxes(msgListMenu);
					this.WriteMsgListScreenTopHeader();
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
				}
			}
			else
				drawMenu = false; // No need to re-draw the menu
		}
		else if (lastUserInputUpper == this.msgListKeys.showHelp) // Show help
			this.DisplayMsgListHelp(allowChgSubBoard, true);
			// Re-draw the message list header & help line before
			// the menu is re-drawn
			this.WriteMsgListScreenTopHeader();
			DisplayHelpLine(this.msgListLightbarModeHelpLine);
		// Spacebar: Select a message for batch operations (such as batch
		// delete, etc.)
			this.ToggleSelectedMessage(this.subBoardCode, this.lightbarListSelectedMsgIdx);
			// Have the menu draw only the check character column in the
			// next iteration
			msgListMenu.nextDrawOnlyItemSubstr = { start: this.MSGNUM_LEN, end: this.MSGNUM_LEN+1 };
		}
		// Ctrl-A: Select/de-select all messages
		else if (lastUserInputUpper == CTRL_A)
			console.clearline();
			console.gotoxy(1, console.screen_rows);

			// Prompt the user to select All, None (un-select all), or Cancel
			console.print("\x01n\x01gSelect \x01c(\x01hA\x01n\x01c)\x01gll, \x01c(\x01hN\x01n\x01c)\x01gone, or \x01c(\x01hC\x01n\x01c)\x01gancel: \x01h\x01g");
			var userChoice = getAllowedKeyWithMode("ANC", K_UPPER | K_NOCRLF);
			if ((userChoice == "A") || (userChoice == "N"))
			{
				// Toggle all the messages
				var messageSelectToggle = (userChoice == "A");
				var totalNumMessages = this.NumMessages();
				var messageIndex = 0;
				for (messageIndex = 0; messageIndex < totalNumMessages; ++messageIndex)
					this.ToggleSelectedMessage(this.subBoardCode, messageIndex, messageSelectToggle);
				// Have the menu draw only the check character column in the
				// next iteration
				msgListMenu.nextDrawOnlyItemSubstr = { start: this.MSGNUM_LEN, end: this.MSGNUM_LEN+1 };
			else
				drawMenu = false; // No need to re-draw the menu
			DisplayHelpLine(this.msgListLightbarModeHelpLine);
		}
		// Ctrl-D: Batch delete (for selected messages)
		else if (lastUserInputUpper == CTRL_D)
			if (this.CanDelete() || this.CanDeleteLastMsg())
				if (this.NumSelectedMessages() > 0)
				{
					console.gotoxy(1, console.screen_rows);
					console.print("\x01n");
					console.clearline();
					// The PromptAndDeleteOrUndeleteSelectedMessages() method will prompt the user for confirmation
					// to delete the message and then delete it if confirmed.
					this.PromptAndDeleteOrUndeleteSelectedMessages({ x: 1, y: console.screen_rows}, true);
					// In case all messages were deleted, if the user can't view deleted messages,
					// show an appropriate message and don't continue listing messages.
					//if (this.NumMessages(true) == 0)
					if (!this.NonDeletedMessagesExist() && !canViewDeletedMsgs())
						continueOn = false;
					else
					{
						// There are still messages to list, so refresh the header & help lines
						this.WriteMsgListScreenTopHeader();
						DisplayHelpLine(this.msgListLightbarModeHelpLine);
					}
				}
					writeWithPause(1, console.screen_rows, "\x01n\x01h\x01yThere are no selected messages.", ERROR_PAUSE_WAIT_MS, "\x01n", true);
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
				}
			}
		}
		// U: Undelete message(s)
		else if (lastUserInputUpper == this.msgListKeys.undeleteMessage)
		{
			if (this.CanDelete() || this.CanDeleteLastMsg())
				console.gotoxy(1, console.screen_rows);
				console.print("\x01n");
				console.clearline();
				if (this.NumSelectedMessages() > 0)
				{
					// Multi-message undelete
					this.PromptAndDeleteOrUndeleteSelectedMessages({ x: 1, y: console.screen_rows}, false);
				}
				else
				{
					// Single-message undelete
					this.PromptAndDeleteOrUndeleteMessage(this.lightbarListSelectedMsgIdx, { x: 1, y: console.screen_rows}, false);
				}

				// Refresh the header & help line.
				this.WriteMsgListScreenTopHeader();
				DisplayHelpLine(this.msgListLightbarModeHelpLine);
			}
		}
		else if (lastUserInputUpper == this.msgListKeys.userSettings)
		{
			var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) { DisplayHelpLine(pReader.msgListLightbarModeHelpLine); });
			lastUserInputUpper = "";
			drawMenu = userSettingsRetObj.needWholeScreenRefresh;
			// In case the user changed their twitlist, re-filter the messages for this sub-board
			if (userSettingsRetObj.userTwitListChanged)
			{
				console.gotoxy(1, console.screen_rows);
				console.crlf();
				console.print("\x01nTwitlist changed; re-filtering..");
				var tmpMsgbase = new MsgBase(this.subBoardCode);
				if (tmpMsgbase.open())
				{
					var tmpAllMsgHdrs = tmpMsgbase.get_all_msg_headers(true);
					tmpMsgbase.close();
					this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpAllMsgHdrs, true);
				}
				else
					console.print("\x01y\x01hFailed to open the messagbase!\x01\r\n\x01p");
				this.SetUpLightbarMsgListVars();
				msgListMenu = this.CreateLightbarMsgListMenu();
				drawMenu = true;
			}
			if (userSettingsRetObj.needWholeScreenRefresh)
			{
				this.WriteMsgListScreenTopHeader();
				DisplayHelpLine(this.msgListLightbarModeHelpLine);
			}
			else
				msgListMenu.DrawPartialAbs(userSettingsRetObj.optionBoxTopLeftX, userSettingsRetObj.optionBoxTopLeftY, userSettingsRetObj.optionBoxWidth, userSettingsRetObj.optionBoxHeight);
		}
		// S: Sorting options
		else if (lastUserInputUpper == "S")
		{
			// Refresh the help line
			DisplayHelpLine(this.msgListLightbarModeHelpLine);
		}
	this.lightbarListSelectedMsgIdx = msgListMenu.selectedItemIdx;
	this.lightbarListTopMsgIdx = msgListMenu.topItemIdx;
// For the DigDistMsgLister class: Creates & returns a DDLightbarMenu for
// performing the lightbar message list.
function DigDistMsgReader_CreateLightbarMsgListMenu()
{
	// Start & end indexes for the various items in each message list row
	var msgListIdxes = {
		msgNumStart: 0,
		msgNumEnd: this.MSGNUM_LEN,
		selectMarkStart: this.MSGNUM_LEN,
		selectMarkEnd: this.MSGNUM_LEN+1,
	};
	msgListIdxes.fromNameStart = this.MSGNUM_LEN + 1;
	msgListIdxes.fromNameEnd = msgListIdxes.fromNameStart + +this.FROM_LEN + 1;
	msgListIdxes.toNameStart = msgListIdxes.fromNameEnd;
	msgListIdxes.toNameEnd = msgListIdxes.toNameStart + +this.TO_LEN + 1;
	msgListIdxes.subjStart = msgListIdxes.toNameEnd;
	msgListIdxes.subjEnd = msgListIdxes.subjStart + +this.SUBJ_LEN + 1;
	if (this.showScoresInMsgList)
	{
		msgListIdxes.scoreStart = msgListIdxes.subjEnd;
		msgListIdxes.scoreEnd = msgListIdxes.scoreStart + +this.SCORE_LEN + 1;
		msgListIdxes.dateStart = msgListIdxes.scoreEnd;
	}
	else
		msgListIdxes.dateStart = msgListIdxes.subjEnd;
	msgListIdxes.dateEnd = msgListIdxes.dateStart + +this.DATE_LEN + 1;
	msgListIdxes.timeStart = msgListIdxes.dateEnd;
	msgListIdxes.timeEnd = console.screen_columns - 1; // msgListIdxes.timeStart + +this.TIME_LEN + 1;
	var msgListMenuHeight = console.screen_rows - this.lightbarMsgListStartScreenRow;
	var msgListMenu = new DDLightbarMenu(1, this.lightbarMsgListStartScreenRow, console.screen_columns, msgListMenuHeight);
	msgListMenu.scrollbarEnabled = true;
	msgListMenu.borderEnabled = false;
	msgListMenu.SetColors({
		itemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListMsgNumColor},
		            {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor},
		            {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListFromColor},
		            {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToColor},
		            {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListSubjectColor},
		            {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListDateColor},
		            {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListTimeColor}],
		altItemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListToUserMsgNumColor},
		               {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor},
		               {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListToUserFromColor},
		               {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToUserToColor},
		               {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListToUserSubjectColor},
		               {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListToUserDateColor},
		               {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListToUserTimeColor}],
		selectedItemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListMsgNumHighlightColor},
		                    {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor},
		                    {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListFromHighlightColor},
		                    {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToHighlightColor},
		                    {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListSubjHighlightColor},
		                    {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListDateHighlightColor},
		                    {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListTimeHighlightColor}],
		altSelectedItemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListMsgNumHighlightColor},
		                       {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor},
		                       {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListFromHighlightColor},
		                       {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToHighlightColor},
		                       {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListSubjHighlightColor},
		                       {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListDateHighlightColor},
		                       {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListTimeHighlightColor}]
	});
	// If we are to show message vote scores in the list (i.e., if the
	// user's terminal is wide enough), then splice in color specifiers
	// for the score column.
	if (this.showScoresInMsgList)
	{
		msgListMenu.colors.itemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListScoreColor});
		msgListMenu.colors.altItemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListToUserScoreColor});
		msgListMenu.colors.selectedItemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListScoreHighlightColor + this.colors.msgListHighlightBkgColor});
		msgListMenu.colors.altSelectedItemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListScoreHighlightColor + this.colors.msgListHighlightBkgColor});
	}

	msgListMenu.multiSelect = false;
	msgListMenu.ampersandHotkeysInItems = false;
	msgListMenu.wrapNavigation = false;

	// Add additional keypresses for quitting the menu's input loop so we can
	// respond to these keys
	// Ctrl-A: Select all messages
	var additionalQuitKeys = "EeqQgGcCsS ?0123456789" + CTRL_A + this.msgListKeys.batchDelete + this.msgListKeys.userSettings;
	if (this.CanDelete() || this.CanDeleteLastMsg())
		additionalQuitKeys += this.msgListKeys.deleteMessage + this.msgListKeys.undeleteMessage.toLowerCase() + this.msgListKeys.undeleteMessage.toUpperCase();
		additionalQuitKeys += '\x7f'; // Ensure DEL is in there
		additionalQuitKeys += '\x08'; // Ensure BACKSPACE is in there (can be an alternate for delete)
	}
	msgListMenu.AddAdditionalQuitKeys(additionalQuitKeys);

	// Change the menu's NumItems() and GetItem() function to reference
	// the message list in this object rather than add the menu items
	// to the menu
	msgListMenu.msgReader = this; // Add this object to the menu object
	msgListMenu.NumItems = function() {
		return this.msgReader.NumMessages();
	};
	msgListMenu.GetItem = function(pItemIndex) {
		var menuItemObj = this.MakeItemWithRetval(-1);
		var itemIdx = (this.msgReader.reverseListOrder ? this.msgReader.NumMessages() - pItemIndex - 1 : pItemIndex);
		// In order to get vote score information (displayed if the user's terminal is wide
		// enough), the 2nd parameter to GetMsgHdrByIdx() should be true.
		var msgHdr = this.msgReader.GetMsgHdrByIdx(itemIdx, this.msgReader.showScoresInMsgList);
		if (msgHdr != null)
		{
			// When setting the item text, call PrintMessageInfo with true as
			// the last parameter to return the string instead
			menuItemObj.text = strip_ctrl(this.msgReader.PrintMessageInfo(msgHdr, false, itemIdx+1, true));
			menuItemObj.retval = msgHdr.number;
			if (this.msgReader.subBoardCode != "mail")
				menuItemObj.useAltColors = userHandleAliasNameMatch(msgHdr.to);
			// If the message is marked as deleted, ensure the correct color is used
			// for the mark character in the menu
			if ((msgHdr.attr & MSG_DELETE) == MSG_DELETE)
			{
				var fromColor = this.msgReader.colors.msgListFromColor;
				var toColor = this.msgReader.colors.msgListToColor;
				var subjColor = this.msgReader.colors.msgListSubjectColor;
				if ((this.msgReader.subBoardCode != "mail") && (userHandleAliasNameMatch(msgHdr.to)))
				{
					fromColor = this.msgReader.colors.msgListToUserFromColor;
					toColor = this.msgReader.colors.msgListToUserToColor;
					subjColor = this.msgReader.colors.msgListToUserSubjectColor;
				}
				menuItemObj.itemColor = [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.msgReader.colors.msgListMsgNumColor},
				                         {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: "\x01r\x01h\x01i"},
				                         {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: fromColor},
				                         {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: toColor},
				                         {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: subjColor},
				                         {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.msgReader.colors.msgListDateColor},
				                         {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.msgReader.colors.msgListTimeColor}];
				menuItemObj.itemSelectedColor = [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.msgReader.colors.msgListMsgNumHighlightColor},
				                                 {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: "\x01r\x01h\x01i" + this.msgReader.colors.msgListHighlightBkgColor},
				                                 {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.msgReader.colors.msgListFromHighlightColor},
				                                 {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.msgReader.colors.msgListToHighlightColor},
				                                 {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.msgReader.colors.msgListSubjHighlightColor},
				                                 {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.msgReader.colors.msgListDateHighlightColor},
				                                 {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.msgReader.colors.msgListTimeHighlightColor}];
			}
		}
		return menuItemObj;
	};

	// Adjust the menu indexes to ensure they're correct for the current sub-board
	this.AdjustLightbarMsgListMenuIdxes(msgListMenu);

	return msgListMenu;
}
// For the DigDistMsgLister class: Creates a DDLightbarMenu object for the user to choose
// a message group.
//
// Return value: A DDLightbarMenu object set up to let the user choose a message group
function DigDistMsgReader_CreateLightbarMsgGrpMenu()
{
	// Start & end indexes for the various items in each mssage group list row
	// Selected mark, group#, description, # sub-boards
	var msgGrpListIdxes = {
		markCharStart: 0,
		markCharEnd: 1,
		grpNumStart: 1,
		grpNumEnd: 2 + (+this.areaNumLen)
	};
	msgGrpListIdxes.descStart = msgGrpListIdxes.grpNumEnd;
	msgGrpListIdxes.descEnd = msgGrpListIdxes.descStart + +this.msgGrpDescLen;
	msgGrpListIdxes.numItemsStart = msgGrpListIdxes.descEnd;
	msgGrpListIdxes.numItemsEnd = msgGrpListIdxes.numItemsStart + +this.numItemsLen;
	// Set numItemsEnd to -1 to let the whole rest of the lines be colored
	msgGrpListIdxes.numItemsEnd = -1;
	var listStartRow = this.areaChangeHdrLines.length + 2;
	var msgGrpMenuHeight = console.screen_rows - listStartRow;
	var msgGrpMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, msgGrpMenuHeight);
	msgGrpMenu.scrollbarEnabled = true;
	msgGrpMenu.borderEnabled = false;
	msgGrpMenu.SetColors({
		itemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor},
		            {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaChooserMsgAreaNumColor},
		            {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescColor},
		            {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsColor}],
		selectedItemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor + this.colors.areaChooserMsgAreaBkgHighlightColor},
		                    {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaChooserMsgAreaNumHighlightColor},
		                    {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescHighlightColor},
		                    {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsHighlightColor}]
	});

	msgGrpMenu.multiSelect = false;
	msgGrpMenu.ampersandHotkeysInItems = false;
	msgGrpMenu.wrapNavigation = false;

	// Add additional keypresses for quitting the menu's input loop so we can
	// respond to these keys
	msgGrpMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F);

	// Change the menu's NumItems() and GetItem() function to reference
	// the message list in this object rather than add the menu items
	// to the menu
	msgGrpMenu.msgReader = this; // Add this object to the menu object
	msgGrpMenu.NumItems = function() {
		return msg_area.grp_list.length;
	};
	msgGrpMenu.GetItem = function(pGrpIndex) {
		var menuItemObj = this.MakeItemWithRetval(-1);
		if ((pGrpIndex >= 0) && (pGrpIndex < msg_area.grp_list.length))
		{
			menuItemObj.text = format(((typeof(bbs.curgrp) == "number") && (pGrpIndex == msg_area.sub[this.msgReader.subBoardCode].grp_index)) ? "*" : " ");
			menuItemObj.text += format(this.msgReader.msgGrpListPrintfStr, +(pGrpIndex+1),
			                           msg_area.grp_list[pGrpIndex].description.substr(0, this.msgReader.msgGrpDescLen),
			                           msg_area.grp_list[pGrpIndex].sub_list.length);
			menuItemObj.text = strip_ctrl(menuItemObj.text);
			menuItemObj.retval = pGrpIndex;
		}

		return menuItemObj;
	};

	// Set the currently selected item to the current group
	msgGrpMenu.selectedItemIdx = msg_area.sub[this.subBoardCode].grp_index;
	if (msgGrpMenu.selectedItemIdx >= msgGrpMenu.topItemIdx+msgGrpMenu.GetNumItemsPerPage())
		msgGrpMenu.topItemIdx = msgGrpMenu.selectedItemIdx - msgGrpMenu.GetNumItemsPerPage() + 1;

	return msgGrpMenu;
}
// For the DigDistMsgLister class: Creates a DDLightbarMenu object for the user to choose
// a sub-board within a message group.
//
// Parameters:
//  pGrpIdx: The index of the group to list sub-boards for
//
// Return value: A DDLightbarMenu object set up to let the user choose a sub-board within the
//               given message group
function DigDistMsgReader_CreateLightbarSubBoardMenu(pGrpIdx)
{
	// Start & end indexes for the various items in each sub-board list row
	// Selected mark, group#, description, # sub-boards
	var subBrdListIdxes = {
		markCharStart: 0,
		markCharEnd: 1,
		subNumStart: 1,
		subNumEnd: 2 + (+this.areaNumLen)
	};
	subBrdListIdxes.descStart = subBrdListIdxes.subNumEnd;
	subBrdListIdxes.descEnd = subBrdListIdxes.descStart + +(this.subBoardListPrintfInfo[pGrpIdx].nameLen) + 1;
	subBrdListIdxes.numItemsStart = subBrdListIdxes.descEnd;
	subBrdListIdxes.numItemsEnd = subBrdListIdxes.numItemsStart + +(this.subBoardListPrintfInfo[pGrpIdx].numMsgsLen) + 1;
	subBrdListIdxes.dateStart = subBrdListIdxes.numItemsEnd;
	subBrdListIdxes.dateEnd = subBrdListIdxes.dateStart + +this.dateLen + 1;
	subBrdListIdxes.timeStart = subBrdListIdxes.dateEnd;
	// Set timeEnd to -1 to let the whole rest of the lines be colored
	subBrdListIdxes.timeEnd = -1;
	var listStartRow = this.areaChangeHdrLines.length + 3;
	var subBrdMenuHeight = console.screen_rows - listStartRow;
	var subBoardMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, subBrdMenuHeight);
	subBoardMenu.scrollbarEnabled = true;
	subBoardMenu.borderEnabled = false;
	subBoardMenu.SetColors({
		itemColor: [{start: subBrdListIdxes.markCharStart, end: subBrdListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor},
		            {start: subBrdListIdxes.subNumStart, end: subBrdListIdxes.subNumEnd, attrs: this.colors.areaChooserMsgAreaNumColor},
		            {start: subBrdListIdxes.descStart, end: subBrdListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescColor},
		            {start: subBrdListIdxes.numItemsStart, end: subBrdListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsColor},
		            {start: subBrdListIdxes.dateStart, end: subBrdListIdxes.dateEnd, attrs: this.colors.areaChooserMsgAreaLatestDateColor},
		            {start: subBrdListIdxes.timeStart, end: subBrdListIdxes.timeEnd, attrs: this.colors.areaChooserMsgAreaLatestTimeColor}],
		selectedItemColor: [{start: subBrdListIdxes.markCharStart, end: subBrdListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor + this.colors.areaChooserMsgAreaBkgHighlightColor},
		                    {start: subBrdListIdxes.subNumStart, end: subBrdListIdxes.subNumEnd, attrs: this.colors.areaChooserMsgAreaNumHighlightColor},
		                    {start: subBrdListIdxes.descStart, end: subBrdListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescHighlightColor},
		                    {start: subBrdListIdxes.numItemsStart, end: subBrdListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsHighlightColor},
		                    {start: subBrdListIdxes.dateStart, end: subBrdListIdxes.dateEnd, attrs: this.colors.areaChooserMsgAreaDateHighlightColor},
		                    {start: subBrdListIdxes.timeStart, end: subBrdListIdxes.timeEnd, attrs: this.colors.areaChooserMsgAreaTimeHighlightColor}]
	});

	subBoardMenu.multiSelect = false;
	subBoardMenu.ampersandHotkeysInItems = false;
	subBoardMenu.wrapNavigation = false;

	// Add additional keypresses for quitting the menu's input loop so we can
	// respond to these keys
	subBoardMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F);

	// Change the menu's NumItems() and GetItem() function to reference
	// the message list in this object rather than add the menu items
	// to the menu
	subBoardMenu.msgReader = this; // Add this object to the menu object
	subBoardMenu.grpIdx = pGrpIdx;
	subBoardMenu.NumItems = function() {
		return msg_area.grp_list[pGrpIdx].sub_list.length;
	};
	subBoardMenu.GetItem = function(pSubIdx) {
		var menuItemObj = this.MakeItemWithRetval(-1);
		if ((pSubIdx >= 0) && (pSubIdx < msg_area.grp_list[this.grpIdx].sub_list.length))
		{
			//var highlight = (msg_area.grp_list[this.grpIdx].sub_list[pSubIdx].code.toUpperCase() == this.msgReader.subBoardCode.toUpperCase());
			menuItemObj.text = this.msgReader.GetMsgSubBoardLine(this.grpIdx, pSubIdx, false);
			menuItemObj.text = strip_ctrl(menuItemObj.text);
			menuItemObj.retval = pSubIdx;
		}

		return menuItemObj;
	};

	// Set the currently selected item to the current group
	if (msg_area.sub[this.subBoardCode].grp_index == pGrpIdx)
	{
		subBoardMenu.selectedItemIdx = msg_area.sub[this.subBoardCode].index;
		if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage())
			subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1;
	}
	else
	{
		subBoardMenu.selectedItemIdx = 0;
		subBoardMenu.topItemIdx = 0;
	}
// For the DigDistMsgLister class: Adjusts lightbar menu indexes for a message list menu
function DigDistMsgReader_AdjustLightbarMsgListMenuIdxes(pMsgListMenu)
{
	pMsgListMenu.selectedItemIdx = this.lightbarListSelectedMsgIdx;
	pMsgListMenu.topItemIdx = this.lightbarListTopMsgIdx;

	// In the DDLightbarMenu class, the top index on the last page should
	// allow for displaying a full page of items.  So if
	// this.lightbarListTopMsgIdx is beyond the top index for the last
	// page in the menu object, then adjust this.lightbarListTopMsgIdx.
	var menuTopItemIdxOnLastPage = pMsgListMenu.GetTopItemIdxOfLastPage();
	if (pMsgListMenu.topItemIdx > menuTopItemIdxOnLastPage)
	{
		pMsgListMenu.topItemIdx = menuTopItemIdxOnLastPage;
		this.lightbarListTopMsgIdx = menuTopItemIdxOnLastPage;
	}
	// TODO: Ensure this.lightbarListTopMsgIdx is always correct for the last page
}
// For the DigDistMsgListerClass: Prints a line of information about
// a message.
//
// Parameters:
//  pMsgHeader: The message header object, returned by MsgBase.get_msg_header().
//  pHighlight: Optional boolean - Whether or not to highlight the line (true) or
//              use the standard colors (false).
//  pMsgNum: Optional - A number to use for the message instead of the number/offset
//           in the message header
//  pReturnStrInstead: Optional boolean - Whether or not to return a formatted string
//                     instead of printing to the console.  Defaults to false.
function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum, pReturnStrInstead)
{
	// pMsgHeader must be a valid object.
	if (typeof(pMsgHeader) == "undefined")
		return;
	if (pMsgHeader == null)
		return;

	var highlight = false;
	// Get the message's import date & time as strings.  If
	// this.msgList_displayMessageDateImported is true, use the message import date.
	// Otherwise, use the message written date.
	var sDate;
	var sTime;
	if (this.msgList_displayMessageDateImported)
	{
		sDate = strftime("%Y-%m-%d", pMsgHeader.when_imported_time);
		sTime = strftime("%H:%M:%S", pMsgHeader.when_imported_time);
	}
	else
	{
		//sDate = strftime("%Y-%m-%d", pMsgHeader.when_written_time);
		//sTime = strftime("%H:%M:%S", pMsgHeader.when_written_time);
		var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(pMsgHeader);
		if (msgWrittenLocalTime != -1)
		{
			sDate = strftime("%Y-%m-%d", msgWrittenLocalTime);
			sTime = strftime("%H:%M:%S", msgWrittenLocalTime);
		}
		else
		{
			sDate = strftime("%Y-%m-%d", pMsgHeader.when_written_time);
			sTime = strftime("%H:%M:%S", pMsgHeader.when_written_time);
		}
	//var msgNum = (typeof(pMsgNum) == "number" ? pMsgNum : pMsgHeader.offset+1);
	var msgNum = (typeof(pMsgNum) == "number" ? pMsgNum : this.GetMsgIdx(pMsgHeader.number)+1);
	if (msgNum == 0) // In case GetMsgIdx() returns -1 for failure
		msgNum = 1;
	// Determine if the message has been deleted.
	var msgDeleted = ((pMsgHeader.attr & MSG_DELETE) == MSG_DELETE);

	// msgIndicatorChar will contain (possibly) a character to display after
	// the message number to indicate whether it has been deleted, selected,
	// etc.  If not, then it will just be a space.
	var msgIndicatorChar = " ";
	// Get the message score value
	var msgVoteInfo = getMsgUpDownvotesAndScore(pMsgHeader);
	// Ensure the score number can fit within 4 digits
	if (msgVoteInfo.voteScore > 9999)
		msgVoteInfo.voteScore = 9999;
	else if (msgVoteInfo.voteScore < -999)
		msgVoteInfo.voteScore = -999;

	// Generate the string with the message header information.
	var msgHdrStr = "";
	// Note: The message header has the following fields:
	// 'number': The message number
	// 'offset': The message offset
	// 'to': Who the message is directed to (string)
	// 'from' Who wrote the message (string)
	// 'subject': The message subject (string)
	// 'date': The date - Full text (string)
	// To access one of these, use brackets; i.e., msgHeader['to']
	if (highlight)
	{
		// For any indicator character next to the message, prioritize selected, then deleted, then attachments
		if (this.MessageIsSelected(this.subBoardCode, msgNum-1))
			msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + CHECK_CHAR + "\x01n";
		else if (msgDeleted)
			msgIndicatorChar = "\x01n\x01r\x01h\x01i" + this.colors.msgListHighlightBkgColor + "*\x01n";
		else if (msgHdrHasAttachmentFlag(pMsgHeader))
			msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + "A\x01n";
		var fromName = pMsgHeader.from;
		// If the message was posted anonymously and the logged-in user is
		// not the sysop, then show "Anonymous" for the 'from' name.
		if (!user.is_sysop && ((pMsgHeader.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS))
			msgHdrStr += format(this.sMsgInfoFormatHighlightStr, msgNum, msgIndicatorChar,
			       pMsgHeader.to.substr(0, this.TO_LEN),
			       pMsgHeader.subject.substr(0, this.SUBJ_LEN),
			       msgVoteInfo.voteScore, sDate, sTime);
		}
		else
		{
			msgHdrStr += format(this.sMsgInfoFormatHighlightStr, msgNum, msgIndicatorChar,
			       pMsgHeader.to.substr(0, this.TO_LEN),
			       pMsgHeader.subject.substr(0, this.SUBJ_LEN),
			       sDate, sTime);
		}
		// For any indicator character next to the message, prioritize selected, then deleted, then attachments
		if (this.MessageIsSelected(this.subBoardCode, msgNum-1))
			msgIndicatorChar = "\x01n" +  this.colors.selectedMsgMarkColor + CHECK_CHAR + "\x01n";
		else if (msgDeleted)
			msgIndicatorChar = "\x01n\x01r\x01h\x01i*\x01n";
		else if (msgHdrHasAttachmentFlag(pMsgHeader))
			msgIndicatorChar = "\x01n" +  this.colors.selectedMsgMarkColor + "A\x01n";
		// Determine whether to use the normal, "to-user", or "from-user" format string.
		// The differences are the colors.  Then, output the message information line.
		var toNameUpper = pMsgHeader.to.toUpperCase();
		var msgToUser = ((toNameUpper == user.alias.toUpperCase()) || (toNameUpper == user.name.toUpperCase()) || (toNameUpper == user.handle.toUpperCase()));
		var fromNameUpper = pMsgHeader.from.toUpperCase();
		var msgIsFromUser = ((fromNameUpper == user.alias.toUpperCase()) || (fromNameUpper == user.name.toUpperCase()) || (fromNameUpper == user.handle.toUpperCase()));
		var formatStr = ""; // Format string for printing the message information
		if (this.readingPersonalEmail)
			formatStr = this.sMsgInfoFormatStr;
		else
			formatStr = (msgToUser ? this.sMsgInfoToUserFormatStr : (msgIsFromUser ? this.sMsgInfoFromUserFormatStr : this.sMsgInfoFormatStr));
		var fromName = pMsgHeader.from;
		// If the message was posted anonymously and the logged-in user is
		// not the sysop, then show "Anonymous" for the 'from' name.
		if (!user.is_sysop && ((pMsgHeader.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS))
			msgHdrStr += format(formatStr, msgNum, msgIndicatorChar, fromName.substr(0, this.FROM_LEN),
			       pMsgHeader.to.substr(0, this.TO_LEN), pMsgHeader.subject.substr(0, this.SUBJ_LEN),
			       msgVoteInfo.voteScore, sDate, sTime);
		}
		else
		{
			msgHdrStr += format(formatStr, msgNum, msgIndicatorChar, fromName.substr(0, this.FROM_LEN),
			       pMsgHeader.to.substr(0, this.TO_LEN), pMsgHeader.subject.substr(0, this.SUBJ_LEN),
			       sDate, sTime);
		}

	var returnStrInstead = (typeof(pReturnStrInstead) == "boolean" ? pReturnStrInstead : false);
	if (!returnStrInstead)
	{
		console.print(msgHdrStr);
		console.cleartoeol("\x01n"); // To clear away any extra text that may have been entered by the user
}
// For the traditional interface of DigDistMsgListerClass: Prompts the user to
// continue or read a message (by number).
//
// Parameters:
//  pStart: Whether or not we're on the first page (true or false)
//  pEnd: Whether or not we're at the last page (true or false)
//  pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow
//                     changing to another sub-board.  Defaults to true.
//
// Return value: An object with the following properties:
//               continueOn: Boolean, whether or not the user wants to continue
//                           listing the messages
//               userInput: The user's input
//               selectedMsgOffset: The offset of the message selected to read,
//                                  if one was selected.  If a message was not
//                                  selected, this will be -1.
function DigDistMsgReader_PromptContinueOrReadMsg(pStart, pEnd, pAllowChgSubBoard)
	// Create the return object and set some initial default values
	var retObj = {
		continueOn: true,
		userInput: "",
		selectedMsgOffset: -1
	};

	var allowChgSubBoard = (typeof(pAllowChgSubBoard) == "boolean" ? pAllowChgSubBoard : true);

	var continueOn = true;
	// Prompt the user whether or not to continue or to read a message
	// (by message number).  Make use of the different prompt texts,
	// depending whether we're at the beginning, in the middle, or at
	// the end of the message list.
	var userInput = "";
	var allowedKeys = "?GS"; // ? = help, G = Go to message #, S = Select message(s), Ctrl-D: Batch delete
	if (allowChgSubBoard)
		allowedKeys += "C"; // Change to another message area
	if (this.CanDelete() || this.CanDeleteLastMsg())
		allowedKeys += this.enhReaderKeys.deleteMessage;
		allowedKeys += '\x7f';
		allowedKeys += '\x08';
	}
	if (this.CanEdit())
		allowedKeys += "E"; // Edit
	if (pStart && pEnd)
	{
		// This is the only page.
		console.print(this.msgListOnlyOnePageContinuePrompt);
		// Get input from the user.  Allow only Q (quit).
		allowedKeys += "Q";
	}
	else if (pStart)
	{
		// We're on the first page.
		console.print(this.sStartContinuePrompt);
		// Get input from the user.  Allow only L (last), N (next), or Q (quit).
		allowedKeys += "LNQ";
	}
	else if (pEnd)
	{
		// We're on the last page.
		console.print(this.sEndContinuePrompt);
		// Get input from the user.  Allow only F (first), P (previous), or Q (quit).
		allowedKeys += "FPQ";
	}
	else
	{
		// We're neither on the first nor last page.  Allow F (first), L (last),
		// N (next), P (previous), or Q (quit).
		console.print(this.sContinuePrompt);
		allowedKeys += "FLNPQ";
	}
	// Get the user's input.  Allow CTRL-D (batch delete) without echoing it.
	// If the user didn't press CTRL-L, allow the keys in allowedKeys or a number from 1
	userInput = console.getkey(K_NOECHO);
	if (userInput != CTRL_D)
	{
		console.ungetstr(userInput);
		userInput = console.getkeys(allowedKeys, this.HighestMessageNum()).toString();
	}
	if (userInput == "Q")
		continueOn = false;

	// If the user has typed all numbers, then read that message.
	if ((userInput != "") && /^[0-9]+$/.test(userInput))
	{
		// If the user entered a valid message number, then let the user read the message.
		// The message number might be invalid if there are search results that
		// have non-continuous message numbers.
		if (this.IsValidMessageNum(userInput))
		{
			// See if the current message header has our "isBogus" property and it's true.
			// Only let the user read the message if it's not a bogus message header.
			// The message header could have the "isBogus" property, for instance, if
			// it's a vote message (introduced in Synchronet 3.17).
			var tmpMsgHdr = this.GetMsgHdrByIdx(+(userInput-1), false);
			var hdrIsBogus = (tmpMsgHdr.hasOwnProperty("isBogus") ? tmpMsgHdr.isBogus : false);
			if (!hdrIsBogus)
				// Confirm with the user whether to read the message
				var readMsg = true;
				if (this.promptToReadMessage)
				{
					var sReadMsgConfirmText = this.colors["readMsgConfirmColor"]
											+ "Read message "
											+ this.colors["readMsgConfirmNumberColor"]
											+ userInput + this.colors["readMsgConfirmColor"]
											+ ": Are you sure";
					readMsg = console.yesno(sReadMsgConfirmText);
				}
				if (readMsg)
				{
					// Update the message list screen variables
					this.CalcMsgListScreenIdxVarsFromMsgNum(+userInput);
					// Return from here so that the calling function can switch
					// into reader mode.
					retObj.continueOn = continueOn;
					retObj.userInput = userInput;
					retObj.selectedMsgOffset = userInput-1;
					return retObj;
				}
				else
					this.deniedReadingMessage = true;

				// Prompt the user whether or not to continue listing
				// messages.
				if (this.promptToContinueListingMessages)
				{
					continueOn = console.yesno(this.colors["afterReadMsg_ListMorePromptColor"] +
											   "Continue listing messages");
				}
				console.print("\x01n\x01h\x01yThat's not a readable message.\x01n");
			}
		}
		else
		{
			// The user entered an invalid message number.
			console.print("\x01n\x01h\x01w" + userInput + " \x01y is not a valid message number.\x01n");
			console.crlf();
			console.pause();
			continueOn = true;
		}
	}

	// Make sure color highlighting is turned off

	// Fill the return object with the required values, and return it.
	retObj.continueOn = continueOn;
	retObj.userInput = userInput;
	return retObj;
}
// For the DigDistMsgReader Class: Given a message number of a message in the
// current message area, lets the user read a message and allows the user to
// respond, etc.  This is an enhanced version that allows scrolling up & down
// the message with the up & down arrow keys, and the left & right arrow keys
// will return from the function to allow calling code to navigate back & forth
// through the message sub-board.
//
// Parameters:
//  pOffset: The offset of the message to be read
//  pAllowChgArea: Optional boolean - Whether or not to allow changing the
//                 message area
//
// Return value: And object with the following properties:
//               offsetValid: Boolean - Whether or not the passed-in offset was valid
//               msgDeleted: Boolean - Whether or not the message is marked as deleted
//                           (not deleted by the user in the reader)
//               userReplied: Boolean - Whether or not the user replied to the message.
//               lastKeypress: The last keypress from the user - For navigation purposes
//               newMsgOffset: The offset of another message to read, if the user
//                             input another message number.  If the user did not
//                             input another message number, this will be -1.
//               nextAction: The next action for the caller to take.  This will be
//                           one of the values specified by the ACTION_* constants.
//                           This defaults to ACTION_NONE on error.
//               refreshEnhancedRdrHelpLine: Boolean - Whether or not to refresh the
//                                           enhanced reader help line on the screen
//                                           (for instance, if switched to the traditional
//                                            non-scrolling interface to read the message)
function DigDistMsgReader_ReadMessageEnhanced(pOffset, pAllowChgArea)
{
	var retObj = {
		offsetValid: true,
		msgNotReadable: false,
		userReplied: false,
		lastKeypress: "",
		newMsgOffset: -1,
		nextAction: ACTION_NONE,
		refreshEnhancedRdrHelpLine: false
	};
	// Get the message header.  Don't expand fields since we may need to save
	// the header later with the MSG_READ attribute.
	//var msgHeader = this.GetMsgHdrByIdx(pOffset, false);
	// Get the message header.  Get expanded fields so that we can show any
	// voting stats/responses that may be included with the message.
	var msgHeader = this.GetMsgHdrByIdx(pOffset, true);
		//console.print("\x01n" + replaceAtCodesInStr(format(this.text.invalidMsgNumText, +(pOffset+1))) + "\x01n");
		//console.crlf();
		console.putmsg("\x01n" + format(this.text.invalidMsgNumText, +(pOffset+1)) + "\x01n");
		console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS);
		retObj.offsetValid = false;
		return retObj;
	}

	// If this message is not readable for the user (it's marked as deleted and
	// the system is set to not show deleted messages, etc.), then don't let the
	// user read it, and just silently return.
	// TODO: If the message is not readable, this will end up causing an infinite loop.
	retObj.msgNotReadable = !isReadableMsgHdr(msgHeader, this.subBoardCode);
	if (retObj.msgNotReadable)
		return retObj;

	// Update the message list index variables so that the message list is in
	// the right spot for the message currently being read
	this.CalcMsgListScreenIdxVarsFromMsgNum(pOffset+1);

	// Check the pAllowChgArea parameter.  If it's a boolean, then use it.  If
	// not, then check to see if we're reading personal mail - If not, then allow
	// the user to change to a different message area.
	var allowChgMsgArea = true;
	if (typeof(pAllowChgArea) == "boolean")
		allowChgMsgArea = pAllowChgArea;
	else
		allowChgMsgArea = (this.subBoardCode != "mail");

	// Get the message text and see if it has any ANSI codes.  Remove any pause
	// codes it might have.  If it has ANSI codes, then don't use the scrolling
	// interface so that the ANSI gets displayed properly.
	var messageText = this.GetMsgBody(msgHeader);
	if (msgHdrHasAttachmentFlag(msgHeader))
	{
		messageText = "\x01n\x01g\x01h- This message contains one or more attachments. Press CTRL-A to download.\x01n\r\n"
		            + "\x01n\x01g\x01h--------------------------------------------------------------------------\x01n\r\n"
	var useScrollingInterface = this.scrollingReaderInterface && console.term_supports(USER_ANSI);
	// If we switch to the non-scrolling interface here, then the calling method should
	// refresh the enhanced reader help line on the screen.
	retObj.refreshEnhancedRdrHelpLine = (this.scrollingReaderInterface && !useScrollingInterface);
	// If the current message is new to the user, update the number of posts read this session.
	if (pOffset > this.GetScanPtrMsgIdx())
		++bbs.posts_read;
	// Use the scrollable reader interface if the setting is enabled & the user's
	// terminal supports ANSI.  Otherwise, use a more traditional user interface.
		retObj = this.ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, pOffset);
		retObj = this.ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, pOffset);
	// Mark the message as read if it was written to the current user
	if (userNameHandleAliasMatch(msgHeader.to) && ((msgHeader.attr & MSG_READ) == 0))
		// Using applyAttrsInMsgHdrInMessagbase(), which loads the header without
		// expanded fields and saves the attributes with that header.
		var saveRetObj = applyAttrsInMsgHdrInMessagbase(this.subBoardCode, msgHeader.number, MSG_READ);
		if (this.SearchTypePopulatesSearchResults() && saveRetObj.saveSucceeded)
			this.RefreshHdrInSavedArrays(pOffset, MSG_READ, true);
	// If not reading personal email and not doing a search, then update the
	// scan & last read message pointers.
	if ((this.subBoardCode != "mail") && (this.searchType == SEARCH_NONE))
		if (msgHeader.number > GetScanPtrOrLastMsgNum(this.subBoardCode))
			msg_area.sub[this.subBoardCode].scan_ptr = msgHeader.number;
		msg_area.sub[this.subBoardCode].last_read = msgHeader.number;
	}

	return retObj;
}
// Helper method for ReadMessageEnhanced() - Does the scrollable reader interface
function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, pOffset)
	var retObj = {
		offsetValid: true,
		msgNotReadable: false,
		userReplied: false,
		lastKeypress: "",
		newMsgOffset: -1,
		nextAction: ACTION_NONE,
		refreshEnhancedRdrHelpLine: false
	};
	// This is a scrollbar update function for use when viewing the message.
	function msgScrollbarUpdateFn(pFractionToLastPage)
	{
		// Update the scrollbar position for the message, depending on the
		// value of pFractionToLastMessage.
		fractionToLastPage = pFractionToLastPage;
		solidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidScrollBlocks * pFractionToLastPage);
		if (solidBlockStartRow != solidBlockLastStartRow)
			msgReaderObj.UpdateEnhancedReaderScollbar(solidBlockStartRow, solidBlockLastStartRow, numSolidScrollBlocks);
		solidBlockLastStartRow = solidBlockStartRow;
		console.gotoxy(1, console.screen_rows);
	}
	// This is a scrollbar update function for use when viewing the header info/kludge lines.
	function msgInfoScrollbarUpdateFn(pFractionToLastPage)
	{
		var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidInfoScrollBlocks * pFractionToLastPage);
		if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow)
			msgReaderObj.UpdateEnhancedReaderScollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, numInfoSolidScrollBlocks);
		lastInfoSolidBlockStartRow = infoSolidBlockStartRow;
		console.gotoxy(1, console.screen_rows);
	}

	var msgAreaWidth = this.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1;

	// We could word-wrap the message to ensure words aren't split across lines, but
	// doing so could make some messages look bad (i.e., messages with drawing characters),
	// and word_wrap also might not handle ANSI or other color/attribute codes..
	//if (!textHasDrawingChars(messageText))
	//	messageText = word_wrap(messageText, msgAreaWidth);
	// If the message has ANSI content, then use a Graphic object to help make
	// the message look good.  Also, remove any ANSI clear screen codes from the
	// message text.
	var msgHasANSICodes = messageText.indexOf("\x1b[") >= 0;
	if (msgHasANSICodes)
	{
		messageText = messageText.replace(/\u001b\[[012]J/gi, "");
		//var graphic = new Graphic(msgAreaWidth, this.msgAreaHeight-1);
		// To help ensure ANSI messages look good, it seems the Graphic object should have
		// its with later set to 1 less than the width used to create it.
		var graphicWidth = (msgAreaWidth < console.screen_columns ? msgAreaWidth+1 : console.screen_columns);
		var graphic = new Graphic(graphicWidth, this.msgAreaHeight-1);
		graphic.auto_extend = true;
		graphic.ANSI = ansiterm.expand_ctrl_a(messageText);
		graphic.width = graphicWidth - 1;
		//messageText = graphic.MSG.split('\n');
	// Show the message header
	this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);

	// Get the message text, interpret any @-codes in it, replace tabs with spaces
	// to prevent weirdness when displaying the message lines, and word-wrap the
	// text so that it looks good on the screen,
	var msgInfo = this.GetMsgInfoForEnhancedReader(msgHeader, true, true, true, messageText);

	var topMsgLineIdxForLastPage = msgInfo.topMsgLineIdxForLastPage;
	var numSolidScrollBlocks = msgInfo.numSolidScrollBlocks;
	var numNonSolidScrollBlocks = msgInfo.numNonSolidScrollBlocks;
	var solidBlockStartRow = msgInfo.solidBlockStartRow;
	var solidBlockLastStartRow = solidBlockStartRow;
	var topMsgLineIdx = 0;
	var fractionToLastPage = 0;
	if (topMsgLineIdxForLastPage != 0)
		fractionToLastPage = topMsgLineIdx / topMsgLineIdxForLastPage;

	// If use of the scrollbar is enabled, draw an initial scrollbar on the rightmost
	// column of the message area showing the fraction of the message shown and what
	// part of the message is currently being shown.  The scrollbar will be updated
	// minimally in the input loop to minimize screen redraws.
	if (this.useEnhReaderScrollbar)
		this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);

	// Input loop (for scrolling the message up & down)
	var msgLineFormatStr = "%-" + msgAreaWidth + "s";
	var writeMessage = true;
	// msgAreaHeight, msgReaderObj, and scrollbarUpdateFunction are for use
	// with scrollTextLines().
	var msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1;
	var msgReaderObj = this;

	var msgHasAttachments = msgHdrHasAttachmentFlag(msgHeader);
	// User input loop
	var continueOn = true;
	while (continueOn)
	{
		// Display the message lines (depending on the value of writeMessage)
		// and handle scroll keys via scrollTextLines().  Handle other keypresses
		// here.
		var scrollbarInfoObj = {
			solidBlockLastStartRow: 0,
			numSolidScrollBlocks: 0
		};
		scrollbarInfoObj.solidBlockLastStartRow = solidBlockLastStartRow;
		scrollbarInfoObj.numSolidScrollBlocks = numSolidScrollBlocks;
		var scrollRetObj = scrollTextLines(msgInfo.messageLines, topMsgLineIdx,
									   this.colors.msgBodyColor, writeMessage,
									   this.msgAreaLeft, this.msgAreaTop, msgAreaWidth,
									   msgAreaHeight, 1, console.screen_rows, this.useEnhReaderScrollbar,
		topMsgLineIdx = scrollRetObj.topLineIdx;
		retObj.lastKeypress = scrollRetObj.lastKeypress;
		switch (retObj.lastKeypress)
		{
			case this.enhReaderKeys.deleteMessage: // Delete message
				var originalCurpos = console.getxy();
				// The 2nd to last row of the screen is where the user will
				// be prompted for confirmation to delete the message.
				// Ideally, I'd like to put the cursor on the last row of
				// the screen for this, but console.noyes() lets the enter
				// key shift everything on screen up one row, and there's
				// no way to avoid that.  So, to optimize screen refreshing,
				// the cursor is placed on the 2nd to the last row on the
				// screen to prompt for confirmation.
				var promptPos = this.EnhReaderPrepLast2LinesForPrompt();

				// Prompt the user for confirmation to delete the message.
				// Note: this.PromptAndDeleteOrUndeleteMessage() will check to see if the user
				// is a sysop or the message was posted by the user.
				// If the message was deleted and the user can't view deleted messages,
				// then exit this read method and return KEY_RIGHT as the last keypress
				// so that the calling method will go to the next message/sub-board.
				// Otherwise (if the message was not deleted), refresh the
				// last 2 lines of the message on the screen.
				var msgWasDeleted = this.PromptAndDeleteOrUndeleteMessage(pOffset, promptPos, true, true, msgAreaWidth, true);
				if (msgWasDeleted && !canViewDeletedMsgs())
				{
					var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
					continueOn = msgSearchObj.continueInputLoop;
					retObj.newMsgOffset = msgSearchObj.newMsgOffset;
					retObj.nextAction = msgSearchObj.nextAction;
					if (msgSearchObj.promptGoToNextArea)
						if (this.EnhReaderPromptYesNo(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText), msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks))
							// Let this method exit and let the caller go to the next sub-board
							continueOn = false;
							retObj.nextAction = ACTION_GO_NEXT_MSG;
						else
							writeMessage = false; // No need to refresh the message
				}
				else
				{
					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
					// Move the cursor back to its original position
					console.gotoxy(originalCurpos);
					writeMessage = false;
				}
				break;
			case this.enhReaderKeys.selectMessage: // Select message (for batch delete, etc.)
				var originalCurpos = console.getxy();
				var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
				if (this.EnhReaderPromptYesNo("Select this message", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks, true))
					this.ToggleSelectedMessage(this.subBoardCode, pOffset, true);
				else
					this.ToggleSelectedMessage(this.subBoardCode, pOffset, false);
				writeMessage = false; // No need to refresh the message
				break;
				// TODO: Write this?  Not sure yet if it makes much sense to
				// have batch delete in the reader interface.
				// Prompt the user for confirmation, and use
				// this.DeleteOrUndeleteSelectedMessages() to mark the selected messages
				// as deleted.
				// Returns an object with the following properties:
				//  deletedAll: Boolean - Whether or not all messages were successfully marked
				//              for deletion
				//  failureList: An object containing indexes of messages that failed to get
				//               marked for deletion, indexed by internal sub-board code, then
				//               containing messages indexes as properties.  Reasons for failing
				//               to mark messages deleted can include the user not having permission
				//               to delete in a sub-board, failure to open the sub-board, etc.
				writeMessage = false; // No need to refresh the message
				break;
			case this.enhReaderKeys.editMsg: // Edit the messaage
				if (this.CanEdit())
				{
					// Move the cursor to the last line in the message area so
					// the edit confirmation prompt will appear there.  Not using
					// the last line on the screen because the yes/no prompt will
					// output a carriage return and move everything on the screen
					// up one line, which is not ideal in case the user says No.
					var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
					// Let the user edit the message if they want to
					var editReturnObj = this.EditExistingMsg(pOffset);
					// If the user didn't confirm, then we only have to refresh the bottom
					// help line.  Otherwise, we need to refresh everything on the screen.
					if (!editReturnObj.userConfirmed)
						// For some reason, the yes/no prompt erases the last character
						// of the scrollbar - So, figure out which block was there and
						// refresh it.
						//var scrollBarBlock = "\x01n\x01h\x01k" + BLOCK1; // Dim block
							var scrollBarBlock = this.colors.scrollbarBGColor + this.text.scrollbarBGChar;
							if (solidBlockStartRow + numSolidScrollBlocks - 1 == this.msgAreaBottom)
							{
								//scrollBarBlock = "\x01w" + BLOCK2; // Bright block
								// Bright block
								scrollBarBlock = this.colors.scrollbarScrollBlockColor + this.text.scrollbarScrollBlockChar;
							}
							else
							{
								// TODO
							}
							console.gotoxy(this.msgAreaRight+1, this.msgAreaBottom);
							console.print(scrollBarBlock);
						}
						// Refresh the last 2 message lines on the screen, then display
						// the key help line
						this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
						// If the message was edited, then refresh the text lines
						// array and update the other message-related variables.
						if (editReturnObj.msgEdited && (editReturnObj.newMsgIdx > -1))
							// When the message is edited, the old message will be
							// deleted and the edited message will be posted as a new
							// message.  So we should return to the caller and have it
							// go directly to that new message.
							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
							continueOn = false;
							retObj.newMsgOffset = editReturnObj.newMsgIdx;
							// The message was not edited.  Refresh everything on the screen.
							// If the enhanced message header width is less than the console
							// width, then clear the screen to remove anything that might be
							// left on the screen by the message editor.
							if (this.enhMsgHeaderWidth < console.screen_columns)
							// Display the message header and key help line
							this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
							// Display the scrollbar again, and ensure it's in the correct position
							if (this.useEnhReaderScrollbar)
							{
								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
							}
							else
							{
								// TODO
							}
							writeMessage = true; // We want to refresh the message on the screen
						}
					}
				}
				else
					writeMessage = false; // Don't write the current message again
				break;
			case this.enhReaderKeys.showHelp: // Show the help screen
				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgHasAttachments);
				// If the enhanced message header width is less than the console
				// width, then clear the screen to remove anything left on the
				// screen from the help screen.
				if (this.enhMsgHeaderWidth < console.screen_columns)
				// Display the message header and key help line
				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
				// Display the scrollbar again, and ensure it's in the correct position
				if (this.useEnhReaderScrollbar)
				{
					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
				}
				else
				{
					// TODO
				}
				writeMessage = true; // We want to refresh the message on the screen
				break;
			case this.enhReaderKeys.reply: // Reply to the message
			case this.enhReaderKeys.privateReply: // Private message reply
				// If the user pressed the private reply key while reading private
				// mail, then do nothing (allow only the regular reply key to reply).
				var privateReply = (retObj.lastKeypress == this.enhReaderKeys.privateReply);
				if (privateReply && this.readingPersonalEmail)
					writeMessage = false; // Don't re-write the current message again
				else
				{
					// Get the message header with fields expanded so we can get the most info possible.
					//var extdMsgHdr = this.GetMsgHdrByAbsoluteNum(msgHeader.number, true);
					var msgbase = new MsgBase(this.subBoardCode);
					if (msgbase.open())
						var extdMsgHdr = msgbase.get_msg_header(false, msgHeader.number, true);
						msgbase.close();
						// Let the user reply to the message.
						var replyRetObj = this.ReplyToMsg(extdMsgHdr, messageText, privateReply, pOffset);
						retObj.userReplied = replyRetObj.postSucceeded;
						//retObj.msgNotReadable = replyRetObj.msgWasDeleted;
						var msgWasDeleted = replyRetObj.msgWasDeleted;
						//if (retObj.msgNotReadable)
						if (msgWasDeleted && !canViewDeletedMsgs())
							var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
							continueOn = msgSearchObj.continueInputLoop;
							retObj.newMsgOffset = msgSearchObj.newMsgOffset;
							retObj.nextAction = msgSearchObj.nextAction;
							if (msgSearchObj.promptGoToNextArea)
								if (this.EnhReaderPromptYesNo(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText), msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks))
								{
									// Let this method exit and let the caller go to the next sub-board
									continueOn = false;
									retObj.nextAction = ACTION_GO_NEXT_MSG;
								}
								else
									writeMessage = true; // We want to refresh the message on the screen
							// If the enhanced message header width is less than the console
							// width, then clear the screen to remove anything left on the
							// screen by the message editor.
							if (this.enhMsgHeaderWidth < console.screen_columns)
							// Display the message header and key help line again
							this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
							// Display the scrollbar again to refresh it on the screen
							if (this.useEnhReaderScrollbar)
							{
								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
							}
							else
							{
								// TODO
							}
							writeMessage = true; // We want to refresh the message on the screen
			case this.enhReaderKeys.postMsg: // Post a message
				if (!this.readingPersonalEmail)
				{
					// Let the user post a message.
					if (bbs.post_msg(this.subBoardCode))
						if (searchTypePopulatesSearchResults(this.searchType))
							// TODO: If the user is doing a search, it might be
							// useful to search their new message and add it to
							// the search results if it's a match..  but maybe
							// not?
					// Refresh things on the screen
					// Display the message header and key help line again
					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
					// Display the scrollbar again to refresh it on the screen
					if (this.useEnhReaderScrollbar)
					{
						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
					}
					else
					{
						// TODO
					}
					writeMessage = true; // We want to refresh the message on the screen
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
			// Numeric digit: The start of a number of a message to read
			case "0":
			case "1":
			case "2":
			case "3":
			case "4":
			case "5":
			case "6":
			case "7":
			case "8":
			case "9":
				var originalCurpos = console.getxy();
				// Put the user's input back in the input buffer to
				// be used for getting the rest of the message number.
				console.ungetstr(retObj.lastKeypress);
				// Move the cursor to the 2nd to last row of the screen and
				// prompt the user for the message number.  Ideally, I'd like
				// to put the cursor on the last row of the screen for this, but
				// console.getnum() lets the enter key shift everything on screen
				// up one row, and there's no way to avoid that.  So, to optimize
				// screen refreshing, the cursor is placed on the 2nd to the last
				// row on the screen to prompt for the message number.
				var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
				// Prompt for the message number
				var msgNumInput = this.PromptForMsgNum(promptPos, replaceAtCodesInStr(this.text.readMsgNumPromptText), false, ERROR_PAUSE_WAIT_MS, false);
				// Only allow reading the message if the message number is valid
				// and it's not the same message number that was passed in.
				if ((msgNumInput > 0) && (msgNumInput-1 != pOffset))
				{
					// If the message is marked as deleted, then output an error
					if (this.MessageIsDeleted(msgNumInput-1))
					{
						writeWithPause(this.msgAreaLeft, console.screen_rows-1,
									   "\x01n" + replaceAtCodesInStr(format(this.text.msgHasBeenDeletedText, msgNumInput)) + "\x01n",
									   ERROR_PAUSE_WAIT_MS, "\x01n", true);
						// Confirm with the user whether to read the message
						var readMsg = true;
						if (this.promptToReadMessage)
							var sReadMsgConfirmText = this.colors["readMsgConfirmColor"]
													+ "Read message "
													+ this.colors["readMsgConfirmNumberColor"]
													+ msgNumInput + this.colors["readMsgConfirmColor"]
													+ ": Are you sure";
							console.gotoxy(promptPos);
							readMsg = console.yesno(sReadMsgConfirmText);
							continueOn = false;
							retObj.newMsgOffset = msgNumInput - 1;
							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						else
							writeMessage = false; // Don't re-write the current message again
				}
				else // Message number invalid or the same as what was passed in
					writeMessage = false; // Don't re-write the current message again
				// If the user chose to continue reading messages, then refresh
				// the last 2 message lines in the last part of the message area
				// and then put the cursor back to its original position.
				if (continueOn)
				{
					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
					// Move the cursor back to its original position
					console.gotoxy(originalCurpos);
				}
				break;
			case this.enhReaderKeys.prevMsgByTitle: // Previous message by title
			case this.enhReaderKeys.prevMsgByAuthor: // Previous message by author
			case this.enhReaderKeys.prevMsgByToUser: // Previous message by 'to user'
			case this.enhReaderKeys.prevMsgByThreadID: // Previous message by thread ID
				// Only allow this if we aren't doing a message search.
				if (!this.SearchingAndResultObjsDefinedForCurSub())
				{
					var threadPrevMsgOffset = this.FindThreadPrevOffset(msgHeader,
					                                                    keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
						retObj.newMsgOffset = threadPrevMsgOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						continueOn = false;
						// Refresh the help line at the bottom of the screen
						//this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
						writeMessage = false; // Don't re-write the current message again
					}
					// Make sure the help line on the bottom of the screen is
					// drawn.
					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
			case this.enhReaderKeys.nextMsgByTitle: // Next message by title (subject)
			case this.enhReaderKeys.nextMsgByAuthor: // Next message by author
			case this.enhReaderKeys.nextMsgByToUser: // Next message by 'to user'
			case this.enhReaderKeys.nextMsgByThreadID: // Next message by thread ID
				// Only allow this if we aren't doing a message search.
				if (!this.SearchingAndResultObjsDefinedForCurSub())
				{
					var threadPrevMsgOffset = this.FindThreadNextOffset(msgHeader,
					                                                    keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
					                                                    true);
						retObj.newMsgOffset = threadPrevMsgOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						continueOn = false;
					}
					else
						writeMessage = false; // Don't re-write the current message again
					// Make sure the help line on the bottom of the screen is
					// drawn.
					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
			case this.enhReaderKeys.previousMsg: // Previous message
				// Look for a prior message that isn't marked for deletion.  Even
				// if we don't find one, we'll still want to return from this
				// function (with message index -1) so that this script can go
				// onto the previous message sub-board/group.
				retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, false);
				// As a screen redraw optimization: Only return if there is a valid new
				// message offset or the user is allowed to change to a different sub-board.
				// Otherwise, don't return, and don't refresh the message on the screen.
				var goToPrevMessage = false;
				if ((retObj.newMsgOffset > -1) || allowChgMsgArea)
				{
					if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
						goToPrevMessage = this.EnhReaderPromptYesNo(replaceAtCodesInStr(this.text.goToPrevMsgAreaPromptText),
																	msgInfo.messageLines, topMsgLineIdx,
																	msgLineFormatStr, solidBlockStartRow,
																	numSolidScrollBlocks);
						// We're not at the beginning of the sub-board, so it's okay to exit this
						// method and go to the previous message.
						goToPrevMessage = true;
				}
				if (goToPrevMessage)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_PREVIOUS_MSG;
				}
				else
					writeMessage = false; // No need to refresh the message
				break;
			case this.enhReaderKeys.nextMsg: // Next message
			case KEY_ENTER:
				// Look for a later message that isn't marked for deletion.  Even
				// if we don't find one, we'll still want to return from this
				// function (with message index -1) so that this script can go
				// onto the next message sub-board/group.
				var findNextMsgRetObj = this.ScrollableReaderNextReadableMessage(pOffset, msgInfo, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks);
				if (findNextMsgRetObj.newMsgOffset > -1)
					retObj.newMsgOffset = findNextMsgRetObj.newMsgOffset;
				writeMessage = findNextMsgRetObj.writeMessage;
				continueOn = findNextMsgRetObj.continueOn;
				retObj.nextAction = findNextMsgRetObj.nextAction;
				break;
				// First & last message: Quit out of this input loop and let the
				// calling function, this.ReadMessages(), handle the action.
			case this.enhReaderKeys.firstMsg: // First message
				// Only leave this function if we aren't already on the first message.
				if (pOffset > 0)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_FIRST_MSG;
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
			case this.enhReaderKeys.lastMsg: // Last message
				// Only leave this function if we aren't already on the last message.
				if (pOffset < this.NumMessages() - 1)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_LAST_MSG;
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
			case this.enhReaderKeys.prevSubBoard: // Go to the previous message area
				if (allowChgMsgArea)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_PREV_MSG_AREA;
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
			case this.enhReaderKeys.nextSubBoard: // Go to the next message area
				if (allowChgMsgArea || this.doingMultiSubBoardScan)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
				// H and K: Display the extended message header info/kludge lines
				// (for the sysop)
			case this.enhReaderKeys.showHdrInfo:
			case this.enhReaderKeys.showKludgeLines:
				{
					// Save the original cursor position
					var originalCurPos = console.getxy();
					// Get an array of the extended header info/kludge lines and then
					// allow the user to scroll through them.
					var extdHdrInfoLines = this.GetExtdMsgHdrInfo(msgHeader, (retObj.lastKeypress == this.enhReaderKeys.showKludgeLines));
						if (this.useEnhReaderScrollbar)
						{
							// Calculate information for the scrollbar for the kludge lines
							var infoFractionShown = this.msgAreaHeight / extdHdrInfoLines.length;
							if (infoFractionShown > 1)
								infoFractionShown = 1.0;
							var numInfoSolidScrollBlocks = Math.floor(this.msgAreaHeight * infoFractionShown);
							if (numInfoSolidScrollBlocks == 0)
								numInfoSolidScrollBlocks = 1;
							var numNonSolidInfoScrollBlocks = this.msgAreaHeight - numInfoSolidScrollBlocks;
							var lastInfoSolidBlockStartRow = this.msgAreaTop;
							// Display the kludge lines and let the user scroll through them
							this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks);
						}
						scrollTextLines(extdHdrInfoLines, 0, this.colors["msgBodyColor"], true, this.msgAreaLeft,
						                this.msgAreaTop, msgAreaWidth, msgAreaHeight, 1, console.screen_rows,
						                this.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn);
						// Display the scrollbar for the message to refresh it on the screen
						if (this.useEnhReaderScrollbar)
						{
							solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
						}
						else
						{
							// TODO
						}
						writeMessage = true; // We want to refresh the message on the screen
					else
					{
						// There are no kludge lines for this message
						this.DisplayEnhReaderError(replaceAtCodesInStr(this.text.noKludgeLinesForThisMsgText), msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
					}
				}
				else // The user is not a sysop
					writeMessage = false;
				break;
				// Message list, change message area: Quit out of this input loop
				// and let the calling function, this.ReadMessages(), handle the
				// action.
			case this.enhReaderKeys.showMsgList: // Message list
				console.crlf();
				console.print("Loading...");
				retObj.nextAction = ACTION_DISPLAY_MSG_LIST;
				continueOn = false;
				break;
			case this.enhReaderKeys.chgMsgArea: // Change message area, if allowed
				if (allowChgMsgArea)
				{
					retObj.nextAction = ACTION_CHG_MSG_AREA;
				}
				else
					writeMessage = false; // No need to refresh the message
				break;
			case this.enhReaderKeys.downloadAttachments: // Download attachments
					console.gotoxy(1, console.screen_rows);
					console.crlf();
					allowUserToDownloadMessage_NewInterface(msgHeader, this.subBoardCode);
					// Display the message header and key help line again
					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
					// Display the scrollbar again to refresh it on the screen
					if (this.useEnhReaderScrollbar)
					{
						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
					}
					else
					{
						// TODO
					}
					writeMessage = true; // We want to refresh the message on the screen
				}
				else
					writeMessage = false;
				break;
			case this.enhReaderKeys.saveToBBSMachine:
				// Save the message to the BBS machine - Only allow this
				// if the user is a sysop.
				{
					// Prompt the user for a filename to save the message to the
					// BBS machine
					var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
					console.print("\x01n\x01cFilename:\x01h");
					var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1
					var filename = console.getstr(inputLen, K_NOCRLF);
						var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename);
						console.cleartoeol("\x01n");
						console.gotoxy(promptPos);
						if (saveMsgRetObj.succeeded)
							var statusMsg = "\x01n\x01cThe message has been saved.";
							if (msgHdrHasAttachmentFlag(msgHeader))
								statusMsg += " Attachments not saved.";
							console.print("\x01n\x01y\x01hFailed: " + saveMsgRetObj.errorMsg + "\x01n");
						console.print("\x01n\x01y\x01hMessage not exported\x01n");
					// Refresh the last 2 lines of the message on the screen to overwrite
					// the file save prompt
					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
				}
				writeMessage = false; // Don't write the whole message again
			case this.enhReaderKeys.userEdit: // Edit the user who wrote the message
					console.crlf();
					console.print("- Edit user " + msgHeader.from);
					console.crlf();
					var editObj = editUser(msgHeader.from);
					if (editObj.errorMsg.length != 0)
						console.print("\x01y\x01h" + editObj.errorMsg + "\x01n");
					// Display the message header and key help line again
					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
					// Display the scrollbar again to refresh it on the screen
					if (this.useEnhReaderScrollbar)
					{
						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
					}
					else
					{
						// TODO
					}
					writeMessage = true; // We want to refresh the message on the screen
				}
				else // The user is not a sysop
			case this.enhReaderKeys.forwardMsg: // Forward the message
				console.print("\x01c- Forward message\x01n");
				console.crlf();
				var retStr = this.ForwardMessage(msgHeader, messageText);
				if (retStr.length > 0)
				{
					console.print("\x01n\x01h\x01y* " + retStr + "\x01n");
					console.crlf();
					console.pause();
				}

				// Refresh things on the screen
				// Display the message header and key help line again
				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
				// Display the scrollbar again to refresh it on the screen
				if (this.useEnhReaderScrollbar)
				{
					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
				}
				else
				{
					// TODO
				}
				writeMessage = true; // We want to refresh the message on the screen
				break;
			case this.enhReaderKeys.vote: // Vote on the message
				// Move the cursor to the last line in the message area so the
				// vote question text prompt will appear there.
				var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
				console.gotoxy(1, console.screen_rows-1);
				// Let the user vote on the message
				var voteRetObj = this.VoteOnMessage(msgHeader, true);
				if (voteRetObj.BBSHasVoteFunction)
				{
					var msgIsPollVote = ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL);
						// If the message is a poll vote, then output any error
						// message on its own line and refresh the whole screen.
						// Otherwise, use the last 2 rows for an error message
						// and only refresh what's necessary.
						if (msgIsPollVote)
							console.gotoxy(1, console.screen_rows-1);
							if (voteRetObj.errorMsg.length > 0)
							{
								if (voteRetObj.mnemonicsRequiredForErrorMsg)
								{
									console.mnemonics(voteRetObj.errorMsg);
									console.print("\x01y\x01h* " + voteRetObj.errorMsg + "\x01n");
							}
							else if (!voteRetObj.savedVote)
								console.print("\x01y\x01h* Failed to save the vote\x01n");
						{
							// Not a poll vote - Just an up/down vote
							if ((voteRetObj.errorMsg.length > 0) || (!voteRetObj.savedVote))
							{
								console.gotoxy(1, console.screen_rows-1);
								if (voteRetObj.errorMsg.length > 0)
								{
									if (voteRetObj.mnemonicsRequiredForErrorMsg)
									{
										console.mnemonics(voteRetObj.errorMsg);
										console.print("\x01y\x01h* " + voteRetObj.errorMsg + "\x01n");
									console.print("\x01y\x01h* Failed to save the vote\x01n");
							}
							else
								msgHeader = voteRetObj.updatedHdr; // To get updated vote information
							mswait(ERROR_PAUSE_WAIT_MS);
						// Exit out of the reader and come back to read
						// the same message again so that the voting results
						// are re-loaded and displayed on the screen.
						retObj.newMsgOffset = pOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						continueOn = false;
						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
					else
					{
						// The user quit out of voting.  Refresh the screen.
						// Exit out of the reader and come back to read
						// the same message again so that the screen is refreshed.
						retObj.newMsgOffset = pOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						continueOn = false;
						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
					}
				break;
			case this.enhReaderKeys.showVotes: // Show votes
				// Save the original cursor position
				var originalCurPos = console.getxy();
				if (msgHeader.hasOwnProperty("total_votes") && msgHeader.hasOwnProperty("upvotes"))
				{
					var voteInfo = this.GetUpvoteAndDownvoteInfo(msgHeader);
					// Display the vote info and let the user scroll through them
					// (the console height should be enough, but do this just in case)
					// Calculate information for the scrollbar for the vote info lines
					if (this.useEnhReaderScrollbar)
					{
						var infoFractionShown = this.msgAreaHeight / voteInfo.length;
						if (infoFractionShown > 1)
							infoFractionShown = 1.0;
						var numInfoSolidScrollBlocks = Math.floor(this.msgAreaHeight * infoFractionShown);
						if (numInfoSolidScrollBlocks == 0)
							numInfoSolidScrollBlocks = 1;
						var numNonSolidInfoScrollBlocks = this.msgAreaHeight - numInfoSolidScrollBlocks;
						var lastInfoSolidBlockStartRow = this.msgAreaTop;
						// Display the vote info lines and let the user scroll through them
						this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks);
					}
					else
					{
						// TODO
					}
					scrollTextLines(voteInfo, 0, this.colors["msgBodyColor"], true, this.msgAreaLeft, this.msgAreaTop, msgAreaWidth,
					                msgAreaHeight, 1, console.screen_rows, this.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn);
					// Display the scrollbar for the message to refresh it on the screen
					if (this.useEnhReaderScrollbar)
					{
						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
					}
					else
					{
						// TODO
					}
					writeMessage = true; // We want to refresh the message on the screen
				}
				else
				{
					this.DisplayEnhReaderError("There is no voting information for this message", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
					console.gotoxy(originalCurPos);
					writeMessage = false;
				}
				break;
			case this.enhReaderKeys.closePoll: // Close a poll message
				// Save the original cursor position
				var originalCurPos = console.getxy();
				var pollCloseMsg = "";
				// If this message is a poll, then allow closing it.
				if ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL)
				{
					if ((msgHeader.auxattr & POLL_CLOSED) == 0)
					{
						// Only let the user close the poll if they created it
						if (userHandleAliasNameMatch(msgHeader.from))
						{
							// Prompt to confirm whether the user wants to close the poll
							console.gotoxy(1, console.screen_rows-1);
							printf("\x01n%" + +(console.screen_columns-1) + "s", "");
							console.gotoxy(1, console.screen_rows-1);
							if (!console.noyes("Close poll"))
							{
								// Close the poll (open the sub-board first)
								var msgbase = new MsgBase(this.subBoardCode);
								if (msgbase.open())
									if (closePollWithOpenMsgbase(msgbase, msgHeader.number))
									{
										msgHeader.auxattr |= POLL_CLOSED;
										pollCloseMsg = "\x01n\x01cThis poll was successfully closed.";
										pollCloseMsg = "\x01n\x01r\x01h* Failed to close this poll!";
									pollCloseMsg = "\x01n\x01y\x01hUnable to open sub-board to close the poll";
							pollCloseMsg = "\x01n\x01y\x01hCan't close this poll because it's not yours";
						pollCloseMsg = "\x01n\x01y\x01hThis poll is already closed";
				}
				else
					pollCloseMsg = "This message is not a poll";

				// Display the poll closing status message
				this.DisplayEnhReaderError(pollCloseMsg, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
				console.gotoxy(originalCurPos);
				writeMessage = false;
				break;
			case this.enhReaderKeys.validateMsg: // Validate the message
				if (user.is_sysop && (this.subBoardCode != "mail") && msg_area.sub[this.subBoardCode].is_moderated)
				{
					var message = "";
					if (this.ValidateMsg(this.subBoardCode, msgHeader.number))
					{
						message = "\x01n\x01cMessage validation successful";
						// Refresh the message header in the arrays
						this.RefreshMsgHdrInArrays(msgHeader.number);
						// Exit out of the reader and come back to read
						// the same message again so that the voting results
						// are re-loaded and displayed on the screen.
						retObj.newMsgOffset = pOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						continueOn = false;
						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
					}
					else
						message = "\x01n\x01y\x01hMessage validation failed!";
					this.DisplayEnhReaderError(message, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
				}
				else
					writeMessage = false;
				break;
			case this.enhReaderKeys.bypassSubBoardInNewScan:
				writeMessage = false; // TODO: Finish
				/*
				if (this.doingMsgScan)
				{
					var originalCurpos = console.getxy();
					// The 2nd to last row of the screen is where the user will
					// be prompted for confirmation to delete the message.
					// Ideally, I'd like to put the cursor on the last row of
					// the screen for this, but console.noyes() lets the enter
					// key shift everything on screen up one row, and there's
					// no way to avoid that.  So, to optimize screen refreshing,
					// the cursor is placed on the 2nd to the last row on the
					// screen to prompt for confirmation.
					var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
					if (!console.noyes("Bypass this sub-board in newscans"))
					{
						continueOn = false;
						msg_area.sub[this.subBoardCode].scan_cfg &= SCAN_CFG_NEW;
					}
					else
					{
						this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
						// Move the cursor back to its original position
						console.gotoxy(originalCurpos);
						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
					}
				}
				else
					writeMessage = false;
				*/
				break;
				// Make a backup copy of the this.useEnhReaderScrollbar setting in case it changes, so we can tell if we need to refresh the scrollbar
				var oldUseEnhReaderScrollbar = this.useEnhReaderScrollbar;
				var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) { pReader.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); });
				retObj.lastKeypress = "";
				writeMessage = userSettingsRetObj.needWholeScreenRefresh;
				// In case the user changed their twitlist, re-filter the messages for this sub-board
				if (userSettingsRetObj.userTwitListChanged)
				{
					console.gotoxy(1, console.screen_rows);
					console.crlf();
					console.print("\x01nTwitlist changed; re-filtering..");
					var tmpMsgbase = new MsgBase(this.subBoardCode);
					if (tmpMsgbase.open())
					{
						continueOn = false;
						writeMessage = false;
						var tmpAllMsgHdrs = tmpMsgbase.get_all_msg_headers(true);
						tmpMsgbase.close();
						this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpAllMsgHdrs, true);
						// If the user is currently reading a message a message by someone who is now
						// in their twit list, change the message currently being viewed.
						if (this.MsgHdrFromOrToInUserTwitlist(msgHeader))
						{
							var findNextMsgRetObj = this.ScrollableReaderNextReadableMessage(pOffset, msgInfo, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks);
							if (findNextMsgRetObj.newMsgOffset > -1)
							{
								retObj.newMsgOffset = findNextMsgRetObj.newMsgOffset;
								retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
							}
							else
								retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
						}
						else
						{
							// If there are still messages in this sub-board, and the message offset is beyond the last
							// message, then show the last message in the sub-board.  Otherwise, go to the next message area.
							if (this.hdrsForCurrentSubBoard.length > 0)
							{
								if (pOffset > this.hdrsForCurrentSubBoard.length)
								{
									//this.hdrsForCurrentSubBoard[this.hdrsForCurrentSubBoard.length-1].number
									retObj.newMsgOffset = this.hdrsForCurrentSubBoard.length - 1;
									retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
								}
							}
							else
								retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
						}
					}
					else
						console.print("\x01y\x01hFailed to open the messagbase!\x01\r\n\x01p");
					this.SetUpLightbarMsgListVars();
					writeMessage = true;
				}
				if (userSettingsRetObj.needWholeScreenRefresh)
				{
					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
					if (this.useEnhReaderScrollbar)
						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
					else
					{
						// TODO
					}
					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
				}
				else
				{
					// If the message scrollbar was toggled, then draw/erase the scrollbar
					if (this.useEnhReaderScrollbar != oldUseEnhReaderScrollbar)
					{
						msgAreaWidth = this.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1;
						// If the message is ANSI, then re-create the Graphic object to account for the
						// new width
						if (msgHasANSICodes)
						{
							//var graphic = new Graphic(msgAreaWidth, this.msgAreaHeight-1);
							// To help ensure ANSI messages look good, it seems the Graphic object should have
							// its with later set to 1 less than the width used to create it.
							var graphicWidth = (msgAreaWidth < console.screen_columns ? msgAreaWidth+1 : console.screen_columns);
							var graphic = new Graphic(graphicWidth, this.msgAreaHeight-1);
							graphic.auto_extend = true;
							graphic.ANSI = ansiterm.expand_ctrl_a(messageText);
							//graphic.width = msgAreaWidth;
							graphic.width = graphicWidth - 1;
							messageText = graphic.MSG;
						}
						// Display or erase the scrollbar
						if (this.useEnhReaderScrollbar)
						{
							solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
						}
						else
						{
							// Erase the scrollbar
							console.attributes = "N";
							for (var screenY = this.msgAreaTop; screenY <= this.msgAreaBottom; ++screenY)
							{
								console.gotoxy(this.msgAreaRight+1, screenY);
								console.print(" ");
							}
						}
					}
					this.RefreshMsgAreaRectangle(msgInfo.messageLines, topMsgLineIdx, userSettingsRetObj.optionBoxTopLeftX, userSettingsRetObj.optionBoxTopLeftY, userSettingsRetObj.optionBoxWidth, userSettingsRetObj.optionBoxHeight);
				retObj.nextAction = ACTION_QUIT;
				continueOn = false;
				break;
			default:
				writeMessage = false;
				break;
// Helper method for ReadMessageEnhanced_Scrollable(): Determines if there is a readable message after the
// one at the given offset.
//
// Parameters:
//  pOfset: The offset of the current message
//  pMsgInfo: An object contaiining message information
//  pTopMsgLineIdx: The index of the message line at the top of the reader area
//  pMsgLineFormatStr: A format string for the message line
//  pSolidBlockStartRow: The screen row of where the solid blocks start for the scrollbar
//  pNumSolidScrollBlocks: The number of solid blocks in the scrollbar
//
// Return value: An object containing the following properties:
//               newMsgOffset: The offset of the next readable message, if available.  If not available, this will be -1.
//               writeMessage: Boolean - Whether or not to write the whole message again (for the scrollable interface)
//               continueOn: Boolean - Whether or not to continue with the reader input loop
//               nextAction: A value indicating the next action for the reader to take after leaving the reader function
function DigDistMsgReader_ScrollableReaderNextReadableMessage(pOffset, pMsgInfo, pTopMsgLineIdx, pMsgLineFormatStr, pSolidBlockStartRow, pNumSolidScrollBlocks)
{
	var retObj = {
		newMsgOffset: -1,
		writeMessage: true,
		continueOn: true,
		nextAction: ACTION_NONE
	};

	// Look for a later message that isn't marked for deletion.  Even
	// if we don't find one, we'll still want to return from this
	// function (with message index -1) so that this script can go
	// onto the next message sub-board/group.
	retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, true);
	// Note: Unlike the left arrow key, we want to exit this method when
	// navigating to the next message, regardless of whether or not the
	// user is allowed to change to a different sub-board, so that processes
	// that require continuation (such as new message scan) can continue.
	// Still, if there are no more readable messages in the current sub-board
	// (and thus the user would go onto the next message area), prompt the
	// user whether they want to continue onto the next message area.
	if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
	{
		// For personal mail, don't do anything, and don't refresh the
		// message.  In a sub-board, ask the user if they want to go
		// to the next one.
		if (this.readingPersonalEmail)
			retObj.writeMessage = false;
		else
		{
			// If configured to allow the user to post in the sub-board
			// instead of going to the next message area and we're not
			// scanning, then do so.
			if (this.readingPostOnSubBoardInsteadOfGoToNext && !this.doingMsgScan)
			{
				console.print("\x01n");
				console.crlf();
				// Ask the user if they want to post on the sub-board.
				// If they say yes, then do so before exiting.
				var grpNameAndDesc = this.GetGroupNameAndDesc();
				if (!console.noyes(replaceAtCodesInStr(format(this.text.postOnSubBoard, grpNameAndDesc.grpName, grpNameAndDesc.grpDesc))))
					bbs.post_msg(this.subBoardCode);
				retObj.continueOn = false;
				retObj.nextAction = ACTION_QUIT;
			}
			else
			{
				// Prompt the user whether they want to go to the next message area
				if (this.EnhReaderPromptYesNo(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText), pMsgInfo.messageLines, pTopMsgLineIdx, pMsgLineFormatStr, pSolidBlockStartRow, pNumSolidScrollBlocks))
				{
					// Let this method exit and let the caller go to the next sub-board
					retObj.continueOn = false;
					retObj.nextAction = ACTION_GO_NEXT_MSG;
				}
				else
					retObj.writeMessage = false; // No need to refresh the message
			}
		}
	}
	else
	{
		// We're not at the end of the sub-board, so it's okay to exit this
		// method and go to the next message.
		retObj.continueOn = false;
		retObj.nextAction = ACTION_GO_NEXT_MSG;
	}

	return retObj;
}
// Helper method for ReadMessageEnhanced() - Determines the next keypress for a click
// coordinate outside the scroll area.
//
// Parameters:
//  pScrollRetObj: The return object of the message scroll function
//  pEnhReadHelpLineClickCoords: An array of click coordinates & action strings
//
// Return value: An object containing the following properties:
//               actionStr: A string containing the next action for the enhanced reader,
//                          or an empty string if there was no valid action found.
function DigDistMsgReader_ScrollReaderDetermineClickCoordAction(pScrollRetObj, pEnhReadHelpLineClickCoords)
{
	var retObj = {
		actionStr: ""
	};

	for (var coordIdx = 0; coordIdx < pEnhReadHelpLineClickCoords.length; ++coordIdx)
	{
		if ((pScrollRetObj.mouse.x == pEnhReadHelpLineClickCoords[coordIdx].x) && (pScrollRetObj.mouse.y == pEnhReadHelpLineClickCoords[coordIdx].y))
		{
			// The up arrow, down arrow, PageUp, PageDown, Home, and End aren't handled
			// here - Those are handled in scrollTextlines().
			if (pEnhReadHelpLineClickCoords[coordIdx].actionStr == LEFT_ARROW)
				retObj.actionStr = this.enhReaderKeys.previousMsg;
			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr == RIGHT_ARROW)
				retObj.actionStr = this.enhReaderKeys.nextMsg;
			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("DEL") == 0)
				retObj.actionStr = this.enhReaderKeys.deleteMessage;
			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("E)") == 0)
				retObj.actionStr = this.enhReaderKeys.editMsg;
			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("F") == 0)
				retObj.actionStr = this.enhReaderKeys.firstMsg;
			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("L") == 0)
				retObj.actionStr = this.enhReaderKeys.lastMsg;
			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("R") == 0)
				retObj.actionStr = this.enhReaderKeys.reply;
			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("C") == 0)
				retObj.actionStr = this.enhReaderKeys.chgMsgArea;
			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("Q") == 0)
				retObj.actionStr = this.enhReaderKeys.quit;
			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("?") == 0)
				retObj.actionStr = this.enhReaderKeys.showHelp;
			break;
		}
	}
	return retObj;
}
// Helper method for ReadMessageEnhanced() - Does the traditional (non-scrollable) reader interface
function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, pOffset)
	var retObj = {
		offsetValid: true,
		msgNotReadable: false,
		userReplied: false,
		lastKeypress: "",
		newMsgOffset: -1,
		nextAction: ACTION_NONE,
		refreshEnhancedRdrHelpLine: false
	};
	// We could word-wrap the message to ensure words aren't split across lines, but
	// doing so could make some messages look bad (i.e., messages with drawing characters),
	// and word_wrap also might not handle ANSI or other color/attribute codes..
	//if (!textHasDrawingChars(messageText))
	//	messageText = word_wrap(messageText, this.msgAreaWidth);

	var msgHasAttachments = msgHdrHasAttachmentFlag(msgHeader);

	// Only interpret @-codes if the user is reading personal email and the sender is a sysop.  There
	// are many @-codes that do some action such as move the cursor, execute a
	// script, etc., and I don't want users on message networks to do anything
	// malicious to users on other BBSes.
	if (this.readingPersonalEmail && msgSenderIsASysop(msgHeader))
		messageText = replaceAtCodesInStr(messageText); // Or this.ParseMsgAtCodes(messageText, msgHeader) to replace only some @ codes
	var msgHasANSICodes = messageText.indexOf("\x1b[") >= 0;
	var msgTextWrapped = (msgHasANSICodes ? messageText : word_wrap(messageText, console.screen_columns-1));
	var keyHelpText = "\x01n\x01c\x01h#\x01n\x01b, \x01c\x01hLeft\x01n\x01b, \x01c\x01hRight\x01n\x01b, ";
	if (this.CanDelete() || this.CanDeleteLastMsg())
		keyHelpText += "\x01c\x01hDEL\x01b, ";
		keyHelpText += "\x01c\x01hE\x01y)\x01n\x01cdit\x01b, ";
	keyHelpText += "\x01c\x01hF\x01y)\x01n\x01cirst\x01b, \x01c\x01hL\x01y)\x01n\x01cast\x01b, \x01c\x01hR\x01y)\x01n\x01ceply\x01b, ";
	// If the user is allowed to change to a different message area, then
	// include that option.
	if (allowChgMsgArea)
	{
		// If there's room for the private reply option, then include that
		// before the change area option.
		if (console.screen_columns >= 89)
			keyHelpText += "\x01c\x01hP\x01y)\x01n\x01crivate reply\x01b, ";
		keyHelpText += "\x01c\x01hC\x01y)\x01n\x01chg area\x01b, ";
		// The user isn't allowed to change to a different message area.
		// Go ahead and include the private reply option.
		keyHelpText += "\x01c\x01hP\x01y)\x01n\x01crivate reply\x01b, ";
	keyHelpText += "\x01c\x01hQ\x01y)\x01n\x01cuit\x01b, \x01c\x01h?\x01g: \x01c";
	// User input loop
	var writeMessage = true;
	var writePromptText = true;
	var continueOn = true;
	while (continueOn)
	{
		if (writeMessage)
			if (console.term_supports(USER_ANSI))
			// Write the message header & message body to the screen
			this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
			console.print("\x01n" + this.colors["msgBodyColor"]);
			console.putmsg(msgTextWrapped, P_NOATCODES);
		// Write the prompt text
		if (writePromptText)
Loading
Loading full blame...