Skip to content
Snippets Groups Projects
DDMsgAreaChooser.js 138 KiB
Newer Older

// For the DDMsgAreaChooser class: Creates the DDLightbarMenu for use with
// 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(pLevel, pGrpIdx, pSubIdx)
{
	// Start & end indexes for the various items in each mssage group list row
	// Selected mark, group#, description, # sub-boards
	var lengthsObj = this.GetSubNameLenAndNumMsgsLen(pGrpIdx);
	var subBoardListIdxes = {
		markCharStart: 0,
		markCharEnd: 1,
		subNumStart: 1,
		subNumEnd: 3 + (+this.areaNumLen)
	subBoardListIdxes.descStart = subBoardListIdxes.subNumEnd;
	subBoardListIdxes.descEnd = subBoardListIdxes.descStart + lengthsObj.nameLen + 1;
	subBoardListIdxes.numItemsStart = subBoardListIdxes.descEnd;
	subBoardListIdxes.numItemsEnd = subBoardListIdxes.numItemsStart + lengthsObj.numMsgsLen + 1;
	subBoardListIdxes.dateStart = subBoardListIdxes.numItemsEnd;
	subBoardListIdxes.dateEnd = subBoardListIdxes.dateStart + +this.dateLen + 1;
	subBoardListIdxes.timeStart = subBoardListIdxes.dateEnd;
	// Set timeEnd to -1 to let the whole rest of the lines be colored
	subBoardListIdxes.timeEnd = -1;
	var listStartRow = this.areaChangeHdrLines.length + 3; // or + 2?
	var subBoardMenuHeight = console.screen_rows - listStartRow;
	var subBoardMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, subBoardMenuHeight);
	subBoardMenu.scrollbarEnabled = true;
	subBoardMenu.borderEnabled = false;
	subBoardMenu.SetColors({
		itemColor: [{start: subBoardListIdxes.markCharStart, end: subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark},
		            {start: subBoardListIdxes.subNumStart, end: subBoardListIdxes.subNumEnd, attrs: this.colors.areaNum},
		            {start: subBoardListIdxes.descStart, end: subBoardListIdxes.descEnd, attrs: this.colors.desc},
		            {start: subBoardListIdxes.numItemsStart, end: subBoardListIdxes.numItemsEnd, attrs: this.colors.numItems},
		            {start: subBoardListIdxes.dateStart, end: subBoardListIdxes.dateEnd, attrs: this.colors.latestDate},
		            {start: subBoardListIdxes.timeStart, end: subBoardListIdxes.timeEnd, attrs: this.colors.latestTime}],
		selectedItemColor: [{start: subBoardListIdxes.markCharStart, end: subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight},
		                    {start: subBoardListIdxes.subNumStart, end: subBoardListIdxes.subNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight},
		                    {start: subBoardListIdxes.descStart, end: subBoardListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight},
		                    {start: subBoardListIdxes.numItemsStart, end: subBoardListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight},
		                    {start: subBoardListIdxes.dateStart, end: subBoardListIdxes.dateEnd, attrs: this.colors.dateHighlight + this.colors.bkgHighlight},
		                    {start: subBoardListIdxes.timeStart, end: subBoardListIdxes.timeEnd, attrs: this.colors.timeHighlight + this.colors.bkgHighlight}]
	});

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

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

	// Change the menu's NumItems() and GetItem() function to reference
	// the message list in this object rather than add the menu items
	// to the menu
	subBoardMenu.areaChooser = this; // Add this object to the menu object
	subBoardMenu.grpIdx = pGrpIdx;
			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.
							numItems = numReadableMsgs(msgBase, this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].code);
					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)
			{
				if ((pSubIdx >= 0) && (pSubIdx < this.group_list[pGrpIdx].sub_list.length))
				{
					var subSubsValid = Array.isArray(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list) && this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0;
					if (!subSubsValid && bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].code)
					{
						subBoardMenu.selectedItemIdx = pSubIdx;
						if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage())
							subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1;
					}
				}
			}
		}
		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;
			}

			// 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)
			{
				for (var i = 0; i < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length; ++i)
				{
					if (bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[i].code)
					{
						subBoardMenu.selectedItemIdx = i;
						if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage())
							subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1;
						break;
					}
				}
			}
		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;
		}
}

// For the DDMsgAreaChooser class: Lets the user choose a message group and
// sub-board via numeric input, using a traditional 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.
function DDMsgAreaChooser_SelectMsgArea_Traditional(pChooseGroup)
	// If there are no message groups, then don't let the user
	// choose one.
	if (msg_area.grp_list.length == 0)
	{
		console.clear("\1n");
		console.print("\1y\1hThere are no message groups.\r\n\1p");
		return;
	}
	var chooseGroup = (typeof(pChooseGroup) == "boolean" ? pChooseGroup : true);
	if (chooseGroup)
	{
		// Show the message groups & sub-boards and let the user choose one.
		var selectedGrp = 0;      // The user's selected message group
		var selectedSubBoard = 0; // The user's selected sub-board
		var usersCurrentIdxVals = getGrpAndSubIdxesFromCode(bbs.cursub_code, true);
		var continueChoosingMsgArea = true;
		while (continueChoosingMsgArea)
		{
			// Clear the BBS command string to make sure there are no extra
			// commands in there that could cause weird things to happen.
			bbs.command_str = "";
			this.DisplayAreaChgHdr(1);
			if (this.areaChangeHdrLines.length > 0)
				console.crlf();
			console.print("\1n\1b\1hþ \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c, or [\1h" + +(usersCurrentIdxVals.grpIdx+1) + "\1n\1c]: \1h");
			// Accept Q (quit), / or CTRL_F (Search) or a file library number
			selectedGrp = console.getkeys("Q/" + CTRL_F, msg_area.grp_list.length);

			// If the user just pressed enter (selectedGrp would be blank),
			// default to the current group.
			if (selectedGrp.toString() == "")
				selectedGrp = usersCurrentIdxVals.grpIdx + 1;

			if (selectedGrp.toString() == "Q")
				continueChoosingMsgArea = false;
			else if ((selectedGrp.toString() == "/") || (selectedGrp.toString() == CTRL_F))
			{
				console.crlf();
				var searchPromptText = "\1n\1c\1hSearch\1g: \1n";
				console.print(searchPromptText);
				var searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE);
				if (searchText.length > 0)
					grpSearchText = searchText;
			}
				// If the user specified a message group number, then
				// set it and let the user choose a sub-board within
				// the group.
				if (selectedGrp > 0)
				{
					// Set the default sub-board #: The current sub-board, or if the
					// user chose a different group, then this should be set
					// to the first sub-board.
					var defaultSubBoard = usersCurrentIdxVals.subIdx + 1;
					if (selectedGrp-1 != usersCurrentIdxVals.grpIdx)
						defaultSubBoard = 1;

					console.clear("\1n");
					var selectSubRetVal = this.SelectSubBoard_Traditional(selectedGrp-1, defaultSubBoard-1);
					// If the user chose a directory, then set the user's
					// message sub-board and quit the message group loop.
					if (selectSubRetVal.subBoardCode != "")
						bbs.cursub_code = selectSubRetVal.subBoardCode;
					}
				}
			}
		}
	}
	else
	{
		// Don't choose a group, just a sub-board within the user's current group.
		var idxVals = getGrpAndSubIdxesFromCode(bbs.cursub_code, true);
		var selectSubRetVal = this.SelectSubBoard_Traditional(idxVals.grpIdx, idxVals.subIdx);
		// If the user chose a directory, then set the user's sub-board.
		if (selectSubRetVal.subBoardCode != "")
			bbs.cursub_code = selectSubRetVal.subBoardCode;
	}
}

// For the DDMsgAreaChooser class: Allows the user to select a sub-board with the
// traditional user interface.
//
// Parameters:
//  pGrpIdx: The index of the message group to choose a sub-board for
//  pDefaultSubBoardIdx: The index of the default sub-board
//
// Return value: An object containing the following values:
//               subBoardChosen: Boolean - Whether or not a sub-board was chosen.
//               subBoardIndex: Numeric - The sub-board that was chosen (if any).
//                              Will be -1 if none chosen.
//               subBoardCode: The internal code of the chosen sub-board (or "" if none chosen)
function DDMsgAreaChooser_SelectSubBoard_Traditional(pGrpIdx, pDefaultSubBoardIdx)
	var retObj = {
		subBoardChosen: false,
		subBoardIndex: 1,
		subBoardCode: ""
	}
	var searchText = "";
	var defaultSubBoardIdx = pDefaultSubBoardIdx;
	var continueOn = false;
	do
	{
		this.DisplayAreaChgHdr(1);
		if (this.areaChangeHdrLines.length > 0)
			console.crlf();
		this.ListSubBoardsInMsgGroup(pGrpIdx, null, defaultSubBoardIdx, searchText);
		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");
		else
			console.print("\1n\1b\1hþ \1n\1cWhich, \1hQ\1n\1cuit, \1hCTRL-F\1n\1c, \1h/\1n\1c: \1h");
		// Accept Q (quit) or a sub-board number
		var selectedSubBoard = console.getkeys("Q/" + CTRL_F, msg_area.grp_list[pGrpIdx].sub_list.length);

		// If the user just pressed enter (selectedSubBoard would be blank),
		// default the selected directory.
		var selectedSubBoardStr = selectedSubBoard.toString();
		if (selectedSubBoardStr == "")
		{
			if (defaultSubBoardIdx >= 0)
			{
				selectedSubBoard = defaultSubBoardIdx + 1; // Make this 1-based
				continueOn = false;
			}
		}
		else if ((selectedSubBoardStr == "/") || (selectedSubBoardStr == 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)
				defaultSubBoardIdx = -1;
			else
				defaultSubBoardIdx = pDefaultSubBoardIdx;
			continueOn = true;
		}
		else if (selectedSubBoardStr == "Q")
			continueOn = false;
		// If a sub-board was chosen, then select it.
		if (selectedSubBoard > 0)
		{
			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)
		{
			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;
}

// For the DDMsgAreaChooser class: Lists all message groups (for the traditional
// user interface).
//
// Parameters:
//  pSearchText: Optional - Search text for the message groups
function DDMsgAreaChooser_ListMsgGrps_Traditional(pSearchText)
	// Print the header
	this.WriteGrpListHdrLine();
	console.print("\1n");

	var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : "");

	for (var i = 0; i < msg_area.grp_list.length; ++i)
	{
		if (searchText.length > 0)
			printIt = ((msg_area.grp_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (msg_area.grp_list[i].description.toUpperCase().indexOf(searchText) >= 0));
		else
			printIt = true;

		if (printIt)
		{
			console.crlf();
			this.WriteMsgGroupLine(i, false);
		}
// 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.
//  pSearchText: Optional - Search text for the sub-boards
//  pSortType: Optional - A string describing how to sort the list (if desired):
//             "none": Default behavior - Sort by sub-board #
//             "dateAsc": Sort by date, ascending
//             "dateDesc": Sort by date, descending
//             "description": Sort by description
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"))
	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"))
		highlightIndex = pMarkIndex;

	// Make sure grpIndex and highlightIndex are valid (they might not be for
	// brand-new users).
	if ((grpIndex == null) || (typeof(grpIndex) == "undefined"))
		grpIndex = 0;
	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);

	// Print the headers
	this.WriteSubBrdListHdr1Line(grpIndex);
	console.crlf();
	var itemsHdrStr = "Posts";
	if (this.useSubCollapsing)
		itemsHdrStr = subIdxValid ? "Posts" : "Items";
		printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + itemsHdrStr, "Latest date & time");
		printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + itemsHdrStr);
	// 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)
	// 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"))
	{
		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)
			{
				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));
				}
				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);
					subBoardInfo = new MsgSubBoardInfo();
					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);

					// 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[subIdx].code);
						if (msgHeader === null)
							msgHeader = getBogusMsgHdr();
						if (this.showImportDates)
							subBoardInfo.newestPostDate = msgHeader.when_imported_time;
						{
							var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader);
							if (msgWrittenLocalBBSTime != -1)
								subBoardInfo.newestPostDate = msgWrittenLocalBBSTime;
							else
								subBoardInfo.newestPostDate = msgHeader.when_written_time;
						}
				msgBase.close();
				subBoardArray.push(subBoardInfo);
				delete msgBase; // Free some memory?
		// Sort sub-board list.
		if (pSortType == "dateAsc")
		{
			subBoardArray.sort(function(pA, pB)
			{
				// Return -1, 0, or 1, depending on whether pA's date comes
				// before, is equal to, or comes after pB's date.
				var returnValue = 0;
				if (pA.newestPostDate < pB.newestPostDate)
					returnValue = -1;
				else if (pA.newestPostDate > pB.newestPostDate)
					returnValue = 1;
				return returnValue;
			});
		}
		else if (pSortType == "dateDesc")
		{
				subBoardArray.sort(function(pA, pB)
				{
					// Return -1, 0, or 1, depending on whether pA's date comes
					// after, is equal to, or comes before pB's date.
					var returnValue = 0;
					if (pA.newestPostDate > pB.newestPostDate)
						returnValue = -1;
					else if (pA.newestPostDate < pB.newestPostDate)
						returnValue = 1;
					return returnValue;
				});
			}
		}
		else if (pSortType == "description")
		{
			// Binary safe string comparison  
			// 
			// version: 909.322
			// discuss at: http://phpjs.org/functions/strcmp    // +   original by: Waldo Malqui Silva
			// +      input by: Steve Hilder
			// +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
			// +    revised by: gorthaur
			// *     example 1: strcmp( 'waldo', 'owald' );    // *     returns 1: 1
			// *     example 2: strcmp( 'owald', 'waldo' );
			// *     returns 2: -1
			subBoardArray.sort(function(pA, pB)
			{
				return ((pA.description == pB.description) ? 0 : ((pA.description > pB.description) ? 1 : -1));
			});
		}
		// Display the sub-board list.
		for (var i = 0; i < subBoardArray.length; ++i)
		{
			console.crlf();
			var showSubBoardMark = false;
			if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != ""))
			{
				if (subBoardArray[i].subBoardNum == highlightIndex)
					showSubBoardMark = ((grpIndex == msg_area.sub[bbs.cursub_code].grp_index) && (highlightIndex == subBoardArray[i].subBoardIdx));
			}
			console.print(showSubBoardMark ? "\1n" + this.colors.areaMark + "*" : " ");
			if (this.showDatesInSubBoardList)
			{
				printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardArray[i].subBoardNum+1),
				       subBoardArray[i].description.substr(0, this.subBoardNameLen),
				       subBoardArray[i].numPosts, strftime("%Y-%m-%d", subBoardArray[i].newestPostDate),
				       strftime("%H:%M:%S", subBoardArray[i].newestPostDate));
			}
			else
			{
				printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardArray[i].subBoardNum+1),
				       subBoardArray[i].description.substr(0, this.subBoardNameLen),
				       subBoardArray[i].numPosts, strftime("%Y-%m-%d", subBoardArray[i].newestPostDate));
			}
		}
	}
	// If no sort type is specified, then output the sub-board information in
	// order of sub-board number.
	else
	{
		//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)
			{
				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));
				}
				// 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)
					// 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);
						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);
						showSubBoardMark = this.CurrentSubBoardIsInSubSubsForSub(grpIndex, +subIdx);
				}
				else
					showSubBoardMark = (subBoardNum == highlightIndex);
				console.print(showSubBoardMark ? "\1n" + this.colors.areaMark + "*" : " ");
				var lengthsObj = this.GetSubNameLenAndNumMsgsLen(grpIndex);
				if (this.showDatesInSubBoardList)
				{
					printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1),
					         subInfo.desc.substr(0, lengthsObj.nameLen), subInfo.numItems,
					         newestDate.date, newestDate.time);
				}
				else
				{
					printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1),
					         subInfo.desc.substr(0, lengthsObj.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
	};
	// 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;
		// Get the number of messages in the sub-board
		var numMsgs = numReadableMsgs(null, msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code);