diff --git a/xtrn/DDMsgReader/DDMsgReader.js b/xtrn/DDMsgReader/DDMsgReader.js
index 00dff7a10e7db8e1fb4a14f32c216d54d0ab9044..111de910a85dd7fac2944eb2a679c1e6f2f71ed1 100644
--- a/xtrn/DDMsgReader/DDMsgReader.js
+++ b/xtrn/DDMsgReader/DDMsgReader.js
@@ -21,40 +21,6 @@
  * Date       Author            Description
  * 2014-09-13 Eric Oulashin     Started (based on my message lister script)
  * ... Comments trimmed ...
- * 2019-07-06 Eric Oulashin     Version 1.23 Beta 1
- *                              Updated DigDistMsgReader_GetMsgBody() to remove any initial
- *                              color code(s) from the start of the message, which can
- *                              color the whole message unnecessarily.
- * 2019-07-26 Eric Oulashin     Started working on supporting utf-8 text conversion to cp437.
- * 2019-07-27 Eric Oulashin     Version 1.23
- *                              Releasing this version
- * 2019-08-15 Eric Oulashin     Version 1.24 Beta
- *                              When making a private reply on local email,
- *                              an error is now outputted if the recipient's
- *                              user number is not found.
- *                              Also, fixed an 'undefined' bug that happened when searching
- *                              for messages sometimes.  searchMsgbase() referenced
- *                              this.subBoardCode, but that function isn't
- *                              part of an object, so this.subBoardCode isn't available
- *                              there.
- * 2019-08-17 Eric Oulashin     Verison 1.24
- *                              Releasing this version
- * 2019-08-23 Eric Oulashin     Version 1.25 Beta
- *                              Started working on adding searching when changing
- *                              to another message area.
- * 2019-08-29 Eric Oulashin     Version 1.25
- *                              Releasing this version
- * 2019-09-12 Eric Oulashin     Version 1.26
- *                              Fixed a bug that caused some tally information to be
- *                              displayed as "undefined".
- * 2019-09-16 Eric Oulashin     Version 1.27
- *                              Bug fix: Now displays the message score in the header
- *                              even if the message only has downvotes
- * 2019-12-28 Eric Oulashin     Version 1.28
- *                              Bug fix: When the user changes to a different message
- *                              area while reading a message, the reader would exit
- *                              with an error due to an invalid last-read message number.
- *                              This has been fixed.
  * 2020-04-03 Eric Oulashin     Version 1.29
  *                              When reading a message, if a message is written to the
  *                              current user, the 'To' username in the header above
@@ -65,6 +31,10 @@
  *                              Later I also plan to update the area chooser code
  *                              to use DDLightbarMenu as well and remove the
  *                              internal lightbar chooser code altogether.
+ * 2020-04-13 Eric Oulashin     Version 1.31
+ *                              The area change feature now uses DDLightbarMenu.
+ *                              There is no more internal lightbar code in this
+ *                              message reader.
  */
 
 
@@ -174,8 +144,8 @@ if (system.version_num < 31500)
 }
 
 // Reader version information
-var READER_VERSION = "1.30";
-var READER_DATE = "2020-04-07";
+var READER_VERSION = "1.31";
+var READER_DATE = "2020-04-13";
 
 // Keyboard key codes for displaying on the screen
 var UP_ARROW = ascii(24);
@@ -716,6 +686,8 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	this.ListMessages_Traditional = DigDistMsgReader_ListMessages_Traditional;
 	this.ListMessages_Lightbar = DigDistMsgReader_ListMessages_Lightbar;
 	this.CreateLightbarMsgListMenu = DigDistMsgReader_CreateLightbarMsgListMenu;
+	this.CreateLightbarMsgGrpMenu = DigDistMsgReader_CreateLightbarMsgGrpMenu;
+	this.CreateLightbarSubBoardMenu = DigDistMsgReader_CreateLightbarSubBoardMenu;
 	this.AdjustLightbarMsgListMenuIdxes = DigDistMsgReader_AdjustLightbarMsgListMenuIdxes;
 	this.ClearSearchData = DigDistMsgReader_ClearSearchData;
 	this.ReadOrListSubBoard = DigDistMsgReader_ReadOrListSubBoard;
@@ -1258,11 +1230,10 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 
 	// Some methods for choosing the message area
 	this.WriteChgMsgAreaKeysHelpLine = DigDistMsgReader_WriteLightbarChgMsgAreaKeysHelpLine;
-	this.WriteGrpListHdrLine = DigDistMsgReader_WriteGrpListTopHdrLine;
-	this.WriteSubBrdListHdr1Line = DMsgAreaChooser_WriteSubBrdListHdr1Line;
+	this.WriteGrpListHdrLine1 = DigDistMsgReader_WriteGrpListTopHdrLine1;
+	this.WriteSubBrdListHdrLine = DigDistMsgReader_WriteSubBrdListHdrLine;
 	this.SelectMsgArea = DigDistMsgReader_SelectMsgArea;
 	this.SelectMsgArea_Lightbar = DigDistMsgReader_SelectMsgArea_Lightbar;
-	this.SelectSubBoard_Lightbar = DigDistMsgReader_SelectSubBoard_Lightbar;
 	this.SelectMsgArea_Traditional = DigDistMsgReader_SelectMsgArea_Traditional;
 	this.ListMsgGrps = DigDistMsgReader_ListMsgGrps_Traditional;
 	this.ListSubBoardsInMsgGroup = DigDistMsgReader_ListSubBoardsInMsgGroup_Traditional;
@@ -1272,6 +1243,7 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	this.UpdateMsgAreaPageNumInHeader = DigDistMsgReader_updateMsgAreaPageNumInHeader;
 	this.ListScreenfulOfSubBrds = DigDistMsgReader_ListScreenfulOfSubBrds;
 	this.WriteMsgSubBoardLine = DigDistMsgReader_WriteMsgSubBrdLine;
+	this.GetMsgSubBoardLine = DigDistMsgReader_GetMsgSubBrdLine;
 	// Choose Message Area help screen
 	this.ShowChooseMsgAreaHelpScreen = DigDistMsgReader_showChooseMsgAreaHelpScreen;
 	// Method to build the sub-board printf information for a message
@@ -3285,7 +3257,8 @@ function DigDistMsgReader_ListMessages_Lightbar(pAllowChgSubBoard)
 		this.SetUpLightbarMsgListVars();
 	}
 
-	// Use a DDLightbarMenu to list messages
+	// Create a DDLightbarMenu for the message list and list messages
+	// and let the user choose one
 	var msgListMenu = this.CreateLightbarMsgListMenu();
 	var msgHeader = null;
 	var drawMenu = true;
@@ -3660,8 +3633,6 @@ function DigDistMsgReader_ListMessages_Lightbar(pAllowChgSubBoard)
 function DigDistMsgReader_CreateLightbarMsgListMenu()
 {
 	// Start & end indexes for the various items in each message list row
-	// TODO: When a message is marked for deletion, currently this will use
-	// the select mark attributes
 	var msgListIdxes = {
 		msgNumStart: 0,
 		msgNumEnd: this.MSGNUM_LEN,
@@ -3782,6 +3753,162 @@ function DigDistMsgReader_CreateLightbarMsgListMenu()
 
 	return msgListMenu;
 }
+// For the DigDistMsgLister class: Creates a DDLightbarMenu object for the user to choose
+// a message group.
+//
+// Return value: A DDLightbarMenu object set up to let the user choose a message group
+function DigDistMsgReader_CreateLightbarMsgGrpMenu()
+{
+	// Start & end indexes for the various items in each mssage group list row
+	// Selected mark, group#, description, # sub-boards
+	var msgGrpListIdxes = {
+		markCharStart: 0,
+		markCharEnd: 1,
+		grpNumStart: 1,
+		grpNumEnd: 2 + (+this.areaNumLen)
+	};
+	msgGrpListIdxes.descStart = msgGrpListIdxes.grpNumEnd;
+	msgGrpListIdxes.descEnd = msgGrpListIdxes.descStart + +this.msgGrpDescLen;
+	msgGrpListIdxes.numItemsStart = msgGrpListIdxes.descEnd;
+	msgGrpListIdxes.numItemsEnd = msgGrpListIdxes.numItemsStart + +this.numItemsLen;
+	// Set numItemsEnd to -1 to let the whole rest of the lines be colored
+	msgGrpListIdxes.numItemsEnd = -1;
+	var listStartRow = this.areaChangeHdrLines.length + 2;
+	var msgGrpMenuHeight = console.screen_rows - listStartRow;
+	var msgGrpMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, msgGrpMenuHeight);
+	msgGrpMenu.scrollbarEnabled = true;
+	msgGrpMenu.borderEnabled = false;
+	msgGrpMenu.SetColors({
+		itemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor},
+		            {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaChooserMsgAreaNumColor},
+		            {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescColor},
+		            {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsColor}],
+		selectedItemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor + this.colors.areaChooserMsgAreaBkgHighlightColor},
+		                    {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaChooserMsgAreaNumHighlightColor},
+		                    {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescHighlightColor},
+		                    {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsHighlightColor}]
+	});
+
+	msgGrpMenu.multiSelect = false;
+	msgGrpMenu.ampersandHotkeysInItems = false;
+	msgGrpMenu.wrapNavigation = false;
+
+	// Add additional keypresses for quitting the menu's input loop so we can
+	// respond to these keys
+	msgGrpMenu.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
+	msgGrpMenu.msgReader = this; // Add this object to the menu object
+	msgGrpMenu.NumItems = function() {
+		return msg_area.grp_list.length;
+	};
+	msgGrpMenu.GetItem = function(pGrpIndex) {
+		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
+	subBoardMenu.selectedItemIdx = msg_area.sub[this.subBoardCode].index;
+	if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage())
+		subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1;
+
+	return subBoardMenu;
+}
 // For the DigDistMsgLister class: Adjusts lightbar menu indexes for a message list menu
 function DigDistMsgReader_AdjustLightbarMsgListMenuIdxes(pMsgListMenu)
 {
@@ -10123,17 +10250,17 @@ function DigDistMsgReader_WriteLightbarChgMsgAreaKeysHelpLine()
 //             not passed, then it won't be used.
 //  pPageNum: The page number.  This is optional; if this is not passed,
 //            then it won't be used.
-function DigDistMsgReader_WriteGrpListTopHdrLine(pNumPages, pPageNum)
+function DigDistMsgReader_WriteGrpListTopHdrLine1(pNumPages, pPageNum)
 {
-  var descStr = "Description";
-  if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
-    descStr += "    (Page " + pPageNum + " of " + pNumPages + ")";
-  else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
-    descStr += "    (Page " + pPageNum + ")";
-  else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
-    descStr += "    (" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
-  printf(this.msgGrpListHdrPrintfStr, "Group#", descStr, "# Sub-Boards");
-  console.cleartoeol("\1n");
+	var descStr = "Description";
+	if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
+		descStr += "    (Page " + pPageNum + " of " + pNumPages + ")";
+	else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
+		descStr += "    (Page " + pPageNum + ")";
+	else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
+		descStr += "    (" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
+	printf(this.msgGrpListHdrPrintfStr, "Group#", descStr, "# Sub-Boards");
+	console.cleartoeol("\1n");
 }
 
 // For the DigDistMsgReader class: Outputs the first header line to appear
@@ -10145,7 +10272,7 @@ function DigDistMsgReader_WriteGrpListTopHdrLine(pNumPages, pPageNum)
 //             not passed, then it won't be used.
 //  pPageNum: The page number.  This is optional; if this is not passed,
 //            then it won't be used.
-function DMsgAreaChooser_WriteSubBrdListHdr1Line(pGrpIndex, pNumPages, pPageNum)
+function DigDistMsgReader_WriteSubBrdListHdrLine(pGrpIndex, pNumPages, pPageNum)
 {
 	var descFormatStr = "\1n" + this.colors.areaChooserSubBoardHeaderColor + "Sub-boards of \1h%-25s     \1n"
 	                  + this.colors.areaChooserSubBoardHeaderColor;
@@ -10172,9 +10299,14 @@ function DigDistMsgReader_SelectMsgArea()
 
 // For the DigDistMsgReader class: Lets the user choose a message group and
 // sub-board via numeric input, using a lightbar user interface.
-function DigDistMsgReader_SelectMsgArea_Lightbar()
+//
+// Parameters:
+//  pMsgGrp: Optional boolean - Whether to let the user choose a message group first.
+//           For internal use.  Defaults to true.
+//  pGrpIdx: If pMsgGrp is true, then this specifies the group index so that
+//           sub-boards can be displayed.
+function DigDistMsgReader_SelectMsgArea_Lightbar(pMsgGrp, pGrpIdx)
 {
-	// TODO: Allow searching
 	// If there are no message groups, then don't let the user
 	// choose one.
 	if (msg_area.grp_list.length == 0)
@@ -10184,1047 +10316,289 @@ function DigDistMsgReader_SelectMsgArea_Lightbar()
 		return;
 	}
 
-	// Returns the index of the bottommost message group that can be displayed
-	// on the screen.
-	//
-	// Parameters:
-	//  pTopGrpIndex: The index of the topmost message group displayed on screen
-	//  pNumItemsPerPage: The number of items per page
-	function getBottommostGrpIndex(pTopGrpIndex, pNumItemsPerPage)
-	{
-		var bottomGrpIndex = pTopGrpIndex + pNumItemsPerPage - 1;
-		// If bottomGrpIndex is beyond the last index, then adjust it.
-		if (bottomGrpIndex >= msg_area.grp_list.length)
-			bottomGrpIndex = msg_area.grp_list.length - 1;
-		return bottomGrpIndex;
-	}
-	
-	// For doing the "next" search result
-	function nextGrpSearchFoundItem(searchObj, numPages, listStartRow, listEndRow, selectedGrpIndex, topMsgGrpIndex, chooserObj)
-	{
-		var retObj = {
-			differentPage: false,
-			topMsgGrpIndex: srchObj.pageTopIdx,
-			selectedGrpIndex: srchObj.itemIdx
-		};
-
-		// For screen refresh optimization, don't redraw the whole
-		// list if the result is on the same page
-		if (srchObj.pageTopIdx != topMsgGrpIndex)
-		{
-			retObj.differentPage = true;
-			chooserObj.UpdateMsgAreaPageNumInHeader(srchObj.pageNum, numPages, true, false);
-			chooserObj.ListScreenfulOfMsgGrps(retObj.topMsgGrpIndex, listStartRow, listEndRow, false, true, srchObj.itemIdx);
-		}
-		else
-		{
-			if (srchObj.itemIdx != selectedGrpIndex)
-			{
-				var screenY = listStartRow + (selectedGrpIndex - topMsgGrpIndex);
-				console.gotoxy(1, screenY);
-				chooserObj.WriteMsgGroupLine(selectedGrpIndex, false);
-				retObj.selectedGrpIndex = srchObj.itemIdx;
-				screenY = listStartRow + (retObj.selectedGrpIndex - topMsgGrpIndex);
-				console.gotoxy(1, screenY);
-				chooserObj.WriteMsgGroupLine(retObj.selectedGrpIndex, true);
-			}
-		}
-
-		return retObj;
-	}
-
+	// Make a backup of the current message group & sub-board indexes so
+	// that later we can tell if the user chose something different.
+	var oldGrp = msg_area.sub[this.subBoardCode].grp_index;
+	var oldSub = msg_area.sub[this.subBoardCode].index;
 
-	// Figure out the index of the user's currently-selected message group
-	var selectedGrpIndex = msg_area.sub[this.subBoardCode].grp_index;
-	// Older code:
-	/*
-	var selectedGrpIndex = 0;
-	if ((bbs.curgrp != null) && (typeof(bbs.curgrp) == "number"))
-		selectedGrpIndex = bbs.curgrp;
-	*/
+	var chooseMsgGrp = (typeof(pMsgGrp) == "boolean" ? pMsgGrp : true);
 
-	// listStartRow is the row on the screen where the list will start
-	var listStartRow = 2 + this.areaChangeHdrLines.length;
-	var listEndRow = console.screen_rows - 1; // Row on screen where list will end
-	var topMsgGrpIndex = 0;    // The index of the message group at the top of the list
-
-	// Figure out the index of the last message group to appear on the screen.
-	var numItemsPerPage = listEndRow - listStartRow + 1;
-	var bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-	// Figure out how many pages are needed to list all the sub-boards.
-	var numPages = Math.ceil(msg_area.grp_list.length / numItemsPerPage);
-	// Figure out the top index for the last page.
-	var topIndexForLastPage = (numItemsPerPage * numPages) - numItemsPerPage;
-
-	// If the highlighted row is beyond the current screen, then
-	// go to the appropriate page.
-	if (selectedGrpIndex > bottomMsgGrpIndex)
-	{
-		var nextPageTopIndex = 0;
-		while (selectedGrpIndex > bottomMsgGrpIndex)
-		{
-			nextPageTopIndex = topMsgGrpIndex + numItemsPerPage;
-			if (nextPageTopIndex < msg_area.grp_list.length)
-			{
-				// Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
-				// refresh the list on the screen.
-				topMsgGrpIndex = nextPageTopIndex;
-				bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-			}
-			else
-				break;
-		}
-
-		// If we didn't find the correct page for some reason, then set the
-		// variables to display page 1 and select the first message group.
-		var foundCorrectPage = ((topMsgGrpIndex < msg_area.grp_list.length) &&
-		                        (selectedGrpIndex >= topMsgGrpIndex) && (selectedGrpIndex <= bottomMsgGrpIndex));
-		if (!foundCorrectPage)
+	// This function displays the header line(s) above the list
+	function displayListHdrLines(pStartRow, pChooseMsgGrp, pReader)
+	{
+		console.gotoxy(1, pStartRow);
+		if (pChooseMsgGrp)
+			pReader.WriteGrpListHdrLine1();
+		else
 		{
-			topMsgGrpIndex = 0;
-			bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-			selectedGrpIndex = 0;
+			pReader.WriteSubBrdListHdrLine(pGrpIdx);
+			console.gotoxy(1, pStartRow+1);
+			printf(pReader.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
 		}
 	}
 
-	// Clear the screen, write the header lines, help line and group list header,
-	// and output a screenful of message groups.
+	// Clear the screen & write the header lines, help line and group list header
 	console.clear("\1n");
 	this.DisplayAreaChgHdr(1);
+	displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this);
 	this.WriteChgMsgAreaKeysHelpLine();
 
-	// Make a backup of the current message group & sub-board indexes so
-	// that later we can tell if the user chose something different.
-	var oldGrp = msg_area.sub[this.subBoardCode].grp_index;
-	var oldSub = msg_area.sub[this.subBoardCode].index;
-	// Older:
-	/*
-	var oldGrp = bbs.curgrp;
-	var oldSub = bbs.cursub;
-	*/
-
-	// Input loop - Let the user choose a message group & sub-board
-	var curpos = {
-		x: 1,
-		y: listStartRow - 1
-	};
-	console.gotoxy(curpos);
-	var pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
-	this.WriteGrpListHdrLine(numPages, pageNum);
-	this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, false);
-	// Start of the input loop.
-	var highlightScrenRow = 0; // The row on the screen for the highlighted group
-	var userInput = "";        // Will store a keypress from the user
-	var retObj = null;        // To store the return value of choosing a sub-board
+	// Create a menu of message groups or sub-boards
+	var msgAreaMenu = (chooseMsgGrp ? this.CreateLightbarMsgGrpMenu() : this.CreateLightbarSubBoardMenu(pGrpIdx));
+	var drawMenu = true;
 	var lastSearchText = "";
 	var lastSearchFoundIdx = -1;
-	var continueChoosingMsgArea = true;
-	while (continueChoosingMsgArea)
+	var chosenIdx = -1;
+	var continueOn = true;
+	// Let the user choose a group, and also respond to other user choices
+	while (continueOn)
 	{
-		// Highlight the currently-selected message group
-		highlightScrenRow = listStartRow + (selectedGrpIndex - topMsgGrpIndex);
-		curpos.y = highlightScrenRow;
-		if ((highlightScrenRow > 0) && (highlightScrenRow < console.screen_rows))
-		{
-			console.gotoxy(1, highlightScrenRow);
-			this.WriteMsgGroupLine(selectedGrpIndex, true);
-		}
-
-		// Get a key from the user (upper-case) and take action based upon it.
-		//userInput = console.getkey(K_UPPER | K_NOCRLF);
-		userInput = getKeyWithESCChars(K_UPPER | K_NOCRLF);
-		switch (userInput)
+		chosenIdx = -1;
+		var msgGrpIdx = msgAreaMenu.GetVal(drawMenu);
+		drawMenu = true;
+		var lastUserInputUpper = (typeof(msgAreaMenu.lastUserInput) == "string" ? msgAreaMenu.lastUserInput.toUpperCase() : msgAreaMenu.lastUserInput);
+		if (typeof(msgGrpIdx) == "number")
+			chosenIdx = msgGrpIdx;
+		// If userChoice is not a number, then it should be null in this case,
+		// and the user would have pressed one of the additional quit keys set
+		// up for the menu.  So look at the menu's lastUserInput and do the
+		// appropriate thing.
+		else if ((lastUserInputUpper == "Q") || (lastUserInputUpper == KEY_ESC)) // Quit
+			continueOn = false;
+		else if ((lastUserInputUpper == "/") || (lastUserInputUpper == CTRL_F)) // Start of find
 		{
-			case KEY_UP: // Move up one message group in the list
-				if (selectedGrpIndex > 0)
-				{
-					// If the previous group index is on the previous page, then
-					// display the previous page.
-					var previousGrpIndex = selectedGrpIndex - 1;
-					if (previousGrpIndex < topMsgGrpIndex)
-					{
-						// Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
-						// refresh the list on the screen.
-						topMsgGrpIndex -= numItemsPerPage;
-						bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-						this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, true, false);
-						this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow,
-						                            listEndRow, false, true);
-					}
-					else
-					{
-						// Display the current line un-highlighted.
-						console.gotoxy(1, curpos.y);
-						this.WriteMsgGroupLine(selectedGrpIndex, false);
-					}
-					selectedGrpIndex = previousGrpIndex;
-				}
-				break;
-			case KEY_DOWN: // Move down one message group in the list
-				if (selectedGrpIndex < msg_area.grp_list.length - 1)
+			console.gotoxy(1, console.screen_rows);
+			console.cleartoeol("\1n");
+			console.gotoxy(1, console.screen_rows);
+			var promptText = "Search: ";
+			console.print(promptText);
+			var searchText = getStrWithTimeout(K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE, console.screen_columns - promptText.length - 1, SEARCH_TIMEOUT_MS);
+			lastSearchText = searchText;
+			// If the user entered text, then do the search, and if found,
+			// found, go to the page and select the item indicated by the
+			// search.
+			if (searchText.length > 0)
+			{
+				var oldLastSearchFoundIdx = lastSearchFoundIdx;
+				var oldSelectedItemIdx = msgAreaMenu.selectedItemIdx;
+				var idx = -1;
+				if (chooseMsgGrp)
+					idx = findMsgGrpIdxFromText(searchText, msgAreaMenu.selectedItemIdx);
+				else
+					idx = findSubBoardIdxFromText(pGrpIdx, searchText, msgAreaMenu.selectedItemIdx+1);
+				lastSearchFoundIdx = idx;
+				if (idx > -1)
 				{
-					// If the next group index is on the next page, then display
-					// the next page.
-					var nextGrpIndex = selectedGrpIndex + 1;
-					if (nextGrpIndex > bottomMsgGrpIndex)
-					{
-						// Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
-						// refresh the list on the screen.
-						topMsgGrpIndex += numItemsPerPage;
-						bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-						this.UpdateMsgAreaPageNumInHeader(pageNum+1, numPages, true, false);
-						this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow,
-						                            listEndRow, false, true);
-					}
+					// Set the currently selected item in the menu, and ensure it's
+					// visible on the page
+					msgAreaMenu.selectedItemIdx = idx;
+					if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
+						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
+					else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
+						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
 					else
 					{
-						// Display the current line un-highlighted.
-						console.gotoxy(1, curpos.y);
-						this.WriteMsgGroupLine(selectedGrpIndex, false);
+						// If the current index and the last index are both on the same page on the
+						// menu, then have the menu only redraw those items.
+						msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
 					}
-					selectedGrpIndex = nextGrpIndex;
-				}
-				break;
-			case KEY_HOME: // Go to the top message group on the screen
-				if (selectedGrpIndex > topMsgGrpIndex)
-				{
-					// Display the current line un-highlighted, then adjust
-					// selectedGrpIndex.
-					console.gotoxy(1, curpos.y);
-					this.WriteMsgGroupLine(selectedGrpIndex, false);
-					selectedGrpIndex = topMsgGrpIndex;
-					// Note: curpos.y is set at the start of the while loop.
-				}
-				break;
-			case KEY_END: // Go to the bottom message group on the screen
-				if (selectedGrpIndex < bottomMsgGrpIndex)
-				{
-					// Display the current line un-highlighted, then adjust
-					// selectedGrpIndex.
-					console.gotoxy(1, curpos.y);
-					this.WriteMsgGroupLine(selectedGrpIndex, false);
-					selectedGrpIndex = bottomMsgGrpIndex;
-					// Note: curpos.y is set at the start of the while loop.
-				}
-				break;
-			case KEY_ENTER: // Select the currently-highlighted message group
-				// Show a "Loading..." text in case there are many sub-boards in
-				// the chosen message group
-				console.gotoxy(1, console.getxy().y);
-				console.print("\1nLoading...        ");
-				console.cleartoeol();
-				retObj = this.SelectSubBoard_Lightbar(selectedGrpIndex);
-				// If the user chose a sub-board, then set bbs.curgrp and
-				// bbs.cursub, and don't continue the input loop anymore.
-				if (retObj.subBoardChosen)
-				{
-					bbs.curgrp = selectedGrpIndex;
-					bbs.cursub = retObj.subBoardIndex;
-					continueChoosingMsgArea = false;
 				}
 				else
 				{
-					// A sub-board was not chosen, so we'll have to re-draw
-					// the header and list of message groups.
-					console.gotoxy(1, this.areaChangeHdrLines.length+1);
-					this.WriteGrpListHdrLine(numPages, pageNum);
-					this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
-					                            false, true);
-				}
-				break;
-			case KEY_PAGE_DOWN: // Go to the next page
-				var nextPageTopIndex = topMsgGrpIndex + numItemsPerPage;
-				if (nextPageTopIndex < msg_area.grp_list.length)
-				{
-					// Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
-					// refresh the list on the screen.
-					topMsgGrpIndex = nextPageTopIndex;
-					pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
-					bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-					this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, true, false);
-					this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow,
-					                            listEndRow, false, true);
-					selectedGrpIndex = topMsgGrpIndex;
-				}
-				break;
-			case KEY_PAGE_UP: // Go to the previous page
-				var prevPageTopIndex = topMsgGrpIndex - numItemsPerPage;
-				if (prevPageTopIndex >= 0)
-				{
-					// Adjust topMsgGrpIndex and bottomMsgGrpIndex, and
-					// refresh the list on the screen.
-					topMsgGrpIndex = prevPageTopIndex;
-					pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
-					bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-					this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, true, false);
-					this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow,
-					                            listEndRow, false, true);
-					selectedGrpIndex = topMsgGrpIndex;
-				}
-				break;
-			case 'F': // Go to the first page
-				if (topMsgGrpIndex > 0)
-				{
-					topMsgGrpIndex = 0;
-					pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
-					bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-					this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, true, false);
-					this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
-					                            false, true);
-					selectedGrpIndex = 0;
-				}
-				break;
-			case 'L': // Go to the last page
-				if (topMsgGrpIndex < topIndexForLastPage)
-				{
-					topMsgGrpIndex = topIndexForLastPage;
-					pageNum = calcPageNum(topMsgGrpIndex, numItemsPerPage);
-					bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-					this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, true, false);
-					this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
-					                            false, true);
-					selectedGrpIndex = topIndexForLastPage;
-				}
-				break;
-			case '/': // Start of find (search)
-			case CTRL_F: // Start of find
-				console.gotoxy(1, console.screen_rows);
-				console.cleartoeol("\1n");
-				console.gotoxy(1, console.screen_rows);
-				var promptText = "Search: ";
-				console.print(promptText);
-				var searchText = getStrWithTimeout(K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE, console.screen_columns - promptText.length - 1, SEARCH_TIMEOUT_MS);
-				// If the user entered text, then do the search, and if found,
-				// found, go to the page and select the item indicated by the
-				// search.
-				if (searchText.length > 0)
-				{
-					var srchObj = getMsgAreaPageNumFromSearch(searchText, numItemsPerPage, false, 0);
-					if (srchObj.pageNum > 0)
+					if (chooseMsgGrp)
+						idx = findMsgGrpIdxFromText(searchText, 0);
+					else
+						idx = findSubBoardIdxFromText(pGrpIdx, searchText, 0);
+					lastSearchFoundIdx = idx;
+					if (idx > -1)
 					{
-						lastSearchText = searchText;
-						lastSearchFoundIdx = srchObj.itemIdx;
-
-						// For screen refresh optimization, don't redraw the whole
-						// list if the result is on the same page
-						if (srchObj.pageTopIdx != topMsgGrpIndex)
-						{
-							topMsgGrpIndex = srchObj.pageTopIdx;
-							selectedGrpIndex = srchObj.itemIdx;
-							bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-							pageNum = srchObj.pageNum;
-							this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, true, false);
-							this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow, false, true);
-						}
+						// Set the currently selected item in the menu, and ensure it's
+						// visible on the page
+						msgAreaMenu.selectedItemIdx = idx;
+						if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
+							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
+						else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
+							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
 						else
 						{
-							if (srchObj.itemIdx != selectedGrpIndex)
-							{
-								var screenY = listStartRow + (selectedGrpIndex - topMsgGrpIndex);
-								console.gotoxy(1, screenY);
-								this.WriteMsgGroupLine(selectedGrpIndex, false);
-								selectedGrpIndex = srchObj.itemIdx;
-								screenY = listStartRow + (selectedGrpIndex - topMsgGrpIndex);
-								console.gotoxy(1, screenY);
-								this.WriteMsgGroupLine(selectedGrpIndex, true);
-							}
+							// The current index and the last index are both on the same page on the
+							// menu, so have the menu only redraw those items.
+							msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
 						}
 					}
 					else
+					{
 						this.WriteLightbarKeyHelpErrorMsg("Not found");
+						drawMenu = false;
+					}
 				}
-				this.WriteChgMsgAreaKeysHelpLine();
-				break;
-			case 'N': // Next search result (requires an existing search term)
-				if ((lastSearchText.length > 0) && (lastSearchFoundIdx > -1))
+			}
+			else
+				drawMenu = false;
+			this.WriteChgMsgAreaKeysHelpLine();
+		}
+		else if (lastUserInputUpper == "N") // Next search result (requires an existing search term)
+		{
+			// This works but seems a little strange sometimes.
+			// - Should this always start from the selected index?
+			// - If it wraps around to one of the items on the first page,
+			//   should it always set the top index to 0?
+			if ((lastSearchText.length > 0) && (lastSearchFoundIdx > -1))
+			{
+				var oldLastSearchFoundIdx = lastSearchFoundIdx;
+				var oldSelectedItemIdx = msgAreaMenu.selectedItemIdx;
+				// Do the search, and if found, go to the page and select the item
+				// indicated by the search.
+				var idx = 0;
+				if (chooseMsgGrp)
+					idx = findMsgGrpIdxFromText(searchText, lastSearchFoundIdx+1);
+				else
+					idx = findSubBoardIdxFromText(pGrpIdx, searchText, lastSearchFoundIdx+1);
+				if (idx > -1)
 				{
-					// Do the search, and if found, go to the page and select the item
-					// indicated by the search.
-					var srchObj = getMsgAreaPageNumFromSearch(lastSearchText, numItemsPerPage, false, lastSearchFoundIdx+1);
-					lastSearchFoundIdx = srchObj.itemIdx;
-					if (srchObj.pageNum > 0)
+					lastSearchFoundIdx = idx;
+					// Set the currently selected item in the menu, and ensure it's
+					// visible on the page
+					msgAreaMenu.selectedItemIdx = idx;
+					if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
 					{
-						var foundItemRetObj = nextGrpSearchFoundItem(srchObj, numPages, listStartRow, listEndRow, selectedGrpIndex, topMsgGrpIndex, this);
-						if (foundItemRetObj.differentPage)
-						{
-							pageNum = srchObj.pageNum;
-							topMsgGrpIndex = foundItemRetObj.topMsgGrpIndex;
-							bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-						}
-						selectedGrpIndex = foundItemRetObj.selectedGrpIndex;
+						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
+						if (msgAreaMenu.topItemIdx < 0)
+							msgAreaMenu.topItemIdx = 0;
 					}
+					else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
+						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
 					else
 					{
-						// Not found - Wrap around and start at 0 again
-						var srchObj = getMsgAreaPageNumFromSearch(lastSearchText, numItemsPerPage, false, 0);
-						lastSearchFoundIdx = srchObj.itemIdx;
-						var foundItemRetObj = nextGrpSearchFoundItem(srchObj, numPages, listStartRow, listEndRow, selectedGrpIndex, topMsgGrpIndex, this);
-						if (foundItemRetObj.selectedGrpIndex != selectedGrpIndex)
-						{
-							if (foundItemRetObj.differentPage)
-							{
-								pageNum = srchObj.pageNum;
-								topMsgGrpIndex = foundItemRetObj.topMsgGrpIndex;
-								bottomMsgGrpIndex = getBottommostGrpIndex(topMsgGrpIndex, numItemsPerPage);
-							}
-							selectedGrpIndex = foundItemRetObj.selectedGrpIndex;
-						}
-						else
-							this.WriteLightbarKeyHelpErrorMsg("No others found", REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE);
+						// The current index and the last index are both on the same page on the
+						// menu, so have the menu only redraw those items.
+						msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
 					}
 				}
 				else
-					this.WriteLightbarKeyHelpErrorMsg("There is no previous search", REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE);
-				break;
-			case 'Q': // Quit
-				continueChoosingMsgArea = false;
-				break;
-			case '?': // Show help
-				this.ShowChooseMsgAreaHelpScreen(true, true);
-				console.pause();
-				// Refresh the screen
-				this.WriteChgMsgAreaKeysHelpLine();
-				this.DisplayAreaChgHdr(1);
-				console.gotoxy(1, 1+this.areaChangeHdrLines.length);
-				this.WriteGrpListHdrLine(numPages, pageNum);
-				this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
-				                            false, true);
-				break;
-			default:
-				// If the user entered a numeric digit, then treat it as
-				// the start of the message group number.
-				if (userInput.match(/[0-9]/))
 				{
-					var originalCurpos = curpos;
-
-					// Put the user's input back in the input buffer to
-					// be used for getting the rest of the message number.
-					console.ungetstr(userInput);
-					// Move the cursor to the bottom of the screen and
-					// prompt the user for the message number.
-					console.gotoxy(1, console.screen_rows);
-					console.clearline("\1n");
-					console.print("\1cChoose group #: \1h");
-					userInput = console.getnum(msg_area.grp_list.length);
-					// If the user made a selection, then let them choose a
-					// sub-board from the group.
-					if (userInput > 0)
+					if (chooseMsgGrp)
+						idx = findMsgGrpIdxFromText(searchText, 0);
+					else
+						idx = findSubBoardIdxFromText(pGrpIdx, searchText, 0);
+					lastSearchFoundIdx = idx;
+					if (idx > -1)
 					{
-						var msgGroupIndex = userInput - 1;
-						// Show a "Loading..." text in case there are many sub-boards in
-						// the chosen message group
-						console.crlf();
-						console.print("\1nLoading...");
-						retObj = this.SelectSubBoard_Lightbar(msgGroupIndex);
-						// If the user chose a sub-board, then set bbs.curgrp and
-						// bbs.cursub, and don't continue the input loop anymore.
-						if (retObj.subBoardChosen)
+						// Set the currently selected item in the menu, and ensure it's
+						// visible on the page
+						msgAreaMenu.selectedItemIdx = idx;
+						if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
 						{
-							// Set the current group & sub-board
-							bbs.curgrp = msgGroupIndex;
-							bbs.cursub = retObj.subBoardIndex;
-							continueChoosingMsgArea = false;
+							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
+							if (msgAreaMenu.topItemIdx < 0)
+								msgAreaMenu.topItemIdx = 0;
 						}
+						else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
+							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
 						else
 						{
-							// A sub-board was not chosen, so we'll have to re-draw
-							// the header and list of message groups.
-							console.gotoxy(1, this.areaChangeHdrLines.length+1);
-							this.WriteGrpListHdrLine(numPages, pageNum);
-							this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
-							                            false, true);
+							// The current index and the last index are both on the same page on the
+							// menu, so have the menu only redraw those items.
+							msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
 						}
 					}
 					else
 					{
-						// The user didn't make a selection.  So, we need to refresh
-						// the screen due to everything being moved up one line.
+						this.WriteLightbarKeyHelpErrorMsg("Not found");
+						drawMenu = false;
 						this.WriteChgMsgAreaKeysHelpLine();
-						console.gotoxy(1, 1);
-						this.WriteGrpListHdrLine(numPages, pageNum);
-						this.ListScreenfulOfMsgGrps(topMsgGrpIndex, listStartRow, listEndRow,
-						                            false, true);
 					}
 				}
-				break;
-		}
-	}
-
-	// If the user chose a different message group & sub-board, then reset the
-	// lister index & cursor variables, as well as this.subBoardCode, etc.
-	if ((bbs.curgrp != oldGrp) || (bbs.cursub != oldSub))
-	{
-		this.tradListTopMsgIdx = -1;
-		this.lightbarListTopMsgIdx = -1;
-		this.lightbarListSelectedMsgIdx = -1;
-		this.lightbarListCurPos = null;
-		this.setSubBoardCode(msg_area.grp_list[bbs.curgrp].sub_list[bbs.cursub].code);
-	}
-}
-
-// For the DigDistMsgReader class: Lets the user choose a sub-board within a
-// message group, with a lightbar interface.  Does not set bbs.cursub.
-//
-// Parameters:
-//  pGrpIndex: The index of the message group to choose from.  This is
-//             optional; if not specified, bbs.curgrp will be used.
-//  pMarkIndex: An index of a message group to display the "current" mark
-//              next to.  This is optional; if left off, this will default to
-//              the current sub-board.
-//
-// Return value: An object containing the following values:
-//               subBoardChosen: Boolean - Whether or not a sub-board was chosen.
-//               subBoardIndex: Numeric - The sub-board that was chosen (if any).
-//                              Will be -1 if none chosen.
-function DigDistMsgReader_SelectSubBoard_Lightbar(pGrpIndex, pMarkIndex)
-{
-	// TODO: Allow searching
-	// Create the return object.
-	var retObj = {
-		subBoardChosen: false,
-		subBoardIndex: -1
-	};
-
-	var grpIndex = 0;
-	if (typeof(pGrpIndex) == "number")
-		grpIndex = pGrpIndex;
-	else
-		grpIndex = msg_area.sub[this.subBoardCode].grp_index;
-	// Older:
-	/*
-	else if ((bbs.curgrp != null) && (typeof(bbs.curgrp) == "number"))
-		grpIndex = bbs.curgrp;
-	*/
-	// Double-check grpIndex
-	if (grpIndex < 0)
-		grpIndex = 0;
-	else if (grpIndex >= msg_area.grp_list.length)
-		grpIndex = msg_area.grp_list.length - 1;
-
-	var markIndex = 0;
-	if ((pMarkIndex != null) && (typeof(pMarkIndex) == "number"))
-		markIndex = pMarkIndex;
-	else
-		markIndex = msg_area.sub[this.subBoardCode].index;
-	// Older:
-	/*
-	else if ((bbs.cursub != null) && (typeof(bbs.cursub) == "number") &&
-				(bbs.curgrp == pGrpIndex))
-	{
-		markIndex = bbs.cursub;
-	}
-	*/
-	// Double-check markIndex
-	if (markIndex < 0)
-		markIndex = 0;
-	else if (markIndex >= msg_area.grp_list[grpIndex].sub_list.length)
-		markIndex = msg_area.grp_list[grpIndex].sub_list.length - 1;
-
-
-	// Ensure that the sub-board printf information is created for
-	// this message group.
-	this.BuildSubBoardPrintfInfoForGrp(grpIndex);
-
-
-	// If there are no sub-boards in the given message group, then show
-	// an error and return.
-	if (msg_area.grp_list[grpIndex].sub_list.length == 0)
-	{
-		console.clear("\1n");
-		console.print("\1y\1hThere are no sub-boards in the chosen group.\r\n\1p");
-		return retObj;
-	}
-
-	// Returns the index of the bottommost sub-board that can be displayed on
-	// the screen.
-	//
-	// Parameters:
-	//  pTopSubIndex: The index of the topmost sub-board displayed on screen
-	//  pNumItemsPerPage: The number of items per page
-	function getBottommostSubIndex(pTopSubIndex, pNumItemsPerPage)
-	{
-		var bottomGrpIndex = topSubIndex + pNumItemsPerPage - 1;
-		// If bottomGrpIndex is beyond the last index, then adjust it.
-		if (bottomGrpIndex >= msg_area.grp_list[grpIndex].sub_list.length)
-			bottomGrpIndex = msg_area.grp_list[grpIndex].sub_list.length - 1;
-		return bottomGrpIndex;
-	}
-	
-	// For doing the "next" search result
-	// TODO: Update for sub-board sarching
-	function nextSubSearchFoundItem(grpIndex, searchObj, numPages, listStartRow, listEndRow, selectedSubIndex, topMsgGrpIndex, chooserObj)
-	{
-		var retObj = {
-			differentPage: false,
-			topSubIndex: srchObj.pageTopIdx,
-			selectedSubIndex: srchObj.itemIdx
-		};
-
-		// For screen refresh optimization, don't redraw the whole
-		// list if the result is on the same page
-		if (srchObj.pageTopIdx != topSubIndex)
-		{
-			retObj.differentPage = true;
-			chooserObj.UpdateMsgAreaPageNumInHeader(srchObj.pageNum, numPages, false, false);
-			chooserObj.ListScreenfulOfSubBrds(grpIndex, retObj.topSubIndex, listStartRow, listEndRow, false, true, srchObj.itemIdx);
-		}
-		else
-		{
-			if (srchObj.itemIdx != selectedSubIndex)
+			}
+			else
 			{
-				var screenY = listStartRow + (selectedSubIndex - topSubIndex);
-				console.gotoxy(1, screenY);
-				chooserObj.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false);
-				retObj.selectedSubIndex = srchObj.itemIdx;
-				screenY = listStartRow + (retObj.selectedSubIndex - topSubIndex);
-				console.gotoxy(1, screenY);
-				chooserObj.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, true);
+				this.WriteLightbarKeyHelpErrorMsg("There is no previous search", REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE);
+				drawMenu = false;
+				this.WriteChgMsgAreaKeysHelpLine();
 			}
 		}
-
-		return retObj;
-	}
-
-
-	// Figure out the index of the user's currently-selected sub-board.
-	var selectedSubIndex = 0;
-	if (msg_area.sub[this.subBoardCode].grp_index == pGrpIndex)
-		selectedSubIndex = msg_area.sub[this.subBoardCode].index;
-	/*
-	var selectedSubIndex = 0;
-	if ((bbs.cursub != null) && (typeof(bbs.cursub) == "number"))
-	{
-		if ((bbs.curgrp != null) && (typeof(bbs.curgrp) == "number") &&
-			(bbs.curgrp == pGrpIndex))
+		else if (lastUserInputUpper == "?") // Show help
 		{
-			selectedSubIndex = bbs.cursub;
+			this.ShowChooseMsgAreaHelpScreen(true, true);
+			console.pause();
+			// Refresh the screen
+			console.clear("\1n");
+			console.gotoxy(1, 1);
+			this.DisplayAreaChgHdr(1);
+			displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this);
+			this.WriteChgMsgAreaKeysHelpLine();
 		}
-	}
-	*/
-
-	// listStartRow is the row on the screen where the list will start
-	var listStartRow = 3 + this.areaChangeHdrLines.length;
-	var listEndRow = console.screen_rows - 1; // Row on screen where list will end
-	var topSubIndex = 0;      // The index of the message group at the top of the list
-	// Figure out the index of the last message group to appear on the screen.
-	var numItemsPerPage = listEndRow - listStartRow + 1;
-	var bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
-	// Figure out how many pages are needed to list all the sub-boards.
-	var numPages = Math.ceil(msg_area.grp_list[grpIndex].sub_list.length / numItemsPerPage);
-	// Figure out the top index for the last page.
-	var topIndexForLastPage = (numItemsPerPage * numPages) - numItemsPerPage;
-
-	// If the highlighted row is beyond the current screen, then
-	// go to the appropriate page.
-	if (selectedSubIndex > bottomSubIndex)
-	{
-		var nextPageTopIndex = 0;
-		while (selectedSubIndex > bottomSubIndex)
+		// If the user entered a numeric digit, then treat it as
+		// the start of the message group number.
+		if (lastUserInputUpper.match(/[0-9]/))
 		{
-			nextPageTopIndex = topSubIndex + numItemsPerPage;
-			if (nextPageTopIndex < msg_area.grp_list[grpIndex].sub_list.length)
+			// Put the user's input back in the input buffer to
+			// be used for getting the rest of the message number.
+			console.ungetstr(lastUserInputUpper);
+			// Move the cursor to the bottom of the screen and
+			// prompt the user for the message number.
+			console.gotoxy(1, console.screen_rows);
+			console.clearline("\1n");
+			console.print("\1cChoose group #: \1h");
+			var userInput = console.getnum(msg_area.grp_list.length);
+			if (userInput > 0)
+				chosenIdx = userInput - 1;
+			else
 			{
-				// Adjust topSubIndex and bottomSubIndex, and
-				// refresh the list on the screen.
-				topSubIndex = nextPageTopIndex;
-				bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
+				// The user didn't make a selection.  So, we need to refresh
+				// the screen due to everything being moved up one line.
+				displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this);
+				this.WriteChgMsgAreaKeysHelpLine();
 			}
-			else
-				break;
-		}
-
-		// If we didn't find the correct page for some reason, then set the
-		// variables to display page 1 and select the first message group.
-		var foundCorrectPage =
-		         ((topSubIndex < msg_area.grp_list[grpIndex].sub_list.length) &&
-		         (selectedSubIndex >= topSubIndex) && (selectedSubIndex <= bottomSubIndex));
-		if (!foundCorrectPage)
-		{
-			topSubIndex = 0;
-			bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
-			selectedSubIndex = 0;
-		}
-	}
-
-	// Clear the screen, write the header line, help line and group list header,
-	// and output a screenful of message sub-boards.
-	console.clear("\1n");
-	this.DisplayAreaChgHdr(1);
-	if (this.areaChangeHdrLines.length > 0)
-		console.crlf();
-	var pageNum = calcPageNum(topSubIndex, numItemsPerPage);
-	this.WriteSubBrdListHdr1Line(grpIndex, numPages, pageNum);
-	this.WriteChgMsgAreaKeysHelpLine();
-
-	var curpos = {
-		x: 1,
-		y: 2 + this.areaChangeHdrLines.length
-	};
-	console.gotoxy(curpos);
-	printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
-	this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, false);
-	// Start of the input loop.
-	var highlightScrenRow = 0; // The row on the screen for the highlighted group
-	var userInput = "";        // Will store a keypress from the user
-	var lastSearchText = "";
-	var lastSearchFoundIdx = -1;
-	var continueChoosingSubBrd = true;
-	while (continueChoosingSubBrd)
-	{
-		// Highlight the currently-selected message group
-		highlightScrenRow = listStartRow + (selectedSubIndex - topSubIndex);
-		curpos.y = highlightScrenRow;
-		if ((highlightScrenRow > 0) && (highlightScrenRow < console.screen_rows))
-		{
-			console.gotoxy(1, highlightScrenRow);
-			this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, true);
 		}
 
-		// Get a key from the user (upper-case) and take action based upon it.
-		//userInput = console.getkey(K_UPPER | K_NOCRLF);
-		userInput = getKeyWithESCChars(K_UPPER | K_NOCRLF);
-		switch (userInput)
+		// If a group/sub-board was chosen, then deal with it.
+		if (chosenIdx > -1)
 		{
-			case KEY_UP: // Move up one message group in the list
-				if (selectedSubIndex > 0)
-				{
-					// If the previous group index is on the previous page, then
-					// display the previous page.
-					var previousSubIndex = selectedSubIndex - 1;
-					if (previousSubIndex < topSubIndex)
-					{
-						// Adjust topSubIndex and bottomSubIndex, and
-						// refresh the list on the screen.
-						topSubIndex -= numItemsPerPage;
-						bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
-						pageNum = calcPageNum(topSubIndex, numItemsPerPage);
-						this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false);
-						this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true);
-					}
-					else
-					{
-						// Display the current line un-highlighted.
-						console.gotoxy(1, curpos.y);
-						this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false);
-					}
-					selectedSubIndex = previousSubIndex;
-				}
-				break;
-			case KEY_DOWN: // Move down one message group in the list
-				if (selectedSubIndex < msg_area.grp_list[grpIndex].sub_list.length - 1)
-				{
-					// If the next group index is on the next page, then display
-					// the next page.
-					var nextGrpIndex = selectedSubIndex + 1;
-					if (nextGrpIndex > bottomSubIndex)
-					{
-						// Adjust topSubIndex and bottomSubIndex, and
-						// refresh the list on the screen.
-						topSubIndex += numItemsPerPage;
-						bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
-						pageNum = calcPageNum(topSubIndex, numItemsPerPage);
-						this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false);
-						this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true);
-					}
-					else
-					{
-						// Display the current line un-highlighted.
-						console.gotoxy(1, curpos.y);
-						this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false);
-					}
-					selectedSubIndex = nextGrpIndex;
-				}
-				break;
-			case KEY_HOME: // Go to the top message group on the screen
-				if (selectedSubIndex > topSubIndex)
-				{
-					// Display the current line un-highlighted, then adjust
-					// selectedSubIndex.
-					console.gotoxy(1, curpos.y);
-					this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false);
-					selectedSubIndex = topSubIndex;
-					// Note: curpos.y is set at the start of the while loop.
-				}
-				break;
-			case KEY_END: // Go to the bottom message group on the screen
-				if (selectedSubIndex < bottomSubIndex)
-				{
-					// Display the current line un-highlighted, then adjust
-					// selectedSubIndex.
-					console.gotoxy(1, curpos.y);
-					this.WriteMsgSubBoardLine(grpIndex, selectedSubIndex, false);
-					selectedSubIndex = bottomSubIndex;
-					// Note: curpos.y is set at the start of the while loop.
-				}
-				break;
-			case KEY_ENTER: // Select the currently-highlighted sub-board
-				// Validate the sub-board choice.  If a search is specified, the
-				// validator function will search for messages in the selected
-				// sub-board and will return true if there are messages to read
-				// there or false if not.  If there is no search specified,
-				// the validator function will return a 'true' value.
-				var msgAreaValidRetval = this.ValidateMsgAreaChoice(grpIndex, selectedSubIndex, curpos);
-				if (msgAreaValidRetval.msgAreaGood)
-				{
-					continueChoosingSubBrd = false;
-					retObj.subBoardChosen = true;
-					retObj.subBoardIndex = selectedSubIndex;
-				}
-				else
-				{
-					// Output the error that was returned by the validator function
-					console.gotoxy(1, curpos.y);
-					console.cleartoeol("\1n");
-					console.print("\1h\1y" + msgAreaValidRetval.errorMsg);
-					mswait(ERROR_PAUSE_WAIT_MS);
-					console.print("\1n");
-					continueChoosingSubBrd = true;
-					retObj.subBoardChosen = false;
-					retObj.subBoardIndex = -1;
-				}
-				break;
-			case KEY_PAGE_DOWN: // Go to the next page
-				var nextPageTopIndex = topSubIndex + numItemsPerPage;
-				if (nextPageTopIndex < msg_area.grp_list[grpIndex].sub_list.length)
-				{
-					// Adjust topSubIndex and bottomSubIndex, and
-					// refresh the list on the screen.
-					topSubIndex = nextPageTopIndex;
-					pageNum = calcPageNum(topSubIndex, numItemsPerPage);
-					bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
-					this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false);
-					this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true);
-					selectedSubIndex = topSubIndex;
-				}
-				break;
-			case KEY_PAGE_UP: // Go to the previous page
-				var prevPageTopIndex = topSubIndex - numItemsPerPage;
-				if (prevPageTopIndex >= 0)
-				{
-					// Adjust topSubIndex and bottomSubIndex, and
-					// refresh the list on the screen.
-					topSubIndex = prevPageTopIndex;
-					pageNum = calcPageNum(topSubIndex, numItemsPerPage);
-					bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
-					this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false);
-					this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true);
-					selectedSubIndex = topSubIndex;
-				}
-				break;
-			case 'F': // Go to the first page
-				if (topSubIndex > 0)
-				{
-					topSubIndex = 0;
-					pageNum = calcPageNum(topSubIndex, numItemsPerPage);
-					bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
-					this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false);
-					this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true);
-					selectedSubIndex = 0;
-				}
-				break;
-			case 'L': // Go to the last page
-				if (topSubIndex < topIndexForLastPage)
-				{
-					topSubIndex = topIndexForLastPage;
-					pageNum = calcPageNum(topSubIndex, numItemsPerPage);
-					bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
-					this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false);
-					this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true);
-					selectedSubIndex = topIndexForLastPage;
-				}
-				break;
-			case '/': // Start of find (search)
-			case CTRL_F: // Start of find
-				console.gotoxy(1, console.screen_rows);
-				console.cleartoeol("\1n");
-				console.gotoxy(1, console.screen_rows);
-				var promptText = "Search: ";
-				console.print(promptText);
-				var searchText = getStrWithTimeout(K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE, console.screen_columns - promptText.length - 1, SEARCH_TIMEOUT_MS);
-				// If the user entered text, then do the search, and if found,
-				// found, go to the page and select the item indicated by the
-				// search.
-				if (searchText.length > 0)
-				{
-					var srchObj = getMsgAreaPageNumFromSearch(searchText, numItemsPerPage, true, 0, grpIndex);
-					if (srchObj.pageNum > 0)
-					{
-						lastSearchText = searchText;
-						lastSearchFoundIdx = srchObj.itemIdx;
-
-						// For screen refresh optimization, don't redraw the whole
-						// list if the result is on the same page
-						if (srchObj.pageTopIdx != topSubIndex)
-						{
-							topSubIndex = srchObj.pageTopIdx;
-							selectedSubIndex = srchObj.itemIdx;
-							bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
-							pageNum = srchObj.pageNum;
-							this.UpdateMsgAreaPageNumInHeader(pageNum, numPages, false, false);
-							this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true);
-						}
-						else
-						{
-							if (srchObj.itemIdx != selectedSubIndex)
-							{
-								var screenY = listStartRow + (selectedSubIndex - topSubIndex);
-								console.gotoxy(1, screenY);
-								this.WriteMsgGroupLine(selectedSubIndex, false);
-								selectedSubIndex = srchObj.itemIdx;
-								screenY = listStartRow + (selectedSubIndex - topSubIndex);
-								console.gotoxy(1, screenY);
-								this.WriteMsgGroupLine(selectedSubIndex, true);
-							}
-						}
-					}
-					else
-						this.WriteLightbarKeyHelpErrorMsg("Not found");
-				}
-				this.WriteChgMsgAreaKeysHelpLine();
-				break;
-			case 'N': // Next search result (requires an existing search term)
-				if ((lastSearchText.length > 0) && (lastSearchFoundIdx > -1))
+			// If choosing a message group, then let the user choose a
+			// sub-board within the group.  Otherwise, return the user's
+			// chosen sub-board.
+			if (chooseMsgGrp)
+			{
+				//SelectMsgArea_Lightbar(pMsgGrp, pGrpIdx)
+				// Show a "Loading..." text in case there are many sub-boards in
+				// the chosen message group
+				console.crlf();
+				console.print("\1nLoading...");
+				console.line_counter = 0; // To prevent a pause before the message list comes up
+				// Ensure that the sub-board printf information is created for
+				// the chosen message group.
+				this.BuildSubBoardPrintfInfoForGrp(chosenIdx);
+				var chosenSubBoardIdx = this.SelectMsgArea_Lightbar(false, chosenIdx);
+				if (chosenSubBoardIdx > -1)
 				{
-					// Do the search, and if found, go to the page and select the item
-					// indicated by the search.
-					var srchObj = getMsgAreaPageNumFromSearch(searchText, numItemsPerPage, true, lastSearchFoundIdx+1, grpIndex);
-					lastSearchFoundIdx = srchObj.itemIdx;
-					if (srchObj.pageNum > 0)
-					{
-						var foundItemRetObj = nextSubSearchFoundItem(grpIndex, srchObj, numPages, listStartRow, listEndRow, selectedSubIndex, topSubIndex, this);
-						if (foundItemRetObj.differentPage)
-						{
-							pageNum = srchObj.pageNum;
-							topSubIndex = foundItemRetObj.topSubIndex;
-							bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
-						}
-						selectedSubIndex = foundItemRetObj.selectedSubIndex;
-					}
-					else
-					{
-						// Not found - Wrap around and start at 0 again
-						var srchObj = getMsgAreaPageNumFromSearch(searchText, numItemsPerPage, true, 0, grpIndex);
-						lastSearchFoundIdx = srchObj.itemIdx;
-						var foundItemRetObj = nextSubSearchFoundItem(grpIndex, srchObj, numPages, listStartRow, listEndRow, selectedSubIndex, topSubIndex, this);
-						if (foundItemRetObj.selectedSubIndex != selectedSubIndex)
-						{
-							if (foundItemRetObj.differentPage)
-							{
-								pageNum = srchObj.pageNum;
-								topSubIndex = foundItemRetObj.topSubIndex;
-								bottomSubIndex = getBottommostSubIndex(topSubIndex, numItemsPerPage);
-							}
-							selectedSubIndex = foundItemRetObj.selectedSubIndex;
-						}
-						else
-						{
-							this.WriteLightbarKeyHelpErrorMsg("No others found");
-							this.WriteChgMsgAreaKeysHelpLine();
-						}
-					}
+					// Set the current group & sub-board
+					bbs.curgrp = chosenIdx;
+					bbs.cursub = chosenSubBoardIdx;
+					continueOn = false;
 				}
 				else
 				{
-					this.WriteLightbarKeyHelpErrorMsg("There is no previous search");
-					this.WriteChgMsgAreaKeysHelpLine();
-				}
-				break;
-			case 'Q': // Quit
-				continueChoosingSubBrd = false;
-				break;
-			case '?': // Show help
-				this.ShowChooseMsgAreaHelpScreen(true, true);
-				console.pause();
-				// Refresh the screen
-				this.DisplayAreaChgHdr(1);
-				//if (this.areaChangeHdrLines.length > 0)
-				//	console.crlf();
-				console.gotoxy(1, 1+this.areaChangeHdrLines.length);
-				this.WriteSubBrdListHdr1Line(grpIndex, numPages, pageNum);
-				console.cleartoeol("\1n");
-				this.WriteChgMsgAreaKeysHelpLine();
-				console.gotoxy(1, 2+this.areaChangeHdrLines.length);
-				printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
-				this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true);
-				break;
-			default:
-				// If the user entered a numeric digit, then treat it as
-				// the start of the message sub-board number.
-				if (userInput.match(/[0-9]/))
-				{
-					var originalCurpos = curpos;
-
-					// Put the user's input back in the input buffer to
-					// be used for getting the rest of the message number.
-					console.ungetstr(userInput);
-					// Move the cursor to the bottom of the screen and
-					// prompt the user for the message number.
-					console.gotoxy(1, console.screen_rows);
-					console.clearline("\1n");
-					console.print("\1cSub-board #: \1h");
-					userInput = console.getnum(msg_area.grp_list[grpIndex].sub_list.length);
-					// If the user made a selection, then set it in the
-					// return object and don't continue the input loop.
-					if (userInput > 0)
-					{
-						// Validate the sub-board choice.  If a search is specified, the
-						// validator function will search for messages in the selected
-						// sub-board and will return true if there are messages to read
-						// there or false if not.  If there is no search specified,
-						// the validator function will return a 'true' value.
-						var msgAreaValidRetval = this.ValidateMsgAreaChoice(grpIndex, selectedSubIndex, curpos);
-						if (msgAreaValidRetval.msgAreaGood)
-						{
-							continueChoosingSubBrd = false;
-							retObj.subBoardChosen = true;
-							retObj.subBoardIndex = userInput - 1;
-						}
-						else
-						{
-							// Output the error that was returned by the validator function
-							console.print("\1n\1y\1h" + msgAreaValidRetval.errorMsg);
-							mswait(ERROR_PAUSE_WAIT_MS);
-							// Set our loop variables so that we continue the sub-board
-							// choosing loop.
-							continueChoosingSubBrd = true;
-							retObj.subBoardChosen = false;
-							retObj.subBoardIndex = -1;
-							// Since the message area selection failed, we need to
-							// re-draw the screen due to everything being moved up one
-							// line.
-							console.gotoxy(1, 1);
-							this.DisplayAreaChgHdr(1);
-							if (this.areaChangeHdrLines.length > 0)
-								console.crlf();
-							this.WriteSubBrdListHdr1Line(grpIndex, numPages, pageNum);
-							console.cleartoeol("\1n");
-							this.WriteChgMsgAreaKeysHelpLine();
-							console.gotoxy(1, 2);
-							printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
-							this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true);
-						}
-					}
-					else
-					{
-						// The user didn't enter a selection.  Now we need to re-draw
-						// the screen due to everything being moved up one line.
-						console.gotoxy(1, 1);
-						this.DisplayAreaChgHdr(1);
-						if (this.areaChangeHdrLines.length > 0)
-							console.crlf();
-						this.WriteSubBrdListHdr1Line(grpIndex, numPages, pageNum);
-						console.cleartoeol("\1n");
-						this.WriteChgMsgAreaKeysHelpLine();
-						console.gotoxy(1, 2);
-						printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
-						this.ListScreenfulOfSubBrds(grpIndex, topSubIndex, listStartRow, listEndRow, false, true);
-					}
+					// A sub-board was not chosen, so we'll have to re-draw
+					// the header and list of message groups.
+					displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this);
 				}
-				break;
+			}
+			else
+				return chosenIdx; // Return the chosen sub-board index
 		}
 	}
 
-	return retObj;
+	// If the user chose a different message group & sub-board, then reset the
+	// lister index & cursor variables, as well as this.subBoardCode, etc.
+	if ((bbs.curgrp != oldGrp) || (bbs.cursub != oldSub))
+	{
+		this.tradListTopMsgIdx = -1;
+		this.lightbarListTopMsgIdx = -1;
+		this.lightbarListSelectedMsgIdx = -1;
+		this.lightbarListCurPos = null;
+		this.setSubBoardCode(msg_area.grp_list[bbs.curgrp].sub_list[bbs.cursub].code);
+	}
 }
 
 // For the DigDistMsgReader class: Lets the user choose a message group and
@@ -11403,7 +10777,7 @@ function DigDistMsgReader_SelectMsgArea_Traditional()
 function DigDistMsgReader_ListMsgGrps_Traditional(pSearchText)
 {
 	// Print the header
-	this.WriteGrpListHdrLine();
+	this.WriteGrpListHdrLine1();
 	console.print("\1n");
 
 	var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : "");
@@ -11462,7 +10836,7 @@ function DigDistMsgReader_ListSubBoardsInMsgGroup_Traditional(pGrpIndex, pMarkIn
 	this.BuildSubBoardPrintfInfoForGrp(grpIndex);
 
 	// Print the headers
-	this.WriteSubBrdListHdr1Line(grpIndex);
+	this.WriteSubBrdListHdrLine(grpIndex);
 	console.crlf();
 	printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
 	console.print("\1n");
@@ -11884,17 +11258,30 @@ function DigDistMsgReader_ListScreenfulOfSubBrds(pGrpIndex, pStartSubIndex,
 //  pHighlight: Boolean - Whether or not to write the line highlighted.
 function DigDistMsgReader_WriteMsgSubBrdLine(pGrpIndex, pSubIndex, pHighlight)
 {
-	console.print("\1n");
-	// Write the highlight background color if pHighlight is true.
-	if (pHighlight)
-		console.print(this.colors.areaChooserMsgAreaBkgHighlightColor);
+	console.print("\1n" + this.GetMsgSubBoardLine(pGrpIndex, pSubIndex, pHighlight));
+}
 
+// For the DigDistMsgReader class: Returns a formatted string with sub-board
+// information for the message area chooser functionality.
+//
+// Parameters:
+//  pGrpIndex: The index of the message group (assumed to be valid)
+//  pSubIndex: The index of the sub-board within the message group (assumed to be valid)
+//  pHighlight: Boolean - Whether or not to write the line highlighted.
+//
+// Return value: A string with the sub-board information
+function DigDistMsgReader_GetMsgSubBrdLine(pGrpIndex, pSubIndex, pHighlight)
+{
 	// Determine if pGrpIndex and pSubIndex specify the user's
 	// currently-selected group and sub-board.
 	var currentSub = false;
 	if ((typeof(bbs.curgrp) == "number") && (typeof(bbs.cursub) == "number"))
 		currentSub = ((pGrpIndex == msg_area.sub[this.subBoardCode].grp_index) && (pSubIndex == msg_area.sub[this.subBoardCode].index));
 
+	var subBoardStr = "";
+	// Use the highlight background color if pHighlight is true.
+	if (pHighlight)
+		subBoardStr += this.colors.areaChooserMsgAreaBkgHighlightColor;
 	// Open the current sub-board with the msgBase object (so that we can get
 	// the date & time of the last imporeted message).
 	var msgBase = new MsgBase(msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].code);
@@ -11939,15 +11326,16 @@ function DigDistMsgReader_WriteMsgSubBrdLine(pGrpIndex, pSubIndex, pHighlight)
 			newestDate.date = newestDate.time = "";
 
 		// Print the sub-board information line.
-		console.print(currentSub ? this.colors.areaChooserMsgAreaMarkColor + "*" : " ");
-		printf((pHighlight ? this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr : this.subBoardListPrintfInfo[pGrpIndex].printfStr),
-		       +(pSubIndex+1),
-		       msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].description.substr(0, this.subBoardListPrintfInfo[pGrpIndex].nameLen),
-		       numMsgs, newestDate.date, newestDate.time);
+		subBoardStr += (currentSub ? this.colors.areaChooserMsgAreaMarkColor + "*" : " ");
+		subBoardStr += format((pHighlight ? this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr : this.subBoardListPrintfInfo[pGrpIndex].printfStr),
+		                      +(pSubIndex+1),
+		                      msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].description.substr(0, this.subBoardListPrintfInfo[pGrpIndex].nameLen),
+		                      numMsgs, newestDate.date, newestDate.time);
 		msgBase.close();
 
 		delete msgBase;
 	}
+	return subBoardStr;
 }
 
 ///////////////////////////////////////////////
@@ -19961,6 +19349,77 @@ function getMsgAreaPageNumFromSearch(pText, pNumItemsPerPage, pSubBoard, pStartI
 	return retObj;
 }
 
+// Finds a message group index with search text, matching either the name or
+// description, case-insensitive.
+//
+// Parameters:
+//  pSearchText: The name/description text to look for
+//  pStartItemIdx: The item index to start at.  Defaults to 0
+//
+// Return value: The index of the message group, or -1 if not found
+function findMsgGrpIdxFromText(pSearchText, pStartItemIdx)
+{
+	if (typeof(pSearchText) != "string")
+		return -1;
+
+	var grpIdx = -1;
+
+	var startIdx = (typeof(pStartItemIdx) == "number" ? pStartItemIdx : 0);
+	if ((startIdx < 0) || (startIdx > msg_area.grp_list.length))
+		startIdx = 0;
+
+	// Go through the message group list and look for a match
+	var searchTextUpper = pSearchText.toUpperCase();
+	for (var i = startIdx; i < msg_area.grp_list.length; ++i)
+	{
+		if ((msg_area.grp_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) ||
+		    (msg_area.grp_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1))
+		{
+			grpIdx = i;
+			break;
+		}
+	}
+
+	return grpIdx;
+}
+
+// Finds a message group index with search text, matching either the name or
+// description, case-insensitive.
+//
+// Parameters:
+//  pGrpIdx: The index of the message group
+//  pSearchText: The name/description text to look for
+//  pStartItemIdx: The item index to start at.  Defaults to 0
+//
+// Return value: The index of the message group, or -1 if not found
+function findSubBoardIdxFromText(pGrpIdx, pSearchText, pStartItemIdx)
+{
+	if (typeof(pGrpIdx) != "number")
+		return -1;
+	if (typeof(pSearchText) != "string")
+		return -1;
+
+	var subBoardIdx = -1;
+
+	var startIdx = (typeof(pStartItemIdx) == "number" ? pStartItemIdx : 0);
+	if ((startIdx < 0) || (startIdx > msg_area.grp_list[pGrpIdx].sub_list.length))
+		startIdx = 0;
+
+	// Go through the message group list and look for a match
+	var searchTextUpper = pSearchText.toUpperCase();
+	for (var i = startIdx; i < msg_area.grp_list[pGrpIdx].sub_list.length; ++i)
+	{
+		if ((msg_area.grp_list[pGrpIdx].sub_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) ||
+		    (msg_area.grp_list[pGrpIdx].sub_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1))
+		{
+			subBoardIdx = i;
+			break;
+		}
+	}
+
+	return subBoardIdx;
+}
+
 // Searches for a @MSG_TO @-code in a string and inserts a color/attribute code
 // before the @-code in the string.
 //
diff --git a/xtrn/DDMsgReader/readme.txt b/xtrn/DDMsgReader/readme.txt
index 22b4d449e429fabb0bf996d17e6f5c2acf6793ca..11c0fd13c8007013edc93a75a7d208c50bc95f75 100644
--- a/xtrn/DDMsgReader/readme.txt
+++ b/xtrn/DDMsgReader/readme.txt
@@ -1,6 +1,6 @@
                       Digital Distortion Message Reader
-                                 Version 1.30
-                           Release date: 2020-04-07
+                                 Version 1.31
+                           Release date: 2020-04-13
 
                                      by
 
diff --git a/xtrn/DDMsgReader/revision_history.txt b/xtrn/DDMsgReader/revision_history.txt
index 8290bdc388f6c1b20174b65663e35ac80dbf3452..25e092c65ac74812a326f2ec73ea371ed6c450a0 100644
--- a/xtrn/DDMsgReader/revision_history.txt
+++ b/xtrn/DDMsgReader/revision_history.txt
@@ -5,6 +5,8 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
+1.31     2020-04-13   The area change feature now uses DDLightbarMenu.  There
+                      is no more internal lightbar code in this message reader.
 1.30     2020-04-07   The message list features now uses DDLightbarMenu rather
                       than the internal lightbar menu code.  Requires the
                       latest dd_lightbar_menu.js (in sbbs/exec/load).