diff --git a/xtrn/DDAreaChoosers/DDFileAreaChooser.cfg b/xtrn/DDAreaChoosers/DDFileAreaChooser.cfg
index 00a7aaeaecaf130710d50f44f3c1e0bd3ea8dec8..92ba5ac1e78ac0d0a3204bdf7f6762eec7fe2cdd 100644
--- a/xtrn/DDAreaChoosers/DDFileAreaChooser.cfg
+++ b/xtrn/DDAreaChoosers/DDFileAreaChooser.cfg
@@ -2,6 +2,15 @@
 useLightbarInterface=true
 areaChooserHdrFilenameBase=fileAreaChgHeader
 areaChooserHdrMaxLines=5
+; Whether or not to enable directory collapsing.  For
+; example, for directories in a library starting with
+; common text and a separator (specified below), the
+; common text will be the only one displayed, and when
+; the user selects it, a 3rd tier with the directories
+; after the separator will be shown
+useDirCollapsing=true
+; The separator to use for directory collapsing
+dirCollapseSeparator=:
 
 [COLORS]
 ; Area number
diff --git a/xtrn/DDAreaChoosers/DDFileAreaChooser.js b/xtrn/DDAreaChoosers/DDFileAreaChooser.js
index 7c1cd4bb68c6c1aa971050917fe16988d8e22e99..56d24f87cd7e83e142aee8f17209e5a89f79c0e9 100644
--- a/xtrn/DDAreaChoosers/DDFileAreaChooser.js
+++ b/xtrn/DDAreaChoosers/DDFileAreaChooser.js
@@ -1,3 +1,5 @@
+// $Id: $
+
 /* This is a script that lets the user choose a file area,
  * with either a lightbar or traditional user interface.
  *
@@ -11,6 +13,8 @@
  * 2020-04-19 Eric Oulashin   Version 1.20
  *                            For lightbar mode, it now uses DDLightbarMenu
  *                            instead of using internal lightbar code.
+ * 2022-01-15 Eric Oulashin   Version 1.21
+ *                            Added directory name collapsing
  */
 
 /* Command-line arguments:
@@ -21,8 +25,7 @@
                 then this file will just provide the DDFileAreaChooser class).
 */
 
-var requireFnExists = (typeof(require) === "function");
-if (requireFnExists)
+if (typeof(require) === "function")
 {
 	require("sbbsdefs.js", "K_NOCRLF");
 	require("dd_lightbar_menu.js", "DDLightbarMenu");
@@ -49,8 +52,8 @@ if (system.version_num < 31400)
 }
 
 // Version & date variables
-var DD_FILE_AREA_CHOOSER_VERSION = "1.20";
-var DD_FILE_AREA_CHOOSER_VER_DATE = "2020-04-19";
+var DD_FILE_AREA_CHOOSER_VERSION = "1.21";
+var DD_FILE_AREA_CHOOSER_VER_DATE = "2022-01-15";
 
 // Keyboard input key codes
 var CTRL_H = "\x08";
@@ -154,30 +157,50 @@ function DDFileAreaChooser()
 	this.areaChooserHdrFilenameBase = "fileAreaChgHeader";
 	this.areaChooserHdrMaxLines = 5;
 
-	// Set the function pointers for the object
+	// Whether or not to enable directory collapsing.  For
+	// example, for directories in a library starting with
+	// common text and a separator (specified below), the
+	// common text will be the only one displayed, and when
+	// the user selects it, a 3rd tier with the directories
+	// after the separator will be shown
+	this.useDirCollapsing = true;
+	// The separator character to use for directory collapsing
+	this.dirCollapseSeparator = ":";
+	// If useDirCollapsing is true, then lib_list will be populated
+	// with some information from Synchronet's file_area.lib_list,
+	// including a sub_list for each library.  The sub_list arrays
+	// could have one that was collapsed from multiple sub-boards
+	// set up in the BBS - The sub_list within that one would then
+	// contain multiple directories split based on the dir collapse
+	// separator.
+	this.lib_list = [];
+
+	// Set the functions for the object
 	this.ReadConfigFile = DDFileAreaChooser_ReadConfigFile;
-	this.SelectFileArea = DDFileAreaChooser_selectFileArea;
-	this.SelectFileArea_Traditional = DDFileAreaChooser_selectFileArea_Traditional;
-	this.SelectDirWithinFileLib_Traditional = DDFileAreaChooser_selectDirWithinFileLib_Traditional;
-	this.ListFileLibs = DDFileAreaChooser_listFileLibs_Traditional;
-	this.ListDirsInFileLib_Traditional = DDFileAreaChooser_listDirsInFileLib_Traditional;
-	this.WriteLibListHdrLine = DDFileAreaChooser_writeLibListTopHdrLine;
-	this.WriteDirListHdr1Line = DDFileAreaChooser_writeDirListHdr1Line;
+	this.SelectFileArea = DDFileAreaChooser_SelectFileArea;
+	this.SelectFileArea_Traditional = DDFileAreaChooser_SelectFileArea_Traditional;
+	this.SelectDirWithinFileLib_Traditional = DDFileAreaChooser_SelectDirWithinFileLib_Traditional;
+	this.SelectSubdirWithinDir_Traditional = DDFileAreaChooser_SelectSubdirWithinDir_Traditional;
+	this.ListFileLibs_Traditional = DDFileAreaChooser_ListFileLibs_Traditional;
+	this.ListDirsInFileLib_Traditional = DDFileAreaChooser_ListDirsInFileLib_Traditional;
+	this.ListSubdirsInFileDir_Traditional = DDFileAreaChooser_ListSubdirsInFileDir_Traditional;
+	this.WriteLibListHdrLine = DDFileAreaChooser_WriteLibListTopHdrLine;
+	this.WriteDirListHdr1Line = DDFileAreaChooser_WriteDirListHdr1Line;
 	// Lightbar-specific functions
-	this.SelectFileArea_Lightbar = DDFileAreaChooser_selectFileArea_Lightbar;
+	this.SelectFileArea_Lightbar = DDFileAreaChooser_SelectFileArea_Lightbar;
 	this.CreateLightbarFileLibMenu = DDFileAreaChooser_CreateLightbarFileLibMenu;
 	this.CreateLightbarFileDirMenu = DDFileAreaChooser_CreateLightbarFileDirMenu;
 	this.WriteKeyHelpLine = DDFileAreaChooser_writeKeyHelpLine;
-	this.updatePageNumInHeader = DDFileAreaChooser_updatePageNumInHeader;
 	// Help screen
 	this.ShowHelpScreen = DDFileAreaChooser_showHelpScreen;
 	// Misc. functions
-	this.NumFilesInDir = DDFileAreaChooser_NumFilesInDir;
 	// Function to build the directory printf information for a file lib
 	this.BuildFileDirPrintfInfoForLib = DDFileAreaChooser_buildFileDirPrintfInfoForLib;
 	// Function to display the header above the area list
 	this.DisplayAreaChgHdr = DDFileAreaChooser_DisplayAreaChgHdr;
 	this.WriteLightbarKeyHelpErrorMsg = DDFileAreaChooser_WriteLightbarKeyHelpErrorMsg;
+	this.SetUpLibListWithCollapsedDirs = DDFileAreaChooser_SetUpLibListWithCollapsedDirs;
+	this.GetGreatestNumFiles = DDFileAreaChooser_GetGreatestNumFiles;
 
 	// Read the settings from the config file.
 	this.ReadConfigFile();
@@ -284,22 +307,32 @@ function DDFileAreaChooser()
 //  pChooseLib: Boolean - Whether or not to choose the file library.  If false,
 //              then this will allow choosing a directory within the user's
 //              current file library.  This is optional; defaults to true.
-function DDFileAreaChooser_selectFileArea(pChooseLib)
+function DDFileAreaChooser_SelectFileArea(pChooseLib)
 {
+	// If file directory collapsing is enabled, then set up
+	// this.lib_list.
+	if (this.useDirCollapsing)
+		this.SetUpLibListWithCollapsedDirs();
+
 	if (this.useLightbarInterface && console.term_supports(USER_ANSI))
-		this.SelectFileArea_Lightbar(pChooseLib);
+		this.SelectFileArea_Lightbar(pChooseLib ? 1 : 2); // TODO: Fix for levels everywhere
 	else
-		this.SelectFileArea_Traditional(pChooseLib);
+		this.SelectFileArea_Traditional(pChooseLib ? 1 : 2); // TODO: Fix for levels everywhere
 }
 
 // For the DDFileAreaChooser class: Traditional user interface for
 // letting the user choose a file area
 //
 // Parameters:
-//  pChooseLib: Boolean - Whether or not to choose the file library.  If false,
-//              then this will allow choosing a directory within the user's
-//              current file library.  This is optional; defaults to true.
-function DDFileAreaChooser_selectFileArea_Traditional(pChooseLib)
+//  pLevel: The file heirarchy level:
+//          1: File libraries
+//          2: File directories within libraries
+//          3: File subdirectories within directories (if directory name collapsing is enabled)
+//          This is optional and defaults to 1.
+//  pLibIdx: Optional - The file library index, if choosing a file directory
+//  pDirIdx: Optional - The file directory index (within a library), for use with
+//           directory name collapsing
+function DDFileAreaChooser_SelectFileArea_Traditional(pLevel, pLibIdx, pDirIdx)
 {
 	// If there are no file libraries, then don't let the user
 	// choose one.
@@ -318,14 +351,13 @@ function DDFileAreaChooser_selectFileArea_Traditional(pChooseLib)
 		curDirIdx = file_area.dir[bbs.curdir_code].index;
 	}
 
-	var chooseLib = (typeof(pChooseLib) == "boolean" ? pChooseLib : true);
-	if (chooseLib)
+	var continueChooseFileLib = true;
+	if (pLevel == 1) // Choose library
 	{
 		// Show the file libraries & directories and let the user choose one.
-		var selectedLib = 0; // The user's selected file library
-		var selectedDir = 0; // The user's selected file directory
+		var selectedLibNum = 0; // The user's selected file library
+		var selectedDirNum = 0; // The user's selected file directory
 		var libSearchText = "";
-		var continueChooseFileLib = true;
 		while (continueChooseFileLib)
 		{
 			// Clear the BBS command string to make sure there are no extra
@@ -336,22 +368,22 @@ function DDFileAreaChooser_selectFileArea_Traditional(pChooseLib)
 			this.DisplayAreaChgHdr(1);
 			if (this.areaChangeHdrLines.length > 0)
 				console.crlf();
-			this.ListFileLibs(libSearchText);
+			this.ListFileLibs_Traditional(libSearchText);
 			console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c, or [\1h" + +(curLibIdx+1) + "\1n\1c]:\1h ");
 			// Accept Q (quit) or a file library number
-			selectedLib = console.getkeys("QN/" + CTRL_F, file_area.lib_list.length);
+			selectedLibNum = console.getkeys("QN/" + CTRL_F, file_area.lib_list.length);
 
-			// If the user just pressed enter (selectedLib would be blank),
+			// If the user just pressed enter (selectedLibNum would be blank),
 			// default to the current library.
-			if (selectedLib.toString() == "")
-				selectedLib = curLibIdx + 1;
+			if (selectedLibNum.toString() == "")
+				selectedLibNum = curLibIdx + 1;
 
 			// If the user chose to quit, then set continueChooseFileLib to
 			// false so we'll exit the loop.  Otherwise, let the user chose
 			// a dir within the library.
-			if (selectedLib.toString() == "Q")
+			if (selectedLibNum.toString() == "Q")
 				continueChooseFileLib = false;
-			else if ((selectedLib.toString() == "/") || (selectedLib.toString() == CTRL_F))
+			else if ((selectedLibNum.toString() == "/") || (selectedLibNum.toString() == CTRL_F))
 			{
 				console.crlf();
 				var searchPromptText = "\1n\1c\1hSearch\1g: \1n";
@@ -362,72 +394,252 @@ function DDFileAreaChooser_selectFileArea_Traditional(pChooseLib)
 			}
 			else
 			{
-				libSearchText = "";
-
-				if (selectedLib-1 == curLibIdx)
-					selectedDir = curDirIdx + 1;
-				else
-					selectedDir = 1;
-				continueChooseFileLib = !this.SelectDirWithinFileLib_Traditional(selectedLib, selectedDir);
+				continueChooseFileLib = this.SelectFileArea_Traditional(2, selectedLibNum-1);
 			}
 		}
 	}
-	else
+	else if (pLevel == 2) // Choose a directory within a library
 	{
 		// Don't choose a library, just a directory within the user's current library.
-		this.SelectDirWithinFileLib_Traditional(curLibIdx, curDirIdx);
+		var selectDirRetObj = this.SelectDirWithinFileLib_Traditional(pLibIdx, curDirIdx);
+		continueChooseFileLib = !selectDirRetObj.areaSelected;
+		if (selectDirRetObj.areaSelected)
+		{
+			var selectedDirIdx = selectDirRetObj.dirIdx;
+			if (this.useDirCollapsing && (this.lib_list[pLibIdx].dir_list[selectedDirIdx].subdir_list.length > 0))
+				continueChooseFileLib = this.SelectFileArea_Traditional(3, pLibIdx, selectedDirIdx);
+			else if (selectDirRetObj.dirCode.length > 0)
+				bbs.curdir_code = selectDirRetObj.dirCode;
+			else
+				continueChooseFileLib = true;
+		}
+	}
+	else if ((pLevel == 3) && this.useDirCollapsing) // Choose a subdirectory within a directory
+	{
+		if (this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length > 0)
+		{
+			var selectSubdirRetObj = this.SelectSubdirWithinDir_Traditional(pLibIdx, pDirIdx);
+			continueChooseFileLib = !selectSubdirRetObj.areaSelected;
+			if (selectSubdirRetObj.areaSelected)
+				bbs.curdir_code = selectSubdirRetObj.dirCode;
+			else
+				continueChooseFileLib = true;
+		}
 	}
+	return continueChooseFileLib; // For recursive calls to this function
 }
 
 // For the DDFileAreaChooser class: Lets the user select a file area (directory)
 // within a specified file library - Traditional user interface.
 //
 // Parameters:
-//  pLibNumber: The file library number
-//  pSelectedDir: The currently-selected file directory
+//  pLibIdx: The file library index
+//  pSelectedDirIdx: The currently-selected file directory index
 //
-// Return value: Boolean - Whether or not the user chose a file area.
-function DDFileAreaChooser_selectDirWithinFileLib_Traditional(pLibNumber, pSelectedDir)
+// Return value: An object containing the following properties:
+//               areaSelected: Boolean - Whether or not the user chose a file area.
+//               dirIdx: The index of the directory chosen in the file library (or -1 if none chosen)
+//               dirCode: The internal code of the directory chosen, if valid.  If not valid
+//                        (i.e., there are subdirectories), this will be an empty string.
+function DDFileAreaChooser_SelectDirWithinFileLib_Traditional(pLibIdx, pSelectedDirIdx)
 {
-	// If pLibNumber is invalid, then just return false.
-	if (pLibNumber <= 0)
-		return false;
-
-	var userChoseAnArea = false;
-	var libIdx = pLibNumber - 1;
+	var retObj = {
+		areaSelected: false,
+		dirIdx: -1,
+		dirCode: ""
+	};
 
 	// If there are no file directories in the given file libraary, then show
 	// an error and return.
-	if (file_area.lib_list[libIdx].dir_list.length == 0)
+	if (file_area.lib_list[pLibIdx].dir_list.length == 0)
 	{
 		console.clear("\1n");
 		console.print("\1y\1hThere are no directories in this library.\r\n\1p");
-		return false;
+		return retObj;
+	}
+
+	// If pLibIdx is invalid, then just return false.
+	if (pLibIdx < 0)
+		return retObj;
+	else
+	{
+		if (this.useDirCollapsing && (this.lib_list.length > 0))
+		{
+			if (pLibIdx >= this.lib_list.length)
+				return retObj;
+		}
+		else if (pLibIdx >= file_area.lib_list[pLibIdx].dir_list.length)
+			return retObj;
 	}
 
 	// Ensure that the file directory printf information is created for
 	// this file library.
-	this.BuildFileDirPrintfInfoForLib(libIdx);
+	this.BuildFileDirPrintfInfoForLib(pLibIdx);
 
 	// Set the default directory #: The current directory, or if the
 	// user chose a different file library, then this should be set
 	// to the first directory.
-	function getDefaultDir(pDir)
+	function getDefaultDirNum(pDir, pUseDirCollapsing, pLibList)
+	{
+		var defaultDirNum = 0;
+		if (pUseDirCollapsing && (pLibList.length > 0))
+		{
+			if (typeof(bbs.curdir_code) == "string")
+			{
+				for (var dirIdx = 0; dirIdx < pLibList[pLibIdx].dir_list.length; ++dirIdx)
+				{
+					if (pLibList[pLibIdx].dir_list[dirIdx].subdir_list.length > 0)
+					{
+						for (var subdirIdx = 0; subdirIdx < pLibList[pLibIdx].dir_list[dirIdx].subdir_list.length; ++subdirIdx)
+						{
+							if (bbs.curdir_code == pLibList[pLibIdx].dir_list[dirIdx].subdir_list[subdirIdx].code)
+							{
+								defaultDirNum = dirIdx + 1;
+								break;
+							}
+						}
+					}
+					else
+					{
+						if (bbs.curdir_code == pLibList[pLibIdx].dir_list[dirIdx].code)
+						{
+							defaultDirNum = dirIdx + 1;
+							break;
+						}
+					}
+				}
+			}
+		}
+		else
+		{
+			if (typeof(pDir) == "number")
+				defaultDirNum = pDir;
+			else if (typeof(bbs.curdir_code) == "string")
+			{
+				if (pLibIdx != file_area.dir[bbs.curdir_code].lib_index)
+					defaultDirNum = 1;
+				else
+					defaultDirNum = file_area.dir[bbs.curdir_code].index + 1;
+			}
+		}
+		return defaultDirNum;
+	}
+
+	var defaultDirNum = getDefaultDirNum(pSelectedDirIdx, this.useDirCollapsing, this.lib_list);
+	var searchText = "";
+	var numDirsListed = 0;
+	var continueOn = false;
+	do
 	{
-		var defaultDir = 0;
-		if (typeof(pDir) == "number")
-			defaultDir = pDir;
-		else if (typeof(bbs.curdir_code) == "string")
+		console.clear("\1n");
+		this.DisplayAreaChgHdr(1);
+		if (this.areaChangeHdrLines.length > 0)
+			console.crlf();
+		numDirsListed = this.ListDirsInFileLib_Traditional(pLibIdx, defaultDirNum - 1, searchText);
+		if ((numDirsListed > 0) && (typeof(pSelectedDirIdx) == "number") && (pSelectedDirIdx >= 0) && (pSelectedDirIdx < numDirsListed))
+			defaultDirNum = getDefaultDirNum(pSelectedDirIdx, this.useDirCollapsing, this.lib_list);
+		if (defaultDirNum >= 1)
+			console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c, or [\1h" + defaultDirNum + "\1n\1c]: \1h");
+		else
+			console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c: \1h");
+		// Accept Q (quit), / or CTRL_F to search, or a file directory number
+		var selectedDirNum = console.getkeys("Q/" + CTRL_F, file_area.lib_list[pLibIdx].dir_list.length);
+
+		// If the user just pressed enter (selectedDirNum would be blank),
+		// default the selected directory.
+		if (selectedDirNum.toString() == "Q")
+			continueOn = false;
+		else if (selectedDirNum.toString() == "")
+			selectedDirNum = defaultDirNum;
+		else if ((selectedDirNum == "/") || (selectedDirNum == CTRL_F))
+		{
+			// Search
+			console.crlf();
+			var searchPromptText = "\1n\1c\1hSearch\1g: \1n";
+			console.print(searchPromptText);
+			searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE);
+			console.print("\1n");
+			console.crlf();
+			if (searchText.length > 0)
+				defaultDirNum = -1;
+			else
+				defaultDirNum = getDefaultDirNum(pSelectedDirIdx, this.useDirCollapsing, this.lib_list);
+			continueOn = true;
+			console.line_counter = 0; // To avoid pausing before the clear screen
+		}
+
+		// If the user chose a directory, then set the user's file directory.
+		if (selectedDirNum > 0)
 		{
-			if (libIdx != file_area.dir[bbs.curdir_code].lib_index)
-				defaultDir = 1;
+			continueOn = false;
+			retObj.areaSelected = true;
+			retObj.dirIdx = selectedDirNum - 1;
+			if (this.useDirCollapsing)
+			{
+				if (this.lib_list[pLibIdx].dir_list[retObj.dirIdx].subdir_list.length == 0)
+					retObj.dirCode = this.lib_list[pLibIdx].dir_list[retObj.dirIdx].code;
+			}
 			else
-				defaultDir = file_area.dir[bbs.curdir_code].index;
+				retObj.dirCode = file_area.lib_list[pLibIdx].dir_list[retObj.dirIdx].code;
+		}
+	} while (continueOn);
+
+	return retObj;
+}
+
+// For the DDFileAreaChooser class: Lets the user select a subdirectory within a
+// file directory - Traditional user interface.  This is meant for directory name
+// collapsing, at the 3rd level.
+//
+// Parameters:
+//  pLibIdx: The file library index
+//  pDirIdx: The index of the directory within the file library
+//
+// Return value: An object containing the following properties:
+//               areaSelected: Boolean - Whether or not the user chose a file area.
+//               dirCode: The internal code of the directory chosen, if chose.  If not chosen,
+//                        this will be an empty string.
+function DDFileAreaChooser_SelectSubdirWithinDir_Traditional(pLibIdx, pDirIdx)
+{
+	var retObj = {
+		areaSelected: false,
+		dirCode: ""
+	};
+
+	if (!this.useDirCollapsing || this.lib_list.length == 0)
+		return retObj;
+	if ((pLibIdx < 0) || (pLibIdx >= this.lib_list.length))
+		return retObj;
+	if ((pDirIdx < 0) || (pDirIdx >= this.lib_list[pLibIdx].dir_list.length))
+	{
+		console.clear("\1n");
+		console.print("\1y\1hThere are no directories in this library.\r\n\1p");
+		return retObj;
+	}
+	if (this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length == 0)
+	{
+		console.clear("\1n");
+		console.print("\1y\1hThere are no subdirectories in this directory.\r\n\1p");
+		return retObj;
+	}
+
+	// Gets the default directory number (1-based)
+	function getDefaultSubdirNum(pLibList, pLibIdx, pDirIdx)
+	{
+		var subdirNum = 0; // Will be 1-based
+		for (var subdirIdx = 0; subdirIdx < pLibList[pLibIdx].dir_list[pDirIdx].subdir_list.length; ++subdirIdx)
+		{
+			if (bbs.curdir_code == pLibList[pLibIdx].dir_list[pDirIdx].subdir_list[subdirIdx].code)
+			{
+				subdirNum = subdirIdx + 1;
+				break;
+			}
 		}
-		return defaultDir;
+		return subdirNum;
 	}
 
-	var defaultDir = getDefaultDir(pSelectedDir);
+	// defaultSubdirNum is the default subdirectory # (will be 1-based)
+	var defaultSubdirNum = getDefaultSubdirNum(this.lib_list, pLibIdx, pDirIdx);
 	var searchText = "";
 	var numDirsListed = 0;
 	var continueOn = false;
@@ -437,23 +649,21 @@ function DDFileAreaChooser_selectDirWithinFileLib_Traditional(pLibNumber, pSelec
 		this.DisplayAreaChgHdr(1);
 		if (this.areaChangeHdrLines.length > 0)
 			console.crlf();
-		numDirsListed = this.ListDirsInFileLib_Traditional(libIdx, defaultDir - 1, searchText);
-		if ((numDirsListed > 0) && (typeof(pSelectedDir) == "number") && (pSelectedDir >= 1) && (pSelectedDir <= numDirsListed))
-			defaultDir = getDefaultDir(pSelectedDir);
-		if (defaultDir >= 1)
-			console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c, or [\1h" + defaultDir + "\1n\1c]: \1h");
+		numDirsListed = this.ListSubdirsInFileDir_Traditional(pLibIdx, pDirIdx, searchText);
+		if (defaultSubdirNum >= 1)
+			console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c, or [\1h" + defaultSubdirNum + "\1n\1c]: \1h");
 		else
 			console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c: \1h");
 		// Accept Q (quit), / or CTRL_F to search, or a file directory number
-		var selectedDir = console.getkeys("Q/" + CTRL_F, file_area.lib_list[libIdx].dir_list.length);
+		var selectedSubdirNum = console.getkeys("Q/" + CTRL_F, this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length);
 
-		// If the user just pressed enter (selectedDir would be blank),
+		// If the user just pressed enter (selectedSubdirNum would be blank),
 		// default the selected directory.
-		if (selectedDir.toString() == "Q")
+		if (selectedSubdirNum.toString() == "Q")
 			continueOn = false;
-		else if (selectedDir.toString() == "")
-			selectedDir = defaultDir;
-		else if ((selectedDir == "/") || (selectedDir == CTRL_F))
+		else if (selectedSubdirNum.toString() == "")
+			selectedSubdirNum = defaultSubdirNum;
+		else if ((selectedSubdirNum == "/") || (selectedSubdirNum == CTRL_F))
 		{
 			// Search
 			console.crlf();
@@ -463,23 +673,23 @@ function DDFileAreaChooser_selectDirWithinFileLib_Traditional(pLibNumber, pSelec
 			console.print("\1n");
 			console.crlf();
 			if (searchText.length > 0)
-				defaultDir = -1;
+				defaultSubdirNum = -1;
 			else
-				defaultDir = getDefaultDir(pSelectedDir);
+				defaultSubdirNum = getDefaultSubdirNum(this.lib_list, pLibIdx, pDirIdx);
 			continueOn = true;
 			console.line_counter = 0; // To avoid pausing before the clear screen
 		}
 
 		// If the user chose a directory, then set the user's file directory.
-		if (selectedDir > 0)
+		if (selectedSubdirNum > 0)
 		{
 			continueOn = false;
-			bbs.curdir_code = file_area.lib_list[libIdx].dir_list[selectedDir-1].code;
-			userChoseAnArea = true;
+			retObj.areaSelected = true;
+			retObj.dirCode = this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list[selectedSubdirNum-1].code;
 		}
 	} while (continueOn);
 
-	return userChoseAnArea;
+	return retObj;
 }
 
 // For the DDFileAreaChooser class: Traditional user interface for listing
@@ -490,7 +700,7 @@ function DDFileAreaChooser_selectDirWithinFileLib_Traditional(pLibNumber, pSelec
 //               If blank or not a string, all will be displayed.
 //
 // Return value: The number of directories listed
-function DDFileAreaChooser_listFileLibs_Traditional(pSearchText)
+function DDFileAreaChooser_ListFileLibs_Traditional(pSearchText)
 {
 	var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : "");
 
@@ -502,10 +712,11 @@ function DDFileAreaChooser_listFileLibs_Traditional(pSearchText)
 	var numDirsListed = 0;
 	var printIt = true;
 	var currentDir = false;
-	for (var i = 0; i < file_area.lib_list.length; ++i)
+	var lib_list = (this.useDirCollapsing ? this.lib_list : file_area.lib_list);
+	for (var i = 0; i < lib_list.length; ++i)
 	{
 		if (searchText.length > 0)
-			printIt = ((file_area.lib_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (file_area.lib_list[i].description.toUpperCase().indexOf(searchText) >= 0));
+			printIt = ((lib_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (lib_list[i].description.toUpperCase().indexOf(searchText) >= 0));
 		else
 			printIt = true;
 
@@ -517,9 +728,8 @@ function DDFileAreaChooser_listFileLibs_Traditional(pSearchText)
 			if (typeof(bbs.curdir_code) == "string")
 				curLibIdx = file_area.dir[bbs.curdir_code].lib_index;
 			console.print(i == curLibIdx ? this.colors.areaMark + "*" : " ");
-			printf(this.fileLibPrintfStr, +(i+1),
-			       file_area.lib_list[i].description.substr(0, this.descFieldLen),
-			       file_area.lib_list[i].dir_list.length);
+			printf(this.fileLibPrintfStr, +(i+1), lib_list[i].description.substr(0, this.descFieldLen),
+			       lib_list[i].dir_list.length);
 			console.crlf();
 		}
 	}
@@ -536,7 +746,7 @@ function DDFileAreaChooser_listFileLibs_Traditional(pSearchText)
 //  pSearchText: Text to search for in the file directories (blank or none to list all)
 //
 // Return value: The number of file directories listed
-function DDFileAreaChooser_listDirsInFileLib_Traditional(pLibIndex, pMarkIndex, pSearchText)
+function DDFileAreaChooser_ListDirsInFileLib_Traditional(pLibIndex, pMarkIndex, pSearchText)
 {
 	// Set libIndex, the library index
 	var libIndex = 0;
@@ -563,15 +773,16 @@ function DDFileAreaChooser_listDirsInFileLib_Traditional(pLibIndex, pMarkIndex,
 	console.print(this.colors.fileAreaHdr + "Directories of \1h" +
 	              file_area.lib_list[libIndex].description);
 	console.crlf();
-	printf(this.fileDirHdrPrintfStr, "Dir #", "Description", "# Files");
+	printf(this.fileDirHdrPrintfStr, "Dir #", "Description", "# Items");
 	console.crlf();
 	console.print("\1n");
 	var numDirsListed = 0;
 	var printIt = true;
-	for (var i = 0; i < file_area.lib_list[libIndex].dir_list.length; ++i)
+	var lib_list = (this.useDirCollapsing ? this.lib_list : file_area.lib_list);
+	for (var i = 0; i < lib_list[libIndex].dir_list.length; ++i)
 	{
 		if (searchText.length > 0)
-			printIt = ((file_area.lib_list[libIndex].dir_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (file_area.lib_list[libIndex].dir_list[i].description.toUpperCase().indexOf(searchText) >= 0));
+			printIt = ((lib_list[libIndex].dir_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (lib_list[libIndex].dir_list[i].description.toUpperCase().indexOf(searchText) >= 0));
 		else
 			printIt = true;
 		if (printIt)
@@ -579,12 +790,96 @@ function DDFileAreaChooser_listDirsInFileLib_Traditional(pLibIndex, pMarkIndex,
 			++numDirsListed;
 			// See if this is the currently-selected directory.
 			console.print(markIndex > -1 && i == markIndex ? this.colors.areaMark + "*" : " ");
+			var dirDesc = lib_list[libIndex].dir_list[i].description;
+			var numItems = this.fileDirListPrintfInfo[libIndex].fileCounts[i];
+			// For directory name collapsing, get subdirectory information for this directory, and
+			// append <subdirs> to the description if the directory has subdirectories.
+			if (this.useDirCollapsing)
+			{
+				if (lib_list[libIndex].dir_list[i].subdir_list.length > 0)
+				{
+					numItems = lib_list[libIndex].dir_list[i].subdir_list.length;
+					dirDesc += "  <subdirs>";
+				}
+				else
+					numItems = this.fileDirListPrintfInfo[libIndex].fileCounts[i];
+			}
+			// Print the directory information
+			printf(this.fileDirListPrintfInfo[libIndex].printfStr, +(i+1),
+				   dirDesc.substr(0, this.descFieldLen), numItems);
+			console.crlf();
+		}
+	}
+	return numDirsListed;
+}
+
+// For the DDFileAreaChooser class: For directory name collapsing, this lists
+// subdirectories in a directory, for the traditional user interface.
+//
+// Parameters:
+//  pLibIndex: The index of the file library (0-based)
+//  pDirIndex: The index of the file directory in the library (0-based)
+//  pSearchText: Text to search for in the file directories (blank or none to list all)
+//
+// Return value: The number of file subdirectories listed
+function DDFileAreaChooser_ListSubdirsInFileDir_Traditional(pLibIndex, pDirIndex, pSearchText)
+{
+	// This is only for directory collapsing, to display subdirectories in a file directory
+	if (!this.useDirCollapsing || (this.lib_list.length == 0))
+		return 0;
+
+	// Set libIndex (the library index)
+	var libIndex = 0;
+	if (typeof(pLibIndex) == "number")
+		libIndex = pLibIndex;
+	else if (typeof(bbs.curdir_code) == "string")
+		libIndex = file_area.dir[bbs.curdir_code].lib_index;
+
+	// Sanity checking
+	if ((libIndex < 0) || (libIndex >= this.lib_list.length))
+		return 0;
+	if ((pDirIndex < 0) || (pDirIndex >= this.lib_list[libIndex].length))
+		return 0;
+	if (this.lib_list[libIndex].dir_list[pDirIndex].subdir_list.length == 0)
+		return 0;
+
+	var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : "");
+
+	// Ensure that the file directory printf information is created for
+	// this file library.
+	this.BuildFileDirPrintfInfoForLib(libIndex);
+
+	// Print the header lines
+	console.print(this.colors.fileAreaHdr + "Directories of \1h" +
+	              file_area.lib_list[libIndex].description);
+	console.crlf();
+	printf(this.fileDirHdrPrintfStr, "Dir #", "Description", "# Items");
+	console.crlf();
+	console.print("\1n");
+	var numDirsListed = 0;
+	var printIt = true;
+	for (var i = 0; i < this.lib_list[libIndex].dir_list[pDirIndex].subdir_list.length; ++i)
+	{
+		if (searchText.length > 0)
+			printIt = ((this.lib_list[libIndex].dir_list[pDirIndex].subdir_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (this.lib_list[libIndex].dir_list[i].description.toUpperCase().indexOf(searchText) >= 0));
+		else
+			printIt = true;
+		if (printIt)
+		{
+			++numDirsListed;
+			// See if this is the currently-selected directory.
+			var displayAreaMark = (bbs.curdir_code == this.lib_list[libIndex].dir_list[pDirIndex].subdir_list[i].code);
+			console.print(displayAreaMark ? this.colors.areaMark + "*" : " ");
+			var dirIdx = this.lib_list[libIndex].dir_list[pDirIndex].subdir_list[i].index;
+			var numFiles = this.fileDirListPrintfInfo[libIndex].fileCounts[dirIdx];
+			var subdirDesc = this.lib_list[libIndex].dir_list[pDirIndex].subdir_list[i].description;
+			// Print the directory information
 			printf(this.fileDirListPrintfInfo[libIndex].printfStr, +(i+1),
-			       file_area.lib_list[libIndex].dir_list[i].description.substr(0, this.descFieldLen),
-			       this.fileDirListPrintfInfo[libIndex].fileCounts[i]);
+				   subdirDesc.substr(0, this.descFieldLen), numFiles);
 			console.crlf();
 		}
 	}
+
 	return numDirsListed;
 }
 
@@ -596,7 +891,7 @@ function DDFileAreaChooser_listDirsInFileLib_Traditional(pLibIndex, pMarkIndex,
 //             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 DDFileAreaChooser_writeLibListTopHdrLine(pNumPages, pPageNum)
+function DDFileAreaChooser_WriteLibListTopHdrLine(pNumPages, pPageNum)
 {
 	var descStr = "Description";
 	if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
@@ -613,24 +908,42 @@ function DDFileAreaChooser_writeLibListTopHdrLine(pNumPages, pPageNum)
 // above the directory list for a file library.
 //
 // Parameters:
-//  pLibIndex: The index of the file library (assumed to be valid)
+//  pLibIdx: The index of the file library (assumed to be valid)
+//  pDirIdx: The directory index, if directory collapsing is enabled and
+//           we need to write the header line for subdirectories within a
+//           directory.  This can be negative if not needed.
 //  pNumPages: The number of pages (a number).  This is optional; if this is
 //             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 DDFileAreaChooser_writeDirListHdr1Line(pLibIndex, pNumPages, pPageNum)
+function DDFileAreaChooser_WriteDirListHdr1Line(pLibIdx, pDirIdx, pNumPages, pPageNum)
 {
-  var descLen = 40;
-  var descFormatStr = this.colors.fileAreaHdr + "Directories of \1h%-" + descLen + "s     \1n"
-                    + this.colors.fileAreaHdr;
-  if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
-    descFormatStr += "(Page " + pPageNum + " of " + pNumPages + ")";
-  else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
-    descFormatStr += "(Page " + pPageNum + ")";
-  else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
-    descFormatStr += "(" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
-  printf(descFormatStr, file_area.lib_list[pLibIndex].description.substr(0, descLen));
-  console.cleartoeol("\1n");
+	var descLen = 40;
+	var descFormatStr = this.colors.fileAreaHdr + "Directories of \1h%-" + descLen + "s     \1n"
+	                  + this.colors.fileAreaHdr;
+	if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
+		descFormatStr += "(Page " + pPageNum + " of " + pNumPages + ")";
+	else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
+		descFormatStr += "(Page " + pPageNum + ")";
+	else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
+		descFormatStr += "(" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
+	// If using subdirectory collapsing, then build the description as needed.  Otherwise,
+	// just use the subdirectory description.
+	var desc = "";
+	if (this.useDirCollapsing)
+	{
+		// Ensure this.lib_list is set up
+		this.SetUpLibListWithCollapsedDirs();
+		// The description should be the library's description.  Also, if pDirIdx
+		// is a number, then append the directory description to the description.
+		desc = this.lib_list[pLibIdx].description;
+		if ((typeof(pDirIdx) === "number") && (this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length > 0))
+			desc += this.dirCollapseSeparator + " " + this.lib_list[pLibIdx].dir_list[pDirIdx].description;
+	}
+	else
+		desc = file_area.lib_list[pLibIdx].description;
+	printf(descFormatStr, desc.substr(0, descLen));
+	console.cleartoeol("\1n");
 }
 
 // Lightbar functions
@@ -639,11 +952,15 @@ function DDFileAreaChooser_writeDirListHdr1Line(pLibIndex, pNumPages, pPageNum)
 // choose a file library and directory
 //
 // Parameters:
-//  pChooseLib: Boolean - Whether or not to choose the file library.  If false,
-//              then this will allow choosing a directory within the user's
-//              current file library.  This is optional; defaults to true.
+//  pLevel: The file heirarchy level:
+//          1: File libraries
+//          2: File directories within libraries
+//          3: File subdirectories within directories (if directory name collapsing is enabled)
+//          This is optional and defaults to 1.
 //  pLibIdx: Optional - The file library index, if choosing a file directory
-function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
+//  pDirIdx: Optional - The file directory index (within a library), for use with
+//           directory name collapsing
+function DDFileAreaChooser_SelectFileArea_Lightbar(pLevel, pLibIdx, pDirIdx)
 {
 	// If there are file libraries, then don't let the user
 	// choose one.
@@ -653,8 +970,12 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 		console.print("\1y\1hThere are no file libraries.\r\n\1p");
 		return;
 	}
-	var chooseLib = (typeof(pChooseLib) == "boolean" ? pChooseLib : true);
-	if (!chooseLib)
+	var level = (typeof(pLevel) == "number" ? pLevel : 1);
+	if ((level < 1) || (level > 3))
+		return;
+	// 2: Choose a file directory within a library
+	// 3: Choose a subdirectory within a directory, for directory name collapsing
+	else if ((level == 2) || (level == 3))
 	{
 		if (typeof(pLibIdx) != "number")
 			return;
@@ -667,28 +988,47 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 	}
 
 	// Displays the header & header lines above the list
-	function displayListHdrLines(pChooseFileLib, pAreaChooser, pLibIdx, pNumPages, pPageNum)
+	function displayListHdrLines(pLevel, pAreaChooser, pLibIdx, pDirIdx, pNumPages, pPageNum)
 	{
 		console.clear("\1n");
 		pAreaChooser.DisplayAreaChgHdr(1);
 		console.gotoxy(1, pAreaChooser.areaChangeHdrLines.length+1);
-		if (pChooseFileLib)
+		if (pLevel == 1)
 			pAreaChooser.WriteLibListHdrLine(pNumPages, pPageNum);
 		else
 		{
-			pAreaChooser.WriteDirListHdr1Line(pLibIdx, pNumPages, pPageNum);
+			pAreaChooser.WriteDirListHdr1Line(pLibIdx, pDirIdx, pNumPages, pPageNum);
 			console.gotoxy(1, pAreaChooser.areaChangeHdrLines.length+2);
-			printf(pAreaChooser.fileDirHdrPrintfStr, "Dir #", "Description", "# Files");
+			if (pAreaChooser.useDirCollapsing)
+			{
+				if (pLevel == 2)
+					printf(pAreaChooser.fileDirHdrPrintfStr, "Dir #", "Description", "# Items");
+				else if (level == 3)
+					printf(pAreaChooser.fileDirHdrPrintfStr, "Dir #", "Description", "# Files");
+					
+			}
+			else
+				printf(pAreaChooser.fileDirHdrPrintfStr, "Dir #", "Description", "# Files");
 		}
 	}
 
 	// Clear the screen, write the header, help line, and library/dir list header(s)
-	displayListHdrLines(chooseLib, this, pLibIdx);
+	displayListHdrLines(level, this, pLibIdx, pDirIdx);
 	this.WriteKeyHelpLine();
 
 	// Create the menu and do the uesr input loop
 	// TODO: The library menu isn't showing any items
-	var fileAreaMenu = (chooseLib ? this.CreateLightbarFileLibMenu() : this.CreateLightbarFileDirMenu(pLibIdx));
+	var fileAreaMenu;
+	switch (level)
+	{
+		case 1:
+			fileAreaMenu = this.CreateLightbarFileLibMenu();
+			break;
+		case 2:
+		case 3:
+			fileAreaMenu = this.CreateLightbarFileDirMenu(pLibIdx, pDirIdx, level);
+			break;
+	}
 	var drawMenu = true;
 	var lastSearchText = "";
 	var lastSearchFoundIdx = -1;
@@ -700,7 +1040,7 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 		chosenIdx = -1;
 		var returnedMenuIdx = fileAreaMenu.GetVal(drawMenu);
 		drawMenu = true;
-		var lastUserInputUpper = (typeof(fileAreaMenu.lastUserInput) == "string" ? fileAreaMenu.lastUserInput.toUpperCase() : fileAreaMenu.lastUserInput);
+		var lastUserInputUpper = (typeof(fileAreaMenu.lastUserInput) == "string" ? fileAreaMenu.lastUserInput.toUpperCase() : "");
 		if (typeof(returnedMenuIdx) == "number")
 			chosenIdx = returnedMenuIdx;
 		// If userChoice is not a number, then it should be null in this case,
@@ -726,10 +1066,18 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 				var oldLastSearchFoundIdx = lastSearchFoundIdx;
 				var oldSelectedItemIdx = fileAreaMenu.selectedItemIdx;
 				var idx = -1;
-				if (chooseLib)
-					idx = findFileLibIdxFromText(searchText, fileAreaMenu.selectedItemIdx);
-				else
-					idx = findFileDirIdxFromText(pLibIdx, searchText, fileAreaMenu.selectedItemIdx+1);
+				switch (level)
+				{
+					case 1:
+						idx = findFileLibIdxFromText(searchText, fileAreaMenu.selectedItemIdx);
+						break;
+					case 2:
+						idx = findFileDirIdxFromText(pLibIdx, searchText, fileAreaMenu.selectedItemIdx+1);
+						break;
+					case 3:
+						// TODO
+						break;
+				}
 				lastSearchFoundIdx = idx;
 				if (idx > -1)
 				{
@@ -749,10 +1097,18 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 				}
 				else
 				{
-					if (chooseLib)
-						idx = findFileLibIdxFromText(searchText, 0);
-					else
-						idx = findFileDirIdxFromText(pLibIdx, searchText, 0);
+					switch (level)
+					{
+						case 1:
+							idx = findFileLibIdxFromText(searchText, 0);
+							break;
+						case 2:
+							idx = findFileDirIdxFromText(pLibIdx, searchText, 0);
+							break;
+						case 3:
+							// TODO
+							break;
+					}
 					lastSearchFoundIdx = idx;
 					if (idx > -1)
 					{
@@ -793,11 +1149,19 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 				var oldSelectedItemIdx = fileAreaMenu.selectedItemIdx;
 				// Do the search, and if found, go to the page and select the item
 				// indicated by the search.
-				var idx = 0;
-				if (chooseLib)
-					idx = findFileLibIdxFromText(searchText, lastSearchFoundIdx+1);
-				else
-					idx = findFileDirIdxFromText(pLibIdx, searchText, lastSearchFoundIdx+1);
+				var idx = -1;
+				switch (level)
+				{
+					case 1:
+						idx = findFileLibIdxFromText(searchText, lastSearchFoundIdx+1);
+						break;
+					case 2:
+						idx = findFileDirIdxFromText(pLibIdx, searchText, lastSearchFoundIdx+1);
+						break;
+					case 3:
+						// TODO
+						break;
+				}
 				if (idx > -1)
 				{
 					lastSearchFoundIdx = idx;
@@ -821,10 +1185,18 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 				}
 				else
 				{
-					if (chooseLib)
-						idx = findFileLibIdxFromText(searchText, 0);
-					else
-						idx = findFileDirIdxFromText(pLibIdx, searchText, 0);
+					switch (level)
+					{
+						case 1:
+							idx = findFileLibIdxFromText(searchText, 0);
+							break;
+						case 2:
+							idx = findFileDirIdxFromText(pLibIdx, searchText, 0);
+							break;
+						case 3:
+							// TODO
+							break;
+					}
 					lastSearchFoundIdx = idx;
 					if (idx > -1)
 					{
@@ -866,11 +1238,12 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 			this.ShowHelpScreen(true, true);
 			console.pause();
 			// Refresh the screen
-			displayListHdrLines(chooseLib, this, pLibIdx);
+			displayListHdrLines(level, this, pLibIdx, pDirIdx);
+			this.WriteKeyHelpLine();
 		}
 		// If the user entered a numeric digit, then treat it as
 		// the start of the message group number.
-		if (lastUserInputUpper.match(/[0-9]/))
+		else if (lastUserInputUpper.match(/[0-9]/))
 		{
 			// Put the user's input back in the input buffer to
 			// be used for getting the rest of the message number.
@@ -887,7 +1260,7 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 			{
 				// The user didn't make a selection.  So, we need to refresh
 				// the screen due to everything being moved up one line.
-				displayListHdrLines(chooseLib, this, pLibIdx);
+				displayListHdrLines(level, this, pLibIdx, pDirIdx);
 				this.WriteKeyHelpLine();
 			}
 		}
@@ -898,7 +1271,7 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 			// If choosing a file library, then let the user choose a file
 			// directory within the library.  Otherwise, return the user's
 			// chosen file directory.
-			if (chooseLib)
+			if (level == 1)
 			{
 				// Show a "Loading..." text in case there are many directories in
 				// the chosen file library
@@ -908,7 +1281,11 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 				// Ensure that the file dir printf information is created for
 				// the chosen file library.
 				this.BuildFileDirPrintfInfoForLib(chosenIdx);
-				var chosenFileDirIdx = this.SelectFileArea_Lightbar(false, chosenIdx);
+				// Make a backup of bbs.curdir_code, in case the directory
+				// changes at the 3rd level (if directory collapsing is
+				// enabled)
+				var dirCodeBackup = bbs.curdir_code;
+				var chosenFileDirIdx = this.SelectFileArea_Lightbar(level+1, chosenIdx);
 				if (chosenFileDirIdx > -1)
 				{
 					// Set the current file directory
@@ -917,15 +1294,57 @@ function DDFileAreaChooser_selectFileArea_Lightbar(pChooseLib, pLibIdx)
 				}
 				else
 				{
-					// A file directory was not chosen, so we'll have to re-draw
-					// the header and list of message groups.
-					displayListHdrLines(chooseLib, this, pLibIdx);
-					// TODO: Is the next line needed?
-					//this.WriteKeyHelpLine();
+					// If the dir changed (probably at level 3 because directory
+					// collapsing is enabled), then exit here.
+					if (bbs.curdir_code != dirCodeBackup)
+						continueOn = false;
+					else
+					{
+						// A file directory was not chosen, so we'll have to re-draw
+						// the header and key help line
+						displayListHdrLines(level, this, pLibIdx, pDirIdx);
+						this.WriteKeyHelpLine();
+					}
 				}
 			}
-			else
-				return chosenIdx; // Return the chosen sub-board index
+			else if (level == 2)
+			{
+				if (this.useDirCollapsing)
+				{
+					// Ensure this.lib_list is set up
+					this.SetUpLibListWithCollapsedDirs();
+
+					// If the current file directory has subdirectories,
+					// let the user choose one
+					// pLibIdx is the library index, and chosenIdx is the file directory index
+					if ((typeof(this.lib_list[pLibIdx].dir_list[chosenIdx]) !== "undefined") && (this.lib_list[pLibIdx].dir_list[chosenIdx].subdir_list.length > 0))
+					{
+						//SelectFileArea_Lightbar(pLevel, pLibIdx, pDirIdx)
+						var chosenSubdirIdx = this.SelectFileArea_Lightbar(level+1, pLibIdx, chosenIdx);
+						if (chosenSubdirIdx > -1)
+						{
+							// Set the current file directory
+							// TODO: This doesn't seem to be exiting, it's just
+							// going back to the library menu
+							bbs.curdir_code = this.lib_list[pLibIdx].dir_list[chosenIdx].subdir_list[chosenSubdirIdx].code;
+							continueOn = false;
+						}
+						else
+						{
+							// A file directory was not chosen, so we'll have to re-draw
+							// the header and list of message groups.
+							displayListHdrLines(level, this, pLibIdx, pDirIdx);
+							this.WriteKeyHelpLine();
+						}
+					}
+					else // No subdirectories - Return the chosen index
+						return chosenIdx;
+				}
+				else
+					return chosenIdx; // Return the chosen file directory index
+			}
+			else if (level == 3)
+				return chosenIdx; // Return the chosen subdirectory index
 		}
 	}
 }
@@ -1011,11 +1430,17 @@ function DDFileAreaChooser_CreateLightbarFileLibMenu()
 //
 // Parameters:
 //  pLibIdx: The index of the file library
+//  pDirIdx: The index of a file directory, if name collapsing is being used
+//  pLevel: The level into the heirarchy.  2 would be file directories, and
+//          3 would be subdirectories inside directories if name collapsing
+//          is being used
 //
 // Return value: A DDLightbarMenu object for choosing a file directory within
 // the given file library
-function DDFileAreaChooser_CreateLightbarFileDirMenu(pLibIdx)
+function DDFileAreaChooser_CreateLightbarFileDirMenu(pLibIdx, pDirIdx, pLevel)
 {
+	// TODO: Update this for sub-board name collapsing
+
 	// Start & end indexes for the various items in each mssage group list row
 	// Selected mark, group#, description, # sub-boards
 	var fileDirListIdxes = {
@@ -1060,78 +1485,106 @@ function DDFileAreaChooser_CreateLightbarFileDirMenu(pLibIdx)
 	// to the menu
 	fileDirMenu.areaChooser = this; // Add this object to the menu object
 	fileDirMenu.libIdx = pLibIdx;
-	fileDirMenu.NumItems = function() {
-		return file_area.lib_list[this.libIdx].dir_list.length;
-	};
-	fileDirMenu.GetItem = function(pDirIdx) {
-		var menuItemObj = this.MakeItemWithRetval(-1);
-		if ((pDirIdx >= 0) && (pDirIdx < file_area.lib_list[this.libIdx].dir_list.length))
+	if (this.useDirCollapsing)
+	{
+		if (pLevel == 2)
 		{
-			var showSubBoardMark = false;
-			if ((typeof(bbs.curdir_code) == "string") && (bbs.curdir_code != ""))
-				showSubBoardMark = ((this.libIdx == file_area.dir[bbs.curdir_code].lib_index) && (pDirIdx == file_area.dir[bbs.curdir_code].index));
-			menuItemObj.text = (showSubBoardMark ? "*" : " ");
-			menuItemObj.text += format(this.areaChooser.fileDirListPrintfInfo[this.libIdx].printfStr, +(pDirIdx+1),
-			                           file_area.lib_list[this.libIdx].dir_list[pDirIdx].description.substr(0, this.areaChooser.descFieldLen),
-			                           this.areaChooser.fileDirListPrintfInfo[this.libIdx].fileCounts[pDirIdx]);
-			menuItemObj.text = strip_ctrl(menuItemObj.text);
-			menuItemObj.retval = pDirIdx;
-		}
+			fileDirMenu.NumItems = function() {
+				return this.areaChooser.lib_list[this.libIdx].dir_list.length;
+			};
+			fileDirMenu.GetItem = function(pDirIdx) {
+				var menuItemObj = this.MakeItemWithRetval(-1);
+				if ((pDirIdx >= 0) && (pDirIdx < this.areaChooser.lib_list[this.libIdx].dir_list.length))
+				{
+					var showDirMark = false;
+					if ((typeof(bbs.curdir_code) == "string") && (bbs.curdir_code != ""))
+						showDirMark = ((this.libIdx == file_area.dir[bbs.curdir_code].lib_index) && (pDirIdx == file_area.dir[bbs.curdir_code].index));
+					// Set the directory description.  And if it has subdirectories,
+					// then append some text indicating so.
+					var dirDesc = this.areaChooser.lib_list[this.libIdx].dir_list[pDirIdx].description;
+					if (this.areaChooser.lib_list[this.libIdx].dir_list[pDirIdx].subdir_list.length > 0)
+						dirDesc += "  <subdirs>";
+					menuItemObj.text = (showDirMark ? "*" : " ");
+					menuItemObj.text += format(this.areaChooser.fileDirListPrintfInfo[this.libIdx].printfStr, +(pDirIdx+1),
+											   dirDesc.substr(0, this.areaChooser.descFieldLen),
+											   this.areaChooser.fileDirListPrintfInfo[this.libIdx].fileCounts[pDirIdx]);
+					menuItemObj.text = strip_ctrl(menuItemObj.text);
+					menuItemObj.retval = pDirIdx;
+				}
 
-		return menuItemObj;
-	};
+				return menuItemObj;
+			};
+		}
+		else if (pLevel == 3)
+		{
+			fileDirMenu.dirIdx = pDirIdx;
+			fileDirMenu.NumItems = function() {
+				return this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list.length;
+			};
+			fileDirMenu.GetItem = function(pSubdirIdx) {
+				var menuItemObj = this.MakeItemWithRetval(-1);
+				if ((pSubdirIdx >= 0) && (pSubdirIdx < this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list.length))
+				{
+					var showDirMark = false;
+					if ((typeof(bbs.curdir_code) == "string") && (bbs.curdir_code != ""))
+						showDirMark = (bbs.curdir_code == this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list[pSubdirIdx].code);
+					menuItemObj.text = (showDirMark ? "*" : " ");
+					var subdirDesc = this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list[pSubdirIdx].description;
+					var subdirDirIdx = this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list[pSubdirIdx].index;
+					var dirCode = this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list[pSubdirIdx].code;
+					menuItemObj.text += format(this.areaChooser.fileDirListPrintfInfo[this.libIdx].printfStr, +(pSubdirIdx+1),
+											   subdirDesc.substr(0, this.areaChooser.descFieldLen),
+											   this.areaChooser.fileDirListPrintfInfo[this.libIdx].fileCountsByCode[dirCode]);
+					menuItemObj.text = strip_ctrl(menuItemObj.text);
+					menuItemObj.retval = pSubdirIdx;
+				}
 
-	// Set the currently selected item.  If the current sub-board is in this list,
-	// then set the selected item to that; otherwise, the selected item should be
-	// the first sub-board.
-	if (file_area.dir[bbs.curdir_code].lib_index == pLibIdx)
-	{
-		fileDirMenu.selectedItemIdx = file_area.dir[bbs.curdir_code].index;
-		if (fileDirMenu.selectedItemIdx >= fileDirMenu.topItemIdx+fileDirMenu.GetNumItemsPerPage())
-			fileDirMenu.topItemIdx = fileDirMenu.selectedItemIdx - fileDirMenu.GetNumItemsPerPage() + 1;
+				return menuItemObj;
+			}
+		}
 	}
 	else
 	{
-		fileDirMenu.selectedItemIdx = 0;
-		fileDirMenu.topItemIdx = 0;
+		fileDirMenu.NumItems = function() {
+			return file_area.lib_list[this.libIdx].dir_list.length;
+		};
+		fileDirMenu.GetItem = function(pDirIdx) {
+			var menuItemObj = this.MakeItemWithRetval(-1);
+			if ((pDirIdx >= 0) && (pDirIdx < file_area.lib_list[this.libIdx].dir_list.length))
+			{
+				var showDirMark = false;
+				if ((typeof(bbs.curdir_code) == "string") && (bbs.curdir_code != ""))
+					showDirMark = ((this.libIdx == file_area.dir[bbs.curdir_code].lib_index) && (pDirIdx == file_area.dir[bbs.curdir_code].index));
+				menuItemObj.text = (showDirMark ? "*" : " ");
+				menuItemObj.text += format(this.areaChooser.fileDirListPrintfInfo[this.libIdx].printfStr, +(pDirIdx+1),
+				                           file_area.lib_list[this.libIdx].dir_list[pDirIdx].description.substr(0, this.areaChooser.descFieldLen),
+				                           this.areaChooser.fileDirListPrintfInfo[this.libIdx].fileCounts[pDirIdx]);
+				menuItemObj.text = strip_ctrl(menuItemObj.text);
+				menuItemObj.retval = pDirIdx;
+			}
+
+			return menuItemObj;
+		};
+
+		// Set the currently selected item.  If the current sub-board is in this list,
+		// then set the selected item to that; otherwise, the selected item should be
+		// the first sub-board.
+		if (file_area.dir[bbs.curdir_code].lib_index == pLibIdx)
+		{
+			fileDirMenu.selectedItemIdx = file_area.dir[bbs.curdir_code].index;
+			if (fileDirMenu.selectedItemIdx >= fileDirMenu.topItemIdx+fileDirMenu.GetNumItemsPerPage())
+				fileDirMenu.topItemIdx = fileDirMenu.selectedItemIdx - fileDirMenu.GetNumItemsPerPage() + 1;
+		}
+		else
+		{
+			fileDirMenu.selectedItemIdx = 0;
+			fileDirMenu.topItemIdx = 0;
+		}
 	}
 
 	return fileDirMenu;
 }
 
-// Updates the page number text in the file library/area list header line on the screen.
-//
-// Parameters:
-//  pPageNum: The page number
-//  pNumPages: The total number of pages
-//  pFileLib: Boolean - Whether or not this is for the file library header.  If so,
-//            then this will go to the right location for the file library page text
-//            and use this.colors.header for the text.  Otherwise, this will
-//            go to the right place for the file area page text and use the
-//            file area header color.
-//  pRestoreCurPos: Optional - Boolean - If true, then move the cursor back
-//                  to the position where it was before this function was called
-function DDFileAreaChooser_updatePageNumInHeader(pPageNum, pNumPages, pFileLib, pRestoreCurPos)
-{
-  var originalCurPos = null;
-  if (pRestoreCurPos)
-    originalCurPos = console.getxy();
-
-  if (pFileLib)
-  {
-    console.gotoxy(29, 1+this.areaChangeHdrLines.length);
-    console.print("\1n" + this.colors.header + pPageNum + " of " + pNumPages + ")   ");
-  }
-  else
-  {
-    console.gotoxy(67, 1+this.areaChangeHdrLines.length);
-    console.print("\1n" + this.colors.fileAreaHdr + pPageNum + " of " + pNumPages + ")   ");
-  }
-
-  if (pRestoreCurPos)
-    console.gotoxy(originalCurPos);
-}
-
 function DDFileAreaChooser_writeKeyHelpLine()
 {
 	console.gotoxy(1, console.screen_rows);
@@ -1209,6 +1662,13 @@ function DDFileAreaChooser_ReadConfigFile()
 						if (maxNumLines > 0)
 							this.areaChooserHdrMaxLines = maxNumLines;
 					}
+					else if (settingUpper == "USEDIRCOLLAPSING")
+						this.useDirCollapsing = (value.toUpperCase() == "TRUE");
+					else if (settingUpper == "DIRCOLLAPSESEPARATOR")
+					{
+						if (value.length > 0)
+							this.dirCollapseSeparator = value;
+					}
 				}
 				else if (settingsMode == "colors")
 					this.colors[setting] = value;
@@ -1288,27 +1748,27 @@ function DDFileAreaChooser_showHelpScreen(pLightbar, pClearScreen)
 // which isn't necessarily the number of files in the database for the directory.
 //
 // Paramters:
-//  pLibNum: The file library number (0-based)
-//  pDirNum: The file directory number (0-based)
+//  pLibIdx: The file library index (0-based)
+//  pDirIdx: The file directory index (0-based)
 //
 // Returns: The number of files in the directory
-function DDFileAreaChooser_NumFilesInDir(pLibNum, pDirNum)
+function numFilesInDir(pLibIdx, pDirIdx)
 {
-   var numFiles = 0;
-
-   // Count the files in the directory.  If it's not a directory, then
-   // increment numFiles.
-   var files = directory(file_area.lib_list[pLibNum].dir_list[pDirNum].path + "*.*");
-   numFiles = files.length;
-   // Make sure directories aren't counted: Go through the files array, and
-   // for each directory, decrement numFiles.
-   for (var i in files)
-   {
-      if (file_isdir(files[i]))
-         --numFiles;
-   }
+	var numFiles = 0;
+
+	// Count the files in the directory.  If it's not a directory, then
+	// increment numFiles.
+	var files = directory(file_area.lib_list[pLibIdx].dir_list[pDirIdx].path + "*.*");
+	numFiles = files.length;
+	// Make sure directories aren't counted: Go through the files array, and
+	// for each directory, decrement numFiles.
+	for (var i in files)
+	{
+		if (file_isdir(files[i]))
+			--numFiles;
+	}
 
-   return numFiles;
+	return numFiles;
 }
 
 // Builds file directory printf format information for a file library.
@@ -1329,20 +1789,43 @@ function DDFileAreaChooser_buildFileDirPrintfInfoForLib(pLibIndex)
 		// Get information about the number of files in each directory
 		// and the greatest number of files and set up the according
 		// information in the file directory list object
-		var fileDirInfo = getGreatestNumFiles(pLibIndex);
+		var fileDirInfo = this.GetGreatestNumFiles(pLibIndex);
 		if (fileDirInfo != null)
 		{
 			this.fileDirListPrintfInfo[pLibIndex].numFilesLen = fileDirInfo.greatestNumFiles.toString().length;
 			this.fileDirListPrintfInfo[pLibIndex].fileCounts = fileDirInfo.fileCounts.slice(0);
+			this.fileDirListPrintfInfo[pLibIndex].fileCountsByCode = fileDirInfo.fileCountsByCode;
 		}
 		else
 		{
 			// fileDirInfo is null.  We still want to create
 			// the fileCounts array in the file directory object
 			// so that it's valid.
-			this.fileDirListPrintfInfo[pLibIndex].fileCounts = new Array(file_area.lib_list[pLibIndex].length);
-			for (var i = 0; i < file_area.lib_list[pLibIndex].length; ++i)
-				this.fileDirListPrintfInfo[pLibIndex].fileCounts[i] == 0;
+			if (this.useDirCollapsing)
+			{
+				// Ensure this.lib_list is set up
+				this.SetUpLibListWithCollapsedDirs();
+				this.fileDirListPrintfInfo[pLibIndex].fileCounts = [];
+				this.fileDirListPrintfInfo[pLibIndex].fileCountsByCode = {};
+				for (var dirIdx = 0; dirIdx < this.lib_list[pLibIndex].dir_list.length; ++dirIdx)
+				{
+					if (this.lib_list[pLibIndex].dir_list.subdir_list.length > 0)
+						this.fileDirListPrintfInfo[pLibIndex].fileCounts[dirIdx] = this.lib_list[pLibIndex].dir_list.subdir_list.length;
+					else
+						this.fileDirListPrintfInfo[pLibIndex].fileCounts[dirIdx] = 0;
+				}
+				for (var dirIdx = 0; i < file_area.lib_list[pLibIndex].dir_list.length; ++dirIdx)
+				{
+					var dirCode = file_area.lib_list[pLibIndex].dir_list[dirIdx].code;
+					this.fileDirListPrintfInfo[pLibIndex].fileCountsByCode[dirCode] = 0;
+				}
+			}
+			else
+			{
+				this.fileDirListPrintfInfo[pLibIndex].fileCounts = new Array(file_area.lib_list[pLibIndex].dir_list.length);
+				for (var dirIdx = 0; i < file_area.lib_list[pLibIndex].dir_list.length; ++dirIdx)
+					this.fileDirListPrintfInfo[pLibIndex].fileCounts[dirIdx] == 0;
+			}
 		}
 
 		// Set the description field length and printf strings for
@@ -1441,6 +1924,175 @@ function DDFileAreaChooser_WriteLightbarKeyHelpErrorMsg(pErrorMsg, pRefreshHelpL
 		this.WriteKeyHelpLine();
 }
 
+// For the DDFileAreaChooser class: Sets up the lib_list array according to
+// the file directory collapse separator
+function DDFileAreaChooser_SetUpLibListWithCollapsedDirs()
+{
+	// Returns a default object for a file library
+	function defaultFileLibObj()
+	{
+		return {
+			index: 0,
+			number: 0,
+			name: "",
+			description: "",
+			ars: 0,
+			dir_list: []
+		};
+	}
+
+	// Returns a default object for a file directory.  It can potentially
+	// contain its own list of subdirectories.
+	function defaultFileDirObj()
+	{
+		return {
+			index: 0,
+			number: 0,
+			lib_index: 0,
+			lib_number: 0,
+			lib_name: 0,
+			code: "",
+			name: "",
+			lib_name: "",
+			description: "",
+			ars: 0,
+			settings: 0,
+			subdir_list: []
+		};
+	}
+
+	function dirObjFromOfficialDir(pLibIdx, pDirIdx)
+	{
+		var dirObj = defaultFileDirObj();
+		for (var prop in dirObj)
+		{
+			if (prop != "subdir_list") // Doesn't exist in the official dir objects
+				dirObj[prop] = file_area.lib_list[pLibIdx].dir_list[pDirIdx][prop];
+		}
+		return dirObj;
+	}
+
+	if (this.lib_list.length == 0)
+	{
+		// Copy some of the information from file_area.lib_list
+		for (var libIdx = 0; libIdx < file_area.lib_list.length; ++libIdx)
+		{
+			// Go through dir_list in the curent file library, and for
+			// any that have the collapse separator, add only one copy
+			// of the name to the dir_list property for this library.
+			// First, we'll have to see if multiple directories with
+			// the collapse separator have the same prefix.
+			// dirDescs is an object indexed by directory description,
+			// and the value will be how many times it was seen.
+			var dirDescs = {};
+			for (var dirIdx = 0; dirIdx < file_area.lib_list[libIdx].dir_list.length; ++dirIdx)
+			{
+				var dirDesc = file_area.lib_list[libIdx].dir_list[dirIdx].description;
+				var sepIdx = dirDesc.indexOf(this.dirCollapseSeparator);
+				if (sepIdx > -1)
+					dirDesc = truncsp(dirDesc.substr(0, sepIdx));  // Remove trailing whitespace
+				if (dirDescs.hasOwnProperty(dirDesc))
+					dirDescs[dirDesc] += 1;
+				else
+					dirDescs[dirDesc] = 1;
+			}
+
+			// Create an initial file library object for this file library (except
+			// for dir_list, which we will build ourselves for this library)
+			var libObj = defaultFileLibObj();
+			for (var prop in libObj)
+			{
+				if (prop != "dir_list")
+					libObj[prop] = file_area.lib_list[libIdx][prop];
+			}
+
+			// Go through the dirs in this library again.  For each directory:
+			// If its whole description exists in dirDescs, then just add it to
+			// the dir_list array.  Otherwise:
+			// If a collapse seprator exists, then split on that and see if the
+			// prefix was seen more than once.  If so, then add the separate
+			// subdirs as their own items in the dir_list array.  Otherwise,
+			// add the single directory to the dir_list array with the whole
+			// description.
+			var addedPrefixDescriptionDirs = {};
+			for (var dirIdx = 0; dirIdx < file_area.lib_list[libIdx].dir_list.length; ++dirIdx)
+			{
+				var dirDesc = file_area.lib_list[libIdx].dir_list[dirIdx].description;
+				if (dirDescs.hasOwnProperty(dirDesc))
+					libObj.dir_list.push(dirObjFromOfficialDir(libIdx, dirIdx));
+				else
+				{
+					var subdirDesc = "";
+					var sepIdx = dirDesc.indexOf(this.dirCollapseSeparator);
+					if (sepIdx > -1)
+					{
+						dirDesc = truncsp(dirDesc.substr(0, sepIdx));  // Remove trailing whitespace
+						// If it has been seen more than once, then the description should
+						// be the prefix description
+						if (dirDescs[dirDesc] > 1)
+						{
+							var addedDirIdx = libObj.dir_list.length - 1;
+							if (!addedPrefixDescriptionDirs.hasOwnProperty(dirDesc))
+							{
+								// Add it to dir_list
+								libObj.dir_list.push(dirObjFromOfficialDir(libIdx, dirIdx));
+								addedDirIdx = libObj.dir_list.length - 1;
+								libObj.dir_list[addedDirIdx].description = dirDesc;
+								addedPrefixDescriptionDirs[dirDesc] = true;
+							}
+							// Add the subdirectory to the directory's subdirectory list
+							// Using skipsp() to strip leading whitespace
+							subdirDesc = skipsp(file_area.lib_list[libIdx].dir_list[dirIdx].description.substr(sepIdx+1));
+							libObj.dir_list[addedDirIdx].subdir_list.push({
+								description: subdirDesc,
+								code: file_area.lib_list[libIdx].dir_list[dirIdx].code,
+								index: file_area.lib_list[libIdx].dir_list[dirIdx].index,
+								lib_index: file_area.lib_list[libIdx].dir_list[dirIdx].lib_index
+							});
+						}
+						else // Add it with the full description
+							libObj.dir_list.push(dirObjFromOfficialDir(libIdx, dirIdx));
+					}
+					if (dirDescs.hasOwnProperty(dirDesc))
+						dirDescs[dirDescs] += 1;
+					else
+						dirDescs[dirDescs] = 1;
+				}
+			}
+
+			this.lib_list.push(libObj);
+		}
+	}
+
+	//dumpLibListToFile(this.lib_list, "D:\\BBS\\Files\\fileAreaChooser_debug.txt"); // Temporary
+}
+// Temporary
+function dumpLibListToFile(pLibList, pFilename)
+{
+	var outFile = new File(pFilename);
+	if (outFile.open("a"))
+	{
+		outFile.writeln("File libraries:");
+		for (var libIdx = 0; libIdx < pLibList.length; ++libIdx)
+		{
+			outFile.writeln(libIdx + ":" + pLibList[libIdx].description + ": - # dirs: " + pLibList[libIdx].dir_list.length);
+			for (var dirIdx = 0; dirIdx < pLibList[libIdx].dir_list.length; ++dirIdx)
+			{
+				//outFile.writeln(" " + dirIdx + ": " + typeof(pLibList[libIdx].dir_list[dirIdx]));
+				outFile.writeln(" " + dirIdx + ":" + pLibList[libIdx].dir_list[dirIdx].description + ": - # subdirs: " + pLibList[libIdx].dir_list[dirIdx].subdir_list.length);
+				for (var subdirIdx = 0; subdirIdx < pLibList[libIdx].dir_list[dirIdx].subdir_list.length; ++subdirIdx)
+				{
+					outFile.writeln("  " + subdirIdx + ":" + pLibList[libIdx].dir_list[dirIdx].subdir_list[subdirIdx].description + ":, :" + pLibList[libIdx].dir_list[dirIdx].subdir_list[subdirIdx].code + ":");
+				}
+			}
+		}
+		outFile.writeln("");
+
+		outFile.close();
+	}
+}
+// End Temporary
+
 // Removes multiple, leading, and/or trailing spaces
 // The search & replace regular expressions used in this
 // function came from the following URL:
@@ -1488,11 +2140,12 @@ function calcPageNum(pTopIndex, pNumPerPage)
   return ((pTopIndex / pNumPerPage) + 1);
 }
 
-// For a given file library index, returns an object containing
-// the greatest number of files of all directories within a file
-// library and an array containing the number of files in each
-// directory.  If the given library index is invalid, this
-// function will return null.
+// For the DDFileAreaChooser class: For a given file library index, returns an
+// object containing the greatest number of files of all directories within a
+// file library and an array containing the number of files in each directory.
+// If the given library index is invalid, this function will return null.
+// If directory collapsing is enabled, this will account for the number of
+// subdirectories in the directories that have them.
 //
 // Parameters:
 //  pLibIndex: The index of the file library
@@ -1503,7 +2156,10 @@ function calcPageNum(pTopIndex, pNumPerPage)
 //          fileCounts: An array, indexed by directory index,
 //                      containing the number of files in each
 //                      directory within the file library
-function getGreatestNumFiles(pLibIndex)
+//          fileCountsByCode: A dictionary indexed by internal code of the
+//                            file directories, and each value is the number
+//                            of files in the directory
+function DDFileAreaChooser_GetGreatestNumFiles(pLibIndex)
 {
 	// Sanity checking
 	if (typeof(pLibIndex) != "number")
@@ -1513,14 +2169,55 @@ function getGreatestNumFiles(pLibIndex)
 
 	var retObj = {
 		greatestNumFiles: 0,
-		fileCounts: new Array(file_area.lib_list[pLibIndex].dir_list.length)
+		fileCounts: null, // Will be an array
+		fileCountsByCode: {}
 	}
+
+	if (this.useDirCollapsing)
+	{
+		// Ensure this.lib_list is set up
+		this.SetUpLibListWithCollapsedDirs();
+
+		retObj.fileCounts = new Array(this.lib_list[pLibIndex].dir_list.length);
+		for (var dirIndex = 0; dirIndex < this.lib_list[pLibIndex].dir_list.length; ++dirIndex)
+		{
+			if (this.lib_list[pLibIndex].dir_list[dirIndex].subdir_list.length > 0)
+				retObj.fileCounts[dirIndex] = this.lib_list[pLibIndex].dir_list[dirIndex].subdir_list.length;
+			else
+				retObj.fileCounts[dirIndex] = numFilesInDir(pLibIndex, dirIndex);
+			if (retObj.fileCounts[dirIndex] > retObj.greatestNumFiles)
+				retObj.greatestNumFiles = retObj.fileCounts[dirIndex];
+		}
+	}
+	else
+	{
+		retObj.fileCounts = new Array(file_area.lib_list[pLibIndex].dir_list.length);
+		for (var dirIndex = 0; dirIndex < file_area.lib_list[pLibIndex].dir_list.length; ++dirIndex)
+		{
+			retObj.fileCounts[dirIndex] = numFilesInDir(pLibIndex, dirIndex);
+			if (retObj.fileCounts[dirIndex] > retObj.greatestNumFiles)
+				retObj.greatestNumFiles = retObj.fileCounts[dirIndex];
+		}
+	}
+
+	// Populate the fileCountsByCode dictionary for the given file library
 	for (var dirIndex = 0; dirIndex < file_area.lib_list[pLibIndex].dir_list.length; ++dirIndex)
 	{
-		retObj.fileCounts[dirIndex] = DDFileAreaChooser_NumFilesInDir(pLibIndex, dirIndex);
-		if (retObj.fileCounts[dirIndex] > retObj.greatestNumFiles)
-			retObj.greatestNumFiles = retObj.fileCounts[dirIndex];
+		var dirCode = file_area.lib_list[pLibIndex].dir_list[dirIndex].code;
+		// If we've alrady got the # of files for the dir, then use it; otherwise,
+		// call NumFilesInDir() to get the file count.  Also, make sure these are
+		// all actual file counts in the directories.
+		if (typeof(retObj.fileCounts[dirIndex]) == "number")
+		{
+			if (this.useDirCollapsing && (this.lib_list[pLibIndex].dir_list[dirIndex].subdir_list.length > 0))
+				retObj.fileCountsByCode[dirCode] = numFilesInDir(pLibIndex, dirIndex);
+			else
+				retObj.fileCountsByCode[dirCode] = retObj.fileCounts[dirIndex];
+		}
+		else
+			retObj.fileCountsByCode[dirCode] = numFilesInDir(pLibIndex, dirIndex);
 	}
+
 	return retObj;
 }
 
diff --git a/xtrn/DDAreaChoosers/DDMsgAreaChooser.cfg b/xtrn/DDAreaChoosers/DDMsgAreaChooser.cfg
index 771f04bb4d496874eb2316cea69a6bc460754bff..941195a3e68ddbb2348281129f53e7a97c36c4a8 100644
--- a/xtrn/DDAreaChoosers/DDMsgAreaChooser.cfg
+++ b/xtrn/DDAreaChoosers/DDMsgAreaChooser.cfg
@@ -10,6 +10,16 @@ areaChooserHdrMaxLines=5
 ; Whether or not to show the latest message date/time in the sub-board list
 showDatesInSubBoardList=true
 
+; Whether or not to enable sub-board collapsing.  For
+; example, for sub-boards in a group starting with
+; common text and a separator (specified below), the
+; common text will be the only one displayed, and when
+; the user selects it, a 3rd tier with the sub-boards
+; after the separator will be shown
+useSubCollapsing=true
+; The separator to use for sub-board collapsing
+subCollapseSeparator=:
+
 [COLORS]
 ; Area number
 areaNum=nwh
diff --git a/xtrn/DDAreaChoosers/DDMsgAreaChooser.js b/xtrn/DDAreaChoosers/DDMsgAreaChooser.js
index f2b146d05db9cd881f78481a7a179132683af9ad..c65908aa26909b21c7e6a4643f1e72eefc7e0899 100644
--- a/xtrn/DDAreaChoosers/DDMsgAreaChooser.js
+++ b/xtrn/DDAreaChoosers/DDMsgAreaChooser.js
@@ -1,3 +1,5 @@
+// $Id: $
+
 /* This is a script that lets the user choose a message area,
  * with either a lightbar or traditional user interface.
  *
@@ -13,9 +15,18 @@
  * 2020-04-19 Eric Oulashin   Version 1.20
  *                            For lightbar mode, it now uses DDLightbarMenu
  *                            instead of using internal lightbar code.
+ * 2020-11-01 Eric Oulashin   Version 1.21 Beta
+ *                            Working on sub-board collapsing
+ * 2022-01-15 Eric Oulashin   Version 1.21
+ *                            Finished sub-board collapsing (finally) and releasing
+ *                            this version.
  *                                  
 */
 
+// TODO: Passing "false" as the first command-line argument no longer works.
+// That should allow choosing a sub-board within the user's current message
+// group.
+
 /* Command-line arguments:
    1 (argv[0]): Boolean - Whether or not to choose a message group first (default).  If
                 false, the user will only be able to choose a different sub-board within
@@ -24,8 +35,7 @@
                 then this file will just provide the DDMsgAreaChooser class).
 */
 
-var requireFnExists = (typeof(require) === "function");
-if (requireFnExists)
+if (typeof(require) === "function")
 {
 	require("sbbsdefs.js", "K_NOCRLF");
 	require("dd_lightbar_menu.js", "DDLightbarMenu");
@@ -52,8 +62,8 @@ if (system.version_num < 31400)
 }
 
 // Version & date variables
-var DD_MSG_AREA_CHOOSER_VERSION = "1.20";
-var DD_MSG_AREA_CHOOSER_VER_DATE = "2020-04-19";
+var DD_MSG_AREA_CHOOSER_VERSION = "1.21";
+var DD_MSG_AREA_CHOOSER_VER_DATE = "2022-01-15";
 
 // Keyboard input key codes
 var CTRL_H = "\x08";
@@ -98,11 +108,11 @@ var gIsSysop = user.compare_ars("SYSOP"); // Whether or not the user is a sysop
 // 1st command-line argument: Whether or not to choose a message group first (if
 // false, then only choose a sub-board within the user's current group).  This
 // can be true or false.
-var chooseMsgGrp = true;
+var gChooseMsgGrpOnStartup = true;
 if (typeof(argv[0]) == "boolean")
-	chooseMsgGrp = argv[0];
+	gChooseMsgGrpOnStartup = argv[0];
 else if (typeof(argv[0]) == "string")
-	chooseMsgGrp = (argv[0].toLowerCase() == "true");
+	gChooseMsgGrpOnStartup = (argv[0].toLowerCase() == "true");
 
 // 2nd command-line argument: Determine whether or not to execute the message listing
 // code (true/false)
@@ -117,7 +127,16 @@ else if (typeof(argv[1]) == "string")
 if (executeThisScript)
 {
 	var msgAreaChooser = new DDMsgAreaChooser();
-	msgAreaChooser.SelectMsgArea(chooseMsgGrp);
+	// If we are to let the user choose a sub-board within
+	// their current group (and not choose a message group
+	// first), then we need to capture the chosen sub-board
+	// here just in case, and change the user's message area
+	// here.  Otherwise, if choosing the message group first,
+	// SelectMsgArea() will change the user's sub-board.
+	var msgGroupIdx = (gChooseMsgGrpOnStartup ? 0/*null*/ : bbs.curgrp);
+	var chosenIdx = msgAreaChooser.SelectMsgArea(gChooseMsgGrpOnStartup, msgGroupIdx);
+	if (!gChooseMsgGrpOnStartup && (typeof(chosenIdx) === "number"))
+		bbs.cursub_code = msg_area.grp_list[bbs.curgrp].sub_list[chosenIdx].code;
 }
 
 // End of script execution
@@ -171,30 +190,54 @@ function DDMsgAreaChooser()
 	// sub-board list
 	this.showDatesInSubBoardList = true;
 
+	// Whether or not to enable sub-board collapsing.  For
+	// example, for sub-board in a group starting with
+	// common text and a separator (specified below), the
+	// common text will be the only one displayed, and when
+	// the user selects it, a 3rd tier with the sub-board
+	// after the separator will be shown
+	this.useSubCollapsing = true;
+	// The separator character to use for sub-board collapsing
+	this.subCollapseSeparator = ":";
+	// If userSubCollapsing is true, then group_list will be populated
+	// with some information from Synchronet's msg_area.grp_list,
+	// including a dir_list for each group.  The dir_list arrays
+	// could have one that was collapsed from multiple sub-board
+	// set up in the BBS - The dir_list within that one would then
+	// contain multiple sub-board split based on the dir collapse
+	// separator.
+	this.group_list = [];
+
 	// Set the function pointers for the object
 	this.ReadConfigFile = DDMsgAreaChooser_ReadConfigFile;
 	this.WriteKeyHelpLine = DDMsgAreaChooser_writeKeyHelpLine;
 	this.WriteGrpListHdrLine = DDMsgAreaChooser_writeGrpListTopHdrLine;
 	this.WriteSubBrdListHdr1Line = DMsgAreaChooser_writeSubBrdListHdr1Line;
-	this.SelectMsgArea = DDMsgAreaChooser_selectMsgArea;
-	this.SelectMsgArea_Lightbar = DDMsgAreaChooser_selectMsgArea_Lightbar;
+	this.SelectMsgArea = DDMsgAreaChooser_SelectMsgArea;
+	this.SelectMsgArea_Lightbar = DDMsgAreaChooser_SelectMsgArea_Lightbar;
 	this.CreateLightbarMsgGrpMenu = DDMsgAreaChooser_CreateLightbarMsgGrpMenu;
 	this.CreateLightbarSubBoardMenu = DDMsgAreaChooser_CreateLightbarSubBoardMenu;
-	this.SelectMsgArea_Traditional = DDMsgAreaChooser_selectMsgArea_Traditional;
-	this.SelectSubBoard_Traditional = DDMsgAreaChooser_selectSubBoard_Traditional;
-	this.ListMsgGrps = DDMsgAreaChooser_listMsgGrps_Traditional;
-	this.ListSubBoardsInMsgGroup = DDMsgAreaChooser_listSubBoardsInMsgGroup_Traditional;
+	this.SelectMsgArea_Traditional = DDMsgAreaChooser_SelectMsgArea_Traditional;
+	this.SelectSubBoard_Traditional = DDMsgAreaChooser_SelectSubBoard_Traditional;
+	this.SelectSubSubWithinSub_Traditional = DDMsgAreaChooser_SelectSubSubWithinSub_Traditional;
+	this.ListMsgGrps = DDMsgAreaChooser_ListMsgGrps_Traditional;
+	this.ListSubBoardsInMsgGroup = DDMsgAreaChooser_ListSubBoardsInMsgGroup_Traditional;
+	this.CurrentSubBoardIsInSubSubsForSub = DDMsgAreaChooser_CurrentSubBoardIsInSubSubsForSub;
+	this.GetSubBoardInfo = DDMsgAreaChooser_GetSubBoardInfo;
 	// Lightbar-specific functions
 	this.WriteMsgGroupLine = DDMsgAreaChooser_writeMsgGroupLine;
-	this.updatePageNumInHeader = DDMsgAreaChooser_updatePageNumInHeader;
 	this.GetMsgSubBrdLine = DDMsgAreaChooser_GetMsgSubBrdLine;
 	// Help screen
 	this.ShowHelpScreen = DDMsgAreaChooser_showHelpScreen;
 	// Function to build the sub-board printf information for a message
 	// group
-	this.BuildSubBoardPrintfInfoForGrp = DDMsgAreaChooser_buildSubBoardPrintfInfoForGrp;
+	this.BuildSubBoardPrintfInfoForGrp = DDMsgAreaChooser_BuildSubBoardPrintfInfoForGrp;
 	this.DisplayAreaChgHdr = DDMsgAreaChooser_DisplayAreaChgHdr;
+	this.DisplayListHdrLines = DDMsgAreaChooser_DisplayListHdrLines;
 	this.WriteLightbarKeyHelpErrorMsg = DDMsgAreaChooser_WriteLightbarKeyHelpErrorMsg;
+	this.SetUpGrpListWithCollapsedSubBoards = DDMsgAreaChooser_SetUpGrpListWithCollapsedSubBoards;
+	this.FindMsgGrpIdxFromText = DDMsgAreaChooser_FindMsgGrpIdxFromText;
+	this.FindSubBoardIdxFromText = DDMsgAreaChooser_FindSubBoardIdxFromText;
 
 	// Read the settings from the config file.
 	this.ReadConfigFile();
@@ -311,7 +354,7 @@ function DDMsgAreaChooser()
 	// message group index.  The sub-board printf information is created
 	// on the fly the first time the user lists sub-boards for a message
 	// group.
-	this.subBoardListPrintfInfo = new Array();
+	this.subBoardListPrintfInfo = [];
 
 	// areaChangeHdrLines is an array of text lines to use as a header to display
 	// above the message area changer lists.
@@ -337,11 +380,11 @@ function DDMsgAreaChooser_writeKeyHelpLine()
 function DDMsgAreaChooser_writeGrpListTopHdrLine(pNumPages, pPageNum)
 {
 	var descStr = "Description";
-	if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
+	if ((typeof(pPageNum) === "number") && (typeof(pNumPages) === "number"))
 		descStr += "    (Page " + pPageNum + " of " + pNumPages + ")";
-	else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
+	else if ((typeof(pPageNum) === "number") && (typeof(pNumPages) !== "number"))
 		descStr += "    (Page " + pPageNum + ")";
-	else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
+	else if ((typeof(pPageNum) !== "number") && (typeof(pNumPages) === "number"))
 		descStr += "    (" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
 	printf(this.msgGrpListHdrPrintfStr, "Group#", descStr, "# Sub-Boards");
 	console.cleartoeol("\1n");
@@ -352,21 +395,39 @@ function DDMsgAreaChooser_writeGrpListTopHdrLine(pNumPages, pPageNum)
 //
 // Parameters:
 //  pGrpIndex: The index of the message group (assumed to be valid)
+//  pSubIndex: Optional - The index of a sub-board within the message group (if using sub-board
+//             collapsing, we might want to append the sub-board name to the description)
 //  pNumPages: The number of pages.  This is optional; if this is
 //             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 DMsgAreaChooser_writeSubBrdListHdr1Line(pGrpIndex, pSubIndex, pNumPages, pPageNum)
 {
-	var descFormatStr = "\1n" + this.colors.subBoardHeader + "Sub-boards of \1h%-25s     \1n"
+	var descLen = 25;
+	var descFormatStr = "\1n" + this.colors.subBoardHeader + "Sub-boards of \1h%-" + descLen + "s     \1n"
 	                  + this.colors.subBoardHeader;
-	if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
+	if ((typeof(pPageNum) === "number") && (typeof(pNumPages) === "number"))
 		descFormatStr += "(Page " + pPageNum + " of " + pNumPages + ")";
-	else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
+	else if ((typeof(pPageNum) === "number") && (typeof(pNumPages) !== "number"))
 		descFormatStr += "(Page " + pPageNum + ")";
-	else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
+	else if ((typeof(pPageNum) !== "number") && (typeof(pNumPages) === "number"))
 		descFormatStr += "(" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
-	printf(descFormatStr, msg_area.grp_list[pGrpIndex].description.substr(0, 25));
+	// If using sub-board collapsing, then build the description as needed.  Otherwise,
+	// just use the sub-board description.
+	var desc = "";
+	if (this.useSubCollapsing)
+	{
+		// Ensure this.group_list is set up
+		this.SetUpGrpListWithCollapsedSubBoards();
+		// The description should be the library's description.  Also, if pSubIdx
+		// is a number, then append the directory description to the description.
+		desc = this.group_list[pGrpIndex].description;
+		if ((typeof(pSubIndex) === "number") && (this.group_list[pGrpIndex].sub_list[pSubIndex].sub_subboard_list.length > 0))
+			desc += this.subCollapseSeparator + " " + this.group_list[pGrpIndex].sub_list[pSubIndex].description;
+	}
+	else
+		desc = msg_area.grp_list[pGrpIndex].description;
+	printf(descFormatStr, desc.substr(0, descLen));
 	console.cleartoeol("\1n");
 }
 
@@ -378,23 +439,76 @@ function DMsgAreaChooser_writeSubBrdListHdr1Line(pGrpIndex, pNumPages, pPageNum)
 //  pChooseGroup: Boolean - Whether or not to choose the message group.  If false,
 //                then this will allow choosing a sub-board within the user's
 //                current message group.  This is optional; defaults to true.
-function DDMsgAreaChooser_selectMsgArea(pChooseGroup)
+//  pGrpIdx: The group index (can be null) - This is for the lightbar
+//           interface logic; the traditional interface doesn't use this
+//           for SelectMsgArea().
+function DDMsgAreaChooser_SelectMsgArea(pChooseGroup, pGrpIdx)
 {
+	// If sub-board collapsing is enabled, then set up
+	// this.group_list.
+	if (this.useSubCollapsing)
+		this.SetUpGrpListWithCollapsedSubBoards();
+
 	if (this.useLightbarInterface && console.term_supports(USER_ANSI))
-		this.SelectMsgArea_Lightbar(pChooseGroup);
+		return this.SelectMsgArea_Lightbar(pChooseGroup ? 1 : 2, pGrpIdx);
 	else
-		this.SelectMsgArea_Traditional(pChooseGroup);
+		return this.SelectMsgArea_Traditional(pChooseGroup ? 1 : 2, pGrpIdx);
+}
+
+// For the DDMsgAreaChooser class: Displays the header & header lines above the list.
+//
+// Parameters:
+//  pScreenRow: The row on the screen to write the lines at.  If no cursor movements are desired, this can be null.
+//  pChooseGroup: Boolean - Whether or not the user is choosing a message group
+//  pGrpIdx: The index of the message group being used
+//  pNumPages: Optional - The number of pages of items
+//  pPageNum: Optional - The current page number for the items
+function DDMsgAreaChooser_DisplayListHdrLines(pScreenRow, pChooseGroup, pGrpIdx, pNumPages, pPageNum)
+{
+	this.DisplayAreaChgHdr(1);
+	if (typeof(pScreenRow) === "number")
+		console.gotoxy(1, pScreenRow);
+	if (pChooseGroup)
+		this.WriteGrpListHdrLine(pNumPages, pPageNum);
+	else
+	{
+		// For the number of items in the sub-board list, use the text "Posts" or "Items", depending
+		// on whether sub-board collapsing is enabled and there are sub-subboard for the given group
+		// & sub-board index
+		var numItemsText = "Posts";
+		if (this.useSubCollapsing)
+		{
+			for (var subIdx in this.group_list[pGrpIdx].sub_list)
+			{
+				if (typeof(this.group_list[pGrpIdx].sub_list[subIdx].sub_subboard_list) !== "undefined" && this.group_list[pGrpIdx].sub_list[subIdx].sub_subboard_list.length > 0)
+				{
+					numItemsText = "Items";
+					break;
+				}
+			}
+		}
+		// Write the list header lines
+		this.WriteSubBrdListHdr1Line(pGrpIdx);
+		if (typeof(pScreenRow) === "number")
+			console.gotoxy(1, pScreenRow+2);
+		if (this.showDatesInSubBoardList)
+			printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + numItemsText, "Latest date & time");
+		else
+			printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + numItemsText);
+	}
 }
 
 // For the DDMsgAreaChooser class: Lets the user choose a message group and
 // sub-board via numeric input, using a lightbar user interface.
 //
 // Parameters:
-//  pChooseGroup: Boolean - Whether or not to choose the message group.  If false,
-//                then this will allow choosing a sub-board within the user's
-//                current message group.  This is optional; defaults to true.
+//  pLevel: The file heirarchy level:
+//          1: Message groups
+//          2: Sub-boards within message groups
+//          3: "Sub-subboards" within sub-boards (if sub-board name collapsing is enabled)
+//          This is optional and defaults to 1.
 //  pGrpIdx: Optional - The group index, if choosing a sub-board
-function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup, pGrpIdx)
+function DDMsgAreaChooser_SelectMsgArea_Lightbar(pLevel, pGrpIdx, pSubIdx)
 {
 	// If there are no message groups or sub-boards, then don't let the user
 	// choose one.
@@ -404,10 +518,12 @@ function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup, pGrpIdx)
 		console.print("\1y\1hThere are no message groups.\r\n\1p");
 		return;
 	}
-	var chooseGroup = (typeof(pChooseGroup) == "boolean" ? pChooseGroup : true);
-	if (!chooseGroup)
+	var level = (typeof(pLevel) === "number" ? pLevel : 1);
+	if ((level < 1) || (level > 3))
+		return;
+	else if (level == 1)
 	{
-		if (typeof(pGrpIdx) != "number")
+		if (typeof(pGrpIdx) !== "number")
 			return;
 		if (msg_area.grp_list[pGrpIdx].sub_list.length == 0)
 		{
@@ -416,32 +532,29 @@ function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup, pGrpIdx)
 			return;
 		}
 	}
-
-	// Displays the header & header lines above the list
-	function displayListHdrLines(pScreenRow, pChooseMsgGrp, pAreaChooser, pGrpIdx, pNumPages, pPageNum)
+	// 2: Choose a sub-board within a message group
+	// 3: Choose a sub-subboard within a sub-board, for sub-board name collapsing
+	else if ((level == 2) || (level == 3))
 	{
-		pAreaChooser.DisplayAreaChgHdr(1);
-		console.gotoxy(1, pScreenRow);
-		if (pChooseMsgGrp)
-			pAreaChooser.WriteGrpListHdrLine(pNumPages, pPageNum);
-		else
+		if (typeof(pGrpIdx) !== "number")
+			return;
+		if (msg_area.grp_list[pGrpIdx].sub_list.length == 0)
 		{
-			pAreaChooser.WriteSubBrdListHdr1Line(pGrpIdx);
-			console.gotoxy(1, pScreenRow+2);
-			if (pAreaChooser.showDatesInSubBoardList)
-				printf(pAreaChooser.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
-			else
-				printf(pAreaChooser.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts");
+			console.clear("\1n");
+			console.print("\1y\1hThere are no sub-boards in " + file_area.lib_list[pLibIdx].description + ".\r\n\1p");
+			return;
 		}
 	}
 
+	var chooseGroup = (pLevel == 1);
+
 	// Clear the screen, write the header, help line, and list header(s)
 	console.clear("\1n");
-	displayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, this, pGrpIdx);
+	this.DisplayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, pGrpIdx);
 	this.WriteKeyHelpLine();
 
 	// Create the menu and do the user input loop
-	var msgAreaMenu = (chooseGroup ? this.CreateLightbarMsgGrpMenu() : this.CreateLightbarSubBoardMenu(pGrpIdx));
+	var msgAreaMenu = (chooseGroup ? this.CreateLightbarMsgGrpMenu() : this.CreateLightbarSubBoardMenu(pLevel, pGrpIdx, pSubIdx));
 	var drawMenu = true;
 	var lastSearchText = "";
 	var lastSearchFoundIdx = -1;
@@ -453,8 +566,8 @@ function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup, pGrpIdx)
 		chosenIdx = -1;
 		var returnedMenuIdx = msgAreaMenu.GetVal(drawMenu);
 		drawMenu = true;
-		var lastUserInputUpper = (typeof(msgAreaMenu.lastUserInput) == "string" ? msgAreaMenu.lastUserInput.toUpperCase() : msgAreaMenu.lastUserInput);
-		if (typeof(returnedMenuIdx) == "number")
+		var lastUserInputUpper = (typeof(msgAreaMenu.lastUserInput) === "string" ? msgAreaMenu.lastUserInput.toUpperCase() : "");
+		if (typeof(returnedMenuIdx) === "number")
 			chosenIdx = returnedMenuIdx;
 		// 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
@@ -480,9 +593,9 @@ function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup, pGrpIdx)
 				var oldSelectedItemIdx = msgAreaMenu.selectedItemIdx;
 				var idx = -1;
 				if (chooseGroup)
-					idx = findMsgGrpIdxFromText(searchText, msgAreaMenu.selectedItemIdx);
+					idx = this.FindMsgGrpIdxFromText(searchText, msgAreaMenu.selectedItemIdx);
 				else
-					idx = findSubBoardIdxFromText(pGrpIdx, searchText, msgAreaMenu.selectedItemIdx+1);
+					idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, msgAreaMenu.selectedItemIdx+1);
 				lastSearchFoundIdx = idx;
 				if (idx > -1)
 				{
@@ -503,9 +616,9 @@ function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup, pGrpIdx)
 				else
 				{
 					if (chooseGroup)
-						idx = findMsgGrpIdxFromText(searchText, 0);
+						idx = this.FindMsgGrpIdxFromText(searchText, 0);
 					else
-						idx = findSubBoardIdxFromText(pGrpIdx, searchText, 0);
+						idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, 0);
 					lastSearchFoundIdx = idx;
 					if (idx > -1)
 					{
@@ -548,9 +661,9 @@ function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup, pGrpIdx)
 				// indicated by the search.
 				var idx = 0;
 				if (chooseGroup)
-					idx = findMsgGrpIdxFromText(searchText, lastSearchFoundIdx+1);
+					idx = this.FindMsgGrpIdxFromText(searchText, lastSearchFoundIdx+1);
 				else
-					idx = findSubBoardIdxFromText(pGrpIdx, searchText, lastSearchFoundIdx+1);
+					idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, lastSearchFoundIdx+1);
 				if (idx > -1)
 				{
 					lastSearchFoundIdx = idx;
@@ -575,9 +688,9 @@ function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup, pGrpIdx)
 				else
 				{
 					if (chooseGroup)
-						idx = findMsgGrpIdxFromText(searchText, 0);
+						idx = this.FindMsgGrpIdxFromText(searchText, 0);
 					else
-						idx = findSubBoardIdxFromText(pGrpIdx, searchText, 0);
+						idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, 0);
 					lastSearchFoundIdx = idx;
 					if (idx > -1)
 					{
@@ -621,7 +734,7 @@ function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup, pGrpIdx)
 			// Refresh the screen
 			console.clear("\1n");
 			this.DisplayAreaChgHdr(1);
-			displayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, this, pGrpIdx);
+			this.DisplayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, pGrpIdx);
 			this.WriteKeyHelpLine();
 		}
 		// If the user entered a numeric digit, then treat it as
@@ -643,12 +756,12 @@ function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup, pGrpIdx)
 			{
 				// 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, chooseGroup, this, pGrpIdx);
+				this.DisplayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, pGrpIdx);
 				this.WriteKeyHelpLine();
 			}
 		}
 
-		// If a group/sub-board was chosen, then deal with it.
+		// If a group/sub-board/sub-subboard was chosen, then deal with it.
 		if (chosenIdx > -1)
 		{
 			// If choosing a message group, then let the user choose a
@@ -664,22 +777,75 @@ function DDMsgAreaChooser_selectMsgArea_Lightbar(pChooseGroup, pGrpIdx)
 				// 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)
+				var defaultSubIdx = chosenIdx == bbs.curgrp ? bbs.cursub : 0;
+				var subCodeBackup = bbs.cursub_code;
+				var chosenSubBoardIdx = this.SelectMsgArea_Lightbar(2, chosenIdx, defaultSubIdx);
+				if (typeof(chosenSubBoardIdx) === "number" && chosenSubBoardIdx > -1)
 				{
 					// Set the current sub-board
-					bbs.cursub_code = msg_area.grp_list[chosenIdx].sub_list[chosenSubBoardIdx].code;
+					if (this.useSubCollapsing)
+						bbs.cursub_code = this.group_list[chosenIdx].sub_list[chosenSubBoardIdx].code;
+					else
+						bbs.cursub_code = msg_area.grp_list[chosenIdx].sub_list[chosenSubBoardIdx].code;
 					continueOn = false;
 				}
 				else
 				{
+					// If using sub-board name collapsing or the sub-board changed (probably
+					// at level 3 because sub-board collapsing is enabled), then exit here.
+					if (this.useSubCollapsing || bbs.cursub_code != subCodeBackup)
+						continueOn = false;
+					else
+					{
+						// A message sub-board was not chosen, so we'll have to re-draw
+						// the header and key help line
+						this.DisplayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, pGrpIdx);
+						this.WriteKeyHelpLine();
+					}
+					// TODO?
+					/*
 					// A sub-board was not chosen, so we'll have to re-draw
 					// the header and list of message groups.
-					displayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, this, pGrpIdx);
+					this.DisplayListHdrLines(this.areaChangeHdrLines.length, chooseGroup, pGrpIdx);
+					*/
 				}
 			}
-			else
-				return chosenIdx; // Return the chosen sub-board index
+			else if (level == 2) // Choosing a sub-board
+			{
+				if (this.useSubCollapsing)
+				{
+					// Ensure this.group_list is set up
+					this.SetUpGrpListWithCollapsedSubBoards();
+
+					// If the current file directory has subdirectories,
+					// let the user choose one
+					// pGrpIdx is the group index, and chosenIdx is the sub-board index
+					if ((typeof(this.group_list[pGrpIdx].sub_list[chosenIdx].sub_subboard_list) !== "undefined") && (this.group_list[pGrpIdx].sub_list[chosenIdx].sub_subboard_list.length > 0))
+					{
+						var chosenSubSubBoardIdx = this.SelectMsgArea_Lightbar(3, pGrpIdx, chosenIdx);
+						if (chosenSubSubBoardIdx > -1)
+						{
+							// Set the current message sub-board
+							bbs.cursub_code = this.group_list[pGrpIdx].sub_list[chosenIdx].sub_subboard_list[chosenSubSubBoardIdx].code;
+							continueOn = false;
+						}
+						else
+						{
+							// A file directory was not chosen, so we'll have to re-draw
+							// the header and list of message groups.
+							// TODO:
+							//this.DisplayListHdrLines(level, pLibIdx);
+							//this.WriteKeyHelpLine();
+						}
+					}
+					else // No subdirectories - Return the chosen index
+						return chosenIdx;
+				}
+				else
+					return chosenIdx; // Return the chosen file directory index
+			}
+			else if (level == 3)
+				return chosenIdx; // Return the chosen subdirectory index
 		}
 	}
 }
@@ -738,7 +904,17 @@ function DDMsgAreaChooser_CreateLightbarMsgGrpMenu()
 		var menuItemObj = this.MakeItemWithRetval(-1);
 		if ((pGrpIndex >= 0) && (pGrpIndex < msg_area.grp_list.length))
 		{
-			menuItemObj.text = (((typeof(bbs.curgrp) == "number") && (pGrpIndex == msg_area.sub[bbs.cursub_code].grp_index)) ? "*" : " ");
+			var showAreaMark = false;
+			if (this.areaChooser.useSubCollapsing)
+			{
+				//this.group_list[pGrpIndex].sub_list[pSubIndex]
+				showAreaMark = ((typeof(bbs.curgrp) === "number") && (pGrpIndex == msg_area.sub[bbs.cursub_code].grp_index));
+			}
+			else
+			{
+				showAreaMark = ((typeof(bbs.curgrp) === "number") && (pGrpIndex == msg_area.sub[bbs.cursub_code].grp_index));
+			}
+			menuItemObj.text = (showAreaMark ? "*" : " ");
 			menuItemObj.text += format(this.areaChooser.msgGrpListPrintfStr, +(pGrpIndex+1),
 			                           msg_area.grp_list[pGrpIndex].description.substr(0, this.areaChooser.msgGrpDescLen),
 			                           msg_area.grp_list[pGrpIndex].sub_list.length);
@@ -761,11 +937,13 @@ function DDMsgAreaChooser_CreateLightbarMsgGrpMenu()
 // choosing a sub-board in lightbar mode.
 //
 // Parameters:
+//  pLevel: The level of menu (1=Group, 2=Sub-board, 3=Sub board within sub-board if sub-board name collapsing is enabled)
 //  pGrpIdx: The index of the message group
+//  pSubIdx: The sub-board index (for sub-board name collapsing)
 //
 // Return value: A DDLightbarMenu object for choosing a sub-board within
 // the given message group
-function DDMsgAreaChooser_CreateLightbarSubBoardMenu(pGrpIdx)
+function DDMsgAreaChooser_CreateLightbarSubBoardMenu(pLevel, pGrpIdx, pSubIdx)
 {
 	// Start & end indexes for the various items in each mssage group list row
 	// Selected mark, group#, description, # sub-boards
@@ -817,36 +995,146 @@ function DDMsgAreaChooser_CreateLightbarSubBoardMenu(pGrpIdx)
 	// to the menu
 	subBoardMenu.areaChooser = this; // Add this object to the menu object
 	subBoardMenu.grpIdx = pGrpIdx;
-	subBoardMenu.NumItems = function() {
-		return msg_area.grp_list[this.grpIdx].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))
+	if (this.useSubCollapsing)
+	{
+		if (pLevel == 2)
 		{
-			var showSubBoardMark = false;
-			if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
-				showSubBoardMark = ((this.grpIdx == msg_area.sub[bbs.cursub_code].grp_index) && (pSubIdx == msg_area.sub[bbs.cursub_code].index));
-			menuItemObj.text = strip_ctrl(this.areaChooser.GetMsgSubBrdLine(this.grpIdx, pSubIdx, false));
-			menuItemObj.retval = pSubIdx;
-		}
+			subBoardMenu.NumItems = function() {
+				return this.areaChooser.group_list[this.grpIdx].sub_list.length;
+			};
+			subBoardMenu.GetItem = function(pSubIdx) {
+				var menuItemObj = this.MakeItemWithRetval(-1);
+				var subIdxValid = true;
+				/*
+				if (this.areaChooser.useSubCollapsing)
+							showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +pSubIdx);
+				*/
+				if ((pSubIdx >= 0) && (pSubIdx < this.areaChooser.group_list[this.grpIdx].sub_list.length))
+				{
+					var showSubBoardMark = false;
+					if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
+					{
+						showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +pSubIdx);
+						/*
+						if (this.areaChooser.useSubCollapsing)
+							showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +pSubIdx);
+						else
+							showSubBoardMark = ((this.grpIdx == msg_area.sub[bbs.cursub_code].grp_index) && (pSubIdx == msg_area.sub[bbs.cursub_code].index));
+						*/
+					}
+					// Set the sub-board description.  And if it has sub-subboards,
+					// then append some text indicating so.
+					var subDesc = this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].description;
+					var numItems = 0;
+					var lastMsgPostTimestamp = 0;
+					var subSubBoardListExists = false;
+					if (Array.isArray(this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].sub_subboard_list) && this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0)
+					{
+						subSubBoardListExists = true;
+						subDesc += "  <subsubs>";
+						numItems = this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].sub_subboard_list.length;
+					}
+					// Get information from the messagebase
+					var msgBase = new MsgBase(this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].code);
+					if (msgBase.open())
+					{
+						if (this.areaChooser.showDatesInSubBoardList)
+						{
+							var latestMsgHdr = getLatestMsgHdrWithMsgbase(msgBase, 100); // One of the last 100 messages should be readable
+							if (latestMsgHdr != null)
+								lastMsgPostTimestamp = latestMsgHdr.when_written_time; // when_imported_time
+						}
+						if (!subSubBoardListExists)
+						{
+							// There is no sub-subboard list, so this is just a regular sub-board.
+							// Get the number of readable messages in the sub-board.
+							// TODO: This can take a long time
+							//numItems = numReadableMsgs(msgBase, this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].code);
+							// Fast but could be inaccurate due to counting deleted messages,
+							// vote responses, etc..
+							numItems = msgBase.total_msgs;
+						}
+						msgBase.close();
+					}
 
-		return menuItemObj;
-	};
+					menuItemObj.text = (showSubBoardMark ? "*" : " ");
+					if (this.areaChooser.showDatesInSubBoardList)
+					{
+						menuItemObj.text += format(this.areaChooser.subBoardListPrintfInfo[this.grpIdx].printfStr, +(pSubIdx+1),
+						                           subDesc.substr(0, this.areaChooser.descFieldLen), numItems,
+						                           strftime("%Y-%m-%d", lastMsgPostTimestamp),
+						                           strftime("%H:%M:%S", lastMsgPostTimestamp));
+					}
+					else
+					{
+						menuItemObj.text += format(this.areaChooser.subBoardListPrintfInfo[this.grpIdx].printfStr, +(pSubIdx+1),
+						                           subDesc.substr(0, this.areaChooser.descFieldLen), numItems);
+					}
+					menuItemObj.text = strip_ctrl(menuItemObj.text);
+					menuItemObj.retval = pSubIdx;
+				}
 
-	// Set the currently selected item.  If the current sub-board is in this list,
-	// then set the selected item to that; otherwise, the selected item should be
-	// the first sub-board.
-	if (msg_area.sub[bbs.cursub_code].grp_index == pGrpIdx)
-	{
-		subBoardMenu.selectedItemIdx = msg_area.sub[bbs.cursub_code].index;
-		if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage())
-			subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1;
+				return menuItemObj;
+			};
+		}
+		else if (pLevel == 3)
+		{
+			subBoardMenu.subIdx = pSubIdx;
+			subBoardMenu.NumItems = function() {
+				return this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list.length;
+			};
+			subBoardMenu.GetItem = function(pSubSubIdx) {
+				var menuItemObj = this.MakeItemWithRetval(-1);
+				if ((pSubSubIdx >= 0) && (pSubSubIdx < this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list.length))
+				{
+					var showSubBoardMark = false;
+					if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
+						showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +(this.subIdx));
+					menuItemObj.text = (showSubBoardMark ? "*" : " ");
+					var subdirDesc = this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list[pSubSubIdx].description;
+					var subdirDirIdx = this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list[pSubSubIdx].index;
+					var subCode = this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list[pSubSubIdx].code;
+					menuItemObj.text = strip_ctrl(this.areaChooser.GetMsgSubBrdLine(this.grpIdx, msg_area.sub[subCode].index, false));
+					menuItemObj.retval = pSubSubIdx;
+				}
+
+				return menuItemObj;
+			}
+		}
 	}
 	else
 	{
-		subBoardMenu.selectedItemIdx = 0;
-		subBoardMenu.topItemIdx = 0;
+		subBoardMenu.NumItems = function() {
+			return msg_area.grp_list[this.grpIdx].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 showSubBoardMark = false;
+				if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
+					showSubBoardMark = ((this.grpIdx == msg_area.sub[bbs.cursub_code].grp_index) && (pSubIdx == msg_area.sub[bbs.cursub_code].index));
+				menuItemObj.text = strip_ctrl(this.areaChooser.GetMsgSubBrdLine(this.grpIdx, pSubIdx, false));
+				menuItemObj.retval = pSubIdx;
+			}
+
+			return menuItemObj;
+		};
+
+		// Set the currently selected item.  If the current sub-board is in this list,
+		// then set the selected item to that; otherwise, the selected item should be
+		// the first sub-board.
+		if (msg_area.sub[bbs.cursub_code].grp_index == pGrpIdx)
+		{
+			subBoardMenu.selectedItemIdx = msg_area.sub[bbs.cursub_code].index;
+			if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage())
+				subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1;
+		}
+		else
+		{
+			subBoardMenu.selectedItemIdx = 0;
+			subBoardMenu.topItemIdx = 0;
+		}
 	}
 
 	return subBoardMenu;
@@ -859,7 +1147,7 @@ function DDMsgAreaChooser_CreateLightbarSubBoardMenu(pGrpIdx)
 //  pChooseGroup: Boolean - Whether or not to choose the message group.  If false,
 //                then this will allow choosing a sub-board within the user's
 //                current message group.  This is optional; defaults to true.
-function DDMsgAreaChooser_selectMsgArea_Traditional(pChooseGroup)
+function DDMsgAreaChooser_SelectMsgArea_Traditional(pChooseGroup)
 {
 	// If there are no message groups, then don't let the user
 	// choose one.
@@ -963,7 +1251,7 @@ function DDMsgAreaChooser_selectMsgArea_Traditional(pChooseGroup)
 //               subBoardIndex: Numeric - The sub-board that was chosen (if any).
 //                              Will be -1 if none chosen.
 //               subBoardCode: The internal code of the chosen sub-board (or "" if none chosen)
-function DDMsgAreaChooser_selectSubBoard_Traditional(pGrpIdx, pDefaultSubBoardIdx)
+function DDMsgAreaChooser_SelectSubBoard_Traditional(pGrpIdx, pDefaultSubBoardIdx)
 {
 	var retObj = {
 		subBoardChosen: false,
@@ -979,7 +1267,7 @@ function DDMsgAreaChooser_selectSubBoard_Traditional(pGrpIdx, pDefaultSubBoardId
 		this.DisplayAreaChgHdr(1);
 		if (this.areaChangeHdrLines.length > 0)
 			console.crlf();
-		this.ListSubBoardsInMsgGroup(pGrpIdx, defaultSubBoardIdx, searchText);
+		this.ListSubBoardsInMsgGroup(pGrpIdx, null, defaultSubBoardIdx, searchText);
 		console.crlf();
 		if (defaultSubBoardIdx >= 0)
 			console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c, or [\1h" + +(defaultSubBoardIdx+1) + "\1n\1c]: \1h");
@@ -1020,10 +1308,144 @@ function DDMsgAreaChooser_selectSubBoard_Traditional(pGrpIdx, pDefaultSubBoardId
 		// If a sub-board was chosen, then select it.
 		if (selectedSubBoard > 0)
 		{
-			retObj.subBoardChosen = true;
-			retObj.subBoardIndex = selectedSubBoard - 1;
-			retObj.subBoardCode = msg_area.grp_list[pGrpIdx].sub_list[retObj.subBoardIndex].code;
+			var selectedSubIdx = selectedSubBoard - 1;
+			// If using sub-board name collapsing and the selected sub-board has sub-subboards, then
+			// let the user choose a sub-subboard within the sub-board.  Otherwise, just select the
+			// current sub-board.
+			if (this.useSubCollapsing && this.group_list[pGrpIdx].sub_list[selectedSubIdx].sub_subboard_list.length > 0)
+			{
+				var subSubRetObj = this.SelectSubSubWithinSub_Traditional(pGrpIdx, selectedSubIdx);
+				if (subSubRetObj.areaSelected)
+				{
+					retObj.subBoardChosen = true;
+					retObj.subBoardIndex = subSubRetObj.subIndex;
+					retObj.subBoardCode = subSubRetObj.subCode;
+					continueOn = false;
+				}
+				else // An area wasn't chosen
+				{
+					continueOn = true;
+					console.clear("\1n");
+				}
+			}
+			else
+			{
+				retObj.subBoardChosen = true;
+				retObj.subBoardIndex = selectedSubIdx;
+				retObj.subBoardCode = msg_area.grp_list[pGrpIdx].sub_list[selectedSubIdx].code;
+				continueOn = false;
+			}
+		}
+	} while (continueOn);
+
+	return retObj;
+}
+
+// For the DDMsgAreaChooser class: Lets the user select a sub-subbboard within a
+// message sub-board - Traditional user interface.  This is meant for sub-board
+// name collapsing, at the 3rd level.
+//
+// Parameters:
+//  pGrpIdx: The message group index
+//  pSubIdx: The index of the sub-board within the message group
+//
+// Return value: An object containing the following properties:
+//               areaSelected: Boolean - Whether or not the user chose a sub-subboard.
+//               subIndex: The index of the sub-board in Synchronet's sub_list array in the group
+//               subCode: The internal code of the sub-board chosen, if chose.  If not chosen,
+//                        this will be an empty string.
+function DDMsgAreaChooser_SelectSubSubWithinSub_Traditional(pGrpIdx, pSubIdx)
+{
+	var retObj = {
+		areaSelected: false,
+		subIndex: -1,
+		subCode: ""
+	};
+
+	if (!this.useSubCollapsing || this.group_list.length == 0)
+		return retObj;
+	if ((pGrpIdx < 0) || (pGrpIdx >= this.group_list.length))
+		return retObj;
+	if ((pSubIdx < 0) || (pSubIdx >= this.group_list[pGrpIdx].sub_list.length))
+	{
+		console.clear("\1n");
+		console.print("\1y\1hThere are no sub-boards in this message group.\r\n\1p");
+		return retObj;
+	}
+	if (this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length == 0)
+	{
+		console.clear("\1n");
+		console.print("\1y\1hThere are no sub-subboards in this sub-board.\r\n\1p");
+		return retObj;
+	}
+
+	// Gets the default sub-subdirectory number (1-based)
+	function getDefaultSubSubNum(pGrpList, pGrpIdx, pSubIdx)
+	{
+		var subSubNum = 0; // Will be 1-based
+		for (var subSubIdx = 0; subSubIdx < pGrpList[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length; ++subSubIdx)
+		{
+			if (bbs.curdir_code == pGrpList[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[subSubIdx].code)
+			{
+				subSubNum = subSubIdx + 1;
+				break;
+			}
+		}
+		return subSubNum;
+	}
+
+	// defaultSubSubNum is the default sub-subboard # (will be 1-based)
+	var defaultSubSubNum = getDefaultSubSubNum(this.group_list, pGrpIdx, pSubIdx);
+	var searchText = "";
+	var continueOn = false;
+	do
+	{
+		console.clear("\1n");
+		this.DisplayAreaChgHdr(1);
+		if (this.areaChangeHdrLines.length > 0)
+			console.crlf();
+		// Note: This will list sub-subboards within the sub-board with a valid sub-board index
+		// as the 2nd parameter.
+		// TODO: When quitting out of the sub-suboard list, it's going back to the
+		// message group list
+		this.ListSubBoardsInMsgGroup(pGrpIdx, pSubIdx, defaultSubSubNum, searchText);
+		if (defaultSubSubNum >= 1)
+			console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c, or [\1h" + defaultSubSubNum + "\1n\1c]: \1h");
+		else
+			console.print("\1n\1b\1h� \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c: \1h");
+		// Accept Q (quit), / or CTRL_F to search, or a file sub-board number
+		var selectedSubSubNum = console.getkeys("Q/" + CTRL_F, this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length);
+
+		// If the user just pressed enter (selectedSubSubNum would be blank),
+		// default the selected sub-board.
+		if (selectedSubSubNum.toString() == "Q")
+			continueOn = false;
+		else if (selectedSubSubNum.toString() == "")
+			selectedSubSubNum = defaultSubSubNum;
+		else if ((selectedSubSubNum == "/") || (selectedSubSubNum == CTRL_F))
+		{
+			// Search
+			console.crlf();
+			var searchPromptText = "\1n\1c\1hSearch\1g: \1n";
+			console.print(searchPromptText);
+			searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE);
+			console.print("\1n");
+			console.crlf();
+			if (searchText.length > 0)
+				defaultSubSubNum = -1;
+			else
+				defaultSubSubNum = getDefaultSubSubNum(this.group_list, pGrpIdx, pSubIdx);
+			continueOn = true;
+			console.line_counter = 0; // To avoid pausing before the clear screen
+		}
+
+		// If the user chose a sub-board, then set the user's message sub-board.
+		if (selectedSubSubNum > 0)
+		{
 			continueOn = false;
+			retObj.areaSelected = true;
+			retObj.subCode = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[selectedSubSubNum-1].code;
+			retObj.subIndex = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[selectedSubSubNum-1].index;
 		}
 	} while (continueOn);
 
@@ -1035,7 +1457,7 @@ function DDMsgAreaChooser_selectSubBoard_Traditional(pGrpIdx, pDefaultSubBoardId
 //
 // Parameters:
 //  pSearchText: Optional - Search text for the message groups
-function DDMsgAreaChooser_listMsgGrps_Traditional(pSearchText)
+function DDMsgAreaChooser_ListMsgGrps_Traditional(pSearchText)
 {
 	// Print the header
 	this.WriteGrpListHdrLine();
@@ -1060,11 +1482,15 @@ function DDMsgAreaChooser_listMsgGrps_Traditional(pSearchText)
 	}
 }
 
-// For the DDMsgAreaChooser class: Lists the sub-boards in a message group,
-// for the traditional user interface.
+// For the DDMsgAreaChooser class: Lists the sub-boards in a message group
+// (or sub-subboards in a sub-board, for sub-board name collapsing), for the
+// traditional user interface.
 //
 // Parameters:
 //  pGrpIndex: The index of the message group (0-based)
+//  pSubIdx: Optional - For sub-board name collapsing, this is the (0-based)
+//           index of the sub-board to show sub-subboards for.  To ignore this
+//           and only show sub-boards of the given group, this can be null or -1.
 //  pMarkIndex: An index of a message group to highlight.  This
 //                   is optional; if left off, this will default to
 //                   the current sub-board.
@@ -1074,19 +1500,19 @@ function DDMsgAreaChooser_listMsgGrps_Traditional(pSearchText)
 //             "dateAsc": Sort by date, ascending
 //             "dateDesc": Sort by date, descending
 //             "description": Sort by description
-function DDMsgAreaChooser_listSubBoardsInMsgGroup_Traditional(pGrpIndex, pMarkIndex, pSearchText, pSortType)
+function DDMsgAreaChooser_ListSubBoardsInMsgGroup_Traditional(pGrpIndex, pSubIdx, pMarkIndex, pSearchText, pSortType)
 {
 	// Default to the current message group & sub-board if pGrpIndex
 	// and pMarkIndex aren't specified.
 	var grpIndex = 0;
 	if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
 		grpIndex = msg_area.sub[bbs.cursub_code].grp_index;
-	if ((pGrpIndex != null) && (typeof(pGrpIndex) == "number"))
+	if ((pGrpIndex != null) && (typeof(pGrpIndex) === "number"))
 		grpIndex = pGrpIndex;
 	var highlightIndex = 0;
 	if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
 		highlightIndex = (pGrpIndex == msg_area.sub[bbs.cursub_code].index);
-	if ((pMarkIndex != null) && (typeof(pMarkIndex) == "number"))
+	if ((pMarkIndex != null) && (typeof(pMarkIndex) === "number"))
 		highlightIndex = pMarkIndex;
 
 	// Make sure grpIndex and highlightIndex are valid (they might not be for
@@ -1096,6 +1522,9 @@ function DDMsgAreaChooser_listSubBoardsInMsgGroup_Traditional(pGrpIndex, pMarkIn
 	if ((highlightIndex == null) || (typeof(highlightIndex) == "undefined"))
 		highlightIndex = 0;
 
+	// Check whether pSubIdx is valid
+	var subIdxValid = typeof(pSubIdx) === "number" && (pSubIdx > -1);
+
 	// Ensure that the sub-board printf information is created for
 	// this message group.
 	this.BuildSubBoardPrintfInfoForGrp(grpIndex);
@@ -1103,21 +1532,24 @@ function DDMsgAreaChooser_listSubBoardsInMsgGroup_Traditional(pGrpIndex, pMarkIn
 	// Print the headers
 	this.WriteSubBrdListHdr1Line(grpIndex);
 	console.crlf();
+	var itemsHdrStr = "Posts";
+	if (this.useSubCollapsing)
+		itemsHdrStr = subIdxValid ? "Posts" : "Items";
 	if (this.showDatesInSubBoardList)
-		printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
+		printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + itemsHdrStr, "Latest date & time");
 	else
-		printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts");
+		printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + itemsHdrStr);
 	console.print("\1n");
 
 	// Make the search text uppercase for case-insensitive matching
 	var searchTextUpper = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : "");
 
 	// List each sub-board in the message group.
-	var subBoardArray = null;       // For sorting, if desired
-	var newestDate = {}; // For storing the date of the newest post in a sub-board
-	var msgBase = null;    // For opening the sub-boards with a MsgBase object
-	var msgHeader = null;  // For getting the date & time of the newest post in a sub-board
-	var subBoardNum = 0;   // 0-based sub-board number (because the array index is the number as a str)
+	var subBoardArray = null; // For sorting, if desired
+	var newestDate = {};      // For storing the date of the newest post in a sub-board
+	var msgBase = null;       // For opening the sub-boards with a MsgBase object
+	var msgHeader = null;     // For getting the date & time of the newest post in a sub-board
+	var subBoardNum = 0;      // 0-based sub-board number (because the array index is the number as a str)
 	// If a sort type is specified, then add the sub-board information to
 	// subBoardArray so that it can be sorted.
 	if ((typeof(pSortType) == "string") && (pSortType != "") && (pSortType != "none"))
@@ -1125,37 +1557,67 @@ function DDMsgAreaChooser_listSubBoardsInMsgGroup_Traditional(pGrpIndex, pMarkIn
 		var addSubBoard = true;
 		subBoardArray = [];
 		var subBoardInfo = null;
-		for (var arrSubBoardNum in msg_area.grp_list[grpIndex].sub_list)
+		var subList = msg_area.grp_list[grpIndex].sub_list;
+		if (this.useSubCollapsing)
+		{
+			if (subIdxValid)
+				subList = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list;
+			else
+				subList = this.group_list[grpIndex].sub_list;
+		}
+		for (var subIdx in subList)
 		{
 			// Open the current sub-board with the msgBase object.
 			// If the search text is set, then use it to filter the sub-boards.
 			addSubBoard = true;
 			if (searchTextUpper.length > 0)
 			{
-				addSubBoard = ((msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].name.indexOf(searchTextUpper) >= 0) ||
-				               (msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.indexOf(searchTextUpper) >= 0));
+				if (this.useSubCollapsing && subIdxValid)
+				{
+					// For sub-board name collapsing, sub-subboards only have descriptions, no names
+					addSubBoard = (this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].description.indexOf(searchTextUpper) >= 0);
+				}
+				else
+				{
+					addSubBoard = ((msg_area.grp_list[grpIndex].sub_list[subIdx].name.indexOf(searchTextUpper) >= 0) ||
+					               (msg_area.grp_list[grpIndex].sub_list[subIdx].description.indexOf(searchTextUpper) >= 0));
+				}
 			}
 			if (addSubBoard)
 			{
-				msgBase = new MsgBase(msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
+				var subCode = "";
+				if (this.useSubCollapsing && subIdxValid)
+					subCode = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].code;
+				else
+					subCode = msg_area.grp_list[grpIndex].sub_list[subIdx].code;
+				msgBase = new MsgBase(subCode);
 				if (msgBase.open())
 				{
 					subBoardInfo = new MsgSubBoardInfo();
-					subBoardInfo.subBoardNum = +(arrSubBoardNum);
-					subBoardInfo.subBoardIdx = msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].index;
-					subBoardInfo.description = msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description;
-					//subBoardInfo.numPosts = numReadableMsgs(msgBase, msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
+					subBoardInfo.subBoardNum = +(subIdx);
+					if (this.useSubCollapsing && subIdxValid)
+					{
+						subBoardInfo.subBoardIdx = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].index;
+						subBoardInfo.description = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].description;
+					}
+					else
+					{
+						subBoardInfo.subBoardIdx = msg_area.grp_list[grpIndex].sub_list[subIdx].index;
+						subBoardInfo.description = msg_area.grp_list[grpIndex].sub_list[subIdx].description;
+					}
+
+					//subBoardInfo.numPosts = numReadableMsgs(msgBase, msg_area.grp_list[grpIndex].sub_list[subIdx].code);
 					subBoardInfo.numPosts = msgBase.total_msgs;
 
 					// Get the date & time when the last message was imported.
 					if (this.showDatesInSubBoardList && (subBoardInfo.numPosts > 0))
 					{
 						//var msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true);
-						var msgHeader = getLatestMsgHdr(msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
+						var msgHeader = getLatestMsgHdr(msg_area.grp_list[grpIndex].sub_list[subIdx].code);
 						if (msgHeader === null)
 							msgHeader = getBogusMsgHdr();
 						if (this.showImportDates)
-							subBoardInfo.newestPostDate = msgHeader.when_imported_time
+							subBoardInfo.newestPostDate = msgHeader.when_imported_time;
 						else
 						{
 							var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader);
@@ -1253,79 +1715,216 @@ function DDMsgAreaChooser_listSubBoardsInMsgGroup_Traditional(pGrpIndex, pMarkIn
 	else
 	{
 		var includeSubBoard = true;
-		for (var arrSubBoardNum in msg_area.grp_list[grpIndex].sub_list)
+		//var subList = this.useSubCollapsing ? this.group_list[grpIndex].sub_list : msg_area.grp_list[grpIndex].sub_list;
+		var subList = msg_area.grp_list[grpIndex].sub_list;
+		if (this.useSubCollapsing)
+		{
+			if (subIdxValid)
+				subList = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list;
+			else
+				subList = this.group_list[grpIndex].sub_list;
+		}
+		for (var subIdx in subList)
 		{
 			// If the search text is set, then use it to filter the sub-board list.
 			includeSubBoard = true;
 			if (searchTextUpper.length > 0)
 			{
-				includeSubBoard = ((msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].name.toUpperCase().indexOf(searchTextUpper) >= 0) ||
-				                   (msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.toUpperCase().indexOf(searchTextUpper) >= 0));
+				if (this.useSubCollapsing && subIdxValid)
+				{
+					// For sub-board name collapsing, sub-subboards only have descriptions, no names
+					includeSubBoard = (this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].description.indexOf(searchTextUpper) >= 0);
+				}
+				else
+				{
+					includeSubBoard = ((msg_area.grp_list[grpIndex].sub_list[subIdx].name.toUpperCase().indexOf(searchTextUpper) >= 0) ||
+					                   (msg_area.grp_list[grpIndex].sub_list[subIdx].description.toUpperCase().indexOf(searchTextUpper) >= 0));
+				}
 			}
 			if (includeSubBoard)
 			{
-				// Open the current sub-board with the msgBase object.
-				msgBase = new MsgBase(msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
-				if (msgBase.open())
+				// Call GetSubBoardInfo() to get the information about the sub-board or sub-subboard.
+				// Make sure we pass the correct indexes.
+				var subInfo = null;
+				if (this.useSubCollapsing)
 				{
-					// Get the date & time when the last message was imported.
-					//var numMsgs = numReadableMsgs(msgBase, msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
-					var numMsgs = msgBase.total_msgs;
-					if (numMsgs > 0)
-					{
-						//var msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true);
-						var msgHeader = getLatestMsgHdr(msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
-						if (msgHeader === null)
-							msgHeader = getBogusMsgHdr();
-						// Construct the date & time strings of the latest post
-						if (this.showImportDates)
-						{
-							newestDate.date = strftime("%Y-%m-%d", msgHeader.when_imported_time);
-							newestDate.time = strftime("%H:%M:%S", msgHeader.when_imported_time);
-						}
-						else
-						{
-							var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader);
-							if (msgWrittenLocalBBSTime != -1)
-							{
-								newestDate.date = strftime("%Y-%m-%d", msgWrittenLocalBBSTime);
-								newestDate.time = strftime("%H:%M:%S", msgWrittenLocalBBSTime);
-							}
-							else
-							{
-								newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time);
-								newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time);
-							}
-						}
-					}
+					// For some reason subIdx is a string, so ensure it's a number when calling GetSubBoardInfo()
+					if (subIdxValid) // If pSubIdx is valid
+						subInfo = this.GetSubBoardInfo(grpIndex, pSubIdx, +subIdx);
 					else
-						newestDate.date = newestDate.time = "";
-
-					// Print the sub-board information
-					subBoardNum = +(arrSubBoardNum);
-					console.crlf();
-					console.print((subBoardNum == highlightIndex) ? "\1n" + this.colors.areaMark + "*" : " ");
-					if (this.showDatesInSubBoardList)
-					{
-						printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1),
-							   msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.substr(0, this.subBoardListPrintfInfo[grpIndex].nameLen),
-							   numMsgs, newestDate.date, newestDate.time);
-					}
+						subInfo = this.GetSubBoardInfo(grpIndex, +subIdx);
+				}
+				else
+					subInfo = this.GetSubBoardInfo(grpIndex, subIdx);
+				newestDate.date = strftime("%Y-%m-%d", subInfo.newestTime);
+				newestDate.time = strftime("%H:%M:%S", subInfo.newestTime);
+				// Print the sub-board information
+				subBoardNum = +(subIdx);
+				console.crlf();
+				var showSubBoardMark = false;
+				if (this.useSubCollapsing)
+				{
+					if (subIdxValid) // If using sub-subboards
+						showSubBoardMark = (bbs.cursub_code == subList[subIdx].code);
 					else
-					{
-						printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1),
-							   msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.substr(0, this.subBoardListPrintfInfo[grpIndex].nameLen),
-							   numMsgs);
-					}
+						showSubBoardMark = this.CurrentSubBoardIsInSubSubsForSub(grpIndex, +subIdx);
+				}
+				else
+					showSubBoardMark = (subBoardNum == highlightIndex);
+				console.print(showSubBoardMark ? "\1n" + this.colors.areaMark + "*" : " ");
+				if (this.showDatesInSubBoardList)
+				{
+					printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1),
+					       subInfo.desc.substr(0, this.subBoardListPrintfInfo[grpIndex].nameLen),
+					       subInfo.numItems, newestDate.date, newestDate.time);
+				}
+				else
+				{
+					printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1),
+					       subInfo.desc.substr(0, this.subBoardListPrintfInfo[grpIndex].nameLen),
+					       subInfo.numItems);
+				}
+			}
+		}
+	}
+}
+
+// For the DDMsgAreaChooser class: Returns whether the user's current selected sub-board is
+// one of the sub-subboards for a sub-board (if sub-board name collapsing is enabled), or
+// is the given sub-board in the message group.
+//
+// Parameters:
+//  pGrpIdx: The index of the message group (0-based)
+//  pSubIdx: The index of the sub-board within the message group (0-based)
+//
+// Return value: True/false, whether or not the user's current selected sub-board is one of
+//               the sub-subboards for a sub-board (if sub-board name collapsing is enabled)
+//               or is the given sub-board in the message group.
+function DDMsgAreaChooser_CurrentSubBoardIsInSubSubsForSub(pGrpIdx, pSubIdx)
+{
+	// Sanity checking
+	if (typeof(bbs.cursub_code) !== "string") // Rare case, for brand new user accounts
+		return false;
+	if (typeof(pGrpIdx) !== "number")
+		return false;
+	if (typeof(pSubIdx) !== "number")
+		return false;
+
+	var chosenSubMatch = false;
+	if (this.useSubCollapsing)
+	{
+		if (pGrpIdx >= 0 && pGrpIdx < this.group_list.length && pSubIdx >= 0 && pSubIdx < this.group_list[pGrpIdx].sub_list.length)
+		{
+			// If this sub-board has a list of sub-subboards, then go through the sub-subboards and see if the
+			// user's current sub-board is one of the sub-subboards.
+			if (typeof(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list) !== "undefined" && this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0)
+			{
+				for (var subSubIdx = 0; subSubIdx < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length && !chosenSubMatch; ++subSubIdx)
+					chosenSubMatch = (bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[subSubIdx].code);
+			}
+			else // There is no sub-subboard list for this sub-board
+				chosenSubMatch = (bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].code);
+		}
+	}
+	else
+	{
+		if (pGrpIdx >= 0 && pGrpIdx < msg_area.grp_list.length && pSubIdx >= 0 && pSubIdx < msg_area.grp_list[pGrpIdx].sub_list.length)
+			chosenSubMatch = (bbs.cursub_code == msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code);
+	}
+	return chosenSubMatch;
+}
+
+// For the DDMsgAreaChooser class: With a group & sub-board index, this function gets the date
+// & time of the latest posted message from a sub-board (or group of sub-boards, if using
+// sub-board name collapsing).  This function also gets the description of the sub-board (or
+// group of sub-boards if using sub-board name collapsing).
+//
+// Parameters:
+//  pGrpIdx: The index of the message group
+//  pSubIdx: The index of the sub-board in the message group (if using
+//           sub-board name collapsing, this could be the index of a set
+//           of sub-subboards).
+//  pSubSubIdx: Optional - When sub-board name collapsing is being used,
+//              this specifies the index of the sub-subboard within the subboard.
+//
+// Return value: An object containing the following properties:
+//               desc: The description of the sub-board (or group of sub-subboards if using name collapsing)
+//               numItems: The number of messages in the sub-board or number of sub-subboards in the group,
+//                         if using sub-board name collapsing
+//               subCode: The internal code of the sub-board (this will be an empty string if it's a group of sub-subboards)
+//               newestTime: A value containing the date & time of the newest post in the sub-board or group of sub-boards
+function DDMsgAreaChooser_GetSubBoardInfo(pGrpIdx, pSubIdx, pSubSubIdx)
+{
+	var retObj = {
+		desc: "",
+		numItems: 0,
+		subCode: "",
+		newestTime: 0
+	};
 
-					msgBase.close();
+	// If using sub-board name collapsing and the given group & sub-board indexes has a
+	// group of sub-subboards, then look through those sub-boards for information.
+	if (this.useSubCollapsing)
+	{
+		if (typeof(pSubSubIdx) === "number" && pSubSubIdx >= 0 && pSubSubIdx < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length)
+		{
+			retObj.desc = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].description;
+			retObj.subCode = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].code;
+			retObj.numItems = getNumMsgsInSubBoard(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].code);
+			retObj.newestTime = getLatestMsgTime(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].code);
+		}
+		else // pSubSubIdx wasn't specified or is invalid
+		{
+			retObj.numItems = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length;
+			retObj.desc = this.group_list[pGrpIdx].sub_list[pSubIdx].description;
+			if (this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0)
+			{
+				for (var subSubIdx = 0; subSubIdx < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list; ++subSubIdx)
+				{
+					var latestPostTime = getLatestMsgTime(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[subSubIdx].code);
+					if (latestPostTime > retObj.newestTime)
+						retObj.newestTime = latestPostTime;
 				}
+			}
+			else
+			{
+				// No sub-subboards in this sub-board
+				retObj.subCode = this.group_list[pGrpIdx].sub_list[pSubIdx].code;
+				retObj.numItems = getNumMsgsInSubBoard(this.group_list[pGrpIdx].sub_list[pSubIdx].code);
+				retObj.newestTime = getLatestMsgTime(this.group_list[pGrpIdx].sub_list[pSubIdx].code);
+			}
+		}
+	}
+	else // No sub-board name collapsing, or there are no sub-subboards in this sub-board
+	{
+		retObj.desc = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].description;
+		retObj.subCode = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code;
 
-				// Free some memory?
-				delete msgBase;
+		// Get the number of messages in the sub-board
+		//var numMsgs = numReadableMsgs(msgBase, msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code);
+		var numMsgs = getNumMsgsInSubBoard(msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code);
+		if (numMsgs > 0)
+		{
+			retObj.numItems = numMsgs;
+			//var msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true);
+			var msgHeader = getLatestMsgHdr(retObj.subCode);
+			if (msgHeader === null)
+				msgHeader = getBogusMsgHdr();
+			// Set the newest post time
+			if (this.showImportDates)
+				retObj.newestTime = msgHeader.when_imported_time;
+			else
+			{
+				var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader);
+				if (msgWrittenLocalBBSTime != -1)
+					retObj.newestTime = msgWrittenLocalBBSTime;
+				else
+					retObj.newestTime = msgHeader.when_written_time;
 			}
 		}
 	}
+
+	return retObj;
 }
 
 //////////////////////////////////////////////
@@ -1360,45 +1959,14 @@ function DDMsgAreaChooser_writeMsgGroupLine(pGrpIndex, pHighlight)
 // Message sub-board list stuff (lightbar mode) //
 //////////////////////////////////////////////////
 
-// Updates the page number text in the group list header line on the screen.
-//
-// Parameters:
-//  pPageNum: The page number
-//  pNumPages: The total number of pages
-//  pGroup: Boolean - Whether or not this is for the group header.  If so,
-//          then this will go to the right location for the group page text
-//          and use this.colors.header for the text.  Otherwise, this will
-//          go to the right place for the sub-board page text and use the
-//          sub-board header color.
-//  pRestoreCurPos: Optional - Boolean - If true, then move the cursor back
-//                  to the position where it was before this function was called
-function DDMsgAreaChooser_updatePageNumInHeader(pPageNum, pNumPages, pGroup, pRestoreCurPos)
-{
-	var originalCurPos = null;
-	if (pRestoreCurPos)
-		originalCurPos = console.getxy();
-
-	if (pGroup)
-	{
-		console.gotoxy(29, 1+this.areaChangeHdrLines.length);
-		console.print("\1n" + this.colors.header + pPageNum + " of " + pNumPages + ")   ");
-	}
-	else
-	{
-		console.gotoxy(51, 1+this.areaChangeHdrLines.length);
-		console.print("\1n" + this.colors.subBoardHeader + pPageNum + " of " + pNumPages + ")   ");
-	}
-
-	if (pRestoreCurPos)
-		console.gotoxy(originalCurPos);
-}
-
-// For the DDMsgAreaChooser class: Writes a message sub-board information line.
+// For the DDMsgAreaChooser class: Gets a message sub-board information line.
 //
 // Parameters:
 //  pGrpIndex: The index of the message group (assumed to be valid)
 //  pSubIndex: The index of the sub-board within the message group to write (assumed to be valid)
 //  pHighlight: Boolean - Whether or not to write the line highlighted.
+
+
 function DDMsgAreaChooser_GetMsgSubBrdLine(pGrpIndex, pSubIndex, pHighlight)
 {
 	var subBoardLine = "\1n";
@@ -1560,6 +2128,13 @@ function DDMsgAreaChooser_ReadConfigFile()
 					}
 					else if (settingUpper == "SHOWDATESINSUBBOARDLIST")
 						this.showDatesInSubBoardList = (value.toUpperCase() == "TRUE");
+					else if (settingUpper == "USESUBCOLLAPSING")
+						this.useSubCollapsing = (value.toUpperCase() == "TRUE");
+					else if (settingUpper == "SUBCOLLAPSESEPARATOR")
+					{
+						if (value.length > 0)
+							this.subCollapseSeparator = value;
+					}
 				}
 				else if (settingsMode == "colors")
 					this.colors[setting] = value;
@@ -1639,7 +2214,7 @@ function DDMsgAreaChooser_showHelpScreen(pLightbar, pClearScreen)
 //
 // Parameters:
 //  pGrpIndex: The index of the message group
-function DDMsgAreaChooser_buildSubBoardPrintfInfoForGrp(pGrpIndex)
+function DDMsgAreaChooser_BuildSubBoardPrintfInfoForGrp(pGrpIndex)
 {
 	// If the array of sub-board printf strings doesn't contain the printf
 	// strings for this message group, then figure out the largest number
@@ -1708,7 +2283,7 @@ function DDMsgAreaChooser_DisplayAreaChgHdr(pStartScreenRow, pClearRowsFirst)
 
 	// If the user's terminal supports ANSI and pStartScreenRow is a number, then
 	// we can move the cursor and display the header where specified.
-	if (console.term_supports(USER_ANSI) && (typeof(pStartScreenRow) == "number"))
+	if (console.term_supports(USER_ANSI) && (typeof(pStartScreenRow) === "number"))
 	{
 		// If specified to clear the rows first, then do so.
 		var screenX = 1;
@@ -1765,6 +2340,147 @@ function DDMsgAreaChooser_WriteLightbarKeyHelpErrorMsg(pErrorMsg, pRefreshHelpLi
 		this.WriteKeyHelpLine();
 }
 
+// For the DDMsgAreaChooser class: Sets up the group_list array according to
+// the sub-board collapse separator
+function DDMsgAreaChooser_SetUpGrpListWithCollapsedSubBoards()
+{
+	// Returns a default object for a message group
+	function defaultMsgGrpObj()
+	{
+		return {
+			index: 0,
+			number: 0,
+			name: "",
+			description: "",
+			ars: 0,
+			sub_list: []
+		};
+	}
+
+	// Returns a default object for a message sub-board.  It can potentially
+	// contain its own list of sub-boards within the original subboard.
+	function defaultSubBoardObj()
+	{
+		return {
+			index: 0,
+			number: 0,
+			grp_index: 0,
+			grp_number: 0,
+			grp_name: 0,
+			code: "",
+			name: "",
+			grp_name: "",
+			description: "",
+			ars: 0,
+			settings: 0,
+			sub_subboard_list: []
+		};
+	}
+
+	function subBoardObjFromOfficialSubBoard(pGrpIdx, pSubIdx)
+	{
+		var subObj = defaultSubBoardObj();
+		for (var prop in subObj)
+		{
+			if (prop != "sub_subboard_list") // Doesn't exist in the official dir objects
+				subObj[prop] = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx][prop];
+		}
+		return subObj;
+	}
+
+	if (this.group_list.length == 0)
+	{
+		// Copy some of the information from msg_area.grp_list
+		for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx)
+		{
+			// Go through sub_list in the curent message group, and for
+			// any that have the collapse separator, add only one copy
+			// of the name to the sub_list property for this group.
+			// First, we'll have to see if multiple sub-boards with
+			// the collapse separator have the same prefix.
+			// dirDescs is an object indexed by sub-board description,
+			// and the value will be how many times it was seen.
+			var subBoardDescs = {};
+			for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx)
+			{
+				var subBoardDesc = msg_area.grp_list[grpIdx].sub_list[subIdx].description;
+				var sepIdx = subBoardDesc.indexOf(this.subCollapseSeparator);
+				if (sepIdx > -1)
+					subBoardDesc = truncsp(subBoardDesc.substr(0, sepIdx));  // Remove trailing whitespace
+				if (subBoardDescs.hasOwnProperty(subBoardDesc))
+					subBoardDescs[subBoardDesc] += 1;
+				else
+					subBoardDescs[subBoardDesc] = 1;
+			}
+
+			// Create an initial file group object for this file group (except
+			// for sub_list, which we will build ourselves for this group)
+			var msgGrpObj = defaultMsgGrpObj();
+			for (var prop in msgGrpObj)
+			{
+				if (prop != "sub_list")
+					msgGrpObj[prop] = msg_area.grp_list[grpIdx][prop];
+			}
+
+			// Go through the dirs in this group again.  For each sub-board:
+			// If its whole description exists in subBoardDescs, then just add it to
+			// the sub_list array.  Otherwise:
+			// If a collapse seprator exists, then split on that and see if the
+			// prefix was seen more than once.  If so, then add the separate
+			// subdirs as their own items in the sub_list array.  Otherwise,
+			// add the single sub-board to the sub_list array with the whole
+			// description.
+			var addedPrefixDescriptionDirs = {};
+			for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx)
+			{
+				var subBoardDesc = msg_area.grp_list[grpIdx].sub_list[subIdx].description;
+				if (subBoardDescs.hasOwnProperty(subBoardDesc))
+					msgGrpObj.sub_list.push(subBoardObjFromOfficialSubBoard(grpIdx, subIdx));
+				else
+				{
+					var subSubDirDesc = "";
+					var sepIdx = subBoardDesc.indexOf(this.subCollapseSeparator);
+					if (sepIdx > -1)
+					{
+						subBoardDesc = truncsp(subBoardDesc.substr(0, sepIdx));  // Remove trailing whitespace
+						// If it has been seen more than once, then the description should
+						// be the prefix description
+						if (subBoardDescs[subBoardDesc] > 1)
+						{
+							var addedSubIdx = msgGrpObj.sub_list.length - 1;
+							if (!addedPrefixDescriptionDirs.hasOwnProperty(subBoardDesc))
+							{
+								// Add it to sub_list
+								msgGrpObj.sub_list.push(subBoardObjFromOfficialSubBoard(grpIdx, subIdx));
+								addedSubIdx = msgGrpObj.sub_list.length - 1;
+								msgGrpObj.sub_list[addedSubIdx].description = subBoardDesc;
+								addedPrefixDescriptionDirs[subBoardDesc] = true;
+							}
+							// Add the sub-subboard to the sub-board's sub-subboard list
+							// Using skipsp() to strip leading whitespace
+							subSubDirDesc = skipsp(msg_area.grp_list[grpIdx].sub_list[subIdx].description.substr(sepIdx+1));
+							msgGrpObj.sub_list[addedSubIdx].sub_subboard_list.push({
+								description: subSubDirDesc,
+								code: msg_area.grp_list[grpIdx].sub_list[subIdx].code,
+								index: msg_area.grp_list[grpIdx].sub_list[subIdx].index,
+								grp_index: msg_area.grp_list[grpIdx].sub_list[subIdx].grp_index
+							});
+						}
+						else // Add it with the full description
+							msgGrpObj.sub_list.push(subBoardObjFromOfficialSubBoard(grpIdx, subIdx));
+					}
+					if (subBoardDescs.hasOwnProperty(subBoardDesc))
+						subBoardDescs[subBoardDescs] += 1;
+					else
+						subBoardDescs[subBoardDescs] = 1;
+				}
+			}
+
+			this.group_list.push(msgGrpObj);
+		}
+	}
+}
+
 // Removes multiple, leading, and/or trailing spaces.
 // The search & replace regular expressions used in this
 // function came from the following URL:
@@ -1823,7 +2539,7 @@ function calcPageNum(pTopIndex, pNumPerPage)
 function getGreatestNumMsgs(pGrpIndex)
 {
 	// Sanity checking
-	if (typeof(pGrpIndex) != "number")
+	if (typeof(pGrpIndex) !== "number")
 		return 0;
 	if (typeof(msg_area.grp_list[pGrpIndex]) == "undefined")
 		return 0;
@@ -1844,6 +2560,28 @@ function getGreatestNumMsgs(pGrpIndex)
 	return greatestNumMsgs;
 }
 
+// Returns the number of messages in a sub-board
+//
+// Parameters:
+//  pSubCode: The internal code of the sub-board
+//
+// Return value: The number of messages in the sub-board, or 0 if can't be opened
+function getNumMsgsInSubBoard(pSubCode)
+{
+	if (typeof(pSubCode) !== "string")
+		return 0;
+
+	var numMsgs = 0;
+	var msgBase = new MsgBase(pSubCode);
+	if (msgBase != null && msgBase.open())
+	{
+		numMsgs = msgBase.total_msgs;
+		msgBase.close();
+	}
+	delete msgBase; // Free some memory?
+	return numMsgs;
+}
+
 // 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
@@ -1861,7 +2599,7 @@ function getGreatestNumMsgs(pGrpIndex)
 function getKeyWithESCChars(pGetKeyMode)
 {
 	var getKeyMode = K_NONE;
-	if (typeof(pGetKeyMode) == "number")
+	if (typeof(pGetKeyMode) === "number")
 		getKeyMode = pGetKeyMode;
 
 	var userInput = console.getkey(getKeyMode);
@@ -1921,7 +2659,7 @@ function loadTextFileIntoArray(pFilenameBase, pMaxNumLines)
 	if (typeof(pFilenameBase) != "string")
 		return new Array();
 
-	var maxNumLines = (typeof(pMaxNumLines) == "number" ? pMaxNumLines : -1);
+	var maxNumLines = (typeof(pMaxNumLines) === "number" ? pMaxNumLines : -1);
 
 	var txtFileLines = new Array();
 	// See if there is a header file that is made for the user's terminal
@@ -2242,13 +2980,13 @@ function getStrWithTimeout(pMode, pMaxLength, pTimeout)
 	var inputStr = "";
 
 	var mode = K_NONE;
-	if (typeof(pMode) == "number")
+	if (typeof(pMode) === "number")
 		mode = pMode;
 	var maxWidth = 0;
-	if (typeof(pMaxLength) == "number")
+	if (typeof(pMaxLength) === "number")
 			maxWidth = pMaxLength;
 	var timeout = 0;
-	if (typeof(pTimeout) == "number")
+	if (typeof(pTimeout) === "number")
 		timeout = pTimeout;
 
 	var setNormalAttrAtEnd = false;
@@ -2336,14 +3074,14 @@ function getPageNumFromSearch(pText, pNumItemsPerPage, pSubBoard, pStartItemIdx,
 	};
 
 	// Sanity checking
-	if ((typeof(pText) != "string") || (typeof(pNumItemsPerPage) != "number") || (typeof(pSubBoard) != "boolean"))
+	if ((typeof(pText) != "string") || (typeof(pNumItemsPerPage) !== "number") || (typeof(pSubBoard) != "boolean"))
 		return retObj;
 
 	// Convert the text to uppercase for case-insensitive searching
 	var srchText = pText.toUpperCase();
 	if (pSubBoard)
 	{
-		if ((typeof(pGrpIdx) == "number") && (pGrpIdx >= 0) && (pGrpIdx < msg_area.grp_list.length))
+		if ((typeof(pGrpIdx) === "number") && (pGrpIdx >= 0) && (pGrpIdx < msg_area.grp_list.length))
 		{
 			// Go through the sub-board list of the given group and
 			// search for text in the descriptions
@@ -2443,30 +3181,96 @@ function calcPageNumAndTopPageIdx(pItemIdx, pNumItemsPerPage)
 function getLatestMsgHdr(pSubCode, pNumMsgsToCheck)
 {
 	var msgHdr = null;
-	var numMsgsToCheck = (typeof(pNumMsgsToCheck) == "number" ? pNumMsgsToCheck : 0);
 	var msgBase = new MsgBase(pSubCode);
 	if (msgBase.open())
 	{
-		// Look through the last pNumMsgsToCheck headers to find the latest
-		// readable message header
-		var numMsgs = msgBase.total_msgs;
-		var firstMsgIdx = 0;
-		if (numMsgsToCheck >= 1)
-			firstMsgIdx = numMsgs - numMsgsToCheck;
+		msgHdr = getLatestMsgHdrWithMsgbase(msgBase, pNumMsgsToCheck);
+		msgBase.close();
+	}
+	delete msgBase; // Free some memory?
+	return msgHdr;
+}
+// Gets the header of the latest readable message in a sub-board,
+// given a number of messages to look at.
+//
+// Paramters:
+//  pMsgbase: A MsgBase object for the sub-board, already opened
+//  pNumMsgsToCheck: The number of messages to check at the end of the sub-board.
+//                   This is optional and if omitted, all messages will be
+//                   checked (from the last message) for the latest readable
+//                   message header.
+//
+// Return value: The message header of the latest readable message.  If
+//               none is found, this will be null.
+function getLatestMsgHdrWithMsgbase(pMsgbase, pNumMsgsToCheck)
+{
+	if (typeof(pMsgbase) !== "object")
+		return null;
+	if (!pMsgbase.is_open)
+		return null;
+
+	var msgHdr = null;
+	var numMsgsToCheck = (typeof(pNumMsgsToCheck) === "number" ? pNumMsgsToCheck : 0);
+
+	// Look through the last numMsgsToCheck headers to find the latest
+	// readable message header
+	var numMsgs = pMsgbase.total_msgs;
+	var firstMsgIdx = 0;
+	if (numMsgsToCheck >= 1)
+		firstMsgIdx = numMsgs - numMsgsToCheck;
+	else
+		firstMsgIdx = 0;
+	if (firstMsgIdx < 0)
+		firstMsgIdx = 0;
+	for (var i = numMsgs - 1; (i >= firstMsgIdx) && (msgHdr == null); --i)
+	{
+		var msgHeader = pMsgbase.get_msg_header(true, i, true);
+		if (isReadableMsgHdr(msgHeader, pMsgbase.cfg.code))
+			msgHdr = msgHeader;
+	}
+
+	return msgHdr;
+}
+
+// Gets the time of the latest post in a sub-board.
+//
+// Parameters:
+//  pSubCode: The internal code of the sub-board
+//
+// Return value: The time of the latest post in the sub-board.  If pSubCode is invalid, this will be 0.
+function getLatestMsgTime(pSubCode)
+{
+	if (typeof(pSubCode) !== "string")
+		return 0;
+
+	var latestPostTime = 0;
+
+	var numMsgs = 0;
+	var msgBase = new MsgBase(pSubCode);
+	if (msgBase.open())
+	{
+		//var numMsgs = numReadableMsgs(msgBase, pSubCode);
+		numMsgs = msgBase.total_msgs;
+		msgBase.close();
+	}
+	delete msgBase; // Free some memory?
+	if (numMsgs > 0)
+	{
+		// Get the latest post time from this sub-board & compare it to retObj.newestTime and
+		// set if necessary
+		var msgHeader = getLatestMsgHdr(pSubCode);
+		if (msgHeader === null)
+			msgHeader = getBogusMsgHdr();
+		if (this.showImportDates)
+			latestPostTime = msgHeader.when_imported_time;
 		else
-			firstMsgIdx = 0;
-		if (firstMsgIdx < 0)
-			firstMsgIdx = 0;
-		for (var i = numMsgs - 1; (i >= firstMsgIdx) && (msgHdr == null); --i)
 		{
-			var msgHeader = msgBase.get_msg_header(true, i, true);
-			if (isReadableMsgHdr(msgHeader, pSubCode))
-				msgHdr = msgHeader;
+			var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader);
+			latestPostTime = msgWrittenLocalBBSTime != -1 ? msgWrittenLocalBBSTime : msgHeader.when_written_time;
 		}
-
-		msgBase.close();
 	}
-	return msgHdr;
+
+	return latestPostTime;
 }
 
 // Finds a message group index with search text, matching either the name or
@@ -2477,26 +3281,49 @@ function getLatestMsgHdr(pSubCode, pNumMsgsToCheck)
 //  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)
+function DDMsgAreaChooser_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;
+	var startIdx = (typeof(pStartItemIdx) === "number" ? pStartItemIdx : 0);
+	if (this.useSubCollapsing)
+	{
+		if ((startIdx < 0) || (startIdx > this.group_list.length))
+			startIdx = 0;
+	}
+	else
+	{
+		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 (this.useSubCollapsing)
 	{
-		if ((msg_area.grp_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) ||
-		    (msg_area.grp_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1))
+		for (var i = startIdx; i < this.group_list.length; ++i)
 		{
-			grpIdx = i;
-			break;
+			if ((this.group_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) ||
+				(this.group_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1))
+			{
+				grpIdx = i;
+				break;
+			}
+		}
+	}
+	else
+	{
+		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;
+			}
 		}
 	}
 
@@ -2508,34 +3335,138 @@ function findMsgGrpIdxFromText(pSearchText, pStartItemIdx)
 //
 // Parameters:
 //  pGrpIdx: The index of the message group
+//  pSubIdx: Optional - The index of the sub-board, for sub-board name collapsing.
+//           If this is null, then sub-board collapsing won't be used.
 //  pSearchText: The name/description text to look for
 //  pStartItemIdx: The item index to start at.  Defaults to 0
 //
 // Return value: The index of the sub-board, or -1 if not found
-function findSubBoardIdxFromText(pGrpIdx, pSearchText, pStartItemIdx)
+function DDMsgAreaChooser_FindSubBoardIdxFromText(pGrpIdx, pSubIdx, pSearchText, pStartItemIdx)
 {
-	if (typeof(pGrpIdx) != "number")
+	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;
+	var startIdx = (typeof(pStartItemIdx) === "number" ? pStartItemIdx : 0);
+	if (this.useSubCollapsing)
+	{
+		if (typeof(pSubIdx) === "number")
+		{
+			if ((startIdx < 0) || (startIdx > this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length))
+				startIdx = 0;
+		}
+		else
+		{
+			if ((startIdx < 0) || (startIdx > this.group_list[pGrpIdx].sub_list.length))
+				startIdx = 0;
+		}
+	}
+	else
+	{
+		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
+	// Go through the message sub-board list in the group (or sub-subboard list in the sub-board)
+	// and look for a match
 	var searchTextUpper = pSearchText.toUpperCase();
-	for (var i = startIdx; i < msg_area.grp_list[pGrpIdx].sub_list.length; ++i)
+	if (this.useSubCollapsing)
+	{
+		if (typeof(pSubIdx) === "number")
+		{
+			// Look through the sub-subboards of the sub-board
+			for (var i = startIdx; i < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length; ++i)
+			{
+				// For the sub-subboards, there is only a description, no name
+				if (this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1)
+				{
+					subBoardIdx = i;
+					break;
+				}
+			}
+		}
+		else
+		{
+			// Look  through the sub-boards in the group
+			for (var i = startIdx; i < this.group_list[pGrpIdx].sub_list.length; ++i)
+			{
+				if ((this.group_list[pGrpIdx].sub_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) ||
+				    (this.group_list[pGrpIdx].sub_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1))
+				{
+					subBoardIdx = i;
+					break;
+				}
+			}
+		}
+	}
+	else
 	{
-		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))
+		for (var i = startIdx; i < msg_area.grp_list[pGrpIdx].sub_list.length; ++i)
 		{
-			subBoardIdx = i;
-			break;
+			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;
 }
+
+
+
+// Temporary - For debugging
+function logStackTrace(levels) {
+    var callstack = [];
+    var isCallstackPopulated = false;
+    try {
+        i.dont.exist += 0; //doesn't exist- that's the point
+    } catch (e) {
+        if (e.stack) { //Firefox / chrome
+            var lines = e.stack.split('\n');
+            for (var i = 0, len = lines.length; i < len; i++) {
+                    callstack.push(lines[i]);
+            }
+            //Remove call to logStackTrace()
+            callstack.shift();
+            isCallstackPopulated = true;
+        }
+        else if (window.opera && e.message) { //Opera
+            var lines = e.message.split('\n');
+            for (var i = 0, len = lines.length; i < len; i++) {
+                if (lines[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {
+                    var entry = lines[i];
+                    //Append next line also since it has the file info
+                    if (lines[i + 1]) {
+                        entry += " at " + lines[i + 1];
+                        i++;
+                    }
+                    callstack.push(entry);
+                }
+            }
+            //Remove call to logStackTrace()
+            callstack.shift();
+            isCallstackPopulated = true;
+        }
+    }
+    if (!isCallstackPopulated) { //IE and Safari
+        var currentFunction = arguments.callee.caller;
+        while (currentFunction) {
+            var fn = currentFunction.toString();
+            var fname = fn.substring(fn.indexOf("function") + 8, fn.indexOf("(")) || "anonymous";
+            callstack.push(fname);
+            currentFunction = currentFunction.caller;
+        }
+    }
+    if (levels) {
+        console.print(callstack.slice(0, levels).join("\r\n"));
+    }
+    else {
+        console.print(callstack.join("\r\n"));
+    }
+}
\ No newline at end of file
diff --git a/xtrn/DDAreaChoosers/readme.txt b/xtrn/DDAreaChoosers/readme.txt
index 423a0ea915774b22f884f3212b52a289be6a3332..dd01c2fc4c193b48f14b81f43072294e96ddf241 100644
--- a/xtrn/DDAreaChoosers/readme.txt
+++ b/xtrn/DDAreaChoosers/readme.txt
@@ -1,6 +1,6 @@
                      Digital Distortion Area Choosers
-                              Version 1.20
-                        Release date: 2020-04-19
+                              Version 1.21
+                        Release date: 2022-01-15
 
                                   by
 
@@ -18,9 +18,10 @@ Contents
 1. Disclaimer
 2. Introduction
 3. Installation & Setup
-4. Configuration file
-5. DDMsgAreaChooser class: Properties & methods
-6. DDFileAreaChooser class: Properties & methods
+4. Configuration files
+5. Sub-board & directory name collapsing
+6. DDMsgAreaChooser class: Properties & methods
+7. DDFileAreaChooser class: Properties & methods
 
 
 1. Disclaimer
@@ -186,8 +187,8 @@ To run that from a Baja script, include this line:
 exec_xtrn MSGARCHO
 
 
-4. Configuration file
-=====================
+4. Configuration files
+======================
 If you want to change the default beavior and colors for one of these scripts,
 you can edit its configuration file, which is a plain text file.  The
 configuration files have two sections: A behavior section (denoted by
@@ -236,6 +237,19 @@ showDatesInSubBoardList               Specifies whether or not to show the date
                                       sub-boards.  Valid values are true and
                                       false.
 
+useSubCollapsing                      Whether or not to enable sub-board name
+                                      collapsing.  For example, for sub-boards
+                                      in a group starting with common text and
+                                      a separator (specified by
+                                      subCollapseSeparator), the common text
+                                      will be the only one displayed, and when
+                                      the user selects it, a 3rd tier with the
+                                      sub-boards after the separator will be
+                                      shown.
+
+subCollapseSeparator                  The separator to use for sub-board name
+                                      collapsing
+
 Colors section: Message area chooser
 ------------------------------------
 Color setting                        Description
@@ -320,6 +334,20 @@ areaChooserHdrFilenameBase            The filename to use (without the
 areaChooserHdrMaxLines                The maximum number of lines to use from
                                       the message area chooser header file
 
+
+useDirCollapsing                      Whether or not to enable directory name
+                                      collapsing.  For example, for directories
+                                      in a library starting with common text and
+                                      a separator (specified by
+                                      dirCollapseSeparator), the common text
+                                      will be the only one displayed, and when
+                                      the user selects it, a 3rd tier with the
+                                      directories after the separator will be
+                                      shown.
+
+dirCollapseSeparator                  The separator to use for directory name
+                                      collapsing
+
 Colors section: File area chooser
 ------------------------------------
 Color setting                        Description
@@ -370,8 +398,51 @@ lightbarHelpLineParen                The color to use for the ) characters in
                                      the help text line displayed at the bottom
                                      of the screen in lightbar mode
 
-
-5. DDMsgAreaChooser class: Properties & methods
+5. Sub-board & directory name collapsing
+========================================
+Sub-board name collapsing (for message areas) and directory name collapsing (for
+file areas) is a way to simplify long lists of sub-boards and file directories
+that share a common theme.  A 3rd tier of options will be created for those with
+a common theme.  For example, within a file library, if you have multiple
+directories relating to Windows, the file area chooser can just include one
+directory entry for Windows, and when the user chooses Windows, another list
+of options will be shown for the Windows directories.  Similarly, for message
+sub-boards, if you have multiple sub-boards in a message group that share a
+common theme, the message area chooser can show just one sub-board entry for
+that, and when the user selects it, the chooser will show a 3rd tier of options
+for the sub-boards in that group.
+
+For example, if you have the following file directory structure:
+BBS files
+  DOS: BBS software
+  DOS: BBS doors
+  DOS: BBS utilities
+  Windows: BBS software
+  Windows: BBS doors
+  Windows: BBS utilities
+
+With directory name collapsing, the selection will be come 3 tiers, as follows:
+BBS files
+  DOS
+    BBS software
+    BBS doors
+    BBS utilities
+  Windows
+    BBS software
+    BBS doors
+    BBS utilities
+
+The message area chooser has similar functionality for sub-boards.  You can
+configure what text to split the names on (i.e, by default, it splits on a
+colon, :).  This only happens at the file directory/message sub-board level, not
+at the file library/message group level.
+
+Name collapsing for message areas can be configured with the useSubCollapsing
+and subCollapseSeparator options in DDMsgAreaChooser.cfg, and for files, with
+the useDirCollapsing and dirCollapseSeparator options in DDFileAreaChooser.cfg.
+
+
+6. DDMsgAreaChooser class: Properties & methods
 ===============================================
 The following are the properties and methods of the DDMsgAreaChooser class, which
 is the class used for letting the user choose a message area:
@@ -417,7 +488,7 @@ ListSubBoardsInMsgGroup(pGrpIndex,    Lists the sub-boards in the user's
                                       "dateDesc" for date descending, or
                                       "description" for description.
 
-6. DDFileAreaChooser class: Properties & methods
+7. DDFileAreaChooser class: Properties & methods
 ===============================================
 The following are the properties and methods of the DDMsgAreaChooser class, which
 is the class used for letting the user choose a message area:
diff --git a/xtrn/DDAreaChoosers/revision_history.txt b/xtrn/DDAreaChoosers/revision_history.txt
index 2cebdd1eda4bd7fd6bf8a09bdee651e7d849f83c..d9a2ef2d9f6595f37edf753d1fc8ba57decffeaa 100644
--- a/xtrn/DDAreaChoosers/revision_history.txt
+++ b/xtrn/DDAreaChoosers/revision_history.txt
@@ -5,6 +5,10 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
+1.21     2022-01-15   Now supports message sub-board & file directory name
+                      collapsing, allowing simplified file directory and message
+                      sub-board lists for ones with common groups, adding a 3rd
+                      tier of options.
 1.20     2020-04-19   Now uses the DDLightbarMenu class for lightbar mode, and
                       the internal lightbar code has been removed.  Requires
                       the latest dd_lightbar_menu.js in sbbs/exec/load.