Skip to content
Snippets Groups Projects
DDMsgReader.js 692 KiB
Newer Older
						console.gotoxy(this.MSGNUM_LEN+1, currentRow);
						console.print("\1n");
						if (this.MessageIsSelected(this.subBoardCode, messageIndex))
							console.print(this.colors.selectedMsgMarkColor + CHECK_CHAR + "\1n");
						else
							console.print(" \1n");
					}
					++currentRow;
				}
			}

			// Refresh the help line and move the cursor back to its original position
			console.gotoxy(1, console.screen_rows);
			DisplayHelpLine(this.msgListLightbarModeHelpLine);
			console.gotoxy(originalCurpos);
		}
		// Ctrl-D: Batch delete (for selected messages)
		else if (userInput == CTRL_D)
		{
			var originalCurpos = console.getxy();
			if (this.NumSelectedMessages() > 0)
			{
				console.gotoxy(1, console.screen_rows);
				console.print("\1n");
				console.clearline();

				// The PromptAndDeleteSelectedMessages() method will prompt the user for confirmation
				// to delete the message and then delete it if confirmed.
				this.PromptAndDeleteSelectedMessages({ x: 1, y: console.screen_rows});

				// In case all messages were deleted, if that's the case, show
				// an appropriate message and don't continue listing messages.
				//if (this.NumMessages(true) == 0)
				if (!this.NonDeletedMessagesExist())
				{
					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.
					/*
					this.msgbase.close();
					this.msgbase = null;
					console.clear("\1n");
					console.center("\1n\1h\1yThere are no messages to display.");
					console.crlf();
					console.pause();
					*/
				}
				else
				{
					// There are still messages to list, so refresh the screen.
					console.clear("\1n");
					this.WriteMsgListScreenTopHeader();
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
					console.gotoxy(1, this.lightbarMsgListStartScreenRow);
					lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
					console.gotoxy(originalCurpos); // Put the cursor back where it should be
				}
			}
			else
			{
				// There are no selected messages
				writeWithPause(1, console.screen_rows, "\1n\1h\1yThere are no selected messages.",
				               ERROR_PAUSE_WAIT_MS, "\1n", true);
				// Refresh the help line and move the cursor back to its original position
				DisplayHelpLine(this.msgListLightbarModeHelpLine);
				console.gotoxy(originalCurpos);
			}
		}
}
// 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
function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum)
{
	// 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 msgNum = (typeof(pMsgNum) == "number" ? pMsgNum : pMsgHeader.offset+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 = " ";

	// Write the message header information.
	// 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)
	{
		if (msgDeleted)
			msgIndicatorChar = "\1n\1r\1h\1i" + this.colors.msgListHighlightBkgColor + "*\1n";
		else if (this.MessageIsSelected(this.subBoardCode, msgNum-1))
			msgIndicatorChar = "\1n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + CHECK_CHAR + "\1n";
		printf(this.sMsgInfoFormatHighlightStr,
		       msgNum,
		       msgIndicatorChar,
		       pMsgHeader.from.substr(0, this.FROM_LEN),
		       pMsgHeader.to.substr(0, this.TO_LEN),
		       pMsgHeader.subject.substr(0, this.SUBJ_LEN),
		       sDate, sTime);
		if (msgDeleted)
			msgIndicatorChar = "\1n\1r\1h\1i*\1n";
		else if (this.MessageIsSelected(this.subBoardCode, msgNum-1))
			msgIndicatorChar = "\1n" +  this.colors.selectedMsgMarkColor + CHECK_CHAR + "\1n";

		// 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()));
		printf((msgToUser ? this.sMsgInfoToUserFormatStr : (msgIsFromUser ? this.sMsgInfoFromUserFormatStr : this.sMsgInfoFormatStr)),
		       msgNum,
		       msgIndicatorChar,
		       pMsgHeader.from.substr(0, this.FROM_LEN),
		       pMsgHeader.to.substr(0, this.TO_LEN),
		       pMsgHeader.subject.substr(0, this.SUBJ_LEN),
		       sDate, sTime);
	}
	console.cleartoeol("\1n"); // 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)
//  pReturnOnMsgSelect: Optional - A boolean to specify whether or not
//                      to return when a message is selected to read.
//  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, pReturnOnMsgSelect, pAllowChgSubBoard)
	// Create the return object and set some initial default values
	var retObj = new Object();
	retObj.continueOn = true;
	retObj.userInput = "";
	retObj.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 += "D"; // Delete
	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))
		{
			// 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);
				// Let the user read the message
				if (pReturnOnMsgSelect)
				{
					// Fill a return object with the required values, and return it.
					retObj.continueOn = continueOn;
					retObj.userInput = userInput;
					retObj.selectedMsgOffset = userInput-1;
					return retObj;
				}
				else
				{
					this.readAMessage = true;
					this.ReadMessage(userInput-1);
				}
			}
			else
				this.deniedReadingMessage = true;

			// Prompt the user whether or not to continue listing
			// messages.
			if (this.promptToContinueListingMessages)
			{
				continueOn = console.yesno(this.colors["afterReadMsg_ListMorePromptColor"] +
			}
		}
		else
		{
			// The user entered an invalid message number.
			console.print("\1n\1h\1w" + userInput + " \1y is not a valid message number.\1n");
			console.crlf();
			console.pause();
			continueOn = true;
		}
	}

	// Make sure color highlighting is turned off
	console.print("\1n");

	// 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, shows the message to the user and allows the user to
// respond.
//
// Parameters:
//  pOffset: The offset of the message to be read
//
// Return value: And object with the following properties:
//               offsetValid: Boolean - Whether or not the passed-in offset was valid
//               userReplied: Boolean - Whether or not the user replied to the message.
//               msgbaseReOpened: Boolean - Whether or not the messagebase is open after
//                                the user replied to the message.  Will be true if
//                                the user didn't reply to the message.
function DigDistMsgReader_ReadMessage(pOffset)
{
	var retObj = new Object();
	retObj.offsetValid = true;
	retObj.userReplied = false;

	// Get the message header
	var msgHeader = this.GetMsgHdrByMsgNum(pOffset+1);
	if (msgHeader == null)
	{
		console.print("\1n" + this.text.invalidMsgNumText.replace("%d", +(pOffset+1)) + "\1n");
		console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS);
		retObj.offsetValid = false;
		return retObj;
	}

	// Show the message header.
	this.DisplaySyncMsgHeader(msgHeader);

	// Show the message body.  Make sure the text is word-wrapped
	// so that it looks good.
	var msgText = this.msgbase.get_msg_body(true, msgHeader.offset);
	var msgTextWrapped = word_wrap(msgText, console.screen_columns-1);
	console.print("\1n" + this.colors["msgBodyColor"]);
	console.putmsg(msgTextWrapped, P_NOATCODES);

	// Hack: If the "from" name in the header is blank (as it might be sometimes), then
	// set it to "All".  This prevents Synchronet from crashing, and it will also default
	// the "to" name in the user's reply to "All".
	if (msgHeader["from"] == "")
		msgHeader["from"] = "All";

	// Mark the message as read, if it was written to the current
	// user.
	var msgToUpper = msgHeader["to"].toUpperCase();
	if ((msgToUpper == user.alias.toUpperCase()) || (msgToUpper == user.name.toUpperCase()))
	{
		msgHeader.attr = (msgHeader.attr | MSG_READ);
		var wroteHeader = this.msgbase.put_msg_header(true, msgHeader.offset, msgHeader);
	}

	// If not reading personal email, then update the scan & last read message pointers.
	if (this.subBoardCode != "mail") // && !this.SearchTypePopulatesSearchResults()
	{
		if (msgHeader.number > msg_area.sub[this.subBoardCode].scan_ptr)
			msg_area.sub[this.subBoardCode].scan_ptr = msgHeader.number;
		msg_area.sub[this.subBoardCode].last_read = msgHeader.number;
	}

	// Allow the user to reply to the message, either publicly or privately.
	console.print("\1n\1cEnd of message. \1hR\1b)\1n\1ceply\1h\1b, " +
	              "\1cP\1b)\1n\1crivate reply\1h\1b, \1cENTER\1b/\1cN\1b)\1n\1co reply\1h\1g: \1n\1c");
	var userKey = console.getkeys("RPN").toString();
	var privateReply = (userKey == "P");
	if ((userKey == "R") || privateReply)
	{
		var replyRetObj = this.ReplyToMsg(msgHeader, msgText, privateReply, pOffset);
		retObj.userReplied = replyRetObj.postSucceeded;
		retObj.msgbaseReOpened = replyRetObj.msgbaseReOpened;
	}

	return retObj;
}
// For the DigDistMsgReader Class: Given a message number of a message in the
// current message area, shows the message to the user and allows the user to
// respond.  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 = new Object();
	retObj.offsetValid = true;
	retObj.msgDeleted = false;
	retObj.userReplied = false;
	retObj.lastKeypress = "";
	retObj.newMsgOffset = -1;
	retObj.nextAction = ACTION_NONE;
	retObj.refreshEnhancedRdrHelpLine = false;

	// Get the message header
	var msgHeader = this.GetMsgHdrByIdx(pOffset);
	if (msgHeader == null)
	{
		console.print("\1n" + this.text.invalidMsgNumText.replace("%d", +(pOffset+1)) + "\1n");
		console.crlf();
		console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS);
		retObj.offsetValid = false;
		return retObj;
	}

	// See if the message is marked as deleted.  If so, don't let the
	// user read it, just silently return.
	retObj.msgDeleted = ((msgHeader.attr & MSG_DELETE) == MSG_DELETE);
	if (retObj.msgDeleted)
		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");

	// Hack: If the "from" name in the header is empty (as it might be sometimes), then
	// set it to "All".  This prevents Synchronet from crashing, and it will also default
	// the "to" name in the user's reply to "All".
	if (msgHeader.from.length == 0)
		msgHeader.from = "All";

	// Some key bindings
	var enhReaderKeys = new Object();
	enhReaderKeys.prevMsgByTitle = "<";
	enhReaderKeys.nextMsgByTitle = ">";
	enhReaderKeys.prevMsgByAuthor = "{";
	enhReaderKeys.nextMsgByAuthor = "}";
	enhReaderKeys.prevMsgByToUser = "[";
	enhReaderKeys.nextMsgByToUser = "]";
	enhReaderKeys.prevMsgByThreadID = "(";
	enhReaderKeys.nextMsgByThreadID = ")";
	enhReaderKeys.prevSubBoard = "-";
	enhReaderKeys.nextSubBoard = "+";
	enhReaderKeys.downloadAttachments = CTRL_A;
	enhReaderKeys.saveToBBSMachine = CTRL_S;
	enhReaderKeys.deleteMessage = KEY_DEL;
	enhReaderKeys.selectMessage = " ";
	enhReaderKeys.batchDelete = CTRL_D;

	// This function converts a thread navigation key character to its
	// corresponding thread type value
	function keypressToThreadType(pKeypress)
	{
		var threadType = THREAD_BY_ID;
		switch (pKeypress)
		{
			case enhReaderKeys.prevMsgByTitle:
			case enhReaderKeys.nextMsgByTitle:
				threadType = THREAD_BY_TITLE;
				break;
			case enhReaderKeys.prevMsgByAuthor:
			case enhReaderKeys.nextMsgByAuthor:
				threadType = THREAD_BY_AUTHOR;
				break;
			case enhReaderKeys.prevMsgByToUser:
			case enhReaderKeys.nextMsgByToUser:
				threadType = THREAD_BY_TO_USER;
				break;
			case enhReaderKeys.prevMsgByThreadID:
			case enhReaderKeys.nextMsgByThreadID:
			default:
				threadType = THREAD_BY_ID;
				break;
		}
		return threadType;
	}

	// Get the message text and see if it has any ANSI codes.  If it has ANSI codes,
	// then don't use the scrolling interface so that the ANSI gets displayed properly.
	var messageText = this.msgbase.get_msg_body(true, msgHeader.offset);
	// If the message has ANSI content, then use the scrolling interface only
	// if frame.js is available on the BBS machine and the option to use the
	// scrolling interface for ANSI messages is enabled.
	var msgHasANSICodes = textHasANSICodes(messageText);
	var useScrollingInterface = this.scrollingReaderInterface && console.term_supports(USER_ANSI);
	if (useScrollingInterface && msgHasANSICodes)
		useScrollingInterface = gFrameJSAvailable && this.useScrollingInterfaceForANSIMessages;
	// 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);
	// Use the scrollable reader interface if the setting is enabled & the user's
	// terminal supports ANSI.  Otherwise, use a more traditional user interface.
	{
		// 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, msgHasANSICodes);

		var topMsgLineIdxForLastPage = msgInfo.topMsgLineIdxForLastPage;
		var msgFractionShown = msgInfo.msgFractionShown;
		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;

		// 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.
		this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);

		// Input loop (for scrolling the message up & down)
		var msgLineFormatStr = "%-" + this.msgAreaWidth + "s";
		var writeMessage = true;
		// msgAreaHeight, msgReaderObj, and scrollbarUpdateFunction are for use
		// with scrollTextLines().
		var msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1;
		var msgReaderObj = this;
		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);
		}
		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 scrollRetObj = null;
			if (msgInfo.displayFrame != null)
			{
				msgInfo.displayFrame.draw();
				scrollRetObj = scrollFrame(msgInfo.displayFrame, msgInfo.displayFrameScrollbar,
				                           topMsgLineIdx, this.colors["msgBodyColor"],
				                           writeMessage, 1, console.screen_rows,
										   msgScrollbarUpdateFn);
			}
			else
			{
				scrollRetObj = scrollTextLines(msgInfo.messageLines, topMsgLineIdx,
				                               this.colors["msgBodyColor"], writeMessage,
				                               this.msgAreaLeft, this.msgAreaTop, this.msgAreaWidth,
				                               msgAreaHeight, 1, console.screen_rows,
				                               msgScrollbarUpdateFn);
			}
			topMsgLineIdx = scrollRetObj.topLineIdx;
			retObj.lastKeypress = scrollRetObj.lastKeypress;
			switch (retObj.lastKeypress)
			{
				case 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.PromptAndDeleteMessage() will check to see if the user
					// is a sysop or the message was posted by the user.
					// If the message was deleted, 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.PromptAndDeleteMessage(pOffset, promptPos, true, this.msgAreaWidth,
					                                                true, msgInfo.attachments);
					if (msgWasDeleted)
					{
						var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
						continueOn = msgSearchObj.continueInputLoop;
						retObj.newMsgOffset = msgSearchObj.newMsgOffset;
						retObj.nextAction = msgSearchObj.nextAction;
						if (msgSearchObj.promptGoToNextArea)
						{
							if (this.EnhReaderPromptYesNo(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 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;
				case enhReaderKeys.batchDelete:
					// 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.DeleteSelectedMessages() 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 "E": // 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 = "\1n\1h\1k" + BLOCK1; // Dim block
							// Dim block
							var scrollBarBlock = this.colors.scrollbarBGColor + this.text.scrollbarBGChar;
							if (solidBlockStartRow + numSolidScrollBlocks - 1 == this.msgAreaBottom)
							{
								//scrollBarBlock = "\1w" + BLOCK2; // Bright block
								// Bright block
								scrollBarBlock = this.colors.scrollbarScrollBlockColor + this.text.scrollbarScrollBlockChar;
							}
							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);
							writeMessage = false;
						}
						else
						{
							// 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;
							}
							else
							{
								// 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)
									console.clear("\1n");
								// 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
								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
								writeMessage = true; // We want to refresh the message on the screen
							}
						}
					}
					else
						writeMessage = false; // Don't write the current message again
					break;
				case "?": // Show the help screen
					this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgInfo.attachments.length > 0);
					// 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)
						console.clear("\1n");
					// 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
					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
					writeMessage = true; // We want to refresh the message on the screen
					break;
				case "R": // Reply to the message
				case "I": // Private message reply
					// If the user pressed P (private reply) while reading private
					// mail, then do nothing (allow only the "R" key to reply).
					var privateReply = (retObj.lastKeypress == "I");
					if (privateReply && this.readingPersonalEmail)
						writeMessage = false; // Don't re-write the current message again
					else
					{
						// Let the user reply to the message.
						var replyRetObj = this.ReplyToMsg(msgHeader, msgInfo.msgText, privateReply, pOffset);
						                                  retObj.userReplied = replyRetObj.postSucceeded;
						//retObj.msgDeleted = replyRetObj.msgWasDeleted;
						var msgWasDeleted = replyRetObj.msgWasDeleted;
						//if (retObj.msgDeleted)
						if (msgWasDeleted)
						{
							var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
							continueOn = msgSearchObj.continueInputLoop;
							retObj.newMsgOffset = msgSearchObj.newMsgOffset;
							retObj.nextAction = msgSearchObj.nextAction;
							if (msgSearchObj.promptGoToNextArea)
							{
								if (this.EnhReaderPromptYesNo(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
							}
						}
						else
						{
							// If the messagebase object was successfully re-opened after
							// posting the message, then refresh the screen.  Otherwise,
							// we'll want to quit.
							if (replyRetObj.msgbaseReOpened)
							{
								// 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)
									console.clear("\1n");
								// 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
								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
								writeMessage = true; // We want to refresh the message on the screen
							}
							else
							{
								retObj.nextAction = ACTION_QUIT;
								continueOn = false;
								// Display an error
								console.print("\1n");
								console.crlf();
								console.print("\1h\1yMessagebase error after replying.  Aborting.\1n");
								mswait(ERROR_PAUSE_WAIT_MS);
							}
						}
					}
					break;
				case "P": // 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
						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
						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, 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,
							               "\1n" + this.text.msgHasBeenDeletedText.replace("%d", msgNumInput) + "\1n",
							               ERROR_PAUSE_WAIT_MS, "\1n", true);
						}
						else
						{
							// 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);
								console.print("\1n");
								readMsg = console.yesno(sReadMsgConfirmText);
							}
							if (readMsg)
							{
								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 enhReaderKeys.prevMsgByTitle: // Previous message by title
				case enhReaderKeys.prevMsgByAuthor: // Previous message by author
				case enhReaderKeys.prevMsgByToUser: // Previous message by 'to user'
				case 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),
																			true);
						if (threadPrevMsgOffset > -1)
						{
							retObj.newMsgOffset = threadPrevMsgOffset;
							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
							continueOn = false;
						}
						else
						{
							// 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 enhReaderKeys.nextMsgByTitle: // Next message by title (subject)
				case enhReaderKeys.nextMsgByAuthor: // Next message by author
				case enhReaderKeys.nextMsgByToUser: // Next message by 'to user'
				case 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),
																			true);
						if (threadPrevMsgOffset > -1)
						{
							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 KEY_LEFT: // 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(this.text.goToPrevMsgAreaPromptText,
							                                            msgInfo.messageLines, topMsgLineIdx,
							                                            msgLineFormatStr, solidBlockStartRow,
							                                            numSolidScrollBlocks);