Skip to content
Snippets Groups Projects
DDMsgReader.js 750 KiB
Newer Older
// DigDistMessageReader class.
function getDefaultColors()
{
	var colorArray = new Array();

	// Header line: "Current msg group:"
	colorArray["msgListHeaderMsgGroupTextColor"] = "\1n\1" + "4\1c"; // Normal cyan on blue background
	//colorArray["msgListHeaderMsgGroupTextColor"] = "\1n\1" + "4\1w"; // Normal white on blue background

	// Header line: Message group name
	colorArray["msgListHeaderMsgGroupNameColor"] = "\1h\1c"; // High cyan
	//colorArray["msgListHeaderMsgGroupNameColor"] = "\1h\1w"; // High white

	// Header line: "Current sub-board:"
	colorArray["msgListHeaderSubBoardTextColor"] = "\1n\1" + "4\1c"; // Normal cyan on blue background
	//colorArray["msgListHeaderSubBoardTextColor"] = "\1n\1" + "4\1w"; // Normal white on blue background

	// Header line: Message sub-board name
	colorArray["msgListHeaderMsgSubBoardName"] = "\1h\1c"; // High cyan
	//colorArray["msgListHeaderMsgSubBoardName"] = "\1h\1w"; // High white
	// Line with column headers
	//colorArray["msgListColHeader"] = "\1h\1w"; // High white (keep blue background)
	colorArray["msgListColHeader"] = "\1n\1h\1w"; // High white on black background
	//colorArray["msgListColHeader"] = "\1h\1c"; // High cyan (keep blue background)
	//colorArray["msgListColHeader"] = "\1" + "4\1h\1y"; // High yellow (keep blue background)

	// Message list information
	colorArray["msgListMsgNumColor"] = "\1n\1h\1y";
	colorArray["msgListFromColor"] = "\1n\1c";
	colorArray["msgListToColor"] = "\1n\1c";
	colorArray["msgListSubjectColor"] = "\1n\1c";
	colorArray["msgListDateColor"] = "\1h\1b";
	colorArray["msgListTimeColor"] = "\1h\1b";
	// Message information for messages written to the user
	colorArray["msgListToUserMsgNumColor"] = "\1n\1h\1y";
	colorArray["msgListToUserFromColor"] = "\1h\1g";
	colorArray["msgListToUserToColor"] = "\1h\1g";
	colorArray["msgListToUserSubjectColor"] = "\1h\1g";
	colorArray["msgListToUserDateColor"] = "\1h\1b";
	colorArray["msgListToUserTimeColor"] = "\1h\1b";
	// Message information for messages from the user
	colorArray["msgListFromUserMsgNumColor"] = "\1n\1h\1y";
	colorArray["msgListFromUserFromColor"] = "\1n\1c";
	colorArray["msgListFromUserToColor"] = "\1n\1c";
	colorArray["msgListFromUserSubjectColor"] = "\1n\1c";
	colorArray["msgListFromUserDateColor"] = "\1h\1b";
	colorArray["msgListFromUserTimeColor"] = "\1h\1b";

	// Message list highlight colors
	colorArray["msgListHighlightBkgColor"] = "\1" + "4"; // Background
	colorArray["msgListMsgNumHighlightColor"] = "\1h\1y";
	colorArray["msgListFromHighlightColor"] = "\1h\1c";
	colorArray["msgListToHighlightColor"] = "\1h\1c";
	colorArray["msgListSubjHighlightColor"] = "\1h\1c";
	colorArray["msgListDateHighlightColor"] = "\1h\1w";
	colorArray["msgListTimeHighlightColor"] = "\1h\1w";

	// Lightbar message list help line colors
	colorArray["lightbarMsgListHelpLineBkgColor"] = "\1" + "7"; // Background
	colorArray["lightbarMsgListHelpLineGeneralColor"] = "\1b";
	colorArray["lightbarMsgListHelpLineHotkeyColor"] = "\1r";
	colorArray["lightbarMsgListHelpLineParenColor"] = "\1m";

	// Continue prompt colors
	colorArray["tradInterfaceContPromptMainColor"] = "\1n\1g"; // Main text color
	colorArray["tradInterfaceContPromptHotkeyColor"] = "\1h\1c"; // Hotkey color
	colorArray["tradInterfaceContPromptUserInputColor"] = "\1h\1g"; // User input color

	// Message body color
	colorArray["msgBodyColor"] = "\1n\1w";

	// Read message confirmation colors
	colorArray["readMsgConfirmColor"] = "\1n\1c";
	colorArray["readMsgConfirmNumberColor"] = "\1h\1c";
	// Prompt for continuing to list messages after reading a message
	colorArray["afterReadMsg_ListMorePromptColor"] = "\1n\1c";

	// Help screen text color
	colorArray["tradInterfaceHelpScreenColor"] = "\1n\1h\1w";

	// Colors for choosing a message group & sub-board
	colorArray["areaChooserMsgAreaNumColor"] = "\1n\1w\1h";
	colorArray["areaChooserMsgAreaDescColor"] = "\1n\1c";
	colorArray["areaChooserMsgAreaNumItemsColor"] = "\1b\1h";
	colorArray["areaChooserMsgAreaHeaderColor"] = "\1n\1y\1h";
	colorArray["areaChooserSubBoardHeaderColor"] = "\1n\1g";
	colorArray["areaChooserMsgAreaMarkColor"] = "\1g\1h";
	colorArray["areaChooserMsgAreaLatestDateColor"] = "\1n\1g";
	colorArray["areaChooserMsgAreaLatestTimeColor"] = "\1n\1m";
	// Highlighted colors (for lightbar mode)
	colorArray["areaChooserMsgAreaBkgHighlightColor"] = "\1" + "4"; // Blue background
	colorArray["areaChooserMsgAreaNumHighlightColor"] = "\1w\1h";
	colorArray["areaChooserMsgAreaDescHighlightColor"] = "\1c";
	colorArray["areaChooserMsgAreaDateHighlightColor"] = "\1w\1h";
	colorArray["areaChooserMsgAreaTimeHighlightColor"] = "\1w\1h";
	colorArray["areaChooserMsgAreaNumItemsHighlightColor"] = "\1w\1h";
	// Lightbar area chooser help line
	colorArray["lightbarAreaChooserHelpLineBkgColor"] = "\1" + "7"; // Background
	colorArray["lightbarAreaChooserHelpLineGeneralColor"] = "\1b";
	colorArray["lightbarAreaChooserHelpLineHotkeyColor"] = "\1r";
	colorArray["lightbarAreaChooserHelpLineParenColor"] = "\1m";

	// Scrollbar background and scroll block colors (for the enhanced
	// message reader interface)
	colorArray["scrollbarBGColor"] = "\1n\1h\1k";
	colorArray["scrollbarScrollBlockColor"] = "\1n\1h\1w";
	// Color for the line drawn in the 2nd to last line of the message
	// area in the enhanced reader mode before a prompt
	colorArray["enhReaderPromptSepLineColor"] = "\1n\1h\1g";
	// Colors for the enhanced reader help line
	colorArray["enhReaderHelpLineBkgColor"] = "\1" + "7";
	colorArray["enhReaderHelpLineGeneralColor"] = "\1b";
	colorArray["enhReaderHelpLineHotkeyColor"] = "\1r";
	colorArray["enhReaderHelpLineParenColor"] = "\1m";

	// Message header line colors
	colorArray["hdrLineLabelColor"] = "\1n\1c";
	colorArray["hdrLineValueColor"] = "\1n\1b\1h";
	// Selected message marker color
	colorArray["selectedMsgMarkColor"] = "\1n\1w\1h";

	return colorArray;
}

// This function returns the month number (1-based) from a capitalized
// month name.
//
// Parameters:
//  pMonthName: The name of the month
//
// Return value: The number of the month (1-12).
function getMonthNum(pMonthName)
{
	var monthNum = 1;

	if (pMonthName.substr(0, 3) == "Jan")
		monthNum = 1;
	else if (pMonthName.substr(0, 3) == "Feb")
		monthNum = 2;
	else if (pMonthName.substr(0, 3) == "Mar")
		monthNum = 3;
	else if (pMonthName.substr(0, 3) == "Apr")
		monthNum = 4;
	else if (pMonthName.substr(0, 3) == "May")
		monthNum = 5;
	else if (pMonthName.substr(0, 3) == "Jun")
		monthNum = 6;
	else if (pMonthName.substr(0, 3) == "Jul")
		monthNum = 7;
	else if (pMonthName.substr(0, 3) == "Aug")
		monthNum = 8;
	else if (pMonthName.substr(0, 3) == "Sep")
		monthNum = 9;
	else if (pMonthName.substr(0, 3) == "Oct")
		monthNum = 10;
	else if (pMonthName.substr(0, 3) == "Nov")
		monthNum = 11;
	else if (pMonthName.substr(0, 3) == "Dec")
		monthNum = 12;

	return monthNum;
}

// Clears each line from a given line to the end of the screen.
//
// Parameters:
//  pStartLineNum: The line number to start at (1-based)
function clearToEOS(pStartLineNum)
{
	if (typeof(pStartLineNum) == "undefined")
		return;
	if (pStartLineNum == null)
		return;

	for (var lineNum = pStartLineNum; lineNum <= console.screen_rows; ++lineNum)
	{
		console.gotoxy(1, lineNum);
		console.clearline();
	}
}

// Returns the number of messages in a sub-board.
//
// Parameters:
//  pSubBoardCode: The sub-board code (i.e., from bbs.cursub_code)
//
// Return value: The number of messages in the sub-board, or 0
//               if the sub-board could not be opened.
function numMessages(pSubBoardCode)
{
   var messageCount = 0;

   var myMsgbase = new MsgBase(pSubBoardCode);
	if (myMsgbase.open())
		messageCount = myMsgbase.total_msgs;
	myMsgbase.close();
	myMsgbase = null;

	return messageCount;
}

// Removes multiple, leading, and/or trailing spaces
// The search & replace regular expressions used in this
// function came from the following URL:
//  http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces
//
// Parameters:
//  pString: The string to trim
//  pLeading: Whether or not to trim leading spaces (optional, defaults to true)
//  pMultiple: Whether or not to trim multiple spaces (optional, defaults to true)
//  pTrailing: Whether or not to trim trailing spaces (optional, defaults to true)
//
// Return value: The string with whitespace trimmed
function trimSpaces(pString, pLeading, pMultiple, pTrailing)
{
	var leading = true;
	var multiple = true;
	var trailing = true;
	if(typeof(pLeading) != "undefined")
		leading = pLeading;
	if(typeof(pMultiple) != "undefined")
		multiple = pMultiple;
	if(typeof(pTrailing) != "undefined")
		trailing = pTrailing;

	// To remove both leading & trailing spaces:
	//pString = pString.replace(/(^\s*)|(\s*$)/gi,"");

	if (leading)
		pString = pString.replace(/(^\s*)/gi,"");
	if (multiple)
		pString = pString.replace(/[ ]{2,}/gi," ");
	if (trailing)
		pString = pString.replace(/(\s*$)/gi,"");

	return pString;
}

// Returns whether an internal sub-board code is valid.
//
// Parameters:
//  pSubBoardCode: The internal sub-board code to test
//
// Return value: Boolean - Whether or not the given internal code is a valid
//               sub-board code
function subBoardCodeIsValid(pSubBoardCode)
{
   return ((pSubBoardCode == "mail") || (typeof(msg_area.sub[pSubBoardCode]) == "object"))
}

// Displays some text with a solid horizontal line on the next line.
//
// Parameters:
//  pText: The text to display
//  pCenter: Whether or not to center the text.  Optional; defaults
//           to false.
//  pTextColor: The color to use for the text.  Optional; by default,
//              normal white will be used.
//  pLineColor: The color to use for the line underneath the text.
//              Optional; by default, bright black will be used.
function displayTextWithLineBelow(pText, pCenter, pTextColor, pLineColor)
{
	var centerText = (typeof(pCenter) == "boolean" ? pCenter : false);
	var textColor = (typeof(pTextColor) == "string" ? pTextColor : "\1n\1w");
	var lineColor = (typeof(pLineColor) == "string" ? pLineColor : "\1n\1k\1h");
	// Output the text and a solid line on the next line.
	if (centerText)
	{
		console.center(textColor + pText);
		var solidLine = "";
		var textLength = console.strlen(pText);
		for (var i = 0; i < textLength; ++i)
			solidLine += "Ä";
		console.center(lineColor + solidLine);
	}
	else
	{
		console.print(textColor + pText);
		console.crlf();
		console.print(lineColor);
		var textLength = console.strlen(pText);
		for (var i = 0; i < textLength; ++i)
			console.print("Ä");
		console.crlf();
	}
14288 14289 14290 14291 14292 14293 14294 14295 14296 14297 14298 14299 14300 14301 14302 14303 14304 14305 14306 14307 14308 14309 14310 14311 14312 14313 14314 14315 14316 14317 14318 14319 14320 14321 14322 14323 14324 14325 14326 14327 14328 14329 14330 14331 14332 14333 14334 14335 14336 14337 14338 14339 14340 14341 14342 14343 14344 14345 14346 14347 14348 14349 14350 14351 14352 14353 14354 14355 14356 14357 14358 14359 14360 14361 14362 14363 14364 14365 14366 14367 14368 14369 14370 14371 14372 14373 14374 14375 14376 14377 14378 14379 14380 14381 14382 14383 14384 14385 14386 14387 14388 14389 14390 14391 14392 14393 14394 14395 14396 14397 14398 14399 14400 14401 14402 14403 14404 14405 14406 14407 14408 14409 14410 14411 14412 14413 14414 14415 14416 14417 14418 14419 14420 14421 14422 14423 14424 14425 14426 14427 14428 14429 14430 14431 14432 14433 14434 14435 14436 14437 14438 14439 14440 14441 14442 14443 14444 14445 14446 14447 14448 14449 14450 14451 14452 14453 14454 14455 14456 14457 14458 14459 14460 14461 14462 14463 14464 14465 14466 14467 14468 14469 14470 14471 14472 14473 14474 14475 14476 14477 14478 14479 14480 14481 14482 14483 14484 14485 14486 14487 14488 14489 14490 14491 14492 14493 14494 14495 14496 14497 14498 14499 14500 14501 14502 14503 14504 14505 14506 14507 14508 14509 14510 14511 14512 14513 14514 14515 14516 14517 14518 14519 14520 14521 14522 14523 14524 14525 14526 14527 14528 14529 14530 14531 14532 14533 14534 14535 14536 14537 14538 14539 14540 14541 14542 14543 14544 14545 14546 14547 14548 14549 14550 14551 14552 14553 14554 14555 14556 14557 14558 14559 14560 14561 14562 14563 14564 14565 14566 14567 14568 14569 14570 14571 14572 14573 14574 14575 14576 14577 14578 14579 14580 14581 14582 14583 14584 14585 14586 14587 14588 14589 14590 14591 14592 14593 14594 14595 14596 14597 14598 14599 14600 14601 14602 14603 14604 14605 14606 14607 14608 14609 14610 14611 14612 14613 14614 14615 14616 14617 14618 14619 14620 14621 14622 14623 14624 14625 14626 14627 14628 14629 14630 14631 14632 14633 14634 14635 14636 14637 14638 14639 14640 14641 14642 14643 14644 14645 14646 14647 14648 14649 14650 14651 14652 14653 14654 14655 14656 14657 14658 14659 14660 14661 14662 14663 14664 14665 14666 14667 14668 14669 14670 14671 14672 14673 14674 14675 14676 14677 14678
}

// Returns whether the Synchronet compile date is at least May 12, 2013.  That
// was when Digital Man's change to make bbs.msg_number work when a script is
// running first went into the Synchronet daily builds.
function compileDateAtLeast2013_05_12()
{
  // system.compiled_when is in the following format:
  // May 12 2013 05:02

  var compileDateParts = system.compiled_when.split(" ");
  if (compileDateParts.length < 4)
    return false;

  // Convert the month to a 1-based number
  var compileMonth = 0;
  if (/^Jan/.test(compileDateParts[0]))
    compileMonth = 1;
  else if (/^Feb/.test(compileDateParts[0]))
    compileMonth = 2;
  else if (/^Mar/.test(compileDateParts[0]))
    compileMonth = 3;
  else if (/^Apr/.test(compileDateParts[0]))
    compileMonth = 4;
  else if (/^May/.test(compileDateParts[0]))
    compileMonth = 5;
  else if (/^Jun/.test(compileDateParts[0]))
    compileMonth = 6;
  else if (/^Jul/.test(compileDateParts[0]))
    compileMonth = 7;
  else if (/^Aug/.test(compileDateParts[0]))
    compileMonth = 8;
  else if (/^Sep/.test(compileDateParts[0]))
    compileMonth = 9;
  else if (/^Oct/.test(compileDateParts[0]))
    compileMonth = 10;
  else if (/^Nov/.test(compileDateParts[0]))
    compileMonth = 11;
  else if (/^Dec/.test(compileDateParts[0]))
    compileMonth = 12;

  // Get the compileDay and compileYear as numeric variables
  var compileDay = +compileDateParts[1];
  var compileYear = +compileDateParts[2];

  // Determine if the compile date is at least 2013-05-12
  var compileDateIsAtLeastMin = true;
  if (compileYear > 2013)
    compileDateIsAtLeastMin = true;
  else if (compileYear < 2013)
    compileDateIsAtLeastMin = false;
  else // compileYear is 2013
  {
    if (compileMonth > 5)
      compileDateIsAtLeastMin = true
    else if (compileMonth < 5)
      compileDateIsAtLeastMin = false;
    else // compileMonth is 5
      compileDateIsAtLeastMin = (compileDay >= 12);
  }

  return compileDateIsAtLeastMin;
}

// Removes multiple, leading, and/or trailing spaces.
// The search & replace regular expressions used in this
// function came from the following URL:
//  http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces
//
// Parameters:
//  pString: The string to trim
//  pLeading: Whether or not to trim leading spaces (optional, defaults to true)
//  pMultiple: Whether or not to trim multiple spaces (optional, defaults to true)
//  pTrailing: Whether or not to trim trailing spaces (optional, defaults to true)
function trimSpaces(pString, pLeading, pMultiple, pTrailing)
{
	var leading = true;
	var multiple = true;
	var trailing = true;
	if(typeof(pLeading) != "undefined")
		leading = pLeading;
	if(typeof(pMultiple) != "undefined")
		multiple = pMultiple;
	if(typeof(pTrailing) != "undefined")
		trailing = pTrailing;
		
	// To remove both leading & trailing spaces:
	//pString = pString.replace(/(^\s*)|(\s*$)/gi,"");

	if (leading)
		pString = pString.replace(/(^\s*)/gi,"");
	if (multiple)
		pString = pString.replace(/[ ]{2,}/gi," ");
	if (trailing)
		pString = pString.replace(/(\s*$)/gi,"");

	return pString;
}

// Calculates & returns a page number.
//
// Parameters:
//  pTopIndex: The index (0-based) of the topmost item on the page
//  pNumPerPage: The number of items per page
//
// Return value: The page number
function calcPageNum(pTopIndex, pNumPerPage)
{
  return ((pTopIndex / pNumPerPage) + 1);
}

// Returns the greatest number of messages of all sub-boards within
// a message group.
//
// Parameters:
//  pGrpIndex: The index of the message group
//
// Returns: The greatest number of messages of all sub-boards within
//          the message group
function getGreatestNumMsgs(pGrpIndex)
{
  // Sanity checking
  if (typeof(pGrpIndex) != "number")
    return 0;
  if (typeof(msg_area.grp_list[pGrpIndex]) == "undefined")
    return 0;

  var greatestNumMsgs = 0;
  var msgBase = null;
  for (var subIndex = 0; subIndex < msg_area.grp_list[pGrpIndex].sub_list.length; ++subIndex)
  {
    msgBase = new MsgBase(msg_area.grp_list[pGrpIndex].sub_list[subIndex].code);
    if (msgBase == null) continue;
    if (msgBase.open())
    {
      if (msgBase.total_msgs > greatestNumMsgs)
        greatestNumMsgs = msgBase.total_msgs;
      msgBase.close();
    }
  }
  return greatestNumMsgs;
}

// Inputs a keypress from the user and handles some ESC-based
// characters such as PageUp, PageDown, and ESC.  If PageUp
// or PageDown are pressed, this function will return the
// string "\1PgUp" (KEY_PAGE_UP) or "\1Pgdn" (KEY_PAGE_DOWN),
// respectively.  Also, F1-F5 will be returned as "\1F1"
// through "\1F5", respectively.
// Thanks goes to Psi-Jack for the original impementation
// of this function.
//
// Parameters:
//  pGetKeyMode: Optional - The mode bits for console.getkey().
//               If not specified, K_NONE will be used.
//
// Return value: The user's keypress
function getKeyWithESCChars(pGetKeyMode)
{
   var getKeyMode = K_NONE;
   if (typeof(pGetKeyMode) == "number")
      getKeyMode = pGetKeyMode;

   var userInput = console.getkey(getKeyMode);
   if (userInput == KEY_ESC) {
      switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
         case '[':
            switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
               case 'V':
                  userInput = KEY_PAGE_UP;
                  break;
               case 'U':
                  userInput = KEY_PAGE_DOWN;
                  break;
           }
           break;
         case 'O':
           switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
              case 'P':
                 userInput = "\1F1";
                 break;
              case 'Q':
                 userInput = "\1F2";
                 break;
              case 'R':
                 userInput = "\1F3";
                 break;
              case 'S':
                 userInput = "\1F4";
                 break;
              case 't':
                 userInput = "\1F5";
                 break;
           }
         default:
           break;
      }
   }

   return userInput;
}

// Finds the next or previous non-empty message sub-board.  Returns an
// object containing the message group & sub-board indexes.  If all of
// the next/previous sub-boards are empty, then the given current indexes
// will be returned.
//
// Parameters:
//  pStartGrpIdx: The index of the message group to start from
//  pStartSubIdx: The index of the sub-board in the message group to start from
//  pForward: Boolean - Whether or not to search forward (true) or backward (false).
//            Optional; defaults to true, to search forward.
//
// Return value: An object with the following properties:
//               foundSubBoard: Boolean - Whether or not a different sub-board was found
//               grpIdx: The message group index of the found sub-board
//               subIdx: The sub-board index in the group of the found sub-board
//               subCode: The internal code of the sub-board
//               subChanged: Boolean - Whether or not the found sub-board is
//                           different from the one that was passed in
//               paramsValid: Boolean - Whether or not all the passed-in parameters
//                            were valid.
function findNextOrPrevNonEmptySubBoard(pStartGrpIdx, pStartSubIdx, pForward)
{
   var retObj = new Object();
   retObj.grpIdx = pStartGrpIdx;
   retObj.subIdx = pStartSubIdx;
   retObj.subCode = msg_area.grp_list[pStartGrpIdx].sub_list[pStartSubIdx].code;
   retObj.foundSubBoard = false;

   // Sanity checking
   retObj.paramsValid = ((pStartGrpIdx >= 0) && (pStartGrpIdx < msg_area.grp_list.length) &&
                         (pStartSubIdx >= 0) &&
                         (pStartSubIdx < msg_area.grp_list[pStartGrpIdx].sub_list.length));
   if (!retObj.paramsValid)
      return retObj;

   var grpIdx = pStartGrpIdx;
   var subIdx = pStartSubIdx;
   var searchForward = (typeof(pForward) == "boolean" ? pForward : true);
   if (searchForward)
   {
      // Advance the sub-board (and group) index, and determine whether or not
      // to do the search (i.e., we might not want to if the starting sub-board
      // is the last sub-board in the last group).
      var searchForSubBoard = true;
      if (subIdx <  msg_area.grp_list[grpIdx].sub_list.length - 1)
         ++subIdx;
      else
      {
         if ((grpIdx < msg_area.grp_list.length - 1) && (msg_area.grp_list[grpIdx+1].sub_list.length > 0))
         {
            subIdx = 0;
            ++grpIdx;
         }
         else
            searchForSubBoard = false;
      }
      // If we can search, then do it.
      if (searchForSubBoard)
      {
         while (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) == 0)
         {
            if (subIdx < msg_area.grp_list[grpIdx].sub_list.length - 1)
               ++subIdx;
            else
            {
               if ((grpIdx < msg_area.grp_list.length - 1) && (msg_area.grp_list[grpIdx+1].sub_list.length > 0))
               {
                  subIdx = 0;
                  ++grpIdx;
               }
               else
                  break; // Stop searching
            }
         }
      }
   }
   else
   {
      // Search the sub-boards in reverse
      // Decrement the sub-board (and group) index, and determine whether or not
      // to do the search (i.e., we might not want to if the starting sub-board
      // is the first sub-board in the first group).
      var searchForSubBoard = true;
      if (subIdx > 0)
         --subIdx;
      else
      {
         if ((grpIdx > 0) && (msg_area.grp_list[grpIdx-1].sub_list.length > 0))
         {
            --grpIdx;
            subIdx = msg_area.grp_list[grpIdx].sub_list.length - 1;
         }
         else
            searchForSubBoard = false;
      }
      // If we can search, then do it.
      if (searchForSubBoard)
      {
         while (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) == 0)
         {
            if (subIdx > 0)
               --subIdx;
            else
            {
               if ((grpIdx > 0) && (msg_area.grp_list[grpIdx-1].sub_list.length > 0))
               {
                  --grpIdx;
                  subIdx = msg_area.grp_list[grpIdx].sub_list.length - 1;
               }
               else
                  break; // Stop searching
            }
         }
      }
   }
   // If we found a sub-board with messages in it, then set the variables
   // in the return object
   if (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) > 0)
   {
      retObj.grpIdx = grpIdx;
      retObj.subIdx = subIdx;
      retObj.subCode = msg_area.grp_list[grpIdx].sub_list[subIdx].code;
      retObj.foundSubBoard = true;
      retObj.subChanged = ((grpIdx != pStartGrpIdx) || (subIdx != pStartSubIdx));
   }

   return retObj;
}

// Returns the number of messages in a sub-board.
//
// Parameters:
//  pSubBoardCode: The internal code of the sub-board to check
//  pIncludeDeleted: Optional boolean - Whether or not to include deleted
//                   messages in the count.  Defaults to false.
//
// Return value: The number of messages in the sub-board
function numMsgsInSubBoard(pSubBoardCode, pIncludeDeleted)
{
   var numMessages = 0;
   var msgbase = new MsgBase(pSubBoardCode);
   if (msgbase.open())
   {
      var includeDeleted = (typeof(pIncludeDeleted) == "boolean" ? pIncludeDeleted : false);
      if (includeDeleted)
         numMessages = msgbase.total_msgs;
      else
      {
         // Don't include deleted messages.  Go through each message
         // in the sub-board and count the ones that aren't marked
         // as deleted.
         for (var msgIdx = 0; msgIdx < msgbase.total_msgs; ++msgIdx)
         {
            var msgHdr = msgbase.get_msg_header(true, msgIdx, false);
            if ((msgHdr != null) && ((msgHdr.attr & MSG_DELETE) == 0))
               ++numMessages;
         }
      }
      msgbase.close();
   }
   return numMessages;
}

// Replaces @-codes in a string and returns the new string.
//
// Parameters:
//  pStr: A string in which to replace @-codes
//
// Return value: A version of the string with @-codes interpreted
function replaceAtCodesInStr(pStr)
{
	if (typeof(pStr) != "string")
		return "";

	// This code was originally written by Deuce.  I updated it to check whether
	// the string returned by bbs.atcode() is null, and if so, just return
	// the original string.
	return pStr.replace(/@([^@]+)@/g, function(m, code) {
		var decoded = bbs.atcode(code);
		return (decoded != null ? decoded : "@" + code + "@");
	});
}

// Shortens a string, accounting for control/attribute codes.  Returns a new
// (shortened) copy of the string.
//
// Parameters:
//  pStr: The string to shorten
//  pNewLength: The new (shorter) length of the string
//  pFromLeft: Optional boolean - Whether to start from the left (default) or
//             from the right.  Defaults to true.
//
// Return value: The shortened version of the string
function shortenStrWithAttrCodes(pStr, pNewLength, pFromLeft)
	if (typeof(pStr) != "string")
		return "";
	if (typeof(pNewLength) != "number")
		return pStr;
	if (pNewLength >= console.strlen(pStr))
		return pStr;

	var fromLeft = (typeof(pFromLeft) == "boolean" ? pFromLeft : true);
	var strCopy = "";
	var tmpStr = "";
	var strIdx = 0;
	var lengthGood = true;
	if (fromLeft)
	{
		while (lengthGood && (strIdx < pStr.length))
		{
			tmpStr = strCopy + pStr.charAt(strIdx++);
			if (console.strlen(tmpStr) <= pNewLength)
				strCopy = tmpStr;
			else
				lengthGood = false;
		}
	}
	else
	{
		strIdx = pStr.length - 1;
		while (lengthGood && (strIdx >= 0))
		{
			tmpStr = pStr.charAt(strIdx--) + strCopy;
			if (console.strlen(tmpStr) <= pNewLength)
				strCopy = tmpStr;
			else
				lengthGood = false;
		}
	}
	return strCopy;
}

// Returns whether a given name matches the logged-in user's handle, alias, or
// name.
//
// Parameters:
//  pName: A name to match against the logged-in user
//
// Return value: Boolean - Whether or not the given name matches the logged-in
//               user's handle, alias, or name
function userHandleAliasNameMatch(pName)
{
   if (typeof(pName) != "string")
      return false;

   var userMatch = false;
   var nameUpper = pName.toUpperCase();
   if (user.handle.length > 0)
      userMatch = (nameUpper.indexOf(user.handle.toUpperCase()) > -1);
   if (!userMatch && (user.alias.length > 0))
      userMatch = (nameUpper.indexOf(user.alias.toUpperCase()) > -1);
   if (!userMatch && (user.name.length > 0))
      userMatch = (nameUpper.indexOf(user.name.toUpperCase()) > -1);
   return userMatch;
}

// Displays a range of text lines on the screen and allows scrolling through them
// with the up & down arrow keys, PageUp, PageDown, HOME, and END.  It is assumed
// that the array of text lines are already truncated to fit in the width of the
// text area, as a speed optimization.
//
// Parameters:
//  pTxtLines: The array of text lines to allow scrolling for
//  pTopLineIdx: The index of the text line to display at the top
//  pTxtAttrib: The attribute(s) to apply to the text lines
//  pWriteTxtLines: Boolean - Whether or not to write the text lines (in addition
//                  to doing the message loop).  If false, this will only do the
//                  the message loop.  This parameter is intended as a screen
//                  refresh optimization.
//  pTopLeftX: The upper-left corner column for the text area
//  pTopLeftY: The upper-left corner row for the text area
//  pWidth: The width of the text area
//  pHeight: The height of the text area
//  pPostWriteCurX: The X location for the cursor after writing the message
//                  lines
//  pPostWriteCurY: The Y location for the cursor after writing the message
//                  lines
//  pScrollUpdateFn: A function that the caller can provide for updating the
//                   scroll position.  This function has one parameter:
//                   - fractionToLastPage: The fraction of the top index divided
//                     by the top index for the last page (basically, the progress
//                     to the last page).
//
// Return value: An object with the following properties:
//               lastKeypress: The last key pressed by the user (a string)
//               topLineIdx: The new top line index of the text lines, in case of scrolling
function scrollTextLines(pTxtLines, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pTopLeftX, pTopLeftY,
                         pWidth, pHeight, pPostWriteCurX, pPostWriteCurY, pScrollUpdateFn)
	// Variables for the top line index for the last page, scrolling, etc.
	var topLineIdxForLastPage = pTxtLines.length - pHeight;
	if (topLineIdxForLastPage < 0)
		topLineIdxForLastPage = 0;
	var msgFractionShown = pHeight / pTxtLines.length;
	if (msgFractionShown > 1)
		msgFractionShown = 1.0;
	var fractionToLastPage = 0;
	var lastTxtRow = pTopLeftY + pHeight - 1;
	var txtLineFormatStr = "%-" + pWidth + "s";
	var retObj = new Object();
	retObj.lastKeypress = "";
	retObj.topLineIdx = pTopLineIdx;
	var writeTxtLines = pWriteTxtLines;
	var continueOn = true;
	while (continueOn)
	{
		// If we are to write the text lines, then write each of them and also
		// clear out the rest of the row on the screen
		if (writeTxtLines)
		{
			// If the scroll update function parameter is a function, then calculate
			// the fraction to the last page and call the scroll update function.
			if (typeof(pScrollUpdateFn) == "function")
			{
				if (topLineIdxForLastPage != 0)
					fractionToLastPage = retObj.topLineIdx / topLineIdxForLastPage;
				pScrollUpdateFn(fractionToLastPage);
			}
			var screenY = pTopLeftY;
			for (var lineIdx = retObj.topLineIdx; (lineIdx < pTxtLines.length) && (screenY <= lastTxtRow); ++lineIdx)
			{
				console.gotoxy(pTopLeftX, screenY++);
				// Print the text line, then clear the rest of the line
				console.print(pTxtAttrib + pTxtLines[lineIdx]);
				printf("\1n%" + +(pWidth - console.strlen(pTxtLines[lineIdx])) + "s", "");
			}
			// If there are still some lines left in the message reading area, then
			// clear the lines.
			console.print("\1n" + pTxtAttrib);
			while (screenY <= lastTxtRow)
			{
				console.gotoxy(pTopLeftX, screenY++);
				printf(txtLineFormatStr, "");
			}
		}
		writeTxtLines = false;

		// Get a keypress from the user and take action based on it
		console.gotoxy(pPostWriteCurX, pPostWriteCurY);
		retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN);
		switch (retObj.lastKeypress)
		{
			case KEY_UP:
				if (retObj.topLineIdx > 0)
				{
					--retObj.topLineIdx;
					writeTxtLines = true;
				}
				break;
			case KEY_DOWN:
				if (retObj.topLineIdx < topLineIdxForLastPage)
				{
					++retObj.topLineIdx;
					writeTxtLines = true;
				}
				break;
			case KEY_PAGE_DOWN: // Next page
				if (retObj.topLineIdx < topLineIdxForLastPage)
				{
					retObj.topLineIdx += pHeight;
					if (retObj.topLineIdx > topLineIdxForLastPage)
						retObj.topLineIdx = topLineIdxForLastPage;
					writeTxtLines = true;
				}
				break;
			case KEY_PAGE_UP: // Previous page
				if (retObj.topLineIdx > 0)
				{
					retObj.topLineIdx -= pHeight;
					if (retObj.topLineIdx < 0)
						retObj.topLineIdx = 0;
					writeTxtLines = true;
				}
				break;
			case KEY_HOME: // First page
				if (retObj.topLineIdx > 0)
				{
					retObj.topLineIdx = 0;
					writeTxtLines = true;
				}
				break;
			case KEY_END: // Last page
				if (retObj.topLineIdx < topLineIdxForLastPage)
				{
					retObj.topLineIdx = topLineIdxForLastPage;
					writeTxtLines = true;
				}
				break;
			default:
				continueOn = false;
				break;
		}
	}
	return retObj;
}

// Displays a Frame on the screen and allows scrolling through it with the up &
// down arrow keys, PageUp, PageDown, HOME, and END.
//
// Parameters:
//  pFrame: A Frame object to display & scroll through
//  pScrollbar: A ScrollBar object associated with the Frame object
//  pTopLineIdx: The index of the text line to display at the top
//  pTxtAttrib: The attribute(s) to apply to the text lines
//  pWriteTxtLines: Boolean - Whether or not to write the text lines (in addition
//                  to doing the message loop).  If false, this will only do the
//                  the message loop.  This parameter is intended as a screen
//                  refresh optimization.
//  pPostWriteCurX: The X location for the cursor after writing the message
//                  lines
//  pPostWriteCurY: The Y location for the cursor after writing the message
//                  lines
//  pScrollUpdateFn: A function that the caller can provide for updating the
//                   scroll position.  This function has one parameter:
//                   - fractionToLastPage: The fraction of the top index divided
//                     by the top index for the last page (basically, the progress
//                     to the last page).
//
// Return value: An object with the following properties:
//               lastKeypress: The last key pressed by the user (a string)
//               topLineIdx: The new top line index of the text lines, in case of scrolling
function scrollFrame(pFrame, pScrollbar, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pPostWriteCurX,
                     pPostWriteCurY, pScrollUpdateFn)
{
	// Variables for the top line index for the last page, scrolling, etc.
	var topLineIdxForLastPage = pFrame.data_height - pFrame.height;
	if (topLineIdxForLastPage < 0)
		topLineIdxForLastPage = 0;

	var retObj = new Object();
	retObj.lastKeypress = "";
	retObj.topLineIdx = pTopLineIdx;

	if (pTopLineIdx > 0)
		pFrame.scrollTo(0, pTopLineIdx);

	var writeTxtLines = pWriteTxtLines;
	if (writeTxtLines)
	{
		pFrame.invalidate(); // Force drawing on the next call to draw() or cycle()
		pFrame.cycle();
		//pFrame.draw();
	}

	var cycleFrame = true;
	var continueOn = true;
	while (continueOn)
	{
		// If we are to write the text lines, then draw the frame.
		// TODO: Do we really need this?  Will this be different from
		// scrollTextLines()?
		//if (writeTxtLines)
		//	pFrame.draw();

		if (cycleFrame)
		{
			// Invalidate the frame to force it to redraw everything, as a
			// workaround to clear the background before writing again
			// TODO: I might want to remove this invalidate() later when
			// Frame is fixed to redraw better on scrolling.
			pFrame.invalidate();
			// Cycle the scrollbar & frame to get them to scroll
		}

		writeTxtLines = false;
		cycleFrame = false;

		// Get a keypress from the user and take action based on it
		console.gotoxy(pPostWriteCurX, pPostWriteCurY);
		retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN);
		switch (retObj.lastKeypress)
		{
			case KEY_UP:
				if (retObj.topLineIdx > 0)
				{
					pFrame.scroll(0, -1);
					--retObj.topLineIdx;
					cycleFrame = true;
					writeTxtLines = true;
				}
				break;
			case KEY_DOWN:
				if (retObj.topLineIdx < topLineIdxForLastPage)
				{
					pFrame.scroll(0, 1);
					cycleFrame = true;
					++retObj.topLineIdx;
					writeTxtLines = true;
				}
				break;
			case KEY_PAGE_DOWN: // Next page
				if (retObj.topLineIdx < topLineIdxForLastPage)
				{
					//pFrame.scroll(0, pFrame.height);
					retObj.topLineIdx += pFrame.height;
					if (retObj.topLineIdx > topLineIdxForLastPage)
						retObj.topLineIdx = topLineIdxForLastPage;
					pFrame.scrollTo(1, retObj.topLineIdx+1);
					cycleFrame = true;
					writeTxtLines = true;
				}
				break;
			case KEY_PAGE_UP: // Previous page
				if (retObj.topLineIdx > 0)