diff --git a/xtrn/DDMsgReader/DDMsgReader.js b/xtrn/DDMsgReader/DDMsgReader.js
index 4bed0e61e9b4f741f8e6e59daddd473baebdb39f..f9127a3f3fbd109549b449fd480e831e8354e006 100644
--- a/xtrn/DDMsgReader/DDMsgReader.js
+++ b/xtrn/DDMsgReader/DDMsgReader.js
@@ -69,6 +69,12 @@
  * 2012-12-14 Eric Oulashin     Version 1.58
  *                              When writing QUOTES.TXT, quote lines are now wrapped if the user's
  *                              external editor configuration is configured to do so.
+ * 2022-12-29 Eric Oulashin     Version 1.59
+ *                              For Synchronet above 3.20, read the external editor quote wrap setting
+ *                              from xtrn.ini.  Below version 3.20, read it from xtrn.cnf.
+ *                              Also, there's a new user setting to toggle whether or not to use the scrollbar
+ *                              in the scrolling reader. Currently there is no alternate progress displayed
+ *                              if not using the scrollbar, but that is planned for a future update.
  */
 
 "use strict";
@@ -173,8 +179,8 @@ var ansiterm = require("ansiterm_lib.js", 'expand_ctrl_a');
 
 
 // Reader version information
-var READER_VERSION = "1.58";
-var READER_DATE = "2022-12-14";
+var READER_VERSION = "1.59";
+var READER_DATE = "2022-12-29";
 
 // Keyboard key codes for displaying on the screen
 var UP_ARROW = ascii(24);
@@ -434,7 +440,7 @@ if (file_exists(backslash(system.exec_dir) + "load/smbdefs.js") && file_exists(b
 
 // User twitlist filename (and settings filename)
 var gUserTwitListFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".DDMsgReader_twitlist";
-//var gUserSettingsFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".DDMsgReader_Settings";
+var gUserSettingsFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".DDMsgReader_Settings";
 
 /////////////////////////////////////////////
 // Script execution code
@@ -759,6 +765,7 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	this.CanQuote = DigDistMsgReader_CanQuote;
 	this.ReadConfigFile = DigDistMsgReader_ReadConfigFile;
 	this.ReadUserSettingsFile = DigDistMsgReader_ReadUserSettingsFile;
+	this.WriteUserSettingsFile = DigDistMsgReader_WriteUserSettingsFile;
 	// TODO: Is this.DisplaySyncMsgHeader even needed anymore?  Looks like it's not being called.
 	this.DisplaySyncMsgHeader = DigDistMsgReader_DisplaySyncMsgHeader;
 	this.GetMsgHdrFilenameFull = DigDistMsgReader_GetMsgHdrFilenameFull;
@@ -900,9 +907,7 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	// of Synchronet
 	this.showScoresInMsgList = ((console.screen_columns >= 86) && (typeof((new MsgBase("mail")).vote_msg) === "function"));
 	if (this.showScoresInMsgList)
-	{
 		this.SUBJ_LEN -= (this.SCORE_LEN + 1); // + 1 to account for a space
-	}
 
 	// Whether or not the user chose to read a message
 	this.readAMessage = false;
@@ -1062,6 +1067,9 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	// Message list sort option
 	this.msgListSort = MSG_LIST_SORT_DATETIME_RECEIVED;
 
+	// Whether or not to use the scrollbar in the enhanced message reader
+	this.useEnhReaderScrollbar = true;
+
 	this.cfgFilename = "DDMsgReader.cfg";
 	// Check the command-line arguments for a custom configuration file name
 	// before reading the configuration file.
@@ -1074,7 +1082,7 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	this.userSettings = {
 		twitList: []
 	};
-	this.ReadUserSettingsFile();
+	this.ReadUserSettingsFile(false);
 	// Set any other values specified by the command-line parameters
 	// Reader start mode - Read or list mode
 	if (scriptArgsIsValid)
@@ -3935,8 +3943,7 @@ function DigDistMsgReader_ListMessages_Lightbar(pAllowChgSubBoard)
 				else
 				{
 					// There are no selected messages
-					writeWithPause(1, console.screen_rows, "\x01n\x01h\x01yThere are no selected messages.",
-					ERROR_PAUSE_WAIT_MS, "\x01n", true);
+					writeWithPause(1, console.screen_rows, "\x01n\x01h\x01yThere are no selected messages.", ERROR_PAUSE_WAIT_MS, "\x01n", true);
 					// Refresh the help line
 					DisplayHelpLine(this.msgListLightbarModeHelpLine);
 				}
@@ -3968,7 +3975,7 @@ function DigDistMsgReader_ListMessages_Lightbar(pAllowChgSubBoard)
 		}
 		else if (lastUserInputUpper == this.msgListKeys.userSettings)
 		{
-			var userSettingsRetObj = this.DoUserSettings_Scrollable();
+			var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) { DisplayHelpLine(pReader.msgListLightbarModeHelpLine); });
 			lastUserInputUpper = "";
 			drawMenu = userSettingsRetObj.needWholeScreenRefresh;
 			// In case the user changed their twitlist, re-filter the messages for this sub-board
@@ -4817,11 +4824,13 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 		console.gotoxy(1, console.screen_rows);
 	}
 
+	var msgAreaWidth = this.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1;
+
 	// We could word-wrap the message to ensure words aren't split across lines, but
 	// doing so could make some messages look bad (i.e., messages with drawing characters),
 	// and word_wrap also might not handle ANSI or other color/attribute codes..
 	//if (!textHasDrawingChars(messageText))
-	//	messageText = word_wrap(messageText, this.msgAreaWidth);
+	//	messageText = word_wrap(messageText, msgAreaWidth);
 
 	// If the message has ANSI content, then use a Graphic object to help make
 	// the message look good.  Also, remove any ANSI clear screen codes from the
@@ -4830,10 +4839,12 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 	if (msgHasANSICodes)
 	{
 		messageText = messageText.replace(/\u001b\[[012]J/gi, "");
-		var graphic = new Graphic(this.msgAreaWidth, this.msgAreaHeight-1);
+		var graphic = new Graphic(msgAreaWidth, this.msgAreaHeight-1);
 		graphic.auto_extend = true;
 		graphic.ANSI = ansiterm.expand_ctrl_a(messageText);
-		graphic.width = this.msgAreaWidth;
+		//graphic.normalize();
+		//graphic.width = msgAreaWidth;
+		//messageText = graphic.MSG.split('\n');
 		messageText = graphic.MSG;
 	}
 
@@ -4855,14 +4866,15 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 	if (topMsgLineIdxForLastPage != 0)
 		fractionToLastPage = topMsgLineIdx / topMsgLineIdxForLastPage;
 
-	// Draw an initial scrollbar on the rightmost column of the message area showing
-	// the fraction of the message shown and what part of the message is currently
-	// being shown.  The scrollbar will be updated minimally in the input loop to
-	// minimize screen redraws.
-	this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+	// If use of the scrollbar is enabled, draw an initial scrollbar on the rightmost
+	// column of the message area showing the fraction of the message shown and what
+	// part of the message is currently being shown.  The scrollbar will be updated
+	// minimally in the input loop to minimize screen redraws.
+	if (this.useEnhReaderScrollbar)
+		this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
 
 	// Input loop (for scrolling the message up & down)
-	var msgLineFormatStr = "%-" + this.msgAreaWidth + "s";
+	var msgLineFormatStr = "%-" + msgAreaWidth + "s";
 	var writeMessage = true;
 	// msgAreaHeight, msgReaderObj, and scrollbarUpdateFunction are for use
 	// with scrollTextLines().
@@ -4885,8 +4897,8 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 		scrollbarInfoObj.numSolidScrollBlocks = numSolidScrollBlocks;
 		var scrollRetObj = scrollTextLines(msgInfo.messageLines, topMsgLineIdx,
 									   this.colors.msgBodyColor, writeMessage,
-									   this.msgAreaLeft, this.msgAreaTop, this.msgAreaWidth,
-									   msgAreaHeight, 1, console.screen_rows,
+									   this.msgAreaLeft, this.msgAreaTop, msgAreaWidth,
+									   msgAreaHeight, 1, console.screen_rows, this.useEnhReaderScrollbar,
 									   msgScrollbarUpdateFn, scrollbarInfoObj);
 		topMsgLineIdx = scrollRetObj.topLineIdx;
 		retObj.lastKeypress = scrollRetObj.lastKeypress;
@@ -4912,7 +4924,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 				// so that the calling method will go to the next message/sub-board.
 				// Otherwise (if the message was not deleted), refresh the
 				// last 2 lines of the message on the screen.
-				var msgWasDeleted = this.PromptAndDeleteOrUndeleteMessage(pOffset, promptPos, true, true, this.msgAreaWidth, true);
+				var msgWasDeleted = this.PromptAndDeleteOrUndeleteMessage(pOffset, promptPos, true, true, msgAreaWidth, true);
 				if (msgWasDeleted && !canViewDeletedMsgs())
 				{
 					var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
@@ -4984,15 +4996,22 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 						// refresh it.
 						//var scrollBarBlock = "\x01n\x01h\x01k" + BLOCK1; // Dim block
 						// Dim block
-						var scrollBarBlock = this.colors.scrollbarBGColor + this.text.scrollbarBGChar;
-						if (solidBlockStartRow + numSolidScrollBlocks - 1 == this.msgAreaBottom)
+						if (this.useEnhReaderScrollbar)
 						{
-							//scrollBarBlock = "\x01w" + BLOCK2; // Bright block
-							// Bright block
-							scrollBarBlock = this.colors.scrollbarScrollBlockColor + this.text.scrollbarScrollBlockChar;
+							var scrollBarBlock = this.colors.scrollbarBGColor + this.text.scrollbarBGChar;
+							if (solidBlockStartRow + numSolidScrollBlocks - 1 == this.msgAreaBottom)
+							{
+								//scrollBarBlock = "\x01w" + BLOCK2; // Bright block
+								// Bright block
+								scrollBarBlock = this.colors.scrollbarScrollBlockColor + this.text.scrollbarScrollBlockChar;
+							}
+							else
+							{
+								// TODO
+							}
+							console.gotoxy(this.msgAreaRight+1, this.msgAreaBottom);
+							console.print(scrollBarBlock);
 						}
-						console.gotoxy(this.msgAreaRight+1, this.msgAreaBottom);
-						console.print(scrollBarBlock);
 						// Refresh the last 2 message lines on the screen, then display
 						// the key help line
 						this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
@@ -5025,8 +5044,15 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 							this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 							// Display the scrollbar again, and ensure it's in the correct position
-							solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+							if (this.useEnhReaderScrollbar)
+							{
+								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
+								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+							}
+							else
+							{
+								// TODO
+							}
 							writeMessage = true; // We want to refresh the message on the screen
 						}
 					}
@@ -5045,8 +5071,15 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 				// Display the scrollbar again, and ensure it's in the correct position
-				solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-				this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+				if (this.useEnhReaderScrollbar)
+				{
+					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
+					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+				}
+				else
+				{
+					// TODO
+				}
 				writeMessage = true; // We want to refresh the message on the screen
 				break;
 			case this.enhReaderKeys.reply: // Reply to the message
@@ -5100,8 +5133,15 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 							this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 							// Display the scrollbar again to refresh it on the screen
-							solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+							if (this.useEnhReaderScrollbar)
+							{
+								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
+								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+							}
+							else
+							{
+								// TODO
+							}
 							writeMessage = true; // We want to refresh the message on the screen
 						}
 					}
@@ -5128,8 +5168,15 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 					// Display the scrollbar again to refresh it on the screen
-					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+					if (this.useEnhReaderScrollbar)
+					{
+						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
+						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+					}
+					else
+					{
+						// TODO
+					}
 					writeMessage = true; // We want to refresh the message on the screen
 				}
 				else
@@ -5364,22 +5411,33 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					var extdHdrInfoLines = this.GetExtdMsgHdrInfo(msgHeader, (retObj.lastKeypress == this.enhReaderKeys.showKludgeLines));
 					if (extdHdrInfoLines.length > 0)
 					{
-						// Calculate information for the scrollbar for the kludge lines
-						var infoFractionShown = this.msgAreaHeight / extdHdrInfoLines.length;
-						if (infoFractionShown > 1)
-							infoFractionShown = 1.0;
-						var numInfoSolidScrollBlocks = Math.floor(this.msgAreaHeight * infoFractionShown);
-						if (numInfoSolidScrollBlocks == 0)
-							numInfoSolidScrollBlocks = 1;
-						var numNonSolidInfoScrollBlocks = this.msgAreaHeight - numInfoSolidScrollBlocks;
-						var lastInfoSolidBlockStartRow = this.msgAreaTop;
-						// Display the kludge lines and let the user scroll through them
-						this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks);
+						if (this.useEnhReaderScrollbar)
+						{
+							// Calculate information for the scrollbar for the kludge lines
+							var infoFractionShown = this.msgAreaHeight / extdHdrInfoLines.length;
+							if (infoFractionShown > 1)
+								infoFractionShown = 1.0;
+							var numInfoSolidScrollBlocks = Math.floor(this.msgAreaHeight * infoFractionShown);
+							if (numInfoSolidScrollBlocks == 0)
+								numInfoSolidScrollBlocks = 1;
+							var numNonSolidInfoScrollBlocks = this.msgAreaHeight - numInfoSolidScrollBlocks;
+							var lastInfoSolidBlockStartRow = this.msgAreaTop;
+							// Display the kludge lines and let the user scroll through them
+							this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks);
+						}
 						scrollTextLines(extdHdrInfoLines, 0, this.colors["msgBodyColor"], true, this.msgAreaLeft,
-						                this.msgAreaTop, this.msgAreaWidth, msgAreaHeight, 1, console.screen_rows, msgInfoScrollbarUpdateFn);
+						                this.msgAreaTop, msgAreaWidth, msgAreaHeight, 1, console.screen_rows,
+						                this.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn);
 						// Display the scrollbar for the message to refresh it on the screen
-						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+						if (this.useEnhReaderScrollbar)
+						{
+							solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
+							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+						}
+						else
+						{
+							// TODO
+						}
 						writeMessage = true; // We want to refresh the message on the screen
 					}
 					else
@@ -5426,8 +5484,15 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 					// Display the scrollbar again to refresh it on the screen
-					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+					if (this.useEnhReaderScrollbar)
+					{
+						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
+						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+					}
+					else
+					{
+						// TODO
+					}
 					writeMessage = true; // We want to refresh the message on the screen
 				}
 				else
@@ -5498,8 +5563,15 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 					// Display the scrollbar again to refresh it on the screen
-					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+					if (this.useEnhReaderScrollbar)
+					{
+						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
+						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+					}
+					else
+					{
+						// TODO
+					}
 					writeMessage = true; // We want to refresh the message on the screen
 				}
 				else // The user is not a sysop
@@ -5524,8 +5596,15 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
 				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 				// Display the scrollbar again to refresh it on the screen
-				solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-				this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+				if (this.useEnhReaderScrollbar)
+				{
+					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
+					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+				}
+				else
+				{
+					// TODO
+				}
 				writeMessage = true; // We want to refresh the message on the screen
 				break;
 			case this.enhReaderKeys.vote: // Vote on the message
@@ -5622,21 +5701,35 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					// Display the vote info and let the user scroll through them
 					// (the console height should be enough, but do this just in case)
 					// Calculate information for the scrollbar for the vote info lines
-					var infoFractionShown = this.msgAreaHeight / voteInfo.length;
-					if (infoFractionShown > 1)
-						infoFractionShown = 1.0;
-					var numInfoSolidScrollBlocks = Math.floor(this.msgAreaHeight * infoFractionShown);
-					if (numInfoSolidScrollBlocks == 0)
-						numInfoSolidScrollBlocks = 1;
-					var numNonSolidInfoScrollBlocks = this.msgAreaHeight - numInfoSolidScrollBlocks;
-					var lastInfoSolidBlockStartRow = this.msgAreaTop;
-					// Display the vote info lines and let the user scroll through them
-					this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks);
-					scrollTextLines(voteInfo, 0, this.colors["msgBodyColor"], true, this.msgAreaLeft, this.msgAreaTop, this.msgAreaWidth,
-					                msgAreaHeight, 1, console.screen_rows, msgInfoScrollbarUpdateFn);
+					if (this.useEnhReaderScrollbar)
+					{
+						var infoFractionShown = this.msgAreaHeight / voteInfo.length;
+						if (infoFractionShown > 1)
+							infoFractionShown = 1.0;
+						var numInfoSolidScrollBlocks = Math.floor(this.msgAreaHeight * infoFractionShown);
+						if (numInfoSolidScrollBlocks == 0)
+							numInfoSolidScrollBlocks = 1;
+						var numNonSolidInfoScrollBlocks = this.msgAreaHeight - numInfoSolidScrollBlocks;
+						var lastInfoSolidBlockStartRow = this.msgAreaTop;
+						// Display the vote info lines and let the user scroll through them
+						this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks);
+					}
+					else
+					{
+						// TODO
+					}
+					scrollTextLines(voteInfo, 0, this.colors["msgBodyColor"], true, this.msgAreaLeft, this.msgAreaTop, msgAreaWidth,
+					                msgAreaHeight, 1, console.screen_rows, this.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn);
 					// Display the scrollbar for the message to refresh it on the screen
-					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+					if (this.useEnhReaderScrollbar)
+					{
+						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
+						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+					}
+					else
+					{
+						// TODO
+					}
 					writeMessage = true; // We want to refresh the message on the screen
 				}
 				else
@@ -5753,7 +5846,9 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 				*/
 				break;
 			case this.enhReaderKeys.userSettings:
-				var userSettingsRetObj = this.DoUserSettings_Scrollable();
+				// Make a backup copy of the this.useEnhReaderScrollbar setting in case it changes, so we can tell if we need to refresh the scrollbar
+				var oldUseEnhReaderScrollbar = this.useEnhReaderScrollbar;
+				var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) { pReader.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); });
 				retObj.lastKeypress = "";
 				writeMessage = userSettingsRetObj.needWholeScreenRefresh;
 				// In case the user changed their twitlist, re-filter the messages for this sub-board
@@ -5808,11 +5903,49 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 				if (userSettingsRetObj.needWholeScreenRefresh)
 				{
 					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+					if (this.useEnhReaderScrollbar)
+						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+					else
+					{
+						// TODO
+					}
 					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 				}
 				else
+				{
+					// If the message scrollbar was toggled, then draw/erase the scrollbar
+					if (this.useEnhReaderScrollbar != oldUseEnhReaderScrollbar)
+					{
+						msgAreaWidth = this.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1;
+						// If the message is ANSI, then re-create the Graphic object to account for the
+						// new width
+						if (msgHasANSICodes)
+						{
+							var graphic = new Graphic(msgAreaWidth, this.msgAreaHeight-1);
+							graphic.auto_extend = true;
+							graphic.ANSI = ansiterm.expand_ctrl_a(messageText);
+							graphic.width = msgAreaWidth;
+							messageText = graphic.MSG;
+						}
+						// Display or erase the scrollbar
+						if (this.useEnhReaderScrollbar)
+						{
+							solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
+							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
+						}
+						else
+						{
+							// Erase the scrollbar
+							console.attributes = "N";
+							for (var screenY = this.msgAreaTop; screenY <= this.msgAreaBottom; ++screenY)
+							{
+								console.gotoxy(this.msgAreaRight+1, screenY);
+								console.print(" ");
+							}
+						}
+					}
 					this.RefreshMsgAreaRectangle(msgInfo.messageLines, topMsgLineIdx, userSettingsRetObj.optionBoxTopLeftX, userSettingsRetObj.optionBoxTopLeftY, userSettingsRetObj.optionBoxWidth, userSettingsRetObj.optionBoxHeight);
+				}
 				break;
 			case this.enhReaderKeys.quit: // Quit
 				retObj.nextAction = ACTION_QUIT;
@@ -8308,73 +8441,36 @@ function DigDistMsgReader_ReadUserSettingsFile(pOnlyTwitlist)
 		userTwitlistFile.close();
 	}
 
-	/*
 	if (!onlyTwitList)
 	{
 		// Open the user settings file, if it exists
 		var userSettingsFile = new File(gUserSettingsFilename);
 		if (userSettingsFile.open("r"))
 		{
-			var settingsMode = "behavior";
-			var equalsPos = 0;       // Position of a = in the line
-			var commentPos = 0;      // Position of the start of a comment
-			var setting = null;      // A setting name (string)
-			var settingUpper = null; // Upper-case setting name
-			var value = null;        // A value for a setting (string)
-			var valueUpper = null;   // Upper-cased value
-			while (!userSettingsFile.eof)
-			{
-				// Read the next line from the config file.
-				var fileLine = userSettingsFile.readln(2048);
-
-				// fileLine should be a string, but I've seen some cases
-				// where for some reason it isn't.  If it's not a string,
-				// then continue onto the next line.
-				if (typeof(fileLine) != "string")
-					continue;
-
-				// If the line starts with with a semicolon (the comment
-				// character) or is blank, then skip it.
-				if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
-					continue;
-
-				// If in the "behavior" section, then set the behavior-related variables.
-				if (fileLine.toUpperCase() == "[BEHAVIOR]")
-				{
-					settingsMode = "behavior";
-					continue;
-				}
-
-				// If the line has a semicolon anywhere in it, then remove
-				// everything from the semicolon onward.
-				commentPos = fileLine.indexOf(";");
-				if (commentPos > -1)
-					fileLine = fileLine.substr(0, commentPos);
-
-				// Look for an equals sign, and if found, separate the line
-				// into the setting name (before the =) and the value (after the
-				// equals sign).
-				equalsPos = fileLine.indexOf("=");
-				if (equalsPos > 0)
-				{
-					// Read the setting & value, and trim leading & trailing spaces.
-					setting = trimSpaces(fileLine.substr(0, equalsPos), true, false, true);
-					settingUpper = setting.toUpperCase();
-					value = trimSpaces(fileLine.substr(equalsPos+1), true, false, true);
-					valueUpper = value.toUpperCase();
-
-					if (settingsMode == "behavior")
-					{
-						if (settingUpper == "SOMETHING")
-							this.userSettings.something = (valueUpper == "TRUE");
-					}
-				}
-			}
+			//var behavior = userSettingsFile.iniGetObject("BEHAVIOR");
+			this.useEnhReaderScrollbar = userSettingsFile.iniGetValue("BEHAVIOR", "useEnhReaderScrollbar", true);
 			userSettingsFile.close();
 		}
 	}
-	*/
 }
+
+// For the DigDistMessageReader class: Writes the user settings file.
+//
+// Return value: Boolean - Whether or not the write succeeded
+function DigDistMsgReader_WriteUserSettingsFile()
+{
+	var writeSucceeded = false;
+	// Open the user settings file, if it exists
+	var userSettingsFile = new File(gUserSettingsFilename);
+	if (userSettingsFile.open(userSettingsFile.exists ? "r+" : "w+"))
+	{
+		userSettingsFile.iniSetValue("BEHAVIOR", "useEnhReaderScrollbar", this.useEnhReaderScrollbar);
+		userSettingsFile.close();
+		writeSucceeded = true;
+	}
+	return writeSucceeded;
+}
+
 // For the DigDistMsgReader class: Lets the user edit an existing message.
 //
 // Parameters:
@@ -13431,6 +13527,11 @@ function DigDistMsgReader_GetGroupNameAndDesc()
 
 // For the DigDistMsgReader class:  Lets the user manage their preferences/settings (scrollable/ANSI user interface).
 //
+// Parameters:
+//  pDrawBottomhelpLineFn: A function to draw the bottom help line (it could be the message list
+//                         help line or reader help line), for refreshing after displaying an error.
+//                         This function must take the reader (this) as a parameter.
+//
 // Return value: An object containing the following properties:
 //               needWholeScreenRefresh: Boolean - Whether or not the whole screen needs to be
 //                                       refreshed (i.e., when the user has edited their twitlist)
@@ -13439,7 +13540,7 @@ function DigDistMsgReader_GetGroupNameAndDesc()
 //               optionBoxWidth: The width of the option box
 //               optionBoxHeight: The height of the option box
 //               userTwitListChanged: Boolean - Whether or not the user's personal twit list changed
-function DigDistMsgReader_DoUserSettings_Scrollable()
+function DigDistMsgReader_DoUserSettings_Scrollable(pDrawBottomhelpLineFn)
 {
 	var retObj = {
 		needWholeScreenRefresh: false,
@@ -13456,9 +13557,10 @@ function DigDistMsgReader_DoUserSettings_Scrollable()
 		return retObj;
 	}
 
-	/*
 	// Save the user's current settings so that we can check them later to see if any
 	// of them changed, in order to determine whether to save the user's settings file.
+	var gOriginalUseEnhScrollbar = this.useEnhReaderScrollbar;
+	/*
 	var originalSettings = {};
 	for (var prop in gUserSettings)
 	{
@@ -13479,7 +13581,7 @@ function DigDistMsgReader_DoUserSettings_Scrollable()
 	optionBox.addInputLoopExitKey(CTRL_U);
 	// Update the bottom help text to be more specific to the user settings box
 	var bottomBorderText = "\x01n\x01h\x01c" + UP_ARROW + "\x01b, \x01c" + DOWN_ARROW + "\x01b, \x01cEnter\x01y=\x01bSelect\x01n\x01c/\x01h\x01btoggle, "
-						  + "\x01cESC\x01n\x01c/\x01hQ\x01n\x01c/\x01hCtrl-U\x01y=\x01bClose";
+	                     + "\x01cESC\x01n\x01c/\x01hQ\x01n\x01c/\x01hCtrl-U\x01y=\x01bClose";
 	// This one contains the page navigation keys..  Don't really need to show those,
 	// since the settings box only has one page right now:
 	/*var bottomBorderText = "\x01n\x01h\x01c"+ UP_ARROW + "\x01b, \x01c"+ DOWN_ARROW + "\x01b, \x01cN\x01y)\x01bext, \x01cP\x01y)\x01brev, "
@@ -13490,13 +13592,13 @@ function DigDistMsgReader_DoUserSettings_Scrollable()
 
 	// Add the options to the option box
 	const checkIdx = 48;
-	//const TAGLINE_OPT_INDEX = optionBox.addTextItem("Taglines                                       [ ]");
-	//if (this.userSettings.twitList.enableTaglines)
-	//	optionBox.chgCharInTextItem(TAGLINE_OPT_INDEX, checkIdx, CHECK_CHAR);
+	const ENH_SCROLLBAR_OPT_INDEX = optionBox.addTextItem("Scrollbar in reader                            [ ]");
+	if (this.useEnhReaderScrollbar)
+		optionBox.chgCharInTextItem(ENH_SCROLLBAR_OPT_INDEX, checkIdx, CHECK_CHAR);
 
 	// Create an object containing toggle values (true/false) for each option index
 	var optionToggles = {};
-	//optionToggles[TAGLINE_OPT_INDEX] = this.userSettings.enableTaglines;
+	optionToggles[ENH_SCROLLBAR_OPT_INDEX] = this.useEnhReaderScrollbar;
 
 	// Other actions
 	var USER_TWITLIST_OPT_INDEX = optionBox.addTextItem("Personal twit list");
@@ -13522,9 +13624,9 @@ function DigDistMsgReader_DoUserSettings_Scrollable()
 				// Toggle the setting for the user in global user setting object.
 				switch (itemIndex)
 				{
-					//case TAGLINE_OPT_INDEX:
-					//	this.userSettings.enableTaglines = !this.userSettings.enableTaglines;
-					//	break;
+					case ENH_SCROLLBAR_OPT_INDEX:
+						this.readerObj.useEnhReaderScrollbar = !this.readerObj.useEnhReaderScrollbar;
+						break;
 					default:
 						break;
 				}
@@ -13560,23 +13662,25 @@ function DigDistMsgReader_DoUserSettings_Scrollable()
 	// If the user changed any of their settings, then save the user settings.
 	// If the save fails, then output an error message.
 	var settingsChanged = false;
+	settingsChanged = (gOriginalUseEnhScrollbar != this.useEnhReaderScrollbar);
 	for (var prop in this.userSettings)
 	{
 		if (this.userSettings.hasOwnProperty(prop))
 		{
 			// TODO
-			//settingsChanged = (originalSettings[prop] != this.userSettings[prop]);
+			//settingsChanged = settingsChanged || (originalSettings[prop] != this.userSettings[prop]);
 			if (settingsChanged)
 				break;
 		}
 	}
 	if (settingsChanged)
 	{
-		// TODO
-		/*
-		if (!WriteUserSettingsFile(this.userSettings))
-			writeMsgOntBtmHelpLineWithPause("\x01n\x01y\x01hFailed to save settings!\x01n", ERRORMSG_PAUSE_MS);
-		*/
+		if (!this.WriteUserSettingsFile())
+		{
+			writeWithPause(1, console.screen_rows, "\x01n\x01y\x01hFailed to save settings!\x01n", ERROR_PAUSE_WAIT_MS, "\x01n", true);
+			// Refresh the help line
+			pDrawBottomhelpLineFn(this);
+		}
 	}
 
 	optionBox.addInputLoopExitKey(CTRL_U);
@@ -13635,6 +13739,16 @@ function DigDistMsgReader_DoUserSettings_Traditional()
 			break;
 	}
 
+	// For future changes, if any settings that apply to the traditional interface:
+	/*
+	if (!WriteUserSettingsFile())
+	{
+		console.print("\x01n\r\n\x01y\x01hFailed to save settings!\x01n");
+		console.crlf();
+		console.pause();
+	}
+	*/
+
 	return retObj;
 }
 
@@ -15994,6 +16108,8 @@ function userHandleAliasNameMatch(pName)
 //                  lines
 //  pPostWriteCurY: The Y location for the cursor after writing the message
 //                  lines
+// pUseScrollbar: Boolean - Whether or not to display the scrollbar.  If false,
+//                this will display a scroll status line at the bottom instead.
 //  pScrollUpdateFn: A function that the caller can provide for updating the
 //                   scroll position.  This function has one parameter:
 //                   - fractionToLastPage: The fraction of the top index divided
@@ -16004,7 +16120,7 @@ function userHandleAliasNameMatch(pName)
 //               lastKeypress: The last key pressed by the user (a string)
 //               topLineIdx: The new top line index of the text lines, in case of scrolling
 function scrollTextLines(pTxtLines, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pTopLeftX, pTopLeftY,
-                         pWidth, pHeight, pPostWriteCurX, pPostWriteCurY, pScrollUpdateFn,
+                         pWidth, pHeight, pPostWriteCurX, pPostWriteCurY, pUseScrollbar, pScrollUpdateFn,
                          pScrollbarInfo)
 {
 	// Variables for the top line index for the last page, scrolling, etc.
@@ -16048,7 +16164,7 @@ function scrollTextLines(pTxtLines, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pTo
 		{
 			// If the scroll update function parameter is a function, then calculate
 			// the fraction to the last page and call the scroll update function.
-			if (typeof(pScrollUpdateFn) == "function")
+			if (pUseScrollbar && typeof(pScrollUpdateFn) == "function")
 			{
 				if (topLineIdxForLastPage != 0)
 					fractionToLastPage = retObj.topLineIdx / topLineIdxForLastPage;
@@ -20116,23 +20232,38 @@ function getExternalEditorQuoteWrapCfgFromSCFG(pEditorCode)
 			retObj.quoteWrapEnabled = true;
 			retObj.quoteWrapCols = console.screen_columns - 1;
 
-			// See exportcfg.js for an example of using cnflib.js
-			// TODO: If running Synchronet 3.20, then there will be an easier way to get the quotewrap
-			// columns, from the .ini configuration
-			var cnflib = load({}, "cnflib.js");
-			var xtrnCnf = cnflib.read("xtrn.cnf");
-			if (typeof(xtrnCnf) === "object")
+			// For Synchronet 3.20 and newer, read the quote wrap setting from xtrn.ini
+			if (system.version_num >= 32000)
+			{
+				// The INI section for the editor should be something like [editor:SLYEDICE], and
+				// it should have a quotewrap_cols property
+				var xtrnIniFile = new File(system.ctrl_dir + "xtrn.ini");
+				if (xtrnIniFile.open("r"))
+				{
+					var quoteWrapCols = xtrnIniFile.iniGetValue("editor:" + pEditorCode.toUpperCase(), "quotewrap_cols", console.screen_columns - 1);
+					if (quoteWrapCols > 0)
+						retObj.quoteWrapCols = quoteWrapCols;
+					xtrnIniFile.close();
+				}
+			}
+			else
 			{
-				for (var i = 0; i < xtrnCnf.xedit.length; ++i)
+				// Synchronet below version 3.20: Read the quote wrap setting from xtrn.cnf
+				var cnflib = load({}, "cnflib.js");
+				var xtrnCnf = cnflib.read("xtrn.cnf");
+				if (typeof(xtrnCnf) === "object")
 				{
-					if (xtrnCnf.xedit[i].code.toLowerCase() == editorCode)
+					for (var i = 0; i < xtrnCnf.xedit.length; ++i)
 					{
-						if (xtrnCnf.xedit[i].hasOwnProperty("quotewrap_cols"))
+						if (xtrnCnf.xedit[i].code.toLowerCase() == editorCode)
 						{
-							if (xtrnCnf.xedit[i].quotewrap_cols > 0)
-								retObj.quoteWrapCols = xtrnCnf.xedit[i].quotewrap_cols;
+							if (xtrnCnf.xedit[i].hasOwnProperty("quotewrap_cols"))
+							{
+								if (xtrnCnf.xedit[i].quotewrap_cols > 0)
+									retObj.quoteWrapCols = xtrnCnf.xedit[i].quotewrap_cols;
+							}
+							break;
 						}
-						break;
 					}
 				}
 			}
@@ -20145,6 +20276,27 @@ function getExternalEditorQuoteWrapCfgFromSCFG(pEditorCode)
 	return retObj;
 }
 
+// Changes a character in a string, and returns the new string.  If any of the
+// parameters are invalid, then the original string will be returned.
+//
+// Parameters:
+//  pStr: The original string
+//  pCharIndex: The index of the character to replace
+//  pNewText: The new character or text to place at that position in the string
+//
+// Return value: The new string
+function chgCharInStr(pStr, pCharIndex, pNewText)
+{
+   if (typeof(pStr) != "string")
+      return "";
+   if ((pCharIndex < 0) || (pCharIndex >= pStr.length))
+      return pStr;
+   if (typeof(pNewText) != "string")
+      return pStr;
+
+   return (pStr.substr(0, pCharIndex) + pNewText + pStr.substr(pCharIndex+1));
+}
+
 ///////////////////////////////////////////////////////////////////////////////////
 
 // For debugging: Writes some text on the screen at a given location with a given pause.
diff --git a/xtrn/DDMsgReader/readme.txt b/xtrn/DDMsgReader/readme.txt
index 4e9b5125b0725cdd7c38a1f3f3a0ce8084b5d351..8bdf7f0304eef53e503ba9e568c1ddb4e19bb5e3 100644
--- a/xtrn/DDMsgReader/readme.txt
+++ b/xtrn/DDMsgReader/readme.txt
@@ -1,6 +1,6 @@
                       Digital Distortion Message Reader
-                                 Version 1.58
-                           Release date: 2022-12-14
+                                 Version 1.59
+                           Release date: 2022-12-29
 
                                      by
 
diff --git a/xtrn/DDMsgReader/revision_history.txt b/xtrn/DDMsgReader/revision_history.txt
index b2c7d455453ebb6291cecebeace6d27216ee79e7..b2c27677b4870fca843794abad78bf49df75d52a 100644
--- a/xtrn/DDMsgReader/revision_history.txt
+++ b/xtrn/DDMsgReader/revision_history.txt
@@ -5,6 +5,13 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
+1.59     2022-12-29   For Synchronet above 3.20, now reads the external editor
+                      quote wrap setting from xtrn.ini.  Below version 3.20, the
+                      quote wrap setting is read from xtrn.cnf.
+                      Also, there's a new user setting to toggle whether or not
+                      to use the scrollbar in the scrolling reader. Currently
+                      there is no alternate progress displayed if not using the
+                      scrollbar, but that is planned for a future update.
 1.58     2022-12-14   Now wraps quote lines, if applicable, according to the
                       quote line wrap settings of the user's external editor,
                       if the user uses one