Skip to content
Snippets Groups Projects
DDMsgReader.js 580 KiB
Newer Older
			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 = 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 KEY_DEL: // 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.DeleteMessage() 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.DeleteMessage(pOffset, promptPos, true, this.msgAreaWidth);
					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 "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);
					// 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);
						}
						else
						{
							// We're not at the beginning of the sub-board, so it's okay to exit this
							// method and go to the previous message.
							goToPrevMessage = true;
						}
					}
					if (goToPrevMessage)
					{
						continueOn = false;
						retObj.nextAction = ACTION_GO_PREVIOUS_MSG;
					}
					else
						writeMessage = false; // No need to refresh the message
					break;
				case KEY_RIGHT: // Next message
					// Look for a later message that isn't marked for deletion.  Even
					// if we don't find one, we'll still want to return from this
					// function (with message index -1) so that this script can go
					// onto the next message sub-board/group.
					retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, true);
					// Note: Unlike the left arrow key, we want to exit this method when
					// navigating to the next message, regardless of whether or not the
					// user is allowed to change to a different sub-board, so that processes
					// that require continuation (such as new message scan) can continue.
					// Still, if there are no more readable messages in the current sub-board
					// (and thus the user would go onto the next message area), prompt the
					// user whether they want to continue onto the next message area.
					if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
					{
						// For personal mail, don't do anything, and don't refresh the
						// message.  In a sub-board, ask the user if they want to go
						// to the next one.
						if (this.readingPersonalEmail)
							writeMessage = false;
						else
						{
							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
					{
						// 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;
					// First & last message: Quit out of this input loop and let the
					// calling function, this.ReadMessages(), handle the action.
				case "F": // First message
					// Only leave this function if we aren't already on the first message.
					if (pOffset > 0)
					{
						continueOn = false;
						retObj.nextAction = ACTION_GO_FIRST_MSG;
					}
					else
						writeMessage = false; // Don't re-write the current message again
					break;
				case "L": // Last message
					// Only leave this function if we aren't already on the last message.
					if (pOffset < this.NumMessages() - 1)
					{
						continueOn = false;
						retObj.nextAction = ACTION_GO_LAST_MSG;
					}
					else
						writeMessage = false; // Don't re-write the current message again
					break;
				case enhReaderKeys.prevSubBoard: // Go to the previous message area
					if (allowChgMsgArea)
					{
						continueOn = false;
						retObj.nextAction = ACTION_GO_PREV_MSG_AREA;
					}
					else
						writeMessage = false; // Don't re-write the current message again
					break;
				case enhReaderKeys.nextSubBoard: // Go to the next message area
					if (allowChgMsgArea || this.doingMultiSubBoardScan)
					{
						continueOn = false;
						retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
					}
					else
						writeMessage = false; // Don't re-write the current message again
					break;
					// H and K: Display the extended message header info/kludge lines
					// (for the sysop)
				case "H":
				case "K":
					if (gIsSysop)
					{
						// Save the original cursor position
						var originalCurPos = console.getxy();

						// Get an array of the extended header info/kludge lines and then
						// allow the user to scroll through them.
						var extdHdrInfoLines = this.GetExtdMsgHdrInfo(msgHeader);
						if (extdHdrInfoLines.length > 0)
						{
							// Calculate information for the scrollbar for the kludge lines
							var infoFractionShown = this.msgAreaHeight / extdHdrInfoLines.length;
							if (infoFractionShown > 1)
								infoFractionShown = 1.0;
							var numInfoSolidScrollBlocks = Math.floor(this.msgAreaHeight * infoFractionShown);
							if (numInfoSolidScrollBlocks == 0)
								numInfoSolidScrollBlocks = 1;
							var numNonSolidInfoScrollBlocks = this.msgAreaHeight - numInfoSolidScrollBlocks;
							var lastInfoSolidBlockStartRow = this.msgAreaTop;
							// Define a scrollbar update function for the header info/kludge lines
							function msgInfoScrollbarUpdateFn(pFractionToLastPage)
							{
								var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidInfoScrollBlocks * pFractionToLastPage);
								if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow)
									msgReaderObj.UpdateEnhancedReaderScollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, numInfoSolidScrollBlocks);
								lastInfoSolidBlockStartRow = infoSolidBlockStartRow;
								console.gotoxy(1, console.screen_rows);
							}
							// Display the kludge lines and let the user scroll through them
							this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks);
							scrollTextLines(extdHdrInfoLines, 0, this.colors["msgBodyColor"], true,
							this.msgAreaLeft, this.msgAreaTop, this.msgAreaWidth,
							msgAreaHeight, 1, console.screen_rows,
							msgInfoScrollbarUpdateFn);
							// Display the scrollbar for the message 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
						{
							// There are no kludge lines for this message
							this.DisplayEnhReaderError(this.text.noKludgeLinesForThisMsgText, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
							console.gotoxy(originalCurPos);
							writeMessage = false;
						}
					}
					else // The user is not a sysop
						writeMessage = false;
					break;
					// Message list, change message area: Quit out of this input loop
					// and let the calling function, this.ReadMessages(), handle the
					// action.
				case "M": // Message list
					retObj.nextAction = ACTION_DISPLAY_MSG_LIST;
					continueOn = false;
					break;
				case "C": // Change message area, if allowed
					if (allowChgMsgArea)
					{
						retObj.nextAction = ACTION_CHG_MSG_AREA;
						continueOn = false;
					}
					else
						writeMessage = false; // No need to refresh the message
					break;
				/*
				case enhReaderKeys.saveToBBSMachine:
					if (gIsSysop)
					{
						// TODO: I'd like to add this functionality at some point
						// - Save the message to the BBS machine (prompt for the
						// directory on the BBS machine)
						//var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
					}
					else
						writeMessage = false;
					break;
				*/
				case "Q": // Quit
					retObj.nextAction = ACTION_QUIT;
					continueOn = false;
					break;
				default:
					writeMessage = false;
					break;
			}
		}
	}
	else
	{
		// Use the non-scrolling interface.
		// Get the message body.  Make sure the text is word-wrapped so that it
		// looks good when written to the screen.
		var msgText = this.msgbase.get_msg_body(true, msgHeader.offset);
		var msgTextWrapped = word_wrap(msgText, console.screen_columns-1);

		// Generate the key help text
		var keyHelpText = "\1n\1c\1h#\1n\1b, \1c\1hLeft\1n\1b, \1c\1hRight\1n\1b, ";
		if (this.CanDelete() || this.CanDeleteLastMsg())
			keyHelpText += "\1c\1hDEL\1b, ";
		if (this.CanEdit())
			keyHelpText += "\1c\1hE\1y)\1n\1cdit\1b, ";
		keyHelpText += "\1c\1hF\1y)\1n\1cirst\1b, \1c\1hL\1y)\1n\1cast\1b, \1c\1hR\1y)\1n\1ceply\1b, ";
		// 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 += "\1c\1hP\1y)\1n\1crivate reply\1b, ";
			keyHelpText += "\1c\1hC\1y)\1n\1chg area\1b, ";
		}
		else
		{
			// The user isn't allowed to change to a different message area.
			// Go ahead and include the private reply option.
			keyHelpText += "\1c\1hP\1y)\1n\1crivate reply\1b, ";
		}
		keyHelpText += "\1c\1hQ\1y)\1n\1cuit\1b, \1c\1h?\1g: \1c";

		// User input loop
		var writeMessage = true;
		var writePromptText = true;
		var continueOn = true;
		while (continueOn)
		{
			if (writeMessage)
			{
				if (console.term_supports(USER_ANSI))
					console.clear("\1n");
				// Write the message header & message body to the screen
				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
				console.print("\1n" + this.colors["msgBodyColor"]);
				console.putmsg(msgTextWrapped, 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*/);
			switch (retObj.lastKeypress)
			{
				case KEY_DEL: // Delete message
					console.crlf();
					// Prompt the user for confirmation to delete the message.
					// Note: this.DeleteMessage() 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.DeleteMessage(pOffset);
					if (msgWasDeleted)
					{
						var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
						continueOn = msgSearchObj.continueInputLoop;
						retObj.newMsgOffset = msgSearchObj.newMsgOffset;
						retObj.nextAction = msgSearchObj.nextAction;
						if (msgSearchObj.promptGoToNextArea)
						{
							if (console.yesno(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 "E": // Edit the message
					if (this.CanEdit())
					{
						console.crlf();
						// 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 "?": // Show help
					if (!console.term_supports(USER_ANSI))
					{
						console.crlf();
						console.crlf();
					}
					this.DisplayEnhancedReaderHelp(allowChgMsgArea);
					if (!console.term_supports(USER_ANSI))
					{
						console.crlf();
						console.crlf();
					}
					break;
				case "R": // Reply to the message
				case "I": // Private reply
					var privateReply = (retObj.lastKeypress == "I");
					// If the user pressed P (private reply) while reading private
					// mail, then do nothing (allow only the "R" key to reply).
					// If not reading personal email, go ahead and let the user reply
					// with either the "P" or "R" keypress.
					var privateReply = (retObj.lastKeypress == "I");
					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();
						var replyRetObj = this.ReplyToMsg(msgHeader, msgText, privateReply, pOffset);
						retObj.userReplied = replyRetObj.postSucceeded;
						//retObj.msgDeleted = replyRetObj.msgWasDeleted;
						var msgWasDeleted = replyRetObj.msgWasDeleted;
						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 not successfully re-opened
							// after posting the message, then  we'll want to quit.
							if (!replyRetObj.msgbaseReOpened)
							{
								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))
						{
							// 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?
						}

						// 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, 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.crlf();
							console.print("\1n" + this.text.msgHasBeenDeletedText.replace("%d", msgNumInput) + "\1n");
							console.crlf();
							console.pause();
						}
						else
						{
							// Confirm with the user whether to read the message
							var readMsg = true;
							if (this.promptToReadMessage)
							{
								readMsg = console.yesno("\1n" + this.colors["readMsgConfirmColor"]
								                        + "Read message "
								                        + this.colors["readMsgConfirmNumberColor"]
								                        + msgNumInput + this.colors["readMsgConfirmColor"]
								                        + ": Are you sure");
							}
							if (readMsg)
							{
								continueOn = false;
								retObj.newMsgOffset = msgNumInput - 1;
								retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
							}
						}
					}
					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())
					{
						console.crlf(); // For the "Searching..." text
						var threadPrevMsgOffset = this.FindThreadPrevOffset(msgHeader,
						                                                    keypressToThreadType(retObj.lastKeypress),
																			false);
						if (threadPrevMsgOffset > -1)
						{
							retObj.newMsgOffset = threadPrevMsgOffset;
							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
							continueOn = false;
						}
					}
					else
					{
						writeMessage = false;
						writePromptText = false;
					}
					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())
					{
						console.crlf(); // For the "Searching..." text
						var threadNextMsgOffset = this.FindThreadNextOffset(msgHeader,
						                                                    keypressToThreadType(retObj.lastKeypress),
																			false);
						if (threadNextMsgOffset > -1)
							retObj.newMsgOffset = threadNextMsgOffset;
							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
							continueOn = false;
						}
					}
					else
					{
						writeMessage = false;
						writePromptText = false;
					}
					break;
				case KEY_LEFT: // 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.FindNextNonDeletedMsgIdx(pOffset, false);
					var goToPrevMessage = false;
					if ((retObj.newMsgOffset > -1) || allowChgMsgArea)
					{
						if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
						{
							console.crlf();
							goToPrevMessage = console.yesno(this.text.goToPrevMsgAreaPromptText);
						}
						else
						{
							// 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 KEY_RIGHT: // Next message
					// Look for a later message that isn't marked for deletion.  Even
					// if we don't find one, we'll still want to return from this
					// function (with message index -1) so that this script can go
					// onto the next message sub-board/group.
					retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, true);
					// Note: Unlike the left arrow key, we want to exit this method when
					// navigating to the next message, regardless of whether or not the
					// user is allowed to change to a different sub-board, so that processes
					// that require continuation (such as new message scan) can continue.
					// Still, if there are no more readable messages in the current sub-board
					// (and thus the user would go onto the next message area), prompt the
					// user whether they want to continue onto the next message area.
					if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
					{
						console.crlf();
						if (console.yesno(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 "F": // 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 "L": // 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)