Skip to content
Snippets Groups Projects
DDMsgReader.js 885 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)
 * 2023-09-22 Eric Oulashin     Version 1.80 beta
 *                              Improved speed of new-to-you scans, and to an extent (hopefully) overall speed
 *                              Bug fix: Setting reverseListOrder to "ask" in the .cfg file works properly again.
 *                              Bug fix: When listing messages in reverse order, the selected menu index
 *                              (for lightbar mode) is now correct.
 *                              Bug fix: If the user is allowed to read deleted messages, then allow
 *                              the left & right arrow keys to to the next/previous message if it's deleted.
 *                              Small fixes for indexed scanning mode.
 *                              New: For personal email, unread emails will have an 'unread' message indicator
 *                              in the message list as a U between the message number and the 'from' name.
 *                              New user setting: "Quit from reader to message list": When enabled,
 *                              quitting from reader mode goes to the message list instead of exiting
 *                              out of DDMsgReader fully.
 *                              New user setting: Enter/selection from indexed mode menu shows message list
 *                              (instead of going into reader mode)
 *                              New user setting: List messages in reverse order
 * 2023-10-10 Eric Oulashin     Version 1.80
 *                              Releasing this version
 * 2023-10-11 Eric Oulashin     Version 1.81
 *                              Updated permission check functions (speed improvement)
 * 2023-10-18 Eric Oulashin     Version 1.82
 *                              Fix for # posts and missing dates in sub-board list when changing sub-board
 * 2023-10-25 Eric Oulashin     Version 1.83
 *                              Personal emails to the sysop received as "sysop" (or starting with "sysop")
 *                              are now correctly identified and marked as read when read
 * 2023-10-26 Eric Oulashin     Version 1.84
 *                              Fix in reader mode for refreshing the message area after
 *                              closing another window (necessary with recent changes to
 *                              substrWithAttrCodes())
/* 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.84";
var READER_DATE = "2023-10-26";

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

// Scan scopes
const SCAN_SCOPE_SUB_BOARD = 0;
const SCAN_SCOPE_GROUP = 1;
const SCAN_SCOPE_ALL = 2;

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;
// Actions for indexed Mode
const INDEXED_MODE_SUBBOARD_MENU = 30;
// 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")
if (typeof(MSG_ORPHAN) != "undefined")
	gNetMsgAttrStrs.MSG_ORPHAN = "Orphan";
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 file is available, and load is if so
if (file_exists(backslash(system.exec_dir) + "load/avatar_lib.js"))
// 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
	// 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);
		msgReader.DoIndexedMode(SCAN_SCOPE_ALL);
		// 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.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.)
// End of script execution.  Functions below:

// 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.FindNextReadableMsgIdx = DigDistMsgReader_FindNextReadableMsgIdx;
	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.UpdateEnhancedReaderScrollbar = DigDistMsgReader_UpdateEnhancedReaderScrollbar;
	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.GetMsgHdrFieldListText = DigDistMsgReader_GetMsgHdrFieldListText;
	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.
	// Note: This isn't used for "indexed" reader mode (but maybe it should be in the future)
	// 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;

	// Whether or not we're in indexed reader mode
	this.indexedMode = false;
	// For indexed reader mode, whether or not to enable caching the message
	// header lists for performance
	this.enableIndexedModeMsgListCache = true;

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

	// 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",
		msgSearchAbortedText: "\x01n\x01h\x01cM\x01n\x01cessage search \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...",
		scanningSubBoardText: "\x01n\x01cScanning \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 \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.",
		noHdrLinesForThisMsgText: "\x01n\x01h\x01yThere are no header lines for this message.",
		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",