Skip to content
Snippets Groups Projects
DDMsgReader.js 731 KiB
Newer Older
		// E: Edit a message
		else if (userInput == "E")
		{
			if (this.CanEdit())
			{
				// See if the current message header has our "isBogus" property and it's true.
				// Only let the user edit 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 hdrIsBogus = (msgHeader.hasOwnProperty("isBogus") ? msgHeader.isBogus : false);
				if (!hdrIsBogus)
				{
					var originalCurpos = console.getxy();
					// Ask the user if they really want to edit the message
					console.gotoxy(1, console.screen_rows);
					console.print("\1n");
					console.clearline();
					// Let the user edit the message
					//var returnObj = this.EditExistingMsg(msgHeader.offset);
					var returnObj = this.EditExistingMsg(this.lightbarListSelectedMsgIdx);
					// Refresh the screen
					console.clear("\1n");
					this.WriteMsgListScreenTopHeader();
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
					console.gotoxy(1, this.lightbarMsgListStartScreenRow);
					lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
					console.gotoxy(originalCurpos); // Put the cursor back where it should be
				}
			}
		}
		// G: Go to a specific message by # (highlight or place that message on the top)
		else if (userInput == "G")
		{
			var originalCurpos = console.getxy();

			// Move the cursor to the bottom of the screen and
			// prompt the user for a message number.
			console.gotoxy(1, console.screen_rows);
			userInput = this.PromptForMsgNum({ x: 1, y: console.screen_rows }, "\n" + this.text.goToMsgNumPromptText, true, ERROR_PAUSE_WAIT_MS, false);
			if (userInput > 0)
			{
				// Make sure the message number is for a valid message (i.e., it
				// could be an invalid message number if there is a search, where
				// not all message numbers are consecutive).
				if (this.GetMsgHdrByMsgNum(userInput) != null)
				{
					// If the message is on the current page, then just go to and
					// highlight it.  Otherwise, set the user's selected message on the
					// top of the page.  We also have to make sure that this.lightbarListCurPos.y and
					// originalCurpos.y are set correctly.  Also, account for search
					// results if there are any (we'll need to have the correct array
					// index for the search results).
					var chosenMsgIndex = userInput - 1;
					if ((chosenMsgIndex <= bottomMsgIndex) && (chosenMsgIndex >= this.lightbarListTopMsgIdx))
					{
						this.lightbarListSelectedMsgIdx = chosenMsgIndex;
						originalCurpos.y = this.lightbarListCurPos.y = this.lightbarListSelectedMsgIdx - this.lightbarListTopMsgIdx + this.lightbarMsgListStartScreenRow;
					}
					else
					{
						this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx = chosenMsgIndex;
						originalCurpos.y = this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow;
					}
				}
				else
				{
					// The user entered an invalid message number
					console.print("\1n" + this.text.invalidMsgNumText.replace("%d", userInput) + "\1n");
					console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS);
				}
			// Clear & re-draw the screen, to fix any possible alignment problems
			// caused by newline output after the user inputs their choice.
			console.clear("\1n");
			this.WriteMsgListScreenTopHeader();
			DisplayHelpLine(this.msgListLightbarModeHelpLine);
			console.gotoxy(1, this.lightbarMsgListStartScreenRow);
			lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
			console.gotoxy(originalCurpos); // Put the cursor back where it should be
		}
		// C: Change to another message area (sub-board)
		else if (userInput == "C")
		{
			if (allowChgSubBoard && (this.subBoardCode != "mail"))
			{
				// Store the current sub-board code so we can see if it changed
				var oldSubCode = bbs.cursub_code;
				// Let the user choose another message area.  If they chose
				// a different message area, then set up the message base
				// object accordingly.
				this.SelectMsgArea();
				if (bbs.cursub_code != oldSubCode)
				{
					var chgSubRetval = this.ChangeSubBoard(bbs.cursub_code);
					continueOn = chgSubRetval.succeeded;
				}
				// Update the lightbar list variables and refresh the screen
				if (continueOn)
				{
					this.SetUpLightbarMsgListVars();
					console.clear("\1n");
					this.WriteMsgListScreenTopHeader();
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
					// List a screenful of message headers
					console.gotoxy(1, this.lightbarMsgListStartScreenRow);
					var lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
					// Move the cursor to where it needs to be
					console.gotoxy(this.lightbarListCurPos);
				}
			}
		}
		// Spacebar: Select a message for batch operations (such as batch
		// delete, etc.)
		else if (userInput == " ")
			this.ToggleSelectedMessage(this.subBoardCode, this.lightbarListSelectedMsgIdx);
		// Ctrl-A: Select/de-select all messages
		else if (userInput == CTRL_A)
		{
			var originalCurpos = console.getxy();
			console.gotoxy(1, console.screen_rows);
			console.print("\1n");
			console.clearline();
			console.gotoxy(1, console.screen_rows);

			// Prompt the user to select All, None (un-select all), or Cancel
			console.print("\1n\1gSelect \1c(\1hA\1n\1c)\1gll, \1c(\1hN\1n\1c)\1gone, or \1c(\1hC\1n\1c)\1gancel: \1h\1g");
			var userChoice = getAllowedKeyWithMode("ANC", K_UPPER | K_NOCRLF);
			if ((userChoice == "A") || (userChoice == "N"))
			{
				// Toggle all the messages
				var messageSelectToggle = (userChoice == "A");
				var totalNumMessages = this.NumMessages();
				var messageIndex = 0;
				for (messageIndex = 0; messageIndex < totalNumMessages; ++messageIndex)
					this.ToggleSelectedMessage(this.subBoardCode, messageIndex, messageSelectToggle);
				// Refresh the selected message checkmarks on the screen - Add the
				// checkmarks for messages that are selected, and write a blank space
				// (no checkmark) for messages that are not selected.
				var currentRow = this.lightbarMsgListStartScreenRow;
				var messageIndexEnd = this.lightbarListTopMsgIdx + this.lightbarMsgListNumLines;
				for (messageIndex = this.lightbarListTopMsgIdx; messageIndex < messageIndexEnd; ++messageIndex)
				{
					// Skip the current selected message because that one's checkmark
					// will be refreshed.  Also skip this one if the message has been
					// marked as deleted already.
					if (!this.MessageIsDeleted(messageIndex) && (messageIndex != this.lightbarListSelectedMsgIdx))
					{
						console.gotoxy(this.MSGNUM_LEN+1, currentRow);
						console.print("\1n");
						if (this.MessageIsSelected(this.subBoardCode, messageIndex))
							console.print(this.colors.selectedMsgMarkColor + CHECK_CHAR + "\1n");
						else
							console.print(" \1n");
					}
					++currentRow;
				}
			}

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

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

				// In case all messages were deleted, if that's the case, show
				// an appropriate message and don't continue listing messages.
				//if (this.NumMessages(true) == 0)
				if (!this.NonDeletedMessagesExist())
				{
					continueOn = false;
					// Note: The following doesn't seem to be necessary, since
					// the ReadOrListSubBoard() method will show a message saying
					// there are no messages to read and then will quit out.
					/*
					this.msgbase.close();
					this.msgbase = null;
					console.clear("\1n");
					console.center("\1n\1h\1yThere are no messages to display.");
					console.crlf();
					console.pause();
					*/
				}
				else
				{
					// There are still messages to list, so refresh the screen.
					console.clear("\1n");
					this.WriteMsgListScreenTopHeader();
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
					console.gotoxy(1, this.lightbarMsgListStartScreenRow);
					lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
					console.gotoxy(originalCurpos); // Put the cursor back where it should be
				}
			}
			else
			{
				// There are no selected messages
				writeWithPause(1, console.screen_rows, "\1n\1h\1yThere are no selected messages.",
				               ERROR_PAUSE_WAIT_MS, "\1n", true);
				// Refresh the help line and move the cursor back to its original position
				DisplayHelpLine(this.msgListLightbarModeHelpLine);
				console.gotoxy(originalCurpos);
			}
		}
}
// For the DigDistMsgListerClass: Prints a line of information about
// a message.
//
// Parameters:
//  pMsgHeader: The message header object, returned by MsgBase.get_msg_header().
//  pHighlight: Optional boolean - Whether or not to highlight the line (true) or
//              use the standard colors (false).
//  pMsgNum: Optional - A number to use for the message instead of the number/offset
//           in the message header
function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum)
{
	// pMsgHeader must be a valid object.
	if (typeof(pMsgHeader) == "undefined")
		return;
	if (pMsgHeader == null)
		return;

	var highlight = false;
	// Get the message's import date & time as strings.  If
	// this.msgList_displayMessageDateImported is true, use the message import date.
	// Otherwise, use the message written date.
	var sDate;
	var sTime;
	if (this.msgList_displayMessageDateImported)
	{
		sDate = strftime("%Y-%m-%d", pMsgHeader.when_imported_time);
		sTime = strftime("%H:%M:%S", pMsgHeader.when_imported_time);
	}
	else
	{
		//sDate = strftime("%Y-%m-%d", pMsgHeader.when_written_time);
		//sTime = strftime("%H:%M:%S", pMsgHeader.when_written_time);
		var 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);
	// Determine if the message has been deleted.
	var msgDeleted = ((pMsgHeader.attr & MSG_DELETE) == MSG_DELETE);

	// msgIndicatorChar will contain (possibly) a character to display after
	// the message number to indicate whether it has been deleted, selected,
	// etc.  If not, then it will just be a space.
	var msgIndicatorChar = " ";

	// Write the message header information.
	// Note: The message header has the following fields:
	// 'number': The message number
	// 'offset': The message offset
	// 'to': Who the message is directed to (string)
	// 'from' Who wrote the message (string)
	// 'subject': The message subject (string)
	// 'date': The date - Full text (string)
	// To access one of these, use brackets; i.e., msgHeader['to']
	if (highlight)
	{
		if (msgDeleted)
			msgIndicatorChar = "\1n\1r\1h\1i" + this.colors.msgListHighlightBkgColor + "*\1n";
		else if (this.MessageIsSelected(this.subBoardCode, msgNum-1))
			msgIndicatorChar = "\1n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + CHECK_CHAR + "\1n";
		printf(this.sMsgInfoFormatHighlightStr,
		       msgNum,
		       msgIndicatorChar,
		       pMsgHeader.from.substr(0, this.FROM_LEN),
		       pMsgHeader.to.substr(0, this.TO_LEN),
		       pMsgHeader.subject.substr(0, this.SUBJ_LEN),
		       sDate, sTime);
		if (msgDeleted)
			msgIndicatorChar = "\1n\1r\1h\1i*\1n";
		else if (this.MessageIsSelected(this.subBoardCode, msgNum-1))
			msgIndicatorChar = "\1n" +  this.colors.selectedMsgMarkColor + CHECK_CHAR + "\1n";

		// Determine whether to use the normal, "to-user", or "from-user" format string.
		// The differences are the colors.  Then, output the message information line.
		var toNameUpper = pMsgHeader.to.toUpperCase();
		var msgToUser = ((toNameUpper == user.alias.toUpperCase()) || (toNameUpper == user.name.toUpperCase()) || (toNameUpper == user.handle.toUpperCase()));
		var fromNameUpper = pMsgHeader.from.toUpperCase();
		var msgIsFromUser = ((fromNameUpper == user.alias.toUpperCase()) || (fromNameUpper == user.name.toUpperCase()) || (fromNameUpper == user.handle.toUpperCase()));
		printf((msgToUser ? this.sMsgInfoToUserFormatStr : (msgIsFromUser ? this.sMsgInfoFromUserFormatStr : this.sMsgInfoFormatStr)),
		       msgNum,
		       msgIndicatorChar,
		       pMsgHeader.from.substr(0, this.FROM_LEN),
		       pMsgHeader.to.substr(0, this.TO_LEN),
		       pMsgHeader.subject.substr(0, this.SUBJ_LEN),
		       sDate, sTime);
	}
	console.cleartoeol("\1n"); // To clear away any extra text that may have been entered by the user
}
// For the traditional interface of DigDistMsgListerClass: Prompts the user to
// continue or read a message (by number).
//
// Parameters:
//  pStart: Whether or not we're on the first page (true or false)
//  pEnd: Whether or not we're at the last page (true or false)
//  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 = new Object();
	retObj.continueOn = true;
	retObj.userInput = "";
	retObj.selectedMsgOffset = -1;

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

	var continueOn = true;
	// Prompt the user whether or not to continue or to read a message
	// (by message number).  Make use of the different prompt texts,
	// depending whether we're at the beginning, in the middle, or at
	// the end of the message list.
	var userInput = "";
	var allowedKeys = "?GS"; // ? = help, G = Go to message #, S = Select message(s), Ctrl-D: Batch delete
	if (allowChgSubBoard)
		allowedKeys += "C"; // Change to another message area
	if (this.CanDelete() || this.CanDeleteLastMsg())
		allowedKeys += "D"; // Delete
	if (this.CanEdit())
		allowedKeys += "E"; // Edit
	if (pStart && pEnd)
	{
		// This is the only page.
		console.print(this.msgListOnlyOnePageContinuePrompt);
		// Get input from the user.  Allow only Q (quit).
		allowedKeys += "Q";
	}
	else if (pStart)
	{
		// We're on the first page.
		console.print(this.sStartContinuePrompt);
		// Get input from the user.  Allow only L (last), N (next), or Q (quit).
		allowedKeys += "LNQ";
	}
	else if (pEnd)
	{
		// We're on the last page.
		console.print(this.sEndContinuePrompt);
		// Get input from the user.  Allow only F (first), P (previous), or Q (quit).
		allowedKeys += "FPQ";
	}
	else
	{
		// We're neither on the first nor last page.  Allow F (first), L (last),
		// N (next), P (previous), or Q (quit).
		console.print(this.sContinuePrompt);
		allowedKeys += "FLNPQ";
	}
	// Get the user's input.  Allow CTRL-D (batch delete) without echoing it.
	// If the user didn't press CTRL-L, allow the keys in allowedKeys or a number from 1
	userInput = console.getkey(K_NOECHO);
	if (userInput != CTRL_D)
	{
		console.ungetstr(userInput);
		userInput = console.getkeys(allowedKeys, this.HighestMessageNum()).toString();
	}
	if (userInput == "Q")
		continueOn = false;

	// If the user has typed all numbers, then read that message.
	if ((userInput != "") && /^[0-9]+$/.test(userInput))
	{
		// If the user entered a valid message number, then let the user read the message.
		// The message number might be invalid if there are search results that
		// have non-continuous message numbers.
		if (this.IsValidMessageNum(userInput))
		{
			// 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("\1n\1h\1yThat's not a readable message.\1n");
				console.crlf();
				console.pause();
			}
		}
		else
		{
			// The user entered an invalid message number.
			console.print("\1n\1h\1w" + userInput + " \1y is not a valid message number.\1n");
			console.crlf();
			console.pause();
			continueOn = true;
		}
	}

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

	// Fill the return object with the required values, and return it.
	retObj.continueOn = continueOn;
	retObj.userInput = userInput;
	return retObj;
}
// For the DigDistMsgReader Class: Given a message number of a message in the
// current message area, 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)
{
	// Temporary
	//console.print("\1n\r\nRead - Offset: " + pOffset + "\r\n");
	//logStackTrace();
	//console.pause();
	// End Temporary
	var retObj = new Object();
	retObj.offsetValid = true;
	retObj.userReplied = false;
	retObj.lastKeypress = "";
	retObj.newMsgOffset = -1;
	retObj.nextAction = ACTION_NONE;
	retObj.refreshEnhancedRdrHelpLine = false;
	// 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);
	if (msgHeader == null)
	{
		console.print("\1n" + this.text.invalidMsgNumText.replace("%d", +(pOffset+1)) + "\1n");
		console.crlf();
		console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS);
		retObj.offsetValid = false;
		return retObj;
	}

	// 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.
	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.  If it has ANSI codes,
	// then don't use the scrolling interface so that the ANSI gets displayed properly.
	var messageText = this.msgbase.get_msg_body(false, msgHeader.number);
	// If the message has ANSI content, then use the scrolling interface only
	// if frame.js is available on the BBS machine and the option to use the
	// scrolling interface for ANSI messages is enabled.
	var msgHasANSICodes = textHasANSICodes(messageText);
	var useScrollingInterface = this.scrollingReaderInterface && console.term_supports(USER_ANSI);
	if (useScrollingInterface && msgHasANSICodes)
		useScrollingInterface = gFrameJSAvailable && this.useScrollingInterfaceForANSIMessages;
	// If we switch to the non-scrolling interface here, then the calling method should
	// refresh the enhanced reader help line on the screen.
	retObj.refreshEnhancedRdrHelpLine = (this.scrollingReaderInterface && !useScrollingInterface);
	// 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, msgHasANSICodes, pOffset);
		retObj = this.ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, msgHasANSICodes, pOffset);

	// Mark the message as read if it was written to the current user or if
	// the user is reading personal email.
	if (userNameHandleAliasMatch(msgHeader.to))
		msgHeader.attr |= MSG_READ;
		var successfullySavedMsgHdr = this.msgbase.put_msg_header(false, msgHeader.number, msgHeader);
		//var successfullySavedMsgHdr = this.msgbase.put_msg_header(msgHeader.number, msgHeader);
		if (this.SearchTypePopulatesSearchResults() && successfullySavedMsgHdr)
			this.RefreshSearchResultMsgHdr(pOffset, MSG_READ);

		// If failed to save the header, then write some error messages in the system & node log.
		if (!successfullySavedMsgHdr)
			writeToSysAndNodeLog("Failed to save message header with the READ attribute set!", LOG_ERR);
			writeToSysAndNodeLog(getMsgAreaDescStr(this.msgbase), LOG_ERR);
			//writeToSysAndNodeLog(format("Message offset: %d, number: %d", msgHeader.offset, msgHeader.number), LOG_ERR);
			writeToSysAndNodeLog(format("Message offset/index: %d, number: %d", this.GetMsgIdx(msgHeader.number), msgHeader.number), LOG_ERR);
			writeToSysAndNodeLog("Status: " + this.msgbase.status, LOG_ERR);
			writeToSysAndNodeLog("Error: " + this.msgbase.error, LOG_ERR);
			/*
			// For sysops, output a debug message
			if (gIsSysop)
				console.print("\1n");
				console.crlf();
				console.print("* Failed to save msg header the with 'read' attribute set!");
				console.crlf();
				console.print("Status: " + this.msgbase.status);
				console.crlf();
				console.print("Error: " + this.msgbase.error);
				console.crlf();
				console.crlf();
				//console.print("put_msg_header params: false, " + msgHeader.number + ", header:\r\n");
				//console.print("put_msg_header params: true, " + msgHeader.offset + ", header:\r\n");
				//console.print("put_msg_header params: " + msgHeader.number + ", header:\r\n");
				printMsgHdr(msgHeader);
	// If not reading personal email, then update the scan & last read message pointers.
	if (this.subBoardCode != "mail") // && !this.SearchTypePopulatesSearchResults()
	{
		if (msgHeader.number > msg_area.sub[this.subBoardCode].scan_ptr)
			msg_area.sub[this.subBoardCode].scan_ptr = msgHeader.number;
		msg_area.sub[this.subBoardCode].last_read = msgHeader.number;
	}

	return retObj;
}
// Helper method for ReadMessageEnhanced() - Does the scrollable reader interface
function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, msgHasANSICodes, pOffset)
{
	var retObj = new Object();
	retObj.offsetValid = true;
	retObj.userReplied = false;
	retObj.lastKeypress = "";
	retObj.newMsgOffset = -1;
	retObj.nextAction = ACTION_NONE;
	retObj.refreshEnhancedRdrHelpLine = false;
	
	// Show the message header
	this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);

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

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

	// Draw an initial scrollbar on the rightmost column of the message area
	// showing the fraction of the message shown and what part of the message
	// is currently being shown.  The scrollbar will be updated minimally in
	// the input loop to minimize screen redraws.
	this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);

	// Input loop (for scrolling the message up & down)
	var msgLineFormatStr = "%-" + this.msgAreaWidth + "s";
	var writeMessage = true;
	// msgAreaHeight, msgReaderObj, and scrollbarUpdateFunction are for use
	// with scrollTextLines().
	var msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1;
	var msgReaderObj = this;
	function msgScrollbarUpdateFn(pFractionToLastPage)
	{
		// Update the scrollbar position for the message, depending on the
		// value of pFractionToLastMessage.
		fractionToLastPage = pFractionToLastPage;
		solidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidScrollBlocks * pFractionToLastPage);
		if (solidBlockStartRow != solidBlockLastStartRow)
			msgReaderObj.UpdateEnhancedReaderScollbar(solidBlockStartRow, solidBlockLastStartRow, numSolidScrollBlocks);
		solidBlockLastStartRow = solidBlockStartRow;
		console.gotoxy(1, console.screen_rows);
	}
	// 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 scrollRetObj = null;
		if (msgInfo.displayFrame != null)
		{
			msgInfo.displayFrame.draw();
			scrollRetObj = scrollFrame(msgInfo.displayFrame, msgInfo.displayFrameScrollbar,
									   topMsgLineIdx, this.colors["msgBodyColor"],
									   writeMessage, 1, console.screen_rows,
									   msgScrollbarUpdateFn);
		}
		else
		{
			scrollRetObj = scrollTextLines(msgInfo.messageLines, topMsgLineIdx,
										   this.colors["msgBodyColor"], writeMessage,
										   this.msgAreaLeft, this.msgAreaTop, this.msgAreaWidth,
										   msgAreaHeight, 1, console.screen_rows,
										   msgScrollbarUpdateFn);
		}
		topMsgLineIdx = scrollRetObj.topLineIdx;
		retObj.lastKeypress = scrollRetObj.lastKeypress;
		switch (retObj.lastKeypress)
		{
			case 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, msgInfo.attachments);
				if (msgWasDeleted)
				{
					var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
					continueOn = msgSearchObj.continueInputLoop;
					retObj.newMsgOffset = msgSearchObj.newMsgOffset;
					retObj.nextAction = msgSearchObj.nextAction;
					if (msgSearchObj.promptGoToNextArea)
						if (this.EnhReaderPromptYesNo(this.text.goToNextMsgAreaPromptText, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks))
							// Let this method exit and let the caller go to the next sub-board
							continueOn = false;
							retObj.nextAction = ACTION_GO_NEXT_MSG;
						else
							writeMessage = false; // No need to refresh the message
				}
				else
				{
					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
					// Move the cursor back to its original position
					console.gotoxy(originalCurpos);
					writeMessage = false;
				}
				break;
			case 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 = "\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);
						// 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)
								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 this.enhReaderKeys.showHelp: // Show the help screen
				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgInfo.attachments.length > 0);
				// If the enhanced message header width is less than the console
				// width, then clear the screen to remove anything left on the
				// screen from the help screen.
				if (this.enhMsgHeaderWidth < console.screen_columns)
					console.clear("\1n");
				// Display the message header and key help line
				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
				// Display the scrollbar again, and ensure it's in the correct position
				solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
				this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
				writeMessage = true; // We want to refresh the message on the screen
				break;
			case 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 extdMsgHdr = this.msgbase.get_msg_header(false, msgHeader.number, true);
					// Let the user reply to the message.
					var replyRetObj = this.ReplyToMsg(extdMsgHdr, msgInfo.msgText, privateReply, pOffset);
													  retObj.userReplied = replyRetObj.postSucceeded;
					//retObj.msgNotReadable = 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
								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
							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);
			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, this.text.readMsgNumPromptText, false, ERROR_PAUSE_WAIT_MS, false);
				// Only allow reading the message if the message number is valid