diff --git a/xtrn/SlyVote/SlyVote.js b/xtrn/SlyVote/SlyVote.js
index 0a3c5033206eb29058e54c155df92868fb45b5ca..b88a5c93e28c373a9ed43e4c186244546b195bc1 100644
--- a/xtrn/SlyVote/SlyVote.js
+++ b/xtrn/SlyVote/SlyVote.js
@@ -54,7 +54,7 @@ load("DDLightbarMenu.js");
 
 // Version information
 var SLYVOTE_VERSION = "0.01 Beta";
-var SLYVOTE_DATE = "2017-01-09";
+var SLYVOTE_DATE = "2017-01-10";
 
 // Determine the script's startup directory.
 // This code is a trick that was created by Deuce, suggested by Rob Swindell
@@ -150,10 +150,24 @@ var gNetMsgAttrStrs = {
 var ERROR_PAUSE_WAIT_MS = 1500;
 
 var gBottomBorderRow = 23;
+var gMessageRow = 3;
+
+// Keyboard key codes for displaying on the screen
+var UP_ARROW = ascii(24);
+var DOWN_ARROW = ascii(25);
+var LEFT_ARROW = ascii(17);
+var RIGHT_ARROW = ascii(16);
+// PageUp & PageDown keys - Not real key codes, but codes defined
+// to be used & recognized in this script
+var KEY_PAGE_UP = "\1PgUp";
+var KEY_PAGE_DOWN = "\1PgDn";
 
 // An object containing keypresses for the vote results reader
 var gReaderKeys = {
-	vote: "V"
+	goToFirst: "F",
+	goToLast: "L",
+	vote: "V",
+	quit: "Q"
 }
 
 
@@ -288,7 +302,7 @@ function ChooseVoteTopic()
 	// Clear the screen between the top & bottom borders
 	var formatStr = "%" + console.screen_columns + "s";
 	console.print("\1n");
-	for (var posY = 3; posY < gBottomBorderRow; ++posY)
+	for (var posY = gMessageRow; posY < gBottomBorderRow; ++posY)
 	{
 		console.gotoxy(1, posY);
 		printf(formatStr, "");
@@ -298,7 +312,7 @@ function ChooseVoteTopic()
 	var voteTopicInfo = GetVoteTopicHdrs(gSubBoardCode, true);
 	if (voteTopicInfo.errorMsg.length > 0)
 	{
-		console.gotoxy(1, 3);
+		console.gotoxy(1, gMessageRow);
 		console.print("\1n\1y\1h" + voteTopicInfo.errorMsg + "\1n");
 		console.crlf();
 		console.pause();
@@ -306,7 +320,7 @@ function ChooseVoteTopic()
 	}
 	else if (voteTopicInfo.msgHdrs.length == 0)
 	{
-		console.gotoxy(1, 3);
+		console.gotoxy(1, gMessageRow);
 		console.print("\1n\1cThere are no polls to vote on in this section\1n");
 		console.crlf();
 		console.pause();
@@ -351,21 +365,20 @@ function ChooseVoteTopic()
 			drawTopicsMenu = true;
 			if (voteRetObj.errorMsg.length > 0)
 			{
-				var errorMsgRow = 3;
-				console.gotoxy(1, errorMsgRow);
+				console.gotoxy(1, gMessageRow);
 				if (voteRetObj.mnemonicsRequiredForErrorMsg)
 				{
-					console.gotoxy(1, errorMsgRow);
+					console.gotoxy(1, gMessageRow);
 					console.mnemonics(voteRetObj.errorMsg);
 					mswait(ERROR_PAUSE_WAIT_MS);
-					console.gotoxy(1, errorMsgRow);
+					console.gotoxy(1, gMessageRow);
 					printf("\1n%" + console.screen_columns + "s", "");
 				}
 				else
 				{
 					console.print("\1n\1y\1h" + voteRetObj.errorMsg);
 					mswait(ERROR_PAUSE_WAIT_MS);
-					console.gotoxy(1, errorMsgRow);
+					console.gotoxy(1, gMessageRow);
 					printf("\1n%" + voteRetObj.errorMsg.length + "s", "");
 				}
 			}
@@ -724,8 +737,8 @@ function GetMsgBody(pMsgbase, pMsgHdr, pSubBoardCode, pUser)
 				voteOptDescLen = 27;
 
 			// Format strings for outputting the voting option lines
-			var unvotedOptionFormatStr = "\1n\1c\1h%2d\1n\1c: \1w\1h%-" + voteOptDescLen + "s [%-4d %6.2f%]\1n";
-			var votedOptionFormatStr = "\1n\1c\1h%2d\1n\1c: \1" + "5\1w\1h%-" + voteOptDescLen + "s [%-4d %6.2f%]\1n";
+			var unvotedOptionFormatStr = "\1n\1" + "0\1c\1h%2d\1n\1" + "0\1c: \1w\1h%-" + voteOptDescLen + "s [%-4d %6.2f%]\1n\1" + "0";
+			var votedOptionFormatStr = "\1n\1" + "0\1c\1h%2d\1n\1" + 0 + "\1c: \1" + "5\1w\1h%-" + voteOptDescLen + "s [%-4d %6.2f%]\1n\1" + "0";
 			// Add up the total number of votes so that we can
 			// calculate vote percentages.
 			var totalNumVotes = 0;
@@ -775,7 +788,7 @@ function GetMsgBody(pMsgbase, pMsgHdr, pSubBoardCode, pUser)
 			// how to vote.
 			var votingAllowed = ((pSubBoardCode != "mail") && (((msg_area.sub[pSubBoardCode].settings & SUB_NOVOTING) == 0)));
 			if (votingAllowed && !HasUserVotedOnMsg(pMsgHdr.number, pSubBoardCode, pMsgbase, pUser))
-				msgBody += "\1n\r\n\1gTo vote in this poll, press \1w\1h" + gReaderKeys.vote + "\1n\1g now.";
+				msgBody += "\1n\1" + "0\r\n\1gTo vote in this poll, press \1w\1h" + gReaderKeys.vote + "\1n\1" + "0\1g now.";
 
 			// If the current logged-in user created this poll, then show the
 			// users who have voted on it so far.
@@ -788,7 +801,7 @@ function GetMsgBody(pMsgbase, pMsgHdr, pSubBoardCode, pUser)
 				{
 					// Get the line from text.dat for writing who voted & when.  It
 					// is a format string and should look something like this:
-					//"\r\n\1n\1hOn %s, in \1c%s \1n\1c%s\r\n\1h\1m%s voted in your poll: \1n\1h%s\r\n" 787 PollVoteNotice
+					//"\r\n\1n\1" + "0\1hOn %s, in \1c%s \1n\1" + "0\1c%s\r\n\1h\1m%s voted in your poll: \1n\1" + "0\1h%s\r\n" 787 PollVoteNotice
 					var userVotedInYourPollText = bbs.text(typeof(PollVoteNotice) != "undefined" ? PollVoteNotice : 787);
 
 					// Pass true to get_all_msg_headers() to tell it to return vote messages
@@ -1253,51 +1266,122 @@ function ViewVoteResults(pSubBoardCode)
 		}
 		delete msgHdrs; // Free some memory
 
+		// If there are no polls, then just return
+		if (pollMsgHdrs.length == 0)
+		{
+			msgbase.close();
+			console.gotoxy(1, gMessageRow);
+			console.print("\1n\1y\1hThere are no topics to view.\1n");
+			mswait(ERROR_PAUSE_WAIT_MS);
+			console.gotoxy(1, gMessageRow);
+			printf("%28s", ""); // Erase the error message
+			return nextProgramState;
+		}
+
+		// Create the key help line to be displayed at the bottom of the screen
+		var keyHelpLine = "\1" + "7" + CenterText("\1rLeft\1b, \1rRight\1b, \1rUp\1b, \1rDn\1b, \1rPgUp\1b/\1rDn\1b, \1rF\1m)\1birst, \1rL\1m)\1bast, \1r#\1b, \1rQ\1m)\1buit", console.screen_columns-1);
+
 		// Get the unmodified default header lines to be displayed
 		var displayMsgHdrUnmodified = GetDefaultMsgDisplayHdr();
 
-		// User input loop
+		// Calculate the height of the frame to use
+		var frameHeight = console.screen_rows - displayMsgHdrUnmodified.length - 1;
+		// Create the frame object to use for displaying the message
+		// TODO: The scrollbar scroll amount seems to change as the message lengths change
+		var displayFrame = new Frame(1, // x: Horizontal coordinate of top left
+		                             displayMsgHdrUnmodified.length + 1, // y: Vertical coordinate of top left
+		                             console.screen_columns, // Width
+		                             frameHeight, // Height
+		                             BG_BLACK);
+		displayFrame.v_scroll = true;
+		displayFrame.h_scroll = false;
+		displayFrame.scrollbars = true;
+		var displayFrameScrollbar = new ScrollBar(displayFrame, {bg: BG_BLACK, fg: LIGHTGRAY, orientation: "vertical", autohide: false});
+
+		// Prepare the screen and display the key help line on the last row of the screen
 		console.clear("\1n");
+		console.gotoxy(1, console.screen_rows);
+		console.print(keyHelpLine);
+
+		// User input loop
 		var currentMsgIdx = 0;
+		var drawMsg = true;
 		var continueOn = true;
 		while (continueOn)
 		{
+			// Do garbage collection to ensure low memory usage
+			js.gc(true);
+
 			// Get the message header lines to be displayed
 			var dateTimeStr = pollMsgHdrs[currentMsgIdx]["date"].replace(/ [-+][0-9]+$/, "");
 			var displayMsgHdr = GetDisplayMsgHdrForMsg(pollMsgHdrs[currentMsgIdx], displayMsgHdrUnmodified, pSubBoardCode, pollMsgHdrs.length, currentMsgIdx+1, dateTimeStr, false, false);
 			// Display the message header on the screen
-			var curPos = { x: 1, y: 1 };
-			for (var i = 0; i < displayMsgHdr.length; ++i)
+			if (drawMsg)
 			{
-				console.gotoxy(curPos.x, curPos.y++);
-				console.print(displayMsgHdr[i]);
+				var curPos = { x: 1, y: 1 };
+				for (var i = 0; i < displayMsgHdr.length; ++i)
+				{
+					console.gotoxy(curPos.x, curPos.y++);
+					console.print(displayMsgHdr[i]);
+				}
+				console.print("\1n");
 			}
-			console.print("\1n");
 			var msgBodyText = GetMsgBody(msgbase, pollMsgHdrs[currentMsgIdx], pSubBoardCode, user);
 			if (msgBodyText == null)
 				msgBodyText = "Unable to load poll";
-			// TODO: Finish this
-			console.print(msgBodyText); // Temporary
-			console.pause(); // Temporary
-			continueOn = false; // Temporary
-			/*
-			// Create a frame object to display
-			var displayFrame = new Frame(1, // x: Horizontal coordinate of top left
-			                             displayMsgHdr.length + 1, // y: Vertical coordinate of top left
-			                             console.screen_columns, // Width
-			                             console.screen_rows - displayMsgHdr.length - 1, // Height
-			                             BG_BLACK);
-			displayFrame.v_scroll = true;
-			displayFrame.h_scroll = false;
-			displayFrame.scrollbars = true;
-			var displayFrameScrollbar = new ScrollBar(displayFrame, {bg: BG_BLACK, fg: LIGHTGRAY, orientation: "vertical", autohide: false});
+
 			// Load the poll text into the Frame object and draw the frame
-			displayFrame.putmsg(msgBodyText);
-			displayFrame.draw();
-			// TODO: Finish this
-			console.pause(); // Temporary
-			continueOn = false; // Temporary
-			*/
+			displayFrame.clear("\1n");
+			displayFrame.putmsg(msgBodyText, "\1n");
+			displayFrame.scrollTo(0, 0);
+			if (drawMsg)
+				displayFrame.draw();
+			var scrollRetObj = ScrollFrame(displayFrame, displayFrameScrollbar, 0, "\1n", false, 1, console.screen_rows);
+			drawMsg = true;
+			if (scrollRetObj.lastKeypress == KEY_LEFT)
+			{
+				// Go back one poll topic
+				if (currentMsgIdx > 0)
+					--currentMsgIdx;
+				else
+					drawMsg = false;
+			}
+			else if (scrollRetObj.lastKeypress == KEY_RIGHT)
+			{
+				// Go to the next poll topic, if there is one
+				if (currentMsgIdx < pollMsgHdrs.length-1)
+					++currentMsgIdx;
+				else
+					drawMsg = false;
+			}
+			else if (scrollRetObj.lastKeypress == gReaderKeys.goToFirst)
+			{
+				// Go to the first poll
+				if (currentMsgIdx > 0)
+					currentMsgIdx = 0;
+				else
+					drawMsg = false;
+			}
+			else if (scrollRetObj.lastKeypress == gReaderKeys.goToLast)
+			{
+				// Go to the last poll
+				if (currentMsgIdx < pollMsgHdrs.length-1)
+					currentMsgIdx = pollMsgHdrs.length-1;
+				else
+					drawMsg = false;
+			}
+			else if (scrollRetObj.lastKeypress == gReaderKeys.vote)
+			{
+				// TODO: Vote on the topic
+			}
+			else if (/[0-9]/.test(scrollRetObj.lastKeypress))
+			{
+				// The user started typing a number - Continue inputting the
+				// poll number and go to that poll
+				// TODO
+			}
+			else if (scrollRetObj.lastKeypress == gReaderKeys.quit)
+				continueOn = false;
 		}
 
 		msgbase.close();
@@ -1868,4 +1952,202 @@ function MsgWrittenTimeToLocalBBSTime(pMsgHdr)
 	var timeZoneDiffSeconds = timeZoneDiffMinutes * 60;
 	var msgWrittenTimeAdjusted = pMsgHdr.when_written_time + timeZoneDiffSeconds;
 	return msgWrittenTimeAdjusted;
+}
+
+// Inputs a keypress from the user and handles some ESC-based
+// characters such as PageUp, PageDown, and ESC.  If PageUp
+// or PageDown are pressed, this function will return the
+// string "\1PgUp" (KEY_PAGE_UP) or "\1Pgdn" (KEY_PAGE_DOWN),
+// respectively.  Also, F1-F5 will be returned as "\1F1"
+// through "\1F5", respectively.
+// Thanks goes to Psi-Jack for the original impementation
+// of this function.
+//
+// Parameters:
+//  pGetKeyMode: Optional - The mode bits for console.getkey().
+//               If not specified, K_NONE will be used.
+//
+// Return value: The user's keypress
+function GetKeyWithESCChars(pGetKeyMode)
+{
+	var getKeyMode = K_NONE;
+	if (typeof(pGetKeyMode) == "number")
+		getKeyMode = pGetKeyMode;
+
+	var userInput = console.getkey(getKeyMode);
+	if (userInput == KEY_ESC) {
+		switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
+			case '[':
+				switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
+				case 'V':
+					userInput = KEY_PAGE_UP;
+					break;
+				case 'U':
+					userInput = KEY_PAGE_DOWN;
+					break;
+				}
+				break;
+			case 'O':
+				switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
+				case 'P':
+					userInput = "\1F1";
+					break;
+				case 'Q':
+					userInput = "\1F2";
+					break;
+				case 'R':
+					userInput = "\1F3";
+					break;
+				case 'S':
+					userInput = "\1F4";
+					break;
+				case 't':
+					userInput = "\1F5";
+					break;
+				}
+			default:
+				break;
+		}
+	}
+
+	return userInput;
+}
+
+// Displays a Frame on the screen and allows scrolling through it with the up &
+// down arrow keys, PageUp, PageDown, HOME, and END.
+//
+// Parameters:
+//  pFrame: A Frame object to display & scroll through
+//  pScrollbar: A ScrollBar object associated with the Frame object
+//  pTopLineIdx: The index of the text line to display at the top
+//  pTxtAttrib: The attribute(s) to apply to the text lines
+//  pWriteTxtLines: Boolean - Whether or not to write the text lines (in addition
+//                  to doing the message loop).  If false, this will only do the
+//                  the message loop.  This parameter is intended as a screen
+//                  refresh optimization.
+//  pPostWriteCurX: The X location for the cursor after writing the message
+//                  lines
+//  pPostWriteCurY: The Y location for the cursor after writing the message
+//                  lines
+//
+// Return value: An object with the following properties:
+//               lastKeypress: The last key pressed by the user (a string)
+//               topLineIdx: The new top line index of the text lines, in case of scrolling
+function ScrollFrame(pFrame, pScrollbar, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pPostWriteCurX,
+                     pPostWriteCurY)
+{
+	// Variables for the top line index for the last page, scrolling, etc.
+	var topLineIdxForLastPage = pFrame.data_height - pFrame.height;
+	if (topLineIdxForLastPage < 0)
+		topLineIdxForLastPage = 0;
+
+	var retObj = new Object();
+	retObj.lastKeypress = "";
+	retObj.topLineIdx = pTopLineIdx;
+
+	if (pTopLineIdx > 0)
+		pFrame.scrollTo(0, pTopLineIdx);
+
+	var writeTxtLines = pWriteTxtLines;
+	if (writeTxtLines)
+	{
+		pFrame.invalidate(); // Force drawing on the next call to draw() or cycle()
+		pFrame.cycle();
+		//pFrame.draw();
+	}
+
+	// Note: It seems that in order for the frame's scrollbar to be accurate,
+	// the frame & scrollbar must be cycled at least once initially.
+	var cycleFrame = true;
+	var continueOn = true;
+	while (continueOn)
+	{
+		// If we are to write the text lines, then draw the frame.
+		// TODO: Do we really need this?  Will this be different from
+		// scrollTextLines()?
+		//if (writeTxtLines)
+		//	pFrame.draw();
+
+		if (cycleFrame)
+		{
+			// Invalidate the frame to force it to redraw everything, as a
+			// workaround to clear the background before writing again
+			// TODO: I might want to remove this invalidate() later when
+			// Frame is fixed to redraw better on scrolling.
+			pFrame.invalidate();
+			// Cycle the scrollbar & frame to get them to scroll
+			if (pScrollbar != null)
+				pScrollbar.cycle();
+			pFrame.cycle();
+		}
+
+		writeTxtLines = false;
+		cycleFrame = false;
+
+		// Get a keypress from the user and take action based on it
+		console.gotoxy(pPostWriteCurX, pPostWriteCurY);
+		retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN);
+		switch (retObj.lastKeypress)
+		{
+			case KEY_UP:
+				if (retObj.topLineIdx > 0)
+				{
+					pFrame.scroll(0, -1);
+					--retObj.topLineIdx;
+					cycleFrame = true;
+					writeTxtLines = true;
+				}
+				break;
+			case KEY_DOWN:
+				if (retObj.topLineIdx < topLineIdxForLastPage)
+				{
+					pFrame.scroll(0, 1);
+					cycleFrame = true;
+					++retObj.topLineIdx;
+					writeTxtLines = true;
+				}
+				break;
+			case KEY_PAGE_DOWN: // Next page
+				if (retObj.topLineIdx < topLineIdxForLastPage)
+				{
+					//pFrame.scroll(0, pFrame.height);
+					retObj.topLineIdx += pFrame.height;
+					if (retObj.topLineIdx > topLineIdxForLastPage)
+						retObj.topLineIdx = topLineIdxForLastPage;
+					pFrame.scrollTo(0, retObj.topLineIdx);
+					cycleFrame = true;
+					writeTxtLines = true;
+				}
+				break;
+			case KEY_PAGE_UP: // Previous page
+				if (retObj.topLineIdx > 0)
+				{
+					//pFrame.scroll(0, -(pFrame.height));
+					retObj.topLineIdx -= pFrame.height;
+					if (retObj.topLineIdx < 0)
+						retObj.topLineIdx = 0;
+					pFrame.scrollTo(0, retObj.topLineIdx);
+					cycleFrame = true;
+					writeTxtLines = true;
+				}
+				break;
+			case KEY_HOME: // First page
+				//pFrame.home();
+				pFrame.scrollTo(0, 0);
+				cycleFrame = true;
+				retObj.topLineIdx = 0;
+				break;
+			case KEY_END: // Last page
+				//pFrame.end();
+				pFrame.scrollTo(0, topLineIdxForLastPage);
+				cycleFrame = true;
+				retObj.topLineIdx = topLineIdxForLastPage;
+				break;
+			default:
+				continueOn = false;
+				break;
+		}
+	}
+
+	return retObj;
 }
\ No newline at end of file