Skip to content
Snippets Groups Projects
DDMsgReader.js 731 KiB
Newer Older
		var menuItemObj = this.MakeItemWithRetval(-1);
		if ((pGrpIndex >= 0) && (pGrpIndex < msg_area.grp_list.length))
		{
			menuItemObj.text = format(((typeof(bbs.curgrp) == "number") && (pGrpIndex == msg_area.sub[this.msgReader.subBoardCode].grp_index)) ? "*" : " ");
			menuItemObj.text += format(this.msgReader.msgGrpListPrintfStr, +(pGrpIndex+1),
			                           msg_area.grp_list[pGrpIndex].description.substr(0, this.msgReader.msgGrpDescLen),
			                           msg_area.grp_list[pGrpIndex].sub_list.length);
			menuItemObj.text = strip_ctrl(menuItemObj.text);
			menuItemObj.retval = pGrpIndex;
		}

		return menuItemObj;
	};

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

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

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

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

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

		return menuItemObj;
	};

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

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

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

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

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

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

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

	var continueOn = true;
	// Prompt the user whether or not to continue or to read a message
	// (by message number).  Make use of the different prompt texts,
	// depending whether we're at the beginning, in the middle, or at
	// the end of the message list.
	var userInput = "";
	var allowedKeys = "?GS"; // ? = help, G = Go to message #, S = Select message(s), Ctrl-D: Batch delete
	if (allowChgSubBoard)
		allowedKeys += "C"; // Change to another message area
	if (this.CanDelete() || this.CanDeleteLastMsg())
		allowedKeys += "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))
		{
			// See if the current message header has our "isBogus" property and it's true.
			// Only let the user read the message if it's not a bogus message header.
			// The message header could have the "isBogus" property, for instance, if
			// it's a vote message (introduced in Synchronet 3.17).
			var tmpMsgHdr = this.GetMsgHdrByIdx(+(userInput-1), false);
			var hdrIsBogus = (tmpMsgHdr.hasOwnProperty("isBogus") ? tmpMsgHdr.isBogus : false);
			if (!hdrIsBogus)
				// Confirm with the user whether to read the message
				var readMsg = true;
				if (this.promptToReadMessage)
				{
					var sReadMsgConfirmText = this.colors["readMsgConfirmColor"]
											+ "Read message "
											+ this.colors["readMsgConfirmNumberColor"]
											+ userInput + this.colors["readMsgConfirmColor"]
											+ ": Are you sure";
					readMsg = console.yesno(sReadMsgConfirmText);
				}
				if (readMsg)
				{
					// Update the message list screen variables
					this.CalcMsgListScreenIdxVarsFromMsgNum(+userInput);
					// Return from here so that the calling function can switch
					// into reader mode.
					retObj.continueOn = continueOn;
					retObj.userInput = userInput;
					retObj.selectedMsgOffset = userInput-1;
					return retObj;
				}
				else
					this.deniedReadingMessage = true;

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

	// Make sure color highlighting is turned off

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

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

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

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

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

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

	return retObj;
}
// Helper method for ReadMessageEnhanced() - Does the scrollable reader interface
function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, pOffset)
	var retObj = {
		offsetValid: true,
		msgNotReadable: false,
		userReplied: false,
		lastKeypress: "",
		newMsgOffset: -1,
		nextAction: ACTION_NONE,
		refreshEnhancedRdrHelpLine: false
	};
	// If the message has ANSI content, then use a Graphic object to help make
	// the message look good.  Also, remove any ANSI clear screen codes from the
	// message text.
	var msgHasANSICodes = messageText.indexOf("\x1b[") >= 0;
	if (msgHasANSICodes)
	{
		messageText = messageText.replace(/\u001b\[[012]J/gi, "");
		//textHasDrawingChars(messageText)
		var graphic = new Graphic(this.msgAreaWidth, this.msgAreaHeight-1);
		graphic.auto_extend = true;
		graphic.ANSI = ansiterm.expand_ctrl_a(messageText);
		graphic.width = this.msgAreaWidth;
		messageText = graphic.MSG;
	}

	// Show the message header
	this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);

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

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

	// 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 msgHasAttachments = msgHdrHasAttachmentFlag(msgHeader);
	// User input loop
	var continueOn = true;
	while (continueOn)
	{
		// Display the message lines (depending on the value of writeMessage)
		// and handle scroll keys via scrollTextLines().  Handle other keypresses
		// here.
		var scrollbarInfoObj = {
			solidBlockLastStartRow: 0,
			numSolidScrollBlocks: 0
		};
		scrollbarInfoObj.solidBlockLastStartRow = solidBlockLastStartRow;
		scrollbarInfoObj.numSolidScrollBlocks = numSolidScrollBlocks;
		var scrollRetObj = scrollTextLines(msgInfo.messageLines, topMsgLineIdx,
									   this.colors.msgBodyColor, writeMessage,
									   this.msgAreaLeft, this.msgAreaTop, this.msgAreaWidth,
									   msgAreaHeight, 1, console.screen_rows,
									   msgScrollbarUpdateFn, scrollbarInfoObj,
									   this.enhReadHelpLineClickCoords);
		if (scrollRetObj.mouse != null)
		{
			// See if there was a click in one of the reader help line click coordinates
			var clickCoordRetObj = this.ScrollReaderDetermineClickCoordAction(scrollRetObj, this.enhReadHelpLineClickCoords);
			if (clickCoordRetObj.actionStr.length > 0)
				scrollRetObj.lastKeypress = clickCoordRetObj.actionStr; // A bit of a kludge
		}
		topMsgLineIdx = scrollRetObj.topLineIdx;
		retObj.lastKeypress = scrollRetObj.lastKeypress;
		switch (retObj.lastKeypress)
		{
			case this.enhReaderKeys.deleteMessage: // Delete message
				var originalCurpos = console.getxy();
				// The 2nd to last row of the screen is where the user will
				// be prompted for confirmation to delete the message.
				// Ideally, I'd like to put the cursor on the last row of
				// the screen for this, but console.noyes() lets the enter
				// key shift everything on screen up one row, and there's
				// no way to avoid that.  So, to optimize screen refreshing,
				// the cursor is placed on the 2nd to the last row on the
				// screen to prompt for confirmation.
				var promptPos = this.EnhReaderPrepLast2LinesForPrompt();

				// Prompt the user for confirmation to delete the message.
				// Note: this.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);
				if (msgWasDeleted)
				{
					var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
					continueOn = msgSearchObj.continueInputLoop;
					retObj.newMsgOffset = msgSearchObj.newMsgOffset;
					retObj.nextAction = msgSearchObj.nextAction;
					if (msgSearchObj.promptGoToNextArea)
						if (this.EnhReaderPromptYesNo(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText), msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks))
							// Let this method exit and let the caller go to the next sub-board
							continueOn = false;
							retObj.nextAction = ACTION_GO_NEXT_MSG;
						else
							writeMessage = false; // No need to refresh the message
				}
				else
				{
					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
					// Move the cursor back to its original position
					console.gotoxy(originalCurpos);
					writeMessage = false;
				}
				break;
			case this.enhReaderKeys.selectMessage: // Select message (for batch delete, etc.)
				var originalCurpos = console.getxy();
				var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
				if (this.EnhReaderPromptYesNo("Select this message", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks, true))
					this.ToggleSelectedMessage(this.subBoardCode, pOffset, true);
				else
					this.ToggleSelectedMessage(this.subBoardCode, pOffset, false);
				writeMessage = false; // No need to refresh the message
				break;
				// TODO: Write this?  Not sure yet if it makes much sense to
				// have batch delete in the reader interface.
				// Prompt the user for confirmation, and use
				// this.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 this.enhReaderKeys.editMsg: // Edit the messaage
				if (this.CanEdit())
				{
					// Move the cursor to the last line in the message area so
					// the edit confirmation prompt will appear there.  Not using
					// the last line on the screen because the yes/no prompt will
					// output a carriage return and move everything on the screen
					// up one line, which is not ideal in case the user says No.
					var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
					// Let the user edit the message if they want to
					var editReturnObj = this.EditExistingMsg(pOffset);
					// If the user didn't confirm, then we only have to refresh the bottom
					// help line.  Otherwise, we need to refresh everything on the screen.
					if (!editReturnObj.userConfirmed)
						// For some reason, the yes/no prompt erases the last character
						// of the scrollbar - So, figure out which block was there and
						// refresh it.
						//var scrollBarBlock = "\x01n\x01h\x01k" + BLOCK1; // Dim block
						// Dim block
						var scrollBarBlock = this.colors.scrollbarBGColor + this.text.scrollbarBGChar;
						if (solidBlockStartRow + numSolidScrollBlocks - 1 == this.msgAreaBottom)
						{
							//scrollBarBlock = "\x01w" + BLOCK2; // Bright block
							// Bright block
							scrollBarBlock = this.colors.scrollbarScrollBlockColor + this.text.scrollbarScrollBlockChar;
						}
						console.gotoxy(this.msgAreaRight+1, this.msgAreaBottom);
						console.print(scrollBarBlock);
						// Refresh the last 2 message lines on the screen, then display
						// the key help line
						this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
						// If the message was edited, then refresh the text lines
						// array and update the other message-related variables.
						if (editReturnObj.msgEdited && (editReturnObj.newMsgIdx > -1))
							// When the message is edited, the old message will be
							// deleted and the edited message will be posted as a new
							// message.  So we should return to the caller and have it
							// go directly to that new message.
							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
							continueOn = false;
							retObj.newMsgOffset = editReturnObj.newMsgIdx;
							// The message was not edited.  Refresh everything on the screen.
							// If the enhanced message header width is less than the console
							// width, then clear the screen to remove anything that might be
							// left on the screen by the message editor.
							if (this.enhMsgHeaderWidth < console.screen_columns)
							// Display the message header and key help line
							this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
							// Display the scrollbar again, and ensure it's in the correct position
							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 this.enhReaderKeys.showHelp: // Show the help screen
				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgHasAttachments);
				// If the enhanced message header width is less than the console
				// width, then clear the screen to remove anything left on the
				// screen from the help screen.
				if (this.enhMsgHeaderWidth < console.screen_columns)
				// Display the message header and key help line
				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
				// Display the scrollbar again, and ensure it's in the correct position
				solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
				this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
				writeMessage = true; // We want to refresh the message on the screen
				break;
			case this.enhReaderKeys.reply: // Reply to the message
			case this.enhReaderKeys.privateReply: // Private message reply
				// If the user pressed the private reply key while reading private
				// mail, then do nothing (allow only the regular reply key to reply).
				var privateReply = (retObj.lastKeypress == this.enhReaderKeys.privateReply);
				if (privateReply && this.readingPersonalEmail)
					writeMessage = false; // Don't re-write the current message again
				else
				{
					// Get the message header with fields expanded so we can get the most info possible.
					//var extdMsgHdr = this.GetMsgHdrByAbsoluteNum(msgHeader.number, true);
					var msgbase = new MsgBase(this.subBoardCode);
					if (msgbase.open())
						var extdMsgHdr = msgbase.get_msg_header(false, msgHeader.number, true);
						msgbase.close();
						// Let the user reply to the message.
						var replyRetObj = this.ReplyToMsg(extdMsgHdr, messageText, privateReply, pOffset);
						retObj.userReplied = replyRetObj.postSucceeded;
						//retObj.msgNotReadable = replyRetObj.msgWasDeleted;
						var msgWasDeleted = replyRetObj.msgWasDeleted;
						//if (retObj.msgNotReadable)
						if (msgWasDeleted)
							var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
							continueOn = msgSearchObj.continueInputLoop;
							retObj.newMsgOffset = msgSearchObj.newMsgOffset;
							retObj.nextAction = msgSearchObj.nextAction;
							if (msgSearchObj.promptGoToNextArea)
								if (this.EnhReaderPromptYesNo(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText), msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks))
								{
									// Let this method exit and let the caller go to the next sub-board
									continueOn = false;
									retObj.nextAction = ACTION_GO_NEXT_MSG;
								}
								else
									writeMessage = true; // We want to refresh the message on the screen
							// If the enhanced message header width is less than the console
							// width, then clear the screen to remove anything left on the
							// screen by the message editor.
							if (this.enhMsgHeaderWidth < console.screen_columns)
							// Display the message header and key help line again
							this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
							// Display the scrollbar again to refresh it on the screen
							solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
							writeMessage = true; // We want to refresh the message on the screen
			case this.enhReaderKeys.postMsg: // Post a message
				if (!this.readingPersonalEmail)
				{
					// Let the user post a message.
					if (bbs.post_msg(this.subBoardCode))
						if (searchTypePopulatesSearchResults(this.searchType))
							// TODO: If the user is doing a search, it might be
							// useful to search their new message and add it to
							// the search results if it's a match..  but maybe
							// not?
					// Refresh things on the screen
					// Display the message header and key help line again
					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
					// Display the scrollbar again to refresh it on the screen
					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, replaceAtCodesInStr(this.text.readMsgNumPromptText), false, ERROR_PAUSE_WAIT_MS, false);
				// Only allow reading the message if the message number is valid
				// and it's not the same message number that was passed in.
				if ((msgNumInput > 0) && (msgNumInput-1 != pOffset))
				{
					// If the message is marked as deleted, then output an error
					if (this.MessageIsDeleted(msgNumInput-1))
					{
						writeWithPause(this.msgAreaLeft, console.screen_rows-1,
									   "\x01n" + replaceAtCodesInStr(format(this.text.msgHasBeenDeletedText, msgNumInput)) + "\x01n",
									   ERROR_PAUSE_WAIT_MS, "\x01n", true);
						// Confirm with the user whether to read the message
						var readMsg = true;
						if (this.promptToReadMessage)
							var sReadMsgConfirmText = this.colors["readMsgConfirmColor"]
													+ "Read message "
													+ this.colors["readMsgConfirmNumberColor"]
													+ msgNumInput + this.colors["readMsgConfirmColor"]
													+ ": Are you sure";
							console.gotoxy(promptPos);
							readMsg = console.yesno(sReadMsgConfirmText);
							continueOn = false;
							retObj.newMsgOffset = msgNumInput - 1;
							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						else
							writeMessage = false; // Don't re-write the current message again
				}
				else // Message number invalid or the same as what was passed in
					writeMessage = false; // Don't re-write the current message again
				// If the user chose to continue reading messages, then refresh
				// the last 2 message lines in the last part of the message area
				// and then put the cursor back to its original position.
				if (continueOn)
				{
					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
					// Move the cursor back to its original position
					console.gotoxy(originalCurpos);
				}
				break;
			case this.enhReaderKeys.prevMsgByTitle: // Previous message by title