Skip to content
Snippets Groups Projects
DDMsgReader.js 892 KiB
Newer Older
					}
					else
					{
						writeMessage = false; // Don't refresh the whole message
						if (valRetObj.refreshBottomLine)
							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
						if (valRetObj.optionBoxTopLeftX > 0 && valRetObj.optionBoxTopLeftY > 0 && valRetObj.optionBoxWidth > 0 && valRetObj.optionBoxHeight > 0)
							this.RefreshMsgAreaRectangle(msgInfo.messageLines, topMsgLineIdx, valRetObj.optionBoxTopLeftX, valRetObj.optionBoxTopLeftY, valRetObj.optionBoxWidth, valRetObj.optionBoxHeight);
					}
				}
				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.userSettings.useEnhReaderScrollbar setting in case it changes, so we can tell if we need to refresh the scrollbar
				var oldUseEnhReaderScrollbar = this.userSettings.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.userSettings.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.userSettings.useEnhReaderScrollbar != oldUseEnhReaderScrollbar)
						msgAreaWidth = this.userSettings.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.userSettings.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);
			case KEY_ESC:
				// Normally, if quitFromReaderGoesToMsgList is enabled, then do that
				if (this.userSettings.quitFromReaderGoesToMsgList)
				{
					console.attributes = "N";
					console.crlf();
					console.print("Loading...");
					retObj.nextAction = ACTION_DISPLAY_MSG_LIST;
				}
				else
					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 the next readable message.  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.FindNextReadableMsgIdx(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.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, pmode)
	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, pmode|P_NOATCODES);
		// Write the prompt text
		if (writePromptText)
			console.print(keyHelpText);
		// Default the writing of the message & input prompt to true for the
		// next iteration.
		writeMessage = true;
		writePromptText = true;
		// Input a key from the user and take action based on the keypress.
		//retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN);
		retObj.lastKeypress = getKeyWithESCChars(K_UPPER);
		switch (retObj.lastKeypress)
			case this.enhReaderKeys.deleteMessage: // Delete message
				console.crlf();
				// 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, 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, null, true);
				if (msgWasDeleted && !canViewDeletedMsgs())
				{
					var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
					continueOn = msgSearchObj.continueInputLoop;
					retObj.newMsgOffset = msgSearchObj.newMsgOffset;
					retObj.nextAction = msgSearchObj.nextAction;
					if (msgSearchObj.promptGoToNextArea)
					{
						if (console.yesno(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText)))
						{
							// 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
					}
				}
				break;
			case this.enhReaderKeys.selectMessage: // Select message (for batch delete, etc.)
				console.crlf();
				var selectMessage = !console.noyes("Select this message");
				this.ToggleSelectedMessage(this.subBoardCode, pOffset, selectMessage);
				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 message
					// Let the user edit the message if they want to
					var editReturnObj = this.EditExistingMsg(pOffset);
					// If the user confirmed editing the message, then see if the
					// message was edited and refresh the screen accordingly.
					if (editReturnObj.userConfirmed)
					{
						// 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.
							continueOn = false;
							retObj.newMsgOffset = editReturnObj.newMsgIdx;
						}
					}
				}
				else
				{
					writeMessage = false;
					writePromptText = false;
				}
				break;
			case this.enhReaderKeys.showHelp: // Show help
				if (!console.term_supports(USER_ANSI))
				{
					console.crlf();
					console.crlf();
				}
				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgHasAttachments);
				if (!console.term_supports(USER_ANSI))
				{
					console.crlf();
					console.crlf();
				}
				break;
			case this.enhReaderKeys.reply: // Reply to the message
			case this.enhReaderKeys.privateReply: // Private reply
				// If the user pressed the private reply key while reading private
				// mail, then do nothing (allow only the regular reply key to reply).
				// If not reading personal email, go ahead and let the user reply
				// with either the reply or private reply keypress.
				var privateReply = (retObj.lastKeypress == this.enhReaderKeys.privateReply);
				if (privateReply && this.readingPersonalEmail)
				{
					writeMessage = false; // Don't re-write the current message again
					writePromptText = false; // Don't write the prompt text again
				}
				else
				{
					console.crlf();
					// 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 (msgWasDeleted && !canViewDeletedMsgs())
							var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
							continueOn = msgSearchObj.continueInputLoop;
							retObj.newMsgOffset = msgSearchObj.newMsgOffset;
							retObj.nextAction = msgSearchObj.nextAction;
							if (msgSearchObj.promptGoToNextArea)
								if (console.yesno(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText)))
								{
									// 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
						console.print("\x01h\x01yFailed to open the sub-board.  Aborting.\x01n");
			case this.enhReaderKeys.postMsg: // Post a message
				if (!this.readingPersonalEmail)
				{
					// Let the user post a message.
					if (bbs.post_msg(this.subBoardCode))
						// 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?

					console.pause();

					// We'll want to refresh the message & prompt text on the screen
					writeMessage = true;
					writePromptText = true;
				}
				else
				{
					// Don't write the current message or prompt text in the next iteration
					writeMessage = false;
					writePromptText = false;
				}
				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":
				console.crlf();
				// 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);
				// Prompt for the message number
				var msgNumInput = this.PromptForMsgNum(null, 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))
						console.print("\x01n" + replaceAtCodesInStr(format(this.text.msgHasBeenDeletedText, msgNumInput)) + "\x01n");
						// Confirm with the user whether to read the message
						var readMsg = true;
						if (this.promptToReadMessage)
							readMsg = console.yesno("\x01n" + this.colors["readMsgConfirmColor"]
													+ "Read message "
													+ this.colors["readMsgConfirmNumberColor"]
													+ msgNumInput + this.colors["readMsgConfirmColor"]
													+ ": Are you sure");
							retObj.newMsgOffset = msgNumInput - 1;
							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						}
					}
			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())
				{
					console.crlf(); // For the "Searching..." text
					var threadPrevMsgOffset = this.FindThreadPrevOffset(msgHeader,
																		keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
						retObj.newMsgOffset = threadPrevMsgOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
			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())
				{
					console.crlf(); // For the "Searching..." text
					var threadNextMsgOffset = this.FindThreadNextOffset(msgHeader,
																		keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
						retObj.newMsgOffset = threadNextMsgOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
			case this.enhReaderKeys.previousMsg: // Previous message
				// TODO: Change the key for this?
				// 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.FindNextReadableMsgIdx(pOffset, false);
				var goToPrevMessage = false;
				if ((retObj.newMsgOffset > -1) || allowChgMsgArea)
				{
					if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
						goToPrevMessage = console.yesno(replaceAtCodesInStr(this.text.goToPrevMsgAreaPromptText));
						// 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;
				}
				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.
				retObj.newMsgOffset = this.FindNextReadableMsgIdx(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())
				{
					console.crlf();
					// 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)
						// 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))))
						if (console.yesno(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText)))
						{
							// Let this method exit and let the caller go to the next sub-board
							continueOn = false;
							retObj.nextAction = ACTION_GO_NEXT_MSG;
						}
				}
				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.
					continueOn = false;
					retObj.nextAction = ACTION_GO_NEXT_MSG;
				}
				break;
			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;
					writePromptText = false;
				}
				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;
					writePromptText = false;
				}
				break;
			case "-": // Go to the previous message area
				if (allowChgMsgArea)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_PREV_MSG_AREA;
				}
				else
				{
					writeMessage = false;
					writePromptText = false;
				}
				break;
			case "+": // Go to the next message area
				if (allowChgMsgArea || this.doingMultiSubBoardScan)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
				}
				else
				{
					writeMessage = false;
					writePromptText = false;
				}
				break;
			// H and K: Display the extended message header info/kludge lines
			// (for the sysop)
			case this.enhReaderKeys.showHdrInfo:
			case this.enhReaderKeys.showKludgeLines:
				{
					console.crlf();
					// Get an array of the extended header info/kludge lines and then
					// display them.
					var extdHdrInfoLines = this.GetExtdMsgHdrInfo(this.subBoardCode, msgHeader.number, (retObj.lastKeypress == this.enhReaderKeys.showKludgeLines));
						for (var infoIter = 0; infoIter < extdHdrInfoLines.length; ++infoIter)
							console.print(extdHdrInfoLines[infoIter]);
						// There are no kludge lines for this message
						console.print(replaceAtCodesInStr(this.text.noKludgeLinesForThisMsgText));
				}
				else // The user is not a sysop
				{
					writeMessage = false;
					writePromptText = 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;
			case this.enhReaderKeys.downloadAttachments: // Download attachments
					console.print("\x01c- Download Attached Files -\x01n");
					allowUserToDownloadMessage_NewInterface(msgHeader, this.subBoardCode);

					// Ensure the message is refreshed on the screen
					writeMessage = true;
					writePromptText = true;
				}
				else
				{
					writeMessage = false;
					writePromptText = false;
				}
				break;
			case this.enhReaderKeys.saveToBBSMachine:
				// Save the message to the BBS machine - Only allow this
				// if the user is a sysop.
					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.print("\x01n\x01cThe message has been saved.\x01n");
							if (msgHdrHasAttachmentFlag(msgHeader))
								console.print(" Attachments not saved.");
							console.print("\x01n\x01y\x01hFailed: " + saveMsgRetObj.errorMsg + "\x01n");
						console.print("\x01n\x01y\x01hMessage not exported\x01n");
					writeMessage = true;
				}
				else
					writeMessage = false;
				break;
			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");
			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();
				}
				writeMessage = true;
				break;
			case this.enhReaderKeys.vote: // Vote on the message
				var voteRetObj = this.VoteOnMessage(msgHeader);
				if (voteRetObj.BBSHasVoteFunction)
				{
					if (!voteRetObj.userQuit)
					{
						if ((voteRetObj.errorMsg.length > 0) || (!voteRetObj.savedVote))
						{
							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");
							console.crlf();
							console.pause();
						}
						else
							msgHeader = voteRetObj.updatedHdr; // To get updated vote information
					}
					// If this message is a poll, then 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.
					if ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL)
					{
						retObj.newMsgOffset = pOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						continueOn = false;
					}
					else
						writeMessage = true; // We want to refresh the message on the screen
				break;
			case this.enhReaderKeys.showVotes: // Show votes
				if (msgHeader.hasOwnProperty("total_votes") && msgHeader.hasOwnProperty("upvotes"))
				{
					var voteInfo = this.GetUpvoteAndDownvoteInfo(msgHeader);
					for (var voteInfoIdx = 0; voteInfoIdx < voteInfo.length; ++voteInfoIdx)
					console.print("\x01n\x01h\x01yThere is no voting information for this message\x01n");
					console.crlf();
				}
				console.pause();
				writeMessage = true;
				break;
			case this.enhReaderKeys.closePoll: // Close a poll message
				var pollCloseMsg = "";
				console.crlf();
				// If this message is a poll, then allow closing it.
				if ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL)